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