Skip to content

Commit 79c93d2

Browse files
committed
Merge branch 'feature/signed-urls'
2 parents 1fe4141 + 5e138ef commit 79c93d2

9 files changed

Lines changed: 256 additions & 47 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ final TaskResponse uploadImportTaskResponse = asyncCloudConvertClient.importUsin
190190
final TaskResponse waitUploadImportTaskResponse = asyncCloudConvertClient.tasks().wait(uploadImportTaskResponse.getId()).get().getBody();
191191
```
192192

193-
## Signing Webhook
193+
## Verify Webhook Signatures
194194
The node SDK allows to verify webhook requests received from CloudConvert.
195195

196196
```java
@@ -207,6 +207,28 @@ final String signature = "signature";
207207
final boolean isValid = cloudConvertClient.webhooks().verify(payload, signature);
208208
```
209209

210+
## Signed URLs
211+
212+
Signed URLs allow converting files on demand only using URL query parameters. The Java SDK allows to generate such URLs. Therefore, you need to obtain a signed URL base and a signing secret on the [CloudConvert Dashboard](https://cloudconvert.com/dashboard/api/v2/signed-urls).
213+
214+
```java
215+
216+
final String base = "https://s.cloudconvert.com/..."; // You can find it in your signed URL settings.
217+
final String signingSecret = "..."; // You can find it in your signed URL settings.
218+
final String cacheKey = "mykey"; // Allows caching of the result file for 24h
219+
220+
final Map<String, TaskRequest> tasks = ImmutableMap.of(
221+
"import-my-file", new UrlImportRequest().setUrl("import-url"),
222+
"convert-my-file", new ConvertFilesTaskRequest()
223+
.setInput("import-my-file")
224+
.setOutputFormat("pdf")
225+
"export-my-file", new UrlExportRequest().setInput("convert-my-file")
226+
);
227+
228+
229+
final String url = cloudConvertClient.signedUrls().sign(base, signingSecret, tasks, cacheKey);
230+
```
231+
210232
## Unit Tests
211233
```
212234
$ mvn clean install -U -Punit-tests

src/main/java/com/cloudconvert/client/AbstractCloudConvertClient.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@
77
import com.cloudconvert.dto.response.UserResponse;
88
import com.cloudconvert.dto.response.WebhookResponse;
99
import com.cloudconvert.dto.result.AbstractResult;
10-
import com.cloudconvert.resource.AbstractExportFilesResource;
11-
import com.cloudconvert.resource.AbstractFilesResource;
12-
import com.cloudconvert.resource.AbstractImportFilesResource;
13-
import com.cloudconvert.resource.AbstractJobsResource;
14-
import com.cloudconvert.resource.AbstractTasksResource;
15-
import com.cloudconvert.resource.AbstractUsersResource;
16-
import com.cloudconvert.resource.AbstractWebhooksResource;
10+
import com.cloudconvert.resource.*;
1711

1812
import java.io.Closeable;
1913
import java.io.IOException;
@@ -33,20 +27,22 @@ public class AbstractCloudConvertClient<
3327
private final AbstractUsersResource<URAR> abstractUsersResource;
3428
private final AbstractWebhooksResource<WRAR, WRPAR, VAR> abstractWebhooksResource;
3529
private final AbstractFilesResource<ISAR> abstractFilesResource;
30+
private final AbstractSignedUrlResource abstractSignedUrlResource;
3631

