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) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { 110 const ( 111 Method = "POST" 112 ) 113 endpoint := fmt.Sprintf("/repo/fork/sync/%s", branch) 114 115 body, _ := json.Marshal(map[string]any{ 116 "did": ownerDid, 117 "source": source, 118 "name": name, 119 }) 120 121 req, err := s.newRequest(Method, endpoint, body) 122 if err != nil { 123 return nil, err 124 } 125 126 return s.client.Do(req) 127} 128 129func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 130 const ( 131 Method = "POST" 132 Endpoint = "/repo/fork" 133 ) 134 135 body, _ := json.Marshal(map[string]any{ 136 "did": ownerDid, 137 "source": source, 138 "name": name, 139 }) 140 141 req, err := s.newRequest(Method, Endpoint, body) 142 if err != nil { 143 return nil, err 144 } 145 146 return s.client.Do(req) 147} 148 149func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 150 const ( 151 Method = "DELETE" 152 Endpoint = "/repo" 153 ) 154 155 body, _ := json.Marshal(map[string]any{ 156 "did": did, 157 "name": repoName, 158 }) 159 160 req, err := s.newRequest(Method, Endpoint, body) 161 if err != nil { 162 return nil, err 163 } 164 165 return s.client.Do(req) 166} 167 168func (s *SignedClient) AddMember(did string) (*http.Response, error) { 169 const ( 170 Method = "PUT" 171 Endpoint = "/member/add" 172 ) 173 174 body, _ := json.Marshal(map[string]any{ 175 "did": did, 176 }) 177 178 req, err := s.newRequest(Method, Endpoint, body) 179 if err != nil { 180 return nil, err 181 } 182 183 return s.client.Do(req) 184} 185 186func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 187 const ( 188 Method = "PUT" 189 ) 190 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 191 192 body, _ := json.Marshal(map[string]any{ 193 "branch": branch, 194 }) 195 196 req, err := s.newRequest(Method, endpoint, body) 197 if err != nil { 198 return nil, err 199 } 200 201 return s.client.Do(req) 202} 203 204func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 205 const ( 206 Method = "POST" 207 ) 208 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 209 210 body, _ := json.Marshal(map[string]any{ 211 "did": memberDid, 212 }) 213 214 req, err := s.newRequest(Method, endpoint, body) 215 if err != nil { 216 return nil, err 217 } 218 219 return s.client.Do(req) 220} 221 222func (s *SignedClient) Merge( 223 patch []byte, 224 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 225) (*http.Response, error) { 226 const ( 227 Method = "POST" 228 ) 229 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 230 231 mr := types.MergeRequest{ 232 Branch: branch, 233 CommitMessage: commitMessage, 234 CommitBody: commitBody, 235 AuthorName: authorName, 236 AuthorEmail: authorEmail, 237 Patch: string(patch), 238 } 239 240 body, _ := json.Marshal(mr) 241 242 req, err := s.newRequest(Method, endpoint, body) 243 if err != nil { 244 return nil, err 245 } 246 247 return s.client.Do(req) 248} 249 250func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 251 const ( 252 Method = "POST" 253 ) 254 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 255 256 body, _ := json.Marshal(map[string]any{ 257 "patch": string(patch), 258 "branch": branch, 259 }) 260 261 req, err := s.newRequest(Method, endpoint, body) 262 if err != nil { 263 return nil, err 264 } 265 266 return s.client.Do(req) 267} 268 269func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 270 const ( 271 Method = "POST" 272 ) 273 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch)) 274 275 req, err := s.newRequest(Method, endpoint, nil) 276 if err != nil { 277 return nil, err 278 } 279 280 return s.client.Do(req) 281} 282 283type UnsignedClient struct { 284 Url *url.URL 285 client *http.Client 286} 287 288func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) { 289 client := &http.Client{ 290 Timeout: 5 * time.Second, 291 } 292 293 scheme := "https" 294 if dev { 295 scheme = "http" 296 } 297 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 298 if err != nil { 299 return nil, err 300 } 301 302 unsignedClient := &UnsignedClient{ 303 client: client, 304 Url: url, 305 } 306 307 return unsignedClient, nil 308} 309 310func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) { 311 reqUrl := us.Url.JoinPath(endpoint) 312 313 // add query parameters 314 if query != nil { 315 reqUrl.RawQuery = query.Encode() 316 } 317 318 return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body)) 319} 320 321func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) { 322 const ( 323 Method = "GET" 324 ) 325 326 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref) 327 if ref == "" { 328 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName) 329 } 330 331 req, err := us.newRequest(Method, endpoint, nil, nil) 332 if err != nil { 333 return nil, err 334 } 335 336 return us.client.Do(req) 337} 338 339func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) { 340 const ( 341 Method = "GET" 342 ) 343 344 endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref)) 345 346 query := url.Values{} 347 query.Add("page", strconv.Itoa(page)) 348 query.Add("per_page", strconv.Itoa(60)) 349 350 req, err := us.newRequest(Method, endpoint, query, nil) 351 if err != nil { 352 return nil, err 353 } 354 355 return us.client.Do(req) 356} 357 358func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) { 359 const ( 360 Method = "GET" 361 ) 362 363 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName) 364 365 req, err := us.newRequest(Method, endpoint, nil, nil) 366 if err != nil { 367 return nil, err 368 } 369 370 return us.client.Do(req) 371} 372 373func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) { 374 const ( 375 Method = "GET" 376 ) 377 378 endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName) 379 380 req, err := us.newRequest(Method, endpoint, nil, nil) 381 if err != nil { 382 return nil, err 383 } 384 385 resp, err := us.client.Do(req) 386 if err != nil { 387 return nil, err 388 } 389 390 body, err := io.ReadAll(resp.Body) 391 if err != nil { 392 return nil, err 393 } 394 395 var result types.RepoTagsResponse 396 err = json.Unmarshal(body, &result) 397 if err != nil { 398 return nil, err 399 } 400 401 return &result, nil 402} 403 404func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 405 const ( 406 Method = "GET" 407 ) 408 409 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch)) 410 411 req, err := us.newRequest(Method, endpoint, nil, nil) 412 if err != nil { 413 return nil, err 414 } 415 416 return us.client.Do(req) 417} 418 419func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) { 420 const ( 421 Method = "GET" 422 ) 423 424 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 425 426 req, err := us.newRequest(Method, endpoint, nil, nil) 427 if err != nil { 428 return nil, err 429 } 430 431 resp, err := us.client.Do(req) 432 if err != nil { 433 return nil, err 434 } 435 defer resp.Body.Close() 436 437 var defaultBranch types.RepoDefaultBranchResponse 438 if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil { 439 return nil, err 440 } 441 442 return &defaultBranch, nil 443} 444 445func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) { 446 const ( 447 Method = "GET" 448 Endpoint = "/capabilities" 449 ) 450 451 req, err := us.newRequest(Method, Endpoint, nil, nil) 452 if err != nil { 453 return nil, err 454 } 455 456 resp, err := us.client.Do(req) 457 if err != nil { 458 return nil, err 459 } 460 defer resp.Body.Close() 461 462 var capabilities types.Capabilities 463 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { 464 return nil, err 465 } 466 467 return &capabilities, nil 468} 469 470func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) { 471 const ( 472 Method = "GET" 473 ) 474 475 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 476 477 req, err := us.newRequest(Method, endpoint, nil, nil) 478 if err != nil { 479 return nil, fmt.Errorf("Failed to create request.") 480 } 481 482 compareResp, err := us.client.Do(req) 483 if err != nil { 484 return nil, fmt.Errorf("Failed to create request.") 485 } 486 defer compareResp.Body.Close() 487 488 switch compareResp.StatusCode { 489 case 404: 490 case 400: 491 return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 492 } 493 494 respBody, err := io.ReadAll(compareResp.Body) 495 if err != nil { 496 log.Println("failed to compare across branches") 497 return nil, fmt.Errorf("Failed to compare branches.") 498 } 499 defer compareResp.Body.Close() 500 501 var formatPatchResponse types.RepoFormatPatchResponse 502 err = json.Unmarshal(respBody, &formatPatchResponse) 503 if err != nil { 504 log.Println("failed to unmarshal format-patch response", err) 505 return nil, fmt.Errorf("failed to compare branches.") 506 } 507 508 return &formatPatchResponse, nil 509}