Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method for generating signed Smart CDN URLs #92

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions src/main/java/com/transloadit/sdk/Transloadit.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@
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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.time.Clock;

/**
* This class serves as a client interface to the Transloadit API.
Expand All @@ -36,6 +42,7 @@ public class Transloadit {
protected ArrayList<String> qualifiedErrorsForRetry;
protected int retryDelay = 0; // default value
protected String versionInfo;
Clock clock = Clock.systemUTC(); // for faking time in tests

/**
* A new instance to transloadit client.
Expand Down Expand Up @@ -430,4 +437,67 @@ 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, int)}, 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, String> urlParams) throws LocalOperationException {
// 1 hours default expiration
return getSignedSmartCDNUrl(workspace, template, input, urlParams, 60 * 60 * 1000);
}

/**
* 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 expiresIn Expiration time of the signature in milliseconds. Defaults to 1 hour.
* @return The signed Smart CDN URL
*/
public String getSignedSmartCDNUrl(@NotNull String workspace, @NotNull String template, @NotNull String input,
@Nullable Map<String, String> urlParams, int expiresIn) 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, String> params = new TreeMap<>(urlParams);
params.put("auth_key", this.key);
params.put("exp", String.valueOf(clock.millis() + expiresIn));

List<String> queryParts = new ArrayList<>(params.size());
for (Map.Entry<String, String> entry : params.entrySet()) {
queryParts.add(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()) + "=" + URLEncoder.encode(
entry.getValue(), 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);

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

import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.HashMap;
//CHECKSTYLE:OFF
import java.util.Map; // Suppress warning as the Map import is needed for the JavaDoc Comments
Expand Down Expand Up @@ -281,5 +284,28 @@ 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
public void getSignedSmartCDNURL() throws LocalOperationException {
Transloadit client = new Transloadit("foo_key", "foo_secret");
client.clock = Clock.fixed(Instant.parse("2024-05-01T00:00:00.000Z"), ZoneOffset.UTC);
Map<String, String> params = new HashMap<>();
params.put("foo", "bar");
params.put("aaa", "42"); // Must be sorted to be the first URL param

String url = client.getSignedSmartCDNUrl(
"foo_workspace",
"foo_template",
"foo/input",
params
);

//CHECKSTYLE:OFF
Assertions.assertEquals("https://foo_workspace.tlcdn.com/foo_template/foo%2Finput?aaa=42&auth_key=foo_key&exp=1714525200000&foo=bar&sig=sha256:995dd1aae135fb77fa98b0e6946bd9768e0443a6028eba0361c03807e8fb68a5", url);
//CHECKSTYLE:ON
}
}

Loading