8182589: TLS SNI in new Java 9 client is not available
authormichaelm
Thu, 29 Jun 2017 11:10:30 +0100
changeset 45713 ee3f2cbfe23a
parent 45712 4f662a7e43a7
child 45714 1820d351198d
8182589: TLS SNI in new Java 9 client is not available Reviewed-by: dfuchs
jdk/src/java.base/share/classes/module-info.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java
jdk/test/java/net/httpclient/http2/TLSConnection.java
jdk/test/java/net/httpclient/http2/server/Http2TestServer.java
jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java
--- a/jdk/src/java.base/share/classes/module-info.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/java.base/share/classes/module-info.java	Thu Jun 29 11:10:30 2017 +0100
@@ -222,7 +222,8 @@
         jdk.naming.dns;
     exports sun.net.util to
         java.desktop,
-        jdk.jconsole;
+        jdk.jconsole,
+        jdk.incubator.httpclient;
     exports sun.net.www to
         java.desktop,
         jdk.incubator.httpclient,
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Thu Jun 29 11:10:30 2017 +0100
@@ -46,11 +46,13 @@
 
     final AsyncSSLDelegate sslDelegate;
     final PlainHttpConnection plainConnection;
+    final String serverName;
 
     AsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
         super(addr, client);
         plainConnection = new PlainHttpConnection(addr, client);
-        sslDelegate = new AsyncSSLDelegate(plainConnection, client, ap);
+        serverName = Utils.getServerName(addr);
+        sslDelegate = new AsyncSSLDelegate(plainConnection, client, ap, serverName);
     }
 
     @Override
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java	Thu Jun 29 11:10:30 2017 +0100
@@ -108,6 +108,7 @@
     final SSLParameters sslParameters;
     final HttpConnection lowerOutput;
     final HttpClientImpl client;
+    final String serverName;
     // should be volatile to provide proper synchronization(visibility) action
     volatile Consumer<ByteBufferReference> asyncReceiver;
     volatile Consumer<Throwable> errorHandler;
@@ -121,9 +122,10 @@
 
     // alpn[] may be null. upcall is callback which receives incoming decoded bytes off socket
 
-    AsyncSSLDelegate(HttpConnection lowerOutput, HttpClientImpl client, String[] alpn)
+    AsyncSSLDelegate(HttpConnection lowerOutput, HttpClientImpl client, String[] alpn, String sname)
     {
         SSLContext context = client.sslContext();
+        this.serverName = sname;
         engine = context.createSSLEngine();
         engine.setUseClientMode(true);
         SSLParameters sslp = client.sslParameters()
@@ -135,6 +137,10 @@
         } else {
             Log.logSSL("AsyncSSLDelegate: no applications set!");
         }
+        if (serverName != null) {
+            SNIHostName sn = new SNIHostName(serverName);
+            sslParameters.setServerNames(List.of(sn));
+        }
         logParams(sslParameters);
         engine.setSSLParameters(sslParameters);
         this.lowerOutput = lowerOutput;
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Thu Jun 29 11:10:30 2017 +0100
@@ -58,7 +58,8 @@
      * Creates an HttpRequestImpl from the given builder.
      */
     public HttpRequestImpl(HttpRequestBuilderImpl builder) {
-        this.method = builder.method();
+        String method = builder.method();
+        this.method = method == null? "GET" : method;
         this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
         this.systemHeaders = new HttpHeadersImpl();
         this.uri = builder.uri();
@@ -77,7 +78,8 @@
      * Creates an HttpRequestImpl from the given request.
      */
     public HttpRequestImpl(HttpRequest request) {
-        this.method = request.method();
+        String method = request.method();
+        this.method = method == null? "GET" : method;
         this.userHeaders = request.headers();
         if (request instanceof HttpRequestImpl) {
             this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java	Thu Jun 29 11:10:30 2017 +0100
@@ -154,14 +154,12 @@
             String k = key.toLowerCase(Locale.US);
             cookedHeaders.merge(k, newValues,
                     (v1, v2) -> {
-                        if (v1 == null) {
-                            ArrayList<String> newV = new ArrayList<>();
-                            newV.addAll(v2);
-                            return newV;
-                        } else {
-                            v1.addAll(v2);
-                            return v1;
+                        ArrayList<String> newV = new ArrayList<>();
+                        if (v1 != null) {
+                            newV.addAll(v1);
                         }
+                        newV.addAll(v2);
+                        return newV;
                     });
         }
         return cookedHeaders;
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java	Thu Jun 29 11:10:30 2017 +0100
@@ -46,13 +46,14 @@
     PlainHttpConnection delegate;
     SSLDelegate sslDelegate;
     final String[] alpn;
+    final String serverName;
 
     @Override
     public CompletableFuture<Void> connectAsync() {
         return delegate.connectAsync()
                 .thenCompose((Void v) ->
                                 MinimalFuture.supply( () -> {
-                                    this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn);
+                                    this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
                                     return null;
                                 }));
     }
@@ -60,12 +61,13 @@
     @Override
     public void connect() throws IOException {
         delegate.connect();
-        this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn);
+        this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
     }
 
     SSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
         super(addr, client);
         this.alpn = ap;
+        this.serverName = Utils.getServerName(addr);
         delegate = new PlainHttpConnection(addr, client);
     }
 
