Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An OkHttp interceptor which does OAuth1 signing. Requires Java 7 (but can easily be ported to Java 6).
/*
* Copyright (C) 2015 Jake Wharton
*
* 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.
*/
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import okio.Buffer;
import okio.ByteString;
public final class Oauth1SigningInterceptor implements Interceptor {
private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
private static final String OAUTH_NONCE = "oauth_nonce";
private static final String OAUTH_SIGNATURE = "oauth_signature";
private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1";
private static final String OAUTH_TIMESTAMP = "oauth_timestamp";
private static final String OAUTH_ACCESS_TOKEN = "oauth_token";
private static final String OAUTH_VERSION = "oauth_version";
private static final String OAUTH_VERSION_VALUE = "1.0";
private final String consumerKey;
private final String consumerSecret;
private final String accessToken;
private final String accessSecret;
private final Random random;
private final Clock clock;
private Oauth1SigningInterceptor(String consumerKey, String consumerSecret, String accessToken,
String accessSecret, Random random, Clock clock) {
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.accessToken = accessToken;
this.accessSecret = accessSecret;
this.random = random;
this.clock = clock;
}
@Override public Response intercept(Chain chain) throws IOException {
return chain.proceed(signRequest(chain.request()));
}
public Request signRequest(Request request) throws IOException {
byte[] nonce = new byte[32];
random.nextBytes(nonce);
String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", "");
String oauthTimestamp = clock.millis();
String consumerKeyValue = UrlEscapeUtils.escape(consumerKey);
String accessTokenValue = UrlEscapeUtils.escape(accessToken);
SortedMap<String, String> parameters = new TreeMap<>();
parameters.put(OAUTH_CONSUMER_KEY, consumerKeyValue);
parameters.put(OAUTH_ACCESS_TOKEN, accessTokenValue);
parameters.put(OAUTH_NONCE, oauthNonce);
parameters.put(OAUTH_TIMESTAMP, oauthTimestamp);
parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE);
parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE);
HttpUrl url = request.httpUrl();
for (int i = 0; i < url.querySize(); i++) {
parameters.put(UrlEscapeUtils.escape(url.queryParameterName(i)),
UrlEscapeUtils.escape(url.queryParameterValue(i)));
}
Buffer body = new Buffer();
RequestBody requestBody = request.body();
if (requestBody != null) {
requestBody.writeTo(body);
}
while (!body.exhausted()) {
long keyEnd = body.indexOf((byte) '=');
if (keyEnd == -1) throw new IllegalStateException("Key with no value: " + body.readUtf8());
String key = body.readUtf8(keyEnd);
body.skip(1); // Equals.
long valueEnd = body.indexOf((byte) '&');
String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd);
if (valueEnd != -1) body.skip(1); // Ampersand.
parameters.put(key, value);
}
Buffer base = new Buffer();
String method = request.method();
base.writeUtf8(method);
base.writeByte('&');
base.writeUtf8(
UrlEscapeUtils.escape(request.httpUrl().newBuilder().query(null).build().toString()));
base.writeByte('&');
boolean first = true;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (!first) base.writeUtf8(UrlEscapeUtils.escape("&"));
first = false;
base.writeUtf8(UrlEscapeUtils.escape(entry.getKey()));
base.writeUtf8(UrlEscapeUtils.escape("="));
base.writeUtf8(UrlEscapeUtils.escape(entry.getValue()));
}
String signingKey =
UrlEscapeUtils.escape(consumerSecret) + "&" + UrlEscapeUtils.escape(accessSecret);
SecretKeySpec keySpec = new SecretKeySpec(signingKey.getBytes(), "HmacSHA1");
Mac mac;
try {
mac = Mac.getInstance("HmacSHA1");
mac.init(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException(e);
}
byte[] result = mac.doFinal(base.readByteArray());
String signature = ByteString.of(result).base64();
String authorization =
"OAuth " + OAUTH_CONSUMER_KEY + "=\"" + consumerKeyValue + "\", " + OAUTH_NONCE + "=\""
+ oauthNonce + "\", " + OAUTH_SIGNATURE + "=\"" + UrlEscapeUtils.escape(signature)
+ "\", " + OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", "
+ OAUTH_TIMESTAMP + "=\"" + oauthTimestamp + "\", " + OAUTH_ACCESS_TOKEN + "=\""
+ accessTokenValue + "\", " + OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\"";
return request.newBuilder().addHeader("Authorization", authorization).build();
}
public static final class Builder {
private String consumerKey;
private String consumerSecret;
private String accessToken;
private String accessSecret;
private Random random = new SecureRandom();
private Clock clock = new Clock();
public Builder consumerKey(String consumerKey) {
if (consumerKey == null) throw new NullPointerException("consumerKey = null");
this.consumerKey = consumerKey;
return this;
}
public Builder consumerSecret(String consumerSecret) {
if (consumerSecret == null) throw new NullPointerException("consumerSecret = null");
this.consumerSecret = consumerSecret;
return this;
}
public Builder accessToken(String accessToken) {
if (accessToken == null) throw new NullPointerException("accessToken == null");
this.accessToken = accessToken;
return this;
}
public Builder accessSecret(String accessSecret) {
if (accessSecret == null) throw new NullPointerException("accessSecret == null");
this.accessSecret = accessSecret;
return this;
}
public Builder random(Random random) {
if (random == null) throw new NullPointerException("random == null");
this.random = random;
return this;
}
public Builder clock(Clock clock) {
if (clock == null) throw new NullPointerException("clock == null");
this.clock = clock;
return this;
}
public Oauth1SigningInterceptor build() {
if (consumerKey == null) throw new IllegalStateException("consumerKey not set");
if (consumerSecret == null) throw new IllegalStateException("consumerSecret not set");
if (accessToken == null) throw new IllegalStateException("accessToken not set");
if (accessSecret == null) throw new IllegalStateException("accessSecret not set");
return new Oauth1SigningInterceptor(consumerKey, consumerSecret, accessToken, accessSecret,
random, clock);
}
}
/** Simple clock like class, to allow time mocking. */
static class Clock {
/** Returns the current time in milliseconds divided by 1K. */
public String millis() {
return Long.toString(System.currentTimeMillis() / 1000L);
}
}
}
/*
* Copyright (C) 2015 Jake Wharton
*
* 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.
*/
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import java.util.Random;
import okio.ByteString;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public final class Oauth1SigningInterceptorTest {
Oauth1SigningInterceptor oauth1;
@Before public void setUp() throws IOException {
// Data from https://dev.twitter.com/oauth/overview/authorizing-requests.
// Tested via http://www.oauth-signatur.de/en
Random notRandom = new Random() {
@Override public void nextBytes(byte[] bytes) {
if (bytes.length != 32) throw new AssertionError();
ByteString hex = ByteString.decodeBase64("kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4c+g");
byte[] nonce = hex.toByteArray();
System.arraycopy(nonce, 0, bytes, 0, nonce.length);
}
};
// Mock the time :)
Oauth1SigningInterceptor.Clock clock = mock(Oauth1SigningInterceptor.Clock.class);
when(clock.millis()).thenReturn("1318622958");
oauth1 = new Oauth1SigningInterceptor.Builder()
.consumerKey("xvz1evFS4wEEPTGEFPHBog")
.consumerSecret("kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw")
.accessToken("370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb")
.accessSecret("LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE")
.random(notRandom)
.clock(clock)
.build();
}
@Test public void withBody() throws IOException {
RequestBody body = new FormEncodingBuilder() //
.add("status", "Hello Ladies + Gentlemen, a signed OAuth request!")
.build();
Request request = new Request.Builder() //
.url("https://api.twitter.com/1/statuses/update.json?include_entities=true")
.post(body)
.build();
Request signed = oauth1.signRequest(request);
assertAuthHeader(signed, "tnnArxj06cWHq44gCs1OSKk%2FjLY%3D");
}
@Test public void noBody() throws IOException {
Request request = new Request.Builder() //
.url("https://api.twitter.com/1.1/statuses/mentions_timeline.json?count=100&include_entities=false")
.build();
Request signed = oauth1.signRequest(request);
assertAuthHeader(signed, "hn5jxegoQxNM6SXvQgVhK15yQL8%3D");
}
@Test public void urlSameQueryParams() throws IOException {
Request request = new Request.Builder() //
.url("https://api.twitter.com/1.1/statuses/home_timeline.json?since_id=12&since_id=13")
.build();
Request signed = oauth1.signRequest(request);
assertAuthHeader(signed, "R8m%2BYY%2FZG5GJ%2F%2F3zCrE65DkTdCk%3D");
}
/**
* Asserts that the provided request contains an expected header, with provided oauth signature.
*/
private static void assertAuthHeader(Request request, String signature) {
assertThat(request.header("Authorization")).isEqualTo("OAuth "
+ "oauth_consumer_key=\"xvz1evFS4wEEPTGEFPHBog\", "
+ "oauth_nonce=\"kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg\", "
+ "oauth_signature=\"" + signature + "\", "
+ "oauth_signature_method=\"HMAC-SHA1\", "
+ "oauth_timestamp=\"1318622958\", "
+ "oauth_token=\"370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb\", "
+ "oauth_version=\"1.0\"");
}
}
/*
* Copyright (C) 2008 The Guava Authors
*
* 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.
*/
/**
* Utility class that converts literal text into a format safe for inclusion in a url form context.
*
* <p>This is a ripoff of Guava's {@code PercentEscaper, UnicodeEscaper, Escaper}, it represents
* the same behaviour as calling {@code UrlEscapers.urlFormParameterEscaper()}. Since the purpose
* for this class is to avoid adding Guava as a dependency, all generic structure and abstractions
* where removed.
*
* <p>Because, in general, escaping operates on the code points of a string and not on its
* individual {@code char} values, it is not safe to assume that {@code escape(s)} is equivalent to
* {@code escape(s.substring(0, n)) + escape(s.substing(n))} for arbitrary {@code n}. This is
* because of the possibility of splitting a surrogate pair. The only case in which it is safe to
* escape strings and concatenate the results is if you can rule out this possibility, either by
* splitting an existing long string into short strings adaptively around {@linkplain
* Character#isHighSurrogate surrogate} {@linkplain Character#isLowSurrogate pairs}, or by starting
* with short strings already known to be free of unpaired surrogates.
*
* @author David Beaumont
*/
final class UrlEscapeUtils {
/** The amount of padding (chars) to use when growing the escape buffer. */
private static final int DEST_PAD = 32;
/** Url form parameter safe chars. */
private static final String SAFE_CHARS = "-_.*";
/** This escaper represents spaces as '+'. */
private static final char[] PLUS_SIGN = { '+' };
/** Percent escapers output upper case hex digits (uri escapers require this). */
private static final char[] UPPER_HEX_DIGITS = "0123456789ABCDEF".toCharArray();
/**
* An array of flags where for any {@code char c} if {@code SAFE_OCTETS[c]} is
* true then {@code c} should remain unmodified in the output. If
* {@code c > SAFE_OCTETS.length} then it should be escaped.
*/
private static final boolean[] SAFE_OCTETS = createSafeOctets(
SAFE_CHARS + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789");
/**
* Returns the escaped form of a given literal string.
*
* @param string the literal string to be escaped
* @return the escaped form of {@code string}
* @throws NullPointerException if {@code string} is null
* @throws IllegalArgumentException if {@code string} contains badly formed UTF-16 or cannot be
* escaped for any other reason
*/
public static String escape(String string) {
checkNotNull(string, "string == null");
int slen = string.length();
for (int index = 0; index < slen; index++) {
char c = string.charAt(index);
if (c >= SAFE_OCTETS.length || !SAFE_OCTETS[c]) {
return escapeSlow(string, index);
}
}
return string;
}
/**
* Returns the escaped form of a given literal string, starting at the given index.
*
* <p>This method is not reentrant and may only be invoked by the top level
* {@link #escape(String)} method.
*
* @param s the literal string to be escaped
* @param index the index to start escaping from
* @return the escaped form of {@code string}
* @throws NullPointerException if {@code string} is null
* @throws IllegalArgumentException if invalid surrogate characters are
* encountered
*/
private static String escapeSlow(String s, int index) {
int end = s.length();
// Get a destination buffer and setup some loop variables.
char[] dest = new char[1024];
int destIndex = 0;
int unescapedChunkStart = 0;
while (index < end) {
int cp = codePointAt(s, index, end);
if (cp < 0) {
throw new IllegalArgumentException("Trailing high surrogate at end of input");
}
// It is possible for this to return null because nextEscapeIndex() may
// (for performance reasons) yield some false positives but it must never
// give false negatives.
char[] escaped = escape(cp);
int nextIndex = index + (Character.isSupplementaryCodePoint(cp) ? 2 : 1);
if (escaped != null) {
int charsSkipped = index - unescapedChunkStart;
// This is the size needed to add the replacement, not the full
// size needed by the string. We only regrow when we absolutely must.
int sizeNeeded = destIndex + charsSkipped + escaped.length;
if (dest.length < sizeNeeded) {
int destLength = sizeNeeded + (end - index) + DEST_PAD;
dest = growBuffer(dest, destIndex, destLength);
}
// If we have skipped any characters, we need to copy them now.
if (charsSkipped > 0) {
s.getChars(unescapedChunkStart, index, dest, destIndex);
destIndex += charsSkipped;
}
if (escaped.length > 0) {
System.arraycopy(escaped, 0, dest, destIndex, escaped.length);
destIndex += escaped.length;
}
// If we dealt with an escaped character, reset the unescaped range.
unescapedChunkStart = nextIndex;
}
index = nextEscapeIndex(s, nextIndex, end);
}
// Process trailing unescaped characters - no need to account for escaped
// length or padding the allocation.
int charsSkipped = end - unescapedChunkStart;
if (charsSkipped > 0) {
int endIndex = destIndex + charsSkipped;
if (dest.length < endIndex) {
dest = growBuffer(dest, destIndex, endIndex);
}
s.getChars(unescapedChunkStart, end, dest, destIndex);
destIndex = endIndex;
}
return new String(dest, 0, destIndex);
}
/** Escapes the given Unicode code point in UTF-8. */
static char[] escape(int cp) {
// We should never get negative values here but if we do it will throw an
// IndexOutOfBoundsException, so at least it will get spotted.
if (cp < SAFE_OCTETS.length && SAFE_OCTETS[cp]) {
return null;
} else if (cp == ' ') {
return PLUS_SIGN;
} else if (cp <= 0x7F) {
// Single byte UTF-8 characters
// Start with "%--" and fill in the blanks
char[] dest = new char[3];
dest[0] = '%';
dest[2] = UPPER_HEX_DIGITS[cp & 0xF];
dest[1] = UPPER_HEX_DIGITS[cp >>> 4];
return dest;
} else if (cp <= 0x7ff) {
// Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff]
// Start with "%--%--" and fill in the blanks
char[] dest = new char[6];
dest[0] = '%';
dest[3] = '%';
dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[2] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[1] = UPPER_HEX_DIGITS[0xC | cp];
return dest;
} else if (cp <= 0xffff) {
// Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff]
// Start with "%E-%--%--" and fill in the blanks
char[] dest = new char[9];
dest[0] = '%';
dest[1] = 'E';
dest[3] = '%';
dest[6] = '%';
dest[8] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[2] = UPPER_HEX_DIGITS[cp];
return dest;
} else if (cp <= 0x10ffff) {
char[] dest = new char[12];
// Four byte UTF-8 characters [cp >= 0xffff && cp <= 0x10ffff]
// Start with "%F-%--%--%--" and fill in the blanks
dest[0] = '%';
dest[1] = 'F';
dest[3] = '%';
dest[6] = '%';
dest[9] = '%';
dest[11] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[10] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[8] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
cp >>>= 4;
dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
cp >>>= 2;
dest[2] = UPPER_HEX_DIGITS[cp & 0x7];
return dest;
} else {
// If this ever happens it is due to bug in UnicodeEscaper, not bad input.
throw new IllegalArgumentException("Invalid unicode character value " + cp);
}
}
/**
* Creates a boolean array with entries corresponding to the character values
* specified in safeChars set to true. The array is as small as is required to
* hold the given character information.
*/
private static boolean[] createSafeOctets(String safeChars) {
int maxChar = -1;
char[] safeCharArray = safeChars.toCharArray();
for (char c : safeCharArray) {
maxChar = Math.max(c, maxChar);
}
boolean[] octets = new boolean[maxChar + 1];
for (char c : safeCharArray) {
octets[c] = true;
}
return octets;
}
/**
* Scans a sub-sequence of characters from a given {@link CharSequence},
* returning the index of the next character that requires escaping.
*
* @param csq a sequence of characters
* @param start the index of the first character to be scanned
* @param end the index immediately after the last character to be scanned
* @throws IllegalArgumentException if the scanned sub-sequence of {@code csq}
* contains invalid surrogate pairs
*/
private static int nextEscapeIndex(CharSequence csq, int start, int end) {
checkNotNull(csq, "csq == null");
for (; start < end; start++) {
char c = csq.charAt(start);
if (c >= SAFE_OCTETS.length || !SAFE_OCTETS[c]) {
break;
}
}
return start;
}
/**
* Returns the Unicode code point of the character at the given index.
*
* <p>Unlike {@link Character#codePointAt(CharSequence, int)} or
* {@link String#codePointAt(int)} this method will never fail silently when
* encountering an invalid surrogate pair.
*
* <p>The behaviour of this method is as follows:
* <ol>
* <li>If {@code index >= end}, {@link IndexOutOfBoundsException} is thrown.
* <li><b>If the character at the specified index is not a surrogate, it is
* returned.</b>
* <li>If the first character was a high surrogate value, then an attempt is
* made to read the next character.
* <ol>
* <li><b>If the end of the sequence was reached, the negated value of
* the trailing high surrogate is returned.</b>
* <li><b>If the next character was a valid low surrogate, the code point
* value of the high/low surrogate pair is returned.</b>
* <li>If the next character was not a low surrogate value, then
* {@link IllegalArgumentException} is thrown.
* </ol>
* <li>If the first character was a low surrogate value,
* {@link IllegalArgumentException} is thrown.
* </ol>
*
* @param seq the sequence of characters from which to decode the code point
* @param index the index of the first character to decode
* @param end the index beyond the last valid character to decode
* @return the Unicode code point for the given index or the negated value of
* the trailing high surrogate character at the end of the sequence
*/
private static int codePointAt(CharSequence seq, int index, int end) {
checkNotNull(seq, "seq == null");
if (index < end) {
char c1 = seq.charAt(index++);
if (c1 < Character.MIN_HIGH_SURROGATE || c1 > Character.MAX_LOW_SURROGATE) {
// Fast path (first test is probably all we need to do)
return c1;
} else if (c1 <= Character.MAX_HIGH_SURROGATE) {
// If the high surrogate was the last character, return its inverse
if (index == end) {
return -c1;
}
// Otherwise look for the low surrogate following it
char c2 = seq.charAt(index);
if (Character.isLowSurrogate(c2)) {
return Character.toCodePoint(c1, c2);
}
throw new IllegalArgumentException("Expected low surrogate but got char '"
+ c2 + "' with value " + (int) c2 + " at index " + index
+ " in '" + seq + "'");
} else {
throw new IllegalArgumentException("Unexpected low surrogate character '" + c1
+ "' with value " + (int) c1 + " at index " + (index - 1)
+ " in '" + seq + "'");
}
}
throw new IndexOutOfBoundsException("Index exceeds specified range");
}
/**
* Helper method to grow the character buffer as needed, this only happens
* once in a while so it's ok if it's in a method call. If the index passed
* in is 0 then no copying will be done.
*/
private static char[] growBuffer(char[] dest, int index, int size) {
char[] copy = new char[size];
if (index > 0) {
System.arraycopy(dest, 0, copy, 0, index);
}
return copy;
}
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
}
return reference;
}
/** No instances. */
private UrlEscapeUtils() {
}
}
/*
* Copyright (C) 2009 The Guava Authors
*
* 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.
*/
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
/**
* Tests for the {@link UrlEscapeUtils} class.
*
* <b>Note: </b> This is a ripoff of Guava's actual {@code UtlEscapersTest}.
*
* @author David Beaumont
* @author Sven Mawson
*/
public class UrlEscapeUtilsTest {
@Test public void actsAsUrlFormParameterEscaper() {
try {
UrlEscapeUtils.escape(null);
fail("Escaping null string should throw exception");
} catch (NullPointerException x) {
// pass
}
// 0-9, A-Z, a-z should be left unescaped
assertUnescaped('a');
assertUnescaped('z');
assertUnescaped('A');
assertUnescaped('Z');
assertUnescaped('0');
assertUnescaped('9');
// Unreserved characters used in java.net.URLEncoder
assertUnescaped('-');
assertUnescaped('_');
assertUnescaped('.');
assertUnescaped('*');
assertEscaping("%3D", '=');
assertEscaping("%00", '\u0000'); // nul
assertEscaping("%7F", '\u007f'); // del
assertEscaping("%C2%80", '\u0080'); // xx-00010,x-000000
assertEscaping("%DF%BF", '\u07ff'); // xx-11111,x-111111
assertEscaping("%E0%A0%80", '\u0800'); // xxx-0000,x-100000,x-00,0000
assertEscaping("%EF%BF%BF", '\uffff'); // xxx-1111,x-111111,x-11,1111
assertUnicodeEscaping("%F0%90%80%80", '\uD800', '\uDC00');
assertUnicodeEscaping("%F4%8F%BF%BF", '\uDBFF', '\uDFFF');
assertEquals("", UrlEscapeUtils.escape(""));
assertEquals("safestring", UrlEscapeUtils.escape("safestring"));
assertEquals("embedded%00null", UrlEscapeUtils.escape("embedded\0null"));
assertEquals("max%EF%BF%BFchar", UrlEscapeUtils.escape("max\uffffchar"));
// Specified as safe by RFC 2396 but not by java.net.URLEncoder.
assertEscaping("%21", '!');
assertEscaping("%28", '(');
assertEscaping("%29", ')');
assertEscaping("%7E", '~');
assertEscaping("%27", '\'');
// Plus for spaces
assertEscaping("+", ' ');
assertEscaping("%2B", '+');
assertEquals("safe+with+spaces", UrlEscapeUtils.escape("safe with spaces"));
assertEquals("foo%40bar.com", UrlEscapeUtils.escape("foo@bar.com"));
}
/**
* Asserts that {@link UrlEscapeUtils} escapes the given character.
*
* @param expected the expected escape result
* @param c the character to test
*/
private static void assertEscaping(String expected, char c) {
String escaped = computeReplacement(c);
assertNotNull(escaped);
assertEquals(expected, escaped);
}
/**
* Asserts that {@link UrlEscapeUtils} does not escape the given character.
*
* @param c the character to test
*/
private static void assertUnescaped(char c) {
assertNull(computeReplacement(c));
}
/**
* Asserts that {@link UrlEscapeUtils} escapes the given hi/lo surrogate pair into
* the expected string.
*
* @param expected the expected output string
* @param hi the high surrogate pair character
* @param lo the low surrogate pair character
*/
private static void assertUnicodeEscaping(String expected, char hi, char lo) {
int cp = Character.toCodePoint(hi, lo);
String escaped = computeReplacement(cp);
assertNotNull(escaped);
assertEquals(expected, escaped);
}
private static String computeReplacement(char c) {
return stringOrNull(UrlEscapeUtils.escape(c));
}
private static String computeReplacement(int cp) {
return stringOrNull(UrlEscapeUtils.escape(cp));
}
private static String stringOrNull(char[] chars) {
return (chars == null) ? null : new String(chars);
}
}
@Anthonyeef

This comment has been minimized.

Copy link

commented Nov 18, 2015

Thank you for making this! And I wonder how to use this code in Android project? It should be fit into Retrofit + OkHttp, right?

@serj-lotutovici

This comment has been minimized.

Copy link
Owner Author

commented Dec 4, 2015

@Anthonyeef yup... you just have to set the interceptor to the OkHttpClient... something like this:

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(oauth1SigningInterceptor);

and set that client to your Retrofit instance.

@Anthonyeef

This comment has been minimized.

Copy link

commented Apr 9, 2016

Thank you : )

@aomari

This comment has been minimized.

Copy link

commented Jan 18, 2018

First of all, thanks for this nice code, but I have a small problem there
inside Oauth1SigningInterceptor class
in parameters.put(key, value);
I have to pass a json object as a body parameter to the request, but after the debugging I see that the key here equals to my json body and the value are equals to ""}}"
do you have any idea why acts like this ?

@Zoha131

This comment has been minimized.

Copy link

commented Jul 17, 2018

How can I create Oauth1SigningInterceptor object? in OAuth 1.0a “one-legged” authentication I have only consumer_key and consumer_secret but the constructor also require accessToken and accessSecret

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.