Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 93 additions & 22 deletions miniweb-core/src/main/java/com/devsmart/miniweb/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManager;

public class Server {

private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
Expand All @@ -44,16 +53,53 @@ public class Server {
private ServerSocket mServerSocket;
private SocketListener mListenThread;
private boolean mRunning = false;
private SSLContext mSslContext;
private boolean mSslEnabled = false;

public void configureSslContext(KeyManager[] keyManagers, TrustManager[] trustManagers) {
try {
mSslContext = SSLContext.getInstance("TLS");
mSslContext.init(keyManagers, trustManagers, null);
mSslEnabled = true;
} catch (Exception e) {
LOGGER.error("Could not initialize SSLContext:", e);
mSslContext = null;
mSslEnabled = false;
}
}

public void start() throws IOException {
if (mRunning) {
LOGGER.warn("server already running");
return;
}

mServerSocket = new ServerSocket();
if (mSslEnabled && mSslContext != null) {
SSLServerSocketFactory factory = mSslContext.getServerSocketFactory();
mServerSocket = factory.createServerSocket();
} else {
ServerSocketFactory factory = ServerSocketFactory.getDefault();
mServerSocket = factory.createServerSocket();
}

mServerSocket.setReuseAddress(true);
mServerSocket.bind(new InetSocketAddress(port));

if (mSslEnabled && mServerSocket instanceof SSLServerSocket) {
SSLServerSocket sslServerSocket = (SSLServerSocket) mServerSocket;
sslServerSocket.setNeedClientAuth(true);
String[] supported = sslServerSocket.getSupportedProtocols();
String[] desired = new String[]{"TLSv1.3", "TLSv1.2"};
String[] filtered = Arrays.stream(desired)
.filter(p -> Arrays.asList(supported).contains(p))
.toArray(String[]::new);
if (filtered.length > 0) {
sslServerSocket.setEnabledProtocols(filtered);
}
LOGGER.info("SSL server socket configured: clientAuth=required, protocols={}",
Arrays.toString(sslServerSocket.getEnabledProtocols()));
}

if (mIsDebugBuild) {
LOGGER.info("Server started listening on {}", mServerSocket.getLocalSocketAddress());
}
Expand All @@ -75,9 +121,11 @@ public void shutdown() {
}
try {
mListenThread.join();
mListenThread = null;
} catch (InterruptedException e) {
LOGGER.error("", e);
LOGGER.error("Interrupted waiting for listener thread shutdown", e);
Thread.currentThread().interrupt();
} finally {
mListenThread = null;
}
LOGGER.info("Server shutdown");
}
Expand Down Expand Up @@ -116,15 +164,13 @@ public void run() {
LOGGER.warn("unknown error - {}", remoteConnection.connection, e);
} finally {
LOGGER.info("Closing connection {}", remoteConnection.connection);
closeConnection();
}
}

public void closeConnection() {
try {
remoteConnection.connection.close();
} catch (IOException e){
LOGGER.error("", e);
try {
if (remoteConnection.connection.isOpen()) {
remoteConnection.connection.close();
}
} catch (UnsupportedOperationException | IOException e) {
LOGGER.error("Error closing connection", e);
}
}
}
}
Expand Down Expand Up @@ -156,28 +202,53 @@ public void run() {
if (mIsDebugBuild) {
LOGGER.info("accepting connection from: {}", socket.getRemoteSocketAddress());
}

DefaultHttpServerConnection connection = new DefaultHttpServerConnection();
SSLSafeHttpServerConnection connection = new SSLSafeHttpServerConnection();
connection.bind(socket, new BasicHttpParams());
RemoteConnection remoteConnection = new RemoteConnection(socket.getInetAddress(), connection);

mWorkerThreads.execute(new WorkerTask(httpService, remoteConnection));
} catch (SocketTimeoutException e) {
continue;
// ignore and continue
} catch (SSLException e) {
closeSocket(socket);
} catch (SocketException e) {
LOGGER.info("SocketListener shutting down");
mRunning = false;
} catch (IOException e) {
LOGGER.error("", e);
LOGGER.error("I/O error", e);
mRunning = false;
} catch (ClassCastException e) {
LOGGER.error("Expected SSLSocket when SSL is enabled; check server socket setup", e);
mRunning = false;
}
}
if (socket != null) {
try {
socket.close();
LOGGER.info("Connection is closed properly");
} catch (IOException e) {
LOGGER.error("Can't close connection. Reason: ", e);
closeSocket(socket);
}
}

private static void closeSocket(Socket socket) {
if (socket != null) {
try {
socket.close();
LOGGER.info("Connection is closed properly");
} catch (IOException e) {
LOGGER.error("Can't close connection. Reason: ", e);
}
}
}


public static class SSLSafeHttpServerConnection extends DefaultHttpServerConnection {
@Override
public void close() throws IOException {
try {
// This attempts to call shutdownOutput(), which fails on Android SSL
super.close();
} catch (UnsupportedOperationException e) {
// Catch the specific Android error
// verify the socket exists and force-close it to prevent leaks
if (getSocket() != null) {
getSocket().close();
Comment on lines +241 to +251
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

https://square.github.io/okhttp/changelogs/changelog/#version-521

close() method attempts to call shutdownOutput() which fails and throw the exception and thus to avoid it I had to override the implementation and catch it in and close force-close the socket to prevent leaks

}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.devsmart.miniweb;



import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerResolver;
import com.devsmart.miniweb.handlers.FileSystemRequestHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;

import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;

public class ServerBuilder {

private int mPort = 8080;
Expand All @@ -19,6 +22,8 @@ public class ServerBuilder {
private UriRequestHandlerResolver mUriMapper = new UriRequestHandlerResolver();
private Gson mGson = new GsonBuilder().create();
private boolean mIsDebugBuild;
private KeyManager[] mKeyManagers;
private TrustManager[] mTrustManagers;

public ServerBuilder setDebugBuild(boolean isDebug) {
mIsDebugBuild = isDebug;
Expand Down Expand Up @@ -87,7 +92,14 @@ public Server create() {
mRequestHandler = mUriMapper;
}
server.requestHandlerResolver = mRequestHandler;

if (mKeyManagers != null && mTrustManagers != null) {
server.configureSslContext(mKeyManagers, mTrustManagers);
}
return server;
}

public void setSslConfigs(KeyManager[] keyManagers, TrustManager[] trustManagers) {
mKeyManagers = keyManagers;
mTrustManagers = trustManagers;
}
}