changeset 54079:57e3fa3574ec

8236859: WebSocket over authenticating proxy fails with NPE Reviewed-by: phh Contributed-by: ilarion@azul.com
author yan
date Fri, 09 Apr 2021 16:01:48 +0300
parents 11dcad575f79
children b3cee5c1366d
files src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java src/java.net.http/share/classes/jdk/internal/net/http/RawChannelTube.java src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java src/java.net.http/share/classes/jdk/internal/net/http/websocket/RawChannel.java test/jdk/java/net/httpclient/websocket/DummySecureWebSocketServer.java test/jdk/java/net/httpclient/websocket/SecureSupport.java test/jdk/java/net/httpclient/websocket/WebSocketProxyTest.java
diffstat 12 files changed, 931 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -240,19 +240,22 @@
         HttpHeaders hdrs = r.headers();
         HttpRequestImpl req = r.request();
 
-        if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) {
-            // check if any authentication succeeded for first time
-            if (exchange.serverauth != null && !exchange.serverauth.fromcache) {
-                AuthInfo au = exchange.serverauth;
-                cache.store(au.scheme, req.uri(), false, au.credentials);
-            }
+        if (status != PROXY_UNAUTHORIZED){
             if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) {
                 AuthInfo au = exchange.proxyauth;
                 URI proxyURI = getProxyURI(req);
                 if (proxyURI != null) {
+                    exchange.proxyauth = null;
                     cache.store(au.scheme, proxyURI, true, au.credentials);
                 }
             }
+            if (status != UNAUTHORIZED) {
+            // check if any authentication succeeded for first time
+                if (exchange.serverauth != null && !exchange.serverauth.fromcache) {
+                    AuthInfo au = exchange.serverauth;
+                    cache.store(au.scheme, req.uri(), false, au.credentials);
+                }
+            }
             return null;
         }
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -142,6 +142,7 @@
         HttpConnection c = secure ? findConnection(key, sslPool)
                                   : findConnection(key, plainPool);
         //System.out.println ("getConnection returning: " + c);
+        assert c == null || c.isSecure() == secure;
         return c;
     }
 
@@ -155,6 +156,10 @@
     // Called also by whitebox tests
     void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
 
+        assert (conn instanceof PlainHttpConnection) || conn.isSecure()
+            : "Attempting to return unsecure connection to SSL pool: "
+                + conn.getClass();
+
         // Don't call registerCleanupTrigger while holding a lock,
         // but register it before the connection is added to the pool,
         // since we don't want to trigger the cleanup if the connection
@@ -450,7 +455,7 @@
         if (c instanceof PlainHttpConnection) {
             removeFromPool(c, plainPool);
         } else {
-            assert c.isSecure();
+            assert c.isSecure() : "connection " + c + " is not secure!";
             removeFromPool(c, sslPool);
         }
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -263,7 +263,7 @@
             connection.close();
             return MinimalFuture.completedFuture(null); // not treating as error
         } else {
-            return readBody(discarding(), true, executor);
+            return readBody(discarding(), !request.isWebSocket(), executor);
         }
     }
 
@@ -387,6 +387,14 @@
     public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
                                          boolean return2Cache,
                                          Executor executor) {
+        if (debug.on()) {
+            debug.log("readBody: return2Cache: " + return2Cache);
+            if (request.isWebSocket() && return2Cache && connection != null) {
+                debug.log("websocket connection will be returned to cache: "
+                        + connection.getClass() + "/" + connection );
+            }
+        }
+        assert !return2Cache || !request.isWebSocket();
         this.return2Cache = return2Cache;
         final Http1BodySubscriber<U> subscriber = new Http1BodySubscriber<>(p);
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -160,6 +160,26 @@
         return rawchan;
     }
 
