forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package knotclient 2 3import ( 4 "bytes" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "io" 11 "log" 12 "net/http" 13 "net/url" 14 "strconv" 15 "time" 16 17 "tangled.sh/tangled.sh/core/types" 18) 19 20type SignerTransport struct { 21 Secret string 22} 23 24func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { 25 timestamp := time.Now().Format(time.RFC3339) 26 mac := hmac.New(sha256.New, []byte(s.Secret)) 27 message := req.Method + req.URL.Path + timestamp 28 mac.Write([]byte(message)) 29 signature := hex.EncodeToString(mac.Sum(nil)) 30 req.Header.Set("X-Signature", signature) 31 req.Header.Set("X-Timestamp", timestamp) 32 return http.DefaultTransport.RoundTrip(req) 33} 34 35type SignedClient struct { 36 Secret string 37 Url *url.URL 38 client *http.Client 39} 40 41func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) { 42 client := &http.Client{ 43 Timeout: 5 * time.Second, 44 Transport: SignerTransport{ 45 Secret: secret, 46 }, 47 } 48 49 scheme := "https" 50 if dev { 51 scheme = "http" 52 } 53 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 54 if err != nil { 55 return nil, err 56 } 57 58 signedClient := &SignedClient{ 59 Secret: secret, 60 client: client, 61 Url: url, 62 } 63 64 return signedClient, nil 65} 66 67func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 68 return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 69} 70 71func (s *SignedClient) Init(did string) (*http.Response, error) { 72 const ( 73 Method = "POST" 74 Endpoint = "/init" 75 ) 76 77 body, _ := json.Marshal(map[string]any{ 78 "did": did, 79 }) 80 81 req, err := s.newRequest(Method, Endpoint, body) 82 if err != nil { 83 return nil, err 84 } 85 86 return s.client.Do(req) 87} 88 89func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) { 90 const ( 91 Method = "PUT" 92 Endpoint = "/repo/new" 93 ) 94 95 body, _ := json.Marshal(map[string]any{ 96 "did": did, 97 "name": repoName, 98 "default_branch": defaultBranch, 99 }) 100 101 req, err := s.newRequest(Method, Endpoint, body) 102 if err != nil { 103 return nil, err 104 } 105 106 return s.client.Do(req) 107} 108 109func (s *SignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) { 110 const ( 111 Method = "GET" 112 ) 113 endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref)) 114 115 req, err := s.newRequest(Method, endpoint, nil) 116 if err != nil { 117 return nil, err 118 } 119 120 resp, err := s.client.Do(req) 121 if err != nil { 122 return nil, err 123 } 124 125 var result types.RepoLanguageResponse 126 if resp.StatusCode != http.StatusOK { 127 log.Println("failed to calculate languages", resp.Status) 128 return &types.RepoLanguageResponse{}, nil 129 } 130 131 body, err := io.ReadAll(resp.Body) 132 if err != nil { 133 return nil, err 134 } 135 136 err = json.Unmarshal(body, &result) 137 if err != nil { 138 return nil, err 139 } 140 141 return &result, nil 142} 143 144func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) { 145 const ( 146 Method = "GET" 147 ) 148 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 149 150 body, _ := json.Marshal(map[string]any{ 151 "did": ownerDid, 152 "source": source, 153 "name": name, 154 "hiddenref": hiddenRef, 155 }) 156 157 req, err := s.newRequest(Method, endpoint, body) 158 if err != nil { 159 return nil, err 160 } 161 162 return s.client.Do(req) 163} 164 165func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { 166 const ( 167 Method = "POST" 168 ) 169 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 170 171 body, _ := json.Marshal(map[string]any{ 172 "did": ownerDid, 173 "source": source, 174 "name": name, 175 }) 176 177 req, err := s.newRequest(Method, endpoint, body) 178 if err != nil { 179 return nil, err 180 } 181 182 return s.client.Do(req) 183} 184 185func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 186 const ( 187 Method = "POST" 188 Endpoint = "/repo/fork" 189 ) 190 191 body, _ := json.Marshal(map[string]any{ 192 "did": ownerDid, 193 "source": source, 194 "name": name, 195 }) 196 197 req, err := s.newRequest(Method, Endpoint, body) 198 if err != nil { 199 return nil, err 200 } 201 202 return s.client.Do(req) 203} 204 205func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 206 const ( 207 Method = "DELETE" 208 Endpoint = "/repo" 209 ) 210 211 body, _ := json.Marshal(map[string]any{ 212 "did": did, 213 "name": repoName, 214 }) 215 216 req, err := s.newRequest(Method, Endpoint, body) 217 if err != nil { 218 return nil, err 219 } 220 221 return s.client.Do(req) 222} 223 224func (s *SignedClient) AddMember(did string) (*http.Response, error) { 225 const ( 226 Method = "PUT" 227 Endpoint = "/member/add" 228 ) 229 230 body, _ := json.Marshal(map[string]any{ 231 "did": did, 232 }) 233 234 req, err := s.newRequest(Method, Endpoint, body) 235 if err != nil { 236 return nil, err 237 } 238 239 return s.client.Do(req) 240} 241 242func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 243 const ( 244 Method = "PUT" 245 ) 246 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 247 248 body, _ := json.Marshal(map[string]any{ 249 "branch": branch, 250 }) 251 252 req, err := s.newRequest(Method, endpoint, body) 253 if err != nil { 254 return nil, err 255 } 256 257 return s.client.Do(req) 258} 259 260func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 261 const ( 262 Method = "POST" 263 ) 264 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 265 266 body, _ := json.Marshal(map[string]any{ 267 "did": memberDid, 268 }) 269 270 req, err := s.newRequest(Method, endpoint, body) 271 if err != nil { 272 return nil, err 273 } 274 275 return s.client.Do(req) 276} 277 278func (s *SignedClient) Merge( 279 patch []byte, 280 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 281) (*http.Response, error) { 282 const ( 283 Method = "POST" 284 ) 285 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 286 287 mr := types.MergeRequest{ 288 Branch: branch, 289 CommitMessage: commitMessage, 290 CommitBody: commitBody, 291 AuthorName: authorName, 292 AuthorEmail: authorEmail, 293 Patch: string(patch), 294 } 295 296 body, _ := json.Marshal(mr) 297 298 req, err := s.newRequest(Method, endpoint, body) 299 if err != nil { 300 return nil, err 301 } 302 303 return s.client.Do(req) 304} 305 306func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 307 const ( 308 Method = "POST" 309 ) 310 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 311 312 body, _ := json.Marshal(map[string]any{ 313 "patch": string(patch), 314 "branch": branch, 315 }) 316 317 req, err := s.newRequest(Method, endpoint, body) 318 if err != nil { 319 return nil, err 320 } 321 322 return s.client.Do(req) 323} 324 325func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 326 const ( 327 Method = "POST" 328 ) 329 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch)) 330 331 req, err := s.newRequest(Method, endpoint, nil) 332 if err != nil { 333 return nil, err 334 } 335 336 return s.client.Do(req) 337} 338 339type UnsignedClient struct { 340 Url *url.URL 341 client *http.Client 342} 343 344func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) { 345 client := &http.Client{ 346 Timeout: 5 * time.Second, 347 } 348 349 scheme := "https" 350 if dev { 351 scheme = "http" 352 } 353 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 354 if err != nil { 355 return nil, err 356 } 357 358 unsignedClient := &UnsignedClient{ 359 client: client, 360 Url: url, 361 } 362 363 return unsignedClient, nil 364} 365 366func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) { 367 reqUrl := us.Url.JoinPath(endpoint) 368 369 // add query parameters 370 if query != nil { 371 reqUrl.RawQuery = query.Encode() 372 } 373 374 return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body)) 375} 376 377func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) { 378 const ( 379 Method = "GET" 380 ) 381 382 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref) 383 if ref == "" { 384 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName) 385 } 386 387 req, err := us.newRequest(Method, endpoint, nil, nil) 388 if err != nil { 389 return nil, err 390 } 391 392 return us.client.Do(req) 393} 394 395func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) { 396 const ( 397 Method = "GET" 398 ) 399 400 endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref)) 401 402 query := url.Values{} 403 query.Add("page", strconv.Itoa(page)) 404 query.Add("per_page", strconv.Itoa(60)) 405 406 req, err := us.newRequest(Method, endpoint, query, nil) 407 if err != nil { 408 return nil, err 409 } 410 411 return us.client.Do(req) 412} 413 414func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) { 415 const ( 416 Method = "GET" 417 ) 418 419 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName) 420 421 req, err := us.newRequest(Method, endpoint, nil, nil) 422 if err != nil { 423 return nil, err 424 } 425 426 return us.client.Do(req) 427} 428 429func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) { 430 const ( 431 Method = "GET" 432 ) 433 434 endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName) 435 436 req, err := us.newRequest(Method, endpoint, nil, nil) 437 if err != nil { 438 return nil, err 439 } 440 441 resp, err := us.client.Do(req) 442 if err != nil { 443 return nil, err 444 } 445 446 body, err := io.ReadAll(resp.Body) 447 if err != nil { 448 return nil, err 449 } 450 451 var result types.RepoTagsResponse 452 err = json.Unmarshal(body, &result) 453 if err != nil { 454 return nil, err 455 } 456 457 return &result, nil 458} 459 460func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 461 const ( 462 Method = "GET" 463 ) 464 465 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch)) 466 467 req, err := us.newRequest(Method, endpoint, nil, nil) 468 if err != nil { 469 return nil, err 470 } 471 472 return us.client.Do(req) 473} 474 475func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) { 476 const ( 477 Method = "GET" 478 ) 479 480 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 481 482 req, err := us.newRequest(Method, endpoint, nil, nil) 483 if err != nil { 484 return nil, err 485 } 486 487 resp, err := us.client.Do(req) 488 if err != nil { 489 return nil, err 490 } 491 defer resp.Body.Close() 492 493 var defaultBranch types.RepoDefaultBranchResponse 494 if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil { 495 return nil, err 496 } 497 498 return &defaultBranch, nil 499} 500 501func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) { 502 const ( 503 Method = "GET" 504 Endpoint = "/capabilities" 505 ) 506 507 req, err := us.newRequest(Method, Endpoint, nil, nil) 508 if err != nil { 509 return nil, err 510 } 511 512 resp, err := us.client.Do(req) 513 if err != nil { 514 return nil, err 515 } 516 defer resp.Body.Close() 517 518 var capabilities types.Capabilities 519 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { 520 return nil, err 521 } 522 523 return &capabilities, nil 524} 525 526func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) { 527 const ( 528 Method = "GET" 529 ) 530 531 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 532 533 req, err := us.newRequest(Method, endpoint, nil, nil) 534 if err != nil { 535 return nil, fmt.Errorf("Failed to create request.") 536 } 537 538 compareResp, err := us.client.Do(req) 539 if err != nil { 540 return nil, fmt.Errorf("Failed to create request.") 541 } 542 defer compareResp.Body.Close() 543 544 switch compareResp.StatusCode { 545 case 404: 546 case 400: 547 return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 548 } 549 550 respBody, err := io.ReadAll(compareResp.Body) 551 if err != nil { 552 log.Println("failed to compare across branches") 553 return nil, fmt.Errorf("Failed to compare branches.") 554 } 555 defer compareResp.Body.Close() 556 557 var formatPatchResponse types.RepoFormatPatchResponse 558 err = json.Unmarshal(respBody, &formatPatchResponse) 559 if err != nil { 560 log.Println("failed to unmarshal format-patch response", err) 561 return nil, fmt.Errorf("failed to compare branches.") 562 } 563 564 return &formatPatchResponse, nil 565}