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 }