blob: ec071b7a28ad40b5f115f5ea1847ecd37d9e6889 [file] [log] [blame]
/*
* Copyright (c) 2019, 2020, 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 static java.lang.System.Logger.Level.INFO;
/*
* A bare-bones (testing aid) server for LDAP scenarios.
*
* Override the following methods to provide customized behavior
*
* * beforeAcceptingConnections
* * beforeConnectionHandled
* * handleRequest (or handleRequestEx)
*
* 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 {
beforeAcceptingConnections();
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 (Throwable t) {
if (isRunning()) {
throw new RuntimeException(
"Unexpected exception while accepting connections", t);
}
} finally {
logger().log(INFO, "Server stopped accepting connections at port {0}",
getPort());
}
}
/*
* Called once immediately preceding the server accepting connections.
*
* Override to customize the behavior.
*/
protected void beforeAcceptingConnections() { }
/*
* 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);
ConnWrapper connWrapper = new ConnWrapper(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;
}
handleRequestEx(socket, new LdapMessage(request), out, connWrapper);
if (connWrapper.updateRequired()) {
var wrapper = connWrapper.getWrapper();
in = wrapper.getInputStream();
out = wrapper.getOutputStream();
connWrapper.clearFlag();
}
}
} catch (Throwable t) {
if (!isRunning()) {
logger.log(INFO, "Connection Handler exit {0}", t.getMessage());
} else {
t.printStackTrace();
}
}
if (connWrapper.getWrapper() != null) {
closeSilently(connWrapper.getWrapper());
}
}
/*
* 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());
}
/*
* Called after an LDAP request has been read in `handleConnection()`.
*
* Override to customize the behavior if you want to handle starttls
* extended op, otherwise override handleRequest method instead.
*
* This is extended handleRequest method which provide possibility to
* wrap current socket connection, that's necessary to handle starttls
* extended request, here is sample code about how to wrap current socket
*
* switch (request.getOperation()) {
* ......
* case EXTENDED_REQUEST:
* if (new String(request.getMessage()).endsWith(STARTTLS_REQ_OID)) {
* out.write(STARTTLS_RESPONSE);
* SSLSocket sslSocket = (SSLSocket) sslSocketFactory
* .createSocket(socket, null, socket.getLocalPort(),
* false);
* sslSocket.setUseClientMode(false);
* connWrapper.setWrapper(sslSocket);
* }
* break;
* ......
* }
*/
protected void handleRequestEx(Socket socket,
LdapMessage request,
OutputStream out,
ConnWrapper connWrapper)
throws IOException {
// by default, just call handleRequest to keep compatibility
handleRequest(socket, request, out);
}
/*
* 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.
*
* This method can be called at any time.
*
* @return the port this server is listening at
*/
public int getPort() {
return serverSocket.getLocalPort();
}
/**
* Returns the address this server is listening at.
*
* This method can be called at any time.
*
* @return the address
*/
public InetAddress getInetAddress() {
return serverSocket.getInetAddress();
}
/*
* 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) { }
}
/*
* To be used for handling starttls extended request
*/
protected class ConnWrapper {
private Socket original;
private Socket wrapper;
private boolean flag = false;
public ConnWrapper(Socket socket) {
original = socket;
}
public Socket getWrapper() {
return wrapper;
}
public void setWrapper(Socket wrapper) {
if (wrapper != null && wrapper != original) {
this.wrapper = wrapper;
flag = true;
}
}
public boolean updateRequired() {
return flag;
}
public void clearFlag() {
flag = false;
}
}
}