Java tutorial
/* * Copyright 2011-2018 GatlingCorp (https://gatling.io) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // // Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. // // This program is licensed to you under the Apache License Version 2.0, // and you may not use this file except in compliance with the Apache License Version 2.0. // You may obtain a copy of the Apache License Version 2.0 at // http://www.apache.org/licenses/LICENSE-2.0. // // Unless required by applicable law or agreed to in writing, // software distributed under the Apache License Version 2.0 is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. // package io.gatling.http.client.ahc.oauth; import io.gatling.http.client.Param; import io.gatling.http.client.ahc.uri.Uri; import io.gatling.netty.util.ahc.StringBuilderPool; import io.gatling.http.client.ahc.util.StringUtils; import io.gatling.netty.util.ahc.Utf8UrlEncoder; import io.netty.handler.codec.http.HttpMethod; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; import static io.gatling.http.client.ahc.util.MiscUtils.isNonEmpty; import static java.nio.charset.StandardCharsets.UTF_8; /** * Supports most common signature inclusion and calculation methods: * HMAC-SHA1 for calculation, and Header inclusion as inclusion method. * Nonce generation uses simple random numbers with base64 encoding. */ public class OAuthSignatureCalculatorInstance { private static final Pattern STAR_CHAR_PATTERN = Pattern.compile("*", Pattern.LITERAL); private static final Pattern PLUS_CHAR_PATTERN = Pattern.compile("+", Pattern.LITERAL); private static final Pattern ENCODED_TILDE_PATTERN = Pattern.compile("%7E", Pattern.LITERAL); private static final String KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key"; private static final String KEY_OAUTH_NONCE = "oauth_nonce"; private static final String KEY_OAUTH_SIGNATURE = "oauth_signature"; private static final String KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; private static final String KEY_OAUTH_TIMESTAMP = "oauth_timestamp"; private static final String KEY_OAUTH_TOKEN = "oauth_token"; private static final String KEY_OAUTH_VERSION = "oauth_version"; private static final String OAUTH_VERSION_1_0 = "1.0"; private static final String OAUTH_SIGNATURE_METHOD = "HMAC-SHA1"; private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; private final Mac mac; private final byte[] nonceBuffer = new byte[16]; private final Params params = new Params(); public OAuthSignatureCalculatorInstance() throws NoSuchAlgorithmException { mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); } public String computeAuthorizationHeader(ConsumerKey consumerAuth, RequestToken userAuth, HttpMethod method, Uri uri, List<Param> formParams) throws InvalidKeyException { String nonce = generateNonce(); long timestamp = generateTimestamp(); return computeAuthorizationHeader(consumerAuth, userAuth, method, uri, formParams, timestamp, nonce); } private String generateNonce() { ThreadLocalRandom.current().nextBytes(nonceBuffer); // let's use base64 encoding over hex, slightly more compact than hex or decimals return Base64.getEncoder().encodeToString(nonceBuffer); } private static long generateTimestamp() { return System.currentTimeMillis() / 1000L; } String computeAuthorizationHeader(ConsumerKey consumerAuth, RequestToken userAuth, HttpMethod method, Uri uri, List<Param> formParams, long timestamp, String nonce) throws InvalidKeyException { String percentEncodedNonce = Utf8UrlEncoder.percentEncodeQueryElement(nonce); String signature = computeSignature(consumerAuth, userAuth, method, uri, formParams, timestamp, percentEncodedNonce); return computeAuthorizationHeader(consumerAuth, userAuth, signature, timestamp, percentEncodedNonce); } String computeSignature(ConsumerKey consumerAuth, RequestToken userAuth, HttpMethod method, Uri uri, List<Param> formParams, long oauthTimestamp, String percentEncodedNonce) throws InvalidKeyException { StringBuilder sb = signatureBaseString(consumerAuth, userAuth, method, uri, formParams, oauthTimestamp, percentEncodedNonce); ByteBuffer rawBase = StringUtils.charSequence2ByteBuffer(sb, UTF_8); byte[] rawSignature = digest(consumerAuth, userAuth, rawBase); // and finally, base64 encoded... phew! return Base64.getEncoder().encodeToString(rawSignature); } StringBuilder signatureBaseString(ConsumerKey consumerAuth, RequestToken userAuth, HttpMethod method, Uri uri, List<Param> formParams, long oauthTimestamp, String percentEncodedNonce) { // beware: must generate first as we're using pooled StringBuilder String baseUrl = uri.toBaseUrl(); String encodedParams = encodedParams(consumerAuth, userAuth, oauthTimestamp, percentEncodedNonce, uri.getEncodedQueryParams(), formParams); StringBuilder sb = StringBuilderPool.DEFAULT.get(); sb.append(method.name()); // POST / GET etc (nothing to URL encode) sb.append('&'); Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, baseUrl); // and all that needs to be URL encoded (... again!) sb.append('&'); Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, encodedParams); return sb; } private String encodedParams(ConsumerKey consumerAuth, RequestToken userAuth, long oauthTimestamp, String percentEncodedNonce, List<Param> queryParams, List<Param> formParams) { params.reset(); // List of all query and form parameters added to this request; needed for calculating request signature // Start with standard OAuth parameters we need params.add(KEY_OAUTH_CONSUMER_KEY, consumerAuth.getPercentEncodedKey()) .add(KEY_OAUTH_NONCE, percentEncodedNonce).add(KEY_OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD) .add(KEY_OAUTH_TIMESTAMP, String.valueOf(oauthTimestamp)); if (userAuth.getKey() != null) { params.add(KEY_OAUTH_TOKEN, userAuth.getPercentEncodedKey()); } params.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0); if (formParams != null) { for (Param param : formParams) { // formParams are not already encoded params.add(Utf8UrlEncoder.percentEncodeQueryElement(param.getName()), Utf8UrlEncoder.percentEncodeQueryElement(param.getValue())); } } if (isNonEmpty(queryParams)) { for (Param param : queryParams) { // queryParams are already form-url-encoded // but OAuth1 uses RFC3986_UNRESERVED_CHARS so * and + have to be encoded params.add(percentEncodeAlreadyFormUrlEncoded(param.getName()), percentEncodeAlreadyFormUrlEncoded(param.getValue())); } } return params.sortAndConcat(); } private String percentEncodeAlreadyFormUrlEncoded(String s) { s = STAR_CHAR_PATTERN.matcher(s).replaceAll("%2A"); s = PLUS_CHAR_PATTERN.matcher(s).replaceAll("%20"); s = ENCODED_TILDE_PATTERN.matcher(s).replaceAll("~"); return s; } private byte[] digest(ConsumerKey consumerAuth, RequestToken userAuth, ByteBuffer message) throws InvalidKeyException { StringBuilder sb = StringBuilderPool.DEFAULT.get(); Utf8UrlEncoder.encodeAndAppendQueryElement(sb, consumerAuth.getSecret()); sb.append('&'); if (userAuth != null && userAuth.getSecret() != null) { Utf8UrlEncoder.encodeAndAppendQueryElement(sb, userAuth.getSecret()); } byte[] keyBytes = StringUtils.charSequence2Bytes(sb, UTF_8); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM); mac.init(signingKey); mac.reset(); mac.update(message); return mac.doFinal(); } String computeAuthorizationHeader(ConsumerKey consumerAuth, RequestToken userAuth, String signature, long oauthTimestamp, String percentEncodedNonce) { StringBuilder sb = StringBuilderPool.DEFAULT.get(); sb.append("OAuth "); sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getPercentEncodedKey()).append("\", "); if (userAuth.getKey() != null) { sb.append(KEY_OAUTH_TOKEN).append("=\"").append(userAuth.getPercentEncodedKey()).append("\", "); } sb.append(KEY_OAUTH_SIGNATURE_METHOD).append("=\"").append(OAUTH_SIGNATURE_METHOD).append("\", "); // careful: base64 has chars that need URL encoding: sb.append(KEY_OAUTH_SIGNATURE).append("=\""); Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, signature).append("\", "); sb.append(KEY_OAUTH_TIMESTAMP).append("=\"").append(oauthTimestamp).append("\", "); sb.append(KEY_OAUTH_NONCE).append("=\"").append(percentEncodedNonce).append("\", "); sb.append(KEY_OAUTH_VERSION).append("=\"").append(OAUTH_VERSION_1_0).append("\""); return sb.toString(); } }