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
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
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 
test1()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 
test2()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 
test3()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 
test4()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 
test5()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 
test6()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 
test7()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 
TestServer()282         TestServer() throws IOException { }
283 
284         @Override
beforeAcceptingConnections()285         protected void beforeAcceptingConnections() {
286             starting.completeAsync(() -> null);
287         }
288 
starting()289         public CompletableFuture<Void> starting() {
290             return starting.copy();
291         }
292     }
293 
294     static class BindableButNotReadableServer extends TestServer {
295 
BindableButNotReadableServer()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
handleRequest(Socket socket, LdapMessage msg, OutputStream out)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 
NotBindableServer()320         NotBindableServer() throws IOException { }
321 
322         @Override
beforeConnectionHandled(Socket socket)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      */
assertCompletion(long loMillis, long hiMillis, Callable<T> code)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      */
assertIncompletion(long millis, Callable<?> code)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 
urlTo(TestServer server)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      */
startAuxiliaryDiagnosticOutput()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 }
508