206 lines
6.4 KiB
Go
206 lines
6.4 KiB
Go
|
// 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. A minimum of at least one policy statement is required for a signed URL.
|
||
|
//
|
||
|
// 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: []sign.Statement{
|
||
|
// {
|
||
|
// Resource: rawURL,
|
||
|
// 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)},
|
||
|
// },
|
||
|
// },
|
||
|
// },
|
||
|
// }
|
||
|
//
|
||
|
// 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
|
||
|
}
|