@@ -77,8 +79,9 @@
         super(c.address, c.client);
         this.delegate = c.plainConnection;
         AsyncSSLDelegate adel = c.sslDelegate;
-        this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client);
+        this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
         this.alpn = adel.alpn;
+        this.serverName = adel.serverName;
     }
 
     @Override
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java	Thu Jun 29 11:10:30 2017 +0100
@@ -29,6 +29,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import javax.net.ssl.SSLEngineResult.HandshakeStatus;
@@ -50,26 +51,33 @@
     final SSLParameters sslParameters;
     final SocketChannel chan;
     final HttpClientImpl client;
+    final String serverName;
 
-    SSLDelegate(SSLEngine eng, SocketChannel chan, HttpClientImpl client)
+    SSLDelegate(SSLEngine eng, SocketChannel chan, HttpClientImpl client, String sn)
     {
         this.engine = eng;
         this.chan = chan;
         this.client = client;
         this.wrapper = new EngineWrapper(chan, engine);
         this.sslParameters = engine.getSSLParameters();
+        this.serverName = sn;
     }
 
     // alpn[] may be null
-    SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn)
+    SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
         throws IOException
     {
+        serverName = sn;
         SSLContext context = client.sslContext();
         engine = context.createSSLEngine();
         engine.setUseClientMode(true);
         SSLParameters sslp = client.sslParameters()
                                    .orElseGet(context::getSupportedSSLParameters);
         sslParameters = Utils.copySSLParameters(sslp);
+        if (sn != null) {
+            SNIHostName sni = new SNIHostName(sn);
+            sslParameters.setServerNames(List.of(sni));
+        }
         if (alpn != null) {
             sslParameters.setApplicationProtocols(alpn);
             Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java	Thu Jun 29 11:10:30 2017 +0100
@@ -46,11 +46,12 @@
     final PlainTunnelingConnection delegate;
     protected SSLDelegate sslDelegate;
     private volatile boolean connected;
+    final String serverName;
 
     @Override
     public void connect() throws IOException, InterruptedException {
         delegate.connect();
-        this.sslDelegate = new SSLDelegate(delegate.channel(), client, null);
+        this.sslDelegate = new SSLDelegate(delegate.channel(), client, null, serverName);
         connected = true;
     }
 
@@ -67,7 +68,7 @@
                     // can this block?
                     this.sslDelegate = new SSLDelegate(delegate.channel(),
                                                        client,
-                                                       null);
+                                                       null, serverName);
                     connected = true;
                 } catch (IOException e) {
                     throw new UncheckedIOException(e);
@@ -80,6 +81,7 @@
                         InetSocketAddress proxy)
     {
         super(addr, client);
+        this.serverName = Utils.getServerName(addr);
         delegate = new PlainTunnelingConnection(addr, proxy, client);
     }
 
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Thu Jun 29 11:10:30 2017 +0100
@@ -27,6 +27,7 @@
 
 import jdk.internal.misc.InnocuousThread;
 import sun.net.NetProperties;
+import sun.net.util.IPAddressUtil;
 
 import javax.net.ssl.SSLParameters;
 import java.io.ByteArrayOutputStream;
@@ -35,6 +36,7 @@
 import java.io.UncheckedIOException;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
 import java.net.NetPermission;
 import java.net.URI;
 import java.net.URLPermission;
@@ -164,6 +166,22 @@
         return !token.isEmpty();
     }
 
