github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/api/http.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/httprequest"
    16  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    17  	"gopkg.in/macaroon.v1"
    18  
    19  	"github.com/juju/juju/apiserver/params"
    20  )
    21  
    22  // HTTPClient implements Connection.APICaller.HTTPClient and returns an HTTP
    23  // client pointing to the API server "/model/:uuid/" path.
    24  func (s *state) HTTPClient() (*httprequest.Client, error) {
    25  	baseURL, err := s.apiEndpoint("/", "")
    26  	if err != nil {
    27  		return nil, errors.Trace(err)
    28  	}
    29  	return s.httpClient(baseURL)
    30  }
    31  
    32  // HTTPClient implements Connection.APICaller.HTTPClient and returns an HTTP
    33  // client pointing to the API server root path.
    34  func (s *state) RootHTTPClient() (*httprequest.Client, error) {
    35  	return s.httpClient(&url.URL{
    36  		Scheme: s.serverScheme,
    37  		Host:   s.Addr(),
    38  	})
    39  }
    40  
    41  func (s *state) httpClient(baseURL *url.URL) (*httprequest.Client, error) {
    42  	if !s.isLoggedIn() {
    43  		return nil, errors.New("no HTTP client available without logging in")
    44  	}
    45  	return &httprequest.Client{
    46  		BaseURL: baseURL.String(),
    47  		Doer: httpRequestDoer{
    48  			st: s,
    49  		},
    50  		UnmarshalError: unmarshalHTTPErrorResponse,
    51  	}, nil
    52  }
    53  
    54  // httpRequestDoer implements httprequest.Doer and httprequest.DoerWithBody
    55  // by using httpbakery and the state to make authenticated requests to
    56  // the API server.
    57  type httpRequestDoer struct {
    58  	st *state
    59  }
    60  
    61  var _ httprequest.Doer = httpRequestDoer{}
    62  
    63  var _ httprequest.DoerWithBody = httpRequestDoer{}
    64  
    65  // Do implements httprequest.Doer.Do.
    66  func (doer httpRequestDoer) Do(req *http.Request) (*http.Response, error) {
    67  	return doer.DoWithBody(req, nil)
    68  }
    69  
    70  // DoWithBody implements httprequest.DoerWithBody.DoWithBody.
    71  func (doer httpRequestDoer) DoWithBody(req *http.Request, body io.ReadSeeker) (*http.Response, error) {
    72  	// Add basic auth if appropriate
    73  	// Call doer.bakeryClient.DoWithBodyAndCustomError
    74  	if doer.st.tag != "" {
    75  		// Note that password may be empty here; we still
    76  		// want to pass the tag along. An empty password
    77  		// indicates that we're using macaroon authentication.
    78  		req.SetBasicAuth(doer.st.tag, doer.st.password)
    79  	}
    80  
    81  	// Set the machine nonce if it was provided.
    82  	if doer.st.nonce != "" {
    83  		req.Header.Set(params.MachineNonceHeader, doer.st.nonce)
    84  	}
    85  
    86  	// Add any explicitly-specified macaroons.
    87  	for _, ms := range doer.st.macaroons {
    88  		encoded, err := encodeMacaroonSlice(ms)
    89  		if err != nil {
    90  			return nil, errors.Trace(err)
    91  		}
    92  		req.Header.Add(httpbakery.MacaroonsHeader, encoded)
    93  	}
    94  	return doer.st.bakeryClient.DoWithBodyAndCustomError(req, body, func(resp *http.Response) error {
    95  		// At this point we are only interested in errors that
    96  		// the bakery cares about, and the CodeDischargeRequired
    97  		// error is the only one, and that always comes with a
    98  		// response code StatusUnauthorized.
    99  		if resp.StatusCode != http.StatusUnauthorized {
   100  			return nil
   101  		}
   102  		return bakeryError(unmarshalHTTPErrorResponse(resp))
   103  	})
   104  }
   105  
   106  // encodeMacaroonSlice base64-JSON-encodes a slice of macaroons.
   107  func encodeMacaroonSlice(ms macaroon.Slice) (string, error) {
   108  	data, err := json.Marshal(ms)
   109  	if err != nil {
   110  		return "", errors.Trace(err)
   111  	}
   112  	return base64.StdEncoding.EncodeToString(data), nil
   113  }
   114  
   115  // unmarshalHTTPErrorResponse unmarshals an error response from
   116  // an HTTP endpoint. For historical reasons, these endpoints
   117  // return several different incompatible error response formats.
   118  // We cope with this by accepting all of the possible formats
   119  // and unmarshaling accordingly.
   120  //
   121  // It always returns a non-nil error.
   122  func unmarshalHTTPErrorResponse(resp *http.Response) error {
   123  	var body json.RawMessage
   124  	if err := httprequest.UnmarshalJSONResponse(resp, &body); err != nil {
   125  		return errors.Trace(err)
   126  	}
   127  	// genericErrorResponse defines a struct that is compatible with all the
   128  	// known error types, so that we can know which of the
   129  	// possible error types has been returned.
   130  	//
   131  	// Another possible approach might be to look at resp.Request.URL.Path
   132  	// and determine the expected error type from that, but that
   133  	// seems more fragile than this approach.
   134  	type genericErrorResponse struct {
   135  		Error json.RawMessage `json:"error"`
   136  	}
   137  	var generic genericErrorResponse
   138  	if err := json.Unmarshal(body, &generic); err != nil {
   139  		return errors.Annotatef(err, "incompatible error response")
   140  	}
   141  	if bytes.HasPrefix(generic.Error, []byte(`"`)) {
   142  		// The error message is in a string, which means that
   143  		// the error must be in a params.CharmsResponse
   144  		var resp params.CharmsResponse
   145  		if err := json.Unmarshal(body, &resp); err != nil {
   146  			return errors.Annotatef(err, "incompatible error response")
   147  		}
   148  		return &params.Error{
   149  			Message: resp.Error,
   150  			Code:    resp.ErrorCode,
   151  			Info:    resp.ErrorInfo,
   152  		}
   153  	}
   154  	var errorBody []byte
   155  	if len(generic.Error) > 0 {
   156  		// We have an Error field, therefore the error must be in that.
   157  		// (it's a params.ErrorResponse)
   158  		errorBody = generic.Error
   159  	} else {
   160  		// There wasn't an Error field, so the error must be directly
   161  		// in the body of the response.
   162  		errorBody = body
   163  	}
   164  	var perr params.Error
   165  	if err := json.Unmarshal(errorBody, &perr); err != nil {
   166  		return errors.Annotatef(err, "incompatible error response")
   167  	}
   168  	if perr.Message == "" {
   169  		return errors.Errorf("error response with no message")
   170  	}
   171  	return &perr
   172  }
   173  
   174  // bakeryError translates any discharge-required error into
   175  // an error value that the httpbakery package will recognize.
   176  // Other errors are returned unchanged.
   177  func bakeryError(err error) error {
   178  	if params.ErrCode(err) != params.CodeDischargeRequired {
   179  		return err
   180  	}
   181  	errResp := errors.Cause(err).(*params.Error)
   182  	if errResp.Info == nil {
   183  		return errors.Annotatef(err, "no error info found in discharge-required response error")
   184  	}
   185  	// It's a discharge-required error, so make an appropriate httpbakery
   186  	// error from it.
   187  	return &httpbakery.Error{
   188  		Message: err.Error(),
   189  		Code:    httpbakery.ErrDischargeRequired,
   190  		Info: &httpbakery.ErrorInfo{
   191  			Macaroon:     errResp.Info.Macaroon,
   192  			MacaroonPath: errResp.Info.MacaroonPath,
   193  		},
   194  	}
   195  }
   196  
   197  // HTTPDoer exposes the functionality of httprequest.Client needed here.
   198  type HTTPDoer interface {
   199  	// Do sends the given request.
   200  	Do(req *http.Request, body io.ReadSeeker, resp interface{}) error
   201  }
   202  
   203  // openBlob streams the identified blob from the controller via the
   204  // provided HTTP client.
   205  func openBlob(httpClient HTTPDoer, endpoint string, args url.Values) (io.ReadCloser, error) {
   206  	apiURL, err := url.Parse(endpoint)
   207  	if err != nil {
   208  		return nil, errors.Trace(err)
   209  	}
   210  	apiURL.RawQuery = args.Encode()
   211  	req, err := http.NewRequest("GET", apiURL.String(), nil)
   212  	if err != nil {
   213  		return nil, errors.Annotate(err, "cannot create HTTP request")
   214  	}
   215  
   216  	var resp *http.Response
   217  	if err := httpClient.Do(req, nil, &resp); err != nil {
   218  		return nil, errors.Trace(err)
   219  	}
   220  	return resp.Body, nil
   221  }