3732
public AbstractCloudConvertClient(
38-
final AbstractTasksResource<TRAR, TRPAR, VAR, ORPAR> abstractTasksResource, final AbstractJobsResource<JRAR, JRPAR, VAR> abstractJobsResource,
39-
final AbstractImportFilesResource<TRAR> abstractImportFilesResource, final AbstractExportFilesResource<TRAR> abstractExportFilesResource,
40-
final AbstractUsersResource<URAR> abstractUsersResource, final AbstractWebhooksResource<WRAR, WRPAR, VAR> abstractWebhooksResource,
41-
final AbstractFilesResource<ISAR> abstractFilesResource
42-
) {
33+
final AbstractTasksResource<TRAR, TRPAR, VAR, ORPAR> abstractTasksResource, final AbstractJobsResource<JRAR, JRPAR, VAR> abstractJobsResource,
34+
final AbstractImportFilesResource<TRAR> abstractImportFilesResource, final AbstractExportFilesResource<TRAR> abstractExportFilesResource,
35+
final AbstractUsersResource<URAR> abstractUsersResource, final AbstractWebhooksResource<WRAR, WRPAR, VAR> abstractWebhooksResource,
36+
final AbstractFilesResource<ISAR> abstractFilesResource, final AbstractSignedUrlResource abstractSignedUrlResource
37+
) {
4338
this.abstractTasksResource = abstractTasksResource;
4439
this.abstractJobsResource = abstractJobsResource;
4540
this.abstractImportFilesResource = abstractImportFilesResource;
4641
this.abstractExportFilesResource = abstractExportFilesResource;
4742
this.abstractUsersResource = abstractUsersResource;
4843
this.abstractWebhooksResource = abstractWebhooksResource;
4944
this.abstractFilesResource = abstractFilesResource;
45+
this.abstractSignedUrlResource = abstractSignedUrlResource;
5046
}
5147

5248
public AbstractTasksResource<TRAR, TRPAR, VAR, ORPAR> tasks() {
@@ -77,6 +73,8 @@ public AbstractFilesResource<ISAR> files() {
7773
return abstractFilesResource;
7874
}
7975

76+
public AbstractSignedUrlResource signedUrls() { return abstractSignedUrlResource; }
77+
8078
@Override
8179
public void close() throws IOException {
8280
abstractTasksResource.close();
@@ -86,5 +84,6 @@ public void close() throws IOException {
8684
abstractUsersResource.close();
8785
abstractWebhooksResource.close();
8886
abstractFilesResource.close();
87+
abstractSignedUrlResource.close();
8988
}
9089
}

src/main/java/com/cloudconvert/client/AsyncCloudConvertClient.java

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,7 @@
1313
import com.cloudconvert.dto.result.AsyncResult;
1414
import com.cloudconvert.executor.AsyncRequestExecutor;
1515
import com.cloudconvert.extractor.ResultExtractor;
16-
import com.cloudconvert.resource.async.AsyncCaptureWebsitesResource;
17-
import com.cloudconvert.resource.async.AsyncConvertFilesResource;
18-
import com.cloudconvert.resource.async.AsyncCreateArchivesResource;
19-
import com.cloudconvert.resource.async.AsyncCreateThumbnailsResource;
20-
import com.cloudconvert.resource.async.AsyncExecuteCommandsResource;
21-
import com.cloudconvert.resource.async.AsyncExportFilesResource;
22-
import com.cloudconvert.resource.async.AsyncFilesResource;
23-
import com.cloudconvert.resource.async.AsyncGetMetadataResource;
24-
import com.cloudconvert.resource.async.AsyncImportFilesResource;
25-
import com.cloudconvert.resource.async.AsyncJobsResource;
26-
import com.cloudconvert.resource.async.AsyncMergeFilesResource;
27-
import com.cloudconvert.resource.async.AsyncOptimizeFilesResource;
28-
import com.cloudconvert.resource.async.AsyncTasksResource;
29-
import com.cloudconvert.resource.async.AsyncUsersResource;
30-
import com.cloudconvert.resource.async.AsyncWebhookResource;
31-
import com.cloudconvert.resource.async.AsyncWriteMetadataResource;
16+
import com.cloudconvert.resource.async.*;
3217

3318
import java.io.IOException;
3419
import java.io.InputStream;
@@ -82,7 +67,8 @@ public AsyncCloudConvertClient(
8267
new AsyncExportFilesResource(settingsProvider, objectMapperProvider, asyncRequestExecutor),
8368
new AsyncUsersResource(settingsProvider, objectMapperProvider, asyncRequestExecutor),
8469
new AsyncWebhookResource(settingsProvider, objectMapperProvider, asyncRequestExecutor),
85-
new AsyncFilesResource(settingsProvider, objectMapperProvider, asyncRequestExecutor)
70+
new AsyncFilesResource(settingsProvider, objectMapperProvider, asyncRequestExecutor),
71+
new AsyncSignedUrlResource(settingsProvider, objectMapperProvider, asyncRequestExecutor)
8672
);
8773
}
8874
}

