github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/apiserver/http/request.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package http 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "io" 10 "io/ioutil" 11 "mime" 12 "mime/multipart" 13 "net/http" 14 "net/textproto" 15 "net/url" 16 "path" 17 "strings" 18 19 "github.com/juju/errors" 20 ) 21 22 // NewRequest returns a new HTTP request suitable for the API. 23 func NewRequest(method string, baseURL *url.URL, pth, uuid, tag, pw string) (*http.Request, error) { 24 baseURL.Path = path.Join("/environment", uuid, pth) 25 26 req, err := http.NewRequest(method, baseURL.String(), nil) 27 if err != nil { 28 return nil, errors.Annotate(err, "while building HTTP request") 29 } 30 31 req.SetBasicAuth(tag, pw) 32 return req, nil 33 } 34 35 // SetRequestArgs JSON-encodes the args and sets them as the request body. 36 func SetRequestArgs(req *http.Request, args interface{}) error { 37 data, err := json.Marshal(args) 38 if err != nil { 39 return errors.Annotate(err, "while serializing args") 40 } 41 42 req.Header.Set("Content-Type", CTypeJSON) 43 req.Body = ioutil.NopCloser(bytes.NewBuffer(data)) 44 return nil 45 } 46 47 // AttachToRequest attaches a reader's data to the request body as 48 // multi-part data, along with associated metadata. "name" is used to 49 // identify the attached "file", so a filename is an appropriate value. 50 func AttachToRequest(req *http.Request, attached io.Reader, meta interface{}, name string) error { 51 var parts bytes.Buffer 52 53 // Set up the multi-part portion of the body. 54 writer := multipart.NewWriter(&parts) 55 req.Header.Set("Content-Type", writer.FormDataContentType()) 56 57 // Initialize the request body. 58 req.Body = ioutil.NopCloser(io.MultiReader( 59 &parts, 60 attached, 61 bytes.NewBufferString("\r\n--"+writer.Boundary()+"--\r\n"), 62 )) 63 64 // Set the metadata part. 65 header := make(textproto.MIMEHeader) 66 header.Set("Content-Disposition", `form-data; name="metadata"`) 67 header.Set("Content-Type", CTypeJSON) 68 part, err := writer.CreatePart(header) 69 if err != nil { 70 return errors.Trace(err) 71 } 72 if err := json.NewEncoder(part).Encode(meta); err != nil { 73 return errors.Trace(err) 74 } 75 76 // Set the attached part. 77 _, err = writer.CreateFormFile("attached", name) 78 if err != nil { 79 return errors.Trace(err) 80 } 81 // We don't actually write the reader's data to the part. Instead We 82 // use a chained reader to facilitate streaming directly from the 83 // reader. 84 return nil 85 } 86 87 // ExtractRequestAttachment extracts the attached file and its metadata 88 // from the multipart data in the request. 89 func ExtractRequestAttachment(req *http.Request, metaResult interface{}) (io.ReadCloser, error) { 90 ctype := req.Header.Get("Content-Type") 91 mediaType, cParams, err := mime.ParseMediaType(ctype) 92 if err != nil { 93 return nil, errors.Annotate(err, "while parsing content type header") 94 } 95 96 if !strings.HasPrefix(mediaType, "multipart/") { 97 return nil, errors.Errorf("expected multipart Content-Type, got %q", mediaType) 98 } 99 reader := multipart.NewReader(req.Body, cParams["boundary"]) 100 101 // Extract the metadata. 102 part, err := reader.NextPart() 103 if err != nil { 104 if err == io.EOF { 105 return nil, errors.New("missing metadata") 106 } 107 return nil, errors.Trace(err) 108 } 109 110 if err := checkContentType(part.Header, CTypeJSON); err != nil { 111 return nil, errors.Trace(err) 112 } 113 if err := json.NewDecoder(part).Decode(metaResult); err != nil { 114 return nil, errors.Trace(err) 115 } 116 117 // Extract the archive. 118 part, err = reader.NextPart() 119 if err != nil { 120 if err == io.EOF { 121 return nil, errors.New("missing archive") 122 } 123 return nil, errors.Trace(err) 124 } 125 if err := checkContentType(part.Header, CTypeRaw); err != nil { 126 return nil, errors.Trace(err) 127 } 128 // We're not going to worry about verifying that the file matches the 129 // metadata (e.g. size, checksum). 130 archive := part 131 132 // We are going to trust that there aren't any more attachments after 133 // the file. If there are, we ignore them. 134 135 return archive, nil 136 } 137 138 type getter interface { 139 Get(string) string 140 } 141 142 func checkContentType(header getter, expected string) error { 143 ctype := header.Get("Content-Type") 144 if ctype != expected { 145 return errors.Errorf("expected Content-Type %q, got %q", expected, ctype) 146 } 147 return nil 148 }