Skip to content

Commit 3ef9eba

Browse files
nitzanjtocker
authored andcommitted
Fix url encoding for AuthToken generation
1 parent 62040de commit 3ef9eba

File tree

3 files changed

+76
-39
lines changed

3 files changed

+76
-39
lines changed

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

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import javax.crypto.spec.SecretKeySpec;
88
import java.io.UnsupportedEncodingException;
99
import java.net.URLEncoder;
10+
import java.nio.charset.Charset;
1011
import java.security.InvalidKeyException;
1112
import java.security.NoSuchAlgorithmException;
1213
import java.util.*;
@@ -31,6 +32,7 @@ public class AuthToken {
3132
private String acl;
3233
private long duration;
3334
private boolean isNullToken = false;
35+
private static final Pattern UNSAFE_URL_CHARS_PATTERN = Pattern.compile("[ \"#%&'/:;<=>?@\\[\\\\\\]^`{|}~]");
3436

3537
public AuthToken() {
3638
}
@@ -46,10 +48,10 @@ public AuthToken(String key) {
4648
*/
4749
public AuthToken(Map options) {
4850
if (options != null) {
49-
this.tokenName = ObjectUtils.asString( options.get("tokenName"), this.tokenName);
51+
this.tokenName = ObjectUtils.asString(options.get("tokenName"), this.tokenName);
5052
this.key = (String) options.get("key");
5153
this.startTime = ObjectUtils.asLong(options.get("startTime"), 0L);
52-
this.expiration = ObjectUtils.asLong(options.get("expiration"),0L);
54+
this.expiration = ObjectUtils.asLong(options.get("expiration"), 0L);
5355
this.ip = (String) options.get("ip");
5456
this.acl = (String) options.get("acl");
5557
this.duration = ObjectUtils.asLong(options.get("duration"), 0L);
@@ -59,6 +61,7 @@ public AuthToken(Map options) {
5961

6062
/**
6163
* Create a new AuthToken configuration overriding the default token name.
64+
*
6265
* @param tokenName the name of the token. must be supported by the server.
6366
* @return this
6467
*/
@@ -91,6 +94,7 @@ public AuthToken expiration(long expiration) {
9194

9295
/**
9396
* Set the ip of the client
97+
*
9498
* @param ip
9599
* @return this
96100
*/
@@ -101,6 +105,7 @@ public AuthToken ip(String ip) {
101105

102106
/**
103107
* Define an ACL for a cookie token
108+
*
104109
* @param acl
105110
* @return this
106111
*/
@@ -132,6 +137,7 @@ public String generate() {
132137

133138
/**
134139
* Generate a URL token for the given URL.
140+
*
135141
* @param url the URL to be authorized
136142
* @return a URL token
137143
*/
@@ -168,32 +174,18 @@ public String generate(String url) {
168174

169175
/**
170176
* Escape url using lowercase hex code
177+
*
171178
* @param url a url string
172179
* @return escaped url
173180
*/
174181
private String escapeToLower(String url) {
175-
String escaped;
176-
String encodedUrl = null;
177-
try {
178-
encodedUrl = URLEncoder.encode(url, "UTF-8");
179-
} catch (UnsupportedEncodingException e) {
180-
throw new RuntimeException("Cannot escape string.", e);
181-
}
182-
StringBuilder sb= new StringBuilder(encodedUrl);
183-
String regex= "%..";
184-
Pattern p = Pattern.compile(regex); // Create the pattern.
185-
Matcher matcher = p.matcher(sb); // Create the matcher.
186-
while (matcher.find()) {
187-
String buf= sb.substring(matcher.start(), matcher.end()).toLowerCase();
188-
sb.replace(matcher.start(), matcher.end(), buf);
189-
}
190-
escaped = sb.toString();
191-
return escaped;
182+
String encodedUrl = StringUtils.urlEncode(url, UNSAFE_URL_CHARS_PATTERN, Charset.forName("UTF-8"));
183+
return encodedUrl;
192184
}
193185

194-
195186
/**
196187
* Create a copy of this AuthToken
188+
*
197189
* @return a new AuthToken object
198190
*/
199191
public AuthToken copy() {
@@ -209,11 +201,12 @@ public AuthToken copy() {
209201

210202
/**
211203
* Merge this token with another, creating a new token. Other's members who are not <code>null</code> or <code>0</code> will override this object's members.
204+
*
212205
* @param other the token to merge from
213206
* @return a new token
214207
*/
215208
public AuthToken merge(AuthToken other) {
216-
if(other.equals(NULL_AUTH_TOKEN)) {
209+
if (other.equals(NULL_AUTH_TOKEN)) {
217210
// NULL_AUTH_TOKEN can't merge
218211
return other;
219212
}
@@ -250,24 +243,24 @@ private AuthToken setNull() {
250243

251244
@Override
252245
public boolean equals(Object o) {
253-
if(o instanceof AuthToken) {
246+
if (o instanceof AuthToken) {
254247
AuthToken other = (AuthToken) o;
255-
return (isNullToken && other.isNullToken) ||
248+
return (isNullToken && other.isNullToken) ||
256249
(key == null ? other.key == null : key.equals(other.key)) &&
257-
tokenName.equals(other.tokenName) &&
258-
startTime == other.startTime &&
259-
expiration == other.expiration &&
260-
duration == other.duration &&
261-
(ip == null ? other.ip == null : ip.equals(other.ip)) &&
262-
(acl == null ? other.acl == null : acl.equals(other.acl));
250+
tokenName.equals(other.tokenName) &&
251+
startTime == other.startTime &&
252+
expiration == other.expiration &&
253+
duration == other.duration &&
254+
(ip == null ? other.ip == null : ip.equals(other.ip)) &&
255+
(acl == null ? other.acl == null : acl.equals(other.acl));
263256
} else {
264257
return false;
265258
}
266259
}
267260

268261
@Override
269262
public int hashCode() {
270-
if(isNullToken) {
263+
if (isNullToken) {
271264
return 0;
272265
} else {
273266
return Arrays.asList(tokenName, startTime, expiration, duration, ip, acl).hashCode();

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

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
import java.io.ByteArrayOutputStream;
44
import java.io.IOException;
55
import java.io.InputStream;
6+
import java.nio.charset.Charset;
67
import java.util.Collection;
78
import java.util.List;
9+
import java.util.regex.Matcher;
10+
import java.util.regex.Pattern;
811

912
public class StringUtils {
1013
public static final String EMPTY = "";
1114

1215
/**
1316
* Join a list of Strings
14-
* @param list strings to join
17+
*
18+
* @param list strings to join
1519
* @param separator the separator to insert between the strings
1620
* @return a string made of the strings in list separated by separator
1721
*/
@@ -25,7 +29,8 @@ public static String join(List<String> list, String separator) {
2529

2630
/**
2731
* Join a array of Strings
28-
* @param array strings to join
32+
*
33+
* @param array strings to join
2934
* @param separator the separator to insert between the strings
3035
* @return a string made of the strings in array separated by separator
3136
*/
@@ -38,8 +43,9 @@ public static String join(Object[] array, String separator) {
3843

3944
/**
4045
* Join a collection of Strings
46+
*
4147
* @param collection strings to join
42-
* @param separator the separator to insert between the strings
48+
* @param separator the separator to insert between the strings
4349
* @return a string made of the strings in collection separated by separator
4450
*/
4551
public static String join(Collection<String> collection, String separator) {
@@ -51,10 +57,11 @@ public static String join(Collection<String> collection, String separator) {
5157

5258
/**
5359
* Join a array of Strings from startIndex to endIndex
54-
* @param array strings to join
55-
* @param separator the separator to insert between the strings
60+
*
61+
* @param array strings to join
62+
* @param separator the separator to insert between the strings
5663
* @param startIndex the string to start from
57-
* @param endIndex the last string to join
64+
* @param endIndex the last string to join
5865
* @return a string made of the strings in array separated by separator
5966
*/
6067
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
@@ -87,6 +94,7 @@ public static String join(final Object[] array, String separator, final int star
8794

8895
/**
8996
* Convert an array of bytes to a string of hex values
97+
*
9098
* @param bytes bytes to convert
9199
* @return a string of hex values.
92100
*/
@@ -102,6 +110,7 @@ public static String encodeHexString(byte[] bytes) {
102110

103111
/**
104112
* Convert a string of hex values to an array of bytes
113+
*
105114
* @param s a string of two digit Hex numbers. The length of string to parse must be even.
106115
* @return bytes representation of the string
107116
*/
@@ -125,14 +134,15 @@ public static byte[] hexStringToByteArray(String s) {
125134
*
126135
* @param input The String to escape
127136
* @return The escaped String
128-
* @see HtmlEscape#escapeTextArea(String)
137+
* @see HtmlEscape#escapeTextArea(String)
129138
*/
130139
public static String escapeHtml(String input) {
131140
return HtmlEscape.escapeTextArea(input);
132141
}
133142

134143
/**
135144
* Verify that the input has non whitespace characters in it
145+
*
136146
* @param input a String-like object
137147
* @return true if input has non whitespace characters in it
138148
*/
@@ -143,6 +153,7 @@ public static boolean isNotBlank(Object input) {
143153

144154
/**
145155
* Verify that the input has non whitespace characters in it
156+
*
146157
* @param input a String
147158
* @return true if input has non whitespace characters in it
148159
*/
@@ -152,6 +163,7 @@ public static boolean isNotBlank(String input) {
152163

153164
/**
154165
* Verify that the input has no characters
166+
*
155167
* @param input a string
156168
* @return true if input is null or has no characters
157169
*/
@@ -161,7 +173,8 @@ public static boolean isEmpty(String input) {
161173

162174
/**
163175
* Verify that the input is an empty string or contains only whitespace characters.<br>
164-
* see {@link Character#isWhitespace(char)}
176+
* see {@link Character#isWhitespace(char)}
177+
*
165178
* @param input a string
166179
* @return true if input is an empty string or contains only whitespace characters
167180
*/
@@ -180,6 +193,7 @@ public static boolean isBlank(String input) {
180193

181194
/**
182195
* Read the entire input stream in 1KB chunks
196+
*
183197
* @param in input stream to read from
184198
* @return a String generated from the input stream
185199
* @throws IOException thrown by the input stream
@@ -198,4 +212,34 @@ public static boolean isRemoteUrl(String file) {
198212
return file.matches("ftp:.*|https?:.*|s3:.*|data:[^;]*;base64,([a-zA-Z0-9/+\n=]+)");
199213
}
200214

215+
/**
216+
* Replaces the unsafe characters in url with url-encoded values.
217+
* This is based on {@link java.net.URLEncoder#encode(String, String)}
218+
* @param url The url to encode
219+
* @param unsafe Regex pattern of unsafe caracters
220+
* @param charset
221+
* @return An encoded url string
222+
*/
223+
public static String urlEncode(String url, Pattern unsafe, Charset charset) {
224+
StringBuffer sb = new StringBuffer(url.length());
225+
Matcher matcher = unsafe.matcher(url);
226+
while (matcher.find()) {
227+
String str = matcher.group(0);
228+
byte[] bytes = str.getBytes(charset);
229+
StringBuilder escaped = new StringBuilder(str.length() * 3);
230+
231+
for (byte aByte : bytes) {
232+
escaped.append('%');
233+
char ch = Character.forDigit((aByte >> 4) & 0xF, 16);
234+
escaped.append(ch);
235+
ch = Character.forDigit(aByte & 0xF, 16);
236+
escaped.append(ch);
237+
}
238+
239+
matcher.appendReplacement(sb, Matcher.quoteReplacement(escaped.toString().toLowerCase()));
240+
}
241+
242+
matcher.appendTail(sb);
243+
return sb.toString();
244+
}
201245
}

cloudinary-core/src/test/java/com/cloudinary/AuthTokenTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public void testAuthenticatedUrl() {
8383

8484
message = "explicit authToken should override global setting";
8585
url = cloudinary.url().signed(true).authToken(new AuthToken(ALT_KEY).startTime(222222222).duration(100)).resourceType("image").type("authenticated").transformation(new Transformation().crop("scale").width(300)).generate("sample.jpg");
86-
assertEquals(message,"http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=7d276841d70c4ecbd0708275cd6a82e1f08e47838fbb0bceb2538e06ddfa3029", url);
86+
assertEquals(message,"http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=55cfe516530461213fe3b3606014533b1eca8ff60aeab79d1bb84c9322eebc1f", url);
8787

8888
message = "should compute expiration as start time + duration";
8989
url = cloudinary.url().signed(true).authToken(new AuthToken().startTime(11111111).duration(300))

0 commit comments

Comments
 (0)