1// A Store that can fetch and set metadata on a remote server. 2// Some API constraints: 3// - Response bodies for error codes should be unmarshallable as: 4// {"errors": [{..., "detail": <serialized validation error>}]} 5// else validation error details, etc. will be unparsable. The errors 6// should have a github.com/theupdateframework/notary/tuf/validation/SerializableError 7// in the Details field. 8// If writing your own server, please have a look at 9// github.com/docker/distribution/registry/api/errcode 10 11package storage 12 13import ( 14 "bytes" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "io/ioutil" 20 "mime/multipart" 21 "net/http" 22 "net/url" 23 "path" 24 25 "github.com/sirupsen/logrus" 26 "github.com/theupdateframework/notary" 27 "github.com/theupdateframework/notary/tuf/data" 28 "github.com/theupdateframework/notary/tuf/validation" 29) 30 31const ( 32 // MaxErrorResponseSize is the maximum size for an error message - 1KiB 33 MaxErrorResponseSize int64 = 1 << 10 34 // MaxKeySize is the maximum size for a stored TUF key - 256KiB 35 MaxKeySize = 256 << 10 36) 37 38// ErrServerUnavailable indicates an error from the server. code allows us to 39// populate the http error we received 40type ErrServerUnavailable struct { 41 code int 42} 43 44// NetworkError represents any kind of network error when attempting to make a request 45type NetworkError struct { 46 Wrapped error 47} 48 49func (n NetworkError) Error() string { 50 if _, ok := n.Wrapped.(*url.Error); ok { 51 // QueryUnescape does the inverse transformation of QueryEscape, 52 // converting %AB into the byte 0xAB and '+' into ' ' (space). 53 // It returns an error if any % is not followed by two hexadecimal digits. 54 // 55 // If this happens, we log out the QueryUnescape error and return the 56 // original error to client. 57 res, err := url.QueryUnescape(n.Wrapped.Error()) 58 if err != nil { 59 logrus.Errorf("unescape network error message failed: %s", err) 60 return n.Wrapped.Error() 61 } 62 return res 63 } 64 65 return n.Wrapped.Error() 66} 67 68func (err ErrServerUnavailable) Error() string { 69 if err.code == 401 { 70 return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.") 71 } 72 return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code) 73} 74 75// ErrMaliciousServer indicates the server returned a response that is highly suspected 76// of being malicious. i.e. it attempted to send us more data than the known size of a 77// particular role metadata. 78type ErrMaliciousServer struct{} 79 80func (err ErrMaliciousServer) Error() string { 81 return "trust server returned a bad response." 82} 83 84// ErrInvalidOperation indicates that the server returned a 400 response and 85// propagate any body we received. 86type ErrInvalidOperation struct { 87 msg string 88} 89 90func (err ErrInvalidOperation) Error() string { 91 if err.msg != "" { 92 return fmt.Sprintf("trust server rejected operation: %s", err.msg) 93 } 94 return "trust server rejected operation." 95} 96 97// HTTPStore manages pulling and pushing metadata from and to a remote 98// service over HTTP. It assumes the URL structure of the remote service 99// maps identically to the structure of the TUF repo: 100// <baseURL>/<metaPrefix>/(root|targets|snapshot|timestamp).json 101// <baseURL>/<targetsPrefix>/foo.sh 102// 103// If consistent snapshots are disabled, it is advised that caching is not 104// enabled. Simple set a cachePath (and ensure it's writeable) to enable 105// caching. 106type HTTPStore struct { 107 baseURL url.URL 108 metaPrefix string 109 metaExtension string 110 keyExtension string 111 roundTrip http.RoundTripper 112} 113 114// NewHTTPStore initializes a new store against a URL and a number of configuration options. 115// 116// In case of a nil `roundTrip`, a default offline store is used instead. 117func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) { 118 base, err := url.Parse(baseURL) 119 if err != nil { 120 return nil, err 121 } 122 if !base.IsAbs() { 123 return nil, errors.New("HTTPStore requires an absolute baseURL") 124 } 125 if roundTrip == nil { 126 return &OfflineStore{}, nil 127 } 128 return &HTTPStore{ 129 baseURL: *base, 130 metaPrefix: metaPrefix, 131 metaExtension: metaExtension, 132 keyExtension: keyExtension, 133 roundTrip: roundTrip, 134 }, nil 135} 136 137func tryUnmarshalError(resp *http.Response, defaultError error) error { 138 b := io.LimitReader(resp.Body, MaxErrorResponseSize) 139 bodyBytes, err := ioutil.ReadAll(b) 140 if err != nil { 141 return defaultError 142 } 143 var parsedErrors struct { 144 Errors []struct { 145 Detail validation.SerializableError `json:"detail"` 146 } `json:"errors"` 147 } 148 if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil { 149 return defaultError 150 } 151 if len(parsedErrors.Errors) != 1 { 152 return defaultError 153 } 154 err = parsedErrors.Errors[0].Detail.Error 155 if err == nil { 156 return defaultError 157 } 158 return err 159} 160 161func translateStatusToError(resp *http.Response, resource string) error { 162 switch resp.StatusCode { 163 case http.StatusOK: 164 return nil 165 case http.StatusNotFound: 166 return ErrMetaNotFound{Resource: resource} 167 case http.StatusBadRequest: 168 return tryUnmarshalError(resp, ErrInvalidOperation{}) 169 default: 170 return ErrServerUnavailable{code: resp.StatusCode} 171 } 172} 173 174// GetSized downloads the named meta file with the given size. A short body 175// is acceptable because in the case of timestamp.json, the size is a cap, 176// not an exact length. 177// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a 178// predefined threshold "notary.MaxDownloadSize". 179func (s HTTPStore) GetSized(name string, size int64) ([]byte, error) { 180 url, err := s.buildMetaURL(name) 181 if err != nil { 182 return nil, err 183 } 184 req, err := http.NewRequest("GET", url.String(), nil) 185 if err != nil { 186 return nil, err 187 } 188 resp, err := s.roundTrip.RoundTrip(req) 189 if err != nil { 190 return nil, NetworkError{Wrapped: err} 191 } 192 defer resp.Body.Close() 193 if err := translateStatusToError(resp, name); err != nil { 194 logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name) 195 return nil, err 196 } 197 if size == NoSizeLimit { 198 size = notary.MaxDownloadSize 199 } 200 if resp.ContentLength > size { 201 return nil, ErrMaliciousServer{} 202 } 203 logrus.Debugf("%d when retrieving metadata for %s", resp.StatusCode, name) 204 b := io.LimitReader(resp.Body, size) 205 body, err := ioutil.ReadAll(b) 206 if err != nil { 207 return nil, err 208 } 209 return body, nil 210} 211 212// Set sends a single piece of metadata to the TUF server 213func (s HTTPStore) Set(name string, blob []byte) error { 214 return s.SetMulti(map[string][]byte{name: blob}) 215} 216 217// Remove always fails, because we should never be able to delete metadata 218// remotely 219func (s HTTPStore) Remove(name string) error { 220 return ErrInvalidOperation{msg: "cannot delete individual metadata files"} 221} 222 223// NewMultiPartMetaRequest builds a request with the provided metadata updates 224// in multipart form 225func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) { 226 body := &bytes.Buffer{} 227 writer := multipart.NewWriter(body) 228 for role, blob := range metas { 229 part, err := writer.CreateFormFile("files", role) 230 if err != nil { 231 return nil, err 232 } 233 _, err = io.Copy(part, bytes.NewBuffer(blob)) 234 if err != nil { 235 return nil, err 236 } 237 } 238 err := writer.Close() 239 if err != nil { 240 return nil, err 241 } 242 req, err := http.NewRequest("POST", url, body) 243 if err != nil { 244 return nil, err 245 } 246 req.Header.Set("Content-Type", writer.FormDataContentType()) 247 return req, nil 248} 249 250// SetMulti does a single batch upload of multiple pieces of TUF metadata. 251// This should be preferred for updating a remote server as it enable the server 252// to remain consistent, either accepting or rejecting the complete update. 253func (s HTTPStore) SetMulti(metas map[string][]byte) error { 254 url, err := s.buildMetaURL("") 255 if err != nil { 256 return err 257 } 258 req, err := NewMultiPartMetaRequest(url.String(), metas) 259 if err != nil { 260 return err 261 } 262 resp, err := s.roundTrip.RoundTrip(req) 263 if err != nil { 264 return NetworkError{Wrapped: err} 265 } 266 defer resp.Body.Close() 267 // if this 404's something is pretty wrong 268 return translateStatusToError(resp, "POST metadata endpoint") 269} 270 271// RemoveAll will attempt to delete all TUF metadata for a GUN 272func (s HTTPStore) RemoveAll() error { 273 url, err := s.buildMetaURL("") 274 if err != nil { 275 return err 276 } 277 req, err := http.NewRequest("DELETE", url.String(), nil) 278 if err != nil { 279 return err 280 } 281 resp, err := s.roundTrip.RoundTrip(req) 282 if err != nil { 283 return NetworkError{Wrapped: err} 284 } 285 defer resp.Body.Close() 286 return translateStatusToError(resp, "DELETE metadata for GUN endpoint") 287} 288 289func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) { 290 var filename string 291 if name != "" { 292 filename = fmt.Sprintf("%s.%s", name, s.metaExtension) 293 } 294 uri := path.Join(s.metaPrefix, filename) 295 return s.buildURL(uri) 296} 297 298func (s HTTPStore) buildKeyURL(name data.RoleName) (*url.URL, error) { 299 filename := fmt.Sprintf("%s.%s", name.String(), s.keyExtension) 300 uri := path.Join(s.metaPrefix, filename) 301 return s.buildURL(uri) 302} 303 304func (s HTTPStore) buildURL(uri string) (*url.URL, error) { 305 sub, err := url.Parse(uri) 306 if err != nil { 307 return nil, err 308 } 309 return s.baseURL.ResolveReference(sub), nil 310} 311 312// GetKey retrieves a public key from the remote server 313func (s HTTPStore) GetKey(role data.RoleName) ([]byte, error) { 314 url, err := s.buildKeyURL(role) 315 if err != nil { 316 return nil, err 317 } 318 req, err := http.NewRequest("GET", url.String(), nil) 319 if err != nil { 320 return nil, err 321 } 322 resp, err := s.roundTrip.RoundTrip(req) 323 if err != nil { 324 return nil, NetworkError{Wrapped: err} 325 } 326 defer resp.Body.Close() 327 if err := translateStatusToError(resp, role.String()+" key"); err != nil { 328 return nil, err 329 } 330 b := io.LimitReader(resp.Body, MaxKeySize) 331 body, err := ioutil.ReadAll(b) 332 if err != nil { 333 return nil, err 334 } 335 return body, nil 336} 337 338// RotateKey rotates a private key and returns the public component from the remote server 339func (s HTTPStore) RotateKey(role data.RoleName) ([]byte, error) { 340 url, err := s.buildKeyURL(role) 341 if err != nil { 342 return nil, err 343 } 344 req, err := http.NewRequest("POST", url.String(), nil) 345 if err != nil { 346 return nil, err 347 } 348 resp, err := s.roundTrip.RoundTrip(req) 349 if err != nil { 350 return nil, NetworkError{Wrapped: err} 351 } 352 defer resp.Body.Close() 353 if err := translateStatusToError(resp, role.String()+" key"); err != nil { 354 return nil, err 355 } 356 b := io.LimitReader(resp.Body, MaxKeySize) 357 body, err := ioutil.ReadAll(b) 358 if err != nil { 359 return nil, err 360 } 361 return body, nil 362} 363 364// Location returns a human readable name for the storage location 365func (s HTTPStore) Location() string { 366 return s.baseURL.String() 367} 368