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