diff --git a/src/main/java/io/fusionauth/http/server/HTTPListenerConfiguration.java b/src/main/java/io/fusionauth/http/server/HTTPListenerConfiguration.java index 6e62256..7a18613 100644 --- a/src/main/java/io/fusionauth/http/server/HTTPListenerConfiguration.java +++ b/src/main/java/io/fusionauth/http/server/HTTPListenerConfiguration.java @@ -15,6 +15,8 @@ */ package io.fusionauth.http.server; +import javax.net.ServerSocketFactory; + import java.net.Inet6Address; import java.net.InetAddress; import java.net.InterfaceAddress; @@ -44,6 +46,8 @@ public class HTTPListenerConfiguration { private final boolean tls; + private final ServerSocketFactory serverSocketFactory; + /** * Stores the configuration for a single HTTP listener for the server. This constructor sets up a non-TLS based listener that binds to all * the network interfaces of the server. @@ -56,6 +60,7 @@ public HTTPListenerConfiguration(int port) { this.tls = false; this.certificateChain = null; this.privateKey = null; + this.serverSocketFactory = null; } /** @@ -75,6 +80,7 @@ public HTTPListenerConfiguration(int port, String certificate, String privateKey this.tls = true; this.certificateChain = SecurityTools.parseCertificates(certificate); this.privateKey = SecurityTools.parsePrivateKey(privateKey); + this.serverSocketFactory = null; } /** @@ -93,6 +99,7 @@ public HTTPListenerConfiguration(int port, Certificate certificate, PrivateKey p this.tls = true; this.certificateChain = new Certificate[]{certificate}; this.privateKey = privateKey; + this.serverSocketFactory = null; } /** @@ -112,6 +119,7 @@ public HTTPListenerConfiguration(int port, Certificate[] certificateChain, Priva this.tls = true; this.certificateChain = certificateChain; this.privateKey = privateKey; + this.serverSocketFactory = null; } /** @@ -128,6 +136,7 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port) { this.tls = false; this.certificateChain = null; this.privateKey = null; + this.serverSocketFactory = null; } /** @@ -150,6 +159,7 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port, String certi this.tls = true; this.certificateChain = SecurityTools.parseCertificates(certificate); this.privateKey = SecurityTools.parsePrivateKey(privateKey); + this.serverSocketFactory = null; } /** @@ -170,6 +180,59 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port, Certificate this.tls = true; this.certificateChain = new Certificate[]{certificate}; this.privateKey = privateKey; + this.serverSocketFactory = null; + } + + /** + * Stores the configuration for a listener that uses a custom server socket factory. + * + * @param bindAddress The bind address of this listener. + * @param port The port of this listener. + * @param tls Whether the custom factory provides a TLS-enabled server socket. + * @param serverSocketFactory The custom factory used to create an unbound server socket. + */ + public HTTPListenerConfiguration(InetAddress bindAddress, int port, boolean tls, ServerSocketFactory serverSocketFactory) { + Objects.requireNonNull(bindAddress); + Objects.requireNonNull(serverSocketFactory); + + this.bindAddress = bindAddress; + this.port = port; + this.tls = tls; + this.certificateChain = null; + this.privateKey = null; + this.serverSocketFactory = serverSocketFactory; + } + + /** + * Stores the configuration for a non-TLS listener that uses a custom server socket factory. + * + * @param bindAddress The bind address of this listener. + * @param port The port of this listener. + * @param serverSocketFactory The custom factory used to create an unbound server socket. + */ + public HTTPListenerConfiguration(InetAddress bindAddress, int port, ServerSocketFactory serverSocketFactory) { + this(bindAddress, port, false, serverSocketFactory); + } + + /** + * Stores the configuration for a non-TLS listener that uses a custom server socket factory and binds to all network interfaces. + * + * @param port The port of this listener. + * @param serverSocketFactory The custom factory used to create an unbound server socket. + */ + public HTTPListenerConfiguration(int port, ServerSocketFactory serverSocketFactory) { + this(allInterfaces(), port, false, serverSocketFactory); + } + + /** + * Stores the configuration for a listener that uses a custom server socket factory and binds to all network interfaces. + * + * @param port The port of this listener. + * @param tls Whether the custom factory provides a TLS-enabled server socket. + * @param serverSocketFactory The custom factory used to create an unbound server socket. + */ + public HTTPListenerConfiguration(int port, boolean tls, ServerSocketFactory serverSocketFactory) { + this(allInterfaces(), port, tls, serverSocketFactory); } public InetAddress getBindAddress() { @@ -200,7 +263,14 @@ public boolean isTLS() { return tls; } - private InetAddress allInterfaces() { + /** + * @return The custom server socket factory, or null when the server should use its default factory. + */ + public ServerSocketFactory getServerSocketFactory() { + return serverSocketFactory; + } + + private static InetAddress allInterfaces() { try { boolean ipv6Supported = Collections.list(NetworkInterface.getNetworkInterfaces()).stream() .map(NetworkInterface::getInterfaceAddresses) diff --git a/src/main/java/io/fusionauth/http/server/internal/HTTPServerThread.java b/src/main/java/io/fusionauth/http/server/internal/HTTPServerThread.java index bfc245f..8099a78 100644 --- a/src/main/java/io/fusionauth/http/server/internal/HTTPServerThread.java +++ b/src/main/java/io/fusionauth/http/server/internal/HTTPServerThread.java @@ -74,7 +74,9 @@ public HTTPServerThread(HTTPServerConfiguration configuration, HTTPListenerConfi this.minimumWriteThroughput = configuration.getMinimumWriteThroughput(); this.cleaner = new HTTPServerCleanerThread(); - if (listener.isTLS()) { + if (listener.getServerSocketFactory() != null) { + this.socket = listener.getServerSocketFactory().createServerSocket(); + } else if (listener.isTLS()) { SSLContext context = SecurityTools.serverContext(listener.getCertificateChain(), listener.getPrivateKey()); this.socket = context.getServerSocketFactory().createServerSocket(); } else { diff --git a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java index b77e1da..0a5302c 100644 --- a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java +++ b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java @@ -116,7 +116,7 @@ public void run() { while (true) { logger.trace("[{}] Running HTTP worker. Block while we wait to read the preamble", Thread.currentThread().threadId()); - request = new HTTPRequest(configuration.getContextPath(), listener.getCertificate() != null ? "https" : "http", listener.getPort(), socket.getInetAddress().getHostAddress()); + request = new HTTPRequest(configuration.getContextPath(), listener.isTLS() ? "https" : "http", listener.getPort(), socket.getInetAddress().getHostAddress()); // Create a deep copy of the MultipartConfiguration so that the request may optionally modify the configuration on a per-request basis. request.getMultiPartStreamProcessor().setMultipartConfiguration(new MultipartConfiguration(configuration.getMultipartConfiguration())); diff --git a/src/test/java/io/fusionauth/http/server/internal/HTTPServerThreadTest.java b/src/test/java/io/fusionauth/http/server/internal/HTTPServerThreadTest.java new file mode 100644 index 0000000..0604b8c --- /dev/null +++ b/src/test/java/io/fusionauth/http/server/internal/HTTPServerThreadTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, FusionAuth, All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fusionauth.http.server.internal; + +import javax.net.ServerSocketFactory; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.concurrent.atomic.AtomicInteger; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +import io.fusionauth.http.server.HTTPListenerConfiguration; +import io.fusionauth.http.server.HTTPServerConfiguration; + +/** Tests for custom server socket factories. */ +public class HTTPServerThreadTest { + @Test + public void customServerSocketFactoryIsUsedForTLSListener() throws Exception { + assertCustomServerSocketFactoryIsUsed(true); + } + + @Test + public void customServerSocketFactoryIsUsedForPlainListener() throws Exception { + assertCustomServerSocketFactoryIsUsed(false); + } + + private void assertCustomServerSocketFactoryIsUsed(boolean tls) throws Exception { + AtomicInteger createCalls = new AtomicInteger(); + ServerSocketFactory factory = new ServerSocketFactory() { + @Override + public ServerSocket createServerSocket() throws IOException { + createCalls.incrementAndGet(); + return new ServerSocket(); + } + + @Override + public ServerSocket createServerSocket(int port) { + throw new UnsupportedOperationException(); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog) { + throw new UnsupportedOperationException(); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog, InetAddress bindAddress) { + throw new UnsupportedOperationException(); + } + }; + + HTTPListenerConfiguration listener = new HTTPListenerConfiguration(InetAddress.getLoopbackAddress(), 0, tls, factory); + HTTPServerThread serverThread = new HTTPServerThread(new HTTPServerConfiguration(), listener); + try { + assertEquals(listener.isTLS(), tls); + assertEquals(listener.getServerSocketFactory(), factory); + assertEquals(createCalls.get(), 1); + } finally { + serverThread.shutdown(); + } + } +}