Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/2 for Netty connector #5300

Draft
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.netty.connector;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.proxy.ProxyHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.innate.ClientProxy;

import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Optional;

import static io.netty.handler.logging.LogLevel.INFO;

class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
private static final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class);

private HttpToHttp2ConnectionHandler connectionHandler;
private Http2SettingsHandler settingsHandler;

private final ClientRequest jerseyRequest;

private Optional<ClientProxy> handlerProxy;

public Http2ClientInitializer(ClientRequest jerseyRequest, Optional<ClientProxy> handlerProxy) {
this.jerseyRequest = jerseyRequest;
this.handlerProxy = handlerProxy;
}

@Override
public void initChannel(SocketChannel ch) throws Exception {
final Http2Connection connection = new DefaultHttp2Connection(false);
connectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
.frameListener(new DelegatingDecompressorFrameListener(
connection,
new InboundHttp2ToHttpAdapterBuilder(connection)
.maxContentLength(jerseyRequest.getLength() > 0 ? jerseyRequest.getLength() : 8192)
.propagateSettings(true)
.build()))
.frameLogger(logger)
.connection(connection)
.build();
settingsHandler = new Http2SettingsHandler(ch.newPromise());

// http proxy
handlerProxy.ifPresent(clientProxy -> {
final URI u = clientProxy.uri();
final InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(),
u.getPort() == -1 ? 8080 : u.getPort());
final Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT,
0);
final ProxyHandler proxy1 = NettyConnector.createProxyHandler(jerseyRequest, proxyAddr,
clientProxy.userName(), clientProxy.password(), connectTimeout);
ch.pipeline().addLast(proxy1);
});

if ("https".equals(jerseyRequest.getUri().getScheme())) {
configureSsl(prepareSslContext(), ch);
} else {
configureClearText(ch);
}
}

private SslContext prepareSslContext() throws SSLException {
final SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
return SslContextBuilder.forClient()
.sslProvider(provider)
/* NOTE: the cipher filter may not include all ciphers required by the HTTP/2 specification.
* Please refer to the HTTP/2 specification for cipher requirements. */
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
}

/**
* Configure the pipeline for TLS NPN negotiation to HTTP/2.
*/
private void configureSsl(SslContext sslCtx, SocketChannel ch) {
final URI requestUri = jerseyRequest.getUri();
final ChannelPipeline pipeline = ch.pipeline();
// Specify Host in SSLContext New Handler to add TLS SNI Extension
pipeline.addLast(sslCtx.newHandler(ch.alloc(), requestUri.getHost(),
requestUri.getPort() <= 0 ? 443 : requestUri.getPort()));
// We must wait for the handshake to finish and the protocol to be negotiated before configuring
// the HTTP/2 components of the pipeline.
pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
final ChannelPipeline p = ctx.pipeline();
p.addLast(connectionHandler, settingsHandler);
return;
}
ctx.close();
throw new IllegalStateException("unknown protocol: " + protocol);
}
});
}

/**
* Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.
*/
private void configureClearText(SocketChannel ch) {
final HttpClientCodec sourceCodec = new HttpClientCodec();
final Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler);
final HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536);

ch.pipeline().addLast(sourceCodec,
upgradeHandler,
new UpgradeRequestHandler(),
new UserEventLogger());
}

/**
* A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request.
*/
private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER);

// Set HOST header as the remote peer may require it.
InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress();
String hostString = remote.getHostString();
if (hostString == null) {
hostString = remote.getAddress().getHostAddress();
}
upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort());

ctx.writeAndFlush(upgradeRequest);

ctx.fireChannelActive();

// Done with this handler, remove it from the pipeline.
ctx.pipeline().remove(this);

ctx.pipeline().addLast(settingsHandler);;
}
}

/**
* Class that logs any User Events triggered on this channel.
*/
private static class UserEventLogger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.netty.connector;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http2.Http2Settings;

public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private final ChannelPromise promise;

