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 }