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