1package knotclient
2
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "net/http"
11 "net/url"
12 "time"
13
14 "tangled.sh/tangled.sh/core/types"
15)
16
17type SignerTransport struct {
18 Secret string
19}
20
21func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
22 timestamp := time.Now().Format(time.RFC3339)
23 mac := hmac.New(sha256.New, []byte(s.Secret))
24 message := req.Method + req.URL.Path + timestamp
25 mac.Write([]byte(message))
26 signature := hex.EncodeToString(mac.Sum(nil))
27 req.Header.Set("X-Signature", signature)
28 req.Header.Set("X-Timestamp", timestamp)
29 return http.DefaultTransport.RoundTrip(req)
30}
31
32type SignedClient struct {
33 Secret string
34 Url *url.URL
35 client *http.Client
36}
37
38func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
39 client := &http.Client{
40 Timeout: 5 * time.Second,
41 Transport: SignerTransport{
42 Secret: secret,
43 },
44 }
45
46 scheme := "https"
47 if dev {
48 scheme = "http"
49 }
50 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
51 if err != nil {
52 return nil, err
53 }
54
55 signedClient := &SignedClient{
56 Secret: secret,
57 client: client,
58 Url: url,
59 }
60
61 return signedClient, nil
62}
63
64func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
65 return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
66}
67
68func (s *SignedClient) Init(did string) (*http.Response, error) {
69 const (
70 Method = "POST"
71 Endpoint = "/init"
72 )
73
74 body, _ := json.Marshal(map[string]any{
75 "did": did,
76 })
77
78 req, err := s.newRequest(Method, Endpoint, body)
79 if err != nil {
80 return nil, err
81 }
82
83 return s.client.Do(req)
84}
85
86func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
87 const (
88 Method = "PUT"
89 Endpoint = "/repo/new"
90 )
91
92 body, _ := json.Marshal(map[string]any{
93 "did": did,
94 "name": repoName,
95 "default_branch": defaultBranch,
96 })
97
98 req, err := s.newRequest(Method, Endpoint, body)
99 if err != nil {
100 return nil, err
101 }
102
103 return s.client.Do(req)
104}
105
106func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
107 const (
108 Method = "POST"
109 Endpoint = "/repo/fork"
110 )
111
112 body, _ := json.Marshal(map[string]any{
113 "did": ownerDid,
114 "source": source,
115 "name": name,
116 })
117
118 req, err := s.newRequest(Method, Endpoint, body)
119 if err != nil {
120 return nil, err
121 }
122
123 return s.client.Do(req)
124}
125
126func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
127 const (
128 Method = "DELETE"
129 Endpoint = "/repo"
130 )
131
132 body, _ := json.Marshal(map[string]any{
133 "did": did,
134 "name": repoName,
135 })
136
137 req, err := s.newRequest(Method, Endpoint, body)
138 if err != nil {
139 return nil, err
140 }
141
142 return s.client.Do(req)
143}
144
145func (s *SignedClient) AddMember(did string) (*http.Response, error) {
146 const (
147 Method = "PUT"
148 Endpoint = "/member/add"
149 )
150
151 body, _ := json.Marshal(map[string]any{
152 "did": did,
153 })
154
155 req, err := s.newRequest(Method, Endpoint, body)
156 if err != nil {
157 return nil, err
158 }
159
160 return s.client.Do(req)
161}
162
163func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
164 const (
165 Method = "PUT"
166 )
167 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
168
169 body, _ := json.Marshal(map[string]any{
170 "branch": branch,
171 })
172
173 req, err := s.newRequest(Method, endpoint, body)
174 if err != nil {
175 return nil, err
176 }
177
178 return s.client.Do(req)
179}
180
181func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
182 const (
183 Method = "POST"
184 )
185 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
186
187 body, _ := json.Marshal(map[string]any{
188 "did": memberDid,
189 })
190
191 req, err := s.newRequest(Method, endpoint, body)
192 if err != nil {
193 return nil, err
194 }
195
196 return s.client.Do(req)
197}
198
199func (s *SignedClient) Merge(
200 patch []byte,
201 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
202) (*http.Response, error) {
203 const (
204 Method = "POST"
205 )
206 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
207
208 mr := types.MergeRequest{
209 Branch: branch,
210 CommitMessage: commitMessage,
211 CommitBody: commitBody,
212 AuthorName: authorName,
213 AuthorEmail: authorEmail,
214 Patch: string(patch),
215 }
216
217 body, _ := json.Marshal(mr)
218
219 req, err := s.newRequest(Method, endpoint, body)
220 if err != nil {
221 return nil, err
222 }
223
224 return s.client.Do(req)
225}
226
227func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
228 const (
229 Method = "POST"
230 )
231 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
232
233 body, _ := json.Marshal(map[string]any{
234 "patch": string(patch),
235 "branch": branch,
236 })
237
238 req, err := s.newRequest(Method, endpoint, body)
239 if err != nil {
240 return nil, err
241 }
242
243 return s.client.Do(req)
244}
245
246func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
247 const (
248 Method = "POST"
249 )
250 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
251
252 req, err := s.newRequest(Method, endpoint, nil)
253 if err != nil {
254 return nil, err
255 }
256
257 return s.client.Do(req)
258}