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