Skip to content
Merged
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
20 changes: 20 additions & 0 deletions src/main/java/io/muserver/HAProxyMessageHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.muserver;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.util.AttributeKey;

class HAProxyMessageHandler extends SimpleChannelInboundHandler<HAProxyMessage> {

static final AttributeKey<ProxiedConnectionInfo> HA_PROXY_INFO = AttributeKey.valueOf("HA_PROXY_INFO");

@Override
protected void channelRead0(ChannelHandlerContext ctx, HAProxyMessage msg) {
ProxiedConnectionInfoImpl proxyConnectionInfo = ProxiedConnectionInfoImpl.fromNetty(msg);
ctx.channel().attr(HA_PROXY_INFO).set(proxyConnectionInfo);
if (!ctx.channel().config().isAutoRead()) {
ctx.read();
Copy link
Collaborator Author

@jaylu jaylu Feb 25, 2026

Choose a reason for hiding this comment

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

one thing to highlight, for JDK 8, if not doing this extra ctx.read(), it will randomly failed the text - client request timeout.

}
}
}
19 changes: 9 additions & 10 deletions src/main/java/io/muserver/Http1Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleState;
Expand Down Expand Up @@ -38,7 +37,6 @@ class Http1Connection extends SimpleChannelInboundHandler<Object> implements Htt
private ChannelHandlerContext nettyCtx;
private InetSocketAddress remoteAddress;
private Exchange currentExchange = null;
private ProxiedConnectionInfo proxyInfo;

Http1Connection(NettyHandlerAdapter nettyHandlerAdapter, MuServerImpl server, String proto) {
this.nettyHandlerAdapter = nettyHandlerAdapter;
Expand All @@ -48,7 +46,7 @@ class Http1Connection extends SimpleChannelInboundHandler<Object> implements Htt
}

static SSLSession getSslSession(ChannelHandlerContext ctx) {
SslHandler ssl = (SslHandler) ctx.channel().pipeline().get("ssl");
SslHandler ssl = (SslHandler) ctx.channel().pipeline().get(SslHandler.class.getName());
return ssl.engine().getSession();
}

Expand Down Expand Up @@ -83,11 +81,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
}

