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
31import java.net.Socket;
32import java.net.ServerSocket;
33import java.net.SocketTimeoutException;
34import java.io.*;
35import javax.naming.*;
36import javax.naming.directory.*;
37import java.util.List;
38import java.util.Hashtable;
39import java.util.ArrayList;
40import java.util.concurrent.Callable;
41import java.util.concurrent.ExecutionException;
42import java.util.concurrent.Executors;
43import java.util.concurrent.ExecutorService;
44import java.util.concurrent.Future;
45import java.util.concurrent.ScheduledExecutorService;
46import java.util.concurrent.ScheduledFuture;
47import java.util.concurrent.TimeoutException;
48import java.util.concurrent.TimeUnit;
49import javax.net.ssl.SSLHandshakeException;
50
51import static java.util.concurrent.TimeUnit.MILLISECONDS;
52import static java.util.concurrent.TimeUnit.NANOSECONDS;
53
54
55abstract 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
148abstract 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
169abstract 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
185class 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
209class 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
228class 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
252class 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
268class 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
290class 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
323class 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
341public 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
445