src/main/java/com/cloudconvert/client/CloudConvertClient.java

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,7 @@
1313
import com.cloudconvert.dto.result.Result;
1414
import com.cloudconvert.executor.RequestExecutor;
1515
import com.cloudconvert.extractor.ResultExtractor;
16-
import com.cloudconvert.resource.sync.CaptureWebsitesResource;
17-
import com.cloudconvert.resource.sync.ConvertFilesResource;
18-
import com.cloudconvert.resource.sync.CreateArchivesResource;
19-
import com.cloudconvert.resource.sync.CreateThumbnailsResource;
20-
import com.cloudconvert.resource.sync.ExecuteCommandsResource;
21-
import com.cloudconvert.resource.sync.ExportFilesResource;
22-
import com.cloudconvert.resource.sync.FilesResource;
23-
import com.cloudconvert.resource.sync.GetMetadataResource;
24-
import com.cloudconvert.resource.sync.ImportFilesResource;
25-
import com.cloudconvert.resource.sync.JobsResource;
26-
import com.cloudconvert.resource.sync.MergeFilesResource;
27-
import com.cloudconvert.resource.sync.OptimizeFilesResource;
28-
import com.cloudconvert.resource.sync.TasksResource;
29-
import com.cloudconvert.resource.sync.UsersResource;
30-
import com.cloudconvert.resource.sync.WebhookResource;
31-
import com.cloudconvert.resource.sync.WriteMetadataResource;
16+
import com.cloudconvert.resource.sync.*;
3217

3318
import java.io.IOException;
3419
import java.io.InputStream;
@@ -84,7 +69,8 @@ public CloudConvertClient(
8469
new ExportFilesResource(settingsProvider, objectMapperProvider, requestExecutor),
8570
new UsersResource(settingsProvider, objectMapperProvider, requestExecutor),
8671
new WebhookResource(settingsProvider, objectMapperProvider, requestExecutor),
87-
new FilesResource(settingsProvider, objectMapperProvider, requestExecutor)
72+
new FilesResource(settingsProvider, objectMapperProvider, requestExecutor),
73+
new SignedUrlResource(settingsProvider, objectMapperProvider, requestExecutor)
8874
);
8975
}
9076
}

src/main/java/com/cloudconvert/resource/AbstractResource.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ protected HttpEntity getHttpEntity(
131131
return new ByteArrayEntity(objectMapperProvider.provide().writeValueAsBytes(map), ContentType.APPLICATION_JSON);
132132
}
133133

