Skip to content

okhttp: support JDK9 ALPN #4136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import static com.google.common.truth.Truth.assertWithMessage;

import com.google.common.io.BaseEncoding;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import javax.xml.bind.DatatypeConverter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand Down Expand Up @@ -78,27 +78,27 @@ TestVectorBuilder withComment(String comment) {
}

TestVectorBuilder withKey(String key) {
this.key = DatatypeConverter.parseHexBinary(key);
this.key = BaseEncoding.base16().lowerCase().decode(key);
return this;
}

TestVectorBuilder withNonce(String nonce) {
this.nonce = DatatypeConverter.parseHexBinary(nonce);
this.nonce = BaseEncoding.base16().lowerCase().decode(nonce);
return this;
}

TestVectorBuilder withAad(String aad) {
this.aad = DatatypeConverter.parseHexBinary(aad);
this.aad = BaseEncoding.base16().lowerCase().decode(aad);
return this;
}

TestVectorBuilder withPlaintext(String plaintext) {
this.plaintext = DatatypeConverter.parseHexBinary(plaintext);
this.plaintext = BaseEncoding.base16().lowerCase().decode(plaintext);
return this;
}

TestVectorBuilder withCiphertext(String ciphertext) {
this.ciphertext = DatatypeConverter.parseHexBinary(ciphertext);
this.ciphertext = BaseEncoding.base16().lowerCase().decode(ciphertext);
return this;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@

/**
* Tests that Channel and Server builders properly hide the static constructors.
*
* <p>This test does nothing on Java 9.
*/
@RunWith(Parameterized.class)
public class ChannelAndServerBuilderTest {
Expand All @@ -49,13 +51,19 @@ public class ChannelAndServerBuilderTest {
@Parameters(name = "class={0}")
public static Collection<Object[]> params() throws Exception {
ClassLoader loader = ChannelAndServerBuilderTest.class.getClassLoader();
Collection<ClassInfo> classInfos =
ClassPath.from(loader).getTopLevelClassesRecursive("io.grpc");
// Java 9 doesn't expose the URLClassLoader, which breaks searching through the classpath
if (classInfos.isEmpty()) {
return new ArrayList<Object[]>();
}
List<Object[]> classes = new ArrayList<Object[]>();
for (ClassInfo classInfo : ClassPath.from(loader).getTopLevelClassesRecursive("io.grpc")) {
for (ClassInfo classInfo : classInfos) {
Class<?> clazz = Class.forName(classInfo.getName(), false /*initialize*/, loader);
if (ServerBuilder.class.isAssignableFrom(clazz) && clazz != ServerBuilder.class) {
classes.add(new Object[]{clazz});
} else if (ManagedChannelBuilder.class.isAssignableFrom(clazz)
&& clazz != ManagedChannelBuilder.class ) {
&& clazz != ManagedChannelBuilder.class) {
classes.add(new Object[]{clazz});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.google.common.base.Charsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand All @@ -32,6 +33,7 @@
import io.grpc.okhttp.internal.Protocol;
import java.io.IOException;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.junit.Rule;
Expand Down Expand Up @@ -110,7 +112,9 @@ public void negotiatorNotNull() {

@Test
public void negotiate_handshakeFails() throws IOException {
SSLParameters parameters = new SSLParameters();
OkHttpProtocolNegotiator negotiator = OkHttpProtocolNegotiator.get();
doReturn(parameters).when(sock).getSSLParameters();
doThrow(new IOException()).when(sock).startHandshake();
thrown.expect(IOException.class);

Expand Down
105 changes: 104 additions & 1 deletion okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,46 @@
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.AccessController;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import okio.Buffer;

/**
* Access to platform-specific features.
*
* <h3>Server name indication (SNI)</h3>
*
* Supported on Android 2.3+.
*
* <h3>Session Tickets</h3>
*
* Supported on Android 2.3+.
*
* <h3>Android Traffic Stats (Socket Tagging)</h3>
*
* Supported on Android 4.0+.
*
* <h3>ALPN (Application Layer Protocol Negotiation)</h3>
*
* Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was
* unstable.
*
* Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
* <p>Supported on OpenJDK 9+.
*
* <p>Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
*/
public class Platform {
public static final Logger logger = Logger.getLogger(Platform.class.getName());
Expand Down Expand Up @@ -199,6 +211,47 @@ private static Platform findPlatform() {
throw new RuntimeException(nsae);
}

// Find JDK9+ ALPN support
try {
// getApplicationProtocol() may throw UnsupportedOperationException, so first construct a
// dummy SSLEngine and verify the method does not throw.
SSLContext context = SSLContext.getInstance("TLS", sslProvider);
context.init(null, null, null);
SSLEngine engine = context.createSSLEngine();
Method getEngineApplicationProtocol =
AccessController.doPrivileged(
new PrivilegedExceptionAction<Method>() {
@Override
public Method run() throws Exception {
return SSLEngine.class.getMethod("getApplicationProtocol");
}
});
getEngineApplicationProtocol.invoke(engine);

Method setApplicationProtocols =
AccessController.doPrivileged(
new PrivilegedExceptionAction<Method>() {
@Override
public Method run() throws Exception {
return SSLParameters.class.getMethod("setApplicationProtocols", String[].class);
}
});
Method getApplicationProtocol =
AccessController.doPrivileged(
new PrivilegedExceptionAction<Method>() {
@Override
public Method run() throws Exception {
return SSLSocket.class.getMethod("getApplicationProtocol");
}
});
return new JdkAlpnPlatform(sslProvider, setApplicationProtocols, getApplicationProtocol);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should verify that sslProvider supports ALPN by calling getApplicationProtocol().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. AFAICT this requires instantiating an SSLEngine to invoke getApplicationProtocol(), which is the approach now taken.

} catch (NoSuchAlgorithmException ignored) {
} catch (KeyManagementException ignored) {
} catch (PrivilegedActionException ignored) {
} catch (IllegalAccessException ignored) {
} catch (InvocationTargetException ignored) {
}

// Find Jetty's ALPN extension for OpenJDK.
try {
String negoClassName = "org.eclipse.jetty.alpn.ALPN";
Expand Down Expand Up @@ -375,6 +428,56 @@ public TlsExtensionType getTlsExtensionType() {
}
}

/** OpenJDK 9+. */
private static class JdkAlpnPlatform extends Platform {
private final Method setApplicationProtocols;
private final Method getApplicationProtocol;

private JdkAlpnPlatform(
Provider provider, Method setApplicationProtocols, Method getApplicationProtocol) {
super(provider);
this.setApplicationProtocols = setApplicationProtocols;
this.getApplicationProtocol = getApplicationProtocol;
}

@Override
public TlsExtensionType getTlsExtensionType() {
return TlsExtensionType.ALPN_AND_NPN;
}

@Override
public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
SSLParameters parameters = sslSocket.getSSLParameters();
List<String> names = new ArrayList<String>(protocols.size());
for (Protocol protocol : protocols) {
if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
names.add(protocol.toString());
}
try {
setApplicationProtocols.invoke(
parameters, new Object[] {names.toArray(new String[names.size()])});
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
sslSocket.setSSLParameters(parameters);
}

/** Returns the negotiated protocol, or null if no protocol was negotiated. */
@Override
public String getSelectedProtocol(SSLSocket socket) {
try {
return (String) getApplicationProtocol.invoke(socket);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is known that getApplicationProtocol can throw UnsupportedOperationException. This case should be handled more cleanly.

For one, we can do e.getCause(). From there we can either propagate it directly (if it is a RuntimeException) or wrap it and provide a message with additional information.

(Actually, if we verify the provider supports this method ahead-of-time, then I'm fine with this current code.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now verify at startup that an SSLEngine created using the provider does not throw when getApplicationProtocol is invoked.

throw new RuntimeException(e);
}
}
}

/**
* OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
*/
Expand Down