package sign import ( "crypto/rsa" "fmt" "net/http" "strings" "time" ) const ( // CookiePolicyName name of the policy cookie CookiePolicyName = "CloudFront-Policy" // CookieSignatureName name of the signature cookie CookieSignatureName = "CloudFront-Signature" // CookieKeyIDName name of the signing Key ID cookie CookieKeyIDName = "CloudFront-Key-Pair-Id" ) // A CookieOptions optional additional options that can be applied to the signed // cookies. type CookieOptions struct { Path string Domain string Secure bool } // apply will integration the options provided into the base cookie options // a new copy will be returned. The base CookieOption will not be modified. func (o CookieOptions) apply(opts ...func(*CookieOptions)) CookieOptions { if len(opts) == 0 { return o } for _, opt := range opts { opt(&o) } return o } // A CookieSigner provides signing utilities to sign Cookies for Amazon CloudFront // resources. Using a private key and Credential Key Pair key ID the CookieSigner // only needs to be created once per Credential Key Pair key ID and private key. // // More information about signed Cookies and their structure can be found at: // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html // // To sign a Cookie, create a CookieSigner with your private key and credential // pair key ID. Once you have a CookieSigner instance you can call Sign or // SignWithPolicy to sign the URLs. // // The signer is safe to use concurrently, but the optional cookies options // are not safe to modify concurrently. type CookieSigner struct { keyID string privKey *rsa.PrivateKey Opts CookieOptions } // NewCookieSigner constructs and returns a new CookieSigner to be used to for // signing Amazon CloudFront URL resources with. func NewCookieSigner(keyID string, privKey *rsa.PrivateKey, opts ...func(*CookieOptions)) *CookieSigner { signer := &CookieSigner{ keyID: keyID, privKey: privKey, Opts: CookieOptions{}.apply(opts...), } return signer } // Sign returns the cookies needed to allow user agents to make arbetrary // requests to cloudfront for the resource(s) defined by the policy. // // Sign will create a CloudFront policy with only a resource and condition of // DateLessThan equal to the expires time provided. // // The returned slice cookies should all be added to the Client's cookies or // server's response. // // Example: // s := sign.NewCookieSigner(keyID, privKey) // // // Get Signed cookies for a resource that will expire in 1 hour // cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour)) // if err != nil { // fmt.Println("failed to create signed cookies", err) // return // } // // // Or get Signed cookies for a resource that will expire in 1 hour // // and set path and domain of cookies // cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour), func(o *sign.CookieOptions) { // o.Path = "/" // o.Domain = ".example.com" // }) // if err != nil { // fmt.Println("failed to create signed cookies", err) // return // } // // // Server Response via http.ResponseWriter // for _, c := range cookies { // http.SetCookie(w, c) // } // // // Client request via the cookie jar // if client.CookieJar != nil { // for _, c := range cookies { // client.Cookie(w, c) // } // } func (s CookieSigner) Sign(u string, expires time.Time, opts ...func(*CookieOptions)) ([]*http.Cookie, error) { scheme, err := cookieURLScheme(u) if err != nil { return nil, err } resource, err := CreateResource(scheme, u) if err != nil { return nil, err } p := NewCannedPolicy(resource, expires) return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...)) } // Returns and validates the URL's scheme. // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-cookies func cookieURLScheme(u string) (string, error) { parts := strings.SplitN(u, "://", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid cookie URL, missing scheme") } scheme := strings.ToLower(parts[0]) if scheme != "http" && scheme != "https" && scheme != "http*" { return "", fmt.Errorf("invalid cookie URL scheme. Expect http, https, or http*. Go, %s", scheme) } return scheme, nil } // SignWithPolicy returns the cookies needed to allow user agents to make // arbetrairy requets to cloudfront for the resource(s) defined by the policy. // // The returned slice cookies should all be added to the Client's cookies or // server's response. // // Example: // s := sign.NewCookieSigner(keyID, privKey) // // policy := &sign.Policy{ // Statements: []sign.Statement{ // { // // Read the provided documentation on how to set this // // correctly, you'll probably want to use wildcards. // Resource: rawCloudFrontURL, // Condition: sign.Condition{ // // Optional IP source address range // IPAddress: &sign.IPAddress{SourceIP: "192.0.2.0/24"}, // // Optional date URL is not valid until // DateGreaterThan: &sign.AWSEpochTime{time.Now().Add(30 * time.Minute)}, // // Required date the URL will expire after // DateLessThan: &sign.AWSEpochTime{time.Now().Add(1 * time.Hour)}, // }, // }, // }, // } // // // Get Signed cookies for a resource that will expire in 1 hour // cookies, err := s.SignWithPolicy(policy) // if err != nil { // fmt.Println("failed to create signed cookies", err) // return // } // // // Or get Signed cookies for a resource that will expire in 1 hour // // and set path and domain of cookies // cookies, err := s.Sign(policy, func(o *sign.CookieOptions) { // o.Path = "/" // o.Domain = ".example.com" // }) // if err != nil { // fmt.Println("failed to create signed cookies", err) // return // } // // // Server Response via http.ResponseWriter // for _, c := range cookies { // http.SetCookie(w, c) // } // // // Client request via the cookie jar // if client.CookieJar != nil { // for _, c := range cookies { // client.Cookie(w, c) // } // } func (s CookieSigner) SignWithPolicy(p *Policy, opts ...func(*CookieOptions)) ([]*http.Cookie, error) { return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...)) } // Prepares the cookies to be attached to the header. An (optional) options // struct is provided in case people don't want to manually edit their cookies. func createCookies(p *Policy, keyID string, privKey *rsa.PrivateKey, opt CookieOptions) ([]*http.Cookie, error) { b64Sig, b64Policy, err := p.Sign(privKey) if err != nil { return nil, err } // Creates proper cookies cPolicy := &http.Cookie{ Name: CookiePolicyName, Value: string(b64Policy), HttpOnly: true, } cSignature := &http.Cookie{ Name: CookieSignatureName, Value: string(b64Sig), HttpOnly: true, } cKey := &http.Cookie{ Name: CookieKeyIDName, Value: keyID, HttpOnly: true, } cookies := []*http.Cookie{cPolicy, cSignature, cKey} // Applie the cookie options for _, c := range cookies { c.Path = opt.Path c.Domain = opt.Domain c.Secure = opt.Secure } return cookies, nil }