diff --git a/dev/com.ibm.ws.transport.http/bnd.bnd b/dev/com.ibm.ws.transport.http/bnd.bnd index 4f1288809bfe..a52e39bcd9c7 100644 --- a/dev/com.ibm.ws.transport.http/bnd.bnd +++ b/dev/com.ibm.ws.transport.http/bnd.bnd @@ -59,6 +59,7 @@ Private-Package: \ com.ibm.ws.tcpchannel.internal.resources, \ com.ibm.ws.genericbnf.internal, \ io.openliberty.http.options, \ + io.openliberty.http.pipeline.configurators, \ io.openliberty.http.utils -dsannotations: com.ibm.ws.http.channel.internal.inbound.HttpPipelineEventHandler,\ diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java index 42a766c5c5af..a4e1745d49ac 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java @@ -66,6 +66,7 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; +import io.openliberty.http.pipeline.configurators.PipelineHandlerUtility; /** * @@ -883,7 +884,7 @@ public void pushNewRequest(Http2PushBuilder pushBuilder) { @Override public void run() { try { - ((HttpDispatcherHandler) nettyContext.channel().pipeline().get(HttpPipelineInitializer.HTTP_DISPATCHER_HANDLER_NAME)).channelRead(nettyContext, + ((HttpDispatcherHandler) nettyContext.channel().pipeline().get(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME)).channelRead(nettyContext, newRequest); } catch (Exception e) { diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/HttpPipelineInitializer.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/HttpPipelineInitializer.java index c828c06b4b1c..0cdcb02dff28 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/HttpPipelineInitializer.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/HttpPipelineInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 IBM Corporation and others. + * Copyright (c) 2023, 2024 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -53,50 +53,51 @@ import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.ReferenceCountUtil; +import io.openliberty.http.pipeline.configurators.H2CConfigurator; +import io.openliberty.http.pipeline.configurators.H2Configurator; +import io.openliberty.http.pipeline.configurators.Http11Configurator; +import io.openliberty.http.pipeline.configurators.Https11Configurator; +import io.openliberty.http.pipeline.configurators.PipelineConfigurator; import io.openliberty.netty.internal.ChannelInitializerWrapper; import io.openliberty.netty.internal.exception.NettyException; import io.openliberty.netty.internal.impl.NettyConstants; import io.openliberty.netty.internal.tls.NettyTlsProvider; /** - * Initializes a Netty Pipeline for an HTTP Endpoint. Configuration options may be - * passed into it. + * HttpPipelineInitializer selects and applies the appropriate pipeline configuration + * for a given channel based on whether the connection is HTTP/1.1, HTTPS, HTTP/2, or H2C. + * + * Responsibilities: + * - Initialize the basic pipeline (via the chain's bootstrap initializer). + * - Determine protocol (HTTP or HTTPS, HTTP/1.1 or HTTP/2) and select the right configurator. + * - Provide necessary configuration maps, including sslOptions, to the chosen configurator. + * - Keep logic focused on selection and delegation, letting protocol-specific configurators + * handle the actual handler arrangement. */ public class HttpPipelineInitializer extends ChannelInitializerWrapper { private static final TraceComponent tc = Tr.register(HttpPipelineInitializer.class, HttpMessages.HTTP_TRACE_NAME, HttpMessages.HTTP_BUNDLE); - public enum ConfigElement { - HTTP_OPTIONS, - SSL_OPTIONS, - REMOTE_IP, - COMPRESSION, - SAMESITE, - HEADERS, - ACCESS_LOG - } - private final NettyChain chain; private final NettyHttpChannelConfig httpConfig; + private final NettyTlsProvider tlsProvider; + private final EndPointInfo endpointInfo; private final Map> configOptions; - public static final String NO_UPGRADE_OCURRED_HANDLER_NAME = "UPGRADE_HANDLER_CHECK"; - public static final String NETTY_HTTP_SERVER_CODEC = "HTTP_SERVER_HANDLER"; - public static final String HTTP_DISPATCHER_HANDLER_NAME = "HTTP_DISPATCHER"; - public static final String HTTP_SSL_HANDLER_NAME = "SSL_HANDLER"; - public static final String HTTP_ALPN_HANDLER_NAME = "ALPN_HANDLER"; - public static final String HTTP_KEEP_ALIVE_HANDLER_NAME = "httpKeepAlive"; - public static final String HTTP_AGGREGATOR_HANDLER_NAME = "LIBERTY_OBJECT_AGGREGATOR"; - public static final String HTTP_REQUEST_HANDLER_NAME = "LIBERTY_REQUEST_HANDLER"; - public static final String HTTP2_CLEARTEXT_UPGRADE_HANDLER_NAME = "H2C_UPGRADE_HANDLER"; - public static final String CRLF_VALIDATION_HANDLER = "CRLFValidationHandler"; - - public static final long maxContentLength = Long.MAX_VALUE; - - private HttpPipelineInitializer(NettyChain chain, NettyHttpChannelConfig httpConfig, Map> configOptions) { + /** + * Constructor for HttpPipelineInitializer. + * + * @param chain The NettyChain representing this connection flow. + * @param httpConfig The HTTP channel configuration. + * @param configOptions The configuration map keyed by ConfigElement that may include SSL_OPTIONS, etc. + */ + HttpPipelineInitializer(NettyChain chain, NettyHttpChannelConfig httpConfig, + Map> configOptions) { this.chain = chain; this.httpConfig = httpConfig; this.configOptions = configOptions; + this.tlsProvider = chain.getOwner().getNettyTlsProvider(); + this.endpointInfo = chain.getEndpointInfo(); httpConfig.registerAccessLog(chain.getOwner().getName()); } @@ -104,204 +105,70 @@ private HttpPipelineInitializer(NettyChain chain, NettyHttpChannelConfig httpCon @Override protected void initChannel(Channel channel) throws Exception { Tr.entry(tc, "initChannel"); - ChannelPipeline pipeline = channel.pipeline(); - - // Initialize with the parent bootstrap initializer this.chain.getBootstrap().getBaseInitializer().init(channel); + // Set channel attributes to mark inbound/outbound nature and endpoint PID channel.attr(NettyHttpConstants.IS_OUTBOUND_KEY).set(false); channel.attr(NettyHttpConstants.ENDPOINT_PID).set(chain.getEndpointPID()); + // Setup logging for receive buffers RecvByteBufAllocator channelAllocator = channel.config().getRecvByteBufAllocator(); LoggingRecvByteBufAllocator loggingAllocator = new LoggingRecvByteBufAllocator(channelAllocator); channel.config().setRecvByteBufAllocator(loggingAllocator); pipeline.addLast("AllocatorContextSetter", new AllocatorContextSetter(loggingAllocator)); - if(chain.isHttps()){ - setupSecurePipeline(pipeline); - } else { - setupUnsecurePipeline(pipeline); - } + // Remove any default inactivity timeout handler if present pipeline.remove(NettyConstants.INACTIVITY_TIMEOUT_HANDLER_NAME); - Tr.exit(tc, "initChannel"); - } - - private void setupSecurePipeline(ChannelPipeline pipeline) throws NettyException{ - if(chain.isHttp2Enabled()){ - setupH2Pipeline(pipeline); - } else { - setupHttpsPipeline(pipeline); - } - } - - private void setupUnsecurePipeline(ChannelPipeline pipeline) { - if(chain.isHttp2Enabled()){ - setupH2cPipeline(pipeline); - } else { - setupHttp11Pipeline(pipeline); - } - } - - private void setupH2Pipeline(ChannelPipeline pipeline) throws NettyException { - - SslContext context = getSslContext(); - SSLEngine engine = context.newEngine(pipeline.channel().alloc()); - - pipeline.addFirst(HTTP_SSL_HANDLER_NAME, new LibertySslHandler(engine, httpConfig)); - addPreHttpCodecHandlers(pipeline); - pipeline.addLast(HTTP_ALPN_HANDLER_NAME, new LibertyNettyALPNHandler(httpConfig)); - pipeline.addLast(HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); - addPreDispatcherHandlers(pipeline, true); - pipeline.channel().attr(NettyHttpConstants.IS_SECURE).set(Boolean.TRUE); - // Turn off half closure with H2 - pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, false); - } - - private void setupHttpsPipeline(ChannelPipeline pipeline) throws NettyException { - SslContext context = getSslContext(); - SSLEngine engine = context.newEngine(pipeline.channel().alloc()); - - pipeline.addFirst(HTTP_SSL_HANDLER_NAME, new LibertySslHandler(engine, httpConfig)); - pipeline.channel().attr(NettyHttpConstants.IS_SECURE).set(Boolean.TRUE); - setupHttp11Pipeline(pipeline); - } - - private SslContext getSslContext() throws NettyException { - NettyTlsProvider tlsProvider = chain.getOwner().getNettyTlsProvider(); - if(tlsProvider == null){ - throw new NettyException("TLS Provider is not loaded"); - } - EndPointInfo ep = this.chain.getEndpointInfo(); - String host = ep.getHost(); - String port = Integer.toString(ep.getPort()); - - SslContext context = chain.isHttp2Enabled() ? - tlsProvider.getInboundALPNSSLContext(configOptions.get(ConfigElement.SSL_OPTIONS), host, port) - : tlsProvider.getInboundSSLContext(configOptions.get(ConfigElement.SSL_OPTIONS), host, port); - if (context == null) { - throw new NettyException("Failed to create SSL context for endpoint: " + ep.getHost() + ":" + ep.getPort()); - } - - return context; - } - - - - /** - * Utility method for building and H2C pipeline - * - * @param pipeline ChannelPipeline to update as necessary - */ - - private void setupH2cPipeline(ChannelPipeline pipeline) { - pipeline.addLast(HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); - addPreHttpCodecHandlers(pipeline); - addH2CCodecHandlers(pipeline); - addPreDispatcherHandlers(pipeline, true); - // Turn off half closure with H2 - pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, false); - } + // Determine the protocol scenario and pick the appropriate configurator + PipelineConfigurator configurator = selectConfigurator(); - /** - * Utility method for building an HTTP1.1 pipeline - * - * @param pipeline ChannelPipeline to update as necessary - */ - private void setupHttp11Pipeline(ChannelPipeline pipeline) { - - //TODO: check for best default first line max size (changing for jwt test) - HttpServerCodec sourceCodec = new HttpServerCodec(8192, httpConfig.getIncomingBodyBufferSize(), httpConfig.getLimitOfFieldSize(), httpConfig.getLimitOnNumberOfHeaders()); - pipeline.addLast(CRLF_VALIDATION_HANDLER, new CRLFValidationHandler()); - pipeline.addLast(NETTY_HTTP_SERVER_CODEC, sourceCodec); - pipeline.addLast(HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); - addPreHttpCodecHandlers(pipeline); - addPreDispatcherHandlers(pipeline, false); + // Configure the pipeline using the chosen configurator + configurator.configure(pipeline); } /** - * Utility method for adding the Netty handlers needed for h2c connections + * Selects the appropriate PipelineConfigurator based on whether the connection is HTTPS and/or HTTP/2. * - * @param pipeline ChannelPipeline to update as necessary + * @return The correct PipelineConfigurator for the current protocol scenario. + * @throws NettyException if TLS is required but tlsProvider is not available, or if other config issues arise. */ - private void addH2CCodecHandlers(ChannelPipeline pipeline) { - final CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = LibertyUpgradeCodec.createCleartextUpgradeHandler(httpConfig, pipeline.channel()); - - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, HTTP2_CLEARTEXT_UPGRADE_HANDLER_NAME, cleartextHttp2ServerUpgradeHandler); - - // Handler to decide if an upgrade occurred or not and to add HTTP1 handlers on top - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, NO_UPGRADE_OCURRED_HANDLER_NAME, new SimpleChannelInboundHandler() { - @Override - protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { - if ("HTTP2".equals(ctx.pipeline().channel().attr(NettyHttpConstants.PROTOCOL).get())) { - - ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); - return; - } - // Turn on half closure for H1 - ctx.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); - - - - pipeline.addBefore("chunkWriteHandler", HTTP_KEEP_ALIVE_HANDLER_NAME, new HttpServerKeepAliveHandler()); - //TODO: this is a very large number, check best practice - pipeline.addAfter(HTTP_KEEP_ALIVE_HANDLER_NAME, HTTP_AGGREGATOR_HANDLER_NAME, - new LibertyHttpObjectAggregator(httpConfig.getMessageSizeLimit() == -1 ? maxContentLength : httpConfig.getMessageSizeLimit())); - pipeline.addAfter(HTTP_AGGREGATOR_HANDLER_NAME, HTTP_REQUEST_HANDLER_NAME, new LibertyHttpRequestHandler()); - ctx.pipeline().remove(this); - - ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); - + private PipelineConfigurator selectConfigurator() throws NettyException { + boolean isHttps = chain.isHttps(); + boolean isHttp2 = chain.isHttp2Enabled(); + + // If HTTPS or H2 is needed, we likely need SSL options + Map sslOptions = null; + if (isHttps) { + sslOptions = configOptions.get(ConfigElement.SSL_OPTIONS); + if (sslOptions == null && (isHttps || isHttp2)) { + throw new NettyException("SSL options are required but not provided for endpoint: " + + endpointInfo.getHost() + ":" + endpointInfo.getPort()); } + } - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof PriorKnowledgeUpgradeEvent) { - ctx.pipeline().remove(NO_UPGRADE_OCURRED_HANDLER_NAME); - } - super.userEventTriggered(ctx, evt); + if (isHttps && isHttp2) { + // HTTP/2 over TLS (ALPN) + if (tlsProvider == null) { + throw new NettyException("TLS Provider not loaded for HTTP/2 over TLS"); } - }); - } - - /** - * Utility method for adding all the handlers that need to go before the HTTP Server codec - * - * @param pipeline ChannelPipeline to update as necessary - */ - private void addPreHttpCodecHandlers(ChannelPipeline pipeline) { - if (httpConfig.isAccessLoggingEnabled()) { - if (pipeline.names().contains(NETTY_HTTP_SERVER_CODEC)){ - pipeline.addLast(new AccessLoggerHandler(httpConfig)); + return new H2Configurator(httpConfig, tlsProvider, endpointInfo, sslOptions); + } else if (isHttps) { + // HTTPS/1.1 + if (tlsProvider == null) { + throw new NettyException("TLS Provider not loaded for HTTPS/1.1"); } - } - } - - /** - * Utility method for adding all the handlers that need to go just before the HTTP Dispatcher Handler - * - * @param pipeline ChannelPipeline to update as necessary - */ - private void addPreDispatcherHandlers(ChannelPipeline pipeline, boolean isHttp2) { - - if (!isHttp2) { - pipeline.addAfter(NETTY_HTTP_SERVER_CODEC, HTTP_KEEP_ALIVE_HANDLER_NAME, new HttpServerKeepAliveHandler()); - //TODO: this is a very large number, check best practice - pipeline.addAfter(HTTP_KEEP_ALIVE_HANDLER_NAME, HTTP_AGGREGATOR_HANDLER_NAME, - new LibertyHttpObjectAggregator(httpConfig.getMessageSizeLimit() == -1 ? maxContentLength : httpConfig.getMessageSizeLimit())); - pipeline.addAfter(HTTP_AGGREGATOR_HANDLER_NAME, HTTP_REQUEST_HANDLER_NAME, new LibertyHttpRequestHandler()); - } - - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, "chunkLoggingHandler", new ChunkSizeLoggingHandler()); - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, "chunkWriteHandler", new ChunkedWriteHandler()); - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, null, new ByteBufferCodec()); - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, null, new TransportInboundHandler(httpConfig)); - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, null, new TransportOutboundHandler(httpConfig)); - if (httpConfig.useForwardingHeaders()) { - pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, null, new RemoteIpHandler(httpConfig)); + return new Https11Configurator(httpConfig, tlsProvider, endpointInfo, sslOptions); + } else if (isHttp2) { + // H2C (cleartext HTTP/2) + // No SSL required here + return new H2CConfigurator(httpConfig); + } else { + // Plain HTTP/1.1 + return new Http11Configurator(httpConfig); } } @@ -370,13 +237,30 @@ public HttpPipelineInitializer build() { } /** - * + * Clears configuration from the underlying httpConfig if needed. */ public void clearConfig() { this.httpConfig.clear(); } + /** + * ConfigElements represent different configuration aspects. SSL_OPTIONS is the one needed for TLS contexts. + */ + public enum ConfigElement { + HTTP_OPTIONS, + SSL_OPTIONS, + REMOTE_IP, + COMPRESSION, + SAMESITE, + HEADERS, + ACCESS_LOG + } + + /** + * AllocatorContextSetter associates the channel's RecvByteBufAllocator with the handler context, + * enabling logging and debugging of inbound buffer allocations. + */ @Sharable private static class AllocatorContextSetter extends ChannelInboundHandlerAdapter{ private final LoggingRecvByteBufAllocator loggingAllocator; diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/TransportOutboundHandler.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/TransportOutboundHandler.java index 017898f5657c..cc0053f57378 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/TransportOutboundHandler.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/TransportOutboundHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 IBM Corporation and others. + * Copyright (c) 2023, 2024 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -16,26 +16,34 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; +import io.openliberty.http.pipeline.configurators.PipelineHandlerUtility; /** - * + * This adapter handles outbound HTTP responses. + * If the response status indicates a protocol switch (e.g., WebSocket upgrade), + * this handler removes itself and certain HTTP/1.1-related handlers from the pipeline, + * preparing the pipeline for the upgraded protocol. */ public class TransportOutboundHandler extends ChannelOutboundHandlerAdapter { - HttpChannelConfig config; - - FullHttpResponse fullResponse; + private final HttpChannelConfig config; + /** + * Constructs a TransportOutboundHandler with the given configuration. + * + * @param config The HttpChannelConfig containing server configuration settings. + */ public TransportOutboundHandler(HttpChannelConfig config) { - Objects.requireNonNull(config); - this.config = config; + this.config = Objects.requireNonNull(config, "HttpChannelConfig cannot be null"); } @Override @@ -47,37 +55,65 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) HttpResponse response = (HttpResponse) msg; final boolean isSwitching = response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS); + ChannelFuture future = ctx.writeAndFlush(msg, promise); - ChannelFuture future = ctx.writeAndFlush(msg); - - future.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - if (future.isSuccess() && isSwitching) { + future.addListener((ChannelFutureListener) f -> { - ctx.pipeline().remove(TransportOutboundHandler.class); - if (Objects.nonNull(ctx.pipeline().get(HttpServerCodec.class))) { - ctx.pipeline().remove(HttpServerCodec.class); - } - - ctx.pipeline().remove("maxConnectionHandler"); - ctx.pipeline().remove("chunkLoggingHandler"); - ctx.pipeline().remove("chunkWriteHandler"); - ctx.pipeline().remove(ByteBufferCodec.class); - - if (ctx.pipeline().get(NettyServletUpgradeHandler.class) == null) { + if(f.isSuccess()){ + System.out.println("MSP HTTP11CONFIG -> " + ctx.pipeline().names()); + System.out.println("Switching: " + isSwitching); + } - NettyServletUpgradeHandler upgradeHandler = new NettyServletUpgradeHandler(ctx.channel()); - ctx.pipeline().addLast(upgradeHandler); - } + if (f.isSuccess() && isSwitching) { + // On successful protocol switch, remove TransportOutboundHandler and HTTP codec + removeHandlerIfPresent(ctx.pipeline(), TransportOutboundHandler.class); + removeHandlerIfPresent(ctx.pipeline(), HttpServerCodec.class); + //TODO: set a PipelineHandlerUtility name to these. + + // Remove other handlers that were part of the HTTP pipeline + removeHandlerIfPresent(ctx.pipeline(), PipelineHandlerUtility.MAX_CONNECTION_HANDLER_NAME); + removeHandlerIfPresent(ctx.pipeline(), PipelineHandlerUtility.CHUNK_LOGGING_HANDLER_NAME); + removeHandlerIfPresent(ctx.pipeline(), PipelineHandlerUtility.CHUNK_WRITE_HANDLER_NAME); + removeHandlerIfPresent(ctx.pipeline(), PipelineHandlerUtility.BYTE_BUFFER_CODEC_HANDLER_NAME); + + // If NettyServletUpgradeHandler is not present, add it + if (ctx.pipeline().get(PipelineHandlerUtility.NETTY_SERVLET_UPGRADE_HANDLER_NAME) == null) { + NettyServletUpgradeHandler upgradeHandler = new NettyServletUpgradeHandler(ctx.channel()); + ctx.pipeline().addLast(PipelineHandlerUtility.NETTY_SERVLET_UPGRADE_HANDLER_NAME, upgradeHandler); } + System.out.println("MSP after switching-> " + ctx.pipeline().names()); + } else{ + System.out.println("MSP OUTBOUND NOT SUCCESS or not switching-> " + ctx.pipeline()); + } }); + } else { + // For non-HttpResponse messages, just pass through + super.write(ctx, msg, promise); } + } - else { - super.write(ctx, msg, promise); + /** + * Removes a handler by name if it exists in the pipeline. + * + * @param pipeline The ChannelPipeline to modify. + * @param handlerName The name of the handler to remove. + */ + private void removeHandlerIfPresent(ChannelPipeline pipeline, String handlerName) { + if (pipeline.context(handlerName) != null) { + pipeline.remove(handlerName); } } -} + /** + * Removes a handler by class if it exists in the pipeline. + * + * @param pipeline The ChannelPipeline to modify. + * @param handlerClass The class of the handler to remove. + */ + private void removeHandlerIfPresent(ChannelPipeline pipeline, Class handlerClass) { + if (pipeline.context(handlerClass) != null) { + pipeline.remove(handlerClass); + } + } +} \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/http2/LibertyNettyALPNHandler.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/http2/LibertyNettyALPNHandler.java index 293942b94580..75f1beb8bbca 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/http2/LibertyNettyALPNHandler.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/pipeline/http2/LibertyNettyALPNHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 IBM Corporation and others. + * Copyright (c) 2023, 2024 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -14,6 +14,7 @@ import com.ibm.ws.http.netty.NettyHttpChannelConfig; import com.ibm.ws.http.netty.pipeline.CRLFValidationHandler; import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer; +import com.ibm.ws.http.netty.pipeline.inbound.HttpDispatcherHandler; import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpObjectAggregator; import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpRequestHandler; @@ -24,9 +25,11 @@ import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.openliberty.http.pipeline.configurators.PipelineHandlerUtility; /** - * ALPN Handler for negotiating what protocol to use + * ALPN Handler for negotiating what protocol (HTTP/2 or HTTP/1.1) to use. + * This class checks the negotiated protocol and reconfigures the pipeline accordingly. */ public class LibertyNettyALPNHandler extends ApplicationProtocolNegotiationHandler { @@ -35,51 +38,68 @@ public class LibertyNettyALPNHandler extends ApplicationProtocolNegotiationHandl private final NettyHttpChannelConfig httpConfig; /** - * Default to HTTP 2.0 for now + * Defaults to HTTP/1.1 if no protocol is negotiated. */ public LibertyNettyALPNHandler(NettyHttpChannelConfig httpConfig) { super(ApplicationProtocolNames.HTTP_1_1); this.httpConfig = httpConfig; } - @Override + @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Configuring pipeline with HTTP 2 for incoming connection " + ctx.channel()); + Tr.debug(this, tc, "Configuring pipeline with HTTP/2 for incoming connection " + ctx.channel()); } + + // For HTTP/2 negotiation, we use a LibertyUpgradeCodec to build an Http2ConnectionHandler LibertyUpgradeCodec codec = new LibertyUpgradeCodec(httpConfig, ctx.channel()); HttpToHttp2ConnectionHandler handler = codec.buildHttp2ConnectionHandler(httpConfig, ctx.channel()); - // HTTP2 to HTTP 1.1 and back pipeline - ctx.pipeline().addAfter(HttpPipelineInitializer.HTTP_ALPN_HANDLER_NAME, null, handler); + + // Insert the HTTP/2 handler after the ALPN handler + ctx.pipeline().addAfter(PipelineHandlerUtility.HTTP_ALPN_HANDLER_NAME, null, handler); + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(this, tc, "Configured pipeline with " + ctx.pipeline().names()); } return; } + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Configuring pipeline with HTTP 1.1 for incoming connection " + ctx.channel()); + Tr.debug(this, tc, "Configuring pipeline with HTTP/1.1 for incoming connection " + ctx.channel()); } - ctx.pipeline().addAfter(HttpPipelineInitializer.HTTP_ALPN_HANDLER_NAME, HttpPipelineInitializer.NETTY_HTTP_SERVER_CODEC, - new HttpServerCodec(8192, Integer.MAX_VALUE, httpConfig.getIncomingBodyBufferSize())); - ctx.pipeline().addBefore(HttpPipelineInitializer.NETTY_HTTP_SERVER_CODEC, HttpPipelineInitializer.CRLF_VALIDATION_HANDLER, new CRLFValidationHandler()); - ctx.pipeline().addAfter(HttpPipelineInitializer.NETTY_HTTP_SERVER_CODEC, HttpPipelineInitializer.HTTP_KEEP_ALIVE_HANDLER_NAME, new HttpServerKeepAliveHandler()); - //TODO: this is a very large number, check best practice - ctx.pipeline().addAfter(HttpPipelineInitializer.HTTP_KEEP_ALIVE_HANDLER_NAME, HttpPipelineInitializer.HTTP_AGGREGATOR_HANDLER_NAME, - new LibertyHttpObjectAggregator(httpConfig.getMessageSizeLimit() == -1 ? HttpPipelineInitializer.maxContentLength : httpConfig.getMessageSizeLimit())); - ctx.pipeline().addAfter(HttpPipelineInitializer.HTTP_AGGREGATOR_HANDLER_NAME, HttpPipelineInitializer.HTTP_REQUEST_HANDLER_NAME, new LibertyHttpRequestHandler()); - // Turn on half closure for H1 + + // Add the HTTP server codec after ALPN handler + ctx.pipeline().addAfter( + PipelineHandlerUtility.HTTP_ALPN_HANDLER_NAME, + PipelineHandlerUtility.NETTY_HTTP_SERVER_CODEC, + new HttpServerCodec(8192, Integer.MAX_VALUE, httpConfig.getIncomingBodyBufferSize()) + ); + + // Add the dispatcher handler for HTTP/1.1 scenario + ctx.pipeline().addLast(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); + + // Now reuse the existing utility methods to add all HTTP/1.1 handlers + // Pre-HTTP codec handlers (e.g., logging if enabled) + PipelineHandlerUtility.addPreHttpCodecHandlers(ctx.pipeline(), httpConfig); + + // Pre-dispatcher handlers for HTTP/1.1 (keep-alive, aggregator, request handler) + PipelineHandlerUtility.addPreDispatcherHandlers(ctx.pipeline(), false, httpConfig); + + // Allow half-closure for HTTP/1.1 ctx.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(this, tc, "Configured pipeline with " + ctx.pipeline().names()); } return; } + + // If neither HTTP/2 nor HTTP/1.1 was negotiated, this is unexpected if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(this, tc, "Pipeline unconfigured for protocol " + protocol); } - throw new IllegalStateException("unknown protocol: " + protocol); + throw new IllegalStateException("Unknown protocol: " + protocol); } - -} +} \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/options/HttpOption.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/options/HttpOption.java index c29707540a9d..c46aa21bd004 100644 --- a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/options/HttpOption.java +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/options/HttpOption.java @@ -22,6 +22,7 @@ */ public enum HttpOption implements EndpointOption { + // HTTP/1.1 Options KEEP_ALIVE_ENABLED("keepAliveEnabled", false, Boolean.class, ConfigType.HTTP), MAX_KEEP_ALIVE_REQUESTS("maxKeepAliveRequests", -1, Integer.class, ConfigType.HTTP), PERSIST_TIMEOUT("persistTimeout", "30s", String.class, ConfigType.HTTP), @@ -38,6 +39,8 @@ public enum HttpOption implements EndpointOption { THROW_IOE_FOR_INBOUND_CONNECTIONS("ThrowIOEForInboundConnections", null, Boolean.class, ConfigType.HTTP), DECOMPRESSION_RATIO_LIMIT("decompressionRatioLimit", 200, Integer.class, ConfigType.HTTP), DECOMPRESSION_TOLERANCE("decompressionTolerance", 3, Integer.class, ConfigType.HTTP), + + // HTTP/2.0 Options HTTP2_CONNECTION_IDLE_TIMEOUT("http2ConnectionIdleTimeout", "0", String.class, ConfigType.HTTP2), MAX_CONCURRENT_STREAMS("maxConcurrentStreams", 100, Integer.class, ConfigType.HTTP2), MAX_FRAME_SIZE("maxFrameSize", 57344, Integer.class, ConfigType.HTTP2), diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2CConfigurator.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2CConfigurator.java new file mode 100644 index 000000000000..867460b00dbc --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2CConfigurator.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import com.ibm.ws.http.netty.NettyHttpChannelConfig; +import com.ibm.ws.http.netty.NettyHttpConstants; +import com.ibm.ws.http.netty.pipeline.http2.LibertyUpgradeCodec; +import com.ibm.ws.http.netty.pipeline.inbound.HttpDispatcherHandler; +import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpObjectAggregator; +import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpRequestHandler; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.util.ReferenceCountUtil; +import io.openliberty.netty.internal.exception.NettyException; + +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler.PriorKnowledgeUpgradeEvent; + +/** + * H2CConfigurator sets up a ChannelPipeline for HTTP/2 cleartext (H2C). + * + * Responsibilities: + * - Start with an HTTP dispatcher. + * - Add a cleartext upgrade handler that attempts to upgrade from HTTP/1.1 to HTTP/2. + * - If no upgrade occurs, fall back to HTTP/1.1 by adding the necessary handlers via PipelineHandlerUtility. + * - Disable half-closure by default, only re-enabling it if the pipeline falls back to HTTP/1.1. + */ +public class H2CConfigurator implements PipelineConfigurator { + + private static final String HTTP2_CLEARTEXT_UPGRADE_HANDLER_NAME = "H2C_UPGRADE_HANDLER"; + private static final String NO_UPGRADE_OCCURRED_HANDLER_NAME = "UPGRADE_HANDLER_CHECK"; + + private final NettyHttpChannelConfig httpConfig; + + /** + * Constructs an H2CConfigurator. + * + * @param httpConfig The HTTP channel configuration with server-specific settings. + */ + public H2CConfigurator(NettyHttpChannelConfig httpConfig) { + this.httpConfig = httpConfig; + } + + @Override + public void configure(ChannelPipeline pipeline) throws NettyException { + // Start with the dispatcher handler + pipeline.addLast(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); + + // Add pre-HTTP codec handlers (e.g., access logging if enabled) + PipelineHandlerUtility.addPreHttpCodecHandlers(pipeline, httpConfig); + + // Create the cleartext upgrade handler for HTTP/2 + CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + LibertyUpgradeCodec.createCleartextUpgradeHandler(httpConfig, pipeline.channel()); + + pipeline.addBefore(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, + HTTP2_CLEARTEXT_UPGRADE_HANDLER_NAME, cleartextHttp2ServerUpgradeHandler); + + // Add a fallback handler to handle the scenario when no HTTP/2 upgrade occurs + pipeline.addBefore(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, + NO_UPGRADE_OCCURRED_HANDLER_NAME, new H2CNoUpgradeFallbackHandler(httpConfig)); + + // By default, disable half-closure for H2C until fallback occurs + pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, false); + + // Add pre-dispatcher handlers for HTTP/2 scenario + PipelineHandlerUtility.addPreDispatcherHandlers(pipeline, true, httpConfig); + } + + /** + * H2CNoUpgradeFallbackHandler handles the scenario where the HTTP/2 upgrade does not occur. + * It uses PipelineHandlerUtility methods to set up HTTP/1.1 handlers (keep-alive, aggregator, request handler), + * then removes itself from the pipeline. + */ + static class H2CNoUpgradeFallbackHandler extends SimpleChannelInboundHandler { + private final NettyHttpChannelConfig httpConfig; + + H2CNoUpgradeFallbackHandler(NettyHttpChannelConfig httpConfig) { + this.httpConfig = httpConfig; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { + String protocol = ctx.pipeline().channel().attr(NettyHttpConstants.PROTOCOL).get(); + + if ("HTTP2".equals(protocol)) { + // Upgrade succeeded, just forward the message + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + return; + } + + // No upgrade; fallback to HTTP/1.1 + ctx.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); + + // Add the HTTP/1.1 handlers using a shared utility method + PipelineHandlerUtility.addHttp11FallbackHandlers(ctx.pipeline(), httpConfig); + + // Remove the fallback handler now that HTTP/1.1 is established + ctx.pipeline().remove(NO_UPGRADE_OCCURRED_HANDLER_NAME); + + // Forward the message to the newly configured HTTP/1.1 pipeline + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof PriorKnowledgeUpgradeEvent) { + // If prior knowledge upgrade succeeded, remove this fallback handler + ctx.pipeline().remove(this); + } + super.userEventTriggered(ctx, evt); + } + } +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2Configurator.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2Configurator.java new file mode 100644 index 000000000000..4ce10e36fd59 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/H2Configurator.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import java.util.Map; + +import javax.net.ssl.SSLEngine; + +import com.ibm.websphere.channelfw.EndPointInfo; +import com.ibm.ws.http.netty.NettyHttpChannelConfig; +import com.ibm.ws.http.netty.NettyHttpConstants; +import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer; +import com.ibm.ws.http.netty.pipeline.LibertySslHandler; +import com.ibm.ws.http.netty.pipeline.inbound.HttpDispatcherHandler; +import com.ibm.ws.http.netty.pipeline.http2.LibertyNettyALPNHandler; +import com.ibm.ws.http.netty.pipeline.http2.LibertyUpgradeCodec; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.ssl.SslContext; +import io.netty.util.ReferenceCountUtil; +import io.openliberty.netty.internal.exception.NettyException; +import io.openliberty.netty.internal.tls.NettyTlsProvider; + +/** + * H2Configurator sets up a ChannelPipeline for HTTP/2 over TLS (ALPN). + * + * Responsibilities: + * - Establish a secure channel by adding an SSL handler first. + * - Insert an ALPN handler (LibertyNettyALPNHandler) to negotiate HTTP/2 vs HTTP/1.1 dynamically. + * - Add the HTTP dispatcher once the protocol is decided. + * - Use PipelineHandlerUtility methods to insert pre-dispatcher handlers that ensure consistent behavior. + * - Disable half-closure as is typical for HTTP/2. + */ +public class H2Configurator implements PipelineConfigurator { + + private final NettyHttpChannelConfig httpConfig; + private final NettyTlsProvider tlsProvider; + private final EndPointInfo endpointInfo; + private final Map sslOptions; + + /** + * Constructs an H2Configurator. + * + * @param httpConfig The HTTP channel configuration with server-specific settings. + * @param tlsProvider The TLS provider responsible for creating ALPN-enabled SSL contexts. + * @param endpointInfo Information about the current endpoint (host and port). + * @param sslOptions The map of SSL options retrieved from the configuration, used to obtain the SslContext. + */ + public H2Configurator(NettyHttpChannelConfig httpConfig, + NettyTlsProvider tlsProvider, + EndPointInfo endpointInfo, + Map sslOptions) { + this.httpConfig = httpConfig; + this.tlsProvider = tlsProvider; + this.endpointInfo = endpointInfo; + this.sslOptions = sslOptions; + } + + @Override + public void configure(ChannelPipeline pipeline) throws NettyException { + // Secure the channel first by adding an SSL handler + addSslHandler(pipeline); + + // Add ALPN handler to negotiate protocols (HTTP/2 vs HTTP/1.1) + pipeline.addLast(PipelineHandlerUtility.HTTP_ALPN_HANDLER_NAME, new LibertyNettyALPNHandler(httpConfig)); + + // Add the dispatcher for handling requests after ALPN negotiation + pipeline.addLast(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); + + // Add pre-HTTP codec handlers if any (e.g., access logging) + // Even though HTTP/2 doesn't use the HTTP server codec, this method is harmless if no logging is enabled. + PipelineHandlerUtility.addPreHttpCodecHandlers(pipeline, httpConfig); + + // Add pre-dispatcher handlers for HTTP/2 scenario + PipelineHandlerUtility.addPreDispatcherHandlers(pipeline, true, httpConfig); + + // Mark the channel as secure since we're using TLS + pipeline.channel().attr(NettyHttpConstants.IS_SECURE).set(Boolean.TRUE); + + // HTTP/2 typically disallows half-closure + pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, false); + } + + /** + * Adds the SSL handler to the pipeline. + * Obtains an ALPN-capable SSL context from the TLS provider and creates an SSLEngine. + * The SSL handler is then inserted at the front of the pipeline, securing all traffic. + * + * @param pipeline The ChannelPipeline to which the SSL handler will be added. + * @throws NettyException if the SSL context cannot be created or if the TLS provider is null. + */ + private void addSslHandler(ChannelPipeline pipeline) throws NettyException { + SslContext context = getSslContext(); + SSLEngine engine = context.newEngine(pipeline.channel().alloc()); + pipeline.addFirst(PipelineHandlerUtility.HTTP_SSL_HANDLER_NAME, new LibertySslHandler(engine, httpConfig)); + } + + /** + * Retrieves the ALPN-enabled SSL context from the TLS provider for this endpoint. + * If the TLS provider is not available or the SSL context cannot be created, a NettyException is thrown. + * + * @return A valid ALPN-capable SslContext for inbound TLS connections. + * @throws NettyException if the TLS provider is missing or fails to produce an SslContext. + */ + private SslContext getSslContext() throws NettyException { + if (tlsProvider == null) { + throw new NettyException("TLS Provider is not loaded for endpoint " + + endpointInfo.getHost() + ":" + endpointInfo.getPort()); + } + + String host = endpointInfo.getHost(); + String port = Integer.toString(endpointInfo.getPort()); + SslContext context = tlsProvider.getInboundALPNSSLContext(sslOptions, host, port); + + if (context == null) { + throw new NettyException("Failed to create ALPN SSL context for endpoint: " + host + ":" + port); + } + + return context; + } +} \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Http11Configurator.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Http11Configurator.java new file mode 100644 index 000000000000..f0ab0d2df9c5 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Http11Configurator.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import com.ibm.ws.http.netty.NettyHttpChannelConfig; +import com.ibm.ws.http.netty.pipeline.CRLFValidationHandler; +import com.ibm.ws.http.netty.pipeline.inbound.HttpDispatcherHandler; + +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpServerCodec; +import io.openliberty.netty.internal.exception.NettyException; + +/** + * Http11Configurator sets up a ChannelPipeline for standard HTTP/1.1 requests. + * + * Responsibilities: + * - Validate incoming requests before decoding them into HTTP objects. + * - Insert an HTTP server codec for parsing HTTP/1.1 messages from raw bytes. + * - Add a dispatcher to handle decoded HTTP messages and produce responses. + * - Utilize PipelineHandlerUtility methods to insert pre-codec and pre-dispatcher handlers + * that handle logging, keep-alive, request aggregation, and other common logic. + * - Configure the pipeline to allow half-closure, which is generally acceptable for HTTP/1.1. + * + */ +public class Http11Configurator implements PipelineConfigurator { + + /** + * Initial maximum line length for the HttpServerCodec. + * A line is the initial request line or a header line. + * 8192 bytes is a commonly used default that balances performance and compatibility. + */ + private static final int INITIAL_LINE_LENGTH = 8192; + + /** + * Maximum allowed size for HTTP headers. + * Using Integer.MAX_VALUE effectively removes size restrictions for headers, but + * consider setting a more practical upper bound if large headers are not expected. + */ + private static final int MAX_HEADER_SIZE = Integer.MAX_VALUE; + + private final NettyHttpChannelConfig httpConfig; + + public Http11Configurator(NettyHttpChannelConfig httpConfig) { + this.httpConfig = httpConfig; + } + + /** + * Configures the ChannelPipeline for HTTP/1.1 requests: + * 1. Validate CRLF sequences in incoming data. + * 2. Add the HTTP server codec to decode inbound requests and encode outbound responses. + * 3. Add the dispatcher to handle fully decoded HTTP messages. + * 4. Insert pre-codec and pre-dispatcher handlers (like keep-alive, request aggregation) + * to maintain a consistent pipeline across various protocol scenarios. + * 5. Enable half-closure for better compatibility with some HTTP/1.1 clients. + * + * @param pipeline The ChannelPipeline to configure. + * @throws NettyException if a pipeline configuration error occurs. + */ + @Override + public void configure(ChannelPipeline pipeline) throws NettyException { + // Validate CRLF sequences before HTTP decoding for security and correctness. + pipeline.addLast(PipelineHandlerUtility.CRLF_VALIDATION_HANDLER, new CRLFValidationHandler()); + + // Add the HTTP server codec with well-defined parameters. + pipeline.addLast(PipelineHandlerUtility.NETTY_HTTP_SERVER_CODEC, createHttpServerCodec()); + + // Add the main dispatcher to route requests and generate responses. + pipeline.addLast(PipelineHandlerUtility.HTTP_DISPATCHER_HANDLER_NAME, new HttpDispatcherHandler(httpConfig)); + + // Pre-codec handlers (e.g., access logging if enabled). + PipelineHandlerUtility.addPreHttpCodecHandlers(pipeline, httpConfig); + + // Pre-dispatcher handlers (e.g., keep-alive, aggregator, request handler). + PipelineHandlerUtility.addPreDispatcherHandlers(pipeline, false, httpConfig); + + // Allow half-closure for HTTP/1.1. + pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); + + + System.out.println("MSP HTTP11CONFIG -> " + pipeline.names()); + + } + + /** + * Creates and returns a configured HttpServerCodec instance. + * + * @return configured HttpServerCodec instance. + */ + private HttpServerCodec createHttpServerCodec() { + return new HttpServerCodec( + INITIAL_LINE_LENGTH, + MAX_HEADER_SIZE, + httpConfig.getIncomingBodyBufferSize() + ); + } +} \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Https11Configurator.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Https11Configurator.java new file mode 100644 index 000000000000..78e7dfc83fe3 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/Https11Configurator.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import java.util.Map; + +import javax.net.ssl.SSLEngine; + +import com.ibm.websphere.channelfw.EndPointInfo; + +import com.ibm.ws.http.netty.NettyHttpChannelConfig; +import com.ibm.ws.http.netty.NettyHttpConstants; +import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer; +import com.ibm.ws.http.netty.pipeline.LibertySslHandler; + +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.ssl.SslContext; +import io.openliberty.http.pipeline.configurators.PipelineHandlerUtility; +import io.openliberty.netty.internal.exception.NettyException; +import io.openliberty.netty.internal.tls.NettyTlsProvider; + +/** + * Https11Configurator sets up a ChannelPipeline for HTTPS (HTTP/1.1 over TLS). + * + * Responsibilities: + * - Secure the connection by adding an SSL handler at the start of the pipeline. + * - Reuse the HTTP/1.1 configuration logic (via Http11Configurator) after establishing TLS. + * - Ensure that HTTP/1.1 handlers (e.g., CRLF validation, HTTP codec, dispatcher) + * integrate seamlessly on top of the secure channel. + * - Configure the pipeline to allow half-closure, if desired. + * + */ +public class Https11Configurator implements PipelineConfigurator { + + private final NettyHttpChannelConfig httpConfig; + private final NettyTlsProvider tlsProvider; + private final EndPointInfo endpointInfo; + private final Map sslOptions; + + /** + * Constructs an Https11Configurator. + * + * @param httpConfig The HTTP channel configuration with server-specific settings. + * @param tlsProvider The TLS provider responsible for creating SSL contexts. + * @param endpointInfo Information about the current endpoint (host and port). + * @param sslOptions A map of SSL options retrieved from configOptions.get(ConfigElement.SSL_OPTIONS). + */ + public Https11Configurator(NettyHttpChannelConfig httpConfig, + NettyTlsProvider tlsProvider, + EndPointInfo endpointInfo, + Map sslOptions) { + this.httpConfig = httpConfig; + this.tlsProvider = tlsProvider; + this.endpointInfo = endpointInfo; + this.sslOptions = sslOptions; + } + + /** + * Configures the ChannelPipeline for HTTPS (HTTP/1.1 over TLS): + * 1. Initialize TLS by adding an SSL handler at the pipeline's start. + * 2. Mark the channel as secure. + * 3. Leverage the Http11Configurator to add HTTP/1.1 logic (CRLF validation, codec, dispatcher, etc.) + * 4. Add pre-codec and pre-dispatcher handlers (logging, keep-alive, request aggregation) + * just as we would for a plain HTTP/1.1 configuration. + * 5. Allow half-closure if desired. In most HTTPS scenarios, this setting can remain consistent + * with HTTP/1.1 behavior. + * + * @param pipeline The ChannelPipeline to configure. + * @throws NettyException if SSL context creation or pipeline modifications fail. + */ + @Override + public void configure(ChannelPipeline pipeline) throws NettyException { + // Secure the channel first + addSslHandler(pipeline); + + // Mark the channel as secure + pipeline.channel().attr(NettyHttpConstants.IS_SECURE).set(Boolean.TRUE); + + // Reuse the HTTP/1.1 configuration on top of the secure channel + new Http11Configurator(httpConfig).configure(pipeline); + + // Adjust options if necessary. HTTP/1.1 over TLS often supports half-closure. + pipeline.channel().config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); + } + + /** + * Creates and adds the SSL handler to the pipeline. + * This method encapsulates SSL context retrieval and engine creation + * + * @param pipeline The ChannelPipeline to which the SSL handler will be added. + * @throws NettyException if the SSL context cannot be created or the TLS provider is null. + */ + private void addSslHandler(ChannelPipeline pipeline) throws NettyException { + SslContext context = getSslContext(); + SSLEngine engine = context.newEngine(pipeline.channel().alloc()); + + // Insert the SSL handler at the pipeline's start, ensuring all inbound/outbound traffic is secure. + pipeline.addFirst(PipelineHandlerUtility.HTTP_SSL_HANDLER_NAME, new LibertySslHandler(engine, httpConfig)); + } + + /** + * Retrieves the SSL context from the TLS provider for this endpoint. + * If the TLS provider is not available or the SSL context cannot be created, a NettyException is thrown. + * + * @return A valid SslContext for inbound SSL/TLS connections. + * @throws NettyException if the TLS provider is missing or fails to produce an SslContext. + */ + private SslContext getSslContext() throws NettyException { + if (tlsProvider == null) { + throw new NettyException("TLS Provider is not loaded for endpoint " + + endpointInfo.getHost() + ":" + endpointInfo.getPort()); + } + + String host = endpointInfo.getHost(); + String port = Integer.toString(endpointInfo.getPort()); + SslContext context = tlsProvider.getInboundSSLContext(sslOptions, host, port); + + if (context == null) { + throw new NettyException("Failed to create SSL context for endpoint: " + host + ":" + port); + } + + return context; + } +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineConfigurator.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineConfigurator.java new file mode 100644 index 000000000000..44be56abdaa7 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineConfigurator.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import io.netty.channel.ChannelPipeline; +import io.openliberty.netty.internal.exception.NettyException; + + /** + * PipelineConfigurator defines a contract for configuring a ChannelPipeline for a specific protocol scenario. + * Implementations of this interface are responsible for adding and arranging handlers + * to meet the requirements of that particular pipeline configuration (HTTP/1.1, HTTPS, HTTP/2, etc.). + */ +public interface PipelineConfigurator { + + /** + * Configures the given ChannelPipeline with the necessary handlers for the protocol scenario. + * + * @param pipeline the ChannelPipeline to configure + * @throws NettyException if there is an error during pipeline configuration + */ + void configure(ChannelPipeline pipeline) throws NettyException; +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineHandlerUtility.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineHandlerUtility.java new file mode 100644 index 000000000000..80f25fec64de --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/PipelineHandlerUtility.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.http.pipeline.configurators; + +import com.ibm.ws.http.netty.NettyHttpChannelConfig; + +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpServerKeepAliveHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.openliberty.netty.internal.exception.NettyException; + +import com.ibm.ws.http.netty.pipeline.AccessLoggerHandler; +import com.ibm.ws.http.netty.pipeline.ByteBufferCodec; +import com.ibm.ws.http.netty.pipeline.ChunkSizeLoggingHandler; +import com.ibm.ws.http.netty.pipeline.RemoteIpHandler; +import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpObjectAggregator; +import com.ibm.ws.http.netty.pipeline.inbound.LibertyHttpRequestHandler; +import com.ibm.ws.http.netty.pipeline.inbound.TransportInboundHandler; +import com.ibm.ws.http.netty.pipeline.TransportOutboundHandler; + +/** + * A utility class for adding common handlers to the Netty ChannelPipeline before and after + * specific "anchor" handlers like the HTTP codec or the dispatcher handler. + * + * This class centralizes repeated logic and helps avoid code duplication across different + * protocol configurators (e.g., HTTP/1.1, HTTPS, H2, H2C). + */ +public final class PipelineHandlerUtility { + + // Pipeline Name Constants for handlers used in various configurators + public static final String HTTP_DISPATCHER_HANDLER_NAME = "HTTP_DISPATCHER"; + public static final String NETTY_HTTP_SERVER_CODEC = "HTTP_SERVER_HANDLER"; + public static final String CRLF_VALIDATION_HANDLER = "CRLFValidationHandler"; + public static final String HTTP_KEEP_ALIVE_HANDLER_NAME = "HTTP_KEEP_ALIVE_HANDLER"; + public static final String HTTP_AGGREGATOR_HANDLER_NAME = "LIBERTY_OBJECT_AGGREGATOR"; + public static final String HTTP_REQUEST_HANDLER_NAME = "LIBERTY_REQUEST_HANDLER"; + public static final String CHUNK_LOGGING_HANDLER_NAME = "CHUNK_LOGGING_HANDLER"; + public static final String CHUNK_WRITE_HANDLER_NAME = "CHUNK_WRITE_HANDLER"; + public static final String BYTE_BUFFER_CODEC_HANDLER_NAME = "BYTE_BUFFER_CODEC"; + public static final String TRANSPORT_INBOUND_HANDLER_NAME = "TRANSPORT_INBOUND_HANDLER"; + public static final String TRANSPORT_OUTBOUND_HANDLER_NAME = "TRANSPORT_OUTBOUND_HANDLER"; + public static final String REMOTE_IP_HANDLER_NAME = "REMOTE_IP_HANDLER"; + public static final String MAX_CONNECTION_HANDLER_NAME = "maxConnectionHandler"; + public static final String NETTY_SERVLET_UPGRADE_HANDLER_NAME = "NettyServletUpgradeHandler"; + + // Additional constants for HTTPS/HTTP2 scenarios + public static final String HTTP_SSL_HANDLER_NAME = "SSL_HANDLER"; + public static final String HTTP_ALPN_HANDLER_NAME = "ALPN_HANDLER"; + + // Default maximum content length for request aggregation + public static final long DEFAULT_MAX_CONTENT_LENGTH = Long.MAX_VALUE; + + // Private constructor to prevent instantiation of this utility class. + private PipelineHandlerUtility() { + // Utility class: No instances allowed. + } + + /** + * Adds handlers that need to appear before the HTTP codec. + * In the original code, this included logging if enabled. + * + * @param pipeline The ChannelPipeline to which handlers will be added. + * @param httpConfig The NettyHttpChannelConfig containing server configuration. + */ + public static void addPreHttpCodecHandlers(ChannelPipeline pipeline, NettyHttpChannelConfig httpConfig) { + if (httpConfig.isAccessLoggingEnabled()) { + pipeline.addLast(new AccessLoggerHandler(httpConfig)); + } + } + + /** + * Adds handlers that should appear before the dispatcher handler. + * Depending on whether we are handling HTTP/2 or HTTP/1.1, different sets of handlers are added. + * + * For HTTP/1.1: + * - Add a keep-alive handler + * - Add an object aggregator with a configurable max message size + * - Add a request handler that transforms aggregated messages into a more convenient format + * + * For both HTTP/1.1 and HTTP/2: + * - Add a chunk size logging handler for debugging large payloads + * - Add a chunked write handler for sending large responses in chunks + * - Add a ByteBufferCodec for processing ByteBuf content + * - Add transport layer inbound/outbound handlers for uniform data handling + * - If forwarding headers are enabled, add a RemoteIpHandler to record client addresses + * + * @param pipeline The ChannelPipeline to configure. + * @param isHttp2 True if this pipeline is for HTTP/2; false for HTTP/1.1. + * @param httpConfig The NettyHttpChannelConfig containing server configuration. + * @throws NettyException if adding handlers fails unexpectedly + */ + public static void addPreDispatcherHandlers(ChannelPipeline pipeline, boolean isHttp2, NettyHttpChannelConfig httpConfig) { + if (!isHttp2) { + // For HTTP/1.1, add keep-alive and request aggregation/handling + pipeline.addAfter(NETTY_HTTP_SERVER_CODEC, HTTP_KEEP_ALIVE_HANDLER_NAME, new HttpServerKeepAliveHandler()); + + long maxContentLength = (httpConfig.getMessageSizeLimit() == -1) + ? DEFAULT_MAX_CONTENT_LENGTH + : httpConfig.getMessageSizeLimit(); + + pipeline.addAfter(HTTP_KEEP_ALIVE_HANDLER_NAME, HTTP_AGGREGATOR_HANDLER_NAME, + new LibertyHttpObjectAggregator(maxContentLength)); + pipeline.addAfter(HTTP_AGGREGATOR_HANDLER_NAME, HTTP_REQUEST_HANDLER_NAME, new LibertyHttpRequestHandler()); + } + + // Add common handlers for both HTTP/1.1 and HTTP/2 pipelines: + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, CHUNK_LOGGING_HANDLER_NAME, new ChunkSizeLoggingHandler()); + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, CHUNK_WRITE_HANDLER_NAME, new ChunkedWriteHandler()); + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, BYTE_BUFFER_CODEC_HANDLER_NAME, new ByteBufferCodec()); + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, TRANSPORT_INBOUND_HANDLER_NAME, new TransportInboundHandler(httpConfig)); + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, TRANSPORT_OUTBOUND_HANDLER_NAME, new TransportOutboundHandler(httpConfig)); + + if (httpConfig.useForwardingHeaders()) { + pipeline.addBefore(HTTP_DISPATCHER_HANDLER_NAME, REMOTE_IP_HANDLER_NAME, new RemoteIpHandler(httpConfig)); + } + } + + public static void addHttp11FallbackHandlers(ChannelPipeline pipeline, NettyHttpChannelConfig httpConfig) { + pipeline.addBefore(CHUNK_WRITE_HANDLER_NAME, HTTP_KEEP_ALIVE_HANDLER_NAME, new HttpServerKeepAliveHandler()); + + long maxContentLength = (httpConfig.getMessageSizeLimit() == -1) + ? DEFAULT_MAX_CONTENT_LENGTH + : httpConfig.getMessageSizeLimit(); + + pipeline.addAfter(HTTP_KEEP_ALIVE_HANDLER_NAME, HTTP_AGGREGATOR_HANDLER_NAME, + new LibertyHttpObjectAggregator(maxContentLength)); + + pipeline.addAfter(HTTP_AGGREGATOR_HANDLER_NAME, HTTP_REQUEST_HANDLER_NAME, new LibertyHttpRequestHandler()); + } +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/package-info.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/package-info.java new file mode 100644 index 000000000000..102a4975e162 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/pipeline/configurators/package-info.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +@org.osgi.annotation.versioning.Version("1.0") +package io.openliberty.http.pipeline.configurators; + diff --git a/dev/io.openliberty.netty.internal.impl/src/io/openliberty/netty/internal/impl/QuiesceHandler.java b/dev/io.openliberty.netty.internal.impl/src/io/openliberty/netty/internal/impl/QuiesceHandler.java index 060bb6ec59d3..d4a8757374a8 100644 --- a/dev/io.openliberty.netty.internal.impl/src/io/openliberty/netty/internal/impl/QuiesceHandler.java +++ b/dev/io.openliberty.netty.internal.impl/src/io/openliberty/netty/internal/impl/QuiesceHandler.java @@ -30,6 +30,7 @@ static class QuiesceEvent{ public static final QuiesceEvent QUIESCE_EVENT = new QuiesceEvent(); + private static final TraceComponent tc = Tr.register(QuiesceHandler.class, NettyConstants.NETTY_TRACE_NAME, NettyConstants.BASE_BUNDLE); @@ -38,11 +39,12 @@ static class QuiesceEvent{ public QuiesceHandler(Callable quiesceTask) { this.quiesceTask = quiesceTask; + } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - if(TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(tc, "Added quiesce handler for channel " + ctx.channel() + " with callable: " + quiesceTask); } super.handlerAdded(ctx); @@ -59,3 +61,4 @@ protected void eventReceived(ChannelHandlerContext ctx, QuiesceEvent arg1) throw } } +