1 /*
   2  * Copyright (c) 2011, 2016, 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  * @run main/othervm LdapTimeoutTest
  27  * @bug 7094377 8000487 6176036 7056489
  28  * @summary Timeout tests for ldap
  29  */
  30 
  31 import java.net.Socket;
  32 import java.net.ServerSocket;
  33 import java.net.SocketTimeoutException;
  34 import java.io.*;
  35 import javax.naming.*;
  36 import javax.naming.directory.*;
  37 import java.util.List;
  38 import java.util.Hashtable;
  39 import java.util.ArrayList;
  40 import java.util.concurrent.Callable;
  41 import java.util.concurrent.ExecutionException;
  42 import java.util.concurrent.Executors;
  43 import java.util.concurrent.ExecutorService;
  44 import java.util.concurrent.Future;
  45 import java.util.concurrent.ScheduledExecutorService;
  46 import java.util.concurrent.ScheduledFuture;
  47 import java.util.concurrent.TimeoutException;
  48 import java.util.concurrent.TimeUnit;
  49 import javax.net.ssl.SSLHandshakeException;
  50 
  51 import static java.util.concurrent.TimeUnit.MILLISECONDS;
  52 import static java.util.concurrent.TimeUnit.NANOSECONDS;
  53 
  54 
  55 abstract class LdapTest implements Callable {
  56 
  57     Hashtable env;
  58     TestServer server;
  59     ScheduledExecutorService killSwitchPool;
  60     boolean passed = false;
  61     private int HANGING_TEST_TIMEOUT = 20_000;
  62 
  63     public LdapTest (TestServer server, Hashtable env) {
  64         this.server = server;
  65         this.env = env;
  66     }
  67 
  68     public LdapTest(TestServer server, Hashtable env,
  69             ScheduledExecutorService killSwitchPool)
  70     {
  71         this(server, env);
  72         this.killSwitchPool = killSwitchPool;
  73     }
  74 
  75     public abstract void performOp(InitialContext ctx) throws NamingException;
  76     public abstract void handleNamingException(
  77         NamingException e, long start, long end);
  78 
  79     public void pass() {
  80         this.passed = true;
  81     }
  82 
  83     public void fail() {
  84         throw new RuntimeException("Test failed");
  85     }
  86 
  87     public void fail(Exception e) {
  88         throw new RuntimeException("Test failed", e);
  89     }
  90 
  91     boolean shutItDown(InitialContext ctx) {
  92         try {
  93             if (ctx != null) ctx.close();
  94             return true;
  95         } catch (NamingException ex) {
  96             return false;
  97         }
  98     }
  99 
 100     public Boolean call() {
 101         InitialContext ctx = null;
 102         ScheduledFuture killer = null;
 103         long start = System.nanoTime();
 104 
 105         try {
 106             while(!server.accepting())
 107                 Thread.sleep(200); // allow the server to start up
 108             Thread.sleep(200); // to be sure
 109 
 110             // if this is a hanging test, scheduled a thread to
 111             // interrupt after a certain time
 112             if (killSwitchPool != null) {
 113                 final Thread current = Thread.currentThread();
 114                 killer = killSwitchPool.schedule(
 115                     new Callable<Void>() {
 116                         public Void call() throws Exception {
 117                             current.interrupt();
 118                             return null;
 119                         }
 120                     }, HANGING_TEST_TIMEOUT, MILLISECONDS);
 121             }
 122 
 123             env.put(Context.PROVIDER_URL, "ldap://localhost:" +
 124                     server.getLocalPort());
 125 
 126             try {
 127                 ctx = new InitialDirContext(env);
 128                 performOp(ctx);
 129                 fail();
 130             } catch (NamingException e) {
 131                 long end = System.nanoTime();
 132                 System.out.println(this.getClass().toString() + " - elapsed: "
 133                         + NANOSECONDS.toMillis(end - start));
 134                 handleNamingException(e, start, end);
 135             } finally {
 136                 if (killer != null && !killer.isDone())
 137                     killer.cancel(true);
 138                 shutItDown(ctx);
 139                 server.close();
 140             }
 141             return passed;
 142         } catch (IOException|InterruptedException e) {
 143             throw new RuntimeException(e);
 144         }
 145     }
 146 }
 147 
 148 abstract class ReadServerTest extends LdapTest {
 149 
 150     public ReadServerTest(Hashtable env) throws IOException {
 151         super(new BindableServer(), env);
 152     }
 153 
 154     public ReadServerTest(Hashtable env,
 155                           ScheduledExecutorService killSwitchPool)
 156             throws IOException
 157     {
 158         super(new BindableServer(), env, killSwitchPool);
 159     }
 160 
 161     public void performOp(InitialContext ctx) throws NamingException {
 162         SearchControls scl = new SearchControls();
 163         scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 164         NamingEnumeration<SearchResult> answer = ((InitialDirContext)ctx)
 165             .search("ou=People,o=JNDITutorial", "(objectClass=*)", scl);
 166     }
 167 }
 168 
 169 abstract class DeadServerTest extends LdapTest {
 170 
 171     public DeadServerTest(Hashtable env) throws IOException {
 172         super(new DeadServer(), env);
 173     }
 174 
 175     public DeadServerTest(Hashtable env,
 176                           ScheduledExecutorService killSwitchPool)
 177             throws IOException
 178     {
 179         super(new DeadServer(), env, killSwitchPool);
 180     }
 181 
 182     public void performOp(InitialContext ctx) throws NamingException {}
 183 }
 184 
 185 class DeadServerNoTimeoutTest extends DeadServerTest {
 186 
 187     public DeadServerNoTimeoutTest(Hashtable env,
 188                                    ScheduledExecutorService killSwitchPool)
 189             throws IOException
 190     {
 191         super(env, killSwitchPool);
 192     }
 193 
 194     public void handleNamingException(NamingException e, long start, long end) {
 195         if (e instanceof InterruptedNamingException) Thread.interrupted();
 196 
 197         if (NANOSECONDS.toMillis(end - start) < LdapTimeoutTest.MIN_TIMEOUT) {
 198             System.err.printf("DeadServerNoTimeoutTest fail: timeout should be " +
 199                               "at least %s ms, actual time is %s ms%n",
 200                               LdapTimeoutTest.MIN_TIMEOUT,
 201                               NANOSECONDS.toMillis(end - start));
 202             fail();
 203         } else {
 204             pass();
 205         }
 206     }
 207 }
 208 
 209 class DeadServerTimeoutTest extends DeadServerTest {
 210 
 211     public DeadServerTimeoutTest(Hashtable env) throws IOException {
 212         super(env);
 213     }
 214 
 215     public void handleNamingException(NamingException e, long start, long end)
 216     {
 217         // non SSL connect will timeout via readReply using connectTimeout
 218         if (NANOSECONDS.toMillis(end - start) < 2_900) {
 219             pass();
 220         } else {
 221             System.err.println("Fail: Waited too long");
 222             fail();
 223         }
 224     }
 225 }
 226 
 227 
 228 class ReadServerNoTimeoutTest extends ReadServerTest {
 229 
 230     public ReadServerNoTimeoutTest(Hashtable env,
 231                                    ScheduledExecutorService killSwitchPool)
 232             throws IOException
 233     {
 234         super(env, killSwitchPool);
 235     }
 236 
 237     public void handleNamingException(NamingException e, long start, long end) {
 238         if (e instanceof InterruptedNamingException) Thread.interrupted();
 239 
 240         if (NANOSECONDS.toMillis(end - start) < LdapTimeoutTest.MIN_TIMEOUT) {
 241             System.err.printf("ReadServerNoTimeoutTest fail: timeout should be " +
 242                               "at least %s ms, actual time is %s ms%n",
 243                               LdapTimeoutTest.MIN_TIMEOUT,
 244                               NANOSECONDS.toMillis(end - start));
 245             fail();
 246         } else {
 247             pass();
 248         }
 249     }
 250 }
 251 
 252 class ReadServerTimeoutTest extends ReadServerTest {
 253 
 254     public ReadServerTimeoutTest(Hashtable env) throws IOException {
 255         super(env);
 256     }
 257 
 258     public void handleNamingException(NamingException e, long start, long end) {
 259         System.out.println("ReadServerTimeoutTest: end-start=" + NANOSECONDS.toMillis(end - start));
 260         if (NANOSECONDS.toMillis(end - start) < 2_500) {
 261             fail();
 262         } else {
 263             pass();
 264         }
 265     }
 266 }
 267 
 268 class TestServer extends Thread {
 269     ServerSocket serverSock;
 270     boolean accepting = false;
 271 
 272     public TestServer() throws IOException {
 273         this.serverSock = new ServerSocket(0);
 274         start();
 275     }
 276 
 277     public int getLocalPort() {
 278         return serverSock.getLocalPort();
 279     }
 280 
 281     public boolean accepting() {
 282         return accepting;
 283     }
 284 
 285     public void close() throws IOException {
 286         serverSock.close();
 287     }
 288 }
 289 
 290 class BindableServer extends TestServer {
 291 
 292     public BindableServer() throws IOException {
 293         super();
 294     }
 295 
 296     private byte[] bindResponse = {
 297         0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A,
 298         0x01, 0x00, 0x04, 0x00, 0x04, 0x00
 299     };
 300 
 301     public void run() {
 302         try {
 303             accepting = true;
 304             Socket socket = serverSock.accept();
 305             InputStream in = socket.getInputStream();
 306             OutputStream out = socket.getOutputStream();
 307 
 308             // Read the LDAP BindRequest
 309             while (in.read() != -1) {
 310                 in.skip(in.available());
 311                 break;
 312             }
 313 
 314             // Write an LDAP BindResponse
 315             out.write(bindResponse);
 316             out.flush();
 317         } catch (IOException e) {
 318             // ignore
 319         }
 320     }
 321 }
 322 
 323 class DeadServer extends TestServer {
 324 
 325     public DeadServer() throws IOException {
 326         super();
 327     }
 328 
 329     public void run() {
 330         while(true) {
 331             try {
 332                 accepting = true;
 333                 Socket socket = serverSock.accept();
 334             } catch (Exception e) {
 335                 break;
 336             }
 337         }
 338     }
 339 }
 340 
 341 public class LdapTimeoutTest {
 342 
 343     private static final ExecutorService testPool =
 344         Executors.newFixedThreadPool(3);
 345     private static final ScheduledExecutorService killSwitchPool =
 346         Executors.newScheduledThreadPool(3);
 347     public static int MIN_TIMEOUT = 18_000;
 348 
 349     static Hashtable createEnv() {
 350         Hashtable env = new Hashtable(11);
 351         env.put(Context.INITIAL_CONTEXT_FACTORY,
 352             "com.sun.jndi.ldap.LdapCtxFactory");
 353         return env;
 354     }
 355 
 356     public static void main(String[] args) throws Exception {
 357 
 358         InitialContext ctx = null;
 359         List<Future> results = new ArrayList<>();
 360 
 361         try {
 362             // run the DeadServerTest with no timeouts set
 363             // this should get stuck indefinitely, so we need to kill
 364             // it after a timeout
 365             System.out.println("Running connect timeout test with 20s kill switch");
 366             Hashtable env = createEnv();
 367             results.add(
 368                     testPool.submit(new DeadServerNoTimeoutTest(env, killSwitchPool)));
 369 
 370             // run the ReadServerTest with connect timeout set
 371             // this should get stuck indefinitely so we need to kill
 372             // it after a timeout
 373             System.out.println("Running read timeout test with 10ms connect timeout & 20s kill switch");
 374             Hashtable env1 = createEnv();
 375             env1.put("com.sun.jndi.ldap.connect.timeout", "10");
 376             results.add(testPool.submit(
 377                     new ReadServerNoTimeoutTest(env1, killSwitchPool)));
 378 
 379             // run the ReadServerTest with no timeouts set
 380             // this should get stuck indefinitely, so we need to kill
 381             // it after a timeout
 382             System.out.println("Running read timeout test with 20s kill switch");
 383             Hashtable env2 = createEnv();
 384             results.add(testPool.submit(
 385                     new ReadServerNoTimeoutTest(env2, killSwitchPool)));
 386 
 387             // run the DeadServerTest with connect / read timeouts set
 388             // this should exit after the connect timeout expires
 389             System.out.println("Running connect timeout test with 10ms connect timeout, 3000ms read timeout");
 390             Hashtable env3 = createEnv();
 391             env3.put("com.sun.jndi.ldap.connect.timeout", "10");
 392             env3.put("com.sun.jndi.ldap.read.timeout", "3000");
 393             results.add(testPool.submit(new DeadServerTimeoutTest(env3)));
 394 
 395 
 396             // run the ReadServerTest with connect / read timeouts set
 397             // this should exit after the connect timeout expires
 398             //
 399             // NOTE: commenting this test out as it is failing intermittently.
 400             //
 401             // System.out.println("Running read timeout test with 10ms connect timeout, 3000ms read timeout");
 402             // Hashtable env4 = createEnv();
 403             // env4.put("com.sun.jndi.ldap.connect.timeout", "10");
 404             // env4.put("com.sun.jndi.ldap.read.timeout", "3000");
 405             // results.add(testPool.submit(new ReadServerTimeoutTest(env4)));
 406 
 407             // run the DeadServerTest with connect timeout set
 408             // this should exit after the connect timeout expires
 409             System.out.println("Running connect timeout test with 10ms connect timeout");
 410             Hashtable env5 = createEnv();
 411             env5.put("com.sun.jndi.ldap.connect.timeout", "10");
 412             results.add(testPool.submit(new DeadServerTimeoutTest(env5)));
 413 
 414             // 8000487: Java JNDI connection library on ldap conn is
 415             // not honoring configured timeout
 416             System.out.println("Running simple auth connection test");
 417             Hashtable env6 = createEnv();
 418             env6.put("com.sun.jndi.ldap.connect.timeout", "10");
 419             env6.put("com.sun.jndi.ldap.read.timeout", "3000");
 420             env6.put(Context.SECURITY_AUTHENTICATION, "simple");
 421             env6.put(Context.SECURITY_PRINCIPAL, "user");
 422             env6.put(Context.SECURITY_CREDENTIALS, "password");
 423             results.add(testPool.submit(new DeadServerTimeoutTest(env6)));
 424 
 425             boolean testFailed = false;
 426             for (Future test : results) {
 427                 while (!test.isDone()) {
 428                     if ((Boolean) test.get() == false)
 429                         testFailed = true;
 430                 }
 431             }
 432 
 433             if (testFailed) {
 434                 throw new AssertionError("some tests failed");
 435             }
 436 
 437         } finally {
 438             LdapTimeoutTest.killSwitchPool.shutdown();
 439             LdapTimeoutTest.testPool.shutdown();
 440         }
 441     }
 442 
 443 }
 444