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 }