134+
protected String getJson(final Map<String, Object> map) throws JsonProcessingException {
135+
return objectMapperProvider.provide().writeValueAsString(map);
136+
}
137+
134138
protected HttpUriRequest getHttpUriRequest(
135139
final Class<? extends HttpRequestBase> httpRequestBaseClass, final URI uri
136140
) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.cloudconvert.resource;
2+
3+
import com.cloudconvert.client.mapper.ObjectMapperProvider;
4+
import com.cloudconvert.client.setttings.SettingsProvider;
5+
import com.cloudconvert.dto.request.TaskRequest;
6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.google.common.collect.ImmutableMap;
8+
import org.apache.commons.codec.binary.Base64;
9+
import org.apache.commons.codec.binary.Hex;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import javax.crypto.Mac;
13+
import javax.crypto.spec.SecretKeySpec;
14+
15+
import java.nio.charset.StandardCharsets;
16+
import java.security.InvalidKeyException;
17+
import java.security.NoSuchAlgorithmException;
18+
import java.util.Map;
19+
20+
21+
public abstract class AbstractSignedUrlResource extends AbstractResource {
22+
23+
public static final String HMAC_SHA256 = "HmacSHA256";
24+
25+
26+
public AbstractSignedUrlResource(
27+
final SettingsProvider settingsProvider, final ObjectMapperProvider objectMapperProvider
28+
) {
29+
super(settingsProvider, objectMapperProvider);
30+
31+
32+
}
33+
34+
35+
public String sign(
36+
@NotNull final String base,
37+
@NotNull final String signingSecret,
38+
@NotNull final Map<String, TaskRequest> tasks,
39+
String cacheKey
40+
) throws InvalidKeyException, NoSuchAlgorithmException, JsonProcessingException {
41+
42+
String url = base;
43+
44+
String jobJson = getJson(ImmutableMap.of("tasks", tasks));
45+
46+
String base64Job = Base64.encodeBase64URLSafeString(jobJson.getBytes(StandardCharsets.UTF_8));
47+
48+
url = url.concat("?job=").concat(base64Job);
49+
50+
if (cacheKey != null) {
51+
url = url.concat("&cache_key=").concat(cacheKey);
52+
}
53+
54+
final Mac mac = Mac.getInstance(HMAC_SHA256);
55+
final SecretKeySpec secretKeySpec = new SecretKeySpec(signingSecret.getBytes(), HMAC_SHA256);
56+
mac.init(secretKeySpec);
57+
58+
url = url.concat("&s=").concat(Hex.encodeHexString(mac.doFinal(url.getBytes())));
59+
60+
return url;
61+
}
62+
63+
64+
public String sign(
65+
@NotNull final String base,
66+
@NotNull final String signingSecret,
67+
@NotNull final Map<String, TaskRequest> tasks
68+
) throws InvalidKeyException, NoSuchAlgorithmException, JsonProcessingException {
69+
return this.sign(base, signingSecret, tasks, null);
70+
}
71+
72+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.cloudconvert.resource.async;
2+
3+
import com.cloudconvert.client.mapper.ObjectMapperProvider;
4+
import com.cloudconvert.client.setttings.SettingsProvider;
5+
import com.cloudconvert.executor.AsyncRequestExecutor;
6+
import com.cloudconvert.resource.AbstractSignedUrlResource;
7+
8+
9+
import java.io.IOException;
10+
11+
12+
public class AsyncSignedUrlResource extends AbstractSignedUrlResource {
13+
14+
private final AsyncRequestExecutor asyncRequestExecutor;
15+
16+
public AsyncSignedUrlResource(
17+
final SettingsProvider settingsProvider,
18+
final ObjectMapperProvider objectMapperProvider, final AsyncRequestExecutor asyncRequestExecutor
19+
) {
20+
super(settingsProvider, objectMapperProvider);
21+
22+
this.asyncRequestExecutor = asyncRequestExecutor;
23+
}
24+
25+
@Override
26+
public void close() throws IOException {
27+
asyncRequestExecutor.close();
28+
}
29+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.cloudconvert.resource.sync;
2+
3+
import com.cloudconvert.client.mapper.ObjectMapperProvider;
4+
import com.cloudconvert.client.setttings.SettingsProvider;
5+
import com.cloudconvert.executor.RequestExecutor;
6+
import com.cloudconvert.resource.AbstractSignedUrlResource;
7+
8+
import java.io.IOException;
9+
10+
public class SignedUrlResource extends AbstractSignedUrlResource {
11+
12+
private final RequestExecutor requestExecutor;
13+
14+
public SignedUrlResource(
15+
final SettingsProvider settingsProvider,
16+
final ObjectMapperProvider objectMapperProvider, final RequestExecutor requestExecutor
17+
) {
18+
super(settingsProvider, objectMapperProvider);
19+
20+
this.requestExecutor = requestExecutor;
21+
}
22+
23+
@Override
24+
public void close() throws IOException {
25+
requestExecutor.close();
26+
}
27+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.cloudconvert.test.unit;
2+
3+
import com.cloudconvert.client.CloudConvertClient;
4+
import com.cloudconvert.client.mapper.ObjectMapperProvider;
5+
import com.cloudconvert.client.setttings.SettingsProvider;
6+
import com.cloudconvert.dto.request.ConvertFilesTaskRequest;
7+
import com.cloudconvert.dto.request.TaskRequest;
8+
import com.cloudconvert.dto.request.UrlExportRequest;
9+
import com.cloudconvert.dto.request.UrlImportRequest;
10+
import com.cloudconvert.executor.RequestExecutor;
11+
import com.cloudconvert.test.framework.AbstractTest;
12+
import com.cloudconvert.test.framework.UnitTest;
13+
import com.fasterxml.jackson.core.JsonProcessingException;
14+
import com.google.common.collect.ImmutableMap;
15+
import org.junit.After;
16+
import org.junit.Before;
17+
import org.junit.Test;
18+
import org.junit.experimental.categories.Category;
19+
import org.junit.runner.RunWith;
20+
import org.mockito.Answers;
21+
import org.mockito.Mock;
22+
import org.mockito.junit.MockitoJUnitRunner;
23+
24+
import java.security.InvalidKeyException;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.util.Map;
27+
28+
import static org.mockito.Mockito.*;
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
@Category(UnitTest.class)
32+
@RunWith(MockitoJUnitRunner.class)
33+
public class SignedUrlUnitTest extends AbstractTest {
34+
35+
36+
@Mock
37+
private SettingsProvider settingsProvider;
38+
39+
@Mock
40+
private RequestExecutor requestExecutor;
41+
42+
@Mock(answer = Answers.CALLS_REAL_METHODS)
43+
private ObjectMapperProvider objectMapperProvider;
44+
45+
private CloudConvertClient cloudConvertClient;
46+
47+
@Before
48+
public void before() {
49+
50+
51+
cloudConvertClient = new CloudConvertClient(settingsProvider, objectMapperProvider, requestExecutor);
52+
}
53+
54+
@Test
55+
public void signSignedUrl() throws NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException {
56+
57+
final Map<String, TaskRequest> tasks = ImmutableMap.of(
58+
"import-my-file", new UrlImportRequest().setUrl("import-url"),
59+
"convert-my-file", new ConvertFilesTaskRequest()
60+
.setInput("import-my-file")
61+
.set("width", 100)
62+
.set("height", 100),
63+
"export-my-file", new UrlExportRequest().setInput("convert-my-file")
64+
);
65+
66+
final String base = "https://s.cloudconvert.com/b3d85428-584e-4639-bc11-76b7dee9c109";
67+
final String signingSecret = "NT8dpJkttEyfSk3qlRgUJtvTkx64vhyX";
68+
final String cacheKey = "mykey";
69+
70+
final String url = cloudConvertClient.signedUrls().sign(base, signingSecret, tasks, cacheKey);
71+
72+
assertThat(url).startsWith(base);
73+
assertThat(url).contains("?job=");
74+
assertThat(url).contains("&cache_key=mykey");
75+
assertThat(url).contains("&s=b04d8cf7d65ec56c839443c69dd2bb75e8792e006441019957c0d9824319612a");
76+
77+
}
78+
79+
80+
@After
81+
public void after() throws Exception {
82+
cloudConvertClient.close();
83+
}
84+
}

0 commit comments

Comments
 (0)