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  }