/**
* Create new instance
*
* @param promise Promise object used to notify when first settings are received
*/
public Http2SettingsHandler(ChannelPromise promise) {
this.promise = promise;
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();


// Only care about the first settings message
ctx.pipeline().remove(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.Configuration;
Expand Down Expand Up @@ -246,58 +248,7 @@ protected void execute(final ClientRequest jerseyRequest, final Set<URI> redirec

b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();

Configuration config = jerseyRequest.getConfiguration();

// http proxy
handlerProxy.ifPresent(clientProxy -> {
final URI u = clientProxy.uri();
InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(),
u.getPort() == -1 ? 8080 : u.getPort());
ProxyHandler proxy1 = createProxyHandler(jerseyRequest, proxyAddr,
clientProxy.userName(), clientProxy.password(), connectTimeout);
p.addLast(proxy1);
});

// Enable HTTPS if necessary.
if ("https".equals(requestUri.getScheme())) {
// making client authentication optional for now; it could be extracted to configurable property
JdkSslContext jdkSslContext = new JdkSslContext(
client.getSslContext(),
true,
(Iterable) null,
IdentityCipherSuiteFilter.INSTANCE,
(ApplicationProtocolConfig) null,
ClientAuth.NONE,
(String[]) null, /* enable default protocols */
false /* true if the first write request shouldn't be encrypted */
);

final int port = requestUri.getPort();
final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
.request(jerseyRequest).setSNIAlways(true).build();
final SslHandler sslHandler = jdkSslContext.newHandler(
ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
);
if (ClientProperties.getValue(config.getProperties(),
NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) {
sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine());
}

sslConfig.setSNIServerName(sslHandler.engine());

p.addLast(sslHandler);
}

p.addLast(new HttpClientCodec());
p.addLast(new ChunkedWriteHandler());
p.addLast(new HttpContentDecompressor());
}
});
.handler(provideChannelInitializer(jerseyRequest, handlerProxy, connectTimeout, requestUri));

// connect timeout
if (connectTimeout > 0) {
Expand Down Expand Up @@ -439,6 +390,64 @@ public void run() {
}
}

ChannelInitializer provideChannelInitializer(ClientRequest jerseyRequest,
Optional<ClientProxy> handlerProxy,
long connectTimeout,
URI requestUri) {
return new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();

Configuration config = jerseyRequest.getConfiguration();

// http proxy
handlerProxy.ifPresent(clientProxy -> {
final URI u = clientProxy.uri();
InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(),
u.getPort() == -1 ? 8080 : u.getPort());
ProxyHandler proxy1 = createProxyHandler(jerseyRequest, proxyAddr,
clientProxy.userName(), clientProxy.password(), connectTimeout);
p.addLast(proxy1);
});

// Enable HTTPS if necessary.
if ("https".equals(requestUri.getScheme())) {
// making client authentication optional for now; it could be extracted to configurable property
final JdkSslContext jdkSslContext = new JdkSslContext(
client.getSslContext(),
true,
null,
IdentityCipherSuiteFilter.INSTANCE,
null,
ClientAuth.NONE,
null, /* enable default protocols */
false /* true if the first write request shouldn't be encrypted */
);

final int port = requestUri.getPort();
final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
.request(jerseyRequest).setSNIAlways(true).build();
final SslHandler sslHandler = jdkSslContext.newHandler(
ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
);
if (ClientProperties.getValue(config.getProperties(),
NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) {
sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine());
}

sslConfig.setSNIServerName(sslHandler.engine());

p.addLast(sslHandler);
}

p.addLast(new HttpClientCodec());
p.addLast(new ChunkedWriteHandler());
p.addLast(new HttpContentDecompressor());
}
};
}

private String buildPathWithQueryParameters(URI requestUri) {
if (requestUri.getRawQuery() != null) {
return String.format("%s?%s", requestUri.getRawPath(), requestUri.getRawQuery());
Expand Down Expand Up @@ -489,7 +498,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
}
}

private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
protected static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
String userName, String password, long connectTimeout) {
HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders());

Expand Down
Loading