1package state
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 "github.com/sotangled/tangled/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) RemoveRepo(did, repoName string) (*http.Response, error) {
107 const (
108 Method = "DELETE"
109 Endpoint = "/repo"
110 )
111
112 body, _ := json.Marshal(map[string]any{
113 "did": did,
114 "name": repoName,
115 })
116
117 req, err := s.newRequest(Method, Endpoint, body)
118 if err != nil {
119 return nil, err
120 }
121
122 return s.client.Do(req)
123}
124
125func (s *SignedClient) AddMember(did string) (*http.Response, error) {
126 const (
127 Method = "PUT"
128 Endpoint = "/member/add"
129 )
130
131 body, _ := json.Marshal(map[string]any{
132 "did": did,
133 })
134
135 req, err := s.newRequest(Method, Endpoint, body)
136 if err != nil {
137 return nil, err
138 }
139
140 return s.client.Do(req)
141}
142
143func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
144 const (
145 Method = "POST"
146 )
147 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
148
149 body, _ := json.Marshal(map[string]any{
150 "did": memberDid,
151 })
152
153 req, err := s.newRequest(Method, endpoint, body)
154 if err != nil {
155 return nil, err
156 }
157
158 return s.client.Do(req)
159}
160
161func (s *SignedClient) Merge(
162 patch []byte,
163 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
164) (*http.Response, error) {
165 const (
166 Method = "POST"
167 )
168 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
169
170 mr := types.MergeRequest{
171 Branch: branch,
172 CommitMessage: commitMessage,
173 CommitBody: commitBody,
174 AuthorName: authorName,
175 AuthorEmail: authorEmail,
176 Patch: string(patch),
177 }
178
179 body, _ := json.Marshal(mr)
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) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
190 const (
191 Method = "POST"
192 )
193 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
194
195 body, _ := json.Marshal(map[string]interface{}{
196 "patch": string(patch),
197 "branch": branch,
198 })
199
200 req, err := s.newRequest(Method, endpoint, body)
201 if err != nil {
202 return nil, err
203 }
204
205 return s.client.Do(req)
206}
207
208type UnsignedClient struct {
209 Url *url.URL
210 client *http.Client
211}
212
213func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
214 client := &http.Client{
215 Timeout: 5 * time.Second,
216 }
217
218 scheme := "https"
219 if dev {
220 scheme = "http"
221 }
222 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
223 if err != nil {
224 return nil, err
225 }
226
227 unsignedClient := &UnsignedClient{
228 client: client,
229 Url: url,
230 }
231
232 return unsignedClient, nil
233}
234
235func (us *UnsignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
236 return http.NewRequest(method, us.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
237}
238
239func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
240 const (
241 Method = "GET"
242 )
243
244 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
245 if ref == "" {
246 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
247 }
248
249 req, err := us.newRequest(Method, endpoint, nil)
250 if err != nil {
251 return nil, err
252 }
253
254 return us.client.Do(req)
255}
256
257func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
258 const (
259 Method = "GET"
260 )
261
262 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
263
264 req, err := us.newRequest(Method, endpoint, nil)
265 if err != nil {
266 return nil, err
267 }
268
269 return us.client.Do(req)
270}