123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- // Package sign provides utilities to generate signed URLs for Amazon CloudFront.
- //
- // More information about signed URLs and their structure can be found at:
- // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html
- //
- // To sign a URL create a URLSigner with your private key and credential pair key ID.
- // Once you have a URLSigner instance you can call Sign or SignWithPolicy to
- // sign the URLs.
- //
- // Example:
- //
- // // Sign URL to be valid for 1 hour from now.
- // signer := sign.NewURLSigner(keyID, privKey)
- // signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
- // if err != nil {
- // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
- // }
- //
- package sign
- import (
- "crypto/rsa"
- "fmt"
- "net/url"
- "strings"
- "time"
- )
- // An URLSigner provides URL signing utilities to sign URLs for Amazon CloudFront
- // resources. Using a private key and Credential Key Pair key ID the URLSigner
- // only needs to be created once per Credential Key Pair key ID and private key.
- //
- // The signer is safe to use concurrently.
- type URLSigner struct {
- keyID string
- privKey *rsa.PrivateKey
- }
- // NewURLSigner constructs and returns a new URLSigner to be used to for signing
- // Amazon CloudFront URL resources with.
- func NewURLSigner(keyID string, privKey *rsa.PrivateKey) *URLSigner {
- return &URLSigner{
- keyID: keyID,
- privKey: privKey,
- }
- }
- // Sign will sign a single URL to expire at the time of expires sign using the
- // Amazon CloudFront default Canned Policy. The URL will be signed with the
- // private key and Credential Key Pair Key ID previously provided to URLSigner.
- //
- // This is the default method of signing Amazon CloudFront URLs. If extra policy
- // conditions are need other than URL expiry use SignWithPolicy instead.
- //
- // Example:
- //
- // // Sign URL to be valid for 1 hour from now.
- // signer := sign.NewURLSigner(keyID, privKey)
- // signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
- // if err != nil {
- // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
- // }
- //
- func (s URLSigner) Sign(url string, expires time.Time) (string, error) {
- scheme, cleanedURL, err := cleanURLScheme(url)
- if err != nil {
- return "", err
- }
- resource, err := CreateResource(scheme, url)
- if err != nil {
- return "", err
- }
- return signURL(scheme, cleanedURL, s.keyID, NewCannedPolicy(resource, expires), false, s.privKey)
- }
- // SignWithPolicy will sign a URL with the Policy provided. The URL will be
- // signed with the private key and Credential Key Pair Key ID previously provided to URLSigner.
- //
- // Use this signing method if you are looking to sign a URL with more than just
- // the URL's expiry time, or reusing Policies between multiple URL signings.
- // If only the expiry time is needed you can use Sign and provide just the
- // URL's expiry time.
- //
- // Note: It is not safe to use Polices between multiple signers concurrently
- //
- // Example:
- //
- // // Sign URL to be valid for 30 minutes from now, expires one hour from now, and
- // // restricted to the 192.0.2.0/24 IP address range.
- // policy := &sign.Policy{
- // Statements: []Statement{
- // {
- // Resource: rawURL,
- // Condition: Condition{
- // // Optional IP source address range
- // IPAddress: &IPAddress{SourceIP: "192.0.2.0/24"},
- // // Optional date URL is not valid until
- // DateGreaterThan: &AWSEpochTime{time.Now().Add(30 * time.Minute)},
- // // Required date the URL will expire after
- // DateLessThan: &AWSEpochTime{time.Now().Add(1 * time.Hour)},
- // }
- // }
- // }
- // }
- //
- // signer := sign.NewURLSigner(keyID, privKey)
- // signedURL, err := signer.SignWithPolicy(rawURL, policy)
- // if err != nil {
- // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
- // }
- //
- func (s URLSigner) SignWithPolicy(url string, p *Policy) (string, error) {
- scheme, cleanedURL, err := cleanURLScheme(url)
- if err != nil {
- return "", err
- }
- return signURL(scheme, cleanedURL, s.keyID, p, true, s.privKey)
- }
- func signURL(scheme, url, keyID string, p *Policy, customPolicy bool, privKey *rsa.PrivateKey) (string, error) {
- // Validation URL elements
- if err := validateURL(url); err != nil {
- return "", err
- }
- b64Signature, b64Policy, err := p.Sign(privKey)
- if err != nil {
- return "", err
- }
- // build and return signed URL
- builtURL := buildSignedURL(url, keyID, p, customPolicy, b64Policy, b64Signature)
- if scheme == "rtmp" {
- return buildRTMPURL(builtURL)
- }
- return builtURL, nil
- }
- func buildSignedURL(baseURL, keyID string, p *Policy, customPolicy bool, b64Policy, b64Signature []byte) string {
- pred := "?"
- if strings.Contains(baseURL, "?") {
- pred = "&"
- }
- signedURL := baseURL + pred
- if customPolicy {
- signedURL += "Policy=" + string(b64Policy)
- } else {
- signedURL += fmt.Sprintf("Expires=%d", p.Statements[0].Condition.DateLessThan.UTC().Unix())
- }
- signedURL += fmt.Sprintf("&Signature=%s&Key-Pair-Id=%s", string(b64Signature), keyID)
- return signedURL
- }
- func buildRTMPURL(u string) (string, error) {
- parsed, err := url.Parse(u)
- if err != nil {
- return "", fmt.Errorf("unable to parse rtmp signed URL, err: %s", err)
- }
- rtmpURL := strings.TrimLeft(parsed.Path, "/")
- if parsed.RawQuery != "" {
- rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
- }
- return rtmpURL, nil
- }
- func cleanURLScheme(u string) (scheme, cleanedURL string, err error) {
- parts := strings.SplitN(u, "://", 2)
- if len(parts) != 2 {
- return "", "", fmt.Errorf("invalid URL, missing scheme and domain/path")
- }
- scheme = strings.Replace(parts[0], "*", "", 1)
- cleanedURL = fmt.Sprintf("%s://%s", scheme, parts[1])
- return strings.ToLower(scheme), cleanedURL, nil
- }
- var illegalQueryParms = []string{"Expires", "Policy", "Signature", "Key-Pair-Id"}
- func validateURL(u string) error {
- parsed, err := url.Parse(u)
- if err != nil {
- return fmt.Errorf("unable to parse URL, err: %s", err.Error())
- }
- if parsed.Scheme == "" {
- return fmt.Errorf("URL missing valid scheme, %s", u)
- }
- q := parsed.Query()
- for _, p := range illegalQueryParms {
- if _, ok := q[p]; ok {
- return fmt.Errorf("%s cannot be a query parameter for a signed URL", p)
- }
- }
- return nil
- }
|