Skip to content

Commit

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

* Fix linter

* Avoid using newer API

* Support duplicate URL params

* Fix Linting Errors

* Fix Linting Errors

* Fix Line Length Constraint in Test-File

* Use absolute expiration timestamp

---------

Co-authored-by: florian <[email protected]>
  • Loading branch information
Acconut and cdr-chakotay authored Nov 28, 2024
1 parent 27bdf97 commit f698f64
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 6 deletions.
83 changes: 83 additions & 0 deletions src/main/java/com/transloadit/sdk/Transloadit.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,28 @@
import com.transloadit.sdk.response.AssemblyResponse;
import com.transloadit.sdk.response.ListResponse;
import com.transloadit.sdk.response.Response;
import org.apache.commons.codec.binary.Hex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;

/**
* This class serves as a client interface to the Transloadit API.
Expand Down Expand Up @@ -430,4 +443,74 @@ public void setRetryDelay(int delay) throws LocalOperationException {
this.retryDelay = delay;
}
}

/**
* Construct a signed Smart CDN URL.
* See the <a href="https://transloadit.com/docs/topics/signature-authentication/#smart-cdn">API documentation</a>.
* Same as {@link Transloadit#getSignedSmartCDNUrl(String, String, String, Map, long)}, but with an expiration in 1 hour.
*
* @param workspace Workspace slug
* @param template Template slug or template ID
* @param input Input value that is provided as ${fields.input} in the template
* @param urlParams Additional parameters for the URL query string (optional)
* @return The signed Smart CDN URL
*/
public String getSignedSmartCDNUrl(@NotNull String workspace, @NotNull String template, @NotNull String input,
@Nullable Map<String, List<String>> urlParams) throws LocalOperationException {
// 1 hours default expiration
long expiresAt = Instant.now().toEpochMilli() + 60 * 60 * 1000;
return getSignedSmartCDNUrl(workspace, template, input, urlParams, expiresAt);
}

/**
* Construct a signed Smart CDN URL.
* See the <a href="https://transloadit.com/docs/topics/signature-authentication/#smart-cdn">API documentation</a>.
*
* @param workspace Workspace slug
* @param template Template slug or template ID
* @param input Input value that is provided as ${fields.input} in the template
* @param urlParams Additional parameters for the URL query string (optional)
* @param expiresAt Expiration timestamp of the signature in milliseconds since the UNIX epoch.
* @return The signed Smart CDN URL
*/
public String getSignedSmartCDNUrl(@NotNull String workspace, @NotNull String template, @NotNull String input,
@Nullable Map<String, List<String>> urlParams, long expiresAt) throws LocalOperationException {

try {
String workspaceSlug = URLEncoder.encode(workspace, StandardCharsets.UTF_8.name());
String templateSlug = URLEncoder.encode(template, StandardCharsets.UTF_8.name());
String inputField = URLEncoder.encode(input, StandardCharsets.UTF_8.name());

// Use TreeMap to ensure keys in URL params are sorted.
SortedMap<String, List<String>> params = new TreeMap<>(urlParams);
params.put("auth_key", Collections.singletonList(this.key));
params.put("exp", Collections.singletonList(String.valueOf(expiresAt)));

List<String> queryParts = new ArrayList<>(params.size());
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
String key = entry.getKey();
for (String value : entry.getValue()) {
queryParts.add(URLEncoder.encode(key, StandardCharsets.UTF_8.name()) + "=" + URLEncoder.encode(
value, StandardCharsets.UTF_8.name()));
}
}

String queryString = String.join("&", queryParts);
String stringToSign = workspaceSlug + "/" + templateSlug + "/" + inputField + "?" + queryString;

Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
this.secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmac.init(secretKey);
byte[] signatureBytes = hmac.doFinal(stringToSign.getBytes());
byte[] signatureHexBytes = new Hex().encode((signatureBytes));
String signature = "sha256:" + new String(signatureHexBytes, StandardCharsets.UTF_8);
String signatureEncoded = URLEncoder.encode(signature, StandardCharsets.UTF_8.name());

return "https://" + workspaceSlug + ".tlcdn.com/" + templateSlug + "/"
+ inputField + "?" + queryString + "&sig=" + signatureEncoded;
} catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new LocalOperationException("Failed to create signature: " + e.getMessage());
}
}
}
33 changes: 27 additions & 6 deletions src/test/java/com/transloadit/sdk/TransloaditTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
import org.mockserver.model.HttpResponse;

import java.io.IOException;
import java.util.HashMap;
//CHECKSTYLE:OFF
import java.util.Map; // Suppress warning as the Map import is needed for the JavaDoc Comments
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//CHECKSTYLE:ON



/**
* Unit test for {@link Transloadit} class. Api-Responses are simulated by mocking the server's response.
Expand Down Expand Up @@ -281,5 +281,26 @@ public void loadVersionInfo() {
Matcher matcher = versionPattern.matcher(info);
Assertions.assertTrue(matcher.find());
}

/**
* Test if the SDK can generate a correct signed Smart CDN URL.
*/
@Test
@SuppressWarnings("checkstyle:linelength")
public void getSignedSmartCDNURL() throws LocalOperationException {
Transloadit client = new Transloadit("foo_key", "foo_secret");
Map<String, List<String>> params = new HashMap<>();
params.put("foo", Collections.singletonList("bar"));
params.put("aaa", Arrays.asList("42", "21")); // Must be sorted before `foo`

String url = client.getSignedSmartCDNUrl(
"foo_workspace",
"foo_template",
"foo/input",
params,
Instant.parse("2024-05-01T01:00:00.000Z").toEpochMilli()
);
Assertions.assertEquals("https://foo_workspace.tlcdn.com/foo_template/foo%2Finput?aaa=42&aaa=21&auth_key=foo_key&exp=1714525200000&foo=bar&sig=sha256%3A9a8df3bb28eea621b46ec808a250b7903b2546be7e66c048956d4f30b8da7519", url);
}
}

0 comments on commit f698f64

Please sign in to comment.