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 }