|
13 | 13 | */ |
14 | 14 | packageorg.asynchttpclient.oauth; |
15 | 15 |
|
16 | | -importstaticjava.nio.charset.StandardCharsets.UTF_8; |
17 | | -importstaticorg.asynchttpclient.util.MiscUtils.isNonEmpty; |
| 16 | +importjava.security.InvalidKeyException; |
| 17 | +importjava.security.NoSuchAlgorithmException; |
18 | 18 |
|
19 | | -importjava.nio.ByteBuffer; |
20 | | -importjava.util.ArrayList; |
21 | | -importjava.util.Arrays; |
22 | | -importjava.util.List; |
23 | | -importjava.util.concurrent.ThreadLocalRandom; |
24 | | -importjava.util.regex.Pattern; |
25 | | - |
26 | | -importorg.asynchttpclient.Param; |
27 | 19 | importorg.asynchttpclient.Request; |
28 | 20 | importorg.asynchttpclient.RequestBuilderBase; |
29 | 21 | importorg.asynchttpclient.SignatureCalculator; |
30 | | -importorg.asynchttpclient.uri.Uri; |
31 | | -importorg.asynchttpclient.util.Base64; |
32 | | -importorg.asynchttpclient.util.StringUtils; |
33 | | -importorg.asynchttpclient.util.Utf8UrlEncoder; |
34 | 22 |
|
35 | 23 | /** |
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 (tatu.saloranta@iki.fi) |
| 24 | + * OAuth {@link SignatureCalculator} that delegates to {@link OAuthSignatureCalculatorInstance}s. |
41 | 25 | */ |
42 | 26 | publicclassOAuthSignatureCalculatorimplementsSignatureCalculator { |
43 | | -publicfinalstaticStringHEADER_AUTHORIZATION ="Authorization"; |
44 | | - |
45 | | -privatestaticfinalStringKEY_OAUTH_CONSUMER_KEY ="oauth_consumer_key"; |
46 | | -privatestaticfinalStringKEY_OAUTH_NONCE ="oauth_nonce"; |
47 | | -privatestaticfinalStringKEY_OAUTH_SIGNATURE ="oauth_signature"; |
48 | | -privatestaticfinalStringKEY_OAUTH_SIGNATURE_METHOD ="oauth_signature_method"; |
49 | | -privatestaticfinalStringKEY_OAUTH_TIMESTAMP ="oauth_timestamp"; |
50 | | -privatestaticfinalStringKEY_OAUTH_TOKEN ="oauth_token"; |
51 | | -privatestaticfinalStringKEY_OAUTH_VERSION ="oauth_version"; |
52 | 27 |
|
53 | | -privatestaticfinalStringOAUTH_VERSION_1_0 ="1.0"; |
54 | | -privatestaticfinalStringOAUTH_SIGNATURE_METHOD ="HMAC-SHA1"; |
55 | | - |
56 | | -protectedstaticfinalThreadLocal<byte[]>NONCE_BUFFER =newThreadLocal<byte[]>() { |
57 | | -protectedbyte[]initialValue() { |
58 | | -returnnewbyte[16]; |
59 | | - } |
| 28 | +privatestaticfinalThreadLocal<OAuthSignatureCalculatorInstance>INSTANCES =newThreadLocal<OAuthSignatureCalculatorInstance>() { |
| 29 | +protectedOAuthSignatureCalculatorInstanceinitialValue() { |
| 30 | +try { |
| 31 | +returnnewOAuthSignatureCalculatorInstance(); |
| 32 | + }catch (NoSuchAlgorithmExceptione) { |
| 33 | +thrownewExceptionInInitializerError(e); |
| 34 | + } |
| 35 | + }; |
60 | 36 | }; |
61 | 37 |
|
62 | | -protectedfinalThreadSafeHMACmac; |
63 | | - |
64 | | -protectedfinalConsumerKeyconsumerAuth; |
| 38 | +privatefinalConsumerKeyconsumerAuth; |
65 | 39 |
|
66 | | -protectedfinalRequestTokenuserAuth; |
| 40 | +privatefinalRequestTokenuserAuth; |
67 | 41 |
|
68 | 42 | /** |
69 | 43 | * @param consumerAuth Consumer key to use for signature calculation |
70 | 44 | * @param userAuth Request/access token to use for signature calculation |
71 | 45 | */ |
72 | 46 | publicOAuthSignatureCalculator(ConsumerKeyconsumerAuth,RequestTokenuserAuth) { |
73 | | -mac =newThreadSafeHMAC(consumerAuth,userAuth); |
74 | 47 | this.consumerAuth =consumerAuth; |
75 | 48 | this.userAuth =userAuth; |
76 | 49 | } |
77 | 50 |
|
78 | 51 | @Override |
79 | 52 | publicvoidcalculateAndAddSignature(Requestrequest,RequestBuilderBase<?>requestBuilder) { |
80 | | -Stringnonce =generateNonce(); |
81 | | -longtimestamp =generateTimestamp(); |
82 | | -Stringsignature =calculateSignature(request,timestamp,nonce); |
83 | | -StringheaderValue =constructAuthHeader(signature,nonce,timestamp); |
84 | | -requestBuilder.setHeader(HEADER_AUTHORIZATION,headerValue); |
85 | | - } |
86 | | - |
87 | | -privateStringencodedParams(longoauthTimestamp,Stringnonce,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 | | -intallParametersSize =5 + (userAuth.getKey() !=null ?1 :0) + (formParams !=null ?formParams.size() :0) + (queryParams !=null ?queryParams.size() :0); |
92 | | -OAuthParameterSetallParameters =newOAuthParameterSet(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 (Paramparam :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 (Paramparam :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 | | -returnallParameters.sortAndConcat(); |
118 | | - } |
119 | | - |
120 | | -privateStringbaseUrl(Uriuri) { |
121 | | -/* |
122 | | - * 07-Oct-2010, tatu: URL may contain default port number; if so, need to remove from base URL. |
123 | | - */ |
124 | | -Stringscheme =uri.getScheme(); |
125 | | - |
126 | | -StringBuildersb =StringUtils.stringBuilder(); |
127 | | -sb.append(scheme).append("://").append(uri.getHost()); |
128 | | - |
129 | | -intport =uri.getPort(); |
130 | | -if (scheme.equals("http")) { |
131 | | -if (port ==80) |
132 | | -port = -1; |
133 | | - }elseif (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 | | -returnsb.toString(); |
145 | | - } |
146 | | - |
147 | | -privatestaticfinalPatternSTAR_CHAR_PATTERN =Pattern.compile("*",Pattern.LITERAL); |
148 | | -privatestaticfinalPatternPLUS_CHAR_PATTERN =Pattern.compile("+",Pattern.LITERAL); |
149 | | -privatestaticfinalPatternENCODED_TILDE_PATTERN =Pattern.compile("%7E",Pattern.LITERAL); |
150 | | - |
151 | | -privateStringpercentEncodeAlreadyFormUrlEncoded(Strings) { |
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 | | -returns; |
156 | | - } |
157 | | - |
158 | | -StringBuildersignatureBaseString(Requestrequest,longoauthTimestamp,Stringnonce) { |
159 | | - |
160 | | -// beware: must generate first as we're using pooled StringBuilder |
161 | | -StringbaseUrl =baseUrl(request.getUri()); |
162 | | -StringencodedParams =encodedParams(oauthTimestamp,nonce,request.getFormParams(),request.getQueryParams()); |
163 | | - |
164 | | -StringBuildersb =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 | | -returnsb; |
173 | | - } |
174 | | - |
175 | | -StringcalculateSignature(Requestrequest,longoauthTimestamp,Stringnonce) { |
176 | | - |
177 | | -StringBuildersb =signatureBaseString(request,oauthTimestamp,nonce); |
178 | | - |
179 | | -ByteBufferrawBase =StringUtils.charSequence2ByteBuffer(sb,UTF_8); |
180 | | -byte[]rawSignature =mac.digest(rawBase); |
181 | | -// and finally, base64 encoded... phew! |
182 | | -returnBase64.encode(rawSignature); |
183 | | - } |
184 | | - |
185 | | -StringconstructAuthHeader(Stringsignature,Stringnonce,longoauthTimestamp) { |
186 | | -StringBuildersb =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 | | -returnsb.toString(); |
206 | | - } |
207 | | - |
208 | | -longgenerateTimestamp() { |
209 | | -returnSystem.currentTimeMillis() /1000L; |
210 | | - } |
211 | | - |
212 | | -StringgenerateNonce() { |
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 | | -returnBase64.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 | | -finalstaticclassOAuthParameterSet { |
226 | | -privatefinalArrayList<Parameter>allParameters; |
227 | | - |
228 | | -publicOAuthParameterSet(intsize) { |
229 | | -allParameters =newArrayList<>(size); |
230 | | - } |
231 | | - |
232 | | -publicOAuthParameterSetadd(Stringkey,Stringvalue) { |
233 | | -allParameters.add(newParameter(key,value)); |
234 | | -returnthis; |
235 | | - } |
236 | | - |
237 | | -publicStringsortAndConcat() { |
238 | | -// then sort them (AFTER encoding, important) |
239 | | -Parameter[]params =allParameters.toArray(newParameter[allParameters.size()]); |
240 | | -Arrays.sort(params); |
241 | | - |
242 | | -// and build parameter section using pre-encoded pieces: |
243 | | -StringBuilderencodedParams =newStringBuilder(100); |
244 | | -for (Parameterparam :params) { |
245 | | -if (encodedParams.length() >0) { |
246 | | -encodedParams.append('&'); |
247 | | - } |
248 | | -encodedParams.append(param.key()).append('=').append(param.value()); |
249 | | - } |
250 | | -returnencodedParams.toString(); |
251 | | - } |
252 | | - } |
253 | | - |
254 | | -/** |
255 | | - * Helper class for sorting query and form parameters that we need |
256 | | - */ |
257 | | -finalstaticclassParameterimplementsComparable<Parameter> { |
258 | | - |
259 | | -privatefinalStringkey,value; |
260 | | - |
261 | | -publicParameter(Stringkey,Stringvalue) { |
262 | | -this.key =key; |
263 | | -this.value =value; |
264 | | - } |
265 | | - |
266 | | -publicStringkey() { |
267 | | -returnkey; |
268 | | - } |
269 | | - |
270 | | -publicStringvalue() { |
271 | | -returnvalue; |
272 | | - } |
273 | | - |
274 | | -@Override |
275 | | -publicintcompareTo(Parameterother) { |
276 | | -intdiff =key.compareTo(other.key); |
277 | | -if (diff ==0) { |
278 | | -diff =value.compareTo(other.value); |
279 | | - } |
280 | | -returndiff; |
281 | | - } |
282 | | - |
283 | | -@Override |
284 | | -publicStringtoString() { |
285 | | -returnkey +"=" +value; |
286 | | - } |
287 | | - |
288 | | -@Override |
289 | | -publicbooleanequals(Objecto) { |
290 | | -if (this ==o) |
291 | | -returntrue; |
292 | | -if (o ==null ||getClass() !=o.getClass()) |
293 | | -returnfalse; |
294 | | - |
295 | | -Parameterparameter = (Parameter)o; |
296 | | - |
297 | | -if (!key.equals(parameter.key)) |
298 | | -returnfalse; |
299 | | -if (!value.equals(parameter.value)) |
300 | | -returnfalse; |
301 | | - |
302 | | -returntrue; |
303 | | - } |
304 | | - |
305 | | -@Override |
306 | | -publicinthashCode() { |
307 | | -intresult =key.hashCode(); |
308 | | -result =31 *result +value.hashCode(); |
309 | | -returnresult; |
| 53 | +try { |
| 54 | +INSTANCES.get().sign(consumerAuth,userAuth,request,requestBuilder); |
| 55 | + }catch (InvalidKeyExceptione) { |
| 56 | +thrownewIllegalArgumentException("Failed to compute a valid key from consumer and user secrets",e); |
310 | 57 | } |
311 | 58 | } |
312 | 59 | } |