diff --git a/dev/com.ibm.ws.transport.http/bnd.bnd b/dev/com.ibm.ws.transport.http/bnd.bnd index aca38de54876..7e6293e9f519 100644 --- a/dev/com.ibm.ws.transport.http/bnd.bnd +++ b/dev/com.ibm.ws.transport.http/bnd.bnd @@ -48,7 +48,8 @@ Export-Package: \ com.ibm.ws.http.netty.inbound,\ com.ibm.ws.http.netty,\ io.openliberty.http.netty*,\ - io.openliberty.http.ext + io.openliberty.http.ext,\ + io.openliberty.http.channel Import-Package: \ !com.ibm.ws.http.logging.source,\ diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/internal/HttpEndpointImpl.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/internal/HttpEndpointImpl.java index e77dfd1f0049..f7766cbc7e39 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/internal/HttpEndpointImpl.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/internal/HttpEndpointImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011, 2023 IBM Corporation and others. + * Copyright (c) 2011, 2025 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 @@ -48,10 +48,8 @@ import com.ibm.websphere.ras.annotation.Trivial; import com.ibm.ws.ffdc.annotation.FFDCIgnore; import com.ibm.ws.http.dispatcher.internal.HttpDispatcher; -import com.ibm.ws.http.internal.HttpChain.ChainState; import com.ibm.ws.http.logging.internal.AccessLogger; import com.ibm.ws.http.logging.internal.DisabledLogger; -import com.ibm.ws.http.netty.NettyChain; import com.ibm.ws.kernel.launch.service.PauseableComponent; import com.ibm.ws.kernel.launch.service.PauseableComponentException; import com.ibm.ws.kernel.productinfo.ProductInfo; @@ -71,6 +69,10 @@ import io.openliberty.checkpoint.spi.CheckpointHook; import io.openliberty.checkpoint.spi.CheckpointPhase; +import io.openliberty.http.channel.Chain; +import io.openliberty.http.channel.ChainState; +import io.openliberty.http.channel.LegacyHttpChain; +import io.openliberty.http.netty.channel.NettyHttpChain; import io.openliberty.netty.internal.NettyFramework; import io.openliberty.netty.internal.impl.NettyConstants; import io.openliberty.netty.internal.tls.NettyTlsProvider; @@ -169,10 +171,10 @@ public String get() { */ protected volatile OnError onError = OnError.WARN; - private final HttpChain httpChain = new HttpChain(this, false); - private final HttpChain httpSecureChain = new HttpChain(this, true); - private final NettyChain nettyChain = new NettyChain(this, false); - private final NettyChain nettySecureChain = new NettyChain(this, true); + private final LegacyHttpChain httpChain = new LegacyHttpChain(this, false); + private final LegacyHttpChain httpSecureChain = new LegacyHttpChain(this, true); + private final NettyHttpChain nettyChain = new NettyHttpChain(this, false); + private final NettyHttpChain nettySecureChain = new NettyHttpChain(this, true); private final AtomicReference accessLogger = new AtomicReference(DisabledLogger.getRef()); @@ -208,7 +210,7 @@ public void run() { synchronized (actionLock) { // Always allow stops. if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) - Tr.debug(this, tc, "EndpointAction: stopping chains " + HttpEndpointImpl.this, httpChain, httpSecureChain); + Tr.debug(this, tc, "EndpointAction: stopping chains " + HttpEndpointImpl.this, getCurrentHttpChain(), getCurrentHttpsChain()); getCurrentHttpChain().stop(); getCurrentHttpsChain().stop(); @@ -223,7 +225,7 @@ public void run() { synchronized (actionLock) { // Always allow stops. if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) - Tr.debug(this, tc, "EndpointAction: stopping https chain " + HttpEndpointImpl.this, httpSecureChain); + Tr.debug(this, tc, "EndpointAction: stopping https chain " + HttpEndpointImpl.this, getCurrentHttpsChain()); getCurrentHttpsChain().stop(); } @@ -307,22 +309,21 @@ protected void activate(ComponentContext ctx, Map config) { MetatypeUtils.parseBoolean(config, NettyConstants.USE_NETTY, config.get(NettyConstants.USE_NETTY), true); + useNetty = false; //FORCE for testing legacy chains initializeChains(); - - modified(config); } private void initializeChains() { if(useNetty) { - nettyChain.initNettyChain(name, netty); - nettySecureChain.initNettyChain(name, netty); + nettyChain.init(name, netty); + nettySecureChain.init(name, netty); }else { - httpChain.init(name, cid, chfw); - httpSecureChain.init(name, cid, chfw); + httpChain.init(name, chfw); + httpSecureChain.init(name, chfw); } } @@ -485,12 +486,12 @@ private synchronized void switchChains(boolean switchToNetty) { if(switchToNetty) { - nettyChain.initNettyChain(name, netty); - nettySecureChain.initNettyChain(name, netty); + nettyChain.init(name, netty); + nettySecureChain.init(name, netty); } else { - httpChain.init(name, cid, chfw); - httpSecureChain.init(name, cid, chfw); + httpChain.init(name, chfw); + httpSecureChain.init(name, chfw); } if(httpPort >=0) { @@ -583,11 +584,11 @@ public void processHttpChainWork(boolean enableEndpoint, boolean isPause) { private void logChainStates(){ if(TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()){ - HttpChain httpChain = getCurrentHttpChain(); - HttpChain httpsChain = getCurrentHttpsChain(); + Chain httpChain = getCurrentHttpChain(); + Chain httpsChain = getCurrentHttpsChain(); - Tr.debug(this, tc, "Chain states after resume - HTTP: " + ChainState.printState(httpChain.getChainState()) - + ", HTTPS: " + ChainState.printState(httpsChain.getChainState())); + Tr.debug(this, tc, "Chain states after resume - HTTP: " + httpChain.state() + + ", HTTPS: " + httpsChain.state()); } } @@ -595,7 +596,7 @@ public String getEventTopic() { return topicString; } - OnError onError() { + public OnError onError() { return onError; } @@ -606,7 +607,7 @@ OnError onError() { * OSGi framework. */ @FFDCIgnore(Exception.class) - final void shutdownFramework() { + public final void shutdownFramework() { Tr.audit(tc, "httpChain.error.shutdown", name); try { @@ -647,7 +648,7 @@ public Supplier getResolvedHostNameSupplier() { * or not yet listening */ public int getListeningHttpPort() { - return useNetty ? nettyChain.getActivePort(): httpChain.getActivePort(); + return useNetty ? nettyChain.activePort(): httpChain.activePort(); } /** @@ -655,7 +656,7 @@ public int getListeningHttpPort() { * or not yet listening */ public int getListeningSecureHttpPort() { - return useNetty ? nettySecureChain.getActivePort(): httpSecureChain.getActivePort(); + return useNetty ? nettySecureChain.activePort(): httpSecureChain.activePort(); } public String getProtocolVersion() { @@ -1318,7 +1319,7 @@ public void pause() throws PauseableComponentException { // Check the state of the HTTP chains. The expectation is that the HTTP chains' states are NOT STARTED // (UNITIALIZED, DESTROYED, QUIESCED or STOPPED). - if (getCurrentHttpChain().getChainState() == ChainState.STARTED.val || getCurrentHttpsChain().getChainState() == ChainState.STARTED.val) { + if (getCurrentHttpChain().state().get() == ChainState.STARTED || getCurrentHttpsChain().state().get() == ChainState.STARTED) { throw new PauseableComponentException("The request to pause HTTP endpoint " + name + " did not complete successfully."); } } catch (Throwable t) { @@ -1361,28 +1362,28 @@ private void verifyResumedChainStates() throws PauseableComponentException { Tr.entry(this, tc, "verifyResumedChainStates"); } - HttpChain httpChain = getCurrentHttpChain(); - HttpChain httpsChain = getCurrentHttpsChain(); + Chain httpChain = getCurrentHttpChain(); + Chain httpsChain = getCurrentHttpsChain(); - int httpChainState = ChainState.UNINITIALIZED.val; - int httpsChainState = ChainState.UNINITIALIZED.val; + ChainState httpChainState = ChainState.INITIALIZED; + ChainState httpsChainState = ChainState.UNINITIALIZED; long startTime = System.currentTimeMillis(); long timeout = 10000; // TODO - testing with ten seconds, but probably want this to be more aggressive while (System.currentTimeMillis() - startTime < timeout) { - httpChainState = httpChain.getChainState(); - httpsChainState = httpsChain.getChainState(); + httpChainState = httpChain.state().get(); + httpsChainState = httpsChain.state().get(); boolean isValid = - (httpChainState == ChainState.STARTED.val && httpsChainState == ChainState.UNINITIALIZED.val) || - (httpChainState == ChainState.UNINITIALIZED.val && httpsChainState == ChainState.STARTED.val) || - (httpChainState == ChainState.STARTED.val && httpsChainState == ChainState.STARTED.val); + (httpChainState == ChainState.STARTED && httpsChainState == ChainState.UNINITIALIZED) || + (httpChainState == ChainState.UNINITIALIZED && httpsChainState == ChainState.STARTED) || + (httpChainState == ChainState.STARTED && httpsChainState == ChainState.STARTED); if (isValid) { if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Chain states verified successfully - HTTP: " + ChainState.printState(httpChainState) - + ", HTTPS: " + ChainState.printState(httpsChainState)); + Tr.debug(this, tc, "Chain states verified successfully - HTTP: " + httpChainState + + ", HTTPS: " + httpsChainState); } if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) { Tr.exit(this, tc, "verifyResumedChainStates"); @@ -1407,8 +1408,8 @@ private void verifyResumedChainStates() throws PauseableComponentException { ". HTTPSChain: " + httpsChain.toString()); if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Chain states after resume - HTTP: " + ChainState.printState(httpChainState) - + ", HTTPS: " + ChainState.printState(httpsChainState)); + Tr.debug(this, tc, "Chain states after resume - HTTP: " + httpChainState + + ", HTTPS: " + httpsChainState); } if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) { @@ -1464,11 +1465,11 @@ public boolean isPaused() { if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) Tr.debug(this, tc, "endpoint and chain data: " + HttpEndpointImpl.this, httpChain, httpSecureChain); - int httpChainState = getCurrentHttpChain().getChainState(); - int httpsChainState = getCurrentHttpsChain().getChainState(); + ChainState httpChainState = getCurrentHttpChain().state().get(); + ChainState httpsChainState = getCurrentHttpsChain().state().get(); // Return true if any of these states apply: UNITIALIZED, DESTROYED, QUIESCED or STOPPED. - return (httpChainState != ChainState.STARTED.val && httpsChainState != ChainState.STARTED.val); + return (httpChainState != ChainState.STARTED && httpsChainState != ChainState.STARTED); } /** {@inheritDoc} */ @@ -1482,10 +1483,10 @@ public HashMap getExtendedInfo() { return info; } - private synchronized HttpChain getCurrentHttpChain() { + private synchronized Chain getCurrentHttpChain() { return useNetty ? nettyChain: httpChain; } - private synchronized HttpChain getCurrentHttpsChain() { + private synchronized Chain getCurrentHttpsChain() { return useNetty ? nettySecureChain: httpSecureChain; } diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/NettyChain.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/NettyChain.java deleted file mode 100644 index 6182b7482e5b..000000000000 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/NettyChain.java +++ /dev/null @@ -1,392 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2023 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 com.ibm.ws.http.netty; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.FutureTask; -import java.util.concurrent.atomic.AtomicReference; - -import com.ibm.websphere.channelfw.EndPointInfo; -import com.ibm.websphere.ras.Tr; -import com.ibm.websphere.ras.TraceComponent; -import com.ibm.ws.http.channel.internal.HttpConfigConstants; -import com.ibm.ws.http.channel.internal.HttpMessages; -import com.ibm.ws.http.internal.HttpChain; -import com.ibm.ws.http.internal.HttpChain.ActiveConfiguration; -import com.ibm.ws.http.internal.HttpChain.ChainState; -import com.ibm.ws.http.internal.HttpEndpointImpl; -import com.ibm.ws.http.internal.HttpServiceConstants; -import com.ibm.ws.http.internal.VirtualHostMap; -import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer; -import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer.ConfigElement; -import com.ibm.wsspi.channelfw.VirtualConnection; -import com.ibm.wsspi.channelfw.VirtualConnectionFactory; -import com.ibm.wsspi.kernel.service.utils.FrameworkState; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelOption; -import io.openliberty.http.netty.quiesce.QuiesceStrategy; -import io.openliberty.netty.internal.ConfigConstants; -import io.openliberty.netty.internal.NettyFramework; -import io.openliberty.netty.internal.ServerBootstrapExtended; -import io.openliberty.netty.internal.exception.NettyException; - -/** - * - */ -public class NettyChain extends HttpChain { - - private static final TraceComponent tc = Tr.register(NettyChain.class, HttpMessages.HTTP_TRACE_NAME, HttpMessages.HTTP_BUNDLE); - - private NettyFramework nettyFramework; - private ServerBootstrapExtended bootstrap; - private volatile Channel serverChannel; - private FutureTask channelFuture; - - private final AtomicReference state = new AtomicReference<>(ChainState.UNINITIALIZED); - - private volatile boolean enabled = false; - - /** - * Netty Http Chain constructor - * - * @param owner - * @param isHttps - */ - public NettyChain(HttpEndpointImpl owner, boolean isHttps) { - super(owner, isHttps); - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "NettyChain constructor, state: " + (state != null ? state.get() : "null")); - } - } - - public synchronized void initNettyChain(String endpointId, NettyFramework netty) { - - Objects.requireNonNull(netty, "NettyFramework cannot be null"); - this.nettyFramework = netty; - endpointMgr = nettyFramework.getEndpointManager(); - - final String root = endpointId + (isHttps ? "-ssl" : ""); - - endpointName = root; - tcpName = "TCP-" + root; - sslName = isHttps ? "SSL-" + root : null; - httpName = "HTTP-" + root; - dispatcherName = "HTTPD-" + root; - chainName = "CHAIN-" + root; - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(tc, "Netty Chain initialized: Endpoint ID = " + endpointId + ", Endpoint Name = " + root); - } - - } - - @Override - public synchronized void stop() { - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.entry(this, tc, "Stopping Netty Chain: " + endpointName + ", Current state: " + state.get()); - } - - if (state.get() == ChainState.STARTED || state.get() == ChainState.STARTING) { - endpointMgr.removeEndPoint(endpointName); - state.set(ChainState.STOPPING); - - try { - if (Objects.nonNull(serverChannel) && serverChannel.isOpen()) { - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Server Channel is open, attempting to close"); - } - - nettyFramework.stop(serverChannel, -1); - serverChannel.closeFuture().syncUninterruptibly(); - serverChannel = null; - } - - } finally { - - VirtualHostMap.notifyStopped(owner, currentConfig.getResolvedHost(), currentConfig.getConfigPort(), isHttps); - currentConfig.clearActivePort(); - String topic = owner.getEventTopic() + HttpServiceConstants.ENDPOINT_STOPPED; - postEvent(topic, currentConfig, null); - - state.set(ChainState.STOPPED); - notifyAll(); - } - } else { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(tc, "Netty Chain is not in a stoppable state. Current state: " + state.get()); - } - } - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.exit(this, tc, "stop chain " + this); - } - } - - private void stopAndWait() { - if (state.get() != ChainState.STOPPED && state.get() != ChainState.UNINITIALIZED) { - stop(); - } - } - - @Override - public synchronized void update(String resolvedHostName) { - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.entry(this, tc, "Updating Netty Chain " + endpointName + " Current state: " + state.get()); - } - - if (!enabled || FrameworkState.isStopping()) { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.exit(this, tc, "Chain is disabled or framework is stopping, skipping update "); - } - return; - } - - final ActiveConfiguration oldConfig = currentConfig; - final ActiveConfiguration newConfig = new ActiveConfiguration(isHttps(), getOwner().getTcpOptions(), isHttps() ? getOwner().getSslOptions() : null, getOwner().getHttpOptions(), getOwner().getRemoteIpConfig(), getOwner().getCompressionConfig(), getOwner().getSamesiteConfig(), getOwner().getHeadersConfig(), getOwner().getEndpointOptions(), resolvedHostName); - - if (newConfig.configPort < 0 || !newConfig.complete()) { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Stopping chain due to configuration " + newConfig); - } - // save the new/changed configuration before we start setting up the new chain - currentConfig = newConfig; - stopAndWait(); - state.set(ChainState.UNINITIALIZED); - } - - else { - - if (!newConfig.unchanged(oldConfig)) { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "This configuration differs and should cause an update "); - } - currentConfig = newConfig; - if (state.get() != ChainState.UNINITIALIZED) { - stopAndWait(); - } - } - startNettyChannel(); - } - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Channel restarted with new configuration"); - } - } - - public synchronized void startNettyChannel() { - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.entry(this, tc, "Starting Netty Channel: " + endpointName + ", Current state: " + state.get() + ", Enabled: " + enabled); - } - - // if (currentConfig == null || !currentConfig.complete() || !enabled || FrameworkState.isStopping()) { - // if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - // Tr.debug(this, tc, "Cannot start channel due to incomplete configuration or disabled state"); - // } - // return; - // } - - if (state.compareAndSet(ChainState.STOPPED, ChainState.STARTING) || state.compareAndSet(ChainState.UNINITIALIZED, ChainState.STARTING)) { - try { - Map httpOptions = new HashMap<>(owner.getHttpOptions()); - httpOptions.put(HttpConfigConstants.PROPNAME_ACCESSLOG_ID, owner.getName()); - // Put the protocol version, which allows the http channel to dynamically - // know what http version it will use. - if (owner.getProtocolVersion() != null) { - httpOptions.put(HttpConfigConstants.PROPNAME_PROTOCOL_VERSION, owner.getProtocolVersion()); - } - - EndPointInfo info = endpointMgr.defineEndPoint(this.endpointName, currentConfig.configHost, currentConfig.configPort); - - Map tcpOptions = new HashMap<>(this.getOwner().getTcpOptions()); - tcpOptions.put(ConfigConstants.EXTERNAL_NAME, endpointName); - - bootstrap = nettyFramework.createTCPBootstrap(tcpOptions); - HttpPipelineInitializer.HttpPipelineBuilder pipelineBuilder = new HttpPipelineInitializer.HttpPipelineBuilder(this).with(ConfigElement.COMPRESSION, - owner.getCompressionConfig()).with(ConfigElement.HTTP_OPTIONS, - httpOptions).with(ConfigElement.HEADERS, - owner.getHeadersConfig()).with(ConfigElement.REMOTE_IP, - owner.getRemoteIpConfig()).with(ConfigElement.SAMESITE, - owner.getSamesiteConfig()); - - // Add SSL options only if the chain is SSL-enabled - if (this.isHttps()) { - Map sslOptions = new HashMap<>(this.getOwner().getSslOptions()); - pipelineBuilder.with(ConfigElement.SSL_OPTIONS, sslOptions); - } - - HttpPipelineInitializer httpPipeline = pipelineBuilder.build(); - - bootstrap.childOption(ChannelOption.ALLOW_HALF_CLOSURE, true); - bootstrap.childHandler(httpPipeline); - - serverChannel = nettyFramework.start(bootstrap, info.getHost(), info.getPort(), this::channelFutureHandler); - - VirtualHostMap.notifyStarted(owner, () -> currentConfig.getResolvedHost(), currentConfig.getConfigPort(), isHttps); - String topic = owner.getEventTopic() + HttpServiceConstants.ENDPOINT_STARTED; - postEvent(topic, currentConfig, null); - - } catch (Exception e) { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.exit(this, tc, "Failed to start Netty Channel: " + e.getMessage()); - } - state.set(ChainState.STOPPED); - } - } - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.exit(this, tc, "Finished starting Netty Channel: " + endpointName + ", Final state: " + state.get()); - } - - } - - private void channelFutureHandler(ChannelFuture future) { - if (state.get() == ChainState.STOPPING) { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Chain: " + endpointName + ", Current state: " + state.get() + ", is stopping so will not notify any virtual hosts and will just return"); - } - return; - } - synchronized (this) { - if (future.isSuccess()) { - state.set(ChainState.STARTED); - EndPointInfo info = endpointMgr.getEndPoint(this.endpointName); - info = endpointMgr.defineEndPoint(this.endpointName, currentConfig.configHost, currentConfig.configPort); - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Channel is now active and listening on port " + getActivePort()); - } - } else { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Channel failed to bind to port: " + future.cause()); - } - handleStartupError(new NettyException(future.cause()), currentConfig); - - if (currentConfig != null) { - VirtualHostMap.notifyStopped(owner, currentConfig.getResolvedHost(), currentConfig.getConfigPort(), isHttps); - currentConfig.clearActivePort(); - } - state.set(ChainState.STOPPED); - } - //Register chain for quiesce, NO_OP is passed as the task as there is no special - //quiesce action required at this time - nettyFramework.registerEndpointQuiesce(future.channel(), QuiesceStrategy.NO_OP.getTask()); - notifyAll(); - } - } - - @Override - public void enable() { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Enabling Netty chain: " + this); - } - enabled = true; - - } - - /** - * Disable this chain. This does not change the chain's state. The caller should - * make subsequent calls to perform actions on the chain. - */ - @Override - public void disable() { - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "disable chain " + this); - } - enabled = false; - - } - - @Override - public int getActivePort() { - return (currentConfig != null) ? currentConfig.configPort : -1; - } - - public String getActiveHost() { - return (currentConfig != null) ? currentConfig.configHost : null; - } - - /** - * @return the bootstrap - */ - public ServerBootstrapExtended getBootstrap() { - return bootstrap; - } - - public EndPointInfo getEndpointInfo() { - EndPointInfo info = endpointMgr.getEndPoint(endpointName); - - if (Objects.isNull(info) && state.get() == ChainState.STARTED) { - info = endpointMgr.defineEndPoint(this.endpointName, currentConfig.configHost, currentConfig.configPort); - } - - return info; - // return endpointMgr.getEndPoint(endpointName); - } - - public String getEndpointPID() { - return (currentConfig != null) ? currentConfig.getEndpointPID() : null; - } - - /** - * Helper method to check if the chain is enabled with HTTP/2.0 or only HTTP/1.1. To do this - * we check the HttpProtocolBehavior reference which is set according to the different servlet - * version loaded. We then compare with the set protocol version in the HttpEndpoint to decide - * which protocol the chain is loaded with. - * - * @return true if HTTP/2.0 is enabled on the chain. False if HTTP/1.1 is enabled on the chain - */ - public boolean isHttp2Enabled() { - String protocolVersion = getOwner().getProtocolVersion(); - Boolean defaultSetting = getOwner().getChfwBundle().getHttp2DefaultSetting(); - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(this, tc, "Protocol version found to be: " + protocolVersion); - } - if (defaultSetting == null) // No default configured, only HTTP 1.1 is enabled - return false; - else - return defaultSetting == Boolean.TRUE ? !!!HttpConfigConstants.PROTOCOL_VERSION_11.equalsIgnoreCase(protocolVersion) : HttpConfigConstants.PROTOCOL_VERSION_2.equalsIgnoreCase(protocolVersion); - } - - public VirtualConnection processNewConnection() { - VirtualConnectionFactory factory = new NettyVirtualConnectionFactoryImpl(); - VirtualConnection vc; - - try { - return factory.createConnection(); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - @Override - public int getChainState() { - return state.get().val; - } - - @Override - public String toString() { - return this.getClass().getSimpleName() - + "[@=" + System.identityHashCode(this) - + ",enabled=" + enabled - + ",state=" + (state != null ? state.get() : "null") - + ",chainName=" + chainName - + ",config=" + currentConfig + "]"; - - } -} 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 abb7aaef1e7c..734c0f41a36c 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, 2024 IBM Corporation and others. + * Copyright (c) 2023, 2025 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 @@ -12,49 +12,42 @@ import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; -import java.util.Set; import java.util.Objects; +import java.util.Set; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSessionContext; import com.ibm.websphere.channelfw.EndPointInfo; import com.ibm.websphere.ras.Tr; import com.ibm.websphere.ras.TraceComponent; import com.ibm.ws.http.channel.internal.HttpConfigConstants; import com.ibm.ws.http.channel.internal.HttpMessages; -import com.ibm.ws.http.netty.NettyChain; import com.ibm.ws.http.netty.NettyHttpChannelConfig; -import com.ibm.ws.http.netty.NettyHttpChannelConfig.NettyConfigBuilder; import com.ibm.ws.http.netty.NettyHttpConstants; -import com.ibm.ws.http.netty.pipeline.LibertySslHandler; import com.ibm.ws.http.netty.pipeline.http2.LibertyNettyALPNHandler; 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 com.ibm.ws.http.netty.pipeline.inbound.TransportInboundHandler; -import com.ibm.ws.http.netty.pipeline.TransportOutboundHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.ChannelHandler.Sharable; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerKeepAliveHandler; import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler.PriorKnowledgeUpgradeEvent; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.ReferenceCountUtil; import io.openliberty.http.netty.channel.AllocatorContextSetter; import io.openliberty.http.netty.channel.LoggingRecvByteBufAllocator; +import io.openliberty.http.netty.channel.NettyHttpChain; import io.openliberty.netty.internal.ChannelInitializerWrapper; import io.openliberty.netty.internal.exception.NettyException; import io.openliberty.netty.internal.impl.NettyConstants; @@ -78,7 +71,7 @@ public enum ConfigElement { ACCESS_LOG } - private final NettyChain chain; + private final NettyHttpChain chain; private final NettyHttpChannelConfig httpConfig; private final Map> configOptions; @@ -95,12 +88,12 @@ public enum ConfigElement { public static final long maxContentLength = Long.MAX_VALUE; - private HttpPipelineInitializer(NettyChain chain, NettyHttpChannelConfig httpConfig, Map> configOptions) { + private HttpPipelineInitializer(NettyHttpChain chain, NettyHttpChannelConfig httpConfig, Map> configOptions) { this.chain = chain; this.httpConfig = httpConfig; this.configOptions = configOptions; - httpConfig.registerAccessLog(chain.getOwner().getName()); + httpConfig.registerAccessLog(chain.endpoint().getName()); } @Override @@ -173,7 +166,7 @@ private void setupHttpsPipeline(ChannelPipeline pipeline) throws NettyException } private SslContext getSslContext() throws NettyException { - NettyTlsProvider tlsProvider = chain.getOwner().getNettyTlsProvider(); + NettyTlsProvider tlsProvider = chain.endpoint().getNettyTlsProvider(); if(tlsProvider == null){ throw new NettyException("TLS Provider is not loaded"); } @@ -309,12 +302,12 @@ private void addPreDispatcherHandlers(ChannelPipeline pipeline, boolean isHttp2) public static class HttpPipelineBuilder { - private final NettyChain chain; + private final NettyHttpChain chain; private final EnumMap> configOptions = new EnumMap<>(ConfigElement.class); private final Set activeConfigs = EnumSet.noneOf(ConfigElement.class); - public HttpPipelineBuilder(NettyChain chain) { + public HttpPipelineBuilder(NettyHttpChain chain) { this.chain = Objects.requireNonNull(chain, "Netty chain cannot be null"); } diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/AbstractHttpChain.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/AbstractHttpChain.java new file mode 100644 index 000000000000..832dfbd671bb --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/AbstractHttpChain.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2025 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.channel; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; + +import com.ibm.websphere.channelfw.EndPointMgr; +import com.ibm.ws.http.internal.HttpEndpointImpl; +import com.ibm.ws.http.internal.HttpServiceConstants; + +import com.ibm.wsspi.kernel.service.utils.OnErrorUtil.OnError; + +public abstract class AbstractHttpChain implements Chain { + + protected volatile ChainConfiguration config; + private volatile boolean enabled; + private volatile boolean https; + private final AtomicReference state = new AtomicReference<>(ChainState.UNINITIALIZED); + private HttpEndpointImpl endpoint; + + protected String endpointName; + protected String tcpName; + protected String sslName; + protected String httpName; + protected String dispatcherName; + protected String chainName; + protected EndPointMgr endpointManager; + + protected String host; + protected int port = -1; + + + public AbstractHttpChain(HttpEndpointImpl endpoint, boolean isHttps) { + this.endpoint = endpoint; + this.https = isHttps; + } + + @Override + public final void disable() { + enabled = false; + + } + + @Override + public final void enable() { + enabled = true; + + } + + public final boolean enabled(){ + return enabled; + } + + @Override + public final AtomicReference state() { + return this.state; + } + + @Override + public final ChainConfiguration config() { + return config; + } + + @Override + public final boolean isHttps() { + return this.https; + } + + public final HttpEndpointImpl endpoint(){ + return this.endpoint; + } + + @Override + public final int activePort() { + return port; + } + + @Override + public final String activeHost(){ + return host; + } + + protected final void handleStartupError(Exception exception, ChainConfiguration configuration){ + + //Delegate to HttpEndpoint, something like endpoint().handleStartupError(e); + if(endpoint().onError() == OnError.FAIL){ + endpoint().shutdownFramework(); + } + else{ + String topic = endpoint().getEventTopic() + HttpServiceConstants.ENDPOINT_FAILED; + postEvent(topic, configuration, exception); + this.port = -1; + } + } + + protected final void postEvent(String topic, ChainConfiguration configuration, Exception exception){ + Map eventProps = new HashMap(4); + + eventProps.put(HttpServiceConstants.ENDPOINT_NAME, endpointName); + eventProps.put(HttpServiceConstants.ENDPOINT_ACTIVE_PORT, activePort()); + eventProps.put(HttpServiceConstants.ENDPOINT_CONFIG_HOST, config.host()); + eventProps.put(HttpServiceConstants.ENDPOINT_CONFIG_PORT, config.port()); + eventProps.put(HttpServiceConstants.ENDPOINT_IS_HTTPS, isHttps()); + + if(exception != null){ + eventProps.put(HttpServiceConstants.ENDPOINT_EXCEPTION, exception.toString()); + } + + EventAdmin engine = endpoint.getEventAdmin(); + if(engine != null){ + Event event = new Event(topic, eventProps); + engine.postEvent(event); + } + } + + public abstract void stop(); + + public abstract void update(String host); + + @Override + public String toString() { + return this.getClass().getSimpleName() + + "[@=" + System.identityHashCode(this) + + ",enabled=" + enabled + + ",state=" + (state != null ? state.get() : "null") + + ",chainName=" + chainName + + ",config=" + config + "]"; + + } + +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/Chain.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/Chain.java new file mode 100644 index 000000000000..031815490d1d --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/Chain.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2025 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.channel; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Represents a common interface for managing HTTP or HTTPS chain lifecycles + * in a predictable and controlled manner. + */ +public interface Chain { + + /** + * Indicates whether this chain manages HTTPS. + */ + boolean isHttps(); + + /** + * Moves the chain from a "disabled" state into an "enabled" state. + */ + void enable(); + + /** + * Moves the chain from an "enabled" state into a "disabled" state. + */ + void disable(); + + /** + * Applies new configuration to the chain. If the chain is enabled + * and the new configuration differs in a way that requires a rebind + * (port, host, etc.), it should stop and then attempt to start with + * the new configuration. + * + * Configuration will be pulled from the associated {@link HttpEndpointImpl} + * + * @param hostname the new resolved hostname + */ + void update(String hostname); + + /** + * Fully stops the chain, releasing resources. Does not necessarily + * disable it; the chain can be updated later if still enabled. + */ + void stop(); + + /** + * Retrieves the current state of the chain as an integer. + * + * @return integer representing the chain’s state + */ + AtomicReference state(); + + /** + * Returns the active port the chain is bound to, or -1 if not bound. + */ + default int activePort() { + return -1; + } + + /** + * @return Returns the active host the chain resolved to. + */ + String activeHost(); + + /** + * Returns the currently stored chain config if available, or null + * if uninitialized. + */ + default ChainConfiguration config() { + return null; + } +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainConfiguration.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainConfiguration.java new file mode 100644 index 000000000000..ba0c454f432e --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainConfiguration.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) 2025 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.channel; + +import java.util.Map; +import java.util.Objects; + +import com.ibm.ws.channelfw.internal.chains.Chain; +import com.ibm.ws.http.internal.HttpServiceConstants; +import com.ibm.wsspi.kernel.service.utils.MetatypeUtils; + +/** + * Represents a common configuration for any implementation of {@link Chain}, + * encapsulating fields and logic for determining if a chain must + * be stopped/restarted based on new config values. + */ +public class ChainConfiguration { + + private final boolean https; + private final String host; + private final int port; + private final Map tcpOptions; + private final Map sslOptions; + private final Map httpOptions; + private final Map endpointOptions; + private final Map remoteIpOptions; + private final Map compressionOptions; + private final Map samesiteOptions; + private final Map headersOptions; + private boolean valid = false; + + /** + * @param https true if this config is for HTTPS + * @param host the configured hostname (e.g. '*' or 'localhost') + * @param tcpOptions map of TCP channel configuration + * @param sslOptions map of SSL channel configuration + * @param httpOptions map of HTTP channel configuration + * @param endpointOptions the HTTP endpoint’s overall config map + * @param remoteIpOptions configuration for remote IP handling + * @param compressionOptions configuration for compression + * @param samesiteOptions configuration for samesite cookies + * @param headersOptions configuration for response custom headers + */ + public ChainConfiguration( + boolean https, + Map tcpOptions, + Map sslOptions, + Map httpOptions, + Map endpointOptions, + Map remoteIpOptions, + Map compressionOptions, + Map samesiteOptions, + Map headersOptions) { + + this.https = https; + this.tcpOptions = tcpOptions; + this.sslOptions = sslOptions; + this.httpOptions = httpOptions; + this.endpointOptions = endpointOptions; + this.remoteIpOptions = remoteIpOptions; + this.compressionOptions = compressionOptions; + this.samesiteOptions = samesiteOptions; + this.headersOptions = headersOptions; + + + String attribute = https ? "httpsPort" : "httpPort"; + this.port = MetatypeUtils.parseInteger(HttpServiceConstants.ENPOINT_FPID_ALIAS, attribute, + this.endpointOptions.get(attribute), + -1); + this.host = (String) endpointOptions.get("host"); + + } + + /** + * + * @return Specifies if this {@Link Chain} is configured for SSL + */ + public boolean isHttps() { + return https; + } + + /** + * @return The configured hostname, such as '*' or 'localhost'. + */ + public String host(){ + return host; + } + + /** + * @return The configured HTTP/HTTPS port. + */ + public int port() { + return port; + } + + /** + * @return The configured TCP options + */ + public Map tcpOptions() { + return tcpOptions; + } + + /** + * @return The configured SSL options + */ + public Map sslOptions() { + return sslOptions; + } + + /** + * @return The configured HTTP options + */ + public Map httpOptions() { + return httpOptions; + } + + /** + * @return The configured endpoint options + */ + public Map endpointOptions() { + return endpointOptions; + } + + /** + * @return The configured remoteIp options + */ + public Map remoteIpOptions() { + return remoteIpOptions; + } + + /** + * @return The configured compression options + */ + public Map compressionOptions() { + return compressionOptions; + } + + /** + * @return The configured samesite options + */ + public Map samesiteOptions() { + return samesiteOptions; + } + + /** + * @return The configured headers options + */ + public Map headersOptions() { + return headersOptions; + } + + /** + * Determines if the minimal fields for starting a chain are available. + * For HTTPS, sslOptions must exist. If port < 0, it’s unconfigured. + */ + public boolean isComplete() { + if (tcpOptions == null || httpOptions == null) return false; + if (port < 0) return false; + if (https && sslOptions == null) return false; + return true; + } + + /** + * Returns whether we need to restart the chain due to a config change. + * Checks all relevant fields like port, host, or changed reference maps. + * + * @param other The previous configuration or null if none. + */ + public boolean requiresRestart(ChainConfiguration other) { + if (other == null) return true; + if (this.https != other.https) return true; + if (!Objects.equals(this.host, other.host)) return true; + if (this.port != other.port) return true; + + // Check whether reference maps differ: + if (!Objects.equals(this.tcpOptions, other.tcpOptions)) return true; + if (!Objects.equals(this.httpOptions, other.httpOptions)) return true; + if (!Objects.equals(this.sslOptions, other.sslOptions)) return true; + if (!Objects.equals(this.remoteIpOptions, other.remoteIpOptions)) return true; + if (!Objects.equals(this.compressionOptions, other.compressionOptions)) return true; + if (!Objects.equals(this.samesiteOptions, other.samesiteOptions)) return true; + if (!Objects.equals(this.headersOptions, other.headersOptions)) return true; + + return false; + } + + public boolean isValid(){ + return valid; + } + + public void setValidity(boolean isValid){ + this.valid = isValid; + } + + @Override + public String toString() { + return "ChainConfiguration{" + + "https=" + https + + ", host='" + host + '\'' + + ", port=" + port + + ", tcp=" + (tcpOptions != null ? tcpOptions.hashCode() : "[N/A]") + + ", ssl=" + (sslOptions != null ? sslOptions.hashCode() : "[N/A]") + + ", http=" + (httpOptions != null ? httpOptions.hashCode() : "[N/A]") + + ", remoteIp=" + (remoteIpOptions != null ? remoteIpOptions.hashCode() : "[N/A]") + + ", compression=" + (compressionOptions != null ? compressionOptions.hashCode() : "[N/A]") + + ", samesite=" + (samesiteOptions != null ? samesiteOptions.hashCode() : "[N/A]") + + ", headers=" + (headersOptions != null ? headersOptions.hashCode() : "[N/A]") + + "}"; + } +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainState.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainState.java new file mode 100644 index 000000000000..fc4cc118fb54 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/ChainState.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2025 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.channel; + +/** + * Enumerates possible states for {@link Chain} implementations. + */ +public enum ChainState { + /** + * The chain has just been constructed or initialized but not yet set up + * in the framework. + */ + UNINITIALIZED(0), + + /** + * The chain has been destroyed or removed entirely. + */ + DESTROYED(1), + + /** + * The chain is initialized but not yet started. + */ + INITIALIZED(2), + + /** + * The chain is fully stopped, not actively listening. + */ + STOPPED(3), + + /** + * The chain is quiesced but not fully stopped (some frameworks + * differentiate "quiescing" from "stopped". + */ + QUIESCED(4), + + /** + * The chain is fully started and actively listening. + */ + STARTED(5), + + /** + * The chain is in the process of starting, typically after an update call. + */ + STARTING(6), + + /** + * The chain is in the process of stopping. + */ + STOPPING(7); + + private final int value; + + ChainState(int val) { + this.value = val; + } + + /** + * @return An integer code associated with the state. + */ + public int value() { + return value; + } + + /** + * @return Returns the enum instance corresponding to the given integer code, + * or {@code null} if none matches. + */ + public static ChainState fromValue(int val) { + for (ChainState st : values()) { + if (st.value == val) { + return st; + } + } + return null; + } + +} diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/LegacyHttpChain.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/LegacyHttpChain.java new file mode 100644 index 000000000000..8ca449278802 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/LegacyHttpChain.java @@ -0,0 +1,649 @@ +/******************************************************************************* + * Copyright (c) 2025 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.channel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import com.ibm.websphere.channelfw.ChainData; +import com.ibm.websphere.channelfw.ChannelData; +import com.ibm.websphere.channelfw.EndPointInfo; +import com.ibm.websphere.channelfw.FlowType; +import com.ibm.websphere.channelfw.osgi.CHFWBundle; +import com.ibm.websphere.ras.Tr; +import com.ibm.websphere.ras.TraceComponent; +import com.ibm.websphere.ras.annotation.Trivial; +import com.ibm.ws.ffdc.annotation.FFDCIgnore; +import com.ibm.ws.http.channel.internal.HttpConfigConstants; +import com.ibm.ws.http.dispatcher.internal.channel.HttpDispatcherConfig; +import com.ibm.ws.http.internal.HttpEndpointImpl; +import com.ibm.ws.http.internal.HttpServiceConstants; +import com.ibm.ws.http.internal.VirtualHostMap; +import com.ibm.ws.http.netty.MSP; +import com.ibm.wsspi.channelfw.ChainEventListener; +import com.ibm.wsspi.channelfw.ChannelFramework; +import com.ibm.wsspi.channelfw.exception.ChainException; +import com.ibm.wsspi.channelfw.exception.ChannelException; +import com.ibm.wsspi.channelfw.exception.InvalidRuntimeStateException; +import com.ibm.wsspi.kernel.service.utils.FrameworkState; + +/** + * A legacy chain implementation that uses the older ChannelFramework APIs. + * It implements {@link Chain} so it can be substituted for newer chain + * implementations without changing {@link HttpEndpointImpl}. + */ +public class LegacyHttpChain extends AbstractHttpChain implements ChainEventListener { + + private static final TraceComponent tc = Tr.register(LegacyHttpChain.class); + + /** Channel framework references needed by the legacy approach. */ + private ChannelFramework framework; + + protected final StopWait stopWait = new StopWait(); + + /** + * Constructs the legacy chain tied to a specific endpoint. + * + * @param endpoint The endpoint that owns this chain. + * @param isHttps Whether this chain is for HTTPS rather than HTTP. + */ + public LegacyHttpChain(HttpEndpointImpl endpoint, boolean isHttps) { + super(endpoint, isHttps); + } + + /** + * Initializes references to ChannelFramework. Typically called + * by {@link HttpEndpointImpl} once. + * + * @param endpointId The ID used for naming the chain. + * @param cfBundle The CHFWBundle that provides ChannelFramework references. + */ + public void init(String endpointId, CHFWBundle cfBundle) { + this.framework = cfBundle.getFramework(); + this.endpointManager = cfBundle.getEndpointManager(); + + final String root = endpointId + (isHttps() ? "-ssl" : ""); + + endpointName = root; + tcpName = root; + sslName = "SSL-" + root; + httpName = "HTTP-" + root; + dispatcherName = "HTTPD-" + root; + chainName = "CHAIN-" + root; + + // If there is a chain that is in the CFW with this name, it was potentially + // left over from a previous instance of the endpoint. There is no way to get + // the state of the existing (old) CFW chain to set our chainState accordingly... + // (in addition to the old chain pointing to old services and things.. ) + // *IF* there is an old chain, stop, destroy, and remove it. + try { + ChainData cd = framework.getChain(chainName); + if (cd != null) { + framework.stopChain(cd, 0L); // no timeout: FORCE the stop. + framework.destroyChain(cd); + framework.removeChain(cd); + } + } catch (ChannelException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error stopping chain " + chainName, this, e); + } + } catch (ChainException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error stopping chain " + chainName, this, e); + } + } + + } + + /** + * Stop this chain. The chain will have to be recreated when port is updated + * notification/follow-on of stop operation is in the chainStopped listener method. + */ + @FFDCIgnore(InvalidRuntimeStateException.class) + public synchronized void stop() { + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(this, tc, "stop chain " + this); + } + + // When the chain is being stopped, remove the previously + // registered EndPoint created in update + endpointManager.removeEndPoint(endpointName); + + // We don't have to check enabled/disabled here: chains are always allowed to stop. + if (config() == null || state().get().value() <= ChainState.QUIESCED.value()) + return; + + // Quiesce and then stop the chain. The CFW internally uses a StopTimer for + // the quiesce/stop operation-- the listener method will be called when the chain + // has stopped. So to see what happens next, visit chainStopped + try { + ChainData cd = framework.getChain(chainName); + if (cd != null) { + framework.stopChain(cd, framework.getDefaultChainQuiesceTimeout()); + stopWait.waitForStop(framework.getDefaultChainQuiesceTimeout()); // BLOCK + try { + framework.destroyChain(cd); + framework.removeChain(cd); + } catch (InvalidRuntimeStateException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error destroying or removing chain " + chainName, this, e); + } + } + } + } catch (ChannelException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error stopping chain " + chainName, this, e); + } + } catch (ChainException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error stopping chain " + chainName, this, e); + } + } + } + + /** + * Update/start the chain configuration. + */ + @FFDCIgnore({ ChannelException.class, ChainException.class }) + public synchronized void update(String host) { + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(this, tc, "update chain " + this); + } + + // Don't update or start the chain if it is disabled or the framework is stopping.. + if (!enabled() || FrameworkState.isStopping()) + return; + + final ChainConfiguration oldConfig = config(); + + // The old configuration was "valid" if it existed, and if it was correctly configured + final boolean validOldConfig = oldConfig == null ? false : oldConfig.isValid(); + + Map tcpOptions = endpoint().getTcpOptions(); + Map sslOptions = (isHttps()) ? endpoint().getSslOptions() : null; + Map httpOptions = endpoint().getHttpOptions(); + Map endpointOptions = endpoint().getEndpointOptions(); + Map remoteIpOptions = endpoint().getRemoteIpConfig(); + Map compressionOptions = endpoint().getCompressionConfig(); + Map samesiteOptions = endpoint().getSamesiteConfig(); + Map headersOptions = endpoint().getHeadersConfig(); + + final ChainConfiguration newConfig = new ChainConfiguration(isHttps(), + tcpOptions, + sslOptions, + httpOptions, + endpointOptions, + remoteIpOptions, + compressionOptions, + samesiteOptions, + headersOptions + ); + + if (!newConfig.isComplete()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Stopping chain due to configuration " + newConfig); + } + + // save the new/changed configuration before we start setting up the new chain + this.config = newConfig; + this.host = host; + + stop(); + } else { + Map chanProps; + + try { + boolean sameConfig = !newConfig.requiresRestart(oldConfig); + if (validOldConfig) { + if (sameConfig) { + if (state().get() == ChainState.STARTED) { + // If configurations are identical, see if the listening port is also the same + // which would indicate that the chain is running with the unchanged configuration + // toggle start/stop of chain if we are somehow active on a different port.. + sameConfig = validateActivePort(); + if (sameConfig) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Configuration is unchanged, and chain is already started: " + oldConfig); + } + // EARLY EXIT: we have nothing else to do here: "new configuration" not saved + return; + } else { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Configuration is unchanged, but chain is running with a mismatched configuration: " + oldConfig); + } + } + } else if (state().get() == ChainState.QUIESCED) { + // Chain is in the process of stopping.. we need to wait for it + // to finish stopping before we start it again + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Configuration is unchanged, chain is quiescing, wait for stop: " + newConfig); + } + stopWait.waitForStop(framework.getDefaultChainQuiesceTimeout()); // BLOCK + } else { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Configuration is unchanged, chain must be started: " + newConfig); + } + } + } + } + + + if (!sameConfig) { + // Note that one path in the above block can change the value of sameConfig: + // if the started chain is actually running on a different port than we expect, + // something strange happened, and the whole thing should be stopped and restarted. + // We come through this block for the stop/teardown... + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "New/changed chain configuration " + newConfig); + } + + // We've been through channel configuration before... + // We have to destroy/rebuild the chains because the channels don't + // really support dynamic updates. *sigh* + ChainData cd = framework.getChain(chainName); + if (cd != null) { + framework.stopChain(cd, framework.getDefaultChainQuiesceTimeout()); + stopWait.waitForStop(framework.getDefaultChainQuiesceTimeout()); // BLOCK + framework.destroyChain(cd); + framework.removeChain(cd); + } + removeChannel(tcpName); + removeChannel(sslName); + removeChannel(httpName); + removeChannel(dispatcherName); + } + + // save the new/changed configuration before we start setting up the new chain + config = newConfig; + this.host = host; + + // Define and register an EndPoint to represent this chain + EndPointInfo ep = endpointManager.defineEndPoint(endpointName, newConfig.host(), newConfig.port()); + + // TCP Channel + ChannelData tcpChannel = framework.getChannel(tcpName); + if (tcpChannel == null) { + String typeName = (String) tcpOptions.get("type"); + chanProps = new HashMap(tcpOptions); + chanProps.put("endPointName", endpointName); + chanProps.put("hostname", ep.getHost()); + chanProps.put("port", String.valueOf(ep.getPort())); + + tcpChannel = framework.addChannel(tcpName, framework.lookupFactory(typeName), chanProps); + } + + // SSL Channel + if (isHttps()) { + ChannelData sslChannel = framework.getChannel(sslName); + if (sslChannel == null) { + chanProps = new HashMap(sslOptions); + // Put the protocol version, which allows the http channel to dynamically + // know what http version it will use. + if (endpoint().getProtocolVersion() != null) { + chanProps.put(HttpConfigConstants.PROPNAME_PROTOCOL_VERSION, endpoint().getProtocolVersion()); + } + sslChannel = framework.addChannel(sslName, framework.lookupFactory("SSLChannel"), chanProps); + } + } + + // HTTP Channel + ChannelData httpChannel = framework.getChannel(httpName); + if (httpChannel == null) { + chanProps = new HashMap(httpOptions); + // Put the endpoint id, which allows us to find the registered access log + // dynamically + chanProps.put(HttpConfigConstants.PROPNAME_ACCESSLOG_ID, endpoint().getName()); + // Put the protocol version, which allows the http channel to dynamically + // know what http version it will use. + if (endpoint().getProtocolVersion() != null) { + chanProps.put(HttpConfigConstants.PROPNAME_PROTOCOL_VERSION, endpoint().getProtocolVersion()); + } + if (remoteIpOptions.get("id").equals("defaultRemoteIp")) { + //Put the internal remoteIp set to false since the element was not configured to be used + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_IP, "false"); + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_PROXIES, null); + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_IP_ACCESS_LOG, null); + } else { + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_IP, "true"); + //Check if the remoteIp is configured to use the remoteIp in the access log or if + //a custom proxy regex was provided + if (remoteIpOptions.containsKey("proxies")) { + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_PROXIES, remoteIpOptions.get("proxies")); + } + if (remoteIpOptions.containsKey("useRemoteIpInAccessLog")) { + chanProps.put(HttpConfigConstants.PROPNAME_REMOTE_IP_ACCESS_LOG, remoteIpOptions.get("useRemoteIpInAccessLog")); + } + } + + if (compressionOptions.get("id").equals("defaultCompression")) { + //Put the internal compression set to false since the element was not configured to be used + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION, "false"); + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION_CONTENT_TYPES_INTERNAL, null); + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION_PREFERRED_ALGORITHM_INTERNAL, null); + } + + else { + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION, "true"); + //Check if the compression is configured to use content-type filter + if (compressionOptions.containsKey("types")) { + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION_CONTENT_TYPES_INTERNAL, compressionOptions.get("types")); + + } + if (compressionOptions.containsKey("serverPreferredAlgorithm")) { + chanProps.put(HttpConfigConstants.PROPNAME_COMPRESSION_PREFERRED_ALGORITHM_INTERNAL, compressionOptions.get("serverPreferredAlgorithm")); + } + } + + if (samesiteOptions.get("id").equals("defaultSameSite")) { + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE, "false"); + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_LAX, null); + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_NONE, null); + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_STRICT, null); + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_PARTITIONED, "false"); + } + + else { + + boolean enableSameSite = false; + if (samesiteOptions.containsKey("lax")) { + enableSameSite = true; + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_LAX_INTERNAL, samesiteOptions.get("lax")); + } + if (samesiteOptions.containsKey("none")) { + enableSameSite = true; + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_NONE_INTERNAL, samesiteOptions.get("none")); + } + if (samesiteOptions.containsKey("strict")) { + enableSameSite = true; + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_STRICT_INTERNAL, samesiteOptions.get("strict")); + } + if (samesiteOptions.containsKey("partitioned")) { + enableSameSite = true; + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE_PARTITIONED, samesiteOptions.get("partitioned")); + } + chanProps.put(HttpConfigConstants.PROPNAME_SAMESITE, enableSameSite); + } + + if (headersOptions.get("id").equals("defaultHeaders")) { + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS, "false"); + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_ADD, null); + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_SET, null); + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_SET_IF_MISSING, null); + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_REMOVE, null); + } + + else { + boolean enableHeadersFeature = false; + if (headersOptions.containsKey("add")) { + enableHeadersFeature = true; + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_ADD, headersOptions.get("add")); + } + if (headersOptions.containsKey("set")) { + enableHeadersFeature = true; + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_SET, headersOptions.get("set")); + } + if (headersOptions.containsKey("setIfMissing")) { + enableHeadersFeature = true; + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_SET_IF_MISSING, headersOptions.get("setIfMissing")); + } + if (headersOptions.containsKey("remove")) { + enableHeadersFeature = true; + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS_REMOVE, headersOptions.get("remove")); + } + chanProps.put(HttpConfigConstants.PROPNAME_RESPONSE_HEADERS, enableHeadersFeature); + } + httpChannel = framework.addChannel(httpName, framework.lookupFactory("HTTPInboundChannel"), chanProps); + } + + // HTTPDispatcher Channel + ChannelData httpDispatcher = framework.getChannel(dispatcherName); + if (httpDispatcher == null) { + chanProps = new HashMap(); + chanProps.put(HttpDispatcherConfig.PROP_ENDPOINT, endpoint().getPid()); + + httpDispatcher = framework.addChannel(dispatcherName, framework.lookupFactory("HTTPDispatcherChannel"), chanProps); + } + + // Add chain + ChainData cd = framework.getChain(chainName); + if (null == cd) { + final String[] chanList; + if (isHttps()) + chanList = new String[] { tcpName, sslName, httpName, dispatcherName }; + else + chanList = new String[] { tcpName, httpName, dispatcherName }; + + cd = framework.addChain(chainName, FlowType.INBOUND, chanList); + cd.setEnabled(enabled()); + framework.addChainEventListener(this, chainName); + + // initialize the chain: this will find/create the channels in the chain, + // initialize each channel, and create the chain. If there are issues with any + // channel properties, they will surface here + // THIS INCLUDES ATTEMPTING TO BIND TO THE PORT + framework.initChain(chainName); + } + + // We configured the chain successfully + newConfig.setValidity(true); + } catch (ChannelException e) { + handleStartupError(e, newConfig); // FFDCIgnore: CFW will have logged and FFDCd already + } catch (ChainException e) { + handleStartupError(e, newConfig); // FFDCIgnore: CFW will have logged and FFDCd already + } catch (Exception e) { + // The exception stack for this is all internals and does not belong in messages.log. + Tr.error(tc, "config.httpChain.error", tcpName, e.toString()); + handleStartupError(e, newConfig); + } + + if (newConfig.isValid()) { + try { + // Start the chain: follow along to chainStarted method (CFW callback) + framework.startChain(chainName); + } catch (ChannelException e) { + handleStartupError(e, newConfig); // FFDCIgnore: CFW will have logged and FFDCd already + } catch (ChainException e) { + handleStartupError(e, newConfig); // FFDCIgnore: CFW will have logged and FFDCd already + } catch (Exception e) { + // The exception stack for this is all internals and does not belong in messages.log. + Tr.error(tc, "start.httpChain.error", tcpName, e.toString()); + handleStartupError(e, newConfig); + } + } + } + } + + @FFDCIgnore({ ChannelException.class, ChainException.class }) + private void removeChannel(String name) { + // Neither of the thrown exceptions are permanent failures: + // they usually indicate that we're the victim of a race. + // If the CFW is also tearing down the chain at the same time + // (for example, the SSL feature was removed), then this could + // fail. + try { + framework.removeChannel(name); + } catch (ChannelException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error removing channel " + name, this, e); + } + } catch (ChainException e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Error removing channel " + name, this, e); + } + } + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public void chainInitialized(ChainData chainData) { + state().set(ChainState.INITIALIZED); + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public synchronized void chainStarted(ChainData chainData) { + state().set(ChainState.STARTED); + port = activatePort(); + + if (port > 0) { + // HOORAY! we have a bound listener. + // Notify listeners that the chain was started. + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "New configuration started " + config()); + } + + VirtualHostMap.notifyStarted(endpoint(), () -> host, port, isHttps()); + + // Post an endpoint started event to anyone listening + String topic = endpoint().getEventTopic() + HttpServiceConstants.ENDPOINT_STARTED; + postEvent(topic, config(), null); + } + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public void chainStopped(ChainData chainData) { + + int oldState = state().getAndSet(ChainState.STOPPED).value(); + if (oldState > ChainState.QUIESCED.value()) { + quiesceChain(); + } + + // Wake up anything waiting for the chain to stop + // (see the update method for one example) + stopWait.notifyStopped(); + + // Post an endpoint stopped event to anyone listening + String topic = endpoint().getEventTopic() + HttpServiceConstants.ENDPOINT_STOPPED; + postEvent(topic, config(), null); + this.port = -1; + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public void chainQuiesced(ChainData chainData) { + int oldState = state().getAndSet(ChainState.QUIESCED).value(); + if (oldState > ChainState.QUIESCED.value()) { + quiesceChain(); + } + } + + private void quiesceChain() { + // Notify the owner (which notifies the virtual hosts) that + // we have stopped (or are in the process of stopping) listening.. + VirtualHostMap.notifyStopped(endpoint(), host, port, isHttps()); + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public void chainDestroyed(ChainData chainData) { + state().set(ChainState.DESTROYED); + } + + /** + * ChainEventListener method. + * This method can not be synchronized (deadlock with update/stop). + * Rely on CFW synchronization of chain operations. + */ + @Override + public void chainUpdated(ChainData chainData) { + // Not Applicable: this method is only called when the channels comprising the + // chain change. We're using fixed chain configurations (in terms of channel + // elements). + } + + /** + * @return true if the active port matches the listening port. False otherwise (not listening or no match) + */ + public boolean validateActivePort() { + try { + return port == framework.getListeningPort(chainName); + } catch (ChainException ce) { + } + return false; + } + + /** + * @return the active port, if it can be determined, or -1. + */ + @FFDCIgnore(ChainException.class) + public int activatePort() { + if (config== null || config.port() < 0) + return -1; + + if (port == -1) { + try { + port = framework.getListeningPort(chainName); + } catch (ChainException ce) { + port = -1; + } + } + return port; + } + + + private class StopWait { + + @Trivial + StopWait() { + } + + public synchronized void waitForStop(long timeout) { + // HttpChain parameter helps with debug.. + + // wait for the configured timeout (the parameter) + a smidgen of time + // to allow the cfw to stop the chain after that configured quiesce + // timeout expires + long interval = timeout + 2345L; + long waited = 0; + + // If, as far as we know, the chain hasn't been stopped yet, wait for + // the stop notification for at most the timeout amount of time. + while (state().get().value() > ChainState.STOPPED.value() && waited < interval) { + long start = System.nanoTime(); + try { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(LegacyHttpChain.this, tc, "Waiting for chain stop", waited, interval); + } + wait(interval - waited); + } catch (InterruptedException ie) { + // ignore + } + waited += System.nanoTime() - start; + } + } + + synchronized void notifyStopped() { + notifyAll(); + } + } +} \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/package-info.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/package-info.java new file mode 100644 index 000000000000..75602574bfda --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/channel/package-info.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +@org.osgi.annotation.versioning.Version("1.0") +package io.openliberty.http.channel; + diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/channel/NettyHttpChain.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/channel/NettyHttpChain.java new file mode 100644 index 000000000000..5fbc05bd7fc7 --- /dev/null +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/channel/NettyHttpChain.java @@ -0,0 +1,301 @@ +/******************************************************************************* + * Copyright (c) 2025 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.netty.channel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.osgi.framework.Constants; + +import com.ibm.websphere.channelfw.EndPointInfo; +import com.ibm.websphere.channelfw.osgi.CHFWBundle; +import com.ibm.websphere.ras.Tr; +import com.ibm.websphere.ras.TraceComponent; +import com.ibm.ws.http.channel.internal.HttpConfigConstants; +import com.ibm.ws.http.internal.HttpEndpointImpl; +import com.ibm.ws.http.internal.HttpServiceConstants; +import com.ibm.ws.http.internal.VirtualHostMap; +import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer; +import com.ibm.ws.http.netty.pipeline.HttpPipelineInitializer.ConfigElement; +import com.ibm.wsspi.kernel.service.utils.FrameworkState; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.openliberty.http.channel.AbstractHttpChain; +import io.openliberty.http.channel.ChainConfiguration; +import io.openliberty.http.channel.ChainState; +import io.openliberty.http.netty.quiesce.QuiesceStrategy; +import io.openliberty.netty.internal.ConfigConstants; +import io.openliberty.netty.internal.NettyFramework; +import io.openliberty.netty.internal.ServerBootstrapExtended; +import io.openliberty.netty.internal.exception.NettyException; + + +public class NettyHttpChain extends AbstractHttpChain { + + private static final TraceComponent tc = Tr.register(NettyHttpChain.class); + + private NettyFramework framework; + private ServerBootstrapExtended bootstrap; + private volatile Channel channel; + + public NettyHttpChain(HttpEndpointImpl endpoint, boolean isHttps){ + super(endpoint, isHttps); + } + + public synchronized void init(String endpointId, NettyFramework framework) { + Objects.requireNonNull(framework, "NettyFramework cannot be null"); + this.framework = framework; + this.endpointManager = framework.getEndpointManager(); + + final String root = endpointId + (isHttps() ? "-ssl" : ""); + + endpointName = root; + tcpName = "TCP-" + root; + sslName = isHttps() ? "SSL-" + root : null; + httpName = "HTTP-" + root; + dispatcherName = "HTTPD-" + root; + chainName = "CHAIN-" + root; + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(tc, "Netty Chain initialized: Endpoint ID = " + endpointId + ", Endpoint Name = " + root); + } + } + + @Override + public void stop() { + synchronized(this){ + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.entry(this, tc, "Stopping Netty Chain: " + endpointName + ", Current state: " + state().get()); + } + + if (state().get() == ChainState.STARTED || state().get() == ChainState.STARTING) { + endpointManager.removeEndPoint(endpointName); + state().set(ChainState.STOPPING); + + try { + if (channel != null && channel.isOpen()) { + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Server Channel is open, attempting to close"); + } + + framework.stop(channel, -1); + channel.closeFuture().syncUninterruptibly(); + channel = null; + } + + } finally { + + VirtualHostMap.notifyStopped(endpoint(), activeHost(), config().port(), isHttps()); + this.port = -1; + String topic = endpoint().getEventTopic() + HttpServiceConstants.ENDPOINT_STOPPED; + postEvent(topic, config(), null); + + state().set(ChainState.STOPPED); + + } + } else { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(tc, "Netty Chain is not in a stoppable state. Current state: " + state().get()); + } + } + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.exit(this, tc, "stop chain " + this); + } + } + } + + private void stopAndWait() { + if (state().get() != ChainState.STOPPED && state().get() != ChainState.UNINITIALIZED) { + stop(); + } + } + + @Override + public synchronized void update(String host) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.entry(this, tc, "Updating Netty Chain " + endpointName + " Current state: " + state().get()); + } + + if (!enabled() || FrameworkState.isStopping()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.exit(this, tc, "Chain is disabled or framework is stopping, skipping update "); + } + return; + } + + ChainConfiguration newConfig = new ChainConfiguration(isHttps(), + endpoint().getTcpOptions(), + endpoint().getSslOptions(), + endpoint().getHttpOptions(), + endpoint().getEndpointOptions(), + endpoint().getRemoteIpConfig(), + endpoint().getCompressionConfig(), + endpoint().getSamesiteConfig(), + endpoint().getHeadersConfig()); + + if (!newConfig.isComplete()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Stopping chain due to configuration " + newConfig); + } + // save the new/changed configuration before we start setting up the new chain + this.config = newConfig; + this.host = host; + stopAndWait(); + state().set(ChainState.UNINITIALIZED); + } + + else { + + if (newConfig.requiresRestart(config)) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "This configuration differs and should cause an update "); + } + config = newConfig; + this.host = host; + if (state().get() != ChainState.UNINITIALIZED) { + stopAndWait(); + } + } + start(); + } + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Channel restarted with new configuration"); + } + + } + + public synchronized void start(){ + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.entry(this, tc, "Starting Netty Channel: " + endpointName + ", Current state: " + state().get() + ", Enabled: " + enabled()); + } + if (state().compareAndSet(ChainState.STOPPED, ChainState.STARTING) || state().compareAndSet(ChainState.UNINITIALIZED, ChainState.STARTING)) { + try { + Map httpOptions = new HashMap<>(endpoint().getHttpOptions()); + httpOptions.put(HttpConfigConstants.PROPNAME_ACCESSLOG_ID, endpoint().getName()); + // Put the protocol version, which allows the http channel to dynamically + // know what http version it will use. + if (endpoint().getProtocolVersion() != null) { + httpOptions.put(HttpConfigConstants.PROPNAME_PROTOCOL_VERSION, endpoint().getProtocolVersion()); + } + + EndPointInfo info = endpointManager.defineEndPoint(this.endpointName, config().host(), config().port()); + + Map tcpOptions = new HashMap<>(endpoint().getTcpOptions()); + tcpOptions.put(ConfigConstants.EXTERNAL_NAME, endpointName); + + bootstrap = framework.createTCPBootstrap(tcpOptions); + HttpPipelineInitializer.HttpPipelineBuilder pipelineBuilder = new HttpPipelineInitializer.HttpPipelineBuilder(this) + .with(ConfigElement.COMPRESSION, endpoint().getCompressionConfig()) + .with(ConfigElement.HTTP_OPTIONS, httpOptions) + .with(ConfigElement.HEADERS, endpoint().getHeadersConfig()) + .with(ConfigElement.REMOTE_IP, endpoint().getRemoteIpConfig()) + .with(ConfigElement.SAMESITE, endpoint().getSamesiteConfig()); + + // Add SSL options only if the chain is SSL-enabled + if (this.isHttps()) { + Map sslOptions = new HashMap<>(endpoint().getSslOptions()); + pipelineBuilder.with(ConfigElement.SSL_OPTIONS, sslOptions); + } + + HttpPipelineInitializer httpPipeline = pipelineBuilder.build(); + + bootstrap.childOption(ChannelOption.ALLOW_HALF_CLOSURE, true); + bootstrap.childHandler(httpPipeline); + + channel = framework.start(bootstrap, info.getHost(), info.getPort(), this::channelFutureHandler); + + VirtualHostMap.notifyStarted(endpoint(), () -> activeHost(), config().port(), isHttps()); + String topic = endpoint().getEventTopic() + HttpServiceConstants.ENDPOINT_STARTED; + postEvent(topic, config(), null); + + } catch (Exception e) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.exit(this, tc, "Failed to start Netty Channel: " + e.getMessage()); + } + state().set(ChainState.STOPPED); + } + } + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.exit(this, tc, "Finished starting Netty Channel: " + endpointName + ", Final state: " + state().get()); + } + } + + private void channelFutureHandler(ChannelFuture future) { + if (state().get() == ChainState.STOPPING) { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Chain: " + endpointName + ", Current state: " + state().get() + ", is stopping so will not notify any virtual hosts and will just return"); + } + return; + } + synchronized (this) { + if (future.isSuccess()) { + state().set(ChainState.STARTED); + EndPointInfo info = endpointManager.getEndPoint(this.endpointName); + info = endpointManager.defineEndPoint(this.endpointName, config().host(), config().port()); + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Channel is now active and listening on port " + activePort()); + } + } else { + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Channel failed to bind to port: " + future.cause()); + } + handleStartupError(new NettyException(future.cause()), config()); + + if (config() != null) { + VirtualHostMap.notifyStopped(endpoint(), activeHost(), activePort(), isHttps()); + port = -1; + } + state().set(ChainState.STOPPED); + } + //Register chain for quiesce, NO_OP is passed as the task as there is no special + //quiesce action required at this time + framework.registerEndpointQuiesce(future.channel(), QuiesceStrategy.NO_OP.getTask()); + notifyAll(); + } + } + + public ServerBootstrapExtended getBootstrap() { + return bootstrap; + } + + //TODO: move this to more global location, perhaps httpEndpoint + public boolean isHttp2Enabled() { + String protocolVersion = endpoint().getProtocolVersion(); + Boolean defaultSetting = CHFWBundle.getHttp2DefaultSetting(); + + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Protocol version found to be: " + protocolVersion); + } + if (defaultSetting == null) // No default configured, only HTTP 1.1 is enabled + return false; + else + return defaultSetting == Boolean.TRUE ? !!!HttpConfigConstants.PROTOCOL_VERSION_11.equalsIgnoreCase(protocolVersion) : HttpConfigConstants.PROTOCOL_VERSION_2.equalsIgnoreCase(protocolVersion); + } + + public String getEndpointPID() { + return (config() != null) ? (String) config().endpointOptions().get(Constants.SERVICE_PID) : null; + } + + public EndPointInfo getEndpointInfo() { + EndPointInfo info = endpointManager.getEndPoint(endpointName); + + if (info == null && state().get() == ChainState.STARTED) { + info = endpointManager.defineEndPoint(this.endpointName, host, port); + } + + return info; + } +} \ No newline at end of file