diff options
21 files changed, 1172 insertions, 29 deletions
@@ -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" } |