727 lines
24 KiB
Go
727 lines
24 KiB
Go
package httpbakery
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/publicsuffix"
|
|
"gopkg.in/errgo.v1"
|
|
"gopkg.in/httprequest.v1"
|
|
"gopkg.in/macaroon.v2"
|
|
|
|
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
|
|
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
|
|
)
|
|
|
|
var unmarshalError = httprequest.ErrorUnmarshaler(&Error{})
|
|
|
|
// maxDischargeRetries holds the maximum number of times that an HTTP
|
|
// request will be retried after a third party caveat has been successfully
|
|
// discharged.
|
|
const maxDischargeRetries = 3
|
|
|
|
// DischargeError represents the error when a third party discharge
|
|
// is refused by a server.
|
|
type DischargeError struct {
|
|
// Reason holds the underlying remote error that caused the
|
|
// discharge to fail.
|
|
Reason *Error
|
|
}
|
|
|
|
func (e *DischargeError) Error() string {
|
|
return fmt.Sprintf("third party refused discharge: %v", e.Reason)
|
|
}
|
|
|
|
// IsDischargeError reports whether err is a *DischargeError.
|
|
func IsDischargeError(err error) bool {
|
|
_, ok := err.(*DischargeError)
|
|
return ok
|
|
}
|
|
|
|
// InteractionError wraps an error returned by a call to visitWebPage.
|
|
type InteractionError struct {
|
|
// Reason holds the actual error returned from visitWebPage.
|
|
Reason error
|
|
}
|
|
|
|
func (e *InteractionError) Error() string {
|
|
return fmt.Sprintf("cannot start interactive session: %v", e.Reason)
|
|
}
|
|
|
|
// IsInteractionError reports whether err is an *InteractionError.
|
|
func IsInteractionError(err error) bool {
|
|
_, ok := err.(*InteractionError)
|
|
return ok
|
|
}
|
|
|
|
// NewHTTPClient returns an http.Client that ensures
|
|
// that headers are sent to the server even when the
|
|
// server redirects a GET request. The returned client
|
|
// also contains an empty in-memory cookie jar.
|
|
//
|
|
// See https://github.com/golang/go/issues/4677
|
|
func NewHTTPClient() *http.Client {
|
|
c := *http.DefaultClient
|
|
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
if len(via) >= 10 {
|
|
return fmt.Errorf("too many redirects")
|
|
}
|
|
if len(via) == 0 {
|
|
return nil
|
|
}
|
|
for attr, val := range via[0].Header {
|
|
if attr == "Cookie" {
|
|
// Cookies are added automatically anyway.
|
|
continue
|
|
}
|
|
if _, ok := req.Header[attr]; !ok {
|
|
req.Header[attr] = val
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
jar, err := cookiejar.New(&cookiejar.Options{
|
|
PublicSuffixList: publicsuffix.List,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
c.Jar = jar
|
|
return &c
|
|
}
|
|
|
|
// Client holds the context for making HTTP requests
|
|
// that automatically acquire and discharge macaroons.
|
|
type Client struct {
|
|
// Client holds the HTTP client to use. It should have a cookie
|
|
// jar configured, and when redirecting it should preserve the
|
|
// headers (see NewHTTPClient).
|
|
*http.Client
|
|
|
|
// InteractionMethods holds a slice of supported interaction
|
|
// methods, with preferred methods earlier in the slice.
|
|
// On receiving an interaction-required error when discharging,
|
|
// the Kind method of each Interactor in turn will be called
|
|
// and, if the error indicates that the interaction kind is supported,
|
|
// the Interact method will be called to complete the discharge.
|
|
InteractionMethods []Interactor
|
|
|
|
// Key holds the client's key. If set, the client will try to
|
|
// discharge third party caveats with the special location
|
|
// "local" by using this key. See bakery.DischargeAllWithKey and
|
|
// bakery.LocalThirdPartyCaveat for more information
|
|
Key *bakery.KeyPair
|
|
|
|
// Logger is used to log information about client activities.
|
|
// If it is nil, bakery.DefaultLogger("httpbakery") will be used.
|
|
Logger bakery.Logger
|
|
}
|
|
|
|
// An Interactor represents a way of persuading a discharger
|
|
// that it should grant a discharge macaroon.
|
|
type Interactor interface {
|
|
// Kind returns the interaction method name. This corresponds to the
|
|
// key in the Error.InteractionMethods type.
|
|
Kind() string
|
|
|
|
// Interact performs the interaction, and returns a token that can be
|
|
// used to acquire the discharge macaroon. The location provides
|
|
// the third party caveat location to make it possible to use
|
|
// relative URLs.
|
|
//
|
|
// If the given interaction isn't supported by the client for
|
|
// the given location, it may return an error with an
|
|
// ErrInteractionMethodNotFound cause which will cause the
|
|
// interactor to be ignored that time.
|
|
Interact(ctx context.Context, client *Client, location string, interactionRequiredErr *Error) (*DischargeToken, error)
|
|
}
|
|
|
|
// DischargeToken holds a token that is intended
|
|
// to persuade a discharger to discharge a third
|
|
// party caveat.
|
|
type DischargeToken struct {
|
|
// Kind holds the kind of the token. By convention this
|
|
// matches the name of the interaction method used to
|
|
// obtain the token, but that's not required.
|
|
Kind string `json:"kind"`
|
|
|
|
// Value holds the value of the token.
|
|
Value []byte `json:"value"`
|
|
}
|
|
|
|
// LegacyInteractor may optionally be implemented by Interactor
|
|
// implementations that implement the legacy interaction-required
|
|
// error protocols.
|
|
type LegacyInteractor interface {
|
|
// LegacyInteract implements the "visit" half of a legacy discharge
|
|
// interaction. The "wait" half will be implemented by httpbakery.
|
|
// The location is the location specified by the third party
|
|
// caveat.
|
|
LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error
|
|
}
|
|
|
|
// NewClient returns a new Client containing an HTTP client
|
|
// created with NewHTTPClient and leaves all other fields zero.
|
|
func NewClient() *Client {
|
|
return &Client{
|
|
Client: NewHTTPClient(),
|
|
}
|
|
}
|
|
|
|
// AddInteractor is a convenience method that appends the given
|
|
// interactor to c.InteractionMethods.
|
|
// For example, to enable web-browser interaction on
|
|
// a client c, do:
|
|
//
|
|
// c.AddInteractor(httpbakery.WebBrowserWindowInteractor)
|
|
func (c *Client) AddInteractor(i Interactor) {
|
|
c.InteractionMethods = append(c.InteractionMethods, i)
|
|
}
|
|
|
|
// DischargeAll attempts to acquire discharge macaroons for all the
|
|
// third party caveats in m, and returns a slice containing all
|
|
// of them bound to m.
|
|
//
|
|
// If the discharge fails because a third party refuses to discharge a
|
|
// caveat, the returned error will have a cause of type *DischargeError.
|
|
// If the discharge fails because visitWebPage returns an error,
|
|
// the returned error will have a cause of *InteractionError.
|
|
//
|
|
// The returned macaroon slice will not be stored in the client
|
|
// cookie jar (see SetCookie if you need to do that).
|
|
func (c *Client) DischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) {
|
|
return bakery.DischargeAllWithKey(ctx, m, c.AcquireDischarge, c.Key)
|
|
}
|
|
|
|
// DischargeAllUnbound is like DischargeAll except that it does not
|
|
// bind the resulting macaroons.
|
|
func (c *Client) DischargeAllUnbound(ctx context.Context, ms bakery.Slice) (bakery.Slice, error) {
|
|
return ms.DischargeAll(ctx, c.AcquireDischarge, c.Key)
|
|
}
|
|
|
|
// Do is like DoWithContext, except the context is automatically derived.
|
|
// If using go version 1.7 or later the context will be taken from the
|
|
// given request, otherwise context.Background() will be used.
|
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|
return c.do(contextFromRequest(req), req, nil)
|
|
}
|
|
|
|
// DoWithContext sends the given HTTP request and returns its response.
|
|
// 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 the required discharges were refused by a third party, an error
|
|
// with a *DischargeError cause will be returned.
|
|
//
|
|
// If interaction is required by the user, the client's InteractionMethods
|
|
// will be used to perform interaction. An error
|
|
// with a *InteractionError cause will be returned if this interaction
|
|
// fails. See WebBrowserWindowInteractor for a possible implementation of
|
|
// an Interactor for an interaction method.
|
|
//
|
|
// DoWithContext may add headers to req.Header.
|
|
func (c *Client) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) {
|
|
return c.do(ctx, req, nil)
|
|
}
|
|
|
|
// DoWithCustomError is like Do except it allows a client
|
|
// to specify a custom error function, getError, which is called on the
|
|
// HTTP response and may return a non-nil error if the response holds an
|
|
// error. If the cause of the returned error is a *Error value and its
|
|
// code is ErrDischargeRequired, the macaroon in its Info field will be
|
|
// discharged and the request will be repeated with the discharged
|
|
// macaroon. If getError returns nil, it should leave the response body
|
|
// unchanged.
|
|
//
|
|
// If getError is nil, DefaultGetError will be used.
|
|
//
|
|
// This method can be useful when dealing with APIs that
|
|
// return their errors in a format incompatible with Error, but the
|
|
// need for it should be avoided when creating new APIs,
|
|
// as it makes the endpoints less amenable to generic tools.
|
|
func (c *Client) DoWithCustomError(req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
|
|
return c.do(contextFromRequest(req), req, getError)
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
|
|
c.logDebugf(ctx, "client do %s %s {", req.Method, req.URL)
|
|
resp, err := c.do1(ctx, req, getError)
|
|
c.logDebugf(ctx, "} -> error %#v", err)
|
|
return resp, err
|
|
}
|
|
|
|
func (c *Client) do1(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
|
|
if getError == nil {
|
|
getError = DefaultGetError
|
|
}
|
|
if c.Client.Jar == nil {
|
|
return nil, errgo.New("no cookie jar supplied in HTTP client")
|
|
}
|
|
rreq, ok := newRetryableRequest(c.Client, req)
|
|
if !ok {
|
|
return nil, fmt.Errorf("request body is not seekable")
|
|
}
|
|
defer rreq.close()
|
|
|
|
req.Header.Set(BakeryProtocolHeader, fmt.Sprint(bakery.LatestVersion))
|
|
|
|
// Make several attempts to do the request, because we might have
|
|
// to get through several layers of security. We only retry if
|
|
// we get a DischargeRequiredError and succeed in discharging
|
|
// the macaroon in it.
|
|
retry := 0
|
|
for {
|
|
resp, err := c.do2(ctx, rreq, getError)
|
|
if err == nil || !isDischargeRequiredError(err) {
|
|
return resp, errgo.Mask(err, errgo.Any)
|
|
}
|
|
if retry++; retry > maxDischargeRetries {
|
|
return nil, errgo.NoteMask(err, fmt.Sprintf("too many (%d) discharge requests", retry-1), errgo.Any)
|
|
}
|
|
if err1 := c.HandleError(ctx, req.URL, err); err1 != nil {
|
|
return nil, errgo.Mask(err1, errgo.Any)
|
|
}
|
|
c.logDebugf(ctx, "discharge succeeded; retry %d", retry)
|
|
}
|
|
}
|
|
|
|
func (c *Client) do2(ctx context.Context, rreq *retryableRequest, getError func(resp *http.Response) error) (*http.Response, error) {
|
|
httpResp, err := rreq.do(ctx)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err, errgo.Any)
|
|
}
|
|
err = getError(httpResp)
|
|
if err == nil {
|
|
c.logInfof(ctx, "HTTP response OK (status %v)", httpResp.Status)
|
|
return httpResp, nil
|
|
}
|
|
httpResp.Body.Close()
|
|
return nil, errgo.Mask(err, errgo.Any)
|
|
}
|
|
|
|
// HandleError tries to resolve the given error, which should be a
|
|
// response to the given URL, by discharging any macaroon contained in
|
|
// it. That is, if the error cause is an *Error and its code is
|
|
// ErrDischargeRequired, then it will try to discharge
|
|
// err.Info.Macaroon. If the discharge succeeds, the discharged macaroon
|
|
// will be saved to the client's cookie jar and ResolveError will return
|
|
// nil.
|
|
//
|
|
// For any other kind of error, the original error will be returned.
|
|
func (c *Client) HandleError(ctx context.Context, reqURL *url.URL, err error) error {
|
|
respErr, ok := errgo.Cause(err).(*Error)
|
|
if !ok {
|
|
return err
|
|
}
|
|
if respErr.Code != ErrDischargeRequired {
|
|
return respErr
|
|
}
|
|
if respErr.Info == nil || respErr.Info.Macaroon == nil {
|
|
return errgo.New("no macaroon found in discharge-required response")
|
|
}
|
|
mac := respErr.Info.Macaroon
|
|
macaroons, err := bakery.DischargeAllWithKey(ctx, mac, c.AcquireDischarge, c.Key)
|
|
if err != nil {
|
|
return errgo.Mask(err, errgo.Any)
|
|
}
|
|
var cookiePath string
|
|
if path := respErr.Info.MacaroonPath; path != "" {
|
|
relURL, err := parseURLPath(path)
|
|
if err != nil {
|
|
c.logInfof(ctx, "ignoring invalid path in discharge-required response: %v", err)
|
|
} else {
|
|
cookiePath = reqURL.ResolveReference(relURL).Path
|
|
}
|
|
}
|
|
// TODO use a namespace taken from the error response.
|
|
cookie, err := NewCookie(nil, macaroons)
|
|
if err != nil {
|
|
return errgo.Notef(err, "cannot make cookie")
|
|
}
|
|
cookie.Path = cookiePath
|
|
if name := respErr.Info.CookieNameSuffix; name != "" {
|
|
cookie.Name = "macaroon-" + name
|
|
}
|
|
c.Jar.SetCookies(reqURL, []*http.Cookie{cookie})
|
|
return nil
|
|
}
|
|
|
|
// DefaultGetError is the default error unmarshaler used by Client.Do.
|
|
func DefaultGetError(httpResp *http.Response) error {
|
|
if httpResp.StatusCode != http.StatusProxyAuthRequired && httpResp.StatusCode != http.StatusUnauthorized {
|
|
return nil
|
|
}
|
|
// Check for the new protocol discharge error.
|
|
if httpResp.StatusCode == http.StatusUnauthorized && httpResp.Header.Get("WWW-Authenticate") != "Macaroon" {
|
|
return nil
|
|
}
|
|
if httpResp.Header.Get("Content-Type") != "application/json" {
|
|
return nil
|
|
}
|
|
var resp Error
|
|
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
|
return fmt.Errorf("cannot unmarshal error response: %v", err)
|
|
}
|
|
return &resp
|
|
}
|
|
|
|
func parseURLPath(path string) (*url.URL, error) {
|
|
u, err := url.Parse(path)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
if u.Scheme != "" ||
|
|
u.Opaque != "" ||
|
|
u.User != nil ||
|
|
u.Host != "" ||
|
|
u.RawQuery != "" ||
|
|
u.Fragment != "" {
|
|
return nil, errgo.Newf("URL path %q is not clean", path)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// PermanentExpiryDuration holds the length of time a cookie
|
|
// holding a macaroon with no time-before caveat will be
|
|
// stored.
|
|
const PermanentExpiryDuration = 100 * 365 * 24 * time.Hour
|
|
|
|
// NewCookie takes a slice of macaroons and returns them
|
|
// encoded as a cookie. The slice should contain a single primary
|
|
// macaroon in its first element, and any discharges after that.
|
|
//
|
|
// The given namespace specifies the first party caveat namespace,
|
|
// used for deriving the expiry time of the cookie.
|
|
func NewCookie(ns *checkers.Namespace, ms macaroon.Slice) (*http.Cookie, error) {
|
|
if len(ms) == 0 {
|
|
return nil, errgo.New("no macaroons in cookie")
|
|
}
|
|
// TODO(rog) marshal cookie as binary if version allows.
|
|
data, err := json.Marshal(ms)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "cannot marshal macaroons")
|
|
}
|
|
cookie := &http.Cookie{
|
|
Name: fmt.Sprintf("macaroon-%x", ms[0].Signature()),
|
|
Value: base64.StdEncoding.EncodeToString(data),
|
|
}
|
|
expires, found := checkers.MacaroonsExpiryTime(ns, ms)
|
|
if !found {
|
|
// The macaroon doesn't expire - use a very long expiry
|
|
// time for the cookie.
|
|
expires = time.Now().Add(PermanentExpiryDuration)
|
|
} else if expires.Sub(time.Now()) < time.Minute {
|
|
// The macaroon might have expired already, or it's
|
|
// got a short duration, so treat it as a session cookie
|
|
// by setting Expires to the zero time.
|
|
expires = time.Time{}
|
|
}
|
|
cookie.Expires = expires
|
|
// TODO(rog) other fields.
|
|
return cookie, nil
|
|
}
|
|
|
|
// SetCookie sets a cookie for the given URL on the given cookie jar
|
|
// that will holds the given macaroon slice. The macaroon slice should
|
|
// contain a single primary macaroon in its first element, and any
|
|
// discharges after that.
|
|
//
|
|
// The given namespace specifies the first party caveat namespace,
|
|
// used for deriving the expiry time of the cookie.
|
|
func SetCookie(jar http.CookieJar, url *url.URL, ns *checkers.Namespace, ms macaroon.Slice) error {
|
|
cookie, err := NewCookie(ns, ms)
|
|
if err != nil {
|
|
return errgo.Mask(err)
|
|
}
|
|
jar.SetCookies(url, []*http.Cookie{cookie})
|
|
return nil
|
|
}
|
|
|
|
// MacaroonsForURL returns any macaroons associated with the
|
|
// given URL in the given cookie jar.
|
|
func MacaroonsForURL(jar http.CookieJar, u *url.URL) []macaroon.Slice {
|
|
return cookiesToMacaroons(jar.Cookies(u))
|
|
}
|
|
|
|
func appendURLElem(u, elem string) string {
|
|
if strings.HasSuffix(u, "/") {
|
|
return u + elem
|
|
}
|
|
return u + "/" + elem
|
|
}
|
|
|
|
// AcquireDischarge acquires a discharge macaroon from the caveat location as an HTTP URL.
|
|
// It fits the getDischarge argument type required by bakery.DischargeAll.
|
|
func (c *Client) AcquireDischarge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) {
|
|
m, err := c.acquireDischarge(ctx, cav, payload, nil)
|
|
if err == nil {
|
|
return m, nil
|
|
}
|
|
cause, ok := errgo.Cause(err).(*Error)
|
|
if !ok {
|
|
return nil, errgo.NoteMask(err, "cannot acquire discharge", IsInteractionError)
|
|
}
|
|
if cause.Code != ErrInteractionRequired {
|
|
return nil, &DischargeError{
|
|
Reason: cause,
|
|
}
|
|
}
|
|
if cause.Info == nil {
|
|
return nil, errgo.Notef(err, "interaction-required response with no info")
|
|
}
|
|
// Make sure the location has a trailing slash so that
|
|
// the relative URL calculations work correctly even when
|
|
// cav.Location doesn't have a trailing slash.
|
|
loc := appendURLElem(cav.Location, "")
|
|
token, m, err := c.interact(ctx, loc, cause, payload)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
|
|
}
|
|
if m != nil {
|
|
// We've acquired the macaroon directly via legacy interaction.
|
|
return m, nil
|
|
}
|
|
|
|
// Try to acquire the discharge again, but this time with
|
|
// the token acquired by the interaction method.
|
|
m, err = c.acquireDischarge(ctx, cav, payload, token)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// acquireDischarge is like AcquireDischarge except that it also
|
|
// takes a token acquired from an interaction method.
|
|
func (c *Client) acquireDischarge(
|
|
ctx context.Context,
|
|
cav macaroon.Caveat,
|
|
payload []byte,
|
|
token *DischargeToken,
|
|
) (*bakery.Macaroon, error) {
|
|
dclient := newDischargeClient(cav.Location, c)
|
|
var req dischargeRequest
|
|
req.Id, req.Id64 = maybeBase64Encode(cav.Id)
|
|
if token != nil {
|
|
req.Token, req.Token64 = maybeBase64Encode(token.Value)
|
|
req.TokenKind = token.Kind
|
|
}
|
|
req.Caveat = base64.RawURLEncoding.EncodeToString(payload)
|
|
resp, err := dclient.Discharge(ctx, &req)
|
|
if err == nil {
|
|
return resp.Macaroon, nil
|
|
}
|
|
return nil, errgo.Mask(err, errgo.Any)
|
|
}
|
|
|
|
// interact gathers a macaroon by directing the user to interact with a
|
|
// web page. The irErr argument holds the interaction-required
|
|
// error response.
|
|
func (c *Client) interact(ctx context.Context, location string, irErr *Error, payload []byte) (*DischargeToken, *bakery.Macaroon, error) {
|
|
if len(c.InteractionMethods) == 0 {
|
|
return nil, nil, &InteractionError{
|
|
Reason: errgo.New("interaction required but not possible"),
|
|
}
|
|
}
|
|
if irErr.Info.InteractionMethods == nil && irErr.Info.LegacyVisitURL != "" {
|
|
// It's an old-style error; deal with it differently.
|
|
m, err := c.legacyInteract(ctx, location, irErr)
|
|
if err != nil {
|
|
return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
|
|
}
|
|
return nil, m, nil
|
|
}
|
|
for _, interactor := range c.InteractionMethods {
|
|
c.logDebugf(ctx, "checking interaction method %q", interactor.Kind())
|
|
if _, ok := irErr.Info.InteractionMethods[interactor.Kind()]; ok {
|
|
c.logDebugf(ctx, "found possible interaction method %q", interactor.Kind())
|
|
token, err := interactor.Interact(ctx, c, location, irErr)
|
|
if err != nil {
|
|
if errgo.Cause(err) == ErrInteractionMethodNotFound {
|
|
continue
|
|
}
|
|
return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
|
|
}
|
|
if token == nil {
|
|
return nil, nil, errgo.New("interaction method returned an empty token")
|
|
}
|
|
return token, nil, nil
|
|
} else {
|
|
c.logDebugf(ctx, "interaction method %q not found in %#v", interactor.Kind(), irErr.Info.InteractionMethods)
|
|
}
|
|
}
|
|
return nil, nil, &InteractionError{
|
|
Reason: errgo.Newf("no supported interaction method"),
|
|
}
|
|
}
|
|
|
|
func (c *Client) legacyInteract(ctx context.Context, location string, irErr *Error) (*bakery.Macaroon, error) {
|
|
visitURL, err := relativeURL(location, irErr.Info.LegacyVisitURL)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
waitURL, err := relativeURL(location, irErr.Info.LegacyWaitURL)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
methodURLs := map[string]*url.URL{
|
|
"interactive": visitURL,
|
|
}
|
|
if len(c.InteractionMethods) > 1 || c.InteractionMethods[0].Kind() != WebBrowserInteractionKind {
|
|
// We have several possible methods or we only support a non-window
|
|
// method, so we need to fetch the possible methods supported by the discharger.
|
|
methodURLs = legacyGetInteractionMethods(ctx, c.logger(), c, visitURL)
|
|
}
|
|
for _, interactor := range c.InteractionMethods {
|
|
kind := interactor.Kind()
|
|
if kind == WebBrowserInteractionKind {
|
|
// This is the old name for browser-window interaction.
|
|
kind = "interactive"
|
|
}
|
|
interactor, ok := interactor.(LegacyInteractor)
|
|
if !ok {
|
|
// Legacy interaction mode isn't supported.
|
|
continue
|
|
}
|
|
visitURL, ok := methodURLs[kind]
|
|
if !ok {
|
|
continue
|
|
}
|
|
visitURL, err := relativeURL(location, visitURL.String())
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
if err := interactor.LegacyInteract(ctx, c, location, visitURL); err != nil {
|
|
return nil, &InteractionError{
|
|
Reason: errgo.Mask(err, errgo.Any),
|
|
}
|
|
}
|
|
return waitForMacaroon(ctx, c, waitURL)
|
|
}
|
|
return nil, &InteractionError{
|
|
Reason: errgo.Newf("no methods supported"),
|
|
}
|
|
}
|
|
|
|
func (c *Client) logDebugf(ctx context.Context, f string, a ...interface{}) {
|
|
c.logger().Debugf(ctx, f, a...)
|
|
}
|
|
|
|
func (c *Client) logInfof(ctx context.Context, f string, a ...interface{}) {
|
|
c.logger().Infof(ctx, f, a...)
|
|
}
|
|
|
|
func (c *Client) logger() bakery.Logger {
|
|
if c.Logger != nil {
|
|
return c.Logger
|
|
}
|
|
return bakery.DefaultLogger("httpbakery")
|
|
}
|
|
|
|
// waitForMacaroon returns a macaroon from a legacy wait endpoint.
|
|
func waitForMacaroon(ctx context.Context, client *Client, waitURL *url.URL) (*bakery.Macaroon, error) {
|
|
req, err := http.NewRequest("GET", waitURL.String(), nil)
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
req = req.WithContext(ctx)
|
|
httpResp, err := client.Client.Do(req)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "cannot get %q", waitURL)
|
|
}
|
|
defer httpResp.Body.Close()
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
err := unmarshalError(httpResp)
|
|
if err1, ok := err.(*Error); ok {
|
|
err = &DischargeError{
|
|
Reason: err1,
|
|
}
|
|
}
|
|
return nil, errgo.NoteMask(err, "failed to acquire macaroon after waiting", errgo.Any)
|
|
}
|
|
var resp WaitResponse
|
|
if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil {
|
|
return nil, errgo.Notef(err, "cannot unmarshal wait response")
|
|
}
|
|
return resp.Macaroon, nil
|
|
}
|
|
|
|
// relativeURL returns newPath relative to an original URL.
|
|
func relativeURL(base, new string) (*url.URL, error) {
|
|
if new == "" {
|
|
return nil, errgo.Newf("empty URL")
|
|
}
|
|
baseURL, err := url.Parse(base)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "cannot parse URL")
|
|
}
|
|
newURL, err := url.Parse(new)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "cannot parse URL")
|
|
}
|
|
return baseURL.ResolveReference(newURL), nil
|
|
}
|
|
|
|
// TODO(rog) move a lot of the code below into server.go, as it's
|
|
// much more about server side than client side.
|
|
|
|
// MacaroonsHeader is the key of the HTTP header that can be used to provide a
|
|
// macaroon for request authorization.
|
|
const MacaroonsHeader = "Macaroons"
|
|
|
|
// RequestMacaroons returns any collections of macaroons from the header and
|
|
// cookies found in the request. By convention, each slice will contain a
|
|
// primary macaroon followed by its discharges.
|
|
func RequestMacaroons(req *http.Request) []macaroon.Slice {
|
|
mss := cookiesToMacaroons(req.Cookies())
|
|
for _, h := range req.Header[MacaroonsHeader] {
|
|
ms, err := decodeMacaroonSlice(h)
|
|
if err != nil {
|
|
// Ignore invalid macaroons.
|
|
continue
|
|
}
|
|
mss = append(mss, ms)
|
|
}
|
|
return mss
|
|
}
|
|
|
|
// cookiesToMacaroons returns a slice of any macaroons found
|
|
// in the given slice of cookies.
|
|
func cookiesToMacaroons(cookies []*http.Cookie) []macaroon.Slice {
|
|
var mss []macaroon.Slice
|
|
for _, cookie := range cookies {
|
|
if !strings.HasPrefix(cookie.Name, "macaroon-") {
|
|
continue
|
|
}
|
|
ms, err := decodeMacaroonSlice(cookie.Value)
|
|
if err != nil {
|
|
// Ignore invalid macaroons.
|
|
continue
|
|
}
|
|
mss = append(mss, ms)
|
|
}
|
|
return mss
|
|
}
|
|
|
|
// decodeMacaroonSlice decodes a base64-JSON-encoded slice of macaroons from
|
|
// the given string.
|
|
func decodeMacaroonSlice(value string) (macaroon.Slice, error) {
|
|
data, err := macaroon.Base64Decode([]byte(value))
|
|
if err != nil {
|
|
return nil, errgo.NoteMask(err, "cannot base64-decode macaroons")
|
|
}
|
|
// TODO(rog) accept binary encoded macaroon cookies.
|
|
var ms macaroon.Slice
|
|
if err := json.Unmarshal(data, &ms); err != nil {
|
|
return nil, errgo.NoteMask(err, "cannot unmarshal macaroons")
|
|
}
|
|
return ms, nil
|
|
}
|