8217606: LdapContext#reconnect always opens a new connection
authorprappo
Wed, 14 Aug 2019 11:14:54 +0100
changeset 57739 6717d7e59db4
parent 57738 807d192fb7dd
child 57744 d462d0422e1d
child 57745 789e967c2731
8217606: LdapContext#reconnect always opens a new connection Reviewed-by: lancea, vtewari, rriggs Contributed-by: Chris Yin <xu.y.yin@oracle.com>
src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtx.java
test/jdk/com/sun/jndi/ldap/LdapCtx/Reconnect.java
test/jdk/com/sun/jndi/ldap/lib/BaseLdapServer.java
test/jdk/com/sun/jndi/ldap/lib/LdapMessage.java
--- a/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtx.java	Wed Aug 14 00:18:00 2019 -0400
+++ b/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtx.java	Wed Aug 14 11:14:54 2019 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -224,7 +224,6 @@
     String hostname = null;             // host name of server (no brackets
                                         //   for IPv6 literals)
     LdapClient clnt = null;             // connection handle
-    private boolean reconnect = false;  // indicates that re-connect requested
     Hashtable<String, java.lang.Object> envprops = null; // environment properties of context
     int handleReferrals = DEFAULT_REFERRAL_MODE; // how referral is handled
     boolean hasLdapsScheme = false;     // true if the context was created
@@ -2668,7 +2667,6 @@
         }
 
         sharable = false;  // can't share with existing contexts
-        reconnect = true;
         ensureOpen();      // open or reauthenticated
     }
 
@@ -2693,7 +2691,8 @@
                 synchronized (clnt) {
                     if (!clnt.isLdapv3
                         || clnt.referenceCount > 1
-                        || clnt.usingSaslStreams()) {
+                        || clnt.usingSaslStreams()
+                        || !clnt.conn.useable) {
                         closeConnection(SOFT_CLOSE);
                     }
                 }
