Switch to using the dep tool and update all the dependencies

This commit is contained in:
Nick Craig-Wood 2017-05-11 15:39:54 +01:00
parent 5135ff73cb
commit 98c2d2c41b
5321 changed files with 4483201 additions and 5922 deletions

View file

@ -0,0 +1,226 @@
package sign
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
"time"
"unicode"
)
// An AWSEpochTime wraps a time value providing JSON serialization needed for
// AWS Policy epoch time fields.
type AWSEpochTime struct {
time.Time
}
// NewAWSEpochTime returns a new AWSEpochTime pointer wrapping the Go time provided.
func NewAWSEpochTime(t time.Time) *AWSEpochTime {
return &AWSEpochTime{t}
}
// MarshalJSON serializes the epoch time as AWS Profile epoch time.
func (t AWSEpochTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"AWS:EpochTime":%d}`, t.UTC().Unix())), nil
}
// An IPAddress wraps an IPAddress source IP providing JSON serialization information
type IPAddress struct {
SourceIP string `json:"AWS:SourceIp"`
}
// A Condition defines the restrictions for how a signed URL can be used.
type Condition struct {
// Optional IP address mask the signed URL must be requested from.
IPAddress *IPAddress `json:"IpAddress,omitempty"`
// Optional date that the signed URL cannot be used until. It is invalid
// to make requests with the signed URL prior to this date.
DateGreaterThan *AWSEpochTime `json:",omitempty"`
// Required date that the signed URL will expire. A DateLessThan is required
// sign cloud front URLs
DateLessThan *AWSEpochTime `json:",omitempty"`
}
// A Statement is a collection of conditions for resources
type Statement struct {
// The Web or RTMP resource the URL will be signed for
Resource string
// The set of conditions for this resource
Condition Condition
}
// A Policy defines the resources that a signed will be signed for.
//
// See the following page for more information on how policies are constructed.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
type Policy struct {
// List of resource and condition statements.
// Signed URLs should only provide a single statement.
Statements []Statement `json:"Statement"`
}
// Override for testing to mock out usage of crypto/rand.Reader
var randReader = rand.Reader
// Sign will sign a policy using an RSA private key. It will return a base 64
// encoded signature and policy if no error is encountered.
//
// The signature and policy should be added to the signed URL following the
// guidelines in:
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
func (p *Policy) Sign(privKey *rsa.PrivateKey) (b64Signature, b64Policy []byte, err error) {
if err = p.Validate(); err != nil {
return nil, nil, err
}
// Build and escape the policy
b64Policy, jsonPolicy, err := encodePolicy(p)
if err != nil {
return nil, nil, err
}
awsEscapeEncoded(b64Policy)
// Build and escape the signature
b64Signature, err = signEncodedPolicy(randReader, jsonPolicy, privKey)
if err != nil {
return nil, nil, err
}
awsEscapeEncoded(b64Signature)
return b64Signature, b64Policy, nil
}
// Validate verifies that the policy is valid and usable, and returns an
// error if there is a problem.
func (p *Policy) Validate() error {
if len(p.Statements) == 0 {
return fmt.Errorf("at least one policy statement is required")
}
for i, s := range p.Statements {
if s.Resource == "" {
return fmt.Errorf("statement at index %d does not have a resource", i)
}
if !isASCII(s.Resource) {
return fmt.Errorf("unable to sign resource, [%s]. "+
"Resources must only contain ascii characters. "+
"Hostnames with unicode should be encoded as Punycode, (e.g. golang.org/x/net/idna), "+
"and URL unicode path/query characters should be escaped.", s.Resource)
}
}
return nil
}
// CreateResource constructs, validates, and returns a resource URL string. An
// error will be returned if unable to create the resource string.
func CreateResource(scheme, u string) (string, error) {
scheme = strings.ToLower(scheme)
if scheme == "http" || scheme == "https" || scheme == "http*" || scheme == "*" {
return u, nil
}
if scheme == "rtmp" {
parsed, err := url.Parse(u)
if err != nil {
return "", fmt.Errorf("unable to parse rtmp URL, err: %s", err)
}
rtmpURL := strings.TrimLeft(parsed.Path, "/")
if parsed.RawQuery != "" {
rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
}
return rtmpURL, nil
}
return "", fmt.Errorf("invalid URL scheme must be http, https, or rtmp. Provided: %s", scheme)
}
// NewCannedPolicy returns a new Canned Policy constructed using the resource
// and expires time. This can be used to generate the basic model for a Policy
// that can be then augmented with additional conditions.
//
// See the following page for more information on how policies are constructed.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
func NewCannedPolicy(resource string, expires time.Time) *Policy {
return &Policy{
Statements: []Statement{
{
Resource: resource,
Condition: Condition{
DateLessThan: NewAWSEpochTime(expires),
},
},
},
}
}
// encodePolicy encodes the Policy as JSON and also base 64 encodes it.
func encodePolicy(p *Policy) (b64Policy, jsonPolicy []byte, err error) {
jsonPolicy, err = json.Marshal(p)
if err != nil {
return nil, nil, fmt.Errorf("failed to encode policy, %s", err.Error())
}
// Remove leading and trailing white space, JSON encoding will note include
// whitespace within the encoding.
jsonPolicy = bytes.TrimSpace(jsonPolicy)
b64Policy = make([]byte, base64.StdEncoding.EncodedLen(len(jsonPolicy)))
base64.StdEncoding.Encode(b64Policy, jsonPolicy)
return b64Policy, jsonPolicy, nil
}
// signEncodedPolicy will sign and base 64 encode the JSON encoded policy.
func signEncodedPolicy(randReader io.Reader, jsonPolicy []byte, privKey *rsa.PrivateKey) ([]byte, error) {
hash := sha1.New()
if _, err := bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil {
return nil, fmt.Errorf("failed to calculate signing hash, %s", err.Error())
}
sig, err := rsa.SignPKCS1v15(randReader, privKey, crypto.SHA1, hash.Sum(nil))
if err != nil {
return nil, fmt.Errorf("failed to sign policy, %s", err.Error())
}
b64Sig := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
base64.StdEncoding.Encode(b64Sig, sig)
return b64Sig, nil
}
// special characters to be replaced with awsEscapeEncoded
var invalidEncodedChar = map[byte]byte{
'+': '-',
'=': '_',
'/': '~',
}
// awsEscapeEncoded will replace base64 encoding's special characters to be URL safe.
func awsEscapeEncoded(b []byte) {
for i, v := range b {
if r, ok := invalidEncodedChar[v]; ok {
b[i] = r
}
}
}
func isASCII(u string) bool {
for _, c := range u {
if c > unicode.MaxASCII {
return false
}
}
return true
}

View file

@ -0,0 +1,139 @@
package sign
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"fmt"
"math/rand"
"strings"
"testing"
"time"
)
func TestEpochTimeMarshal(t *testing.T) {
v := AWSEpochTime{time.Now()}
b, err := v.MarshalJSON()
if err != nil {
t.Fatalf("Unexpected error, %#v", err)
}
expected := fmt.Sprintf(`{"AWS:EpochTime":%d}`, v.UTC().Unix())
if string(b) != expected {
t.Errorf("Expected marshaled time to match, expect: %s, actual: %s",
expected, string(b))
}
}
var testCreateResource = []struct {
scheme, u string
expect string
errPrefix string
}{
{
"https", "https://example.com/a?b=1",
"https://example.com/a?b=1", "",
},
{
"http", "http*://example.com/a?b=1",
"http*://example.com/a?b=1", "",
},
{
"rtmp", "https://example.com/a?b=1",
"a?b=1", "",
},
{
"ftp", "ftp://example.com/a?b=1",
"", "invalid URL scheme",
},
}
func TestCreateResource(t *testing.T) {
for i, v := range testCreateResource {
r, err := CreateResource(v.scheme, v.u)
if err != nil {
if v.errPrefix == "" {
t.Errorf("%d, Unexpected error %s", i, err.Error())
continue
}
if !strings.HasPrefix(err.Error(), v.errPrefix) {
t.Errorf("%d, Expected to find prefix\nexpect: %s\nactual: %s", i, v.errPrefix, err.Error())
continue
}
} else if v.errPrefix != "" {
t.Errorf("%d, Expected error %s", i, v.errPrefix)
continue
}
if v.expect != r {
t.Errorf("%d, Expected to find prefix\nexpect: %s\nactual: %s", i, v.expect, r)
}
}
}
var testTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
const expectedJSONPolicy = `{"Statement":[{"Resource":"https://example.com/a","Condition":{"DateLessThan":{"AWS:EpochTime":1257894000}}}]}`
const expectedB64Policy = `eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9hIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxMjU3ODk0MDAwfX19XX0=`
func TestEncodePolicy(t *testing.T) {
p := NewCannedPolicy("https://example.com/a", testTime)
b64Policy, jsonPolicy, err := encodePolicy(p)
if err != nil {
t.Fatalf("Unexpected error, %#v", err)
}
if string(jsonPolicy) != expectedJSONPolicy {
t.Errorf("Expected json encoding to match, \nexpect: %s\nactual: %s\n", expectedJSONPolicy, jsonPolicy)
}
if string(b64Policy) != expectedB64Policy {
t.Errorf("Expected b64 encoding to match, \nexpect: %s\nactual: %s\n", expectedB64Policy, b64Policy)
}
}
func TestSignEncodedPolicy(t *testing.T) {
p := NewCannedPolicy("https://example.com/a", testTime)
_, jsonPolicy, err := encodePolicy(p)
if err != nil {
t.Fatalf("Unexpected policy encode error, %#v", err)
}
r := newRandomReader(rand.New(rand.NewSource(1)))
privKey, err := rsa.GenerateKey(r, 1024)
if err != nil {
t.Fatalf("Unexpected priv key error, %#v", err)
}
b64Signature, err := signEncodedPolicy(r, jsonPolicy, privKey)
if err != nil {
t.Fatalf("Unexpected policy sign error, %#v", err)
}
hash := sha1.New()
if _, err = bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil {
t.Fatalf("Unexpected hash error, %#v", err)
}
decodedSig, err := base64.StdEncoding.DecodeString(string(b64Signature))
if err != nil {
t.Fatalf("Unexpected base64 decode signature, %#v", err)
}
if err := rsa.VerifyPKCS1v15(&privKey.PublicKey, crypto.SHA1, hash.Sum(nil), decodedSig); err != nil {
t.Fatalf("Unable to verify signature, %#v", err)
}
}
func TestAWSEscape(t *testing.T) {
expect := "a-b_c~"
actual := []byte("a+b=c/")
awsEscapeEncoded(actual)
if string(actual) != expect {
t.Errorf("expect: %s, actual: %s", expect, string(actual))
}
}

View file

@ -0,0 +1,68 @@
package sign
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"os"
)
// LoadPEMPrivKeyFile reads a PEM encoded RSA private key from the file name.
// A new RSA private key will be returned if no error.
func LoadPEMPrivKeyFile(name string) (*rsa.PrivateKey, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
return LoadPEMPrivKey(file)
}
// LoadPEMPrivKey reads a PEM encoded RSA private key from the io.Reader.
// A new RSA private key will be returned if no error.
func LoadPEMPrivKey(reader io.Reader) (*rsa.PrivateKey, error) {
block, err := loadPem(reader)
if err != nil {
return nil, err
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
// LoadEncryptedPEMPrivKey decrypts the PEM encoded private key using the
// password provided returning a RSA private key. If the PEM data is invalid,
// or unable to decrypt an error will be returned.
func LoadEncryptedPEMPrivKey(reader io.Reader, password []byte) (*rsa.PrivateKey, error) {
block, err := loadPem(reader)
if err != nil {
return nil, err
}
decryptedBlock, err := x509.DecryptPEMBlock(block, password)
if err != nil {
return nil, err
}
return x509.ParsePKCS1PrivateKey(decryptedBlock)
}
func loadPem(reader io.Reader) (*pem.Block, error) {
b, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil {
// pem.Decode will set block to nil if there is no PEM data in the input
// the second parameter will contain the provided bytes that failed
// to be decoded.
return nil, fmt.Errorf("no valid PEM data provided")
}
return block, nil
}

View file

@ -0,0 +1,90 @@
package sign
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io"
"math/rand"
"strings"
"testing"
)
func generatePEM(randReader io.Reader, password []byte) (buf *bytes.Buffer, err error) {
k, err := rsa.GenerateKey(randReader, 1024)
if err != nil {
return nil, err
}
derBytes := x509.MarshalPKCS1PrivateKey(k)
var block *pem.Block
if password != nil {
block, err = x509.EncryptPEMBlock(randReader, "RSA PRIVATE KEY", derBytes, password, x509.PEMCipherAES128)
} else {
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: derBytes,
}
}
buf = &bytes.Buffer{}
err = pem.Encode(buf, block)
return buf, err
}
func TestLoadPemPrivKey(t *testing.T) {
reader, err := generatePEM(newRandomReader(rand.New(rand.NewSource(1))), nil)
if err != nil {
t.Errorf("Unexpected pem generation err %s", err.Error())
}
privKey, err := LoadPEMPrivKey(reader)
if err != nil {
t.Errorf("Unexpected key load error, %s", err.Error())
}
if privKey == nil {
t.Errorf("Expected valid privKey, but got nil")
}
}
func TestLoadPemPrivKeyInvalidPEM(t *testing.T) {
reader := strings.NewReader("invalid PEM data")
privKey, err := LoadPEMPrivKey(reader)
if err == nil {
t.Errorf("Expected error invalid PEM data error")
}
if privKey != nil {
t.Errorf("Expected nil privKey but got %#v", privKey)
}
}
func TestLoadEncryptedPEMPrivKey(t *testing.T) {
reader, err := generatePEM(newRandomReader(rand.New(rand.NewSource(1))), []byte("password"))
if err != nil {
t.Errorf("Unexpected pem generation err %s", err.Error())
}
privKey, err := LoadEncryptedPEMPrivKey(reader, []byte("password"))
if err != nil {
t.Errorf("Unexpected key load error, %s", err.Error())
}
if privKey == nil {
t.Errorf("Expected valid privKey, but got nil")
}
}
func TestLoadEncryptedPEMPrivKeyWrongPassword(t *testing.T) {
reader, err := generatePEM(newRandomReader(rand.New(rand.NewSource(1))), []byte("password"))
privKey, err := LoadEncryptedPEMPrivKey(reader, []byte("wrong password"))
if err == nil {
t.Errorf("Expected error invalid PEM data error")
}
if privKey != nil {
t.Errorf("Expected nil privKey but got %#v", privKey)
}
}

View file

@ -0,0 +1,30 @@
package sign
import (
"bytes"
"encoding/binary"
"math/rand"
)
// A randomReader wraps a math/rand.Rand within an reader so that it can used
// as a predictable testing replacement for crypto/rand.Reader
type randomReader struct {
b *bytes.Buffer
r *rand.Rand
}
// newRandomReader returns a new instance of the random reader
func newRandomReader(r *rand.Rand) *randomReader {
return &randomReader{b: &bytes.Buffer{}, r: r}
}
// Read will read random bytes from up to the length of b.
func (m *randomReader) Read(b []byte) (int, error) {
for i := 0; i < len(b); {
binary.Write(m.b, binary.LittleEndian, m.r.Int63())
n, _ := m.b.Read(b[i:])
i += n
}
return len(b), nil
}

View file

@ -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 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
}

View file

@ -0,0 +1,163 @@
package sign
import (
"fmt"
"io"
"math/rand"
"net/http"
"time"
)
func examplePEMReader() io.Reader {
reader, err := generatePEM(randReader, nil)
if err != nil {
panic(fmt.Sprintf("Unexpected pem generation err %v", err))
}
return reader
}
func ExampleCookieSigner_Sign() {
origRandReader := randReader
randReader = newRandomReader(rand.New(rand.NewSource(1)))
defer func() {
randReader = origRandReader
}()
// Load your private key so it can be used by the CookieSigner
// To load private key from file use `sign.LoadPEMPrivKeyFile`.
privKey, err := LoadPEMPrivKey(examplePEMReader())
if err != nil {
fmt.Println("failed to load private key", err)
return
}
cookieSigner := NewCookieSigner("keyID", privKey)
// Use the signer to sign the URL
cookies, err := cookieSigner.Sign("http://example.com/somepath/*", testSignTime.Add(30*time.Minute))
if err != nil {
fmt.Println("failed to sign cookies with policy,", err)
return
}
printExampleCookies(cookies)
// Output:
// Cookies:
// CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2V4YW1wbGUuY29tL3NvbWVwYXRoLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTU4MDB9fX1dfQ__, , , false
// CloudFront-Signature: o~jvj~CFkvGZB~yYED3elicKZag-CRijy8yD2E5yF1s7VNV7kNeQWC7MDtEcBQ8-eh7Xgjh0wMPQdAVdh09gBObd-hXDpKUyh8YKxogj~oloV~8KOvqE5xzWiKcqjdfJjmT5iEqIui~H1ExYjyKjgir79npmlyYkaJS5s62EQa8_, , , false
// CloudFront-Key-Pair-Id: keyID, , , false
}
func ExampleCookieSigner_SignWithPolicy() {
origRandReader := randReader
randReader = newRandomReader(rand.New(rand.NewSource(1)))
defer func() {
randReader = origRandReader
}()
// Sign cookie 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.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html
p := &Policy{
// Only a single policy statement can be used with CloudFront
// cookie signatures.
Statements: []Statement{{
// Read the provided documentation on how to set this correctly,
// you'll probably want to use wildcards
Resource: "http://sub.cloudfront.com",
Condition: Condition{
// Optional IP source address range
IPAddress: &IPAddress{SourceIP: "192.0.2.0/24"},
// Optional date URL is not valid until
DateGreaterThan: &AWSEpochTime{testSignTime.Add(30 * time.Minute)},
// Required date the URL will expire after
DateLessThan: &AWSEpochTime{testSignTime.Add(1 * time.Hour)},
},
},
},
}
// Load your private key so it can be used by the CookieSigner
// To load private key from file use `sign.LoadPEMPrivKeyFile`.
privKey, err := LoadPEMPrivKey(examplePEMReader())
if err != nil {
fmt.Println("failed to load private key", err)
return
}
// Key ID that represents the key pair associated with the private key
keyID := "privateKeyID"
// Set credentials to the CookieSigner.
cookieSigner := NewCookieSigner(keyID, privKey)
// Avoid adding an Expire or MaxAge. See provided AWS Documentation for
// more info.
cookies, err := cookieSigner.SignWithPolicy(p)
if err != nil {
fmt.Println("failed to sign cookies with policy,", err)
return
}
printExampleCookies(cookies)
// Output:
// Cookies:
// CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL3N1Yi5jbG91ZGZyb250LmNvbSIsIkNvbmRpdGlvbiI6eyJJcEFkZHJlc3MiOnsiQVdTOlNvdXJjZUlwIjoiMTkyLjAuMi4wLzI0In0sIkRhdGVHcmVhdGVyVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxMjU3ODk1ODAwfSwiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTc2MDB9fX1dfQ__, , , false
// CloudFront-Signature: JaWdcbr98colrDAhOpkyxqCZev2IAxURu1RKKo1wS~sI5XdNXWYbZJs2FdpbJ475ZvmhZ1-r4ENUqBXAlRfPfOc21Hm4~24jRmPTO3512D4uuJHrPVxSfgeGuFeigfCGWAqyfYYH1DsFl5JQDpzetsNI3ZhGRkQb8V-oYFanddg_, , , false
// CloudFront-Key-Pair-Id: privateKeyID, , , false
}
func ExampleCookieOptions() {
origRandReader := randReader
randReader = newRandomReader(rand.New(rand.NewSource(1)))
defer func() {
randReader = origRandReader
}()
// Load your private key so it can be used by the CookieSigner
// To load private key from file use `sign.LoadPEMPrivKeyFile`.
privKey, err := LoadPEMPrivKey(examplePEMReader())
if err != nil {
fmt.Println("failed to load private key", err)
return
}
// Create the CookieSigner with options set. These options can be set
// directly with cookieSigner.Opts. These values can be overridden on
// individual Sign and SignWithProfile calls.
cookieSigner := NewCookieSigner("keyID", privKey, func(o *CookieOptions) {
//provide an optional struct fields to specify other options
o.Path = "/"
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html
o.Domain = ".cNameAssociatedWithMyDistribution.com"
// Make sure your app/site can handle https payloads, otherwise
// set this to false.
o.Secure = true
})
// Use the signer to sign the URL
cookies, err := cookieSigner.Sign("http*://*", testSignTime.Add(30*time.Minute), func(o *CookieOptions) {
o.Path = "/mypath/"
})
if err != nil {
fmt.Println("failed to sign cookies with policy,", err)
return
}
printExampleCookies(cookies)
// Output:
// Cookies:
// CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cCo6Ly8qIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxMjU3ODk1ODAwfX19XX0_, /mypath/, .cNameAssociatedWithMyDistribution.com, true
// CloudFront-Signature: Yco06vgowwvSYgTSY9XbXpBcTlUlqpyyYXgRhus3nfnC74A7oQ~fMBH0we-rGxvph8ZyHnTxC5ubbPKSzo3EHUm2IcQeEo4p6WCgZZMzCuLlkpeMKhMAkCqX7rmUfkXhTslBHe~ylcmaZqo-hdnOiWrXk2U974ZQbbt5cOjwQG0_, /mypath/, .cNameAssociatedWithMyDistribution.com, true
// CloudFront-Key-Pair-Id: keyID, /mypath/, .cNameAssociatedWithMyDistribution.com, true
}
func printExampleCookies(cookies []*http.Cookie) {
fmt.Println("Cookies:")
for _, c := range cookies {
fmt.Printf("%s: %s, %s, %s, %t\n", c.Name, c.Value, c.Path, c.Domain, c.Secure)
}
}

View file

@ -0,0 +1,83 @@
package sign
import (
"crypto/rsa"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewCookieSigner(t *testing.T) {
privKey, err := rsa.GenerateKey(randReader, 1024)
if err != nil {
t.Fatalf("Unexpected priv key error, %#v", err)
}
signer := NewCookieSigner("keyID", privKey)
assert.Equal(t, "keyID", signer.keyID)
assert.Equal(t, privKey, signer.privKey)
}
func TestSignCookie(t *testing.T) {
privKey, err := rsa.GenerateKey(randReader, 1024)
assert.NoError(t, err)
signer := NewCookieSigner("keyID", privKey)
cookies, err := signer.Sign("http*://*", time.Now().Add(1*time.Hour))
assert.NoError(t, err)
assert.Equal(t, CookiePolicyName, cookies[0].Name)
assert.Equal(t, CookieSignatureName, cookies[1].Name)
assert.Equal(t, CookieKeyIDName, cookies[2].Name)
}
func TestSignCookie_WithPolicy(t *testing.T) {
privKey, err := rsa.GenerateKey(randReader, 1024)
assert.NoError(t, err)
p := &Policy{
Statements: []Statement{
{
Resource: "*",
Condition: Condition{
DateLessThan: &AWSEpochTime{time.Now().Add(1 * time.Hour)},
},
},
},
}
signer := NewCookieSigner("keyID", privKey)
cookies, err := signer.SignWithPolicy(p)
assert.NoError(t, err)
assert.Equal(t, CookiePolicyName, cookies[0].Name)
assert.Equal(t, CookieSignatureName, cookies[1].Name)
assert.Equal(t, CookieKeyIDName, cookies[2].Name)
}
func TestSignCookie_WithCookieOptions(t *testing.T) {
privKey, err := rsa.GenerateKey(randReader, 1024)
assert.NoError(t, err)
expires := time.Now().Add(1 * time.Hour)
signer := NewCookieSigner("keyID", privKey)
cookies, err := signer.Sign("https://example.com/*", expires, func(o *CookieOptions) {
o.Path = "/"
o.Domain = ".example.com"
o.Secure = true
})
assert.NoError(t, err)
assert.Equal(t, CookiePolicyName, cookies[0].Name)
assert.Equal(t, CookieSignatureName, cookies[1].Name)
assert.Equal(t, CookieKeyIDName, cookies[2].Name)
for _, c := range cookies {
assert.Equal(t, "/", c.Path)
assert.Equal(t, ".example.com", c.Domain)
assert.True(t, c.Secure)
}
}

View file

@ -0,0 +1,205 @@
// 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
}

View file

@ -0,0 +1,149 @@
package sign
import (
"crypto/rsa"
"math/rand"
"strings"
"testing"
"time"
)
var testSignTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
var testSignURL = []struct {
u string
p *Policy
t time.Time
customPolicy bool
expectErr bool
out string
}{
{
"http://example.com/a", NewCannedPolicy("http://example.com/a", testSignTime), time.Time{}, true, false,
"http://example.com/a?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2V4YW1wbGUuY29tL2EiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTQwMDB9fX1dfQ__&Signature=Y6qvWOZNl99uNPMGprvrKXEmXpLWJ-xXKVHL~nmF0BR1jPb2XA2jor0MUYKBE4ViTkWZZ1dz46zSFMsEEfw~n6-SVYXZ2QHBBTkSAoxGtH6dH33Ph9pz~f9Wy7aYXq~9I-Ah0E6yC~BMiQuXe5qAOucuMPorKgPfC0dvLMw2EF0_&Key-Pair-Id=KeyID",
},
{
"http://example.com/a", nil, testSignTime, false, false,
"http://example.com/a?Expires=1257894000&Signature=Y6qvWOZNl99uNPMGprvrKXEmXpLWJ-xXKVHL~nmF0BR1jPb2XA2jor0MUYKBE4ViTkWZZ1dz46zSFMsEEfw~n6-SVYXZ2QHBBTkSAoxGtH6dH33Ph9pz~f9Wy7aYXq~9I-Ah0E6yC~BMiQuXe5qAOucuMPorKgPfC0dvLMw2EF0_&Key-Pair-Id=KeyID",
},
{
"http://example.com/Ƿ", nil, testSignTime, false, true,
"http://example.com/Ƿ?Expires=1257894000&Signature=Y6qvWOZNl99uNPMGprvrKXEmXpLWJ-xXKVHL~nmF0BR1jPb2XA2jor0MUYKBE4ViTkWZZ1dz46zSFMsEEfw~n6-SVYXZ2QHBBTkSAoxGtH6dH33Ph9pz~f9Wy7aYXq~9I-Ah0E6yC~BMiQuXe5qAOucuMPorKgPfC0dvLMw2EF0_&Key-Pair-Id=KeyID",
},
{
"http://example.com/a", &Policy{}, time.Time{}, true, true,
"http://example.com/a?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2V4YW1wbGUuY29tL2EiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTQwMDB9fX1dfQ__&Signature=Y6qvWOZNl99uNPMGprvrKXEmXpLWJ-xXKVHL~nmF0BR1jPb2XA2jor0MUYKBE4ViTkWZZ1dz46zSFMsEEfw~n6-SVYXZ2QHBBTkSAoxGtH6dH33Ph9pz~f9Wy7aYXq~9I-Ah0E6yC~BMiQuXe5qAOucuMPorKgPfC0dvLMw2EF0_&Key-Pair-Id=KeyID",
},
{
"http://example.com/a", NewCannedPolicy("", testSignTime), time.Time{}, true, true,
"http://example.com/a?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2V4YW1wbGUuY29tL2EiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTQwMDB9fX1dfQ__&Signature=Y6qvWOZNl99uNPMGprvrKXEmXpLWJ-xXKVHL~nmF0BR1jPb2XA2jor0MUYKBE4ViTkWZZ1dz46zSFMsEEfw~n6-SVYXZ2QHBBTkSAoxGtH6dH33Ph9pz~f9Wy7aYXq~9I-Ah0E6yC~BMiQuXe5qAOucuMPorKgPfC0dvLMw2EF0_&Key-Pair-Id=KeyID",
},
{
"rtmp://example.com/a", nil, testSignTime, false, false,
"a?Expires=1257894000&Signature=Ds9NbpGwIcDKG1iZDyjfPXp0ZFYSIzfvGzJj-x28XlXfrarHrJbTOQj3bec~aAyb8NAqghBYRdKF9~RdjNrdyxyiequo-SCjFgFHnRNIk0FiqH0fVt2NO63f0X8-Kbur9cPtJoHR9Jzk0I1CQnECqhL6A0OgPhijTfKUITocmzA_&Key-Pair-Id=KeyID",
},
{
"rtmp://example.com/a", NewCannedPolicy("a", testSignTime), time.Time{}, true, false,
"a?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiYSIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTI1Nzg5NDAwMH19fV19&Signature=Ds9NbpGwIcDKG1iZDyjfPXp0ZFYSIzfvGzJj-x28XlXfrarHrJbTOQj3bec~aAyb8NAqghBYRdKF9~RdjNrdyxyiequo-SCjFgFHnRNIk0FiqH0fVt2NO63f0X8-Kbur9cPtJoHR9Jzk0I1CQnECqhL6A0OgPhijTfKUITocmzA_&Key-Pair-Id=KeyID",
},
}
// TODO Sign URL HTTP
// TODO Sign URL RMTP
func TestSignURL(t *testing.T) {
origRandReader := randReader
randReader = newRandomReader(rand.New(rand.NewSource(1)))
defer func() {
randReader = origRandReader
}()
privKey, err := rsa.GenerateKey(randReader, 1024)
if err != nil {
t.Fatalf("Unexpected priv key error, %#v", err)
}
s := NewURLSigner("KeyID", privKey)
for i, v := range testSignURL {
var u string
var err error
if v.customPolicy {
u, err = s.SignWithPolicy(v.u, v.p)
} else {
u, err = s.Sign(v.u, v.t)
}
if err != nil {
if v.expectErr {
continue
}
t.Errorf("%d, Unexpected error, %s", i, err.Error())
continue
} else if v.expectErr {
t.Errorf("%d Expected error, but got none", i)
continue
}
if u != v.out {
t.Errorf("%d, Unexpected URL\nexpect: %s\nactual: %s\n", i, v.out, u)
}
}
}
var testBuildSignedURL = []struct {
u, keyID string
p *Policy
customPolicy bool
b64Policy, b64Sig []byte
out string
}{
{
"https://example.com/a?b=1", "KeyID", NewCannedPolicy("", testSignTime), true, []byte("b64Policy"), []byte("b64Sig"),
"https://example.com/a?b=1&Policy=b64Policy&Signature=b64Sig&Key-Pair-Id=KeyID",
},
{
"https://example.com/a", "KeyID", NewCannedPolicy("", testSignTime), true, []byte("b64Policy"), []byte("b64Sig"),
"https://example.com/a?Policy=b64Policy&Signature=b64Sig&Key-Pair-Id=KeyID",
},
{
"https://example.com/a?b=1", "KeyID", NewCannedPolicy("https://example.com/a?b=1", testSignTime), false, []byte("b64Policy"), []byte("b64Sig"),
"https://example.com/a?b=1&Expires=1257894000&Signature=b64Sig&Key-Pair-Id=KeyID",
},
}
func TestBuildSignedURL(t *testing.T) {
for i, v := range testBuildSignedURL {
u := buildSignedURL(v.u, v.keyID, v.p, v.customPolicy, v.b64Policy, v.b64Sig)
if u != v.out {
t.Errorf("%d, Unexpected URL\nexpect: %s\nactual: %s\n", i, v.out, u)
}
}
}
var testValidURL = []struct {
in, errPrefix string
}{
{"https://example.com/a?b=1&else=b", ""},
{"https://example.com/a?b=1&Policy=something&else=b", "Policy"},
{"https://example.com/a?b=1&Signature=something&else=b", "Signature"},
{"https://example.com/a?b=1&Key-Pair-Id=something&else=b", "Key-Pair-Id"},
{"http?://example.com/a?b=1", "URL missing valid scheme"},
}
func TestValidateURL(t *testing.T) {
for i, v := range testValidURL {
err := validateURL(v.in)
if err != nil {
if v.errPrefix == "" {
t.Errorf("%d, Unexpected error %s", i, err.Error())
}
if !strings.HasPrefix(err.Error(), v.errPrefix) {
t.Errorf("%d, Expected to find prefix\nexpect: %s\nactual: %s", i, v.errPrefix, err.Error())
}
} else if v.errPrefix != "" {
t.Errorf("%d, Expected error %s", i, v.errPrefix)
}
}
}