github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/logsync/http.go (about) 1 package logsync 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 12 "github.com/qri-io/qri/auth/key" 13 "github.com/qri-io/qri/dsref" 14 "github.com/qri-io/qri/logbook" 15 "github.com/qri-io/qri/profile" 16 "github.com/qri-io/qri/repo" 17 reporef "github.com/qri-io/qri/repo/ref" 18 ) 19 20 // httpClient is the request side of doing dsync over HTTP 21 type httpClient struct { 22 URL string 23 } 24 25 // compile time assertion that httpClient is a remote 26 // httpClient exists to satisfy the Remote interface on the client side 27 var _ remote = (*httpClient)(nil) 28 29 func (c *httpClient) addr() string { 30 return c.URL 31 } 32 33 func (c *httpClient) put(ctx context.Context, author profile.Author, ref dsref.Ref, r io.Reader) error { 34 log.Debugw("httpClient.put", "ref", ref) 35 u, err := url.Parse(c.URL) 36 if err != nil { 37 return fmt.Errorf("invalid logsync client url: %w", err) 38 } 39 q := u.Query() 40 // TODO(B5): we need the old serialization format here b/c logsync checks the 41 // profileID matches the author. Migrate to a new system for validating who-can-push-what 42 // using keystore & UCANs, then switch this to standard reference string serialization 43 q.Set("ref", ref.LegacyProfileIDString()) 44 u.RawQuery = q.Encode() 45 46 req, err := http.NewRequest("PUT", u.String(), r) 47 if err != nil { 48 return err 49 } 50 req = req.WithContext(ctx) 51 52 if err := addAuthorHTTPHeaders(req.Header, author); err != nil { 53 return err 54 } 55 56 res, err := http.DefaultClient.Do(req) 57 if err != nil { 58 return err 59 } 60 if res.StatusCode != http.StatusOK { 61 if errmsg, err := ioutil.ReadAll(res.Body); err == nil { 62 return fmt.Errorf(string(errmsg)) 63 } 64 return err 65 } 66 67 return nil 68 } 69 70 func (c *httpClient) get(ctx context.Context, author profile.Author, ref dsref.Ref) (profile.Author, io.Reader, error) { 71 log.Debugw("httpClient.get", "ref", ref) 72 u, err := url.Parse(c.URL) 73 if err != nil { 74 return nil, nil, fmt.Errorf("invalid logsync client url: %w", err) 75 } 76 q := u.Query() 77 // TODO(b5): remove initID for backwards compatiblity. get doesn't rely on the 78 // ProfileID field, but the other end of the wire may error if we send an InitID 79 // field 80 ref.InitID = "" 81 q.Set("ref", ref.String()) 82 u.RawQuery = q.Encode() 83 84 req, err := http.NewRequest("GET", u.String(), nil) 85 if err != nil { 86 return nil, nil, err 87 } 88 req = req.WithContext(ctx) 89 90 if err := addAuthorHTTPHeaders(req.Header, author); err != nil { 91 log.Debugf("addAuthorHTTPHeaders error=%q", err) 92 return nil, nil, err 93 } 94 95 res, err := http.DefaultClient.Do(req) 96 if err != nil { 97 log.Debugf("http.DefaultClient.Do error=%q", err) 98 return nil, nil, err 99 } 100 101 if res.StatusCode != http.StatusOK { 102 log.Debugf("httpClient.get statusCode=%d", res.StatusCode) 103 if errmsg, err := ioutil.ReadAll(res.Body); err == nil { 104 return nil, nil, fmt.Errorf(string(errmsg)) 105 } 106 return nil, nil, err 107 } 108 109 sender, err := senderFromHTTPHeaders(res.Header) 110 if err != nil { 111 return nil, nil, err 112 } 113 114 return sender, res.Body, nil 115 } 116 117 func (c *httpClient) del(ctx context.Context, author profile.Author, ref dsref.Ref) error { 118 req, err := http.NewRequest("DELETE", fmt.Sprintf("%s?ref=%s", c.URL, ref), nil) 119 if err != nil { 120 return err 121 } 122 req = req.WithContext(ctx) 123 124 if err := addAuthorHTTPHeaders(req.Header, author); err != nil { 125 return err 126 } 127 128 res, err := http.DefaultClient.Do(req) 129 if err != nil { 130 return err 131 } 132 if res.StatusCode != http.StatusOK { 133 if errmsg, err := ioutil.ReadAll(res.Body); err == nil { 134 return fmt.Errorf(string(errmsg)) 135 } 136 } 137 return err 138 } 139 140 func addAuthorHTTPHeaders(h http.Header, author profile.Author) error { 141 h.Set("ID", author.AuthorID()) 142 h.Set("username", author.Username()) 143 pubKey, err := key.EncodePubKeyB64(author.AuthorPubKey()) 144 if err != nil { 145 return err 146 } 147 h.Set("PubKey", pubKey) 148 return nil 149 } 150 151 func senderFromHTTPHeaders(h http.Header) (profile.Author, error) { 152 pub, err := key.DecodeB64PubKey(h.Get("PubKey")) 153 if err != nil { 154 return nil, fmt.Errorf("decoding public key: %s", err) 155 } 156 157 return profile.NewAuthor(h.Get("ID"), pub, h.Get("username")), nil 158 } 159 160 // HTTPHandler exposes a Dsync remote over HTTP by exposing a HTTP handler 161 // that interlocks with methods exposed by httpClient 162 func HTTPHandler(lsync *Logsync) http.HandlerFunc { 163 return func(w http.ResponseWriter, r *http.Request) { 164 sender, err := senderFromHTTPHeaders(r.Header) 165 if err != nil { 166 log.Debugf("senderFromHTTPHeaders error=%q", err) 167 w.WriteHeader(http.StatusBadRequest) 168 w.Write([]byte(err.Error())) 169 return 170 } 171 172 switch r.Method { 173 case "PUT": 174 ref, err := dsref.Parse(r.FormValue("ref")) 175 if err != nil { 176 log.Debugf("PUT dsref.Parse error=%q", err) 177 w.WriteHeader(http.StatusBadRequest) 178 w.Write([]byte(err.Error())) 179 return 180 } 181 if err := lsync.put(r.Context(), sender, ref, r.Body); err != nil { 182 w.WriteHeader(http.StatusBadRequest) 183 w.Write([]byte(err.Error())) 184 return 185 } 186 r.Body.Close() 187 188 addAuthorHTTPHeaders(w.Header(), lsync.Author()) 189 return 190 case "GET": 191 ref, err := dsref.Parse(r.FormValue("ref")) 192 if err != nil { 193 log.Debugf("GET dsref.Parse error=%q", err) 194 w.WriteHeader(http.StatusBadRequest) 195 w.Write([]byte(err.Error())) 196 return 197 } 198 199 receiver, r, err := lsync.get(r.Context(), sender, ref) 200 if err != nil { 201 log.Debugf("GET error=%q ref=%q", err, ref) 202 // TODO (ramfox): implement a robust error response strategy 203 if errors.Is(err, logbook.ErrNotFound) { 204 w.WriteHeader(http.StatusNotFound) 205 w.Write([]byte(err.Error())) 206 return 207 } 208 w.WriteHeader(http.StatusBadRequest) 209 w.Write([]byte(err.Error())) 210 return 211 } 212 addAuthorHTTPHeaders(w.Header(), receiver) 213 io.Copy(w, r) 214 return 215 case "DELETE": 216 ref, err := repo.ParseDatasetRef(r.FormValue("ref")) 217 if err != nil { 218 w.WriteHeader(http.StatusBadRequest) 219 w.Write([]byte(err.Error())) 220 return 221 } 222 223 if err = lsync.del(r.Context(), sender, reporef.ConvertToDsref(ref)); err != nil { 224 w.WriteHeader(http.StatusBadRequest) 225 w.Write([]byte(err.Error())) 226 return 227 } 228 229 addAuthorHTTPHeaders(w.Header(), lsync.Author()) 230 return 231 default: 232 w.WriteHeader(http.StatusNotFound) 233 w.Write([]byte(`not found`)) 234 return 235 } 236 } 237 }