Skip to content

Commit

Permalink
Add method for generating signed Smart CDN URLs (#38)
Browse files Browse the repository at this point in the history
* @Acconut Add method for generating signed Smart CDN URLs

* Make it backwards compatible with older Go versions

* Support duplicate URL parameters

* Use expiration timestamp
  • Loading branch information
Acconut authored Nov 28, 2024
1 parent 68e8b4d commit 613496c
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 0 deletions.
67 changes: 67 additions & 0 deletions transloadit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/json"
Expand All @@ -14,6 +15,8 @@ import (
"math/rand"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -237,3 +240,67 @@ func getExpireString() string {
expiresStr := fmt.Sprintf("%04d/%02d/%02d %02d:%02d:%02d+00:00", expires.Year(), expires.Month(), expires.Day(), expires.Hour(), expires.Minute(), expires.Second())
return string(expiresStr)
}

// SignedSmartCDNUrlOptions contains options for creating a signed Smart CDN URL
type SignedSmartCDNUrlOptions struct {
// Workspace slug
Workspace string
// Template slug or template ID
Template string
// Input value that is provided as `${fields.input}` in the template
Input string
// Additional parameters for the URL query string. Can be nil.
URLParams url.Values
// Expiration timestamp of the signature. Defaults to 1 hour from now if left unset.
ExpiresAt time.Time
}

// CreateSignedSmartCDNUrl constructs a signed Smart CDN URL.
// See https://transloadit.com/docs/topics/signature-authentication/#smart-cdn
func (client *Client) CreateSignedSmartCDNUrl(opts SignedSmartCDNUrlOptions) string {
workspaceSlug := url.PathEscape(opts.Workspace)
templateSlug := url.PathEscape(opts.Template)
inputField := url.PathEscape(opts.Input)

var expiresAt int64
if !opts.ExpiresAt.IsZero() {
expiresAt = opts.ExpiresAt.Unix() * 1000
} else {
expiresAt = time.Now().Add(time.Hour).Unix() * 1000 // 1 hour
}

queryParams := make(url.Values, len(opts.URLParams)+2)
for key, values := range opts.URLParams {
queryParams[key] = values
}

queryParams.Set("auth_key", client.config.AuthKey)
queryParams.Set("exp", strconv.FormatInt(expiresAt, 10))

// Build query string with sorted keys
queryParamsKeys := make([]string, 0, len(queryParams))
for k := range queryParams {
queryParamsKeys = append(queryParamsKeys, k)
}
sort.Strings(queryParamsKeys)

var queryParts []string
for _, k := range queryParamsKeys {
for _, v := range queryParams[k] {
queryParts = append(queryParts, url.QueryEscape(k)+"="+url.QueryEscape(v))
}
}
queryString := strings.Join(queryParts, "&")

stringToSign := fmt.Sprintf("%s/%s/%s?%s", workspaceSlug, templateSlug, inputField, queryString)

// Create signature using SHA-256
hash := hmac.New(sha256.New, []byte(client.config.AuthSecret))
hash.Write([]byte(stringToSign))
signature := url.QueryEscape("sha256:" + hex.EncodeToString(hash.Sum(nil)))

signedURL := fmt.Sprintf("https://%s.tlcdn.com/%s/%s?%s&sig=%s",
workspaceSlug, templateSlug, inputField, queryString, signature)

return signedURL
}
27 changes: 27 additions & 0 deletions transloadit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"net/url"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -120,3 +121,29 @@ func generateTemplateName() string {
}
return "gosdk-" + string(b)
}

func TestCreateSignedSmartCDNUrl(t *testing.T) {
client := NewClient(Config{
AuthKey: "foo_key",
AuthSecret: "foo_secret",
})

params := url.Values{}
params.Add("foo", "bar")
params.Add("aaa", "42") // This must be sorted before `foo`
params.Add("aaa", "21")

url := client.CreateSignedSmartCDNUrl(SignedSmartCDNUrlOptions{
Workspace: "foo_workspace",
Template: "foo_template",
Input: "foo/input",
URLParams: params,
ExpiresAt: time.Date(2024, 5, 1, 1, 0, 0, 0, time.UTC),
})

expected := "https://foo_workspace.tlcdn.com/foo_template/foo%2Finput?aaa=42&aaa=21&auth_key=foo_key&exp=1714525200000&foo=bar&sig=sha256%3A9a8df3bb28eea621b46ec808a250b7903b2546be7e66c048956d4f30b8da7519"

if url != expected {
t.Errorf("Expected URL:\n%s\nGot:\n%s", expected, url)
}
}

0 comments on commit 613496c

Please sign in to comment.