1 /*
   2  * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 8217429 8236859
  27  * @summary WebSocket proxy tunneling tests
  28  * @library /test/lib
  29  * @compile SecureSupport.java DummySecureWebSocketServer.java ../ProxyServer.java
  30  * @build jdk.test.lib.net.SimpleSSLContext WebSocketProxyTest
  31  * @run testng/othervm
  32  *         -Djdk.internal.httpclient.debug=true
  33  *         -Djdk.internal.httpclient.websocket.debug=true
  34  *         -Djdk.httpclient.HttpClient.log=errors,requests,headers
  35  *         -Djdk.http.auth.tunneling.disabledSchemes=
  36  *         WebSocketProxyTest
  37  */
  38 
  39 import java.io.IOException;
  40 import java.io.UncheckedIOException;
  41 import java.net.Authenticator;
  42 import java.net.InetAddress;
  43 import java.net.InetSocketAddress;
  44 import java.net.PasswordAuthentication;
  45 import java.net.ProxySelector;
  46 import java.net.http.HttpResponse;
  47 import java.net.http.WebSocket;
  48 import java.net.http.WebSocketHandshakeException;
  49 import java.nio.ByteBuffer;
  50 import java.nio.charset.StandardCharsets;
  51 import java.util.ArrayList;
  52 import java.util.Base64;
  53 import java.util.List;
  54 import java.util.concurrent.CompletableFuture;
  55 import java.util.concurrent.CompletionException;
  56 import java.util.concurrent.CompletionStage;
  57 import java.util.function.Function;
  58 import java.util.function.Supplier;
  59 import java.util.stream.Collectors;
  60 
  61 import jdk.test.lib.net.SimpleSSLContext;
  62 import org.testng.annotations.BeforeMethod;
  63 import org.testng.annotations.DataProvider;
  64 import org.testng.annotations.Test;
  65 
  66 import javax.net.ssl.SSLContext;
  67 
  68 import static java.net.http.HttpClient.newBuilder;
  69 import static java.nio.charset.StandardCharsets.UTF_8;
  70 import static org.testng.Assert.assertEquals;
  71 import static org.testng.FileAssert.fail;
  72 
  73 public class WebSocketProxyTest {
  74 
  75     // Used to verify a proxy/websocket server requiring Authentication
  76     private static final String USERNAME = "wally";
  77     private static final String PASSWORD = "xyz987";
  78 
  79     static {
  80         try {
  81             SSLContext.setDefault(new SimpleSSLContext().get());
  82         } catch (IOException ex) {
  83             throw new ExceptionInInitializerError(ex);
  84         }
  85     }
  86 
  87     static class WSAuthenticator extends Authenticator {
  88         @Override
  89         protected PasswordAuthentication getPasswordAuthentication() {
  90             return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
  91         }
  92     }
  93 
  94     static final Function<int[],DummySecureWebSocketServer> SERVER_WITH_CANNED_DATA =
  95         new Function<>() {
  96             @Override public DummySecureWebSocketServer apply(int[] data) {
  97                 return SecureSupport.serverWithCannedData(data); }
  98             @Override public String toString() { return "SERVER_WITH_CANNED_DATA"; }
  99         };
 100 
 101     static final Function<int[],DummySecureWebSocketServer> SSL_SERVER_WITH_CANNED_DATA =
 102             new Function<>() {
 103                 @Override public DummySecureWebSocketServer apply(int[] data) {
 104                     return SecureSupport.serverWithCannedData(data).secure(); }
 105                 @Override public String toString() { return "SSL_SERVER_WITH_CANNED_DATA"; }
 106             };
 107 
 108     static final Function<int[],DummySecureWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
 109         new Function<>() {
 110             @Override public DummySecureWebSocketServer apply(int[] data) {
 111                 return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
 112             @Override public String toString() { return "AUTH_SERVER_WITH_CANNED_DATA"; }
 113         };
 114 
 115     static final Function<int[],DummySecureWebSocketServer> AUTH_SSL_SVR_WITH_CANNED_DATA =
 116             new Function<>() {
 117                 @Override public DummySecureWebSocketServer apply(int[] data) {
 118                     return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data).secure(); }
 119                 @Override public String toString() { return "AUTH_SSL_SVR_WITH_CANNED_DATA"; }
 120             };
 121 
 122     static final Supplier<ProxyServer> TUNNELING_PROXY_SERVER =
 123         new Supplier<>() {
 124             @Override public ProxyServer get() {
 125                 try { return new ProxyServer(0, true);}
 126                 catch(IOException e) { throw new UncheckedIOException(e); } }
 127             @Override public String toString() { return "TUNNELING_PROXY_SERVER"; }
 128         };
 129     static final Supplier<ProxyServer> AUTH_TUNNELING_PROXY_SERVER =
 130         new Supplier<>() {
 131             @Override public ProxyServer get() {
 132                 try { return new ProxyServer(0, true, USERNAME, PASSWORD);}
 133                 catch(IOException e) { throw new UncheckedIOException(e); } }
 134             @Override public String toString() { return "AUTH_TUNNELING_PROXY_SERVER"; }
 135         };
 136 
 137     @DataProvider(name = "servers")
 138     public Object[][] servers() {
 139         return new Object[][] {
 140             { SERVER_WITH_CANNED_DATA,       TUNNELING_PROXY_SERVER      },
 141             { SERVER_WITH_CANNED_DATA,       AUTH_TUNNELING_PROXY_SERVER },
 142             { SSL_SERVER_WITH_CANNED_DATA,   TUNNELING_PROXY_SERVER      },
 143             { SSL_SERVER_WITH_CANNED_DATA,   AUTH_TUNNELING_PROXY_SERVER },
 144             { AUTH_SERVER_WITH_CANNED_DATA,  TUNNELING_PROXY_SERVER      },
 145             { AUTH_SSL_SVR_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER      },
 146             { AUTH_SERVER_WITH_CANNED_DATA,  AUTH_TUNNELING_PROXY_SERVER },
 147             { AUTH_SSL_SVR_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
 148         };
 149     }
 150 
 151     @Test(dataProvider = "servers")
 152     public void simpleAggregatingBinaryMessages
 153             (Function<int[],DummySecureWebSocketServer> serverSupplier,
 154              Supplier<ProxyServer> proxyServerSupplier)
 155         throws IOException
 156     {
 157         List<byte[]> expected = List.of("hello", "chegar")
 158                 .stream()
 159                 .map(s -> s.getBytes(StandardCharsets.US_ASCII))
 160                 .collect(Collectors.toList());
 161         int[] binary = new int[]{
 162                 0x82, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F,       // hello
 163                 0x82, 0x06, 0x63, 0x68, 0x65, 0x67, 0x61, 0x72, // chegar
 164                 0x88, 0x00                                      // <CLOSE>
 165         };
 166         CompletableFuture<List<byte[]>> actual = new CompletableFuture<>();
 167 
 168         try (var proxyServer = proxyServerSupplier.get();
 169              var server = serverSupplier.apply(binary)) {
 170 
 171             InetSocketAddress proxyAddress = new InetSocketAddress(
 172                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 173             server.open();
 174             System.out.println("Server: " + server.getURI());
 175             System.out.println("Proxy: " + proxyAddress);
 176 
 177             WebSocket.Listener listener = new WebSocket.Listener() {
 178 
 179                 List<byte[]> collectedBytes = new ArrayList<>();
 180                 ByteBuffer buffer = ByteBuffer.allocate(1024);
 181 
 182                 @Override
 183                 public CompletionStage<?> onBinary(WebSocket webSocket,
 184                                                    ByteBuffer message,
 185                                                    boolean last) {
 186                     System.out.printf("onBinary(%s, %s)%n", message, last);
 187                     webSocket.request(1);
 188 
 189                     append(message);
 190                     if (last) {
 191                         buffer.flip();
 192                         byte[] bytes = new byte[buffer.remaining()];
 193                         buffer.get(bytes);
 194                         buffer.clear();
 195                         processWholeBinary(bytes);
 196                     }
 197                     return null;
 198                 }
 199 
 200                 private void append(ByteBuffer message) {
 201                     if (buffer.remaining() < message.remaining()) {
 202                         assert message.remaining() > 0;
 203                         int cap = (buffer.capacity() + message.remaining()) * 2;
 204                         ByteBuffer b = ByteBuffer.allocate(cap);
 205                         b.put(buffer.flip());
 206                         buffer = b;
 207                     }
 208                     buffer.put(message);
 209                 }
 210 
 211                 private void processWholeBinary(byte[] bytes) {
 212                     String stringBytes = new String(bytes, UTF_8);
 213                     System.out.println("processWholeBinary: " + stringBytes);
 214                     collectedBytes.add(bytes);
 215                 }
 216 
 217                 @Override
 218                 public CompletionStage<?> onClose(WebSocket webSocket,
 219                                                   int statusCode,
 220                                                   String reason) {
 221                     actual.complete(collectedBytes);
 222                     return null;
 223                 }
 224 
 225                 @Override
 226                 public void onError(WebSocket webSocket, Throwable error) {
 227                     actual.completeExceptionally(error);
 228                 }
 229             };
 230 
 231             var webSocket = newBuilder()
 232                     .proxy(ProxySelector.of(proxyAddress))
 233                     .authenticator(new WSAuthenticator())
 234                     .build().newWebSocketBuilder()
 235                     .buildAsync(server.getURI(), listener)
 236                     .join();
 237 
 238             List<byte[]> a = actual.join();
 239             assertEquals(a, expected);
 240         }
 241     }
 242 
 243     // -- authentication specific tests
 244 
 245     /*
 246      * Ensures authentication succeeds when an Authenticator set on client builder.
 247      */
 248     @Test
 249     public void clientAuthenticate() throws IOException  {
 250         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 251              var server = new DummySecureWebSocketServer()){
 252             server.open();
 253             InetSocketAddress proxyAddress = new InetSocketAddress(
 254                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 255 
 256             var webSocket = newBuilder()
 257                     .proxy(ProxySelector.of(proxyAddress))
 258                     .authenticator(new WSAuthenticator())
 259                     .build()
 260                     .newWebSocketBuilder()
 261                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
 262                     .join();
 263         }
 264     }
 265 
 266     /*
 267      * Ensures authentication succeeds when an `Authorization` header is explicitly set.
 268      */
 269     @Test
 270     public void explicitAuthenticate() throws IOException  {
 271         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 272              var server = new DummySecureWebSocketServer()) {
 273             server.open();
 274             InetSocketAddress proxyAddress = new InetSocketAddress(
 275                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 276 
 277             String hv = "Basic " + Base64.getEncoder().encodeToString(
 278                     (USERNAME + ":" + PASSWORD).getBytes(UTF_8));
 279 
 280             var webSocket = newBuilder()
 281                     .proxy(ProxySelector.of(proxyAddress)).build()
 282                     .newWebSocketBuilder()
 283                     .header("Proxy-Authorization", hv)
 284                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
 285                     .join();
 286         }
 287     }
 288 
 289     /*
 290      * Ensures authentication succeeds when an `Authorization` header is explicitly set.
 291      */
 292     @Test
 293     public void explicitAuthenticate2() throws IOException  {
 294         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 295              var server = new DummySecureWebSocketServer(USERNAME, PASSWORD).secure()) {
 296             server.open();
 297             InetSocketAddress proxyAddress = new InetSocketAddress(
 298                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 299 
 300             String hv = "Basic " + Base64.getEncoder().encodeToString(
 301                     (USERNAME + ":" + PASSWORD).getBytes(UTF_8));
 302 
 303             var webSocket = newBuilder()
 304                     .proxy(ProxySelector.of(proxyAddress)).build()
 305                     .newWebSocketBuilder()
 306                     .header("Proxy-Authorization", hv)
 307                     .header("Authorization", hv)
 308                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
 309                     .join();
 310         }
 311     }
 312 
 313     /*
 314      * Ensures authentication does not succeed when no authenticator is present.
 315      */
 316     @Test
 317     public void failNoAuthenticator() throws IOException  {
 318         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 319              var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
 320             server.open();
 321             InetSocketAddress proxyAddress = new InetSocketAddress(
 322                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 323 
 324             CompletableFuture<WebSocket> cf = newBuilder()
 325                     .proxy(ProxySelector.of(proxyAddress)).build()
 326                     .newWebSocketBuilder()
 327                     .buildAsync(server.getURI(), new WebSocket.Listener() { });
 328 
 329             try {
 330                 var webSocket = cf.join();
 331                 fail("Expected exception not thrown");
 332             } catch (CompletionException expected) {
 333                 WebSocketHandshakeException e = (WebSocketHandshakeException)expected.getCause();
 334                 HttpResponse<?> response = e.getResponse();
 335                 assertEquals(response.statusCode(), 407);
 336             }
 337         }
 338     }
 339 
 340     /*
 341      * Ensures authentication does not succeed when the authenticator presents
 342      * unauthorized credentials.
 343      */
 344     @Test
 345     public void failBadCredentials() throws IOException  {
 346         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 347              var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
 348             server.open();
 349             InetSocketAddress proxyAddress = new InetSocketAddress(
 350                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 351 
 352             Authenticator authenticator = new Authenticator() {
 353                 @Override protected PasswordAuthentication getPasswordAuthentication() {
 354                     return new PasswordAuthentication("BAD"+USERNAME, "".toCharArray());
 355                 }
 356             };
 357 
 358             CompletableFuture<WebSocket> cf = newBuilder()
 359                     .proxy(ProxySelector.of(proxyAddress))
 360                     .authenticator(authenticator)
 361                     .build()
 362                     .newWebSocketBuilder()
 363                     .buildAsync(server.getURI(), new WebSocket.Listener() { });
 364 
 365             try {
 366                 var webSocket = cf.join();
 367                 fail("Expected exception not thrown");
 368             } catch (CompletionException expected) {
 369                 System.out.println("caught expected exception:" + expected);
 370             }
 371         }
 372     }
 373 
 374     @BeforeMethod
 375     public void breakBetweenTests() {
 376         System.out.println("\n-------\n");
 377     }
 378 }