Skip to content

Commit 96bba69

Browse files
committed
Optimize OAuthSignatureCalculator , close AsyncHttpClient#1333
Motivation: * `ThreadSafeHMAC` is synchronized. `NONCE_BUFFER` uses a `ThreadLocal`, why not use a global one so Mac doesn't need synchronization? * `OAuthParameterSet. sortAndConcat` allocates a `StringBuilder` * `OAuthParameterSet. sortAndConcat` allocates a an Array. Modifications: * Introduce OAuthSignatureCalculatorInstance and have OAuthSignatureCalculator delegate * Use `Collections.sort` to sort parameters Result: Less allocation. No more synchronized block.
1 parent d201d8f commit 96bba69

File tree

8 files changed

+452
-426
lines changed

8 files changed

+452
-426
lines changed

client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java

Lines changed: 17 additions & 270 deletions
Original file line numberDiff line numberDiff line change
@@ -13,300 +13,47 @@
1313
*/
1414
package org.asynchttpclient.oauth;
1515

16-
import static java.nio.charset.StandardCharsets.UTF_8;
17-
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
16+
import java.security.InvalidKeyException;
17+
import java.security.NoSuchAlgorithmException;
1818

19-
import java.nio.ByteBuffer;
20-
import java.util.ArrayList;
21-
import java.util.Arrays;
22-
import java.util.List;
23-
import java.util.concurrent.ThreadLocalRandom;
24-
import java.util.regex.Pattern;
25-
26-
import org.asynchttpclient.Param;
2719
import org.asynchttpclient.Request;
2820
import org.asynchttpclient.RequestBuilderBase;
2921
import org.asynchttpclient.SignatureCalculator;
30-
import org.asynchttpclient.uri.Uri;
31-
import org.asynchttpclient.util.Base64;
32-
import org.asynchttpclient.util.StringUtils;
33-
import org.asynchttpclient.util.Utf8UrlEncoder;
3422

