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 "time"
15
16 "tangled.sh/tangled.sh/core/types"
17)
18
19type SignerTransport struct {
20 Secret string
21}
22
23func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
24 timestamp := time.Now().Format(time.RFC3339)
25 mac := hmac.New(sha256.New, []byte(s.Secret))
26 message := req.Method + req.URL.Path + timestamp
27 mac.Write([]byte(message))
28 signature := hex.EncodeToString(mac.Sum(nil))
29 req.Header.Set("X-Signature", signature)
30 req.Header.Set("X-Timestamp", timestamp)
31 return http.DefaultTransport.RoundTrip(req)
32}
33
34type SignedClient struct {
35 Secret string
36 Url *url.URL
37 client *http.Client
38}
39
40func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
41 client := &http.Client{
42 Timeout: 5 * time.Second,
43 Transport: SignerTransport{
44 Secret: secret,
45 },
46 }
47
48 scheme := "https"
49 if dev {
50 scheme = "http"
51 }
52 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
53 if err != nil {
54 return nil, err
55 }
56
57 signedClient := &SignedClient{
58 Secret: secret,
59 client: client,
60 Url: url,
61 }
62
63 return signedClient, nil
64}
65
66func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
67 return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
68}
69
70func (s *SignedClient) Init(did string) (*http.Response, error) {
71 const (
72 Method = "POST"
73 Endpoint = "/init"
74 )
75
76 body, _ := json.Marshal(map[string]any{
77 "did": did,
78 })
79
80 req, err := s.newRequest(Method, Endpoint, body)
81 if err != nil {
82 return nil, err
83 }
84
85 return s.client.Do(req)
86}
87
88func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
89 const (
90 Method = "PUT"
91 Endpoint = "/repo/new"
92 )
93
94 body, _ := json.Marshal(map[string]any{
95 "did": did,
96 "name": repoName,
97 "default_branch": defaultBranch,
98 })
99
100 req, err := s.newRequest(Method, Endpoint, body)
101 if err != nil {
102 return nil, err
103 }
104
105 return s.client.Do(req)
106}
107
108func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
109 const (
110 Method = "POST"
111 Endpoint = "/repo/fork"
112 )
113
114 body, _ := json.Marshal(map[string]any{
115 "did": ownerDid,
116 "source": source,
117 "name": name,
118 })
119
120 req, err := s.newRequest(Method, Endpoint, body)
121 if err != nil {
122 return nil, err
123 }
124
125 return s.client.Do(req)
126}
127
128func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
129 const (
130 Method = "DELETE"
131 Endpoint = "/repo"
132 )
133
134 body, _ := json.Marshal(map[string]any{
135 "did": did,
136 "name": repoName,
137 })
138
139 req, err := s.newRequest(Method, Endpoint, body)
140 if err != nil {
141 return nil, err
142 }
143
144 return s.client.Do(req)
145}
146
147func (s *SignedClient) AddMember(did string) (*http.Response, error) {
148 const (
149 Method = "PUT"
150 Endpoint = "/member/add"
151 )
152
153 body, _ := json.Marshal(map[string]any{
154 "did": did,
155 })
156
157 req, err := s.newRequest(Method, Endpoint, body)
158 if err != nil {
159 return nil, err
160 }
161
162 return s.client.Do(req)
163}
164
165func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
166 const (
167 Method = "PUT"
168 )
169 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
170
171 body, _ := json.Marshal(map[string]any{
172 "branch": branch,
173 })
174
175 req, err := s.newRequest(Method, endpoint, body)
176 if err != nil {
177 return nil, err
178 }
179
180 return s.client.Do(req)
181}
182
183func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
184 const (
185 Method = "POST"
186 )
187 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
188
189 body, _ := json.Marshal(map[string]any{
190 "did": memberDid,
191 })
192
193 req, err := s.newRequest(Method, endpoint, body)
194 if err != nil {
195 return nil, err
196 }
197
198 return s.client.Do(req)
199}
200
201func (s *SignedClient) Merge(
202 patch []byte,
203 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
204) (*http.Response, error) {
205 const (
206 Method = "POST"
207 )
208 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
209
210 mr := types.MergeRequest{
211 Branch: branch,
212 CommitMessage: commitMessage,
213 CommitBody: commitBody,
214 AuthorName: authorName,
215 AuthorEmail: authorEmail,
216 Patch: string(patch),
217 }
218
219 body, _ := json.Marshal(mr)
220
221 req, err := s.newRequest(Method, endpoint, body)
222 if err != nil {
223 return nil, err
224 }
225
226 return s.client.Do(req)
227}
228
229func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
230 const (
231 Method = "POST"
232 )
233 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
234
235 body, _ := json.Marshal(map[string]any{
236 "patch": string(patch),
237 "branch": branch,
238 })
239
240 req, err := s.newRequest(Method, endpoint, body)
241 if err != nil {
242 return nil, err
243 }
244
245 return s.client.Do(req)
246}
247
248func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
249 const (
250 Method = "POST"
251 )
252 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, forkBranch, remoteBranch)
253
254 req, err := s.newRequest(Method, endpoint, nil)
255 if err != nil {
256 return nil, err
257 }
258
259 return s.client.Do(req)
260}
261
262type UnsignedClient struct {
263 Url *url.URL
264 client *http.Client
265}
266
267func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
268 client := &http.Client{
269 Timeout: 5 * time.Second,
270 }
271
272 scheme := "https"
273 if dev {
274 scheme = "http"
275 }
276 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
277 if err != nil {
278 return nil, err
279 }
280
281 unsignedClient := &UnsignedClient{
282 client: client,
283 Url: url,
284 }
285
286 return unsignedClient, nil
287}
288
289func (us *UnsignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
290 return http.NewRequest(method, us.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
291}
292
293func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
294 const (
295 Method = "GET"
296 )
297
298 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
299 if ref == "" {
300 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
301 }
302
303 req, err := us.newRequest(Method, endpoint, nil)
304 if err != nil {
305 return nil, err
306 }
307
308 return us.client.Do(req)
309}
310
311func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
312 const (
313 Method = "GET"
314 )
315
316 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
317
318 req, err := us.newRequest(Method, endpoint, nil)
319 if err != nil {
320 return nil, err
321 }
322
323 return us.client.Do(req)
324}
325
326func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
327 const (
328 Method = "GET"
329 )
330
331 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, branch)
332
333 req, err := us.newRequest(Method, endpoint, nil)
334 if err != nil {
335 return nil, err
336 }
337
338 return us.client.Do(req)
339}
340
341func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) {
342 const (
343 Method = "GET"
344 )
345
346 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
347
348 req, err := us.newRequest(Method, endpoint, nil)
349 if err != nil {
350 return nil, err
351 }
352
353 return us.client.Do(req)
354}
355
356func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
357 const (
358 Method = "GET"
359 Endpoint = "/capabilities"
360 )
361
362 req, err := us.newRequest(Method, Endpoint, nil)
363 if err != nil {
364 return nil, err
365 }
366
367 resp, err := us.client.Do(req)
368 if err != nil {
369 return nil, err
370 }
371 defer resp.Body.Close()
372
373 var capabilities types.Capabilities
374 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
375 return nil, err
376 }
377
378 return &capabilities, nil
379}
380
381func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
382 const (
383 Method = "GET"
384 )
385
386 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
387
388 req, err := us.newRequest(Method, endpoint, nil)
389 if err != nil {
390 return nil, fmt.Errorf("Failed to create request.")
391 }
392
393 compareResp, err := us.client.Do(req)
394 if err != nil {
395 return nil, fmt.Errorf("Failed to create request.")
396 }
397 defer compareResp.Body.Close()
398
399 switch compareResp.StatusCode {
400 case 404:
401 case 400:
402 return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
403 }
404
405 respBody, err := io.ReadAll(compareResp.Body)
406 if err != nil {
407 log.Println("failed to compare across branches")
408 return nil, fmt.Errorf("Failed to compare branches.")
409 }
410 defer compareResp.Body.Close()
411
412 var formatPatchResponse types.RepoFormatPatchResponse
413 err = json.Unmarshal(respBody, &formatPatchResponse)
414 if err != nil {
415 log.Println("failed to unmarshal format-patch response", err)
416 return nil, fmt.Errorf("failed to compare branches.")
417 }
418
419 return &formatPatchResponse, nil
420}