-
-
Save serj-lotutovici/f85518e99b23008b3180 to your computer and use it in GitHub Desktop.
/* | |
* 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 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.
Thank you : )
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 ?
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
Hey, Thanks for the job ! I only have one problem, when one of my query parameter contains a space, it seems that it is not signing correctly the request (i'm consuming Twitter API). Do you have any idea ?
I have the same question as Zoha131 posted. How do I use Oauth1SigningInterceptor, when only have consumer_key and consumer_secret alone? The REST API publisher has not shared any thing else apart from consumer_key & consumer_secret values. I believe as per 2-legged we only can use "consumer_key & consumer_secret" and 3-legged has more. Please clarify that will be helpful to use this as my SigningInterceptor.
Thank you for making this! And I wonder how to use this code in Android project? It should be fit into Retrofit + OkHttp, right?