Skip to content

Commit

Permalink
Force secure WebSocket connections to use http/1.1
Browse files Browse the repository at this point in the history
Connection manager is updated to use a separate SSLContext for
WebSocket connections that will only advertise http/1.1 in the list
of supported protocols in the ALPN section of the TLS handshake.

WebSocket is not currently supported over HTTP 2 connections, thus
if an HTTP 2 connection is established through ALPN, the subsequent
upgrade to the WebSocket protocol would fail.

This resolves #10744
  • Loading branch information
jeremyg484 committed Apr 23, 2024
1 parent 6a78f0c commit a497838
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.micronaut.core.reflect.InstantiationUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.HttpVersion;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.client.exceptions.HttpClientException;
Expand Down Expand Up @@ -143,6 +144,7 @@ public class ConnectionManager {
private Bootstrap udpBootstrap;
private final HttpClientConfiguration configuration;
private volatile SslContext sslContext;
private volatile SslContext websocketSslContext;
private volatile /* QuicSslContext */ Object http3SslContext;
private final NettyClientCustomizer clientCustomizer;
private final String informationalServiceId;
Expand All @@ -164,6 +166,7 @@ public class ConnectionManager {
this.udpBootstrap = from.udpBootstrap;
this.configuration = from.configuration;
this.sslContext = from.sslContext;
this.websocketSslContext = from.websocketSslContext;
this.http3SslContext = from.http3SslContext;
this.clientCustomizer = from.clientCustomizer;
this.informationalServiceId = from.informationalServiceId;
Expand Down Expand Up @@ -209,10 +212,13 @@ public class ConnectionManager {

final void refresh() {
SslContext oldSslContext = sslContext;
SslContext oldWebsocketSslContext = websocketSslContext;
if (configuration.getSslConfiguration().isEnabled()) {
sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion);
websocketSslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), HttpVersionSelection.forLegacyVersion(HttpVersion.HTTP_1_1));
} else {
sslContext = null;
websocketSslContext = null;
}
if (httpVersion.isHttp3()) {
http3SslContext = nettyClientSslBuilder.buildHttp3(configuration.getSslConfiguration());
Expand All @@ -224,6 +230,7 @@ final void refresh() {
pool.forEachConnection(c -> ((Pool.ConnectionHolder) c).windDownConnection());
}
ReferenceCountUtil.release(oldSslContext);
ReferenceCountUtil.release(oldWebsocketSslContext);
}

/**
Expand Down Expand Up @@ -432,6 +439,26 @@ public final Mono<PoolHandle> connect(DefaultHttpClient.RequestKey requestKey, @
return pools.computeIfAbsent(requestKey, Pool::new).acquire(blockHint);
}

/**
* Builds an {@link SslContext} for the given WebSocket URI if necessary.
*
* @return The {@link SslContext} instance
*/
@Nullable
private SslContext buildWebsocketSslContext(DefaultHttpClient.RequestKey requestKey) {
final SslContext sslCtx;
if (requestKey.isSecure()) {
sslCtx = websocketSslContext;
//Allow wss requests to be sent if SSL is disabled but a proxy is present
if (sslCtx == null && !configuration.getProxyAddress().isPresent()) {
throw decorate(new HttpClientException("Cannot send WSS request. SSL is disabled"));
}
} else {
sslCtx = null;
}
return sslCtx;
}

/**
* Connect to a remote websocket. The given {@link ChannelHandler} is added to the pipeline
* when the handshakes complete.
Expand All @@ -448,7 +475,7 @@ final Mono<?> connectForWebsocket(DefaultHttpClient.RequestKey requestKey, Chann
protected void initChannel(@NonNull Channel ch) {
addLogHandler(ch);

SslContext sslContext = buildSslContext(requestKey);
SslContext sslContext = buildWebsocketSslContext(requestKey);
if (sslContext != null) {
ch.pipeline().addLast(configureSslHandler(sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort())));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.micronaut.websocket.exceptions.WebSocketClientException
import jakarta.inject.Inject
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import spock.lang.Issue
import spock.lang.Specification

import java.util.concurrent.ExecutionException
Expand Down Expand Up @@ -38,6 +39,47 @@ class ClientWebsocketSpec extends Specification {
client.close()
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10744")
void 'websocket bean can connect to echo server over SSL with wss scheme'() {
given:
def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec'])
def client = ctx.getBean(WebSocketClient)
def registry = ctx.getBean(ClientBeanRegistry)
def mono = Mono.from(client.connect(ClientBean.class, 'wss://websocket-echo.com'))

when:
mono.toFuture().get()

then:
registry.clientBeans.size() == 1
registry.clientBeans[0].opened
!registry.clientBeans[0].autoClosed
!registry.clientBeans[0].onClosed

cleanup:
client.close()
}

void 'websocket bean can connect to echo server over SSL with https scheme'() {
given:
def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec'])//, "micronaut.http.client.alpn-modes":"http/1.1"])
def client = ctx.getBean(WebSocketClient)
def registry = ctx.getBean(ClientBeanRegistry)
def mono = Mono.from(client.connect(ClientBean.class, 'https://websocket-echo.com'))

when:
mono.toFuture().get()

then:
registry.clientBeans.size() == 1
registry.clientBeans[0].opened
!registry.clientBeans[0].autoClosed
!registry.clientBeans[0].onClosed

cleanup:
client.close()
}

@Singleton
@Requires(property = 'spec.name', value = 'ClientWebsocketSpec')
static class ClientBeanRegistry {
Expand Down

0 comments on commit a497838

Please sign in to comment.