diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2ClientInitializer.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2ClientInitializer.java new file mode 100644 index 0000000000..2c50ca8a07 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2ClientInitializer.java @@ -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 { + private static final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class); + + private HttpToHttp2ConnectionHandler connectionHandler; + private Http2SettingsHandler settingsHandler; + + private final ClientRequest jerseyRequest; + + private Optional handlerProxy; + + public Http2ClientInitializer(ClientRequest jerseyRequest, Optional 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); + } + } +} \ No newline at end of file diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2SettingsHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2SettingsHandler.java new file mode 100644 index 0000000000..ded02b4360 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Http2SettingsHandler.java @@ -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 { + 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); + } +} \ No newline at end of file diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index 9406b51f15..7556a6fffb 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -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; @@ -246,58 +248,7 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec b.group(group) .channel(NioSocketChannel.class) - .handler(new ChannelInitializer() { - @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) { @@ -439,6 +390,64 @@ public void run() { } } + ChannelInitializer provideChannelInitializer(ClientRequest jerseyRequest, + Optional handlerProxy, + long connectTimeout, + URI requestUri) { + return new ChannelInitializer() { + @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()); @@ -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()); diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2Connector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2Connector.java new file mode 100644 index 0000000000..3220542924 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2Connector.java @@ -0,0 +1,41 @@ +/* + * 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.ChannelInitializer; +import javax.ws.rs.client.Client; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.innate.ClientProxy; + +import java.net.URI; +import java.util.Optional; + +public class NettyHttp2Connector extends NettyConnector { + + NettyHttp2Connector(Client client) { + super(client); + } + + + @Override + public ChannelInitializer provideChannelInitializer(ClientRequest jerseyRequest, + Optional handlerProxy, + long connectTimeout, + URI requestUri) { + return new Http2ClientInitializer(jerseyRequest, handlerProxy); + } +} diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2ConnectorProvider.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2ConnectorProvider.java new file mode 100644 index 0000000000..c4ef37fadd --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttp2ConnectorProvider.java @@ -0,0 +1,31 @@ +/* + * 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 javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import org.glassfish.jersey.Beta; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +@Beta +public class NettyHttp2ConnectorProvider implements ConnectorProvider { + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return new NettyHttp2Connector(client); + } +} diff --git a/pom.xml b/pom.xml index 395d8b3f6e..107c77195d 100644 --- a/pom.xml +++ b/pom.xml @@ -2228,7 +2228,7 @@ 4.0.2 3.12.4 0.9.10 - 4.1.79.Final + 4.1.90.Final 0.33.0 6.0.0 1.10.0