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 }