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