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}