1package sign 2 3import ( 4 "crypto/rsa" 5 "fmt" 6 "net/http" 7 "strings" 8 "time" 9) 10 11const ( 12 // CookiePolicyName name of the policy cookie 13 CookiePolicyName = "CloudFront-Policy" 14 // CookieSignatureName name of the signature cookie 15 CookieSignatureName = "CloudFront-Signature" 16 // CookieKeyIDName name of the signing Key ID cookie 17 CookieKeyIDName = "CloudFront-Key-Pair-Id" 18) 19 20// A CookieOptions optional additional options that can be applied to the signed 21// cookies. 22type CookieOptions struct { 23 Path string 24 Domain string 25 Secure bool 26} 27 28// apply will integration the options provided into the base cookie options 29// a new copy will be returned. The base CookieOption will not be modified. 30func (o CookieOptions) apply(opts ...func(*CookieOptions)) CookieOptions { 31 if len(opts) == 0 { 32 return o 33 } 34 35 for _, opt := range opts { 36 opt(&o) 37 } 38 39 return o 40} 41 42// A CookieSigner provides signing utilities to sign Cookies for Amazon CloudFront 43// resources. Using a private key and Credential Key Pair key ID the CookieSigner 44// only needs to be created once per Credential Key Pair key ID and private key. 45// 46// More information about signed Cookies and their structure can be found at: 47// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html 48// 49// To sign a Cookie, create a CookieSigner with your private key and credential 50// pair key ID. Once you have a CookieSigner instance you can call Sign or 51// SignWithPolicy to sign the URLs. 52// 53// The signer is safe to use concurrently, but the optional cookies options 54// are not safe to modify concurrently. 55type CookieSigner struct { 56 keyID string 57 privKey *rsa.PrivateKey 58 59 Opts CookieOptions 60} 61 62// NewCookieSigner constructs and returns a new CookieSigner to be used to for 63// signing Amazon CloudFront URL resources with. 64func NewCookieSigner(keyID string, privKey *rsa.PrivateKey, opts ...func(*CookieOptions)) *CookieSigner { 65 signer := &CookieSigner{ 66 keyID: keyID, 67 privKey: privKey, 68 Opts: CookieOptions{}.apply(opts...), 69 } 70 71 return signer 72} 73 74// Sign returns the cookies needed to allow user agents to make arbetrary 75// requests to cloudfront for the resource(s) defined by the policy. 76// 77// Sign will create a CloudFront policy with only a resource and condition of 78// DateLessThan equal to the expires time provided. 79// 80// The returned slice cookies should all be added to the Client's cookies or 81// server's response. 82// 83// Example: 84// s := sign.NewCookieSigner(keyID, privKey) 85// 86// // Get Signed cookies for a resource that will expire in 1 hour 87// cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour)) 88// if err != nil { 89// fmt.Println("failed to create signed cookies", err) 90// return 91// } 92// 93// // Or get Signed cookies for a resource that will expire in 1 hour 94// // and set path and domain of cookies 95// cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour), func(o *sign.CookieOptions) { 96// o.Path = "/" 97// o.Domain = ".example.com" 98// }) 99// if err != nil { 100// fmt.Println("failed to create signed cookies", err) 101// return 102// } 103// 104// // Server Response via http.ResponseWriter 105// for _, c := range cookies { 106// http.SetCookie(w, c) 107// } 108// 109// // Client request via the cookie jar 110// if client.CookieJar != nil { 111// for _, c := range cookies { 112// client.Cookie(w, c) 113// } 114// } 115func (s CookieSigner) Sign(u string, expires time.Time, opts ...func(*CookieOptions)) ([]*http.Cookie, error) { 116 scheme, err := cookieURLScheme(u) 117 if err != nil { 118 return nil, err 119 } 120 121 resource, err := CreateResource(scheme, u) 122 if err != nil { 123 return nil, err 124 } 125 126 p := NewCannedPolicy(resource, expires) 127 return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...)) 128} 129 130// Returns and validates the URL's scheme. 131// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-cookies 132func cookieURLScheme(u string) (string, error) { 133 parts := strings.SplitN(u, "://", 2) 134 if len(parts) != 2 { 135 return "", fmt.Errorf("invalid cookie URL, missing scheme") 136 } 137 138 scheme := strings.ToLower(parts[0]) 139 if scheme != "http" && scheme != "https" && scheme != "http*" { 140 return "", fmt.Errorf("invalid cookie URL scheme. Expect http, https, or http*. Go, %s", scheme) 141 } 142 143 return scheme, nil 144} 145 146// SignWithPolicy returns the cookies needed to allow user agents to make 147// arbetrairy requets to cloudfront for the resource(s) defined by the policy. 148// 149// The returned slice cookies should all be added to the Client's cookies or 150// server's response. 151// 152// Example: 153// s := sign.NewCookieSigner(keyID, privKey) 154// 155// policy := &sign.Policy{ 156// Statements: []sign.Statement{ 157// { 158// // Read the provided documentation on how to set this 159// // correctly, you'll probably want to use wildcards. 160// Resource: rawCloudFrontURL, 161// Condition: sign.Condition{ 162// // Optional IP source address range 163// IPAddress: &sign.IPAddress{SourceIP: "192.0.2.0/24"}, 164// // Optional date URL is not valid until 165// DateGreaterThan: &sign.AWSEpochTime{time.Now().Add(30 * time.Minute)}, 166// // Required date the URL will expire after 167// DateLessThan: &sign.AWSEpochTime{time.Now().Add(1 * time.Hour)}, 168// }, 169// }, 170// }, 171// } 172// 173// // Get Signed cookies for a resource that will expire in 1 hour 174// cookies, err := s.SignWithPolicy(policy) 175// if err != nil { 176// fmt.Println("failed to create signed cookies", err) 177// return 178// } 179// 180// // Or get Signed cookies for a resource that will expire in 1 hour 181// // and set path and domain of cookies 182// cookies, err := s.SignWithPolicy(policy, func(o *sign.CookieOptions) { 183// o.Path = "/" 184// o.Domain = ".example.com" 185// }) 186// if err != nil { 187// fmt.Println("failed to create signed cookies", err) 188// return 189// } 190// 191// // Server Response via http.ResponseWriter 192// for _, c := range cookies { 193// http.SetCookie(w, c) 194// } 195// 196// // Client request via the cookie jar 197// if client.CookieJar != nil { 198// for _, c := range cookies { 199// client.Cookie(w, c) 200// } 201// } 202func (s CookieSigner) SignWithPolicy(p *Policy, opts ...func(*CookieOptions)) ([]*http.Cookie, error) { 203 return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...)) 204} 205 206// Prepares the cookies to be attached to the header. An (optional) options 207// struct is provided in case people don't want to manually edit their cookies. 208func createCookies(p *Policy, keyID string, privKey *rsa.PrivateKey, opt CookieOptions) ([]*http.Cookie, error) { 209 b64Sig, b64Policy, err := p.Sign(privKey) 210 if err != nil { 211 return nil, err 212 } 213 214 // Creates proper cookies 215 cPolicy := &http.Cookie{ 216 Name: CookiePolicyName, 217 Value: string(b64Policy), 218 HttpOnly: true, 219 } 220 cSignature := &http.Cookie{ 221 Name: CookieSignatureName, 222 Value: string(b64Sig), 223 HttpOnly: true, 224 } 225 cKey := &http.Cookie{ 226 Name: CookieKeyIDName, 227 Value: keyID, 228 HttpOnly: true, 229 } 230 231 cookies := []*http.Cookie{cPolicy, cSignature, cKey} 232 233 // Applie the cookie options 234 for _, c := range cookies { 235 c.Path = opt.Path 236 c.Domain = opt.Domain 237 c.Secure = opt.Secure 238 } 239 240 return cookies, nil 241} 242