Skip to content

Commit

Permalink
Merge pull request #19 from rogpeppe/021-httpbakery-updates
Browse files Browse the repository at this point in the history
httpbakery: error handling, redirection, explicit discharge
  • Loading branch information
rogpeppe committed Oct 18, 2014
2 parents d0a04be + d2714f0 commit d6445a2
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 84 deletions.
14 changes: 11 additions & 3 deletions bakery/example/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io/ioutil"
"net/http"

"github.com/juju/errgo"

"github.com/rogpeppe/macaroon/httpbakery"
)

Expand All @@ -15,15 +17,21 @@ import (
func clientRequest(serverEndpoint string) (string, error) {
req, err := http.NewRequest("GET", serverEndpoint, nil)
if err != nil {
return "", fmt.Errorf("new request error: %v", err)
return "", errgo.Notef(err, "cannot make new HTTP request")
}
// The Do function implements the mechanics
// of actually gathering discharge macaroons
// when required, and retrying the request
// when necessary.
resp, err := httpbakery.Do(httpbakery.DefaultHTTPClient, req)

visitWebPage := func(url string) error {
fmt.Printf("please visit this web page:\n")
fmt.Printf("\t%s\n", url)
return nil
}
resp, err := httpbakery.Do(httpbakery.DefaultHTTPClient, req, visitWebPage)
if err != nil {
return "", fmt.Errorf("GET failed: %v", err)
return "", errgo.NoteMask(err, "GET failed", errgo.Any)
}
defer resp.Body.Close()
// TODO(rog) unmarshal error
Expand Down
11 changes: 8 additions & 3 deletions httpbakery/caveatid.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type KeyPair struct {
// GenerateKey generates a new key pair.
func GenerateKey() (*KeyPair, error) {
var key KeyPair
priv, pub, err := box.GenerateKey(rand.Reader)
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
Expand All @@ -61,6 +61,11 @@ func GenerateKey() (*KeyPair, error) {
return &key, nil
}

// PublicKey returns the public part of the key pair.
func (key *KeyPair) PublicKey() *[32]byte {
return &key.public
}

// newCaveatIdEncoder returns a new caveatIdEncoder using key, which should
// have been created using GenerateKey.
func newCaveatIdEncoder(c *http.Client, key *KeyPair) *caveatIdEncoder {
Expand Down Expand Up @@ -231,7 +236,7 @@ func (d *caveatIdDecoder) DecodeCaveatId(id string) (rootKey []byte, condition s
}
var tpid thirdPartyCaveatId
if err := json.Unmarshal(data, &tpid); err != nil {
return nil, "", fmt.Errorf("cannot unmarshal caveat id: %v", err)
return nil, "", fmt.Errorf("cannot unmarshal caveat id %q: %v", data, err)
}
var recordData []byte

Expand Down Expand Up @@ -275,7 +280,7 @@ func (d *caveatIdDecoder) encryptedCaveatId(id thirdPartyCaveatId) ([]byte, erro
}
out, ok := box.Open(nil, sealed, &nonce, &firstPartyPublicKey, &d.key.private)
if !ok {
return nil, fmt.Errorf("decryption of public-key encrypted caveat id failed")
return nil, fmt.Errorf("decryption of public-key encrypted caveat id %#v failed", id)
}
return out, nil
}
Expand Down
77 changes: 62 additions & 15 deletions httpbakery/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@ import (
"net/http"
"net/url"

"github.com/juju/errgo"

"github.com/rogpeppe/macaroon"
)

// WaitResponse holds the type that should be returned
// by an HTTP response made to a WaitURL
// (See the ErrorInfo type).
type WaitResponse struct {
Macaroon *macaroon.Macaroon
}

// Do makes an http request to the given client.
// If the request fails with a discharge-required error,
// any required discharge macaroons will be acquired,
// and the request will be repeated with those attached.
//
// If c.Jar field is non-nil, the macaroons will be
// stored there and made available to subsequent requests.
func Do(c *http.Client, req *http.Request) (*http.Response, error) {
func Do(c *http.Client, req *http.Request, visitWebPage func(url string) error) (*http.Response, error) {
httpResp, err := c.Do(req)
if err != nil {
return nil, err
Expand All @@ -31,33 +40,65 @@ func Do(c *http.Client, req *http.Request) (*http.Response, error) {
}
defer httpResp.Body.Close()

var resp dischargeRequestedResponse
var resp Error
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return nil, fmt.Errorf("cannot unmarshal discharge-required response: %v", err)
}
if resp.ErrorCode != codeDischargeRequired {
return nil, fmt.Errorf("unexpected error code: %q", resp.ErrorCode)
return nil, errgo.Notef(err, "cannot unmarshal discharge-required response")
}
if resp.Macaroon == nil {
return nil, fmt.Errorf("no macaroon found in response")
var mac *macaroon.Macaroon
switch resp.Code {
case ErrInteractionRequired:
if resp.Info == nil {
return nil, errgo.Notef(&resp, "interaction-required response with no info")
}
if err := visitWebPage(resp.Info.VisitURL); err != nil {
return nil, errgo.Notef(err, "cannot start interactive session")
}
waitResp, err := c.Get(resp.Info.WaitURL)
if err != nil {
return nil, errgo.Notef(err, "cannot get %q: %v", resp.Info.WaitURL)
}
defer waitResp.Body.Close()
if waitResp.StatusCode != http.StatusOK {
var resp Error
if err := json.NewDecoder(waitResp.Body).Decode(&resp); err != nil {
return nil, errgo.Notef(err, "cannot unmarshal wait error response")
}
return nil, errgo.NoteMask(&resp, "failed to acquire macaroon after waiting", errgo.Any)
}
var resp WaitResponse
if err := json.NewDecoder(waitResp.Body).Decode(&resp); err != nil {
return nil, errgo.Notef(err, "cannot unmarshal wait response")
}
if resp.Macaroon == nil {
return nil, fmt.Errorf("no macaroon found in wait response")
}
mac = resp.Macaroon
case ErrDischargeRequired:
if resp.Info == nil || resp.Info.Macaroon == nil {
return nil, fmt.Errorf("no macaroon found in response")
}
mac = resp.Info.Macaroon
default:
return nil, errgo.NoteMask(&resp, fmt.Sprintf("%s %s failed", req.Method, req.URL), errgo.Any)
}
macaroons, err := dischargeMacaroon(c, resp.Macaroon)

macaroons, err := dischargeMacaroon(c, mac)
if err != nil {
return nil, err
}

// Bind the discharge macaroons to the original macaroon.
for _, m := range macaroons {
m.Bind(resp.Macaroon.Signature())
m.Bind(mac.Signature())
}
macaroons = append(macaroons, resp.Macaroon)
macaroons = append(macaroons, mac)
for _, m := range macaroons {
if err := addCookie(req, m); err != nil {
return nil, fmt.Errorf("cannot add cookie: %v", err)
}
}
// Try again with our newly acquired discharge macaroons
return c.Do(req)
hresp, err := c.Do(req)
return hresp, err
}

func addCookie(req *http.Request, m *macaroon.Macaroon) error {
Expand All @@ -84,7 +125,8 @@ func dischargeMacaroon(c *http.Client, m *macaroon.Macaroon) ([]*macaroon.Macaro
}
m, err := obtainThirdPartyDischarge(c, m.Location(), cav)
if err != nil {
return nil, fmt.Errorf("cannot obtain discharge from %q: %v", cav.Location, err)
// TODO errgo.NoteMask... "cannot obtain discharge from %q
return nil, err
}
macaroons = append(macaroons, m)
}
Expand Down Expand Up @@ -118,7 +160,12 @@ func postFormJSON(c *http.Client, url string, vals url.Values, resp interface{})
return fmt.Errorf("failed to read body from %q: %v", url, err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("POST %q failed with status %q (body %q)", url, httpResp.Status, data)
var errResp Error
if err := json.Unmarshal(data, &errResp); err != nil {
// TODO better error here
return fmt.Errorf("POST %q failed with status %q; cannot parse body %q: %v", url, httpResp.Status, data, err)
}
return &errResp
}
if err := json.Unmarshal(data, resp); err != nil {
return fmt.Errorf("cannot unmarshal response from %q: %v", url, err)
Expand Down
Loading

0 comments on commit d6445a2

Please sign in to comment.