-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #626 from aws/pr614SignCookies
Add CookieSigner for CloudFront resources
- Loading branch information
Showing
5 changed files
with
562 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go/service/cloudfront/sign" | ||
) | ||
|
||
// Usage example: | ||
// go run main.go -file <privkey file> -id <keyId> -r <resource pattern> -g <object to get> | ||
func main() { | ||
var keyFile string // Private key PEM file | ||
var keyID string // Key pair ID of CloudFront key pair | ||
var resource string // CloudFront resource pattern | ||
var object string // S3 object frontented by CloudFront | ||
|
||
flag.StringVar(&keyFile, "file", "", "private key file") | ||
flag.StringVar(&keyID, "id", "", "key pair id") | ||
flag.StringVar(&resource, "r", "", "resource to request") | ||
flag.StringVar(&object, "g", "", "object to get") | ||
flag.Parse() | ||
|
||
// Load the PEM file into memory so it can be used by the signer | ||
privKey, err := sign.LoadPEMPrivKeyFile(keyFile) | ||
if err != nil { | ||
fmt.Println("failed to load key,", err) | ||
return | ||
} | ||
|
||
// Create the new CookieSigner to get signed cookies for CloudFront | ||
// resource requests | ||
signer := sign.NewCookieSigner(keyID, privKey) | ||
|
||
// Get the cookies for the resource. These will be used | ||
// to make the requests with | ||
cookies, err := signer.Sign(resource, time.Now().Add(1*time.Hour)) | ||
if err != nil { | ||
fmt.Println("failed to sign cookies", err) | ||
return | ||
} | ||
|
||
// Use the cookies in a http.Client to show how they allow the client | ||
// to request resources from CloudFront. | ||
req, err := http.NewRequest("GET", object, nil) | ||
fmt.Println("Cookies:") | ||
for _, c := range cookies { | ||
fmt.Printf("%s=%s;\n", c.Name, c.Value) | ||
req.AddCookie(c) | ||
} | ||
|
||
// Send and handle the response. For a successful response the object's | ||
// content will be written to stdout. The same process could be applied | ||
// to a http service written cookies to the response but useing | ||
// http.SetCookie(w, c,) on the ResponseWriter. | ||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
fmt.Println("failed to send request", err) | ||
return | ||
} | ||
defer resp.Body.Close() | ||
|
||
b, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
fmt.Println("failed to read requested body", err) | ||
return | ||
} | ||
|
||
fmt.Println("Response:", resp.Status) | ||
fmt.Println(string(b)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
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 additonal 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 := 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 := 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 | ||
} |
Oops, something went wrong.