1 /*
   2  * Copyright (c) 2019, 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 import java.io.ByteArrayOutputStream;
  25 import java.io.Closeable;
  26 import java.io.IOException;
  27 import java.io.InputStream;
  28 import java.io.OutputStream;
  29 import java.net.InetAddress;
  30 import java.net.ServerSocket;
  31 import java.net.Socket;
  32 import java.util.ArrayList;
  33 import java.util.Arrays;
  34 import java.util.List;
  35 import java.util.Objects;
  36 import java.util.concurrent.ExecutorService;
  37 import java.util.concurrent.Executors;
  38 import java.util.concurrent.RejectedExecutionException;
  39 
  40 import static java.lang.System.Logger.Level.INFO;
  41 
  42 /*
  43  * A bare-bones (testing aid) server for LDAP scenarios.
  44  *
  45  * Override the following methods to provide customized behavior
  46  *
  47  *     * beforeConnectionHandled
  48  *     * handleRequest
  49  *
  50  * Instances of this class are safe for use by multiple threads.
  51  */
  52 public class BaseLdapServer implements Closeable {
  53 
  54     private static final System.Logger logger = System.getLogger("BaseLdapServer");
  55 
  56     private final Thread acceptingThread = new Thread(this::acceptConnections);
  57     private final ServerSocket serverSocket;
  58     private final List<Socket> socketList = new ArrayList<>();
  59     private final ExecutorService connectionsPool;
  60 
  61     private final Object lock = new Object();
  62     /*
  63      * 3-valued state to detect restarts and other programming errors.
  64      */
  65     private State state = State.NEW;
  66 
  67     private enum State {
  68         NEW,
  69         STARTED,
  70         STOPPED
  71     }
  72 
  73     public BaseLdapServer() throws IOException {
  74         this(new ServerSocket(0, 0, InetAddress.getLoopbackAddress()));
  75     }
  76 
  77     public BaseLdapServer(ServerSocket serverSocket) {
  78         this.serverSocket = Objects.requireNonNull(serverSocket);
  79         this.connectionsPool = Executors.newCachedThreadPool();
  80     }
  81 
  82     private void acceptConnections() {
  83         logger().log(INFO, "Server is accepting connections at port {0}",
  84                      getPort());
  85         try {
  86             while (isRunning()) {
  87                 Socket socket = serverSocket.accept();
  88                 logger().log(INFO, "Accepted new connection at {0}", socket);
  89                 synchronized (lock) {
  90                     // Recheck if the server is still running
  91                     // as someone has to close the `socket`
  92                     if (isRunning()) {
  93                         socketList.add(socket);
  94                     } else {
  95                         closeSilently(socket);
  96                     }
  97                 }
  98                 connectionsPool.submit(() -> handleConnection(socket));
  99             }
 100         } catch (IOException | RejectedExecutionException e) {
 101             if (isRunning()) {
 102                 throw new RuntimeException(
 103                         "Unexpected exception while accepting connections", e);
 104             }
 105         } finally {
 106             logger().log(INFO, "Server stopped accepting connections at port {0}",
 107                                 getPort());
 108         }
 109     }
 110 
 111     /*
 112      * A "Template Method" describing how a connection (represented by a socket)
 113      * is handled.
 114      *
 115      * The socket is closed immediately before the method returns (normally or
 116      * abruptly).
 117      */
 118     private void handleConnection(Socket socket) {
 119         // No need to close socket's streams separately, they will be closed
 120         // automatically when `socket.close()` is called
 121         beforeConnectionHandled(socket);
 122         try (socket) {
 123             OutputStream out = socket.getOutputStream();
 124             InputStream in = socket.getInputStream();
 125             byte[] inBuffer = new byte[1024];
 126             int count;
 127             byte[] request;
 128 
 129             ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 130             int msgLen = -1;
 131 
 132             // As inBuffer.length > 0, at least 1 byte is read
 133             while ((count = in.read(inBuffer)) > 0) {
 134                 buffer.write(inBuffer, 0, count);
 135                 if (msgLen <= 0) {
 136                     msgLen = LdapMessage.getMessageLength(buffer.toByteArray());
 137                 }
 138 
 139                 if (msgLen > 0 && buffer.size() >= msgLen) {
 140                     if (buffer.size() > msgLen) {
 141                         byte[] tmpBuffer = buffer.toByteArray();
 142                         request = Arrays.copyOf(tmpBuffer, msgLen);
 143                         buffer.reset();
 144                         buffer.write(tmpBuffer, msgLen, tmpBuffer.length - msgLen);
 145                     } else {
 146                         request = buffer.toByteArray();
 147                         buffer.reset();
 148                     }
 149                     msgLen = -1;
 150                 } else {
 151                     logger.log(INFO, "Request message incomplete, " +
 152                             "bytes received {0}, expected {1}", buffer.size(), msgLen);
 153                     continue;
 154                 }
 155                 handleRequest(socket, new LdapMessage(request), out);
 156             }
 157         } catch (Throwable t) {
 158             if (!isRunning()) {
 159                 logger.log(INFO, "Connection Handler exit {0}", t.getMessage());
 160             } else {
 161                 t.printStackTrace();
 162             }
 163         }
 164     }
 165 
 166     /*
 167      * Called first thing in `handleConnection()`.
 168      *
 169      * Override to customize the behavior.
 170      */
 171     protected void beforeConnectionHandled(Socket socket) { /* empty */ }
 172 
 173     /*
 174      * Called after an LDAP request has been read in `handleConnection()`.
 175      *
 176      * Override to customize the behavior.
 177      */
 178     protected void handleRequest(Socket socket,
 179                                  LdapMessage request,
 180                                  OutputStream out)
 181             throws IOException
 182     {
 183         logger().log(INFO, "Discarding message {0} from {1}. "
 184                              + "Override {2}.handleRequest to change this behavior.",
 185                      request, socket, getClass().getName());
 186     }
 187 
 188     /*
 189      * To be used by subclasses.
 190      */
 191     protected final System.Logger logger() {
 192         return logger;
 193     }
 194 
 195     /*
 196      * Starts this server. May be called only once.
 197      */
 198     public BaseLdapServer start() {
 199         synchronized (lock) {
 200             if (state != State.NEW) {
 201                 throw new IllegalStateException(state.toString());
 202             }
 203             state = State.STARTED;
 204             logger().log(INFO, "Starting server at port {0}", getPort());
 205             acceptingThread.start();
 206             return this;
 207         }
 208     }
 209 
 210     /*
 211      * Stops this server.
 212      *
 213      * May be called at any time, even before a call to `start()`. In the latter
 214      * case the subsequent call to `start()` will throw an exception. Repeated
 215      * calls to this method have no effect.
 216      *
 217      * Stops accepting new connections, interrupts the threads serving already
 218      * accepted connections and closes all the sockets.
 219      */
 220     @Override
 221     public void close() {
 222         synchronized (lock) {
 223             if (state == State.STOPPED) {
 224                 return;
 225             }
 226             state = State.STOPPED;
 227             logger().log(INFO, "Stopping server at port {0}", getPort());
 228             acceptingThread.interrupt();
 229             closeSilently(serverSocket);
 230             // It's important to signal an interruption so that overridden
 231             // methods have a chance to return if they use
 232             // interruption-sensitive blocking operations. However, blocked I/O
 233             // operations on the socket will NOT react on that, hence the socket
 234             // also has to be closed to propagate shutting down.
 235             connectionsPool.shutdownNow();
 236             socketList.forEach(BaseLdapServer.this::closeSilently);
 237         }
 238     }
 239 
 240     /**
 241      * Returns the local port this server is listening at.
 242      *
 243      * @return the port this server is listening at
 244      */
 245     public int getPort() {
 246         return serverSocket.getLocalPort();
 247     }
 248 
 249     /*
 250      * Returns a flag to indicate whether this server is running or not.
 251      *
 252      * @return {@code true} if this server is running, {@code false} otherwise.
 253      */
 254     public boolean isRunning() {
 255         synchronized (lock) {
 256             return state == State.STARTED;
 257         }
 258     }
 259 
 260     /*
 261      * To be used by subclasses.
 262      */
 263     protected final void closeSilently(Closeable resource) {
 264         try {
 265             resource.close();
 266         } catch (IOException ignored) { }
 267     }
 268 }