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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmstore
     5  
     6  import (
     7  	"net/http"
     8  	"net/http/cookiejar"
     9  	"net/url"
    10  
    11  	"github.com/juju/errors"
    12  	"gopkg.in/juju/charm.v6-unstable"
    13  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    14  	"gopkg.in/macaroon.v1"
    15  )
    16  
    17  // newMacaroonJar returns a new macaroonJar wrapping the given cache and
    18  // expecting to be used against the given URL.  Both the cache and url must
    19  // be non-nil.
    20  func newMacaroonJar(cache MacaroonCache, serverURL *url.URL) (*macaroonJar, error) {
    21  	jar, err := cookiejar.New(nil)
    22  	if err != nil {
    23  		return nil, errors.Trace(err)
    24  	}
    25  	return &macaroonJar{
    26  		underlying: jar,
    27  		cache:      cache,
    28  		serverURL:  serverURL,
    29  	}, nil
    30  }
    31  
    32  // macaroonJar is a special form of http.CookieJar that uses a backing
    33  // MacaroonCache to populate the jar and store updated macaroons.
    34  // This is a fairly specifically crafted type in order to deal with the fact
    35  // that gopkg.in/juju/charmrepo.v2-unstable/csclient.Client does all the work
    36  // of handling updated macaroons.  If a request with a macaroon returns with a
    37  // DischargeRequiredError, csclient.Client will discharge the returned
    38  // macaroon's caveats, and then save the final macaroon in the cookiejar, then
    39  // retry the request.  This type handles populating the macaroon from the
    40  // macaroon cache (which for Juju's purposes will be a wrapper around state),
    41  // and then responds to csclient's setcookies call to save the new macaroon
    42  // into state for the appropriate charm.
    43  //
    44  // Note that Activate and Deactivate are not safe to call concurrently.
    45  type macaroonJar struct {
    46  	underlying   http.CookieJar
    47  	cache        MacaroonCache
    48  	serverURL    *url.URL
    49  	currentCharm *charm.URL
    50  	err          error
    51  }
    52  
    53  // Activate empties the cookiejar and loads the macaroon for the given charm
    54  // (if any) into the cookiejar, avoiding the mechanism in SetCookies
    55  // that records new macaroons.  This also enables the functionality of storing
    56  // macaroons in SetCookies.
    57  // If the macaroonJar is nil, this is NOP.
    58  func (j *macaroonJar) Activate(cURL *charm.URL) error {
    59  	if j == nil {
    60  		return nil
    61  	}
    62  	if err := j.reset(); err != nil {
    63  		return errors.Trace(err)
    64  	}
    65  	j.currentCharm = cURL
    66  
    67  	m, err := j.cache.Get(cURL)
    68  	if err != nil {
    69  		return errors.Trace(err)
    70  	}
    71  	if m != nil {
    72  		httpbakery.SetCookie(j.underlying, j.serverURL, m)
    73  	}
    74  	return nil
    75  }
    76  
    77  // Deactivate empties the cookiejar and disables the functionality of storing
    78  // macaroons in SetCookies.
    79  // If the macaroonJar is nil, this is NOP.
    80  func (j *macaroonJar) Deactivate() error {
    81  	if j == nil {
    82  		return nil
    83  	}
    84  	return j.reset()
    85  }
    86  
    87  // reset empties the cookiejar and disables the functionality of storing
    88  // macaroons in SetCookies.
    89  func (j *macaroonJar) reset() error {
    90  	j.err = nil
    91  	j.currentCharm = nil
    92  
    93  	// clear out the cookie jar to ensure we don't have any cruft left over.
    94  	underlying, err := cookiejar.New(nil)
    95  	if err != nil {
    96  		// currently this is impossible, since the above never actually
    97  		// returns an error
    98  		return errors.Trace(err)
    99  	}
   100  	j.underlying = underlying
   101  	return nil
   102  }
   103  
   104  // SetCookies handles the receipt of the cookies in a reply for the
   105  // given URL.  Cookies do not persist past a successive call to Activate or
   106  // Deactivate.  If the jar is currently activated, macaroons set via this method
   107  // will be stored in the underlying MacaroonCache for the currently activated
   108  // charm.
   109  func (j *macaroonJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
   110  	j.underlying.SetCookies(u, cookies)
   111  
   112  	if j.currentCharm == nil {
   113  		// nothing else to do
   114  		return
   115  	}
   116  
   117  	mac, err := extractMacaroon(cookies)
   118  	if err != nil {
   119  		j.err = errors.Trace(err)
   120  		logger.Errorf(err.Error())
   121  		return
   122  	}
   123  	if mac == nil {
   124  		return
   125  	}
   126  	if err := j.cache.Set(j.currentCharm, mac); err != nil {
   127  		j.err = errors.Trace(err)
   128  		logger.Errorf("Failed to store macaroon for %s: %s", j.currentCharm, err)
   129  	}
   130  }
   131  
   132  // Cookies returns the cookies stored in the underlying cookiejar.
   133  func (j macaroonJar) Cookies(u *url.URL) []*http.Cookie {
   134  	return j.underlying.Cookies(u)
   135  }
   136  
   137  // Error returns any error encountered during SetCookies.
   138  func (j *macaroonJar) Error() error {
   139  	if j == nil {
   140  		return nil
   141  	}
   142  	return j.err
   143  }
   144  
   145  func extractMacaroon(cookies []*http.Cookie) (macaroon.Slice, error) {
   146  	macs := httpbakery.MacaroonsForURL(jarFromCookies(cookies), nil)
   147  	switch len(macs) {
   148  	case 0:
   149  		// no macaroons in cookies, that's ok.
   150  		return nil, nil
   151  	case 1:
   152  		// hurray!
   153  		return macs[0], nil
   154  	default:
   155  		return nil, errors.Errorf("Expected exactly one macaroon, received %d", len(macs))
   156  	}
   157  }
   158  
   159  // jarFromCookies is a bit of sleight of hand to get the cookies we already
   160  // have to be in the form of a http.CookieJar that is suitable for using with
   161  // the httpbakery's MacaroonsForURL function, which expects to extract
   162  // the cookies from a cookiejar first.
   163  type jarFromCookies []*http.Cookie
   164  
   165  func (jarFromCookies) SetCookies(_ *url.URL, _ []*http.Cookie) {}
   166  
   167  func (j jarFromCookies) Cookies(_ *url.URL) []*http.Cookie {
   168  	return []*http.Cookie(j)
   169  }