Skip to content

Commit 330f11b

Browse files
author
Amir Tocker
committed
Add support for URL authorization token. Refactor AuthToken.
Rename window to duration. Rename end time to expiration. Rename setters.
1 parent 94ea0a4 commit 330f11b

File tree

5 files changed

+253
-32
lines changed

5 files changed

+253
-32
lines changed

cloudinary-core/src/main/java/com/cloudinary/AuthToken.java

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,41 @@
55
import javax.crypto.Mac;
66
import javax.crypto.spec.SecretKeySpec;
77
import javax.xml.bind.DatatypeConverter;
8+
import java.io.UnsupportedEncodingException;
9+
import java.net.URLEncoder;
810
import java.security.InvalidKeyException;
911
import java.security.NoSuchAlgorithmException;
1012
import java.util.ArrayList;
13+
import java.util.Arrays;
1114
import java.util.Calendar;
1215
import java.util.TimeZone;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1318

1419
/**
1520
* Authentication Token generator
1621
*/
1722
public class AuthToken {
18-
public String tokenName = Cloudinary.AKAMAI_TOKEN_NAME;
23+
public static final AuthToken NULL_AUTH_TOKEN = new AuthToken().setNull();
24+
public static final String AUTH_TOKEN_NAME = "__cld_token__";
25+
26+
public String tokenName = AUTH_TOKEN_NAME;
1927
public String key;
2028
public long startTime;
21-
public long endTime;
29+
public long expiration;
2230
public String ip;
2331
public String acl;
24-
public long window;
32+
public long duration;
33+
private boolean isNullToken = false;
34+
35+
public AuthToken() {
36+
}
2537

2638
public AuthToken(String key) {
2739
this.key = key;
2840
}
2941

30-
public AuthToken setTokenName(String tokenName) {
42+
public AuthToken tokenName(String tokenName) {
3143
this.tokenName = tokenName;
3244
return this;
3345
}
@@ -38,41 +50,41 @@ public AuthToken setTokenName(String tokenName) {
3850
* @param startTime in seconds since epoch
3951
* @return
4052
*/
41-
public AuthToken setStartTime(long startTime) {
53+
public AuthToken startTime(long startTime) {
4254
this.startTime = startTime;
4355
return this;
4456
}
4557

4658
/**
4759
* Set the end time (expiration) of the token
4860
*
49-
* @param endTime in seconds since epoch
61+
* @param expiration in seconds since epoch
5062
* @return
5163
*/
52-
public AuthToken setEndTime(long endTime) {
53-
this.endTime = endTime;
64+
public AuthToken expiration(long expiration) {
65+
this.expiration = expiration;
5466
return this;
5567
}
5668

57-
public AuthToken setIp(String ip) {
69+
public AuthToken ip(String ip) {
5870
this.ip = ip;
5971
return this;
6072
}
6173

62-
public AuthToken setAcl(String acl) {
74+
public AuthToken acl(String acl) {
6375
this.acl = acl;
6476
return this;
6577
}
6678

6779
/**
6880
* The duration of the token in seconds. This value is used to calculate the expiration of the token.
69-
* It is ignored if endTime is provided.
81+
* It is ignored if expiration is provided.
7082
*
71-
* @param window
83+
* @param duration in seconds
7284
* @return
7385
*/
74-
public AuthToken setWindow(long window) {
75-
this.window = window;
86+
public AuthToken duration(long duration) {
87+
this.duration = duration;
7688
return this;
7789
}
7890

@@ -86,13 +98,13 @@ public String generate() {
8698
}
8799

88100
public String generate(String url) {
89-
long expiration = endTime;
101+
long expiration = this.expiration;
90102
if (expiration == 0) {
91-
if (window > 0) {
103+
if (duration > 0) {
92104
final long start = startTime > 0 ? startTime : Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L;
93-
expiration = start + window;
105+
expiration = start + duration;
94106
} else {
95-
throw new IllegalArgumentException("Must provide either endTime or window");
107+
throw new IllegalArgumentException("Must provide either expiration or duration");
96108
}
97109
}
98110
ArrayList<String> tokenParts = new ArrayList<String>();
@@ -103,19 +115,56 @@ public String generate(String url) {
103115
tokenParts.add("st=" + startTime);
104116
}
105117
tokenParts.add("exp=" + expiration);
106-
if (url != null) {
107-
tokenParts.add("url=" + url);
108-
} else if (acl != null) {
118+
if (acl != null) {
109119
tokenParts.add("acl=" + acl);
110-
} else {
111-
throw new IllegalArgumentException("Must provide either url or acl");
112120
}
113-
String auth = digest(StringUtils.join(tokenParts, "~"));
121+
ArrayList<String> toSign = new ArrayList<String>(tokenParts);
122+
if (url != null) {
123+
124+
try {
125+
toSign.add("url=" + escapeUrl(url));
126+
} catch (UnsupportedEncodingException e) {
127+
e.printStackTrace();
128+
}
129+
}
130+
String auth = digest(StringUtils.join(toSign, "~"));
114131
tokenParts.add("hmac=" + auth);
115132
return tokenName + "=" + StringUtils.join(tokenParts, "~");
116133

117134
}
118135

136+
/**
137+
* Escape url using lowercase hex code
138+
* @param url a url string
139+
* @return escaped url
140+
* @throws UnsupportedEncodingException see {@link URLEncoder#encode}
141+
*/
142+
private String escapeUrl(String url) throws UnsupportedEncodingException {
143+
String escaped;
144+
StringBuilder sb= new StringBuilder(URLEncoder.encode(url, "UTF-8"));
145+
String regex= "%..";
146+
Pattern p = Pattern.compile(regex); // Create the pattern.
147+
Matcher matcher = p.matcher(sb); // Create the matcher.
148+
while (matcher.find()) {
149+
String buf= sb.substring(matcher.start(), matcher.end()).toLowerCase();
150+
sb.replace(matcher.start(), matcher.end(), buf);
151+
}
152+
escaped = sb.toString();
153+
return escaped;
154+
}
155+
156+
157+
public AuthToken copy() {
158+
final AuthToken authToken = new AuthToken(key);
159+
authToken.tokenName = tokenName;
160+
authToken.startTime = startTime;
161+
authToken.expiration = expiration;
162+
authToken.ip = ip;
163+
authToken.acl = acl;
164+
authToken.duration = duration;
165+
return authToken;
166+
}
167+
119168
private String digest(String message) {
120169
byte[] binKey = DatatypeConverter.parseHexBinary(key);
121170
try {
@@ -132,5 +181,34 @@ private String digest(String message) {
132181
return null;
133182
}
134183

184+
private AuthToken setNull() {
185+
isNullToken = true;
186+
return this;
187+
}
135188

189+
@Override
190+
public boolean equals(Object o) {
191+
if(o instanceof AuthToken) {
192+
AuthToken other = (AuthToken) o;
193+
return (isNullToken && other.isNullToken) ||
194+
key == null ? other.key == null : key.equals(other.key) &&
195+
tokenName.equals(other.tokenName) &&
196+
startTime == other.startTime &&
197+
expiration == other.expiration &&
198+
duration == other.duration &&
199+
(ip == null ? other.ip == null : ip.equals(other.ip)) &&
200+
(acl == null ? other.acl == null : acl.equals(other.acl));
201+
} else {
202+
return false;
203+
}
204+
}
205+
206+
@Override
207+
public int hashCode() {
208+
if(isNullToken) {
209+
return 0;
210+
} else {
211+
return Arrays.asList(tokenName, startTime, expiration, duration, ip, acl).hashCode();
212+
}
213+
}
136214
}

cloudinary-core/src/main/java/com/cloudinary/Configuration.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class Configuration {
3939
public int timeout;
4040
public boolean loadStrategies = true;
4141
public boolean clientHints = false;
42+
public AuthToken authToken;
4243

4344
public Configuration() {
4445
}
@@ -92,6 +93,17 @@ public void update(Map config) {
9293
this.loadStrategies = ObjectUtils.asBoolean(config.get("load_strategies"), true);
9394
this.timeout = ObjectUtils.asInteger(config.get("timeout"), 0);
9495
this.clientHints = ObjectUtils.asBoolean(config.get("client_hints"), false);
96+
Map tokenMap = (Map) config.get("auth_token");
97+
if (tokenMap != null) {
98+
this.authToken = new AuthToken();
99+
this.authToken.tokenName = (String) tokenMap.get("tokenName");
100+
this.authToken.key = (String) tokenMap.get("key");
101+
this.authToken.startTime = ObjectUtils.asLong(tokenMap.get("startTime"), 0L);
102+
this.authToken.expiration = ObjectUtils.asLong(tokenMap.get("expiration"),0L);
103+
this.authToken.ip = (String) tokenMap.get("ip");
104+
this.authToken.acl = (String) tokenMap.get("acl");
105+
this.authToken.duration = ObjectUtils.asLong(tokenMap.get("duration"), 0L);
106+
}
95107
}
96108

97109
@SuppressWarnings("rawtypes")
@@ -115,6 +127,7 @@ public Map<String, Object> asMap() {
115127
map.put("load_strategies", loadStrategies);
116128
map.put("timeout", timeout);
117129
map.put("client_hints", clientHints);
130+
map.put("auth_token", authToken.copy());
118131
return map;
119132
}
120133

@@ -137,6 +150,7 @@ public Configuration(Configuration other) {
137150
this.useRootPath = other.useRootPath;
138151
this.timeout = other.timeout;
139152
this.clientHints = other.clientHints;
153+
this.authToken = other.authToken.copy();
140154
}
141155

142156
/**
@@ -174,6 +188,7 @@ private static Configuration parseConfigUrl(String cloudinaryUrl) {
174188
builder.setPrivateCdn(!StringUtils.isEmpty(cloudinaryUri.getPath()));
175189
builder.setSecureDistribution(cloudinaryUri.getPath());
176190
if (cloudinaryUri.getQuery() != null) {
191+
AuthToken token = null;
177192
for (String param : cloudinaryUri.getQuery().split("&")) {
178193
String[] keyValue = param.split("=");
179194
String val = null;
@@ -197,10 +212,33 @@ private static Configuration parseConfigUrl(String cloudinaryUrl) {
197212
builder.setShorten(ObjectUtils.asBoolean(val, false));
198213
} else if (key.equals("load_strategies")) {
199214
builder.setLoadStrategies(ObjectUtils.asBoolean(val, true));
200-
} else {
215+
} else if (key.matches("auth_token\\[\\w+\\]")){
216+
if (token == null) {
217+
token = new AuthToken();
218+
}
219+
String subKey = key.substring(key.indexOf('[') + 1, key.indexOf(']') +1);
220+
System.out.println("sub key is " + subKey);
221+
if (subKey.equals("tokenName")) {
222+
token.tokenName = val;
223+
} else if (subKey.equals("key")) {
224+
token.key = val;
225+
} else if (subKey.equals("startTime")) {
226+
token.startTime = ObjectUtils.asLong(val, 0L);
227+
} else if (subKey.equals("expiration")) {
228+
token.expiration = ObjectUtils.asLong(val, 0L);
229+
} else if (subKey.equals("ip")) {
230+
token.ip = val;
231+
} else if (subKey.equals("acl")) {
232+
token.acl = val;
233+
} else if (subKey.equals("duration")) {
234+
token.duration = ObjectUtils.asLong(val, 0L);
235+
}
201236
// Log.w("Cloudinary", "ignoring invalid parameter " + val);
202237
}
203238
}
239+
if (token != null) {
240+
builder.setAuthToken(token);
241+
}
204242
}
205243
return builder.build();
206244
}
@@ -227,6 +265,7 @@ public static class Builder {
227265
private boolean loadStrategies = true;
228266
private int timeout;
229267
private boolean clientHints = false;
268+
private AuthToken authToken;
230269

231270
/**
232271
* Set the HTTP connection timeout.
@@ -353,6 +392,11 @@ public Builder setClientHints(boolean clientHints) {
353392
return this;
354393
}
355394

395+
public Builder setAuthToken(AuthToken authToken) {
396+
this.authToken = authToken;
397+
return this;
398+
}
399+
356400
/**
357401
* Initialize builder from existing {@link Configuration}
358402
*
@@ -378,6 +422,7 @@ public Builder from(Configuration other) {
378422
this.loadStrategies = other.loadStrategies;
379423
this.timeout = other.timeout;
380424
this.clientHints = other.clientHints;
425+
this.authToken = other.authToken;
381426
return this;
382427
}
383428
}

cloudinary-core/src/main/java/com/cloudinary/Url.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class Url {
2828
String version = null;
2929
Transformation transformation = null;
3030
boolean signUrl;
31+
private AuthToken authToken;
3132
String source = null;
3233
private String urlSuffix;
3334
private Boolean useRootPath;
@@ -45,6 +46,7 @@ public class Url {
4546
public Url(Cloudinary cloudinary) {
4647
this.cloudinary = cloudinary;
4748
this.config = new Configuration(cloudinary.config);
49+
this.authToken = config.authToken;
4850
}
4951

5052
public Url clone() {
@@ -210,6 +212,11 @@ public Url signed(boolean signUrl) {
210212
return this;
211213
}
212214

215+
public Url authToken(AuthToken authToken) {
216+
this.authToken = authToken;
217+
return this;
218+
}
219+
213220
public Url sourceTransformation(Map<String, Transformation> sourceTransformation) {
214221
this.sourceTransformation = sourceTransformation;
215222
return this;
@@ -346,7 +353,7 @@ public String generate(String source) {
346353
version = "v" + version;
347354

348355

349-
if (signUrl) {
356+
if (signUrl && authToken == null) {
350357
MessageDigest md = null;
351358
try {
352359
md = MessageDigest.getInstance("SHA-1");
@@ -367,7 +374,12 @@ public String generate(String source) {
367374
String finalResourceType = finalizeResourceType(resourceType, type, urlSuffix, useRootPath, config.shorten);
368375
String prefix = unsignedDownloadUrlPrefix(source, config.cloudName, config.privateCdn, config.cdnSubdomain, config.secureCdnSubdomain, config.cname, config.secure, config.secureDistribution);
369376

370-
return StringUtils.join(new String[]{prefix, finalResourceType, signature, transformationStr, version, source}, "/").replaceAll("([^:])\\/+", "$1/");
377+
String s = "/" + StringUtils.join(new String[]{finalResourceType, signature, transformationStr, version, source}, "/").replaceAll("([^:])\\/+", "$1/");
378+
if (signUrl && authToken != null && !authToken.equals(AuthToken.NULL_AUTH_TOKEN)) {
379+
String token = authToken.generate(s);
380+
s = s + "?" + token;
381+
}
382+
return prefix + s;
371383
}
372384

373385
private String[] finalizeSource(String source, String format, String urlSuffix) {

cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,14 @@ public static Integer asInteger(Object value, Integer defaultValue) {
162162
}
163163
}
164164

165+
public static Long asLong(Object value, Long defaultValue) {
166+
if (value == null) {
167+
return defaultValue;
168+
} else if (value instanceof Long) {
169+
return (Long) value;
170+
} else {
171+
return Long.parseLong(value.toString());
172+
}
173+
}
174+
165175
}

0 commit comments

Comments
 (0)