@@ -2745,7 +2744,7 @@
         try {
             boolean initial = (clnt == null);
 
-            if (initial || reconnect) {
+            if (initial) {
                 ldapVersion = (ver != null) ? Integer.parseInt(ver) :
                     DEFAULT_LDAP_VERSION;
 
@@ -2773,8 +2772,6 @@
                     // Required for SASL client identity
                     envprops);
 
-                reconnect = false;
-
                 /**
                  * Pooled connections are preauthenticated;
                  * newly created ones are not.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/ldap/LdapCtx/Reconnect.java	Wed Aug 14 11:14:54 2019 +0100
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.naming.Context;
+import javax.naming.ldap.InitialLdapContext;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.Hashtable;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/*
+ * @test
+ * @bug 8217606
+ * @summary The LdapContext.reconnect method allows LDAP clients to initiate an
+ *          LDAP bind operation on the existing connection. Invoking this method
+ *          should not open a new connection under those circumstances.
+ *
+ * @library ../lib/
+ * @run main Reconnect
+ */
+public class Reconnect {
+
+    private static final byte[] BIND_RESPONSE = {
+            0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A,
+            0x01, 0x00, 0x04, 0x00, 0x04, 0x00
+    };
+
+    /*
+     * This test checks that there's only one connection from the client to
+     * the server.
+     *
+     * The mechanics is as follows. The first connection is awaited for some
+     * generous timeout to factor in a possibility of running on a slow system.
+     * Once the connection has been made, the second timeout begins. This
+     * second timeout is smaller. The test then verifies that no further
+     * connections have been made for that amount of time.
+     */
+    public static void main(String[] args) throws Exception {
+
+        final Semaphore s = new Semaphore(0);
+
+        BaseLdapServer server = new BaseLdapServer() {
+
+            @Override
+            protected void beforeConnectionHandled(Socket socket) {
+                // Increment the number of connections from LDAP client
+                s.release(1);
+            }
+
+            @Override
+            protected void handleRequest(Socket socket,
+                                         LdapMessage msg,
+                                         OutputStream out)
+                    throws IOException
+            {
+                switch (msg.getOperation()) {
+                    case BIND_REQUEST:
+                        out.write(BIND_RESPONSE);
+                    default:
+                        break;
+                }
+            }
+        };
+
+        try (var s1 = server.start()) {
+            Hashtable<String, Object> env = new Hashtable<>();
+            env.put(Context.INITIAL_CONTEXT_FACTORY,
+                    "com.sun.jndi.ldap.LdapCtxFactory");
+            env.put(Context.PROVIDER_URL,
+                    "ldap://" + InetAddress.getLoopbackAddress().getHostName()
+                            + ":" + server.getPort());
+            env.put("java.naming.ldap.version", "3");
+
+            // open connection
+            InitialLdapContext context = new InitialLdapContext(env, null);
+
+            // send bind request
+            context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
+            context.addToEnvironment(Context.SECURITY_PRINCIPAL, "test");
+            context.addToEnvironment(Context.SECURITY_CREDENTIALS, "secret");
+
+            context.reconnect(null);
+        }
+
+        if (!s.tryAcquire(60L, TimeUnit.SECONDS)) {
+            throw new RuntimeException("No connection has been made");
+        }
+
+        if (s.tryAcquire(5L, TimeUnit.SECONDS)) {
+            throw new RuntimeException("Expected 1 connection, but found: "
+                                               + (s.availablePermits() + 2));
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/ldap/lib/BaseLdapServer.java	Wed Aug 14 11:14:54 2019 +0100
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+import static java.lang.System.Logger.Level.INFO;
+
+/*
+ * A bare-bones (testing aid) server for LDAP scenarios.
+ *
+ * Override the following methods to provide customized behavior
+ *
+ *     * beforeConnectionHandled
+ *     * handleRequest
+ *
+ * Instances of this class are safe for use by multiple threads.
+ */
+public class BaseLdapServer implements Closeable {
+
+    private static final System.Logger logger = System.getLogger("BaseLdapServer");
+
+    private final Thread acceptingThread = new Thread(this::acceptConnections);
+    private final ServerSocket serverSocket;
+    private final List<Socket> socketList = new ArrayList<>();
+    private final ExecutorService connectionsPool;
+
+    private final Object lock = new Object();
+    /*
+     * 3-valued state to detect restarts and other programming errors.
+     */
+    private State state = State.NEW;
+
+    private enum State {
+        NEW,
+        STARTED,
+        STOPPED
+    }
+
+    public BaseLdapServer() throws IOException {
+        this(new ServerSocket(0, 0, InetAddress.getLoopbackAddress()));
+    }
+
+    public BaseLdapServer(ServerSocket serverSocket) {
+        this.serverSocket = Objects.requireNonNull(serverSocket);
+        this.connectionsPool = Executors.newCachedThreadPool();
+    }
+
+    private void acceptConnections() {
+        logger().log(INFO, "Server is accepting connections at port {0}",
+                     getPort());
+        try {
+            while (isRunning()) {
+                Socket socket = serverSocket.accept();
+                logger().log(INFO, "Accepted new connection at {0}", socket);
+                synchronized (lock) {
+                    // Recheck if the server is still running
+                    // as someone has to close the `socket`
+                    if (isRunning()) {
+                        socketList.add(socket);
+                    } else {
+                        closeSilently(socket);
+                    }
+                }
+                connectionsPool.submit(() -> handleConnection(socket));
+            }
+        } catch (IOException | RejectedExecutionException e) {
+            if (isRunning()) {
+                throw new RuntimeException(
+                        "Unexpected exception while accepting connections", e);
+            }
+        } finally {
+            logger().log(INFO, "Server stopped accepting connections at port {0}",
+                                getPort());
+        }
+    }
+
+    /*
+     * A "Template Method" describing how a connection (represented by a socket)
+     * is handled.
+     *
+     * The socket is closed immediately before the method returns (normally or
+     * abruptly).
+     */
+    private void handleConnection(Socket socket) {
+        // No need to close socket's streams separately, they will be closed
+        // automatically when `socket.close()` is called
+        beforeConnectionHandled(socket);
+        try (socket) {
+            OutputStream out = socket.getOutputStream();
+            InputStream in = socket.getInputStream();
+            byte[] inBuffer = new byte[1024];
+            int count;
+            byte[] request;
+
+            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+            int msgLen = -1;
+
+            // As inBuffer.length > 0, at least 1 byte is read
+            while ((count = in.read(inBuffer)) > 0) {
+                buffer.write(inBuffer, 0, count);
+                if (msgLen <= 0) {
+                    msgLen = LdapMessage.getMessageLength(buffer.toByteArray());
+                }
+
+                if (msgLen > 0 && buffer.size() >= msgLen) {
+                    if (buffer.size() > msgLen) {
+                        byte[] tmpBuffer = buffer.toByteArray();
+                        request = Arrays.copyOf(tmpBuffer, msgLen);
+                        buffer.reset();
+                        buffer.write(tmpBuffer, msgLen, tmpBuffer.length - msgLen);
+                    } else {
+                        request = buffer.toByteArray();
+                        buffer.reset();
+                    }
+                    msgLen = -1;
+                } else {
+                    logger.log(INFO, "Request message incomplete, " +
+                            "bytes received {0}, expected {1}", buffer.size(), msgLen);
+                    continue;
+                }
+                handleRequest(socket, new LdapMessage(request), out);
+            }
+        } catch (Throwable t) {
+            if (!isRunning()) {
+                logger.log(INFO, "Connection Handler exit {0}", t.getMessage());
+            } else {
+                t.printStackTrace();
+            }
+        }
+    }
+
+    /*
+     * Called first thing in `handleConnection()`.
+     *
+     * Override to customize the behavior.
+     */
+    protected void beforeConnectionHandled(Socket socket) { /* empty */ }
+
+    /*
+     * Called after an LDAP request has been read in `handleConnection()`.
+     *
+     * Override to customize the behavior.
+     */
+    protected void handleRequest(Socket socket,
+                                 LdapMessage request,
+                                 OutputStream out)
+            throws IOException
+    {
+        logger().log(INFO, "Discarding message {0} from {1}. "
+                             + "Override {2}.handleRequest to change this behavior.",
+                     request, socket, getClass().getName());
+    }
+
+    /*
+     * To be used by subclasses.
+     */
+    protected final System.Logger logger() {
+        return logger;
+    }
+
+    /*
+     * Starts this server. May be called only once.
+     */
+    public BaseLdapServer start() {
+        synchronized (lock) {
+            if (state != State.NEW) {
+                throw new IllegalStateException(state.toString());
+            }
+            state = State.STARTED;
+            logger().log(INFO, "Starting server at port {0}", getPort());
+            acceptingThread.start();
+            return this;
+        }
+    }
+
+    /*
+     * Stops this server.
+     *
+     * May be called at any time, even before a call to `start()`. In the latter
+     * case the subsequent call to `start()` will throw an exception. Repeated
+     * calls to this method have no effect.
+     *
+     * Stops accepting new connections, interrupts the threads serving already
+     * accepted connections and closes all the sockets.
+     */
+    @Override
+    public void close() {
+        synchronized (lock) {
+            if (state == State.STOPPED) {
+                return;
+            }
+            state = State.STOPPED;
+            logger().log(INFO, "Stopping server at port {0}", getPort());
+            acceptingThread.interrupt();
+            closeSilently(serverSocket);
+            // It's important to signal an interruption so that overridden
+            // methods have a chance to return if they use
+            // interruption-sensitive blocking operations. However, blocked I/O
+            // operations on the socket will NOT react on that, hence the socket
+            // also has to be closed to propagate shutting down.
+            connectionsPool.shutdownNow();
+            socketList.forEach(BaseLdapServer.this::closeSilently);
+        }
+    }
+
+    /**
+     * Returns the local port this server is listening at.
+     *
+     * @return the port this server is listening at
+     */
+    public int getPort() {
+        return serverSocket.getLocalPort();
+    }
+
+    /*
+     * Returns a flag to indicate whether this server is running or not.
+     *
+     * @return {@code true} if this server is running, {@code false} otherwise.
+     */
+    public boolean isRunning() {
+        synchronized (lock) {
+            return state == State.STARTED;
+        }
+    }
+
+    /*
+     * To be used by subclasses.
+     */
+    protected final void closeSilently(Closeable resource) {
+        try {
+            resource.close();
+        } catch (IOException ignored) { }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/ldap/lib/LdapMessage.java	Wed Aug 14 11:14:54 2019 +0100
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * An LDAP message.
+ */
+public class LdapMessage {
+
+    private final byte[] message;
+    private int messageID;
+    private Operation operation;
+
+    public enum Operation {
+        BIND_REQUEST(0x60, "BindRequest"),                      // [APPLICATION 0]
+        BIND_RESPONSE(0x61, "BindResponse"),                    // [APPLICATION 1]
+        UNBIND_REQUEST(0x42, "UnbindRequest"),                  // [APPLICATION 2]
+        SEARCH_REQUEST(0x63, "SearchRequest"),                  // [APPLICATION 3]
+        SEARCH_RESULT_ENTRY(0x64, "SearchResultEntry"),         // [APPLICATION 4]
+        SEARCH_RESULT_DONE(0x65, "SearchResultDone"),           // [APPLICATION 5]
+        MODIFY_REQUEST(0x66, "ModifyRequest"),                  // [APPLICATION 6]
+        MODIFY_RESPONSE(0x67, "ModifyResponse"),                // [APPLICATION 7]
+        ADD_REQUEST(0x68, "AddRequest"),                        // [APPLICATION 8]
+        ADD_RESPONSE(0x69, "AddResponse"),                      // [APPLICATION 9]
+        DELETE_REQUEST(0x4A, "DeleteRequest"),                  // [APPLICATION 10]
+        DELETE_RESPONSE(0x6B, "DeleteResponse"),                // [APPLICATION 11]
+        MODIFY_DN_REQUEST(0x6C, "ModifyDNRequest"),             // [APPLICATION 12]
+        MODIFY_DN_RESPONSE(0x6D, "ModifyDNResponse"),           // [APPLICATION 13]
+        COMPARE_REQUEST(0x6E, "CompareRequest"),                // [APPLICATION 14]
+        COMPARE_RESPONSE(0x6F, "CompareResponse"),              // [APPLICATION 15]
+        ABANDON_REQUEST(0x50, "AbandonRequest"),                // [APPLICATION 16]
+        SEARCH_RESULT_REFERENCE(0x73, "SearchResultReference"), // [APPLICATION 19]
+        EXTENDED_REQUEST(0x77, "ExtendedRequest"),              // [APPLICATION 23]
+        EXTENDED_RESPONSE(0x78, "ExtendedResponse"),            // [APPLICATION 24]
+        INTERMEDIATE_RESPONSE(0x79, "IntermediateResponse");    // [APPLICATION 25]
+
+        private final int id;
+        private final String name;
+
+        Operation(int id, String name) {
+            this.id = id;
+            this.name = name;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+
+        private static Operation fromId(int id) {
+            Optional<Operation> optional = Stream.of(Operation.values())
+                    .filter(o -> o.id == id).findFirst();
+            if (optional.isPresent()) {
+                return optional.get();
+            } else {
+                throw new RuntimeException(
+                        "Unknown id " + id + " for enum Operation.");
+            }
+        }
+    }
+
+    public LdapMessage(byte[] message) {
+        this.message = message;
+        parse();
+    }
+
+    public LdapMessage(String hexString) {
+        this(parseHexBinary(hexString));
+    }
+
+    // Extracts the message ID and operation ID from an LDAP protocol encoding
+    private void parse() {
+        if (message == null || message.length < 2) {
+            throw new RuntimeException(
+                    "Invalid ldap message: " + Arrays.toString(message));
+        }
+
+        if (message[0] != 0x30) {
+            throw new RuntimeException("Bad LDAP encoding in message, "
+                    + "expected ASN.1 SEQUENCE tag (0x30), encountered "
+                    + message[0]);
+        }
+
+        int index = 2;
+        if ((message[1] & 0x80) == 0x80) {
+            index += (message[1] & 0x0F);
+        }
+
+        if (message[index] != 0x02) {
+            throw new RuntimeException("Bad LDAP encoding in message, "
+                    + "expected ASN.1 INTEGER tag (0x02), encountered "
+                    + message[index]);
+        }
+        int length = message[index + 1];
+        index += 2;
+        messageID = new BigInteger(1,
+                                   Arrays.copyOfRange(message, index, index + length)).intValue();
+        index += length;
+        int operationID = message[index];
+        operation = Operation.fromId(operationID);
+    }
+
+    /**
+     * Return original ldap message in byte array.
+     *
+     * @return original ldap message
+     */
+    public byte[] getMessage() {
+        return Arrays.copyOf(message, message.length);
+    }
+
+    /**
+     * Return ldap message id.
+     *
+     * @return ldap message id.
+     */
+    public int getMessageID() {
+        return messageID;
+    }
+
+    /**
+     * Return ldap message's operation.
+     *
+     * @return ldap message's operation.
+     */
+    public Operation getOperation() {
+        return operation;
+    }
+
+    private static byte[] parseHexBinary(String s) {
+
+        final int len = s.length();
+
+        // "111" is not a valid hex encoding.
+        if (len % 2 != 0) {
+            throw new IllegalArgumentException(
+                    "hexBinary needs to be even-length: " + s);
+        }
+
+        byte[] out = new byte[len / 2];
+
+        for (int i = 0; i < len; i += 2) {
+            int h = Character.digit(s.charAt(i), 16);
+            int l = Character.digit(s.charAt(i + 1), 16);
+            if (h == -1 || l == -1) {
+                throw new IllegalArgumentException(
+                        "contains illegal character for hexBinary: " + s);
+            }
+
+            out[i / 2] = (byte) (h * 16 + l);
+        }
+
+        return out;
+    }
+
+    public static int getMessageLength(byte[] encoding) {
+        if (encoding.length < 2) {
+            // not enough data to extract msg len, just return -1
+            return -1;
+        }
+
+        if (encoding[0] != 0x30) {
+            throw new RuntimeException("Error: bad LDAP encoding message: "
+                                               + "expected ASN.1 SEQUENCE tag (0x30), encountered "
+                                               + encoding[0]);
+        }
+
+        int len;
+        int index = 1;
+        int payloadLen = 0;
+
+        if ((encoding[1] & 0x80) == 0x80) {
+            len = (encoding[1] & 0x0F);
+            index++;
+        } else {
+            len = 1;
+        }
+
+        if (len > 4) {
+            throw new RuntimeException(
+                    "Error: LDAP encoding message payload too large");
+        }
+
+        if (encoding.length < index + len) {
+            // additional data required to extract payload len, return -1
+            return -1;
+        }
+
+        for (byte b : Arrays.copyOfRange(encoding, index, index + len)) {
+            payloadLen = payloadLen << 8 | (b & 0xFF);
+        }
+
+        if (payloadLen <= 0) {
+            throw new RuntimeException(
+                    "Error: invalid LDAP encoding message length or payload too large");
+        }
+
+        return index + len + payloadLen;
+    }
+}