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