Merge "Be consistent about host names in RouteSelector."
diff --git a/Android.mk b/Android.mk
index 4baf397..7444ae8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -68,7 +68,7 @@
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/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.DeflaterOutputStream;
import javax.net.ssl.SSLSocket;
-import com.android.org.conscrypt.OpenSSLSocketImpl;
import com.squareup.okhttp.Protocol;
+
import okio.ByteString;
/**
@@ -44,6 +44,25 @@
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 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 @@
* 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 @@
* 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/internal/http/DisconnectTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
new file mode 100644
index 0000000..db84214
--- /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 = 2 * 1024 * 1024; // 2 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 = 2 * 1024 * 1024; // 2 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/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index a9f902a..127807f 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.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;
@@ -1029,7 +1030,9 @@
}
@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 +1040,10 @@
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) {
@@ -1317,11 +1324,13 @@
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());
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 @@
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 @@
}
/**
+ * 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 @@
// 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 @@
@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/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 @@
@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 @@
*/
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.