diff --git a/miniweb-core/src/main/java/com/devsmart/miniweb/Server.java b/miniweb-core/src/main/java/com/devsmart/miniweb/Server.java index d581dfd..a9f921d 100644 --- a/miniweb-core/src/main/java/com/devsmart/miniweb/Server.java +++ b/miniweb-core/src/main/java/com/devsmart/miniweb/Server.java @@ -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); @@ -44,6 +53,20 @@ 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) { @@ -51,9 +74,32 @@ public void start() throws IOException { 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()); } @@ -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"); } @@ -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); + } } } } @@ -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(); } } } diff --git a/miniweb-core/src/main/java/com/devsmart/miniweb/ServerBuilder.java b/miniweb-core/src/main/java/com/devsmart/miniweb/ServerBuilder.java index e1be5fb..e32c8a0 100644 --- a/miniweb-core/src/main/java/com/devsmart/miniweb/ServerBuilder.java +++ b/miniweb-core/src/main/java/com/devsmart/miniweb/ServerBuilder.java @@ -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; @@ -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; @@ -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; + } }