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) 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}