+    /**
+     * If the address was created with a domain name, then return
+     * the domain name string. If created with a literal IP address
+     * then return null. We do this to avoid doing a reverse lookup
+     * Used to populate the TLS SNI parameter. So, SNI is only set
+     * when a domain name was supplied.
+     */
+    public static String getServerName(InetSocketAddress addr) {
+        String host = addr.getHostString();
+        if (IPAddressUtil.textToNumericFormatV4(host) != null)
+            return null;
+        if (IPAddressUtil.textToNumericFormatV6(host) != null)
+            return null;
+        return host;
+    }
+
     /*
      * Validates a RFC 7230 field-value.
      *
--- a/jdk/test/java/net/httpclient/http2/TLSConnection.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/test/java/net/httpclient/http2/TLSConnection.java	Thu Jun 29 11:10:30 2017 +0100
@@ -65,7 +65,7 @@
 
         Handler handler = new Handler();
 
-        try (Http2TestServer server = new Http2TestServer(true, 0)) {
+        try (Http2TestServer server = new Http2TestServer("127.0.0.1", true, 0)) {
             server.addHandler(handler, "/");
             server.start();
 
--- a/jdk/test/java/net/httpclient/http2/server/Http2TestServer.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/test/java/net/httpclient/http2/server/Http2TestServer.java	Thu Jun 29 11:10:30 2017 +0100
@@ -33,6 +33,7 @@
 import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLServerSocket;
 import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SNIServerName;
 
 /**
  * Waits for incoming TCP connections from a client and establishes
@@ -48,6 +49,7 @@
     volatile boolean stopping = false;
     final Map<String,Http2Handler> handlers;
     final SSLContext sslContext;
+    final String serverName;
     final HashMap<InetSocketAddress,Http2TestServerConnection> connections;
 
     private static ThreadFactory defaultThreadFac =
@@ -62,8 +64,12 @@
         return Executors.newCachedThreadPool(defaultThreadFac);
     }
 
+    public Http2TestServer(String serverName, boolean secure, int port) throws Exception {
+        this(serverName, secure, port, getDefaultExecutor(), null);
+    }
+
     public Http2TestServer(boolean secure, int port) throws Exception {
-        this(secure, port, getDefaultExecutor(), null);
+        this(null, secure, port, getDefaultExecutor(), null);
     }
 
     public InetSocketAddress getAddress() {
@@ -72,7 +78,19 @@
 
     public Http2TestServer(boolean secure,
                            SSLContext context) throws Exception {
-        this(secure, 0, null, context);
+        this(null, secure, 0, null, context);
+    }
+
+    public Http2TestServer(String serverName, boolean secure,
+                           SSLContext context) throws Exception {
+        this(serverName, secure, 0, null, context);
+    }
+
+    public Http2TestServer(boolean secure,
+                           int port,
+                           ExecutorService exec,
+                           SSLContext context) throws Exception {
+        this(null, secure, port, exec, context);
     }
 
     /**
@@ -80,17 +98,20 @@
      * to know in advance whether incoming connections are plain TCP "h2c"
      * or TLS "h2"/
      *
+     * @param serverName SNI servername
      * @param secure https or http
      * @param port listen port
      * @param exec executor service (cached thread pool is used if null)
      * @param context the SSLContext used when secure is true
      */
-    public Http2TestServer(boolean secure,
+    public Http2TestServer(String serverName,
+                           boolean secure,
                            int port,
                            ExecutorService exec,
                            SSLContext context)
         throws Exception
     {
+        this.serverName = serverName;
         if (secure) {
             server = initSecure(port);
         } else {
@@ -165,6 +186,10 @@
         return se;
     }
 
+    public String serverName() {
+        return serverName;
+    }
+
     /**
      * Starts a thread which waits for incoming connections.
      */
--- a/jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java	Wed Jun 28 17:57:20 2017 -0700
+++ b/jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java	Thu Jun 29 11:10:30 2017 +0100
@@ -25,12 +25,13 @@
 import java.io.BufferedOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.Socket;
 import java.net.URI;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
+import java.net.InetAddress;
+import javax.net.ssl.*;
 import java.net.URISyntaxException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
@@ -79,6 +80,9 @@
     final static byte[] clientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes();
 
     Http2TestServerConnection(Http2TestServer server, Socket socket) throws IOException {
+        if (socket instanceof SSLSocket) {
+            handshake(server.serverName(), (SSLSocket)socket);
+        }
         System.err.println("TestServer: New connection from " + socket);
         this.server = server;
         this.streams = Collections.synchronizedMap(new HashMap<>());
@@ -92,6 +96,42 @@
         os = new BufferedOutputStream(socket.getOutputStream());
     }
 
+    private static boolean compareIPAddrs(InetAddress addr1, String host) {
+        try {
+            InetAddress addr2 = InetAddress.getByName(host);
+            return addr1.equals(addr2);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static void handshake(String name, SSLSocket sock) throws IOException {
+        if (name == null) {
+            // no name set. No need to check
+            return;
+        } else if (name.equals("127.0.0.1")) {
+            name = "localhost";
+        }
+        final String fname = name;
+        final InetAddress addr1 = InetAddress.getByName(name);
+        SSLParameters params = sock.getSSLParameters();
+        SNIMatcher matcher = new SNIMatcher(StandardConstants.SNI_HOST_NAME) {
+            public boolean matches (SNIServerName n) {
+                String host = ((SNIHostName)n).getAsciiName();
+                if (host.equals("127.0.0.1"))
+                    host = "localhost";
+                boolean cmp = host.equalsIgnoreCase(fname);
+                if (cmp)
+                    return true;
+                return compareIPAddrs(addr1, host);
+            }
+        };
+        List<SNIMatcher> list = List.of(matcher);
+        params.setSNIMatchers(list);
+        sock.setSSLParameters(params);
+        sock.getSession(); // blocks until handshake done
+    }
+
     void close() {
         streams.forEach((i, q) -> {
             q.close();