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 
  39 import static java.lang.System.Logger.Level.INFO;
  40 
  41 /*
  42  * A bare-bones (testing aid) server for LDAP scenarios.
  43  *
  44  * Override the following methods to provide customized behavior
  45  *
  46  *     * beforeAcceptingConnections
  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             beforeAcceptingConnections();
  87             while (isRunning()) {
  88                 Socket socket = serverSocket.accept();
  89                 logger().log(INFO, "Accepted new connection at {0}", socket);
  90                 synchronized (lock) {
  91                     // Recheck if the server is still running
  92                     // as someone has to close the `socket`
  93                     if (isRunning()) {
  94                         socketList.add(socket);
  95                     } else {
  96                         closeSilently(socket);
  97                     }
  98                 }
  99                 connectionsPool.submit(() -> handleConnection(socket));
 100             }
 101         } catch (Throwable t) {
 102             if (isRunning()) {
 103                 throw new RuntimeException(
 104                         "Unexpected exception while accepting connections", t);
 105             }
 106         } finally {
 107             logger().log(INFO, "Server stopped accepting connections at port {0}",
 108                                 getPort());
 109         }
 110     }
 111 
 112     /*
 113      * Called once immediately preceding the server accepting connections.
 114      *
 115      * Override to customize the behavior.
 116      */
 117     protected void beforeAcceptingConnections() { }
 118 
 119     /*
 120      * A "Template Method" describing how a connection (represented by a socket)
 121      * is handled.
 122      *
 123      * The socket is closed immediately before the method returns (normally or
 124      * abruptly).
 125      */
 126     private void handleConnection(Socket socket) {
 127         // No need to close socket's streams separately, they will be closed
 128         // automatically when `socket.close()` is called
 129         beforeConnectionHandled(socket);
 130         try (socket) {
 131             OutputStream out = socket.getOutputStream();
 132             InputStream in = socket.getInputStream();
 133             byte[] inBuffer = new byte[1024];
 134             int count;
 135             byte[] request;
 136 
 137             ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 138             int msgLen = -1;
 139 
 140             // As inBuffer.length > 0, at least 1 byte is read
 141             while ((count = in.read(inBuffer)) > 0) {
 142                 buffer.write(inBuffer, 0, count);
 143                 if (msgLen <= 0) {
 144                     msgLen = LdapMessage.getMessageLength(buffer.toByteArray());
 145                 }
 146 
 147                 if (msgLen > 0 && buffer.size() >= msgLen) {
 148                     if (buffer.size() > msgLen) {
 149                         byte[] tmpBuffer = buffer.toByteArray();
 150                         request = Arrays.copyOf(tmpBuffer, msgLen);
 151                         buffer.reset();
 152                         buffer.write(tmpBuffer, msgLen, tmpBuffer.length - msgLen);
 153                     } else {
 154                         request = buffer.toByteArray();
 155                         buffer.reset();
 156                     }
 157                     msgLen = -1;
 158                 } else {
 159                     logger.log(INFO, "Request message incomplete, " +
 160                             "bytes received {0}, expected {1}", buffer.size(), msgLen);
 161                     continue;
 162                 }
 163                 handleRequest(socket, new LdapMessage(request), out);
 164             }
 165         } catch (Throwable t) {
 166             if (!isRunning()) {
 167                 logger.log(INFO, "Connection Handler exit {0}", t.getMessage());
 168             } else {
 169                 t.printStackTrace();
 170             }
 171         }
 172     }
 173 
 174     /*
 175      * Called first thing in `handleConnection()`.
 176      *
 177      * Override to customize the behavior.
 178      */
 179     protected void beforeConnectionHandled(Socket socket) { /* empty */ }
 180 
 181     /*
 182      * Called after an LDAP request has been read in `handleConnection()`.
 183      *
 184      * Override to customize the behavior.
 185      */
 186     protected void handleRequest(Socket socket,
 187                                  LdapMessage request,
 188                                  OutputStream out)
 189             throws IOException
 190     {
 191         logger().log(INFO, "Discarding message {0} from {1}. "
 192                              + "Override {2}.handleRequest to change this behavior.",
 193                      request, socket, getClass().getName());
 194     }
 195 
 196     /*
 197      * To be used by subclasses.
 198      */
 199     protected final System.Logger logger() {
 200         return logger;
 201     }
 202 
 203     /*
 204      * Starts this server. May be called only once.
 205      */
 206     public BaseLdapServer start() {
 207         synchronized (lock) {
 208             if (state != State.NEW) {
 209                 throw new IllegalStateException(state.toString());
 210             }
 211             state = State.STARTED;
 212             logger().log(INFO, "Starting server at port {0}", getPort());
 213             acceptingThread.start();
 214             return this;
 215         }
 216     }
 217 
 218     /*
 219      * Stops this server.
 220      *
 221      * May be called at any time, even before a call to `start()`. In the latter
 222      * case the subsequent call to `start()` will throw an exception. Repeated
 223      * calls to this method have no effect.
 224      *
 225      * Stops accepting new connections, interrupts the threads serving already
 226      * accepted connections and closes all the sockets.
 227      */
 228     @Override
 229     public void close() {
 230         synchronized (lock) {
 231             if (state == State.STOPPED) {
 232                 return;
 233             }
 234             state = State.STOPPED;
 235             logger().log(INFO, "Stopping server at port {0}", getPort());
 236             acceptingThread.interrupt();
 237             closeSilently(serverSocket);
 238             // It's important to signal an interruption so that overridden
 239             // methods have a chance to return if they use
 240             // interruption-sensitive blocking operations. However, blocked I/O
 241             // operations on the socket will NOT react on that, hence the socket
 242             // also has to be closed to propagate shutting down.
 243             connectionsPool.shutdownNow();
 244             socketList.forEach(BaseLdapServer.this::closeSilently);
 245         }
 246     }
 247 
 248     /**
 249      * Returns the local port this server is listening at.
 250      *
 251      * This method can be called at any time.
 252      *
 253      * @return the port this server is listening at
 254      */
 255     public int getPort() {
 256         return serverSocket.getLocalPort();
 257     }
 258 
 259     /**
 260      * Returns the address this server is listening at.
 261      *
 262      * This method can be called at any time.
 263      *
 264      * @return the address
 265      */
 266     public InetAddress getInetAddress() {
 267         return serverSocket.getInetAddress();
 268     }
 269 
 270     /*
 271      * Returns a flag to indicate whether this server is running or not.
 272      *
 273      * @return {@code true} if this server is running, {@code false} otherwise.
 274      */
 275     public boolean isRunning() {
 276         synchronized (lock) {
 277             return state == State.STARTED;
 278         }
 279     }
 280 
 281     /*
 282      * To be used by subclasses.
 283      */
 284     protected final void closeSilently(Closeable resource) {
 285         try {
 286             resource.close();
 287         } catch (IOException ignored) { }
 288     }
 289 }