1 /*
   2  * Copyright (c) 2011, 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  * @library /test/lib
  27  *          lib/
  28  * @run testng/othervm LdapTimeoutTest
  29  * @bug 7094377 8000487 6176036 7056489 8151678
  30  * @summary Timeout tests for ldap
  31  */
  32 
  33 import org.testng.Assert;
  34 import org.testng.annotations.BeforeTest;
  35 import org.testng.annotations.Test;
  36 
  37 import javax.naming.Context;
  38 import javax.naming.NamingException;
  39 import javax.naming.directory.InitialDirContext;
  40 import javax.naming.directory.SearchControls;
  41 import java.io.IOException;
  42 import java.io.OutputStream;
  43 import java.net.Socket;
  44 import java.util.ArrayList;
  45 import java.util.Hashtable;
  46 import java.util.List;
  47 import java.util.Objects;
  48 import java.util.concurrent.Callable;
  49 import java.util.concurrent.CompletableFuture;
  50 import java.util.concurrent.ExecutionException;
  51 import java.util.concurrent.ExecutorService;
  52 import java.util.concurrent.Executors;
  53 import java.util.concurrent.Future;
  54 import java.util.concurrent.FutureTask;
  55 import java.util.concurrent.SynchronousQueue;
  56 import java.util.concurrent.TimeUnit;
  57 import java.util.concurrent.TimeoutException;
  58 
  59 import static java.lang.String.format;
  60 import static java.util.concurrent.TimeUnit.MILLISECONDS;
  61 import static java.util.concurrent.TimeUnit.NANOSECONDS;
  62 import static jdk.test.lib.Utils.adjustTimeout;
  63 import static org.testng.Assert.assertTrue;
  64 import static org.testng.Assert.expectThrows;
  65 
  66 public class LdapTimeoutTest {
  67 
  68     // ------ configure test timeouts here ------
  69 
  70     /*
  71      * Practical representation of an infinite timeout.
  72      */
  73     private static final long INFINITY_MILLIS = adjustTimeout(20_000);
  74     /*
  75      * The acceptable variation in timeout measurements.
  76      */
  77     private static final long TOLERANCE       = adjustTimeout( 3_500);
  78 
  79     private static final long CONNECT_MILLIS  = adjustTimeout( 3_000);
  80     private static final long READ_MILLIS     = adjustTimeout(10_000);
  81 
  82     static {
  83         // a series of checks to make sure this timeouts configuration is
  84         // consistent and the timeouts do not overlap
  85 
  86         assert (TOLERANCE >= 0);
  87         // context creation
  88         assert (2 * CONNECT_MILLIS + TOLERANCE < READ_MILLIS);
  89         // context creation immediately followed by search
  90         assert (2 * CONNECT_MILLIS + READ_MILLIS + TOLERANCE < INFINITY_MILLIS);
  91     }
  92 
  93     @BeforeTest
  94     public void beforeTest() {
  95         startAuxiliaryDiagnosticOutput();
  96     }
  97 
  98     /*
  99      * These are timeout tests and they are run in parallel to reduce the total
 100      * amount of run time.
 101      *
 102      * Currently it doesn't seem possible to instruct JTREG to run TestNG test
 103      * methods in parallel. That said, this JTREG test is still
 104      * a "TestNG-flavored" test for the sake of having org.testng.Assert
 105      * capability.
 106      */
 107     @Test
 108     public void test() throws Exception {
 109         List<Future<?>> futures = new ArrayList<>();
 110         ExecutorService executorService = Executors.newCachedThreadPool();
 111         try {
 112             futures.add(executorService.submit(() -> { test1(); return null; }));
 113             futures.add(executorService.submit(() -> { test2(); return null; }));
 114             futures.add(executorService.submit(() -> { test3(); return null; }));
 115             futures.add(executorService.submit(() -> { test4(); return null; }));
 116             futures.add(executorService.submit(() -> { test5(); return null; }));
 117             futures.add(executorService.submit(() -> { test6(); return null; }));
 118             futures.add(executorService.submit(() -> { test7(); return null; }));
 119         } finally {
 120             executorService.shutdown();
 121         }
 122         int failedCount = 0;
 123         for (var f : futures) {
 124             try {
 125                 f.get();
 126             } catch (ExecutionException e) {
 127                 failedCount++;
 128                 e.getCause().printStackTrace(System.out);
 129             }
 130         }
 131         if (failedCount > 0)
 132             throw new RuntimeException(failedCount + " (sub)tests failed");
 133     }
 134 
 135     static void test1() throws Exception {
 136         Hashtable<Object, Object> env = new Hashtable<>();
 137         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 138         // Here and in the other tests it's important to close the server as
 139         // calling `thread.interrupt` from assertion may not be enough
 140         // (depending on where the blocking call has stuck)
 141         try (TestServer server = new NotBindableServer()) {
 142             env.put(Context.PROVIDER_URL, urlTo(server));
 143             server.start();
 144             // Here and in the other tests joining done purely to reduce timing
 145             // jitter. Commenting out or removing that should not make the test
 146             // incorrect. (ServerSocket can accept connection as soon as it is
 147             // bound, not need to call `accept` before that.)
 148             server.starting().join();
 149             assertIncompletion(INFINITY_MILLIS, () -> new InitialDirContext(env));
 150         }
 151     }
 152 
 153     static void test2() throws Exception {
 154         Hashtable<Object, Object> env = new Hashtable<>();
 155         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 156         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 157         try (TestServer server = new BindableButNotReadableServer()) {
 158             env.put(Context.PROVIDER_URL, urlTo(server));
 159             server.start();
 160             server.starting().join();
 161             InitialDirContext ctx = new InitialDirContext(env);
 162             SearchControls scl = new SearchControls();
 163             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 164             assertIncompletion(INFINITY_MILLIS,
 165                                () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 166         }
 167     }
 168 
 169     static void test3() throws Exception {
 170         Hashtable<Object, Object> env = new Hashtable<>();
 171         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 172         try (TestServer server = new BindableButNotReadableServer()) {
 173             env.put(Context.PROVIDER_URL, urlTo(server));
 174             server.start();
 175             server.starting().join();
 176             InitialDirContext ctx = new InitialDirContext(env);
 177             SearchControls scl = new SearchControls();
 178             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 179             assertIncompletion(INFINITY_MILLIS,
 180                                () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 181         }
 182     }
 183 
 184     static void test4() throws Exception {
 185         Hashtable<Object, Object> env = new Hashtable<>();
 186         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 187         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 188         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 189         try (TestServer server = new NotBindableServer()) {
 190             env.put(Context.PROVIDER_URL, urlTo(server));
 191             server.start();
 192             server.starting().join();
 193             Assert.ThrowingRunnable completion =
 194                     () -> assertCompletion(CONNECT_MILLIS,
 195                                            2 * CONNECT_MILLIS + TOLERANCE,
 196                                            () -> new InitialDirContext(env));
 197             NamingException e = expectThrows(NamingException.class, completion);
 198             String msg = e.getMessage();
 199             assertTrue(msg != null && msg.contains("timeout")
 200                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 201                        msg);
 202         }
 203     }
 204 
 205     static void test5() throws Exception {
 206         Hashtable<Object, Object> env = new Hashtable<>();
 207         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 208         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 209         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 210         try (TestServer server = new BindableButNotReadableServer()) {
 211             env.put(Context.PROVIDER_URL, urlTo(server));
 212             server.start();
 213             server.starting().join();
 214             InitialDirContext ctx = new InitialDirContext(env);
 215             SearchControls scl = new SearchControls();
 216             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 217             Assert.ThrowingRunnable completion =
 218                     () -> assertCompletion(READ_MILLIS,
 219                                            READ_MILLIS + TOLERANCE,
 220                                            () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 221             NamingException e = expectThrows(NamingException.class, completion);
 222             String msg = e.getMessage();
 223             assertTrue(msg != null && msg.contains("timeout")
 224                                && msg.contains(String.valueOf(READ_MILLIS)),
 225                        msg);
 226         }
 227     }
 228 
 229     static void test6() throws Exception {
 230         Hashtable<Object, Object> env = new Hashtable<>();
 231         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 232         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 233         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 234         try (TestServer server = new NotBindableServer()) {
 235             env.put(Context.PROVIDER_URL, urlTo(server));
 236             server.start();
 237             server.starting().join();
 238             Assert.ThrowingRunnable completion =
 239                     () -> assertCompletion(CONNECT_MILLIS,
 240                                            2 * CONNECT_MILLIS + TOLERANCE,
 241                                            () -> new InitialDirContext(env));
 242             NamingException e = expectThrows(NamingException.class, completion);
 243             String msg = e.getMessage();
 244             assertTrue(msg != null && msg.contains("timeout")
 245                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 246                        msg);
 247         }
 248     }
 249 
 250     static void test7() throws Exception {
 251         // 8000487: Java JNDI connection library on ldap conn is
 252         // not honoring configured timeout
 253         Hashtable<Object, Object> env = new Hashtable<>();
 254         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 255         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 256         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 257         env.put(Context.SECURITY_AUTHENTICATION, "simple");
 258         env.put(Context.SECURITY_PRINCIPAL, "user");
 259         env.put(Context.SECURITY_CREDENTIALS, "password");
 260         try (TestServer server = new NotBindableServer()) {
 261             env.put(Context.PROVIDER_URL, urlTo(server));
 262             server.start();
 263             server.starting().join();
 264             Assert.ThrowingRunnable completion =
 265                     () -> assertCompletion(CONNECT_MILLIS,
 266                                            2 * CONNECT_MILLIS + TOLERANCE,
 267                                            () -> new InitialDirContext(env));
 268             NamingException e = expectThrows(NamingException.class, completion);
 269             String msg = e.getMessage();
 270             assertTrue(msg != null && msg.contains("timeout")
 271                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 272                        msg);
 273         }
 274     }
 275 
 276     // ------ test stub servers ------
 277 
 278     static class TestServer extends BaseLdapServer {
 279 
 280         private final CompletableFuture<Void> starting = new CompletableFuture<>();
 281 
 282         TestServer() throws IOException { }
 283 
 284         @Override
 285         protected void beforeAcceptingConnections() {
 286             starting.completeAsync(() -> null);
 287         }
 288 
 289         public CompletableFuture<Void> starting() {
 290             return starting.copy();
 291         }
 292     }
 293 
 294     static class BindableButNotReadableServer extends TestServer {
 295 
 296         BindableButNotReadableServer() throws IOException { }
 297 
 298         private static final byte[] bindResponse = {
 299                 0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A,
 300                 0x01, 0x00, 0x04, 0x00, 0x04, 0x00
 301         };
 302 
 303         @Override
 304         protected void handleRequest(Socket socket,
 305                                      LdapMessage msg,
 306                                      OutputStream out)
 307                 throws IOException {
 308             switch (msg.getOperation()) {
 309                 case BIND_REQUEST:
 310                     out.write(bindResponse);
 311                     out.flush();
 312                 default:
 313                     break;
 314             }
 315         }
 316     }
 317 
 318     static class NotBindableServer extends TestServer {
 319 
 320         NotBindableServer() throws IOException { }
 321 
 322         @Override
 323         protected void beforeConnectionHandled(Socket socket) {
 324             try {
 325                 TimeUnit.DAYS.sleep(Integer.MAX_VALUE);
 326             } catch (InterruptedException e) {
 327                 Thread.currentThread().interrupt();
 328             }
 329         }
 330     }
 331 
 332     // ------ timeouts check utilities ------
 333 
 334     /*
 335      * Asserts that the specified executable yields a result or an exception
 336      * within the specified time frame. Interrupts the executable
 337      * unconditionally.
 338      *
 339      * If the executable yields a result or an exception within the specified
 340      * time frame, the result will be returned and the exception will be
 341      * rethrown respectively in a transparent fashion as if the executable was
 342      * executed directly.
 343      */
 344     public static <T> T assertCompletion(long loMillis,
 345                                          long hiMillis,
 346                                          Callable<T> code)
 347             throws Throwable {
 348         if (loMillis < 0 || hiMillis < 0 || loMillis > hiMillis) {
 349             throw new IllegalArgumentException("loMillis=" + loMillis +
 350                                                        ", hiMillis=" + hiMillis);
 351         }
 352         Objects.requireNonNull(code);
 353 
 354         // this queue acts both as an exchange point and a barrier
 355         SynchronousQueue<Long> startTime = new SynchronousQueue<>();
 356 
 357         Callable<T> wrappedTask = () -> {
 358             // by the time this value reaches the "stopwatch" thread it might be
 359             // well outdated and that's okay, we will adjust the wait time
 360             startTime.put(System.nanoTime());
 361             return code.call();
 362         };
 363 
 364         FutureTask<T> task = new FutureTask<>(wrappedTask);
 365         Thread t = new Thread(task);
 366         t.start();
 367 
 368         final long startNanos;
 369         try {
 370             startNanos = startTime.take(); // (1) wait for the initial time mark
 371         } catch (Throwable e) {
 372             t.interrupt();
 373             throw e;
 374         }
 375 
 376         final long waitTime = hiMillis -
 377                 NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time
 378 
 379         try {
 380             T r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete
 381             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 382             if (elapsed < loMillis || elapsed > hiMillis) {
 383                 throw new RuntimeException(format(
 384                         "After %s ms. (waitTime %s ms.) returned result '%s'", elapsed, waitTime, r));
 385             }
 386             return r;
 387         } catch (ExecutionException e) {
 388             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 389             if (elapsed < loMillis || elapsed > hiMillis) {
 390                 throw new RuntimeException(format(
 391                         "After %s ms. (waitTime %s ms.) thrown exception", elapsed, waitTime), e);
 392             }
 393             throw e.getCause();
 394         } catch (TimeoutException e) {
 395             // We trust timed get not to throw TimeoutException prematurely
 396             // (i.e. before the wait time elapses)
 397             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 398             throw new RuntimeException(format(
 399                     "After %s ms. (waitTime %s ms.) is incomplete", elapsed, waitTime));
 400         } finally {
 401             t.interrupt();
 402         }
 403     }
 404 
 405     /*
 406      * Asserts that the specified executable yields no result and no exception
 407      * for at least the specified amount of time. Interrupts the executable
 408      * unconditionally.
 409      */
 410     public static void assertIncompletion(long millis, Callable<?> code)
 411             throws Exception
 412     {
 413         if (millis < 0) {
 414             throw new IllegalArgumentException("millis=" + millis);
 415         }
 416         Objects.requireNonNull(code);
 417 
 418         // this queue acts both as an exchange point and a barrier
 419         SynchronousQueue<Long> startTime = new SynchronousQueue<>();
 420 
 421         Callable<?> wrappedTask = () -> {
 422             // by the time this value reaches the "stopwatch" thread it might be
 423             // well outdated and that's okay, we will adjust the wait time
 424             startTime.put(System.nanoTime());
 425             return code.call();
 426         };
 427 
 428         FutureTask<?> task = new FutureTask<>(wrappedTask);
 429         Thread t = new Thread(task);
 430         t.start();
 431 
 432         final long startNanos;
 433         try {
 434             startNanos = startTime.take(); // (1) wait for the initial time mark
 435         } catch (Throwable e) {
 436             t.interrupt();
 437             throw e;
 438         }
 439 
 440         final long waitTime = millis -
 441                 NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time
 442 
 443         try {
 444             Object r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete
 445             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 446             if (elapsed < waitTime) {
 447                 throw new RuntimeException(format(
 448                         "After %s ms. (waitTime %s ms.) returned result '%s'", elapsed, waitTime, r));
 449             }
 450         } catch (ExecutionException e) {
 451             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 452             if (elapsed < waitTime) {
 453                 throw new RuntimeException(format(
 454                         "After %s ms. (waitTime %s ms.) thrown exception", elapsed, waitTime), e);
 455             }
 456         } catch (TimeoutException expected) {
 457         } finally {
 458             t.interrupt();
 459         }
 460     }
 461 
 462     // ------ miscellaneous utilities ------
 463 
 464     private static String urlTo(TestServer server) {
 465         String hostAddress = server.getInetAddress().getHostAddress();
 466         String addr;
 467         if (hostAddress.contains(":")) { // IPv6
 468             addr = '[' + hostAddress + ']';
 469         } else {                         // IPv4
 470             addr = hostAddress;
 471         }
 472         return "ldap://" + addr + ":" + server.getPort();
 473     }
 474 
 475     /*
 476      * A diagnostic aid that might help with debugging timeout issues. The idea
 477      * is to continuously measure accuracy and responsiveness of the system that
 478      * runs this test. If the system is overwhelmed (with something else), it
 479      * might affect the test run. At the very least we will have traces of that
 480      * in the logs.
 481      *
 482      * This utility does not automatically scale up test timeouts, it simply
 483      * gathers information.
 484      */
 485     private static void startAuxiliaryDiagnosticOutput() {
 486         System.out.printf("Starting diagnostic output (probe)%n");
 487         Thread t = new Thread(() -> {
 488             for (int i = 0; ; i = ((i % 20) + 1)) {
 489                 // 500, 1_000, 1_500, ..., 9_500, 10_000, 500, 1_000, ...
 490                 long expected = i * 500;
 491                 long start = System.nanoTime();
 492                 try {
 493                     MILLISECONDS.sleep(expected);
 494                 } catch (InterruptedException e) {
 495                     return;
 496                 }
 497                 long stop = System.nanoTime();
 498                 long actual = NANOSECONDS.toMillis(stop - start);
 499                 System.out.printf("(probe) expected [ms.]: %s, actual [ms.]: %s%n",
 500                                   expected, actual);
 501 
 502             }
 503         }, "probe");
 504         t.setDaemon(true);
 505         t.start();
 506     }
 507 }