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 }