tinyrelayfile
tinyrelay/internal/blob/path_upload.go/

internal/blob/path_upload.go raw
1package blob
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10)
11
12// maxPathUploadSources bounds the remote sources one BUD-13 upload may name.
13const maxPathUploadSources = 8
14
15// pathUpload implements the draft BUD-13 path-addressed upload endpoint.
16// The route itself is dispatched by serveHTTP after validating the hash path.
17func (s *Service) pathUpload(w http.ResponseWriter, r *http.Request, expectedHash string) {
18 // Authenticate before inspecting the body. This is important for signed
19 // uploads: an invalid request must not consume a potentially large stream.
20 pubkey, err := s.authorize(r, ActionUpload)
21 if err != nil {
22 s.fail(w, http.StatusUnauthorized, err)
23 return
24 }
25
26 urls := r.URL.Query()["url"]
27 if len(urls) == 0 {
28 entry, created, putErr := s.putValidatedWithMetadata(r.Context(), r.Body,
29 contentType(r.Header.Get("content-type")), pubkey, expectedHash,
30 r.URL.Query().Get("filename"), r.URL.Query().Get("path"), r.URL.Query().Get("access"), r.URL.Query().Get("purpose"),
31 func(actual string) error {
32 if s.config.ValidateUpload != nil {
33 return s.config.ValidateUpload(r, actual)
34 }
35 return nil
36 }, nil)
37 if putErr != nil {
38 s.fail(w, statusFor(putErr), putErr)
39 return
40 }
41 s.writeJSON(w, chooseStatus(created, http.StatusCreated), s.descriptor(r, entry))
42 return
43 }
44
45 // BUD-13 remote uploads must have an empty request body. Check this after
46 // authorization, including chunked requests whose ContentLength is unknown.
47 if r.Body != nil {
48 var probe [1]byte
49 n, readErr := io.ReadFull(r.Body, probe[:])
50 if n != 0 || (readErr != nil && !errors.Is(readErr, io.EOF)) {
51 s.fail(w, http.StatusBadRequest, errors.New("invalid: remote upload body must be empty"))
52 return
53 }
54 }
55 if len(urls) > maxPathUploadSources {
56 s.fail(w, http.StatusBadRequest, fmt.Errorf("invalid: at most %d remote sources per upload", maxPathUploadSources))
57 return
58 }
59 for _, raw := range urls {
60 if err := s.validatePathUploadURL(r.Context(), raw); err != nil {
61 s.fail(w, http.StatusBadRequest, err)
62 return
63 }
64 }
65
66 client := s.config.HTTPClient
67 if client == nil {
68 client = s.safeClient()
69 }
70 var mismatch bool
71 for _, raw := range urls {
72 fetchRequest, requestErr := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil)
73 if requestErr != nil {
74 continue
75 }
76 response, fetchErr := client.Do(fetchRequest)
77 if fetchErr != nil {
78 continue
79 }
80 if response.StatusCode < 200 || response.StatusCode >= 300 {
81 _ = response.Body.Close()
82 continue
83 }
84 typ := contentType(response.Header.Get("content-type"))
85 if typ == "application/octet-stream" {
86 typ = typeForExtension(raw)
87 }
88 entry, created, putErr := s.putValidatedWithMetadata(r.Context(), response.Body, typ,
89 pubkey, expectedHash, "", "", r.URL.Query().Get("access"), r.URL.Query().Get("purpose"), func(actual string) error {
90 if s.config.ValidateUpload != nil {
91 return s.config.ValidateUpload(r, actual)
92 }
93 return nil
94 }, nil)
95 _ = response.Body.Close()
96 if putErr == nil {
97 s.writeJSON(w, chooseStatus(created, http.StatusCreated), s.descriptor(r, entry))
98 return
99 }
100 if errors.Is(putErr, ErrHashMismatch) {
101 mismatch = true
102 continue
103 }
104 s.fail(w, statusFor(putErr), putErr)
105 return
106 }
107 if mismatch {
108 s.fail(w, http.StatusConflict, ErrHashMismatch)
109 return
110 }
111 s.fail(w, http.StatusBadGateway, errors.New("error: no usable remote source"))
112}
113
114func (s *Service) validatePathUploadURL(ctx context.Context, raw string) error {
115 u, err := url.Parse(raw)
116 if err != nil || u == nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.Fragment != "" {
117 return errors.New("invalid: url must be an absolute http or https url")
118 }
119 ips, err := s.resolve(ctx, u.Hostname())
120 if err != nil || len(ips) == 0 {
121 return errors.New("invalid: origin hostname did not resolve")
122 }
123 for _, ip := range ips {
124 if !publicIP(ip) {
125 return errors.New("invalid: origin resolves to a private address")
126 }
127 }
128 return nil
129}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs