From 613496c1d6d47e554d15807d28922f2e486c678d Mon Sep 17 00:00:00 2001 From: Marius Kleidl <1375043+Acconut@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:07:57 +0100 Subject: [PATCH] Add method for generating signed Smart CDN URLs (#38) * @Acconut Add method for generating signed Smart CDN URLs * Make it backwards compatible with older Go versions * Support duplicate URL parameters * Use expiration timestamp --- transloadit.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ transloadit_test.go | 27 ++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/transloadit.go b/transloadit.go index 51f7a9c..588e697 100755 --- a/transloadit.go +++ b/transloadit.go @@ -5,6 +5,7 @@ import ( "context" "crypto/hmac" "crypto/sha1" + "crypto/sha256" "crypto/sha512" "encoding/hex" "encoding/json" @@ -14,6 +15,8 @@ import ( "math/rand" "net/http" "net/url" + "sort" + "strconv" "strings" "time" ) @@ -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 +} diff --git a/transloadit_test.go b/transloadit_test.go index aeb0cdf..d4eb73b 100755 --- a/transloadit_test.go +++ b/transloadit_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/rand" + "net/url" "os" "strings" "testing" @@ -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) + } +}