private void onChannelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HAProxyMessage) {
HAProxyMessage hap = (HAProxyMessage) msg;
this.proxyInfo = ProxiedConnectionInfoImpl.fromNetty(hap);
ctx.channel().read();
} else if (msg instanceof HttpRequest) {
if (msg instanceof HttpRequest) {
try {
this.currentExchange = HttpExchange.create(server, proto, ctx, this, (HttpRequest) msg,
nettyHandlerAdapter, connectionStats,
Expand Down Expand Up @@ -287,12 +281,17 @@ public Optional<Certificate> clientCertificate() {

@Override
public Optional<ProxiedConnectionInfo> proxyInfo() {
return Optional.ofNullable(proxyInfo);
return Optional.ofNullable(nettyCtx.channel().attr(HAProxyMessageHandler.HA_PROXY_INFO).get());
}

@Override
public Optional<String> sniHostName() {
return Optional.ofNullable(nettyCtx.channel().attr(MuSniHandler.SNI_HOSTNAME).get());
}

static Optional<Certificate> fromContext(ChannelHandlerContext channelHandlerContext) {
try {
SslHandler sslhandler = (SslHandler) channelHandlerContext.channel().pipeline().get("ssl");
SslHandler sslhandler = (SslHandler) channelHandlerContext.channel().pipeline().get(SslHandler.class.getName());
if (sslhandler == null) {
return Optional.empty();
}
Expand Down
17 changes: 6 additions & 11 deletions src/main/java/io/muserver/Http2Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.*;
import io.netty.handler.timeout.IdleState;
Expand Down Expand Up @@ -144,15 +143,6 @@ final class Http2Connection extends Http2ConnectionFlowControl implements HttpCo
this.nettyHandlerAdapter = nettyHandlerAdapter;
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HAProxyMessage) {
HAProxyMessage hap = (HAProxyMessage) msg;
this.proxyInfo = ProxiedConnectionInfoImpl.fromNetty(hap);
}
super.channelRead(ctx, msg);
}

@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
server.stats.onConnectionOpened();
Expand Down Expand Up @@ -566,7 +556,12 @@ public Optional<Certificate> clientCertificate() {

@Override
public Optional<ProxiedConnectionInfo> proxyInfo() {
return Optional.ofNullable(proxyInfo);
return Optional.ofNullable(this.nettyContext.channel().attr(HAProxyMessageHandler.HA_PROXY_INFO).get());
}

@Override
public Optional<String> sniHostName() {
return Optional.ofNullable(this.nettyContext.channel().attr(MuSniHandler.SNI_HOSTNAME).get());
}

}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/io/muserver/HttpConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,10 @@ public interface HttpConnection {
*/
Optional<ProxiedConnectionInfo> proxyInfo();

/**
* Gets the SNI host name that the client sent during the TLS handshake, if any.
* @return SNI host name sent by the client, or {@link Optional#empty()} if none specified or if the connection is not over HTTPS.
*/
Optional<String> sniHostName();

}
20 changes: 10 additions & 10 deletions src/main/java/io/muserver/MuServerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@
import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
import io.netty.handler.flow.FlowControlHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.DomainWildcardMappingBuilder;
import io.netty.util.HashedWheelTimer;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLParameters;
import java.net.InetSocketAddress;
import java.net.URI;
import java.time.Duration;
Expand All @@ -39,6 +38,7 @@
* <p>Use the <code>withXXX()</code> methods to set the ports, config, and request handlers needed.</p>
*/
public class MuServerBuilder {

static {
MuRuntimeDelegate.ensureSet();
}
Expand Down Expand Up @@ -409,6 +409,7 @@ public MuServerBuilder withRateLimiter(RateLimitSelector selector) {
* return true;
* })
* </code></pre>
*
* @param exceptionHandler The handler to be called when an unhandled exception is encountered
* @return This builder
*/
Expand Down Expand Up @@ -542,7 +543,9 @@ public long maxRequestSize() {
/**
* @return The current value of this property
*/
public boolean haProxyProtocolEnabled() { return this.haProxyProtocolEnabled; }
public boolean haProxyProtocolEnabled() {
return this.haProxyProtocolEnabled;
}

/**
* @return The current value of this property
Expand Down Expand Up @@ -596,6 +599,7 @@ public static MuServerBuilder httpsServer() {
/**
* If enabled, then <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">HA Proxy Protocol</a>
* parsing on new connections will be enabled.
*
* @param enabled <code>true</code> to enable. The default is <code>false</code>
* @return This builder
*/
Expand Down Expand Up @@ -730,15 +734,11 @@ protected void initChannel(SocketChannel socketChannel) {
p.addLast("idle", new IdleStateHandler(0, 0, idleTimeoutMills, TimeUnit.MILLISECONDS));
p.addLast(trafficShapingHandler);
if (haProxyProtocolEnabled) {
HAProxyMessageDecoder haProxyMessageDecoder = new HAProxyMessageDecoder();
p.addLast("HAProxyMessageDecoder", haProxyMessageDecoder);
p.addLast("HAProxyMessageDecoder", new HAProxyMessageDecoder());
p.addLast("HAProxyMessageHandler", new HAProxyMessageHandler());
}
if (usesSsl) {
SslHandler sslHandler = sslContextProvider.get().newHandler(socketChannel.alloc());
SSLParameters params = sslHandler.engine().getSSLParameters();
params.setUseCipherSuitesOrder(true);
sslHandler.engine().setSSLParameters(params);
p.addLast("ssl", sslHandler);
p.addLast("sni", new MuSniHandler(() -> new DomainWildcardMappingBuilder<>(sslContextProvider.get()).build()));
}
boolean addAlpn = http2 && usesSsl;
if (addAlpn) {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/io/muserver/MuSniHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.muserver;

import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import io.netty.util.Mapping;

import javax.net.ssl.SSLParameters;
import java.util.function.Supplier;


class MuSniHandler extends SniHandler {

static final AttributeKey<String> SNI_HOSTNAME = AttributeKey.valueOf("SNI_HOSTNAME");

public MuSniHandler(Supplier<Mapping<? super String, ? extends SslContext>> mappingProvider) {
super(mappingProvider.get());
}

@Override
protected void replaceHandler(ChannelHandlerContext ctx, String hostname, SslContext sslContext) throws Exception {
ctx.channel().attr(SNI_HOSTNAME).set(hostname());
super.replaceHandler(ctx, hostname, sslContext);
}

@Override
protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) {
SslHandler sslHandler = context.newHandler(allocator);
SSLParameters params = sslHandler.engine().getSSLParameters();
params.setUseCipherSuitesOrder(true);
sslHandler.engine().setSSLParameters(params);
return sslHandler;
}
}
69 changes: 69 additions & 0 deletions src/test/java/io/muserver/HttpsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
Expand Down Expand Up @@ -122,9 +124,76 @@ private String certInformationBySni(URI uri, String sni) throws IOException {
sb.append(x.getIssuerDN());
}
}
conn.disconnect();
return sb.toString();
}

@Test
public void canGetClientSniOnHttpsServer() throws Exception {

/* jks-keystore-combine.jks generated with:
keytool -genkeypair -keystore jks-keystore-combine.jks -storetype JKS -alias mykey-1 -storepass MY_PASSWORD -keypass MY_PASSWORD -keyalg RSA -keysize 2048 -validity 999999 -dname "CN=TEST-1, OU=Ronin, O=MuServer, L=NA, ST=NA, C=NA" -ext san=dns:test-1.com,dns:localhost,ip:127.0.0.1
keytool -genkeypair -keystore jks-keystore-temp-2.jks -storetype JKS -alias mykey-2 -storepass MY_PASSWORD -keypass MY_PASSWORD -keyalg RSA -keysize 2048 -validity 999999 -dname "CN=TEST-2, OU=Ronin, O=MuServer, L=NA, ST=NA, C=NA" -ext san=dns:test-2.com,ip:127.0.0.1
keytool -importkeystore -srckeystore jks-keystore-temp-2.jks -destkeystore jks-keystore-combine.jks -srcalias mykey-2 -destalias mykey-2 -srcstorepass MY_PASSWORD -deststorepass MY_PASSWORD
*/
server = ServerUtils.httpsServerForTest()
.withHttpsConfig(HttpsConfigBuilder.httpsConfig()
.withKeystoreType("JKS")
.withKeystorePassword("MY_PASSWORD")
.withKeyPassword("MY_PASSWORD")
.withKeystoreFromClasspath("/jks-keystore-combine.jks"))
.addHandler((request, response) -> {
response.write("This is encrypted, sni is " + request.connection().sniHostName().orElse("Optional.empty()"));
return true;
})
.start();

// when client not specify the sni, it's empty
try (Response resp = call(request(server.httpsUri()))) {
assertThat(resp.body().string(), equalTo("This is encrypted, sni is Optional.empty()"));
}

assertThat(responseBySni(server.uri(), "localhost"), equalTo("200 This is encrypted, sni is localhost"));
assertThat(responseBySni(server.uri(), "test-1.com"), equalTo("200 This is encrypted, sni is test-1.com"));
assertThat(responseBySni(server.uri(), "test-2.com"), equalTo("200 This is encrypted, sni is test-2.com"));
assertThat(responseBySni(server.uri(), "not-matching"), equalTo("200 This is encrypted, sni is not-matching"));
}

@Test
public void canGetEmptyClientSniOnHttpServer() throws Exception {

server = MuServerBuilder.httpServer()
.addHandler((request, response) -> {
response.write("This is encrypted, sni is " + request.connection().sniHostName().orElse("Optional.empty()"));
return true;
})
.start();

try (Response resp = call(request(server.httpUri()))) {
assertThat(resp.body().string(), equalTo("This is encrypted, sni is Optional.empty()"));
}
}

private String responseBySni(URI uri, String sni) throws IOException {
SSLContext sslContext = sslContextForTesting(veryTrustingTrustManager());
SSLParameters sslParameters = new SSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(sni)));
HttpsURLConnection.setDefaultSSLSocketFactory(new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters));
HttpsURLConnection conn = (HttpsURLConnection) uri.toURL().openConnection();
conn.setHostnameVerifier((hostname, session) -> true); // disable the client side handshake verification
conn.connect();
int responseCode = conn.getResponseCode();
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())) ) {
String inputLine;
StringBuilder body = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
body.append(inputLine);
}
conn.disconnect();
return responseCode + " " + body;
}
}

@Test
public void certsCanBeChangedWhileRunning() throws Exception {

Expand Down