sign_url.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. // Package sign provides utilities to generate signed URLs for Amazon CloudFront.
  2. //
  3. // More information about signed URLs and their structure can be found at:
  4. // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html
  5. //
  6. // To sign a URL create a URLSigner with your private key and credential pair key ID.
  7. // Once you have a URLSigner instance you can call Sign or SignWithPolicy to
  8. // sign the URLs.
  9. //
  10. // Example:
  11. //
  12. // // Sign URL to be valid for 1 hour from now.
  13. // signer := sign.NewURLSigner(keyID, privKey)
  14. // signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
  15. // if err != nil {
  16. // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
  17. // }
  18. //
  19. package sign
  20. import (
  21. "crypto/rsa"
  22. "fmt"
  23. "net/url"
  24. "strings"
  25. "time"
  26. )
  27. // An URLSigner provides URL signing utilities to sign URLs for Amazon CloudFront
  28. // resources. Using a private key and Credential Key Pair key ID the URLSigner
  29. // only needs to be created once per Credential Key Pair key ID and private key.
  30. //
  31. // The signer is safe to use concurrently.
  32. type URLSigner struct {
  33. keyID string
  34. privKey *rsa.PrivateKey
  35. }
  36. // NewURLSigner constructs and returns a new URLSigner to be used to for signing
  37. // Amazon CloudFront URL resources with.
  38. func NewURLSigner(keyID string, privKey *rsa.PrivateKey) *URLSigner {
  39. return &URLSigner{
  40. keyID: keyID,
  41. privKey: privKey,
  42. }
  43. }
  44. // Sign will sign a single URL to expire at the time of expires sign using the
  45. // Amazon CloudFront default Canned Policy. The URL will be signed with the
  46. // private key and Credential Key Pair Key ID previously provided to URLSigner.
  47. //
  48. // This is the default method of signing Amazon CloudFront URLs. If extra policy
  49. // conditions are need other than URL expiry use SignWithPolicy instead.
  50. //
  51. // Example:
  52. //
  53. // // Sign URL to be valid for 1 hour from now.
  54. // signer := sign.NewURLSigner(keyID, privKey)
  55. // signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
  56. // if err != nil {
  57. // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
  58. // }
  59. //
  60. func (s URLSigner) Sign(url string, expires time.Time) (string, error) {
  61. scheme, cleanedURL, err := cleanURLScheme(url)
  62. if err != nil {
  63. return "", err
  64. }
  65. resource, err := CreateResource(scheme, url)
  66. if err != nil {
  67. return "", err
  68. }
  69. return signURL(scheme, cleanedURL, s.keyID, NewCannedPolicy(resource, expires), false, s.privKey)
  70. }
  71. // SignWithPolicy will sign a URL with the Policy provided. The URL will be
  72. // signed with the private key and Credential Key Pair Key ID previously provided to URLSigner.
  73. //
  74. // Use this signing method if you are looking to sign a URL with more than just
  75. // the URL's expiry time, or reusing Policies between multiple URL signings.
  76. // If only the expiry time is needed you can use Sign and provide just the
  77. // URL's expiry time.
  78. //
  79. // Note: It is not safe to use Polices between multiple signers concurrently
  80. //
  81. // Example:
  82. //
  83. // // Sign URL to be valid for 30 minutes from now, expires one hour from now, and
  84. // // restricted to the 192.0.2.0/24 IP address range.
  85. // policy := &sign.Policy{
  86. // Statements: []Statement{
  87. // {
  88. // Resource: rawURL,
  89. // Condition: Condition{
  90. // // Optional IP source address range
  91. // IPAddress: &IPAddress{SourceIP: "192.0.2.0/24"},
  92. // // Optional date URL is not valid until
  93. // DateGreaterThan: &AWSEpochTime{time.Now().Add(30 * time.Minute)},
  94. // // Required date the URL will expire after
  95. // DateLessThan: &AWSEpochTime{time.Now().Add(1 * time.Hour)},
  96. // }
  97. // }
  98. // }
  99. // }
  100. //
  101. // signer := sign.NewURLSigner(keyID, privKey)
  102. // signedURL, err := signer.SignWithPolicy(rawURL, policy)
  103. // if err != nil {
  104. // log.Fatalf("Failed to sign url, err: %s\n", err.Error())
  105. // }
  106. //
  107. func (s URLSigner) SignWithPolicy(url string, p *Policy) (string, error) {
  108. scheme, cleanedURL, err := cleanURLScheme(url)
  109. if err != nil {
  110. return "", err
  111. }
  112. return signURL(scheme, cleanedURL, s.keyID, p, true, s.privKey)
  113. }
  114. func signURL(scheme, url, keyID string, p *Policy, customPolicy bool, privKey *rsa.PrivateKey) (string, error) {
  115. // Validation URL elements
  116. if err := validateURL(url); err != nil {
  117. return "", err
  118. }
  119. b64Signature, b64Policy, err := p.Sign(privKey)
  120. if err != nil {
  121. return "", err
  122. }
  123. // build and return signed URL
  124. builtURL := buildSignedURL(url, keyID, p, customPolicy, b64Policy, b64Signature)
  125. if scheme == "rtmp" {
  126. return buildRTMPURL(builtURL)
  127. }
  128. return builtURL, nil
  129. }
  130. func buildSignedURL(baseURL, keyID string, p *Policy, customPolicy bool, b64Policy, b64Signature []byte) string {
  131. pred := "?"
  132. if strings.Contains(baseURL, "?") {
  133. pred = "&"
  134. }
  135. signedURL := baseURL + pred
  136. if customPolicy {
  137. signedURL += "Policy=" + string(b64Policy)
  138. } else {
  139. signedURL += fmt.Sprintf("Expires=%d", p.Statements[0].Condition.DateLessThan.UTC().Unix())
  140. }
  141. signedURL += fmt.Sprintf("&Signature=%s&Key-Pair-Id=%s", string(b64Signature), keyID)
  142. return signedURL
  143. }
  144. func buildRTMPURL(u string) (string, error) {
  145. parsed, err := url.Parse(u)
  146. if err != nil {
  147. return "", fmt.Errorf("unable to parse rtmp signed URL, err: %s", err)
  148. }
  149. rtmpURL := strings.TrimLeft(parsed.Path, "/")
  150. if parsed.RawQuery != "" {
  151. rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
  152. }
  153. return rtmpURL, nil
  154. }
  155. func cleanURLScheme(u string) (scheme, cleanedURL string, err error) {
  156. parts := strings.SplitN(u, "://", 2)
  157. if len(parts) != 2 {
  158. return "", "", fmt.Errorf("invalid URL, missing scheme and domain/path")
  159. }
  160. scheme = strings.Replace(parts[0], "*", "", 1)
  161. cleanedURL = fmt.Sprintf("%s://%s", scheme, parts[1])
  162. return strings.ToLower(scheme), cleanedURL, nil
  163. }
  164. var illegalQueryParms = []string{"Expires", "Policy", "Signature", "Key-Pair-Id"}
  165. func validateURL(u string) error {
  166. parsed, err := url.Parse(u)
  167. if err != nil {
  168. return fmt.Errorf("unable to parse URL, err: %s", err.Error())
  169. }
  170. if parsed.Scheme == "" {
  171. return fmt.Errorf("URL missing valid scheme, %s", u)
  172. }
  173. q := parsed.Query()
  174. for _, p := range illegalQueryParms {
  175. if _, ok := q[p]; ok {
  176. return fmt.Errorf("%s cannot be a query parameter for a signed URL", p)
  177. }
  178. }
  179. return nil
  180. }