1 /*
   2  * Copyright (c) 2019, 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
  27  * @summary WebSocket proxy tunneling tests
  28  * @compile DummyWebSocketServer.java ../ProxyServer.java
  29  * @run testng/othervm
  30  *         -Djdk.http.auth.tunneling.disabledSchemes=
  31  *         WebSocketProxyTest
  32  */
  33 
  34 import java.io.IOException;
  35 import java.io.UncheckedIOException;
  36 import java.net.Authenticator;
  37 import java.net.InetAddress;
  38 import java.net.InetSocketAddress;
  39 import java.net.PasswordAuthentication;
  40 import java.net.ProxySelector;
  41 import java.net.http.HttpResponse;
  42 import java.net.http.WebSocket;
  43 import java.net.http.WebSocketHandshakeException;
  44 import java.nio.ByteBuffer;
  45 import java.nio.charset.StandardCharsets;
  46 import java.util.ArrayList;
  47 import java.util.Base64;
  48 import java.util.List;
  49 import java.util.concurrent.CompletableFuture;
  50 import java.util.concurrent.CompletionException;
  51 import java.util.concurrent.CompletionStage;
  52 import java.util.function.Function;
  53 import java.util.function.Supplier;
  54 import java.util.stream.Collectors;
  55 import org.testng.annotations.BeforeMethod;
  56 import org.testng.annotations.DataProvider;
  57 import org.testng.annotations.Test;
  58 import static java.net.http.HttpClient.newBuilder;
  59 import static java.nio.charset.StandardCharsets.UTF_8;
  60 import static org.testng.Assert.assertEquals;
  61 import static org.testng.FileAssert.fail;
  62 
  63 public class WebSocketProxyTest {
  64 
  65     // Used to verify a proxy/websocket server requiring Authentication
  66     private static final String USERNAME = "wally";
  67     private static final String PASSWORD = "xyz987";
  68 
  69     static class WSAuthenticator extends Authenticator {
  70         @Override
  71         protected PasswordAuthentication getPasswordAuthentication() {
  72             return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
  73         }
  74     }
  75 
  76     static final Function<int[],DummyWebSocketServer> SERVER_WITH_CANNED_DATA =
  77         new Function<>() {
  78             @Override public DummyWebSocketServer apply(int[] data) {
  79                 return Support.serverWithCannedData(data); }
  80             @Override public String toString() { return "SERVER_WITH_CANNED_DATA"; }
  81         };
  82 
  83     static final Function<int[],DummyWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
  84         new Function<>() {
  85             @Override public DummyWebSocketServer apply(int[] data) {
  86                 return Support.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
  87             @Override public String toString() { return "AUTH_SERVER_WITH_CANNED_DATA"; }
  88         };
  89 
  90     static final Supplier<ProxyServer> TUNNELING_PROXY_SERVER =
  91         new Supplier<>() {
  92             @Override public ProxyServer get() {
  93                 try { return new ProxyServer(0, true);}
  94                 catch(IOException e) { throw new UncheckedIOException(e); } }
  95             @Override public String toString() { return "TUNNELING_PROXY_SERVER"; }
  96         };
  97     static final Supplier<ProxyServer> AUTH_TUNNELING_PROXY_SERVER =
  98         new Supplier<>() {
  99             @Override public ProxyServer get() {
 100                 try { return new ProxyServer(0, true, USERNAME, PASSWORD);}
 101                 catch(IOException e) { throw new UncheckedIOException(e); } }
 102             @Override public String toString() { return "AUTH_TUNNELING_PROXY_SERVER"; }
 103         };
 104 
 105     @DataProvider(name = "servers")
 106     public Object[][] servers() {
 107         return new Object[][] {
 108             { SERVER_WITH_CANNED_DATA,      TUNNELING_PROXY_SERVER      },
 109             { SERVER_WITH_CANNED_DATA,      AUTH_TUNNELING_PROXY_SERVER },
 110             { AUTH_SERVER_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER      },
 111         };
 112     }
 113 
 114     @Test(dataProvider = "servers")
 115     public void simpleAggregatingBinaryMessages
 116             (Function<int[],DummyWebSocketServer> serverSupplier,
 117              Supplier<ProxyServer> proxyServerSupplier)
 118         throws IOException
 119     {
 120         List<byte[]> expected = List.of("hello", "chegar")
 121                 .stream()
 122                 .map(s -> s.getBytes(StandardCharsets.US_ASCII))
 123                 .collect(Collectors.toList());
 124         int[] binary = new int[]{
 125                 0x82, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F,       // hello
 126                 0x82, 0x06, 0x63, 0x68, 0x65, 0x67, 0x61, 0x72, // chegar
 127                 0x88, 0x00                                      // <CLOSE>
 128         };
 129         CompletableFuture<List<byte[]>> actual = new CompletableFuture<>();
 130 
 131         try (var proxyServer = proxyServerSupplier.get();
 132              var server = serverSupplier.apply(binary)) {
 133 
 134             InetSocketAddress proxyAddress = new InetSocketAddress(
 135                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 136             server.open();
 137 
 138             WebSocket.Listener listener = new WebSocket.Listener() {
 139 
 140                 List<byte[]> collectedBytes = new ArrayList<>();
 141                 ByteBuffer buffer = ByteBuffer.allocate(1024);
 142 
 143                 @Override
 144                 public CompletionStage<?> onBinary(WebSocket webSocket,
 145                                                    ByteBuffer message,
 146                                                    boolean last) {
 147                     System.out.printf("onBinary(%s, %s)%n", message, last);
 148                     webSocket.request(1);
 149 
 150                     append(message);
 151                     if (last) {
 152                         buffer.flip();
 153                         byte[] bytes = new byte[buffer.remaining()];
 154                         buffer.get(bytes);
 155                         buffer.clear();
 156                         processWholeBinary(bytes);
 157                     }
 158                     return null;
 159                 }
 160 
 161                 private void append(ByteBuffer message) {
 162                     if (buffer.remaining() < message.remaining()) {
 163                         assert message.remaining() > 0;
 164                         int cap = (buffer.capacity() + message.remaining()) * 2;
 165                         ByteBuffer b = ByteBuffer.allocate(cap);
 166                         b.put(buffer.flip());
 167                         buffer = b;
 168                     }
 169                     buffer.put(message);
 170                 }
 171 
 172                 private void processWholeBinary(byte[] bytes) {
 173                     String stringBytes = new String(bytes, UTF_8);
 174                     System.out.println("processWholeBinary: " + stringBytes);
 175                     collectedBytes.add(bytes);
 176                 }
 177 
 178                 @Override
 179                 public CompletionStage<?> onClose(WebSocket webSocket,
 180                                                   int statusCode,
 181                                                   String reason) {
 182                     actual.complete(collectedBytes);
 183                     return null;
 184                 }
 185 
 186                 @Override
 187                 public void onError(WebSocket webSocket, Throwable error) {
 188                     actual.completeExceptionally(error);
 189                 }
 190             };
 191 
 192             var webSocket = newBuilder()
 193                     .proxy(ProxySelector.of(proxyAddress))
 194                     .authenticator(new WSAuthenticator())
 195                     .build().newWebSocketBuilder()
 196                     .buildAsync(server.getURI(), listener)
 197                     .join();
 198 
 199             List<byte[]> a = actual.join();
 200             assertEquals(a, expected);
 201         }
 202     }
 203 
 204     // -- authentication specific tests
 205 
 206     /*
 207      * Ensures authentication succeeds when an Authenticator set on client builder.
 208      */
 209     @Test
 210     public void clientAuthenticate() throws IOException  {
 211         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 212              var server = new DummyWebSocketServer()){
 213             server.open();
 214             InetSocketAddress proxyAddress = new InetSocketAddress(
 215                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 216 
 217             var webSocket = newBuilder()
 218                     .proxy(ProxySelector.of(proxyAddress))
 219                     .authenticator(new WSAuthenticator())
 220                     .build()
 221                     .newWebSocketBuilder()
 222                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
 223                     .join();
 224         }
 225     }
 226 
 227     /*
 228      * Ensures authentication succeeds when an `Authorization` header is explicitly set.
 229      */
 230     @Test
 231     public void explicitAuthenticate() throws IOException  {
 232         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 233              var server = new DummyWebSocketServer()) {
 234             server.open();
 235             InetSocketAddress proxyAddress = new InetSocketAddress(
 236                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 237 
 238             String hv = "Basic " + Base64.getEncoder().encodeToString(
 239                     (USERNAME + ":" + PASSWORD).getBytes(UTF_8));
 240 
 241             var webSocket = newBuilder()
 242                     .proxy(ProxySelector.of(proxyAddress)).build()
 243                     .newWebSocketBuilder()
 244                     .header("Proxy-Authorization", hv)
 245                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
 246                     .join();
 247         }
 248     }
 249 
 250     /*
 251      * Ensures authentication does not succeed when no authenticator is present.
 252      */
 253     @Test
 254     public void failNoAuthenticator() throws IOException  {
 255         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 256              var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
 257             server.open();
 258             InetSocketAddress proxyAddress = new InetSocketAddress(
 259                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 260 
 261             CompletableFuture<WebSocket> cf = newBuilder()
 262                     .proxy(ProxySelector.of(proxyAddress)).build()
 263                     .newWebSocketBuilder()
 264                     .buildAsync(server.getURI(), new WebSocket.Listener() { });
 265 
 266             try {
 267                 var webSocket = cf.join();
 268                 fail("Expected exception not thrown");
 269             } catch (CompletionException expected) {
 270                 WebSocketHandshakeException e = (WebSocketHandshakeException)expected.getCause();
 271                 HttpResponse<?> response = e.getResponse();
 272                 assertEquals(response.statusCode(), 407);
 273             }
 274         }
 275     }
 276 
 277     /*
 278      * Ensures authentication does not succeed when the authenticator presents
 279      * unauthorized credentials.
 280      */
 281     @Test
 282     public void failBadCredentials() throws IOException  {
 283         try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
 284              var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
 285             server.open();
 286             InetSocketAddress proxyAddress = new InetSocketAddress(
 287                     InetAddress.getLoopbackAddress(), proxyServer.getPort());
 288 
 289             Authenticator authenticator = new Authenticator() {
 290                 @Override protected PasswordAuthentication getPasswordAuthentication() {
 291                     return new PasswordAuthentication("BAD"+USERNAME, "".toCharArray());
 292                 }
 293             };
 294 
 295             CompletableFuture<WebSocket> cf = newBuilder()
 296                     .proxy(ProxySelector.of(proxyAddress))
 297                     .authenticator(authenticator)
 298                     .build()
 299                     .newWebSocketBuilder()
 300                     .buildAsync(server.getURI(), new WebSocket.Listener() { });
 301 
 302             try {
 303                 var webSocket = cf.join();
 304                 fail("Expected exception not thrown");
 305             } catch (CompletionException expected) {
 306                 System.out.println("caught expected exception:" + expected);
 307             }
 308         }
 309     }
 310 
 311     @BeforeMethod
 312     public void breakBetweenTests() {
 313         System.out.println("\n-------\n");
 314     }
 315 }