3523
/**
36-
* Simple OAuth signature calculator that can used for constructing client signatures for accessing services that use OAuth for authorization. <br>
37-
* Supports most common signature inclusion and calculation methods: HMAC-SHA1 for calculation, and Header inclusion as inclusion method. Nonce generation uses simple random
38-
* numbers with base64 encoding.
39-
*
40-
* @author tatu ([email protected])
24+
* OAuth {@link SignatureCalculator} that delegates to {@link OAuthSignatureCalculatorInstance}s.
4125
*/
4226
public class OAuthSignatureCalculator implements SignatureCalculator {
43-
public final static String HEADER_AUTHORIZATION = "Authorization";
44-
45-
private static final String KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key";
46-
private static final String KEY_OAUTH_NONCE = "oauth_nonce";
47-
private static final String KEY_OAUTH_SIGNATURE = "oauth_signature";
48-
private static final String KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
49-
private static final String KEY_OAUTH_TIMESTAMP = "oauth_timestamp";
50-
private static final String KEY_OAUTH_TOKEN = "oauth_token";
51-
private static final String KEY_OAUTH_VERSION = "oauth_version";
5227

53-
private static final String OAUTH_VERSION_1_0 = "1.0";
54-
private static final String OAUTH_SIGNATURE_METHOD = "HMAC-SHA1";
55-
56-
protected static final ThreadLocal<byte[]> NONCE_BUFFER = new ThreadLocal<byte[]>() {
57-
protected byte[] initialValue() {
58-
return new byte[16];
59-
}
28+
private static final ThreadLocal<OAuthSignatureCalculatorInstance> INSTANCES = new ThreadLocal<OAuthSignatureCalculatorInstance>() {
29+
protected OAuthSignatureCalculatorInstance initialValue() {
30+
try {
31+
return new OAuthSignatureCalculatorInstance();
32+
} catch (NoSuchAlgorithmException e) {
33+
throw new ExceptionInInitializerError(e);
34+
}
35+
};
6036
};
6137

62-
protected final ThreadSafeHMAC mac;
63-
64-
protected final ConsumerKey consumerAuth;
38+
private final ConsumerKey consumerAuth;
6539

66-
protected final RequestToken userAuth;
40+
private final RequestToken userAuth;
6741

6842
/**
6943
* @param consumerAuth Consumer key to use for signature calculation
7044
* @param userAuth Request/access token to use for signature calculation
7145
*/
7246
public OAuthSignatureCalculator(ConsumerKey consumerAuth, RequestToken userAuth) {
73-
mac = new ThreadSafeHMAC(consumerAuth, userAuth);
7447
this.consumerAuth = consumerAuth;
7548
this.userAuth = userAuth;
7649
}
7750

7851
@Override
7952
public void calculateAndAddSignature(Request request, RequestBuilderBase<?> requestBuilder) {
80-
String nonce = generateNonce();
81-
long timestamp = generateTimestamp();
82-
String signature = calculateSignature(request, timestamp, nonce);
83-
String headerValue = constructAuthHeader(signature, nonce, timestamp);
84-
requestBuilder.setHeader(HEADER_AUTHORIZATION, headerValue);
85-
}
86-
87-
private String encodedParams(long oauthTimestamp, String nonce, List<Param> formParams, List<Param> queryParams) {
88-
/**
89-
* List of all query and form parameters added to this request; needed for calculating request signature
90-
*/
91-
int allParametersSize = 5 + (userAuth.getKey() != null ? 1 : 0) + (formParams != null ? formParams.size() : 0) + (queryParams != null ? queryParams.size() : 0);
92-
OAuthParameterSet allParameters = new OAuthParameterSet(allParametersSize);
93-
94-
// start with standard OAuth parameters we need
95-
allParameters.add(KEY_OAUTH_CONSUMER_KEY, Utf8UrlEncoder.percentEncodeQueryElement(consumerAuth.getKey()));
96-
allParameters.add(KEY_OAUTH_NONCE, Utf8UrlEncoder.percentEncodeQueryElement(nonce));
97-
allParameters.add(KEY_OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD);
98-
allParameters.add(KEY_OAUTH_TIMESTAMP, String.valueOf(oauthTimestamp));
99-
if (userAuth.getKey() != null) {
100-
allParameters.add(KEY_OAUTH_TOKEN, Utf8UrlEncoder.percentEncodeQueryElement(userAuth.getKey()));
101-
}
102-
allParameters.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0);
103-
104-
if (formParams != null) {
105-
for (Param param : formParams) {
106-
// formParams are not already encoded
107-
allParameters.add(Utf8UrlEncoder.percentEncodeQueryElement(param.getName()), Utf8UrlEncoder.percentEncodeQueryElement(param.getValue()));
108-
}
109-
}
110-
if (queryParams != null) {
111-
for (Param param : queryParams) {
112-
// queryParams are already form-url-encoded
113-
// but OAuth1 uses RFC3986_UNRESERVED_CHARS so * and + have to be encoded
114-
allParameters.add(percentEncodeAlreadyFormUrlEncoded(param.getName()), percentEncodeAlreadyFormUrlEncoded(param.getValue()));
115-
}
116-
}
117-
return allParameters.sortAndConcat();
118-
}
119-
120-
private String baseUrl(Uri uri) {
121-
/*
122-
* 07-Oct-2010, tatu: URL may contain default port number; if so, need to remove from base URL.
123-
*/
124-
String scheme = uri.getScheme();
125-
126-
StringBuilder sb = StringUtils.stringBuilder();
127-
sb.append(scheme).append("://").append(uri.getHost());
128-
129-
int port = uri.getPort();
130-
if (scheme.equals("http")) {
131-
if (port == 80)
132-
port = -1;
133-
} else if (scheme.equals("https")) {
134-
if (port == 443)
135-
port = -1;
136-
}
137-
138-
if (port != -1)
139-
sb.append(':').append(port);
140-
141-
if (isNonEmpty(uri.getPath()))
142-
sb.append(uri.getPath());
143-
144-
return sb.toString();
145-
}
146-
147-
private static final Pattern STAR_CHAR_PATTERN = Pattern.compile("*", Pattern.LITERAL);
148-
private static final Pattern PLUS_CHAR_PATTERN = Pattern.compile("+", Pattern.LITERAL);
149-
private static final Pattern ENCODED_TILDE_PATTERN = Pattern.compile("%7E", Pattern.LITERAL);
150-
151-
private String percentEncodeAlreadyFormUrlEncoded(String s) {
152-
s = STAR_CHAR_PATTERN.matcher(s).replaceAll("%2A");
153-
s = PLUS_CHAR_PATTERN.matcher(s).replaceAll("%20");
154-
s = ENCODED_TILDE_PATTERN.matcher(s).replaceAll("~");
155-
return s;
156-
}
157-
158-
StringBuilder signatureBaseString(Request request, long oauthTimestamp, String nonce) {
159-
160-
// beware: must generate first as we're using pooled StringBuilder
161-
String baseUrl = baseUrl(request.getUri());
162-
String encodedParams = encodedParams(oauthTimestamp, nonce, request.getFormParams(), request.getQueryParams());
163-
164-
StringBuilder sb = StringUtils.stringBuilder();
165-
sb.append(request.getMethod()); // POST / GET etc (nothing to URL encode)
166-
sb.append('&');
167-
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, baseUrl);
168-
169-
// and all that needs to be URL encoded (... again!)
170-
sb.append('&');
171-
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, encodedParams);
172-
return sb;
173-
}
174-
175-
String calculateSignature(Request request, long oauthTimestamp, String nonce) {
176-
177-
StringBuilder sb = signatureBaseString(request, oauthTimestamp, nonce);
178-
179-
ByteBuffer rawBase = StringUtils.charSequence2ByteBuffer(sb, UTF_8);
180-
byte[] rawSignature = mac.digest(rawBase);
181-
// and finally, base64 encoded... phew!
182-
return Base64.encode(rawSignature);
183-
}
184-
185-
String constructAuthHeader(String signature, String nonce, long oauthTimestamp) {
186-
StringBuilder sb = StringUtils.stringBuilder();
187-
sb.append("OAuth ");
188-
sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getKey()).append("\", ");
189-
if (userAuth.getKey() != null) {
190-
sb.append(KEY_OAUTH_TOKEN).append("=\"").append(userAuth.getKey()).append("\", ");
191-
}
192-
sb.append(KEY_OAUTH_SIGNATURE_METHOD).append("=\"").append(OAUTH_SIGNATURE_METHOD).append("\", ");
193-
194-
// careful: base64 has chars that need URL encoding:
195-
sb.append(KEY_OAUTH_SIGNATURE).append("=\"");
196-
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, signature).append("\", ");
197-
sb.append(KEY_OAUTH_TIMESTAMP).append("=\"").append(oauthTimestamp).append("\", ");
198-
199-
// also: nonce may contain things that need URL encoding (esp. when using base64):
200-
sb.append(KEY_OAUTH_NONCE).append("=\"");
201-
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, nonce);
202-
sb.append("\", ");
203-
204-
sb.append(KEY_OAUTH_VERSION).append("=\"").append(OAUTH_VERSION_1_0).append("\"");
205-
return sb.toString();
206-
}
207-
208-
long generateTimestamp() {
209-
return System.currentTimeMillis() / 1000L;
210-
}
211-
212-
String generateNonce() {
213-
byte[] nonceBuffer = NONCE_BUFFER.get();
214-
ThreadLocalRandom.current().nextBytes(nonceBuffer);
215-
// let's use base64 encoding over hex, slightly more compact than hex or decimals
216-
return Base64.encode(nonceBuffer);
217-
// return String.valueOf(Math.abs(random.nextLong()));
218-
}
219-
220-
/**
221-
* Container for parameters used for calculating OAuth signature. About the only confusing aspect is that of whether entries are to be sorted before encoded or vice versa: if
222-
* my reading is correct, encoding is to occur first, then sorting; although this should rarely matter (since sorting is primary by key, which usually has nothing to encode)...
223-
* of course, rarely means that when it would occur it'd be harder to track down.
224-
*/
225-
final static class OAuthParameterSet {
226-
private final ArrayList<Parameter> allParameters;
227-
228-
public OAuthParameterSet(int size) {
229-
allParameters = new ArrayList<>(size);
230-
}
231-
232-
public OAuthParameterSet add(String key, String value) {
233-
allParameters.add(new Parameter(key, value));
234-
return this;
235-
}
236-
237-
public String sortAndConcat() {
238-
// then sort them (AFTER encoding, important)
239-
Parameter[] params = allParameters.toArray(new Parameter[allParameters.size()]);
240-
Arrays.sort(params);
241-
242-
// and build parameter section using pre-encoded pieces:
243-
StringBuilder encodedParams = new StringBuilder(100);
244-
for (Parameter param : params) {
245-
if (encodedParams.length() > 0) {
246-
encodedParams.append('&');
247-
}
248-
encodedParams.append(param.key()).append('=').append(param.value());
249-
}
250-
return encodedParams.toString();
251-
}
252-
}
253-
254-
/**
255-
* Helper class for sorting query and form parameters that we need
256-
*/
257-
final static class Parameter implements Comparable<Parameter> {
258-
259-
private final String key, value;
260-
261-
public Parameter(String key, String value) {
262-
this.key = key;
263-
this.value = value;
264-
}
265-
266-
public String key() {
267-
return key;
268-
}
269-
270-
public String value() {
271-
return value;
272-
}
273-
274-
@Override
275-
public int compareTo(Parameter other) {
276-
int diff = key.compareTo(other.key);
277-
if (diff == 0) {
278-
diff = value.compareTo(other.value);
279-
}
280-
return diff;
281-
}
282-
283-
@Override
284-
public String toString() {
285-
return key + "=" + value;
286-
}
287-
288-
@Override
289-
public boolean equals(Object o) {
290-
if (this == o)
291-
return true;
292-
if (o == null || getClass() != o.getClass())
293-
return false;
294-
295-
Parameter parameter = (Parameter) o;
296-
297-
if (!key.equals(parameter.key))
298-
return false;
299-
if (!value.equals(parameter.value))
300-
return false;
301-
302-
return true;
303-
}
304-
305-
@Override
306-
public int hashCode() {
307-
int result = key.hashCode();
308-
result = 31 * result + value.hashCode();
309-
return result;
53+
try {
54+
INSTANCES.get().sign(consumerAuth, userAuth, request, requestBuilder);
55+
} catch (InvalidKeyException e) {
56+
throw new IllegalArgumentException("Failed to compute a valid key from consumer and user secrets", e);
31057
}
31158
}
31259
}

0 commit comments

Comments
 (0)