1 /* 2 * Copyright (c) 2017, 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 * @summary Basic security checks for WebSocket URI from the Builder 27 * @compile ../DummyWebSocketServer.java ../../ProxyServer.java 28 * @run testng/othervm/java.security.policy=httpclient.policy WSURLPermissionTest 29 */ 30 31 import java.io.IOException; 32 import java.net.InetSocketAddress; 33 import java.net.Proxy; 34 import java.net.ProxySelector; 35 import java.net.SocketAddress; 36 import java.net.URI; 37 import java.net.URLPermission; 38 import java.security.AccessControlContext; 39 import java.security.AccessController; 40 import java.security.Permission; 41 import java.security.Permissions; 42 import java.security.PrivilegedActionException; 43 import java.security.PrivilegedExceptionAction; 44 import java.security.ProtectionDomain; 45 import java.util.List; 46 import java.util.concurrent.ExecutionException; 47 import jdk.incubator.http.HttpClient; 48 import jdk.incubator.http.WebSocket; 49 import org.testng.annotations.AfterTest; 50 import org.testng.annotations.BeforeTest; 51 import org.testng.annotations.DataProvider; 52 import org.testng.annotations.Test; 53 import static org.testng.Assert.*; 54 55 public class WSURLPermissionTest { 56 57 static AccessControlContext withPermissions(Permission... perms) { 58 Permissions p = new Permissions(); 59 for (Permission perm : perms) { 60 p.add(perm); 61 } 62 ProtectionDomain pd = new ProtectionDomain(null, p); 63 return new AccessControlContext(new ProtectionDomain[]{ pd }); 64 } 65 66 static AccessControlContext noPermissions() { 67 return withPermissions(/*empty*/); 68 } 69 70 URI wsURI; 71 DummyWebSocketServer webSocketServer; 72 InetSocketAddress proxyAddress; 73 74 @BeforeTest 75 public void setup() throws Exception { 76 ProxyServer proxyServer = new ProxyServer(0, true); 77 proxyAddress = new InetSocketAddress("127.0.0.1", proxyServer.getPort()); 78 webSocketServer = new DummyWebSocketServer(); 79 webSocketServer.open(); 80 wsURI = webSocketServer.getURI(); 81 82 System.out.println("Proxy Server: " + proxyAddress); 83 System.out.println("DummyWebSocketServer: " + wsURI); 84 } 85 86 @AfterTest 87 public void teardown() { 88 webSocketServer.close(); 89 } 90 91 static class NoOpListener implements WebSocket.Listener {} 92 static final WebSocket.Listener noOpListener = new NoOpListener(); 93 94 @DataProvider(name = "passingScenarios") 95 public Object[][] passingScenarios() { 96 HttpClient noProxyClient = HttpClient.newHttpClient(); 97 return new Object[][]{ 98 { (PrivilegedExceptionAction<?>)() -> { 99 noProxyClient.newWebSocketBuilder() 100 .buildAsync(wsURI, noOpListener).get().abort(); 101 return null; }, // no actions 102 new URLPermission[] { new URLPermission(wsURI.toString()) }, 103 "0" /* for log file identification */ }, 104 105 { (PrivilegedExceptionAction<?>)() -> { 106 noProxyClient.newWebSocketBuilder() 107 .buildAsync(wsURI, noOpListener).get().abort(); 108 return null; }, // scheme wildcard 109 new URLPermission[] { new URLPermission("ws://*") }, 110 "0.1" }, 111 112 { (PrivilegedExceptionAction<?>)() -> { 113 noProxyClient.newWebSocketBuilder() 114 .buildAsync(wsURI, noOpListener).get().abort(); 115 return null; }, // port wildcard 116 new URLPermission[] { new URLPermission("ws://"+wsURI.getHost()+":*") }, 117 "0.2" }, 118 119 { (PrivilegedExceptionAction<?>)() -> { 120 noProxyClient.newWebSocketBuilder() 121 .buildAsync(wsURI, noOpListener).get().abort(); 122 return null; }, // empty actions 123 new URLPermission[] { new URLPermission(wsURI.toString(), "") }, 124 "1" }, 125 126 { (PrivilegedExceptionAction<?>)() -> { 127 noProxyClient.newWebSocketBuilder() 128 .buildAsync(wsURI, noOpListener).get().abort(); 129 return null; }, // colon 130 new URLPermission[] { new URLPermission(wsURI.toString(), ":") }, 131 "2" }, 132 133 { (PrivilegedExceptionAction<?>)() -> { 134 noProxyClient.newWebSocketBuilder() 135 .buildAsync(wsURI, noOpListener).get().abort(); 136 return null; }, // wildcard 137 new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") }, 138 "3" }, 139 140 // WS permission checking is agnostic of method, any/none will do 141 { (PrivilegedExceptionAction<?>)() -> { 142 noProxyClient.newWebSocketBuilder() 143 .buildAsync(wsURI, noOpListener).get().abort(); 144 return null; }, // specific method 145 new URLPermission[] { new URLPermission(wsURI.toString(), "GET") }, 146 "3.1" }, 147 148 { (PrivilegedExceptionAction<?>)() -> { 149 noProxyClient.newWebSocketBuilder() 150 .buildAsync(wsURI, noOpListener).get().abort(); 151 return null; }, // specific method 152 new URLPermission[] { new URLPermission(wsURI.toString(), "POST") }, 153 "3.2" }, 154 155 { (PrivilegedExceptionAction<?>)() -> { 156 URI uriWithPath = wsURI.resolve("/path/x"); 157 noProxyClient.newWebSocketBuilder() 158 .buildAsync(uriWithPath, noOpListener).get().abort(); 159 return null; }, // path 160 new URLPermission[] { new URLPermission(wsURI.resolve("/path/x").toString()) }, 161 "4" }, 162 163 { (PrivilegedExceptionAction<?>)() -> { 164 URI uriWithPath = wsURI.resolve("/path/x"); 165 noProxyClient.newWebSocketBuilder() 166 .buildAsync(uriWithPath, noOpListener).get().abort(); 167 return null; }, // same dir wildcard 168 new URLPermission[] { new URLPermission(wsURI.resolve("/path/*").toString()) }, 169 "5" }, 170 171 { (PrivilegedExceptionAction<?>)() -> { 172 URI uriWithPath = wsURI.resolve("/path/x"); 173 noProxyClient.newWebSocketBuilder() 174 .buildAsync(uriWithPath, noOpListener).get().abort(); 175 return null; }, // recursive 176 new URLPermission[] { new URLPermission(wsURI.resolve("/path/-").toString()) }, 177 "6" }, 178 179 { (PrivilegedExceptionAction<?>)() -> { 180 URI uriWithPath = wsURI.resolve("/path/x"); 181 noProxyClient.newWebSocketBuilder() 182 .buildAsync(uriWithPath, noOpListener).get().abort(); 183 return null; }, // recursive top 184 new URLPermission[] { new URLPermission(wsURI.resolve("/-").toString()) }, 185 "7" }, 186 187 { (PrivilegedExceptionAction<?>)() -> { 188 noProxyClient.newWebSocketBuilder() 189 .header("A-Header", "A-Value") // header 190 .buildAsync(wsURI, noOpListener).get().abort(); 191 return null; }, 192 new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") }, 193 "8" }, 194 195 { (PrivilegedExceptionAction<?>)() -> { 196 noProxyClient.newWebSocketBuilder() 197 .header("A-Header", "A-Value") // header 198 .buildAsync(wsURI, noOpListener).get().abort(); 199 return null; }, // wildcard 200 new URLPermission[] { new URLPermission(wsURI.toString(), ":*") }, 201 "9" }, 202 203 { (PrivilegedExceptionAction<?>)() -> { 204 noProxyClient.newWebSocketBuilder() 205 .header("A-Header", "A-Value") // headers 206 .header("B-Header", "B-Value") // headers 207 .buildAsync(wsURI, noOpListener).get().abort(); 208 return null; }, 209 new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header,B-Header") }, 210 "10" }, 211 212 { (PrivilegedExceptionAction<?>)() -> { 213 noProxyClient.newWebSocketBuilder() 214 .header("A-Header", "A-Value") // headers 215 .header("B-Header", "B-Value") // headers 216 .buildAsync(wsURI, noOpListener).get().abort(); 217 return null; }, // wildcard 218 new URLPermission[] { new URLPermission(wsURI.toString(), ":*") }, 219 "11" }, 220 221 { (PrivilegedExceptionAction<?>)() -> { 222 noProxyClient.newWebSocketBuilder() 223 .header("A-Header", "A-Value") // headers 224 .header("B-Header", "B-Value") // headers 225 .buildAsync(wsURI, noOpListener).get().abort(); 226 return null; }, // wildcards 227 new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") }, 228 "12" }, 229 230 { (PrivilegedExceptionAction<?>)() -> { 231 noProxyClient.newWebSocketBuilder() 232 .header("A-Header", "A-Value") // multi-value 233 .header("A-Header", "B-Value") // headers 234 .buildAsync(wsURI, noOpListener).get().abort(); 235 return null; }, // wildcard 236 new URLPermission[] { new URLPermission(wsURI.toString(), ":*") }, 237 "13" }, 238 239 { (PrivilegedExceptionAction<?>)() -> { 240 noProxyClient.newWebSocketBuilder() 241 .header("A-Header", "A-Value") // multi-value 242 .header("A-Header", "B-Value") // headers 243 .buildAsync(wsURI, noOpListener).get().abort(); 244 return null; }, // single grant 245 new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") }, 246 "14" }, 247 248 // client with a DIRECT proxy 249 { (PrivilegedExceptionAction<?>)() -> { 250 ProxySelector ps = ProxySelector.of(null); 251 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 252 client.newWebSocketBuilder() 253 .buildAsync(wsURI, noOpListener).get().abort(); 254 return null; }, 255 new URLPermission[] { new URLPermission(wsURI.toString()) }, 256 "15" }, 257 258 // client with a SOCKS proxy! ( expect implementation to ignore SOCKS ) 259 { (PrivilegedExceptionAction<?>)() -> { 260 ProxySelector ps = new ProxySelector() { 261 @Override public List<Proxy> select(URI uri) { 262 return List.of(new Proxy(Proxy.Type.SOCKS, proxyAddress)); } 263 @Override 264 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { } 265 }; 266 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 267 client.newWebSocketBuilder() 268 .buildAsync(wsURI, noOpListener).get().abort(); 269 return null; }, 270 new URLPermission[] { new URLPermission(wsURI.toString()) }, 271 "16" }, 272 273 // client with a HTTP/HTTPS proxy 274 { (PrivilegedExceptionAction<?>)() -> { 275 assert proxyAddress != null; 276 ProxySelector ps = ProxySelector.of(proxyAddress); 277 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 278 client.newWebSocketBuilder() 279 .buildAsync(wsURI, noOpListener).get().abort(); 280 return null; }, 281 new URLPermission[] { 282 new URLPermission(wsURI.toString()), // CONNECT action string 283 new URLPermission("socket://"+proxyAddress.getHostName() 284 +":"+proxyAddress.getPort(), "CONNECT")}, 285 "17" }, 286 287 { (PrivilegedExceptionAction<?>)() -> { 288 assert proxyAddress != null; 289 ProxySelector ps = ProxySelector.of(proxyAddress); 290 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 291 client.newWebSocketBuilder() 292 .buildAsync(wsURI, noOpListener).get().abort(); 293 return null; }, 294 new URLPermission[] { 295 new URLPermission(wsURI.toString()), // no action string 296 new URLPermission("socket://"+proxyAddress.getHostName() 297 +":"+proxyAddress.getPort())}, 298 "18" }, 299 300 { (PrivilegedExceptionAction<?>)() -> { 301 assert proxyAddress != null; 302 ProxySelector ps = ProxySelector.of(proxyAddress); 303 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 304 client.newWebSocketBuilder() 305 .buildAsync(wsURI, noOpListener).get().abort(); 306 return null; }, 307 new URLPermission[] { 308 new URLPermission(wsURI.toString()), // wildcard headers 309 new URLPermission("socket://"+proxyAddress.getHostName() 310 +":"+proxyAddress.getPort(), "CONNECT:*")}, 311 "19" }, 312 313 { (PrivilegedExceptionAction<?>)() -> { 314 assert proxyAddress != null; 315 CountingProxySelector ps = CountingProxySelector.of(proxyAddress); 316 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 317 client.newWebSocketBuilder() 318 .buildAsync(wsURI, noOpListener).get().abort(); 319 assertEquals(ps.count(), 1); // ps.select only invoked once 320 return null; }, 321 new URLPermission[] { 322 new URLPermission(wsURI.toString()), // empty headers 323 new URLPermission("socket://"+proxyAddress.getHostName() 324 +":"+proxyAddress.getPort(), "CONNECT:")}, 325 "20" }, 326 327 { (PrivilegedExceptionAction<?>)() -> { 328 assert proxyAddress != null; 329 ProxySelector ps = ProxySelector.of(proxyAddress); 330 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 331 client.newWebSocketBuilder() 332 .buildAsync(wsURI, noOpListener).get().abort(); 333 return null; }, 334 new URLPermission[] { 335 new URLPermission(wsURI.toString()), 336 new URLPermission("socket://*")}, // wildcard socket URL 337 "21" }, 338 339 { (PrivilegedExceptionAction<?>)() -> { 340 assert proxyAddress != null; 341 ProxySelector ps = ProxySelector.of(proxyAddress); 342 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 343 client.newWebSocketBuilder() 344 .buildAsync(wsURI, noOpListener).get().abort(); 345 return null; }, 346 new URLPermission[] { 347 new URLPermission("ws://*"), // wildcard ws URL 348 new URLPermission("socket://*")}, // wildcard socket URL 349 "22" }, 350 351 }; 352 } 353 354 @Test(dataProvider = "passingScenarios") 355 public void testWithNoSecurityManager(PrivilegedExceptionAction<?> action, 356 URLPermission[] unused, 357 String dataProviderId) 358 throws Exception 359 { 360 // sanity ( no security manager ) 361 System.setSecurityManager(null); 362 try { 363 AccessController.doPrivileged(action); 364 } finally { 365 System.setSecurityManager(new SecurityManager()); 366 } 367 } 368 369 @Test(dataProvider = "passingScenarios") 370 public void testWithAllPermissions(PrivilegedExceptionAction<?> action, 371 URLPermission[] unused, 372 String dataProviderId) 373 throws Exception 374 { 375 // Run with all permissions, i.e. no further restrictions than test's AllPermission 376 assert System.getSecurityManager() != null; 377 AccessController.doPrivileged(action); 378 } 379 380 @Test(dataProvider = "passingScenarios") 381 public void testWithMinimalPermissions(PrivilegedExceptionAction<?> action, 382 URLPermission[] perms, 383 String dataProviderId) 384 throws Exception 385 { 386 // Run with minimal permissions, i.e. just what is required 387 assert System.getSecurityManager() != null; 388 AccessControlContext minimalACC = withPermissions(perms); 389 AccessController.doPrivileged(action, minimalACC); 390 } 391 392 @Test(dataProvider = "passingScenarios") 393 public void testWithNoPermissions(PrivilegedExceptionAction<?> action, 394 URLPermission[] unused, 395 String dataProviderId) 396 throws Exception 397 { 398 // Run with NO permissions, i.e. expect SecurityException 399 assert System.getSecurityManager() != null; 400 try { 401 AccessController.doPrivileged(action, noPermissions()); 402 fail("EXPECTED SecurityException"); 403 } catch (PrivilegedActionException expected) { 404 Throwable t = expected.getCause(); 405 if (t instanceof ExecutionException) 406 t = t.getCause(); 407 408 if (t instanceof SecurityException) 409 System.out.println("Caught expected SE:" + expected); 410 else 411 fail("Expected SecurityException, but got: " + t); 412 } 413 } 414 415 // --- Negative tests --- 416 417 @DataProvider(name = "failingScenarios") 418 public Object[][] failingScenarios() { 419 HttpClient noProxyClient = HttpClient.newHttpClient(); 420 return new Object[][]{ 421 { (PrivilegedExceptionAction<?>) () -> { 422 noProxyClient.newWebSocketBuilder() 423 .buildAsync(wsURI, noOpListener).get().abort(); 424 return null; 425 }, 426 new URLPermission[]{ /* no permissions */ }, 427 "50" /* for log file identification */}, 428 429 { (PrivilegedExceptionAction<?>) () -> { 430 noProxyClient.newWebSocketBuilder() 431 .buildAsync(wsURI, noOpListener).get().abort(); 432 return null; 433 }, // wrong scheme 434 new URLPermission[]{ new URLPermission("http://*") }, 435 "51" }, 436 437 { (PrivilegedExceptionAction<?>) () -> { 438 noProxyClient.newWebSocketBuilder() 439 .buildAsync(wsURI, noOpListener).get().abort(); 440 return null; 441 }, // wrong scheme 442 new URLPermission[]{ new URLPermission("socket://*") }, 443 "52" }, 444 445 { (PrivilegedExceptionAction<?>) () -> { 446 noProxyClient.newWebSocketBuilder() 447 .buildAsync(wsURI, noOpListener).get().abort(); 448 return null; 449 }, // wrong host 450 new URLPermission[]{ new URLPermission("ws://foo.com/") }, 451 "53" }, 452 453 { (PrivilegedExceptionAction<?>) () -> { 454 noProxyClient.newWebSocketBuilder() 455 .buildAsync(wsURI, noOpListener).get().abort(); 456 return null; 457 }, // wrong port 458 new URLPermission[]{ new URLPermission("ws://"+ wsURI.getHost()+":5") }, 459 "54" }, 460 461 { (PrivilegedExceptionAction<?>) () -> { 462 noProxyClient.newWebSocketBuilder() 463 .header("A-Header", "A-Value") 464 .buildAsync(wsURI, noOpListener).get().abort(); 465 return null; 466 }, // only perm to set B not A 467 new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") }, 468 "55" }, 469 470 { (PrivilegedExceptionAction<?>) () -> { 471 noProxyClient.newWebSocketBuilder() 472 .header("A-Header", "A-Value") 473 .header("B-Header", "B-Value") 474 .buildAsync(wsURI, noOpListener).get().abort(); 475 return null; 476 }, // only perm to set B not A 477 new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") }, 478 "56" }, 479 480 { (PrivilegedExceptionAction<?>)() -> { 481 URI uriWithPath = wsURI.resolve("/path/x"); 482 noProxyClient.newWebSocketBuilder() 483 .buildAsync(uriWithPath, noOpListener).get().abort(); 484 return null; }, // wrong path 485 new URLPermission[] { new URLPermission(wsURI.resolve("/aDiffPath/").toString()) }, 486 "57" }, 487 488 { (PrivilegedExceptionAction<?>)() -> { 489 URI uriWithPath = wsURI.resolve("/path/x"); 490 noProxyClient.newWebSocketBuilder() 491 .buildAsync(uriWithPath, noOpListener).get().abort(); 492 return null; }, // more specific path 493 new URLPermission[] { new URLPermission(wsURI.resolve("/path/x/y").toString()) }, 494 "58" }, 495 496 // client with a HTTP/HTTPS proxy 497 { (PrivilegedExceptionAction<?>)() -> { 498 assert proxyAddress != null; 499 ProxySelector ps = ProxySelector.of(proxyAddress); 500 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 501 client.newWebSocketBuilder() 502 .buildAsync(wsURI, noOpListener).get().abort(); 503 return null; }, // missing proxy perm 504 new URLPermission[] { new URLPermission(wsURI.toString()) }, 505 "100" }, 506 507 // client with a HTTP/HTTPS proxy 508 { (PrivilegedExceptionAction<?>)() -> { 509 assert proxyAddress != null; 510 ProxySelector ps = ProxySelector.of(proxyAddress); 511 HttpClient client = HttpClient.newBuilder().proxy(ps).build(); 512 client.newWebSocketBuilder() 513 .buildAsync(wsURI, noOpListener).get().abort(); 514 return null; }, 515 new URLPermission[] { 516 new URLPermission(wsURI.toString()), // missing proxy CONNECT 517 new URLPermission("socket://*", "GET") }, 518 "101" }, 519 }; 520 } 521 522 @Test(dataProvider = "failingScenarios") 523 public void testWithoutEnoughPermissions(PrivilegedExceptionAction<?> action, 524 URLPermission[] perms, 525 String dataProviderId) 526 throws Exception 527 { 528 // Run without Enough permissions, i.e. expect SecurityException 529 assert System.getSecurityManager() != null; 530 AccessControlContext notEnoughPermsACC = withPermissions(perms); 531 try { 532 AccessController.doPrivileged(action, notEnoughPermsACC); 533 fail("EXPECTED SecurityException"); 534 } catch (PrivilegedActionException expected) { 535 Throwable t = expected.getCause(); 536 if (t instanceof ExecutionException) 537 t = t.getCause(); 538 539 if (t instanceof SecurityException) 540 System.out.println("Caught expected SE:" + expected); 541 else 542 fail("Expected SecurityException, but got: " + t); 543 } 544 } 545 546 /** 547 * A Proxy Selector that wraps a ProxySelector.of(), and counts the number 548 * of times its select method has been invoked. This can be used to ensure 549 * that the Proxy Selector is invoked only once per WebSocket.Builder::buildAsync 550 * invocation. 551 */ 552 static class CountingProxySelector extends ProxySelector { 553 private final ProxySelector proxySelector; 554 private volatile int count; // 0 555 private CountingProxySelector(InetSocketAddress proxyAddress) { 556 proxySelector = ProxySelector.of(proxyAddress); 557 } 558 559 public static CountingProxySelector of(InetSocketAddress proxyAddress) { 560 return new CountingProxySelector(proxyAddress); 561 } 562 563 int count() { return count; } 564 565 @Override 566 public List<Proxy> select(URI uri) { 567 System.out.println("PS: uri"); 568 Throwable t = new Throwable(); 569 t.printStackTrace(System.out); 570 count++; 571 return proxySelector.select(uri); 572 } 573 574 @Override 575 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { 576 proxySelector.connectFailed(uri, sa, ioe); 577 } 578 } 579 }