github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/httpattachment/attachment.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package httpattachment provides facilities for attaching a streaming
     5  // blob of data and associated metadata to an HTTP API request,
     6  // and for reading that blob on the server side.
     7  package httpattachment
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/json"
    12  	"io"
    13  	"mime"
    14  	"mime/multipart"
    15  	"net/http"
    16  	"net/textproto"
    17  	"strings"
    18  
    19  	"github.com/juju/errors"
    20  
    21  	"github.com/juju/juju/apiserver/params"
    22  )
    23  
    24  // NewBody returns an HTTP request body and content type
    25  // suitable for using to make an HTTP request containing
    26  // the given attached body data and JSON-marshaled metadata.
    27  //
    28  // The name parameter is used to identify the attached "file", so
    29  // a filename is an appropriate value.
    30  func NewBody(attached io.ReadSeeker, meta interface{}, name string) (body io.ReadSeeker, contentType string, err error) {
    31  	var parts bytes.Buffer
    32  
    33  	// Set up the multi-part portion of the body.
    34  	writer := multipart.NewWriter(&parts)
    35  
    36  	// Set the metadata part.
    37  	header := make(textproto.MIMEHeader)
    38  	header.Set("Content-Disposition", `form-data; name="metadata"`)
    39  	header.Set("Content-Type", params.ContentTypeJSON)
    40  	part, err := writer.CreatePart(header)
    41  	if err != nil {
    42  		return nil, "", errors.Trace(err)
    43  	}
    44  	if err := json.NewEncoder(part).Encode(meta); err != nil {
    45  		return nil, "", errors.Trace(err)
    46  	}
    47  
    48  	// Set the attached part.
    49  	_, err = writer.CreateFormFile("attached", name)
    50  	if err != nil {
    51  		return nil, "", errors.Trace(err)
    52  	}
    53  
    54  	// We don't actually write the reader's data to the part.
    55  	// Instead We use a chained reader to facilitate streaming
    56  	// directly from the reader.
    57  	//
    58  	// Technically this is boundary-breaking, as the knowledge of
    59  	// how to make multipart archives should be kept to the
    60  	// mime/multipart package, but doing it this way means we don't
    61  	// need to return a Writer which would be harder to turn into
    62  	// a ReadSeeker.
    63  	return newMultiReaderSeeker(
    64  		bytes.NewReader(parts.Bytes()),
    65  		attached,
    66  		strings.NewReader("\r\n--"+writer.Boundary()+"--\r\n"),
    67  	), writer.FormDataContentType(), nil
    68  }
    69  
    70  type multiReaderSeeker struct {
    71  	readers []io.ReadSeeker
    72  	index   int
    73  }
    74  
    75  // mewMultiReaderSeeker returns an io.ReadSeeker implementation that
    76  // reads from all the given readers in turn. Its Seek method can be used
    77  // to seek to the start, but returns an error if used to seek anywhere
    78  // else (this corresponds with the needs of httpbakery.Client.DoWithBody
    79  // which needs to re-read the body when retrying the request).
    80  func newMultiReaderSeeker(readers ...io.ReadSeeker) *multiReaderSeeker {
    81  	return &multiReaderSeeker{
    82  		readers: readers,
    83  	}
    84  }
    85  
    86  // Read implements io.Reader.Read.
    87  func (r *multiReaderSeeker) Read(buf []byte) (int, error) {
    88  	if r.index >= len(r.readers) {
    89  		return 0, io.EOF
    90  	}
    91  	n, err := r.readers[r.index].Read(buf)
    92  	if err == io.EOF {
    93  		r.index++
    94  		err = nil
    95  	}
    96  	return n, err
    97  }
    98  
    99  // Read implements io.Seeker.Seek. It can only be used to seek to the
   100  // start.
   101  func (r *multiReaderSeeker) Seek(offset int64, whence int) (int64, error) {
   102  	if offset != 0 || whence != 0 {
   103  		return 0, errors.New("cannot only seek to the start of multipart reader")
   104  	}
   105  	for _, reader := range r.readers {
   106  		if _, err := reader.Seek(0, 0); err != nil {
   107  			return 0, errors.Trace(err)
   108  		}
   109  	}
   110  	r.index = 0
   111  	return 0, nil
   112  }
   113  
   114  // Get extracts the attached file and its metadata from the multipart
   115  // data in the request. The metadata is JSON-unmarshaled into the value
   116  // pointed to by metaResult.
   117  func Get(req *http.Request, metaResult interface{}) (io.ReadCloser, error) {
   118  	ctype := req.Header.Get("Content-Type")
   119  	mediaType, cParams, err := mime.ParseMediaType(ctype)
   120  	if err != nil {
   121  		return nil, errors.Annotate(err, "while parsing content type header")
   122  	}
   123  
   124  	if !strings.HasPrefix(mediaType, "multipart/") {
   125  		return nil, errors.Errorf("expected multipart Content-Type, got %q", mediaType)
   126  	}
   127  	reader := multipart.NewReader(req.Body, cParams["boundary"])
   128  
   129  	// Extract the metadata.
   130  	part, err := reader.NextPart()
   131  	if err != nil {
   132  		if err == io.EOF {
   133  			return nil, errors.New("missing metadata")
   134  		}
   135  		return nil, errors.Trace(err)
   136  	}
   137  
   138  	if err := checkContentType(part.Header, params.ContentTypeJSON); err != nil {
   139  		return nil, errors.Trace(err)
   140  	}
   141  	if err := json.NewDecoder(part).Decode(metaResult); err != nil {
   142  		return nil, errors.Trace(err)
   143  	}
   144  
   145  	// Extract the archive.
   146  	part, err = reader.NextPart()
   147  	if err != nil {
   148  		if err == io.EOF {
   149  			return nil, errors.New("missing archive")
   150  		}
   151  		return nil, errors.Trace(err)
   152  	}
   153  	if err := checkContentType(part.Header, params.ContentTypeRaw); err != nil {
   154  		return nil, errors.Trace(err)
   155  	}
   156  	// We're not going to worry about verifying that the file matches the
   157  	// metadata (e.g. size, checksum).
   158  	archive := part
   159  
   160  	// We are going to trust that there aren't any more attachments after
   161  	// the file. If there are, we ignore them.
   162  
   163  	return archive, nil
   164  }
   165  
   166  func checkContentType(h textproto.MIMEHeader, expected string) error {
   167  	ctype := h.Get("Content-Type")
   168  	if ctype != expected {
   169  		return errors.Errorf("expected Content-Type %q, got %q", expected, ctype)
   170  	}
   171  	return nil
   172  }