+    /**
+     * Closes the RawChannel that may have been used for WebSocket protocol.
+     *
+     * @apiNote This method should be called to close the connection
+     * if an exception occurs during the websocket handshake, in cases where
+     * {@link #rawChannel() rawChannel().close()} would have been called.
+     * An unsuccessful handshake may prevent the creation of the RawChannel:
+     * if a RawChannel has already been created, this method wil close it.
+     * Otherwise, it will close the connection.
+     *
+     * @throws IOException if an I/O exception occurs while closing
+     *         the channel.
+     */
+    public synchronized void closeRawChannel() throws IOException {
+        //  close the rawChannel, if created, or the
+        // connection, if not.
+        if (rawchan != null) rawchan.close();
+        else connection.close();
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -363,6 +363,10 @@
                             this.response =
                                 new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
                             Exchange<T> oldExch = exch;
+                            if (currentreq.isWebSocket()) {
+                                // need to close the connection and open a new one.
+                                exch.exchImpl.connection().close();
+                            }
                             return exch.ignoreBody().handle((r,t) -> {
                                 previousreq = currentreq;
                                 currentreq = newrequest;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/RawChannelTube.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/RawChannelTube.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -73,7 +73,7 @@
         this.initial = initial;
         this.writePublisher = new WritePublisher();
         this.readSubscriber = new ReadSubscriber();
-        dbgTag = "[WebSocket] RawChannelTube(" + tube.toString() +")";
+        dbgTag = "[WebSocket] RawChannelTube(" + tube +")";
         debug = Utils.getWebSocketLogger(dbgTag::toString, Utils.DEBUG_WS);
         connection.client().webSocketOpen();
         connectFlows();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -41,7 +41,7 @@
 import javax.net.ssl.SSLParameters;
 
 /**
- * -Djava.net.HttpClient.log=
+ * -Djdk.httpclient.HttpClient.log=
  *          errors,requests,headers,
  *          frames[:control:data:window:all..],content,ssl,trace,channel
  *
--- a/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -215,19 +215,26 @@
         //
         // See https://tools.ietf.org/html/rfc6455#section-7.4.1
         Result result = null;
-        Exception exception = null;
+        Throwable exception = null;
         try {
             result = handleResponse(response);
         } catch (IOException e) {
             exception = e;
         } catch (Exception e) {
             exception = new WebSocketHandshakeException(response).initCause(e);
+        } catch (Error e) {
+            // We should attempt to close the connection and relay
+            // the error through the completable future even in this
+            // case.
+            exception = e;
         }
         if (exception == null) {
             return MinimalFuture.completedFuture(result);
         }
         try {
-            ((RawChannel.Provider) response).rawChannel().close();
+            // calling this method will close the rawChannel, if created,
+            // or the connection, if not.
+            ((RawChannel.Provider) response).closeRawChannel();
         } catch (IOException e) {
             exception.addSuppressed(e);
         }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/websocket/RawChannel.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/RawChannel.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -40,6 +40,7 @@
     interface Provider {
 
         RawChannel rawChannel() throws IOException;
+        void closeRawChannel() throws IOException;
     }
 
     interface RawEvent {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/websocket/DummySecureWebSocketServer.java	Fri Apr 09 16:01:48 2021 +0300
@@ -0,0 +1,610 @@
+/*
+ * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.net.SocketAddress;
+import java.net.SocketOption;
+import java.net.StandardSocketOptions;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ClosedByInterruptException;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.CharacterCodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+import static java.lang.System.err;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Dummy WebSocket Server, which supports TLS.
+ * By default the dummy webserver uses a plain TCP connection,
+ * but it can use a TLS connection if secure() is called before
+ * open(). It will use the default SSL context.
+ *
+ * Performs simpler version of the WebSocket Opening Handshake over HTTP (i.e.
+ * no proxying, cookies, etc.) Supports sequential connections, one at a time,
+ * i.e. in order for a client to connect to the server the previous client must
+ * disconnect first.
+ *
+ * Expected client request:
+ *
+ *     GET /chat HTTP/1.1
+ *     Host: server.example.com
+ *     Upgrade: websocket
+ *     Connection: Upgrade
+ *     Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
+ *     Origin: http://example.com
+ *     Sec-WebSocket-Protocol: chat, superchat
+ *     Sec-WebSocket-Version: 13
+ *
+ * This server response:
+ *
+ *     HTTP/1.1 101 Switching Protocols
+ *     Upgrade: websocket
+ *     Connection: Upgrade
+ *     Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+ *     Sec-WebSocket-Protocol: chat
+ */
+public class DummySecureWebSocketServer implements Closeable {
+
+    /**
+     * Emulates some of the SocketChannel APIs over a Socket
+     * instance.
+     */
+    public static class WebSocketChannel implements AutoCloseable {
+        interface Reader {
+            int read(ByteBuffer buf) throws IOException;
+        }
+        interface Writer {
+            void write(ByteBuffer buf) throws IOException;
+        }
+        interface Config {
+            <T> void setOption(SocketOption<T> option, T value) throws IOException;
+        }
+        interface Closer {
+            void close() throws IOException;
+        }
+        final AutoCloseable channel;
+        final Reader reader;
+        final Writer writer;
+        final Config config;
+        final Closer closer;
+        WebSocketChannel(AutoCloseable channel, Reader reader, Writer writer, Config config, Closer closer) {
+            this.channel = channel;
+            this.reader = reader;
+            this.writer = writer;
+            this.config = config;
+            this.closer = closer;
+        }
+        public void close() throws IOException {
+            closer.close();
+        }
+        public String toString() {
+            return channel.toString();
+        }
+        public int read(ByteBuffer bb) throws IOException {
+            return reader.read(bb);
+        }
+        public void write(ByteBuffer bb) throws IOException {
+            writer.write(bb);
+        }
+        public <T> void setOption(SocketOption<T> option, T value) throws IOException {
+            config.setOption(option, value);
+        }
+        public static WebSocketChannel of(Socket s) {
+            Reader reader = (bb) -> DummySecureWebSocketServer.read(s.getInputStream(), bb);
+            Writer writer = (bb) -> DummySecureWebSocketServer.write(s.getOutputStream(), bb);
+            return new WebSocketChannel(s, reader, writer, s::setOption, s::close);
+        }
+    }
+
+    /**
+     * Emulates some of the ServerSocketChannel APIs over a ServerSocket
+     * instance.
+     */
+    public static class WebServerSocketChannel implements AutoCloseable {
+        interface Accepter {
+            WebSocketChannel accept() throws IOException;
+        }
+        interface Binder {
+            void bind(SocketAddress address) throws IOException;
+        }
+        interface Config {
+            <T> void setOption(SocketOption<T> option, T value) throws IOException;
+        }
+        interface Closer {
+            void close() throws IOException;
+        }
+        interface Addressable {
+            SocketAddress getLocalAddress() throws IOException;
+        }
+        final AutoCloseable server;
+        final Accepter accepter;
+        final Binder binder;
+        final Addressable address;
+        final Config config;
+        final Closer closer;
+        WebServerSocketChannel(AutoCloseable server,
+                               Accepter accepter,
+                               Binder binder,
+                               Addressable address,
+                               Config config,
+                               Closer closer) {
+            this.server = server;
+            this.accepter = accepter;
+            this.binder = binder;
+            this.address = address;
+            this.config = config;
+            this.closer = closer;
+        }
+        public void close() throws IOException {
+            closer.close();
+        }
+        public String toString() {
+            return server.toString();
+        }
+        public WebSocketChannel accept() throws IOException {
+            return accepter.accept();
+        }
+        public void bind(SocketAddress address) throws IOException {
+            binder.bind(address);
+        }
+        public <T> void setOption(SocketOption<T> option, T value) throws IOException {
+            config.setOption(option, value);
+        }
+        public SocketAddress getLocalAddress()  throws IOException {
+            return address.getLocalAddress();
+        }
+        public static WebServerSocketChannel of(ServerSocket ss) {
+            Accepter a = () -> WebSocketChannel.of(ss.accept());
+            return new WebServerSocketChannel(ss, a, ss::bind, ss::getLocalSocketAddress, ss::setOption, ss::close);
+        }
+    }
+
+    // Creates a secure WebServerSocketChannel
+    static WebServerSocketChannel openWSS() throws IOException {
+       return WebServerSocketChannel.of(SSLServerSocketFactory.getDefault().createServerSocket());
+    }
+
+    // Creates a plain WebServerSocketChannel
+    static WebServerSocketChannel openWS() throws IOException {
+        return WebServerSocketChannel.of(ServerSocketFactory.getDefault().createServerSocket());
+    }
+
+
+    static int read(InputStream str, ByteBuffer buffer) throws IOException {
+        int len = Math.min(buffer.remaining(), 1024);
+        if (len <= 0) return 0;
+        byte[] bytes = new byte[len];
+        int res = 0;
+        if (buffer.hasRemaining()) {
+            len = Math.min(len, buffer.remaining());
+            int n = str.read(bytes, 0, len);
+            if (n > 0) {
+                buffer.put(bytes, 0, n);
+                res += n;
+            } else if (res > 0) {
+                return res;
+            } else {
+                return n;
+            }
+        }
+        return res;
+    }
+
+    static void write(OutputStream str, ByteBuffer buffer) throws IOException {
+        int len = Math.min(buffer.remaining(), 1024);
+        if (len <= 0) return;
+        byte[] bytes = new byte[len];
+        int res = 0;
+        int pos = buffer.position();
+        while (buffer.hasRemaining()) {
+            len = Math.min(len, buffer.remaining());
+            buffer.get(bytes, 0, len);
+            str.write(bytes, 0, len);
+        }
+    }
+
+    private final AtomicBoolean started = new AtomicBoolean();
+    private final Thread thread;
+    private volatile WebServerSocketChannel ss;
+    private volatile InetSocketAddress address;
+    private volatile boolean secure;
+    private ByteBuffer read = ByteBuffer.allocate(16384);
+    private final CountDownLatch readReady = new CountDownLatch(1);
+    private volatile boolean done;
+
+    private static class Credentials {
+        private final String name;
+        private final String password;
+        private Credentials(String name, String password) {
+            this.name = name;
+            this.password = password;
+        }
+        public String name() { return name; }
+        public String password() { return password; }
+    }
+
+    public DummySecureWebSocketServer() {
+        this(defaultMapping(), null, null);
+    }
+
+    public DummySecureWebSocketServer(String username, String password) {
+        this(defaultMapping(), username, password);
+    }
+
+    public DummySecureWebSocketServer(BiFunction<List<String>,Credentials,List<String>> mapping,
+                                String username,
+                                String password) {
+        requireNonNull(mapping);
+        Credentials credentials = username != null ?
+                new Credentials(username, password) : null;
+
+        thread = new Thread(() -> {
+            try {
+                while (!Thread.currentThread().isInterrupted() && !done) {
+                    err.println("Accepting next connection at: " + ss);
+                    WebSocketChannel channel = ss.accept();
+                    err.println("Accepted: " + channel);
+                    try {
+                        channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
+                        while (!done) {
+                            StringBuilder request = new StringBuilder();
+                            if (!readRequest(channel, request)) {
+                                throw new IOException("Bad request:[" + request + "]");
+                            }
+                            List<String> strings = asList(request.toString().split("\r\n"));
+                            List<String> response = mapping.apply(strings, credentials);
+                            writeResponse(channel, response);
+
+                            if (response.get(0).startsWith("HTTP/1.1 401")) {
+                                err.println("Sent 401 Authentication response " + channel);
+                                continue;
+                            } else {
+                                serve(channel);
+                                break;
+                            }
+                        }
+                    } catch (IOException e) {
+                        if (!done) {
+                            err.println("Error in connection: " + channel + ", " + e);
+                        }
+                    } finally {
+                        err.println("Closed: " + channel);
+                        close(channel);
+                        readReady.countDown();
+                    }
+                }
+            } catch (ClosedByInterruptException ignored) {
+            } catch (Throwable e) {
+                if (!done) {
+                    e.printStackTrace(err);
+                }
+            } finally {
+                done = true;
+                close(ss);
+                err.println("Stopped at: " + getURI());
+            }
+        });
+        thread.setName("DummySecureWebSocketServer");
+        thread.setDaemon(false);
+    }
+
+    // must be called before open()
+    public DummySecureWebSocketServer secure() {
+        secure = true;
+        return this;
+    }
+
+    protected void read(WebSocketChannel ch) throws IOException {
+        // Read until the thread is interrupted or an error occurred
+        // or the input is shutdown
+        ByteBuffer b = ByteBuffer.allocate(65536);
+        while (ch.read(b) != -1) {
+            b.flip();
+            if (read.remaining() < b.remaining()) {
+                int required = read.capacity() - read.remaining() + b.remaining();
+                int log2required = 32 - Integer.numberOfLeadingZeros(required - 1);
+                ByteBuffer newBuffer = ByteBuffer.allocate(1 << log2required);
+                newBuffer.put(read.flip());
+                read = newBuffer;
+            }
+            read.put(b);
+            b.clear();
+        }
+    }
+
+    protected void write(WebSocketChannel ch) throws IOException { }
+
+    protected final void serve(WebSocketChannel channel)
+            throws InterruptedException
+    {
+        Thread reader = new Thread(() -> {
+            try {
+                read(channel);
+            } catch (IOException ignored) { }
+        });
+        Thread writer = new Thread(() -> {
+            try {
+                write(channel);
+            } catch (IOException ignored) { }
+        });
+        reader.start();
+        writer.start();
+        try {
+            while (!done) {
+                try {
+                    reader.join(500);
+                } catch (InterruptedException x) {
+                    if (done) {
+                        close(channel);
+                        break;
+                    }
+                }
+            }
+        } finally {
+            reader.interrupt();
+            try {
+                while (!done) {
+                    try {
+                        writer.join(500);
+                    } catch (InterruptedException x) {
+                        if (done) break;
+                    }
+                }
+            } finally {
+                writer.interrupt();
+            }
+        }
+    }
+
+    public ByteBuffer read() throws InterruptedException {
+        readReady.await();
+        return read.duplicate().asReadOnlyBuffer().flip();
+    }
+
+    public void open() throws IOException {
+        err.println("Starting");
+        if (!started.compareAndSet(false, true)) {
+            throw new IllegalStateException("Already started");
+        }
+        ss = secure ? openWSS() : openWS();
+        try {
+            ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+            address = (InetSocketAddress) ss.getLocalAddress();
+            thread.start();
+        } catch (IOException e) {
+            done = true;
+            close(ss);
+            throw e;
+        }
+        err.println("Started at: " + getURI());
+    }
+
+    @Override
+    public void close() {
+        err.println("Stopping: " + getURI());
+        done = true;
+        thread.interrupt();
+        close(ss);
+    }
+
+    URI getURI() {
+        if (!started.get()) {
+            throw new IllegalStateException("Not yet started");
+        }
+        if (!secure) {
+            return URI.create("ws://localhost:" + address.getPort());
+        } else {
+            return URI.create("wss://localhost:" + address.getPort());
+        }
+    }
+
+    private boolean readRequest(WebSocketChannel channel, StringBuilder request)
+            throws IOException
+    {
+        ByteBuffer buffer = ByteBuffer.allocate(512);
+        while (channel.read(buffer) != -1) {
+            // read the complete HTTP request headers, there should be no body
+            CharBuffer decoded;
+            buffer.flip();
+            try {
+                decoded = ISO_8859_1.newDecoder().decode(buffer);
+            } catch (CharacterCodingException e) {
+                throw new UncheckedIOException(e);
+            }
+            request.append(decoded);
+            if (Pattern.compile("\r\n\r\n").matcher(request).find())
+                return true;
+            buffer.clear();
+        }
+        return false;
+    }
+
+    private void writeResponse(WebSocketChannel channel, List<String> response)
+            throws IOException
+    {
+        String s = response.stream().collect(Collectors.joining("\r\n"))
+                + "\r\n\r\n";
+        ByteBuffer encoded;
+        try {
+            encoded = ISO_8859_1.newEncoder().encode(CharBuffer.wrap(s));
+        } catch (CharacterCodingException e) {
+            throw new UncheckedIOException(e);
+        }
+        while (encoded.hasRemaining()) {
+            channel.write(encoded);
+        }
+    }
+
+    private static BiFunction<List<String>,Credentials,List<String>> defaultMapping() {
+        return (request, credentials) -> {
+            List<String> response = new LinkedList<>();
+            Iterator<String> iterator = request.iterator();
+            if (!iterator.hasNext()) {
+                throw new IllegalStateException("The request is empty");
+            }
+            String statusLine = iterator.next();
+            if (!(statusLine.startsWith("GET /") && statusLine.endsWith(" HTTP/1.1"))) {
+                throw new IllegalStateException
+                        ("Unexpected status line: " + request.get(0));
+            }
+            response.add("HTTP/1.1 101 Switching Protocols");
+            Map<String, List<String>> requestHeaders = new HashMap<>();
+            while (iterator.hasNext()) {
+                String header = iterator.next();
+                String[] split = header.split(": ");
+                if (split.length != 2) {
+                    throw new IllegalStateException
+                            ("Unexpected header: " + header
+                                     + ", split=" + Arrays.toString(split));
+                }
+                requestHeaders.computeIfAbsent(split[0], k -> new ArrayList<>()).add(split[1]);
+
+            }
+            if (requestHeaders.containsKey("Sec-WebSocket-Protocol")) {
+                throw new IllegalStateException("Subprotocols are not expected");
+            }
+            if (requestHeaders.containsKey("Sec-WebSocket-Extensions")) {
+                throw new IllegalStateException("Extensions are not expected");
+            }
+            expectHeader(requestHeaders, "Connection", "Upgrade");
+            response.add("Connection: Upgrade");
+            expectHeader(requestHeaders, "Upgrade", "websocket");
+            response.add("Upgrade: websocket");
+            expectHeader(requestHeaders, "Sec-WebSocket-Version", "13");
+            List<String> key = requestHeaders.get("Sec-WebSocket-Key");
+            if (key == null || key.isEmpty()) {
+                throw new IllegalStateException("Sec-WebSocket-Key is missing");
+            }
+            if (key.size() != 1) {
+                throw new IllegalStateException("Sec-WebSocket-Key has too many values : " + key);
+            }
+            MessageDigest sha1 = null;
+            try {
+                sha1 = MessageDigest.getInstance("SHA-1");
+            } catch (NoSuchAlgorithmException e) {
+                throw new InternalError(e);
+            }
+            String x = key.get(0) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+            sha1.update(x.getBytes(ISO_8859_1));
+            String v = Base64.getEncoder().encodeToString(sha1.digest());
+            response.add("Sec-WebSocket-Accept: " + v);
+
+            // check authorization credentials, if required by the server
+            if (credentials != null && !authorized(credentials, requestHeaders)) {
+                response.clear();
+                response.add("HTTP/1.1 401 Unauthorized");
+                response.add("Content-Length: 0");
+                response.add("WWW-Authenticate: Basic realm=\"dummy server realm\"");
+            }
+
+            return response;
+        };
+    }
+
+    // Checks credentials in the request against those allowable by the server.
+    private static boolean authorized(Credentials credentials,
+                                      Map<String,List<String>> requestHeaders) {
+        List<String> authorization = requestHeaders.get("Authorization");
+        if (authorization == null)
+            return false;
+
+        if (authorization.size() != 1) {
+            throw new IllegalStateException("Authorization unexpected count:" + authorization);
+        }
+        String header = authorization.get(0);
+        if (!header.startsWith("Basic "))
+            throw new IllegalStateException("Authorization not Basic: " + header);
+
+        header = header.substring("Basic ".length());
+        String values = new String(Base64.getDecoder().decode(header), UTF_8);
+        int sep = values.indexOf(':');
+        if (sep < 1) {
+            throw new IllegalStateException("Authorization not colon: " +  values);
+        }
+        String name = values.substring(0, sep);
+        String password = values.substring(sep + 1);
+
+        if (name.equals(credentials.name()) && password.equals(credentials.password()))
+            return true;
+
+        return false;
+    }
+
+    protected static String expectHeader(Map<String, List<String>> headers,
+                                         String name,
+                                         String value) {
+        List<String> v = headers.get(name);
+        if (v == null) {
+            throw new IllegalStateException(
+                    format("Expected '%s' header, not present in %s",
+                           name, headers));
+        }
+        if (!v.contains(value)) {
+            throw new IllegalStateException(
+                    format("Expected '%s: %s', actual: '%s: %s'",
+                           name, value, name, v)
+            );
+        }
+        return value;
+    }
+
+    private static void close(AutoCloseable... acs) {
+        for (AutoCloseable ac : acs) {
+            try {
+                ac.close();
+            } catch (Exception ignored) { }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/websocket/SecureSupport.java	Fri Apr 09 16:01:48 2021 +0300
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.testng.Assert.assertThrows;
+
+/**
+ * Helper class to create instances of DummySecureWebSocketServer which
+ * can support both plain and secure connections.
+ * The caller should invoke DummySecureWebSocketServer::secure before
+ * DummySecureWebSocketServer::open in order to enable secure connection.
+ * When secure, the DummySecureWebSocketServer currently only support using the
+ * default SSLEngine through the default SSLSocketServerFacrtory.
+ */
+public class SecureSupport {
+
+    private SecureSupport() { }
+
+    public static DummySecureWebSocketServer serverWithCannedData(int... data) {
+        return serverWithCannedDataAndAuthentication(null, null, data);
+    }
+
+    public static DummySecureWebSocketServer serverWithCannedDataAndAuthentication(
+            String username,
+            String password,
+            int... data)
+    {
+        byte[] copy = new byte[data.length];
+        for (int i = 0; i < data.length; i++) {
+            copy[i] = (byte) data[i];
+        }
+        return serverWithCannedDataAndAuthentication(username, password, copy);
+    }
+
+    public static DummySecureWebSocketServer serverWithCannedData(byte... data) {
+       return serverWithCannedDataAndAuthentication(null, null, data);
+    }
+
+    public static DummySecureWebSocketServer serverWithCannedDataAndAuthentication(
+            String username,
+            String password,
+            byte... data)
+    {
+        byte[] copy = Arrays.copyOf(data, data.length);
+        return new DummySecureWebSocketServer(username, password) {
+            @Override
+            protected void write(WebSocketChannel ch) throws IOException {
+                int off = 0; int n = 1; // 1 byte at a time
+                while (off + n < copy.length + n) {
+                    int len = Math.min(copy.length - off, n);
+                    ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
+                    off += len;
+                    ch.write(bytes);
+                }
+                super.write(ch);
+            }
+        };
+    }
+
+    /*
+     * This server does not read from the wire, allowing its client to fill up
+     * their send buffer. Used to test scenarios with outstanding send
+     * operations.
+     */
+    public static DummySecureWebSocketServer notReadingServer() {
+        return new DummySecureWebSocketServer() {
+            @Override
+            protected void read(WebSocketChannel ch) throws IOException {
+                try {
+                    Thread.sleep(Long.MAX_VALUE);
+                } catch (InterruptedException e) {
+                    throw new IOException(e);
+                }
+            }
+        };
+    }
+
+    public static DummySecureWebSocketServer writingServer(int... data) {
+        byte[] copy = new byte[data.length];
+        for (int i = 0; i < data.length; i++) {
+            copy[i] = (byte) data[i];
+        }
+        return new DummySecureWebSocketServer() {
+
+            @Override
+            protected void read(WebSocketChannel ch) throws IOException {
+                try {
+                    Thread.sleep(Long.MAX_VALUE);
+                } catch (InterruptedException e) {
+                    throw new IOException(e);
+                }
+            }
+
+            @Override
+            protected void write(WebSocketChannel ch) throws IOException {
+                int off = 0; int n = 1; // 1 byte at a time
+                while (off + n < copy.length + n) {
+                    int len = Math.min(copy.length - off, n);
+                    ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
+                    off += len;
+                    ch.write(bytes);
+                }
+                super.write(ch);
+            }
+        };
+
+    }
+
+    public static String stringWith2NBytes(int n) {
+        // -- Russian Alphabet (33 characters, 2 bytes per char) --
+        char[] abc = {
+                0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
+                0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
+                0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
+                0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
+                0x042F,
+        };
+        // repeat cyclically
+        StringBuilder sb = new StringBuilder(n);
+        for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
+            sb.append(abc[j]);
+        }
+        String s = sb.toString();
+        assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
+        return s;
+    }
+
+    public static String malformedString() {
+        return new String(new char[]{0xDC00, 0xD800});
+    }
+
+    public static String incompleteString() {
+        return new String(new char[]{0xD800});
+    }
+
+    public static String stringWithNBytes(int n) {
+        char[] chars = new char[n];
+        Arrays.fill(chars, 'A');
+        return new String(chars);
+    }
+}
--- a/test/jdk/java/net/httpclient/websocket/WebSocketProxyTest.java	Fri Apr 09 14:17:57 2021 +0200
+++ b/test/jdk/java/net/httpclient/websocket/WebSocketProxyTest.java	Fri Apr 09 16:01:48 2021 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,10 +23,15 @@
 
 /*
  * @test
- * @bug 8217429
+ * @bug 8217429 8236859
  * @summary WebSocket proxy tunneling tests
- * @compile DummyWebSocketServer.java ../ProxyServer.java
+ * @library /test/lib
+ * @compile SecureSupport.java DummySecureWebSocketServer.java ../ProxyServer.java
+ * @build jdk.test.lib.net.SimpleSSLContext WebSocketProxyTest
  * @run testng/othervm
+ *         -Djdk.internal.httpclient.debug=true
+ *         -Djdk.internal.httpclient.websocket.debug=true
+ *         -Djdk.httpclient.HttpClient.log=errors,requests,headers
  *         -Djdk.http.auth.tunneling.disabledSchemes=
  *         WebSocketProxyTest
  */
@@ -52,9 +57,14 @@
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
+
+import jdk.test.lib.net.SimpleSSLContext;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+
 import static java.net.http.HttpClient.newBuilder;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.testng.Assert.assertEquals;
@@ -66,6 +76,14 @@
     private static final String USERNAME = "wally";
     private static final String PASSWORD = "xyz987";
 
+    static {
+        try {
+            SSLContext.setDefault(new SimpleSSLContext().get());
+        } catch (IOException ex) {
+            throw new ExceptionInInitializerError(ex);
+        }
+    }
+
     static class WSAuthenticator extends Authenticator {
         @Override
         protected PasswordAuthentication getPasswordAuthentication() {
@@ -73,20 +91,34 @@
         }
     }
 
-    static final Function<int[],DummyWebSocketServer> SERVER_WITH_CANNED_DATA =
+    static final Function<int[],DummySecureWebSocketServer> SERVER_WITH_CANNED_DATA =
         new Function<>() {
-            @Override public DummyWebSocketServer apply(int[] data) {
-                return Support.serverWithCannedData(data); }
+            @Override public DummySecureWebSocketServer apply(int[] data) {
+                return SecureSupport.serverWithCannedData(data); }
             @Override public String toString() { return "SERVER_WITH_CANNED_DATA"; }
         };
 
-    static final Function<int[],DummyWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
+    static final Function<int[],DummySecureWebSocketServer> SSL_SERVER_WITH_CANNED_DATA =
+            new Function<>() {
+                @Override public DummySecureWebSocketServer apply(int[] data) {
+                    return SecureSupport.serverWithCannedData(data).secure(); }
+                @Override public String toString() { return "SSL_SERVER_WITH_CANNED_DATA"; }
+            };
+
+    static final Function<int[],DummySecureWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
         new Function<>() {
-            @Override public DummyWebSocketServer apply(int[] data) {
-                return Support.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
+            @Override public DummySecureWebSocketServer apply(int[] data) {
+                return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
             @Override public String toString() { return "AUTH_SERVER_WITH_CANNED_DATA"; }
         };
 
+    static final Function<int[],DummySecureWebSocketServer> AUTH_SSL_SVR_WITH_CANNED_DATA =
+            new Function<>() {
+                @Override public DummySecureWebSocketServer apply(int[] data) {
+                    return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data).secure(); }
+                @Override public String toString() { return "AUTH_SSL_SVR_WITH_CANNED_DATA"; }
+            };
+
     static final Supplier<ProxyServer> TUNNELING_PROXY_SERVER =
         new Supplier<>() {
             @Override public ProxyServer get() {
@@ -105,15 +137,20 @@
     @DataProvider(name = "servers")
     public Object[][] servers() {
         return new Object[][] {
-            { SERVER_WITH_CANNED_DATA,      TUNNELING_PROXY_SERVER      },
-            { SERVER_WITH_CANNED_DATA,      AUTH_TUNNELING_PROXY_SERVER },
-            { AUTH_SERVER_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER      },
+            { SERVER_WITH_CANNED_DATA,       TUNNELING_PROXY_SERVER      },
+            { SERVER_WITH_CANNED_DATA,       AUTH_TUNNELING_PROXY_SERVER },
+            { SSL_SERVER_WITH_CANNED_DATA,   TUNNELING_PROXY_SERVER      },
+            { SSL_SERVER_WITH_CANNED_DATA,   AUTH_TUNNELING_PROXY_SERVER },
+            { AUTH_SERVER_WITH_CANNED_DATA,  TUNNELING_PROXY_SERVER      },
+            { AUTH_SSL_SVR_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER      },
+            { AUTH_SERVER_WITH_CANNED_DATA,  AUTH_TUNNELING_PROXY_SERVER },
+            { AUTH_SSL_SVR_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
         };
     }
 
     @Test(dataProvider = "servers")
     public void simpleAggregatingBinaryMessages
-            (Function<int[],DummyWebSocketServer> serverSupplier,
+            (Function<int[],DummySecureWebSocketServer> serverSupplier,
              Supplier<ProxyServer> proxyServerSupplier)
         throws IOException
     {
@@ -134,6 +171,8 @@
             InetSocketAddress proxyAddress = new InetSocketAddress(
                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
             server.open();
+            System.out.println("Server: " + server.getURI());
+            System.out.println("Proxy: " + proxyAddress);
 
             WebSocket.Listener listener = new WebSocket.Listener() {
 
@@ -209,7 +248,7 @@
     @Test
     public void clientAuthenticate() throws IOException  {
         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
-             var server = new DummyWebSocketServer()){
+             var server = new DummySecureWebSocketServer()){
             server.open();
             InetSocketAddress proxyAddress = new InetSocketAddress(
                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
@@ -230,7 +269,7 @@
     @Test
     public void explicitAuthenticate() throws IOException  {
         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
-             var server = new DummyWebSocketServer()) {
+             var server = new DummySecureWebSocketServer()) {
             server.open();
             InetSocketAddress proxyAddress = new InetSocketAddress(
                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
@@ -248,12 +287,36 @@
     }
 
     /*
+     * Ensures authentication succeeds when an `Authorization` header is explicitly set.
+     */
+    @Test
+    public void explicitAuthenticate2() throws IOException  {
+        try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
+             var server = new DummySecureWebSocketServer(USERNAME, PASSWORD).secure()) {
+            server.open();
+            InetSocketAddress proxyAddress = new InetSocketAddress(
+                    InetAddress.getLoopbackAddress(), proxyServer.getPort());
+
+            String hv = "Basic " + Base64.getEncoder().encodeToString(
+                    (USERNAME + ":" + PASSWORD).getBytes(UTF_8));
+
+            var webSocket = newBuilder()
+                    .proxy(ProxySelector.of(proxyAddress)).build()
+                    .newWebSocketBuilder()
+                    .header("Proxy-Authorization", hv)
+                    .header("Authorization", hv)
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+        }
+    }
+
+    /*
      * Ensures authentication does not succeed when no authenticator is present.
      */
     @Test
     public void failNoAuthenticator() throws IOException  {
         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
-             var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
+             var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
             server.open();
             InetSocketAddress proxyAddress = new InetSocketAddress(
                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
@@ -281,7 +344,7 @@
     @Test
     public void failBadCredentials() throws IOException  {
         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
-             var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
+             var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
             server.open();
             InetSocketAddress proxyAddress = new InetSocketAddress(
                     InetAddress.getLoopbackAddress(), proxyServer.getPort());