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 ¶ms.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 }