aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk2
-rw-r--r--android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java4
-rw-r--r--android/main/java/com/squareup/okhttp/internal/OptionalMethod.java169
-rw-r--r--android/main/java/com/squareup/okhttp/internal/Platform.java71
-rw-r--r--android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java337
-rw-r--r--android/test/java/com/squareup/okhttp/internal/PlatformTest.java234
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java33
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java96
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java18
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java59
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java1
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Connection.java16
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java98
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java4
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java12
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java4
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java13
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java22
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java4
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java2
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java2
21 files changed, 1172 insertions, 29 deletions
diff --git a/Android.mk b/Android.mk
index 4baf397..7444ae8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -68,7 +68,7 @@ LOCAL_MODULE := okhttp-tests-nojarjar
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(okhttp_test_src_files)
LOCAL_JAVACFLAGS := -encoding UTF-8
-LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar
+LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar conscrypt
LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
index 36c3101..e64eec4 100644
--- a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
+++ b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
@@ -86,7 +86,11 @@ public class ConfigAwareConnectionPool {
// If the network config has changed then existing pooled connections should not be
// re-used. By setting connectionPool to null it ensures that the next time
// getConnectionPool() is called a new pool will be created.
+ ConnectionPool oldConnectionPool = connectionPool;
connectionPool = null;
+ if (oldConnectionPool != null) {
+ oldConnectionPool.enterDrainMode();
+ }
}
}
});
diff --git a/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
new file mode 100644
index 0000000..81aef8e
--- /dev/null
+++ b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package com.squareup.okhttp.internal;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Duck-typing for methods: Represents a method that may or may not be present on an object.
+ *
+ * @param <T> the type of the object the method might be on, typically an interface or base class
+ */
+class OptionalMethod<T> {
+
+ /** The return type of the method. null means "don't care". */
+ private final Class<?> returnType;
+
+ private final String methodName;
+
+ private final Class[] methodParams;
+
+ /**
+ * Creates an optional method.
+ *
+ * @param returnType the return type to required, null if it does not matter
+ * @param methodName the name of the method
+ * @param methodParams the method parameter types
+ */
+ public OptionalMethod(Class<?> returnType, String methodName, Class... methodParams) {
+ this.returnType = returnType;
+ this.methodName = methodName;
+ this.methodParams = methodParams;
+ }
+
+ /**
+ * Returns true if the method exists on the supplied {@code target}.
+ */
+ public boolean isSupported(T target) {
+ return getMethod(target.getClass()) != null;
+ }
+
+ /**
+ * Invokes the method on {@code target} with {@code args}. If the method does not exist or is not
+ * public then {@code null} is returned. See also
+ * {@link #invokeOptionalWithoutCheckedException(Object, Object...)}.
+ *
+ * @throws IllegalArgumentException if the arguments are invalid
+ * @throws InvocationTargetException if the invocation throws an exception
+ */
+ public Object invokeOptional(T target, Object... args) throws InvocationTargetException {
+ Method m = getMethod(target.getClass());
+ if (m == null) {
+ return null;
+ }
+ try {
+ return m.invoke(target, args);
+ } catch (IllegalAccessException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Invokes the method on {@code target}. If the method does not exist or is not
+ * public then {@code null} is returned. Any RuntimeException thrown by the method is thrown,
+ * checked exceptions are wrapped in an {@link AssertionError}.
+ *
+ * @throws IllegalArgumentException if the arguments are invalid
+ */
+ public Object invokeOptionalWithoutCheckedException(T target, Object... args) {
+ try {
+ return invokeOptional(target, args);
+ } catch (InvocationTargetException e) {
+ Throwable targetException = e.getTargetException();
+ if (targetException instanceof RuntimeException) {
+ throw (RuntimeException) targetException;
+ }
+ throw new AssertionError("Unexpected exception", targetException);
+ }
+ }
+
+ /**
+ * Invokes the method on {@code target} with {@code args}. Throws an error if the method is not
+ * supported. See also {@link #invokeWithoutCheckedException(Object, Object...)}.
+ *
+ * @throws IllegalArgumentException if the arguments are invalid
+ * @throws InvocationTargetException if the invocation throws an exception
+ */
+ public Object invoke(T target, Object... args) throws InvocationTargetException {
+ Method m = getMethod(target.getClass());
+ if (m == null) {
+ throw new AssertionError("Method " + methodName + " not supported for object " + target);
+ }
+ try {
+ return m.invoke(target, args);
+ } catch (IllegalAccessException e) {
+ // Method should be public: we checked.
+ throw new AssertionError("Unexpectedly could not call: " + m, e);
+ }
+ }
+
+ /**
+ * Invokes the method on {@code target}. Throws an error if the method is not supported. Any
+ * RuntimeException thrown by the method is thrown, checked exceptions are wrapped in
+ * an {@link AssertionError}.
+ *
+ * @throws IllegalArgumentException if the arguments are invalid
+ */
+ public Object invokeWithoutCheckedException(T target, Object... args) {
+ try {
+ return invoke(target, args);
+ } catch (InvocationTargetException e) {
+ Throwable targetException = e.getTargetException();
+ if (targetException instanceof RuntimeException) {
+ throw (RuntimeException) targetException;
+ }
+ throw new AssertionError("Unexpected exception", targetException);
+ }
+ }
+
+ /**
+ * Perform a lookup for the method. No caching.
+ * In order to return a method the method name and arguments must match those specified when
+ * the {@link OptionalMethod} was created. If the return type is specified (i.e. non-null) it
+ * must also be compatible. The method must also be public.
+ */
+ private Method getMethod(Class<?> clazz) {
+ Method method = null;
+ if (methodName != null) {
+ method = getPublicMethod(clazz, methodName, methodParams);
+ if (method != null
+ && returnType != null
+ && !returnType.isAssignableFrom(method.getReturnType())) {
+
+ // If the return type is non-null it must be compatible.
+ method = null;
+ }
+ }
+ return method;
+ }
+
+ private static Method getPublicMethod(Class<?> clazz, String methodName, Class[] parameterTypes) {
+ Method method = null;
+ try {
+ method = clazz.getMethod(methodName, parameterTypes);
+ if ((method.getModifiers() & Modifier.PUBLIC) == 0) {
+ method = null;
+ }
+ } catch (NoSuchMethodException e) {
+ // None.
+ }
+ return method;
+ }
+}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 7d0e847..121b156 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -30,8 +30,8 @@ import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import javax.net.ssl.SSLSocket;
-import com.android.org.conscrypt.OpenSSLSocketImpl;
import com.squareup.okhttp.Protocol;
+
import okio.ByteString;
/**
@@ -44,6 +44,25 @@ public final class Platform {
return PLATFORM;
}
+ /** setUseSessionTickets(boolean) */
+ private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
+ new OptionalMethod<Socket>(null, "setUseSessionTickets", Boolean.TYPE);
+ /** setHostname(String) */
+ private static final OptionalMethod<Socket> SET_HOSTNAME =
+ new OptionalMethod<Socket>(null, "setHostname", String.class);
+ /** byte[] getAlpnSelectedProtocol() */
+ private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
+ new OptionalMethod<Socket>(byte[].class, "getAlpnSelectedProtocol");
+ /** setAlpnSelectedProtocol(byte[]) */
+ private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
+ new OptionalMethod<Socket>(null, "setAlpnProtocols", byte[].class );
+ /** byte[] getNpnSelectedProtocol() */
+ private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL =
+ new OptionalMethod<Socket>(byte[].class, "getNpnSelectedProtocol");
+ /** setNpnSelectedProtocol(byte[]) */
+ private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
+ new OptionalMethod<Socket>(null, "setNpnProtocols", byte[].class);
+
public void logW(String warning) {
System.logW(warning);
}
@@ -61,11 +80,8 @@ public final class Platform {
}
public void enableTlsExtensions(SSLSocket socket, String uriHost) {
- if (socket instanceof OpenSSLSocketImpl) {
- OpenSSLSocketImpl openSSLSocket = (OpenSSLSocketImpl) socket;
- openSSLSocket.setUseSessionTickets(true);
- openSSLSocket.setHostname(uriHost);
- }
+ SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(socket, true);
+ SET_HOSTNAME.invokeOptionalWithoutCheckedException(socket, uriHost);
}
public void supportTlsIntolerantServer(SSLSocket socket) {
@@ -97,18 +113,28 @@ public final class Platform {
* Returns the negotiated protocol, or null if no protocol was negotiated.
*/
public ByteString getNpnSelectedProtocol(SSLSocket socket) {
- if (!(socket instanceof OpenSSLSocketImpl)) {
+ boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
+ boolean npnSupported = GET_NPN_SELECTED_PROTOCOL.isSupported(socket);
+ if (!(alpnSupported || npnSupported)) {
return null;
}
- OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
// Prefer ALPN's result if it is present.
- byte[] alpnResult = socketImpl.getAlpnSelectedProtocol();
- if (alpnResult != null) {
- return ByteString.of(alpnResult);
+ if (alpnSupported) {
+ byte[] alpnResult =
+ (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+ if (alpnResult != null) {
+ return ByteString.of(alpnResult);
+ }
}
- byte[] npnResult = socketImpl.getNpnSelectedProtocol();
- return npnResult == null ? null : ByteString.of(npnResult);
+ if (npnSupported) {
+ byte[] npnResult =
+ (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+ if (npnResult != null) {
+ return ByteString.of(npnResult);
+ }
+ }
+ return null;
}
/**
@@ -116,11 +142,20 @@ public final class Platform {
* protocols are only sent if the socket implementation supports NPN.
*/
public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
- if (socket instanceof OpenSSLSocketImpl) {
- OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
- byte[] protocols = concatLengthPrefixed(npnProtocols);
- socketImpl.setAlpnProtocols(protocols);
- socketImpl.setNpnProtocols(protocols);
+ boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(socket);
+ boolean npnSupported = SET_NPN_PROTOCOLS.isSupported(socket);
+ if (!(alpnSupported || npnSupported)) {
+ return;
+ }
+
+ byte[] protocols = concatLengthPrefixed(npnProtocols);
+ if (alpnSupported) {
+ SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(
+ socket, new Object[] { protocols });
+ }
+ if (npnSupported) {
+ SET_NPN_PROTOCOLS.invokeWithoutCheckedException(
+ socket, new Object[] { protocols });
}
}
diff --git a/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
new file mode 100644
index 0000000..c53fb21
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
@@ -0,0 +1,337 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package com.squareup.okhttp.internal;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link OptionalMethod}.
+ */
+public class OptionalMethodTest {
+ @SuppressWarnings("unused")
+ private static class BaseClass {
+ public String stringMethod() {
+ return "string";
+ }
+
+ public void voidMethod() {}
+ }
+
+ @SuppressWarnings("unused")
+ private static class SubClass1 extends BaseClass {
+ public String subclassMethod() {
+ return "subclassMethod1";
+ }
+
+ public String methodWithArgs(String arg) {
+ return arg;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class SubClass2 extends BaseClass {
+ public int subclassMethod() {
+ return 1234;
+ }
+
+ public String methodWithArgs(String arg) {
+ return arg;
+ }
+
+ public void throwsException() throws IOException {
+ throw new IOException();
+ }
+
+ public void throwsRuntimeException() throws Exception {
+ throw new NumberFormatException();
+ }
+
+ protected void nonPublic() {}
+ }
+
+ private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_ANY =
+ new OptionalMethod<BaseClass>(null, "stringMethod");
+ private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_STRING =
+ new OptionalMethod<BaseClass>(String.class, "stringMethod");
+ private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_INT =
+ new OptionalMethod<BaseClass>(Integer.TYPE, "stringMethod");
+ private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_ANY =
+ new OptionalMethod<BaseClass>(null, "voidMethod");
+ private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_VOID =
+ new OptionalMethod<BaseClass>(Void.TYPE, "voidMethod");
+ private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_ANY =
+ new OptionalMethod<BaseClass>(null, "subclassMethod");
+ private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_STRING =
+ new OptionalMethod<BaseClass>(String.class, "subclassMethod");
+ private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_INT =
+ new OptionalMethod<BaseClass>(Integer.TYPE, "subclassMethod");
+ private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_WRONG_PARAMS =
+ new OptionalMethod<BaseClass>(null, "methodWithArgs", Integer.class);
+ private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_CORRECT_PARAMS =
+ new OptionalMethod<BaseClass>(null, "methodWithArgs", String.class);
+
+ private final static OptionalMethod<BaseClass> THROWS_EXCEPTION =
+ new OptionalMethod<BaseClass>(null, "throwsException");
+ private final static OptionalMethod<BaseClass> THROWS_RUNTIME_EXCEPTION =
+ new OptionalMethod<BaseClass>(null, "throwsRuntimeException");
+ private final static OptionalMethod<BaseClass> NON_PUBLIC =
+ new OptionalMethod<BaseClass>(null, "nonPublic");
+
+ @Test
+ public void isSupported() throws Exception {
+ {
+ BaseClass base = new BaseClass();
+ assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(base));
+ assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(base));
+ assertFalse(STRING_METHOD_RETURNS_INT.isSupported(base));
+ assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(base));
+ assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(base));
+ assertFalse(SUBCLASS_METHOD_RETURNS_ANY.isSupported(base));
+ assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(base));
+ assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(base));
+ assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(base));
+ assertFalse(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(base));
+ }
+ {
+ SubClass1 subClass1 = new SubClass1();
+ assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass1));
+ assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass1));
+ assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass1));
+ assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass1));
+ assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass1));
+ assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass1));
+ assertTrue(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass1));
+ assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass1));
+ assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass1));
+ assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass1));
+ }
+ {
+ SubClass2 subClass2 = new SubClass2();
+ assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass2));
+ assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass2));
+ assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass2));
+ assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass2));
+ assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass2));
+ assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass2));
+ assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass2));
+ assertTrue(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass2));
+ assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass2));
+ assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass2));
+ }
+ }
+
+ @Test
+ public void invoke() throws Exception {
+ {
+ BaseClass base = new BaseClass();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(base));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(base));
+ assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, base);
+ assertNull(VOID_METHOD_RETURNS_ANY.invoke(base));
+ assertNull(VOID_METHOD_RETURNS_VOID.invoke(base));
+ assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_ANY, base);
+ assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, base);
+ assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, base);
+ assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, base);
+ assertErrorOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, base);
+ }
+ {
+ SubClass1 subClass1 = new SubClass1();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass1));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass1));
+ assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass1);
+ assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass1));
+ assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass1));
+ assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass1));
+ assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invoke(subClass1));
+ assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, subClass1);
+ assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass1);
+ assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass1, "arg"));
+ }
+
+ {
+ SubClass2 subClass2 = new SubClass2();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass2));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass2));
+ assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass2);
+ assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass2));
+ assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass2));
+ assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass2));
+ assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, subClass2);
+ assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invoke(subClass2));
+ assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass2);
+ assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass2, "arg"));
+ }
+ }
+
+ @Test
+ public void invokeBadArgs() throws Exception {
+ SubClass1 subClass1 = new SubClass1();
+ assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+ assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+ assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+ assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+ assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+ }
+
+ @Test
+ public void invokeWithException() throws Exception {
+ SubClass2 subClass2 = new SubClass2();
+ try {
+ THROWS_EXCEPTION.invoke(subClass2);
+ } catch (InvocationTargetException expected) {
+ assertTrue(expected.getTargetException() instanceof IOException);
+ }
+
+ try {
+ THROWS_RUNTIME_EXCEPTION.invoke(subClass2);
+ } catch (InvocationTargetException expected) {
+ assertTrue(expected.getTargetException() instanceof NumberFormatException);
+ }
+ }
+
+ @Test
+ public void invokeNonPublic() throws Exception {
+ SubClass2 subClass2 = new SubClass2();
+ assertFalse(NON_PUBLIC.isSupported(subClass2));
+ assertErrorOnInvoke(NON_PUBLIC, subClass2);
+ }
+
+ @Test
+ public void invokeOptional() throws Exception {
+ {
+ BaseClass base = new BaseClass();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(base));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(base));
+ assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(base));
+ assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(base));
+ assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(base));
+ assertNull(SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(base));
+ assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(base));
+ assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(base));
+ assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(base));
+ assertNull(METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(base));
+ }
+ {
+ SubClass1 subClass1 = new SubClass1();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+ assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass1));
+ assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+ assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass1));
+ assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+ assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+ assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass1));
+ assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass1));
+ assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass1, "arg"));
+ }
+
+ {
+ SubClass2 subClass2 = new SubClass2();
+ assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+ assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+ assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass2));
+ assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+ assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass2));
+ assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+ assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+ assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass2));
+ assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass2));
+ assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass2, "arg"));
+ }
+ }
+
+ @Test
+ public void invokeOptionalBadArgs() throws Exception {
+ SubClass1 subClass1 = new SubClass1();
+ assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+ assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+ assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+ assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+ assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+ }
+
+ @Test
+ public void invokeOptionalWithException() throws Exception {
+ SubClass2 subClass2 = new SubClass2();
+ try {
+ THROWS_EXCEPTION.invokeOptional(subClass2);
+ } catch (InvocationTargetException expected) {
+ assertTrue(expected.getTargetException() instanceof IOException);
+ }
+
+ try {
+ THROWS_RUNTIME_EXCEPTION.invokeOptional(subClass2);
+ } catch (InvocationTargetException expected) {
+ assertTrue(expected.getTargetException() instanceof NumberFormatException);
+ }
+ }
+
+ @Test
+ public void invokeOptionalNonPublic() throws Exception {
+ SubClass2 subClass2 = new SubClass2();
+ assertFalse(NON_PUBLIC.isSupported(subClass2));
+ assertErrorOnInvokeOptional(NON_PUBLIC, subClass2);
+ }
+
+ private static <T> void assertErrorOnInvoke(
+ OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+ try {
+ optionalMethod.invoke(base, args);
+ fail();
+ } catch (Error expected) {
+ }
+ }
+
+ private static <T> void assertIllegalArgumentExceptionOnInvoke(
+ OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+ try {
+ optionalMethod.invoke(base, args);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ private static <T> void assertErrorOnInvokeOptional(
+ OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+ try {
+ optionalMethod.invokeOptional(base, args);
+ fail();
+ } catch (Error expected) {
+ }
+ }
+
+ private static <T> void assertIllegalArgumentExceptionOnInvokeOptional(
+ OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+ try {
+ optionalMethod.invokeOptional(base, args);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+}
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
new file mode 100644
index 0000000..9e293f6
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package com.squareup.okhttp.internal;
+
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+import com.squareup.okhttp.Protocol;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import okio.ByteString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link Platform}.
+ */
+public class PlatformTest {
+
+ @Test
+ public void enableTlsExtensionOptionalMethods() throws Exception {
+ Platform platform = new Platform();
+
+ // Expect no error
+ TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+ platform.enableTlsExtensions(arbitrarySocketImpl, "host");
+
+ FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+ platform.enableTlsExtensions(openSslSocket, "host");
+ assertTrue(openSslSocket.useSessionTickets);
+ assertEquals("host", openSslSocket.hostname);
+ }
+
+ @Test
+ public void getNpnSelectedProtocol() throws Exception {
+ Platform platform = new Platform();
+ byte[] npnBytes = "npn".getBytes();
+ byte[] alpnBytes = "alpn".getBytes();
+
+ TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+ assertNull(platform.getNpnSelectedProtocol(arbitrarySocketImpl));
+
+ NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+ npnOnlySSLSocketImpl.npnProtocols = npnBytes;
+ assertEquals(ByteString.of(npnBytes), platform.getNpnSelectedProtocol(npnOnlySSLSocketImpl));
+
+ FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+ openSslSocket.npnProtocols = npnBytes;
+ openSslSocket.alpnProtocols = alpnBytes;
+ assertEquals(ByteString.of(alpnBytes), platform.getNpnSelectedProtocol(openSslSocket));
+ }
+
+ @Test
+ public void setNpnProtocols() throws Exception {
+ Platform platform = new Platform();
+ List<Protocol> protocols = Arrays.asList(Protocol.SPDY_3);
+
+ // No error
+ TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+ platform.setNpnProtocols(arbitrarySocketImpl, protocols);
+
+ NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+ platform.setNpnProtocols(npnOnlySSLSocketImpl, protocols);
+ assertNotNull(npnOnlySSLSocketImpl.npnProtocols);
+
+ FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+ platform.setNpnProtocols(openSslSocket, protocols);
+ assertNotNull(openSslSocket.npnProtocols);
+ assertNotNull(openSslSocket.alpnProtocols);
+ }
+
+ private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
+ private boolean useSessionTickets;
+ private String hostname;
+ private byte[] npnProtocols;
+ private byte[] alpnProtocols;
+
+ public FullOpenSSLSocketImpl() throws IOException {
+ super(null);
+ }
+
+ @Override
+ public void setUseSessionTickets(boolean useSessionTickets) {
+ this.useSessionTickets = useSessionTickets;
+ }
+
+ @Override
+ public void setHostname(String hostname) {
+ this.hostname = hostname;
+ }
+
+ @Override
+ public void setNpnProtocols(byte[] npnProtocols) {
+ this.npnProtocols = npnProtocols;
+ }
+
+ @Override
+ public byte[] getNpnSelectedProtocol() {
+ return npnProtocols;
+ }
+
+ @Override
+ public void setAlpnProtocols(byte[] alpnProtocols) {
+ this.alpnProtocols = alpnProtocols;
+ }
+
+ @Override
+ public byte[] getAlpnSelectedProtocol() {
+ return alpnProtocols;
+ }
+ }
+
+ // Legacy case
+ private static class NpnOnlySSLSocketImpl extends TestSSLSocketImpl {
+
+ private byte[] npnProtocols;
+
+ public void setNpnProtocols(byte[] npnProtocols) {
+ this.npnProtocols = npnProtocols;
+ }
+
+ public byte[] getNpnSelectedProtocol() {
+ return npnProtocols;
+ }
+ }
+
+ private static class TestSSLSocketImpl extends SSLSocket {
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getEnabledCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public void setEnabledCipherSuites(String[] suites) {
+ }
+
+ @Override
+ public String[] getSupportedProtocols() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getEnabledProtocols() {
+ return new String[0];
+ }
+
+ @Override
+ public void setEnabledProtocols(String[] protocols) {
+ }
+
+ @Override
+ public SSLSession getSession() {
+ return null;
+ }
+
+ @Override
+ public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
+ }
+
+ @Override
+ public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
+ }
+
+ @Override
+ public void startHandshake() throws IOException {
+ }
+
+ @Override
+ public void setUseClientMode(boolean mode) {
+ }
+
+ @Override
+ public boolean getUseClientMode() {
+ return false;
+ }
+
+ @Override
+ public void setNeedClientAuth(boolean need) {
+ }
+
+ @Override
+ public void setWantClientAuth(boolean want) {
+ }
+
+ @Override
+ public boolean getNeedClientAuth() {
+ return false;
+ }
+
+ @Override
+ public boolean getWantClientAuth() {
+ return false;
+ }
+
+ @Override
+ public void setEnableSessionCreation(boolean flag) {
+ }
+
+ @Override
+ public boolean getEnableSessionCreation() {
+ return false;
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index 3ab47b4..a2a2653 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -380,6 +380,39 @@ public final class ConnectionPoolTest {
assertEquals(0, pool.getSpdyConnectionCount());
}
+ // Tests to demonstrate Android bug http://b/18369687 and the solution to it.
+ @Test public void connectionCleanup_draining() throws IOException, InterruptedException {
+ ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+
+ // Add 3 connections to the pool.
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ pool.share(spdyA);
+ assertEquals(3, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // With no method calls made to the pool it will not clean up any connections.
+ Thread.sleep(KEEP_ALIVE_DURATION_MS * 5);
+ assertEquals(3, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Change the pool into a mode that will clean up connections.
+ pool.enterDrainMode();
+
+ // Give the drain thread a chance to run.
+ for (int i = 0; i < 5; i++) {
+ Thread.sleep(KEEP_ALIVE_DURATION_MS);
+ if (pool.isDrained()) {
+ break;
+ }
+ }
+
+ // All connections should have drained.
+ assertEquals(0, pool.getConnectionCount());
+ }
+
@Test public void evictAllConnections() throws Exception {
resetWithPoolSize(10);
pool.recycle(httpA);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
new file mode 100644
index 0000000..2b05638
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * 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.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public final class DisconnectTest {
+ private final MockWebServer server = new MockWebServer();
+ private final OkHttpClient client = new OkHttpClient();
+
+ @Test public void interruptWritingRequestBody() throws Exception {
+ int requestBodySize = 10 * 1024 * 1024; // 10 MiB
+
+ server.enqueue(new MockResponse()
+ .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ disconnectLater(connection, 500);
+
+ connection.setDoOutput(true);
+ connection.setFixedLengthStreamingMode(requestBodySize);
+ OutputStream requestBody = connection.getOutputStream();
+ byte[] buffer = new byte[1024];
+ try {
+ for (int i = 0; i < requestBodySize; i += buffer.length) {
+ requestBody.write(buffer);
+ requestBody.flush();
+ }
+ fail("Expected connection to be closed");
+ } catch (IOException expected) {
+ }
+
+ connection.disconnect();
+ }
+
+ @Test public void interruptReadingResponseBody() throws Exception {
+ int responseBodySize = 10 * 1024 * 1024; // 10 MiB
+
+ server.enqueue(new MockResponse()
+ .setBody(new byte[responseBodySize])
+ .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ disconnectLater(connection, 500);
+
+ InputStream responseBody = connection.getInputStream();
+ byte[] buffer = new byte[1024];
+ try {
+ while (responseBody.read(buffer) != -1) {
+ }
+ fail("Expected connection to be closed");
+ } catch (IOException expected) {
+ }
+
+ connection.disconnect();
+ }
+
+ private void disconnectLater(final HttpURLConnection connection, final int delayMillis) {
+ Thread interruptingCow = new Thread() {
+ @Override public void run() {
+ try {
+ sleep(delayMillis);
+ connection.disconnect();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ interruptingCow.start();
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index c8e2647..4c94e91 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -401,6 +401,24 @@ public final class RouteSelectorTest {
assertEquals(regularRoutes.size(), routesWithFailedRoute.size());
}
+ @Test public void getHostString() throws Exception {
+ // Name proxy specification.
+ InetSocketAddress socketAddress = InetSocketAddress.createUnresolved("host", 1234);
+ assertEquals("host", RouteSelector.getHostString(socketAddress));
+ socketAddress = InetSocketAddress.createUnresolved("127.0.0.1", 1234);
+ assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+
+ // InetAddress proxy specification.
+ socketAddress = new InetSocketAddress(InetAddress.getByName("localhost"), 1234);
+ assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+ socketAddress = new InetSocketAddress(
+ InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }), 1234);
+ assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+ socketAddress = new InetSocketAddress(
+ InetAddress.getByAddress("foobar", new byte[] { 127, 0, 0, 1 }), 1234);
+ assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+ }
+
private void assertConnection(Connection connection, Address address, Proxy proxy,
InetAddress socketAddress, int socketPort, boolean modernTls) {
assertEquals(address, connection.getRoute().getAddress());
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index a9f902a..0fcc4bd 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -65,6 +65,7 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -429,6 +430,11 @@ public final class URLConnectionTest {
HttpURLConnection connection1 = client.open(server.getUrl("/a"));
connection1.setReadTimeout(100);
assertContent("This connection won't pool properly", connection1);
+
+ // Give the server time to enact the socket policy if it's one that could happen after the
+ // client has received the response.
+ Thread.sleep(500);
+
assertEquals(0, server.takeRequest().getSequenceNumber());
HttpURLConnection connection2 = client.open(server.getUrl("/b"));
connection2.setReadTimeout(100);
@@ -686,6 +692,10 @@ public final class URLConnectionTest {
client.setHostnameVerifier(new RecordingHostnameVerifier());
assertContent("abc", client.open(server.getUrl("/")));
+
+ // Give the server time to disconnect.
+ Thread.sleep(500);
+
assertContent("def", client.open(server.getUrl("/")));
RecordedRequest request1 = server.takeRequest();
@@ -1029,7 +1039,9 @@ public final class URLConnectionTest {
}
@Test public void disconnectedConnection() throws IOException {
- server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+ server.enqueue(new MockResponse()
+ .throttleBody(2, 100, TimeUnit.MILLISECONDS)
+ .setBody("ABCD"));
server.play();
connection = client.open(server.getUrl("/"));
@@ -1037,6 +1049,10 @@ public final class URLConnectionTest {
assertEquals('A', (char) in.read());
connection.disconnect();
try {
+ // Reading 'B' may succeed if it's buffered.
+ in.read();
+
+ // But 'C' shouldn't be buffered (the response is throttled) and this should fail.
in.read();
fail("Expected a connection closed exception");
} catch (IOException expected) {
@@ -1272,6 +1288,9 @@ public final class URLConnectionTest {
// Seed the pool with a bad connection.
assertContent("a", client.open(server.getUrl("/")));
+ // Give the server time to disconnect.
+ Thread.sleep(500);
+
// This connection will need to be recovered. When it is, transparent gzip should still work!
assertContent("b", client.open(server.getUrl("/")));
@@ -1317,11 +1336,13 @@ public final class URLConnectionTest {
HttpURLConnection connection1 = client.open(server.getUrl("/"));
InputStream in1 = connection1.getInputStream();
assertEquals("ABCDE", readAscii(in1, 5));
+ in1.close();
connection1.disconnect();
HttpURLConnection connection2 = client.open(server.getUrl("/"));
InputStream in2 = connection2.getInputStream();
assertEquals("LMNOP", readAscii(in2, 5));
+ in2.close();
connection2.disconnect();
assertEquals(0, server.takeRequest().getSequenceNumber());
@@ -2621,6 +2642,9 @@ public final class URLConnectionTest {
assertContent("A", client.open(server.getUrl("/a")));
+ // Give the server time to disconnect.
+ Thread.sleep(500);
+
// If the request body is larger than OkHttp's replay buffer, the failure may still occur.
byte[] requestBody = new byte[requestSize];
new Random(0).nextBytes(requestBody);
@@ -2937,6 +2961,39 @@ public final class URLConnectionTest {
}
/**
+ * Tolerate bad https proxy response when using HttpResponseCache. Android bug 6754912.
+ */
+ @Test
+ public void testConnectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
+ initResponseCache();
+
+ server.useHttps(sslContext.getSocketFactory(), true);
+ // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912.
+ MockResponse
+ badProxyResponse = new MockResponse()
+ .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+ .clearHeaders()
+ .setBody("bogus proxy connect response content");
+
+ server.enqueue(badProxyResponse);
+ server.enqueue(new MockResponse().setBody("response"));
+
+ server.play();
+
+ URL url = new URL("https://android.com/foo");
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ ProxyConfig proxyConfig = ProxyConfig.PROXY_SYSTEM_PROPERTY;
+ HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, client, url);
+ assertContent("response", connection);
+
+ RecordedRequest connect = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
+ assertContains(connect.getHeaders(), "Host: android.com");
+ }
+
+ /**
* The RFC is unclear in this regard as it only specifies that this should
* invalidate the cache entry (if any).
*/
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
index f1decc8..82b1952 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
@@ -293,6 +293,7 @@ public final class HostnameVerifierTest {
assertTrue(verifier.verify("www.foo.com", session));
assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
assertFalse(verifier.verify("a.b.foo.com", session));
+ assertFalse(verifier.verify("foo.com.au", session));
}
@Test public void verifyWilcardCnOnTld() throws Exception {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index 94527af..743c33b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -17,10 +17,12 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpConnection;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.HttpTransport;
+import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.http.SpdyTransport;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import java.io.Closeable;
@@ -29,6 +31,8 @@ import java.net.Proxy;
import java.net.Socket;
import javax.net.ssl.SSLSocket;
import okio.ByteString;
+import okio.OkBuffer;
+import okio.Source;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
@@ -353,12 +357,22 @@ public final class Connection implements Closeable {
tunnelConnection.writeRequest(request.headers(), requestLine);
tunnelConnection.flush();
Response response = tunnelConnection.readResponse().request(request).build();
- tunnelConnection.emptyResponseBody();
+ // The response body from a CONNECT should be empty, but if it is not then we should consume
+ // it before proceeding.
+ long contentLength = OkHeaders.contentLength(response);
+ if (contentLength != -1) {
+ Source body = tunnelConnection.newFixedLengthSource(null, contentLength);
+ Util.skipAll(body, Integer.MAX_VALUE);
+ } else {
+ tunnelConnection.emptyResponseBody();
+ }
switch (response.code()) {
case HTTP_OK:
// Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If that
// happens, then we will have buffered bytes that are needed by the SSLSocket!
+ // This check is imperfect: it doesn't tell us whether a handshake will succeed, just that
+ // it will almost certainly fail because the proxy has sent unexpected data.
if (tunnelConnection.bufferSize() > 0) {
throw new IOException("TLS tunnel buffered too many bytes!");
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index fbd8351..1840701 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -82,6 +82,62 @@ public class ConnectionPool {
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
Util.threadFactory("OkHttp ConnectionPool", true));
+
+ private enum CleanMode {
+ /**
+ * Connection clean up is driven by usage of the pool. Each usage of the pool can schedule a
+ * clean up. A pool left in this state and unused may contain idle connections indefinitely.
+ */
+ NORMAL,
+ /**
+ * Entered when a pool has been orphaned and is not expected to receive more usage, except for
+ * references held by existing connections. See {@link #enterDrainMode()}.
+ * A thread runs periodically to close idle connections in the pool until the pool is empty and
+ * then the state moves to {@link #DRAINED}.
+ */
+ DRAINING,
+ /**
+ * The pool is empty and no clean-up is taking place. Connections may still be added to the
+ * pool due to latent references to the pool, in which case the pool re-enters
+ * {@link #DRAINING}. If the pool is DRAINED and no longer referenced it is safe to be garbage
+ * collected.
+ */
+ DRAINED
+ }
+ /** The current mode for cleaning connections in the pool */
+ private CleanMode cleanMode = CleanMode.NORMAL;
+
+ // A scheduled drainModeRunnable keeps a reference to the enclosing ConnectionPool,
+ // preventing the ConnectionPool from being garbage collected before all held connections have
+ // been explicitly closed. If this was not the case any open connections in the pool would trigger
+ // StrictMode violations in Android when they were garbage collected. http://b/18369687
+ private final Runnable drainModeRunnable = new Runnable() {
+ @Override public void run() {
+ // Close any connections we can.
+ connectionsCleanupRunnable.run();
+
+ synchronized (ConnectionPool.this) {
+ // See whether we should continue checking the connection pool.
+ if (connections.size() > 0) {
+ // Pause to avoid checking too regularly, which would drain the battery on mobile
+ // devices. The wait() surrenders the pool monitor and will not block other calls.
+ try {
+ // Use the keep alive duration as a rough indicator of a good check interval.
+ long keepAliveDurationMillis = keepAliveDurationNs / (1000 * 1000);
+ ConnectionPool.this.wait(keepAliveDurationMillis);
+ } catch (InterruptedException e) {
+ // Ignored.
+ }
+
+ // Reschedule "this" to perform another clean-up.
+ executorService.execute(this);
+ } else {
+ cleanMode = CleanMode.DRAINED;
+ }
+ }
+ }
+ };
+
private final Runnable connectionsCleanupRunnable = new Runnable() {
@Override public void run() {
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
@@ -123,6 +179,7 @@ public class ConnectionPool {
/**
* Returns a snapshot of the connections in this pool, ordered from newest to
* oldest. Waits for the cleanup callable to run if it is currently scheduled.
+ * Only use in tests.
*/
List<Connection> getConnections() {
waitForCleanupCallableToRun();
@@ -203,7 +260,7 @@ public class ConnectionPool {
connections.addFirst(foundConnection); // Add it back after iteration.
}
- executorService.execute(connectionsCleanupRunnable);
+ scheduleCleanupAsRequired();
return foundConnection;
}
@@ -240,9 +297,9 @@ public class ConnectionPool {
connections.addFirst(connection);
connection.incrementRecycleCount();
connection.resetIdleStartTime();
+ scheduleCleanupAsRequired();
}
- executorService.execute(connectionsCleanupRunnable);
}
/**
@@ -251,10 +308,10 @@ public class ConnectionPool {
*/
public void share(Connection connection) {
if (!connection.isSpdy()) throw new IllegalArgumentException();
- executorService.execute(connectionsCleanupRunnable);
if (connection.isAlive()) {
synchronized (this) {
connections.addFirst(connection);
+ scheduleCleanupAsRequired();
}
}
}
@@ -271,4 +328,39 @@ public class ConnectionPool {
Util.closeQuietly(connections.get(i));
}
}
+
+ /**
+ * A less abrupt way of draining the pool than {@link #evictAll()}. For use when the pool
+ * may still be referenced by active shared connections which cannot safely be closed.
+ */
+ public void enterDrainMode() {
+ synchronized(this) {
+ cleanMode = CleanMode.DRAINING;
+ executorService.execute(drainModeRunnable);
+ }
+ }
+
+ public boolean isDrained() {
+ synchronized(this) {
+ return cleanMode == CleanMode.DRAINED;
+ }
+ }
+
+ // Callers must synchronize on "this".
+ private void scheduleCleanupAsRequired() {
+ switch (cleanMode) {
+ case NORMAL:
+ executorService.execute(connectionsCleanupRunnable);
+ break;
+ case DRAINING:
+ // Do nothing -drainModeRunnable is already scheduled, and will reschedules itself as
+ // needed.
+ break;
+ case DRAINED:
+ // A new connection has potentially been offered up to a drained pool. Restart the drain.
+ cleanMode = CleanMode.DRAINING;
+ executorService.execute(drainModeRunnable);
+ break;
+ }
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
index b12b12d..718d471 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -122,6 +122,10 @@ public final class HttpConnection {
return state == STATE_CLOSED;
}
+ public void closeIfOwnedBy(Object owner) throws IOException {
+ connection.closeIfOwnedBy(owner);
+ }
+
public void flush() throws IOException {
sink.flush();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index f00fbe7..d796a6c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -411,6 +411,18 @@ public class HttpEngine {
}
/**
+ * Immediately closes the socket connection if it's currently held by this
+ * engine. Use this to interrupt an in-flight request from any thread. It's
+ * the caller's responsibility to close the request body and response body
+ * streams; otherwise resources may be leaked.
+ */
+ public final void disconnect() throws IOException {
+ if (transport != null) {
+ transport.disconnect(this);
+ }
+ }
+
+ /**
* Release any resources held by this engine. If a connection is still held by
* this engine, it is returned.
*/
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index a1b367f..2ffe039 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -150,4 +150,8 @@ public final class HttpTransport implements Transport {
// reference escapes.
return httpConnection.newUnknownLengthSource(cacheRequest);
}
+
+ @Override public void disconnect(HttpEngine engine) throws IOException {
+ httpConnection.closeIfOwnedBy(engine);
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index 899d914..32be0be 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -109,9 +109,18 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
@Override public final void disconnect() {
// Calling disconnect() before a connection exists should have no effect.
- if (httpEngine != null) {
- httpEngine.close();
+ if (httpEngine == null) return;
+
+ try {
+ httpEngine.disconnect();
+ } catch (IOException ignored) {
}
+
+ // This doesn't close the stream because doing so would require all stream
+ // access to be synchronized. It's expected that the thread using the
+ // connection will close its streams directly. If it doesn't, the worst
+ // case is that the GzipSource's Inflater won't be released until it's
+ // finalized. (This logs a warning on Android.)
}
/**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index c634bab..305be25 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -215,7 +215,7 @@ public final class RouteSelector {
String socketHost;
if (proxy.type() == Proxy.Type.DIRECT) {
- socketHost = uri.getHost();
+ socketHost = address.getUriHost();
socketPort = getEffectivePort(uri);
} else {
SocketAddress proxyAddress = proxy.address();
@@ -224,7 +224,7 @@ public final class RouteSelector {
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
- socketHost = proxySocketAddress.getHostName();
+ socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
@@ -233,6 +233,24 @@ public final class RouteSelector {
nextSocketAddressIndex = 0;
}
+ /**
+ * Obtain a "host" from an {@link InetSocketAddress}. This returns a string containing either an
+ * actual host name or a numeric IP address.
+ */
+ // Visible for testing
+ static String getHostString(InetSocketAddress socketAddress) {
+ InetAddress address = socketAddress.getAddress();
+ if (address == null) {
+ // The InetSocketAddress was specified with a string (either a numeric IP or a host name). If
+ // it is a name, all IPs for that name should be tried. If it is an IP address, only that IP
+ // address should be tried.
+ return socketAddress.getHostName();
+ }
+ // The InetSocketAddress has a specific address: we should only try that address. Therefore we
+ // return the address and ignore any host name that may be available.
+ return address.getHostAddress();
+ }
+
/** Returns true if there's another socket address to try. */
private boolean hasNextInetSocketAddress() {
return socketAddresses != null;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index e775d34..9db9643 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -219,6 +219,10 @@ public final class SpdyTransport implements Transport {
@Override public void releaseConnectionOnIdle() {
}
+ @Override public void disconnect(HttpEngine engine) throws IOException {
+ stream.close(ErrorCode.CANCEL);
+ }
+
@Override public boolean canReuseConnection() {
return true; // TODO: spdyConnection.isClosed() ?
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index 94c90d4..852a15b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -76,6 +76,8 @@ interface Transport {
*/
void releaseConnectionOnIdle() throws IOException;
+ void disconnect(HttpEngine engine) throws IOException;
+
/**
* Returns true if the socket connection held by this transport can be reused
* for a follow-up exchange.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
index a08773f..21e539c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
@@ -162,7 +162,7 @@ public final class OkHostnameVerifier implements HostnameVerifier {
return hostName.equals(cn);
}
- if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
+ if (cn.startsWith("*.") && hostName.equals(cn.substring(2))) {
return true; // "*.foo.com" matches "foo.com"
}