master
go 401 lines 9.14 KB
Raw
1 package pinningservice
2
3 import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "reflect"
8 "strconv"
9 "strings"
10 "sync"
11 "time"
12
13 "github.com/google/uuid"
14 "github.com/julienschmidt/httprouter"
15 )
16
17 func NewRouter(authToken string, svc *PinningService) http.Handler {
18 router := httprouter.New()
19 router.GET("/api/v1/pins", svc.listPins)
20 router.POST("/api/v1/pins", svc.addPin)
21 router.GET("/api/v1/pins/:requestID", svc.getPin)
22 router.POST("/api/v1/pins/:requestID", svc.replacePin)
23 router.DELETE("/api/v1/pins/:requestID", svc.removePin)
24
25 handler := authHandler(authToken, router)
26
27 return handler
28 }
29
30 func authHandler(authToken string, delegate http.Handler) http.Handler {
31 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32 authz := r.Header.Get("Authorization")
33 if !strings.HasPrefix(authz, "Bearer ") {
34 errResp(w, "invalid authorization token, must start with 'Bearer '", "", http.StatusBadRequest)
35 return
36 }
37
38 token := strings.TrimPrefix(authz, "Bearer ")
39 if token != authToken {
40 errResp(w, "access denied", "", http.StatusUnauthorized)
41 return
42 }
43
44 delegate.ServeHTTP(w, r)
45 })
46 }
47
48 func New() *PinningService {
49 return &PinningService{
50 PinAdded: func(*AddPinRequest, *PinStatus) {},
51 }
52 }
53
54 // PinningService is a basic pinning service that implements the Remote Pinning API, for testing Kubo's integration with remote pinning services.
55 // Pins are not persisted, they are just kept in-memory, and this provides callbacks for controlling the behavior of the pinning service.
56 type PinningService struct {
57 m sync.Mutex
58 // PinAdded is a callback that is invoked after a new pin is added via the API.
59 PinAdded func(*AddPinRequest, *PinStatus)
60 pins []*PinStatus
61 }
62
63 type Pin struct {
64 CID string `json:"cid"`
65 Name string `json:"name"`
66 Origins []string `json:"origins"`
67 Meta map[string]any `json:"meta"`
68 }
69
70 type PinStatus struct {
71 M sync.Mutex
72 RequestID string
73 Status string
74 Created time.Time
75 Pin Pin
76 Delegates []string
77 Info map[string]any
78 }
79
80 func (p *PinStatus) MarshalJSON() ([]byte, error) {
81 type pinStatusJSON struct {
82 RequestID string `json:"requestid"`
83 Status string `json:"status"`
84 Created time.Time `json:"created"`
85 Pin Pin `json:"pin"`
86 Delegates []string `json:"delegates"`
87 Info map[string]any `json:"info"`
88 }
89 // lock the pin before marshaling it to protect against data races while marshaling
90 p.M.Lock()
91 pinJSON := pinStatusJSON{
92 RequestID: p.RequestID,
93 Status: p.Status,
94 Created: p.Created,
95 Pin: p.Pin,
96 Delegates: p.Delegates,
97 Info: p.Info,
98 }
99 p.M.Unlock()
100 return json.Marshal(pinJSON)
101 }
102
103 func (p *PinStatus) Clone() PinStatus {
104 return PinStatus{
105 RequestID: p.RequestID,
106 Status: p.Status,
107 Created: p.Created,
108 Pin: p.Pin,
109 Delegates: p.Delegates,
110 Info: p.Info,
111 }
112 }
113
114 const (
115 matchExact = "exact"
116 matchIExact = "iexact"
117 matchPartial = "partial"
118 matchIPartial = "ipartial"
119
120 statusQueued = "queued"
121 statusPinning = "pinning"
122 statusPinned = "pinned"
123 statusFailed = "failed"
124
125 timeLayout = "2006-01-02T15:04:05.999Z"
126 )
127
128 func errResp(w http.ResponseWriter, reason, details string, statusCode int) {
129 type errorObj struct {
130 Reason string `json:"reason"`
131 Details string `json:"details"`
132 }
133 type errorResp struct {
134 Error errorObj `json:"error"`
135 }
136 resp := errorResp{
137 Error: errorObj{
138 Reason: reason,
139 Details: details,
140 },
141 }
142 writeJSON(w, resp, statusCode)
143 }
144
145 func writeJSON(w http.ResponseWriter, val any, statusCode int) {
146 b, err := json.Marshal(val)
147 if err != nil {
148 w.Header().Set("Content-Type", "text/plain")
149 errResp(w, fmt.Sprintf("marshaling response: %s", err), "", http.StatusInternalServerError)
150 return
151 }
152 w.Header().Set("Content-Type", "application/json")
153 w.WriteHeader(statusCode)
154 _, _ = w.Write(b)
155 }
156
157 type AddPinRequest struct {
158 CID string `json:"cid"`
159 Name string `json:"name"`
160 Origins []string `json:"origins"`
161 Meta map[string]any `json:"meta"`
162 }
163
164 func (p *PinningService) addPin(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
165 var addReq AddPinRequest
166 err := json.NewDecoder(req.Body).Decode(&addReq)
167 if err != nil {
168 errResp(writer, fmt.Sprintf("unmarshaling req: %s", err), "", http.StatusBadRequest)
169 return
170 }
171
172 pin := &PinStatus{
173 RequestID: uuid.NewString(),
174 Status: statusQueued,
175 Created: time.Now(),
176 Pin: Pin(addReq),
177 }
178
179 p.m.Lock()
180 p.pins = append(p.pins, pin)
181 p.m.Unlock()
182
183 writeJSON(writer, &pin, http.StatusAccepted)
184 p.PinAdded(&addReq, pin)
185 }
186
187 type ListPinsResponse struct {
188 Count int `json:"count"`
189 Results []*PinStatus `json:"results"`
190 }
191
192 func (p *PinningService) listPins(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
193 q := req.URL.Query()
194
195 cidStr := q.Get("cid")
196 name := q.Get("name")
197 match := q.Get("match")
198 status := q.Get("status")
199 beforeStr := q.Get("before")
200 afterStr := q.Get("after")
201 limitStr := q.Get("limit")
202 metaStr := q.Get("meta")
203
204 if limitStr == "" {
205 limitStr = "10"
206 }
207 limit, err := strconv.Atoi(limitStr)
208 if err != nil {
209 errResp(writer, fmt.Sprintf("parsing limit: %s", err), "", http.StatusBadRequest)
210 return
211 }
212
213 var cids []string
214 if cidStr != "" {
215 cids = strings.Split(cidStr, ",")
216 }
217
218 var statuses []string
219 if status != "" {
220 statuses = strings.Split(status, ",")
221 }
222
223 p.m.Lock()
224 defer p.m.Unlock()
225 var pins []*PinStatus
226 for _, pinStatus := range p.pins {
227 // clone it so we can immediately release the lock
228 pinStatus.M.Lock()
229 clonedPS := pinStatus.Clone()
230 pinStatus.M.Unlock()
231
232 // cid
233 var matchesCID bool
234 if len(cids) == 0 {
235 matchesCID = true
236 } else {
237 for _, cid := range cids {
238 if cid == clonedPS.Pin.CID {
239 matchesCID = true
240 }
241 }
242 }
243 if !matchesCID {
244 continue
245 }
246
247 // name
248 if match == "" {
249 match = matchExact
250 }
251 if name != "" {
252 switch match {
253 case matchExact:
254 if name != clonedPS.Pin.Name {
255 continue
256 }
257 case matchIExact:
258 if !strings.EqualFold(name, clonedPS.Pin.Name) {
259 continue
260 }
261 case matchPartial:
262 if !strings.Contains(clonedPS.Pin.Name, name) {
263 continue
264 }
265 case matchIPartial:
266 if !strings.Contains(strings.ToLower(clonedPS.Pin.Name), strings.ToLower(name)) {
267 continue
268 }
269 default:
270 errResp(writer, fmt.Sprintf("unknown match %q", match), "", http.StatusBadRequest)
271 return
272 }
273 }
274
275 // status
276 var matchesStatus bool
277 if len(statuses) == 0 {
278 statuses = []string{statusPinned}
279 }
280 for _, status := range statuses {
281 if status == clonedPS.Status {
282 matchesStatus = true
283 }
284 }
285 if !matchesStatus {
286 continue
287 }
288
289 // before
290 if beforeStr != "" {
291 before, err := time.Parse(timeLayout, beforeStr)
292 if err != nil {
293 errResp(writer, fmt.Sprintf("parsing before: %s", err), "", http.StatusBadRequest)
294 return
295 }
296 if !clonedPS.Created.Before(before) {
297 continue
298 }
299 }
300
301 // after
302 if afterStr != "" {
303 after, err := time.Parse(timeLayout, afterStr)
304 if err != nil {
305 errResp(writer, fmt.Sprintf("parsing before: %s", err), "", http.StatusBadRequest)
306 return
307 }
308 if !clonedPS.Created.After(after) {
309 continue
310 }
311 }
312
313 // meta
314 if metaStr != "" {
315 meta := map[string]any{}
316 err := json.Unmarshal([]byte(metaStr), &meta)
317 if err != nil {
318 errResp(writer, fmt.Sprintf("parsing meta: %s", err), "", http.StatusBadRequest)
319 return
320 }
321 var matchesMeta bool
322 for k, v := range meta {
323 pinV, contains := clonedPS.Pin.Meta[k]
324 if !contains || !reflect.DeepEqual(pinV, v) {
325 matchesMeta = false
326 break
327 }
328 }
329 if !matchesMeta {
330 continue
331 }
332 }
333
334 // add the original pin status, not the cloned one
335 pins = append(pins, pinStatus)
336
337 if len(pins) == limit {
338 break
339 }
340 }
341
342 out := ListPinsResponse{
343 Count: len(pins),
344 Results: pins,
345 }
346 writeJSON(writer, out, http.StatusOK)
347 }
348
349 func (p *PinningService) getPin(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
350 requestID := params.ByName("requestID")
351 p.m.Lock()
352 defer p.m.Unlock()
353 for _, pin := range p.pins {
354 if pin.RequestID == requestID {
355 writeJSON(writer, pin, http.StatusOK)
356 return
357 }
358 }
359 errResp(writer, "", "", http.StatusNotFound)
360 }
361
362 func (p *PinningService) replacePin(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
363 requestID := params.ByName("requestID")
364
365 var replaceReq Pin
366 err := json.NewDecoder(req.Body).Decode(&replaceReq)
367 if err != nil {
368 errResp(writer, fmt.Sprintf("decoding request: %s", err), "", http.StatusBadRequest)
369 return
370 }
371
372 p.m.Lock()
373 defer p.m.Unlock()
374 for _, pin := range p.pins {
375 if pin.RequestID == requestID {
376 pin.M.Lock()
377 pin.Pin = replaceReq
378 pin.M.Unlock()
379 writer.WriteHeader(http.StatusAccepted)
380 return
381 }
382 }
383 errResp(writer, "", "", http.StatusNotFound)
384 }
385
386 func (p *PinningService) removePin(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
387 requestID := params.ByName("requestID")
388
389 p.m.Lock()
390 defer p.m.Unlock()
391
392 for i, pin := range p.pins {
393 if pin.RequestID == requestID {
394 p.pins = append(p.pins[0:i], p.pins[i+1:]...)
395 writer.WriteHeader(http.StatusAccepted)
396 return
397 }
398 }
399
400 errResp(writer, "", "", http.StatusNotFound)
401 }