/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.core.remoting.impl.netty;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetector.Level;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.client.ActiveMQClientLogger;
import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle;
import org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQClientProtocolManager;
import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport;
import org.apache.activemq.artemis.core.server.ActiveMQComponent;
import org.apache.activemq.artemis.spi.core.remoting.AbstractConnector;
import org.apache.activemq.artemis.spi.core.remoting.BaseConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.BufferHandler;
import org.apache.activemq.artemis.spi.core.remoting.ClientConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.FutureLatch;
import org.apache.activemq.artemis.utils.IPV6Util;
import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.utils.Base64.encodeBytes;
public class NettyConnector extends AbstractConnector {
private static final Logger logger = Logger.getLogger(NettyConnector.class);
// Constants -----------------------------------------------------
public static final String JAVAX_KEYSTORE_PATH_PROP_NAME = "javax.net.ssl.keyStore";
public static final String JAVAX_KEYSTORE_PASSWORD_PROP_NAME = "javax.net.ssl.keyStorePassword";
public static final String JAVAX_TRUSTSTORE_PATH_PROP_NAME = "javax.net.ssl.trustStore";
public static final String JAVAX_TRUSTSTORE_PASSWORD_PROP_NAME = "javax.net.ssl.trustStorePassword";
public static final String ACTIVEMQ_KEYSTORE_PROVIDER_PROP_NAME = "org.apache.activemq.ssl.keyStoreProvider";
public static final String ACTIVEMQ_KEYSTORE_PATH_PROP_NAME = "org.apache.activemq.ssl.keyStore";
public static final String ACTIVEMQ_KEYSTORE_PASSWORD_PROP_NAME = "org.apache.activemq.ssl.keyStorePassword";
public static final String ACTIVEMQ_TRUSTSTORE_PROVIDER_PROP_NAME = "org.apache.activemq.ssl.trustStoreProvider";
public static final String ACTIVEMQ_TRUSTSTORE_PATH_PROP_NAME = "org.apache.activemq.ssl.trustStore";
public static final String ACTIVEMQ_TRUSTSTORE_PASSWORD_PROP_NAME = "org.apache.activemq.ssl.trustStorePassword";
// Constants for HTTP upgrade
// These constants are exposed publicly as they are used on the server-side to fetch
// headers from the HTTP request, compute some values and fill the HTTP response
public static final String MAGIC_NUMBER = "CF70DEB8-70F9-4FBA-8B4F-DFC3E723B4CD";
public static final String SEC_ACTIVEMQ_REMOTING_KEY = "Sec-ActiveMQRemoting-Key";
public static final String SEC_ACTIVEMQ_REMOTING_ACCEPT = "Sec-ActiveMQRemoting-Accept";
public static final String ACTIVEMQ_REMOTING = "activemq-remoting";
private static final AttributeKey<String> REMOTING_KEY = AttributeKey.valueOf(SEC_ACTIVEMQ_REMOTING_KEY);
// Default Configuration
public static final Map<String, Object> DEFAULT_CONFIG;
static {
// Disable resource leak detection for performance reasons by default
ResourceLeakDetector.setLevel(Level.DISABLED);
// Set default Configuration
Map<String, Object> config = new HashMap<>();
config.put(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST);
config.put(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT);
DEFAULT_CONFIG = Collections.unmodifiableMap(config);
}
// Attributes ----------------------------------------------------
private Class<? extends Channel> channelClazz;
private Bootstrap bootstrap;
private ChannelGroup channelGroup;
private final BufferHandler handler;
private final BaseConnectionLifeCycleListener<?> listener;
private boolean sslEnabled = TransportConstants.DEFAULT_SSL_ENABLED;
private boolean httpEnabled;
private long httpMaxClientIdleTime;
private long httpClientIdleScanPeriod;
private boolean httpRequiresSessionId;
// if true, after the connection, the connector will send
// a HTTP GET request (+ Upgrade: activemq-remoting) that
// will be handled by the server's http server.
private boolean httpUpgradeEnabled;
private boolean useServlet;
private String host;
private int port;
private String localAddress;
private int localPort;
private String keyStoreProvider;
private String keyStorePath;
private String keyStorePassword;
private String trustStoreProvider;
private String trustStorePath;
private String trustStorePassword;
private String enabledCipherSuites;
private String enabledProtocols;
private boolean verifyHost;
private boolean useDefaultSslContext;
private boolean tcpNoDelay;
private int tcpSendBufferSize;
private int tcpReceiveBufferSize;
private final int writeBufferLowWaterMark;
private final int writeBufferHighWaterMark;
private long batchDelay;
private ConcurrentMap<Object, Connection> connections = new ConcurrentHashMap<>();
private String servletPath;
private boolean useEpoll;
private int remotingThreads;
private boolean useGlobalWorkerPool;
private ScheduledExecutorService scheduledThreadPool;
private Executor closeExecutor;
private BatchFlusher flusher;
private ScheduledFuture<?> batchFlusherFuture;
private EventLoopGroup group;
private int connectTimeoutMillis;
private final ClientProtocolManager protocolManager;
// Static --------------------------------------------------------
// Constructors --------------------------------------------------
// Public --------------------------------------------------------
public NettyConnector(final Map<String, Object> configuration,
final BufferHandler handler,
final BaseConnectionLifeCycleListener<?> listener,
final Executor closeExecutor,
final Executor threadPool,
final ScheduledExecutorService scheduledThreadPool) {
this(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, new ActiveMQClientProtocolManager());
}
public NettyConnector(final Map<String, Object> configuration,
final BufferHandler handler,
final BaseConnectionLifeCycleListener<?> listener,
final Executor closeExecutor,
final Executor threadPool,
final ScheduledExecutorService scheduledThreadPool,
final ClientProtocolManager protocolManager) {
super(configuration);
this.protocolManager = protocolManager;
if (listener == null) {
throw ActiveMQClientMessageBundle.BUNDLE.nullListener();
}
if (handler == null) {
throw ActiveMQClientMessageBundle.BUNDLE.nullHandler();
}
this.listener = listener;
this.handler = handler;
sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration);
httpEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.HTTP_ENABLED_PROP_NAME, TransportConstants.DEFAULT_HTTP_ENABLED, configuration);
servletPath = ConfigurationHelper.getStringProperty(TransportConstants.SERVLET_PATH, TransportConstants.DEFAULT_SERVLET_PATH, configuration);
if (httpEnabled) {
httpMaxClientIdleTime = ConfigurationHelper.getLongProperty(TransportConstants.HTTP_CLIENT_IDLE_PROP_NAME, TransportConstants.DEFAULT_HTTP_CLIENT_IDLE_TIME, configuration);
httpClientIdleScanPeriod = ConfigurationHelper.getLongProperty(TransportConstants.HTTP_CLIENT_IDLE_SCAN_PERIOD, TransportConstants.DEFAULT_HTTP_CLIENT_SCAN_PERIOD, configuration);
httpRequiresSessionId = ConfigurationHelper.getBooleanProperty(TransportConstants.HTTP_REQUIRES_SESSION_ID, TransportConstants.DEFAULT_HTTP_REQUIRES_SESSION_ID, configuration);
} else {
httpMaxClientIdleTime = 0;
httpClientIdleScanPeriod = -1;
httpRequiresSessionId = false;
}
httpUpgradeEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.HTTP_UPGRADE_ENABLED_PROP_NAME, TransportConstants.DEFAULT_HTTP_UPGRADE_ENABLED, configuration);
remotingThreads = ConfigurationHelper.getIntProperty(TransportConstants.NIO_REMOTING_THREADS_PROPNAME, -1, configuration);
remotingThreads = ConfigurationHelper.getIntProperty(TransportConstants.REMOTING_THREADS_PROPNAME, remotingThreads, configuration);
useGlobalWorkerPool = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_NIO_GLOBAL_WORKER_POOL_PROP_NAME, TransportConstants.DEFAULT_USE_GLOBAL_WORKER_POOL, configuration);
useGlobalWorkerPool = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME, useGlobalWorkerPool, configuration);
useEpoll = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_EPOLL_PROP_NAME, TransportConstants.DEFAULT_USE_EPOLL, configuration);
useServlet = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_SERVLET_PROP_NAME, TransportConstants.DEFAULT_USE_SERVLET, configuration);
host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, configuration);
port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, configuration);
localAddress = ConfigurationHelper.getStringProperty(TransportConstants.LOCAL_ADDRESS_PROP_NAME, TransportConstants.DEFAULT_LOCAL_ADDRESS, configuration);
localPort = ConfigurationHelper.getIntProperty(TransportConstants.LOCAL_PORT_PROP_NAME, TransportConstants.DEFAULT_LOCAL_PORT, configuration);
if (sslEnabled) {
keyStoreProvider = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PROVIDER, configuration);
keyStorePath = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PATH_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PATH, configuration);
keyStorePassword = ConfigurationHelper.getPasswordProperty(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PASSWORD, configuration, ActiveMQDefaultConfiguration.getPropMaskPassword(), ActiveMQDefaultConfiguration.getPropPasswordCodec());
trustStoreProvider = ConfigurationHelper.getStringProperty(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER, configuration);
trustStorePath = ConfigurationHelper.getStringProperty(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, TransportConstants.DEFAULT_TRUSTSTORE_PATH, configuration);
trustStorePassword = ConfigurationHelper.getPasswordProperty(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, TransportConstants.DEFAULT_TRUSTSTORE_PASSWORD, configuration, ActiveMQDefaultConfiguration.getPropMaskPassword(), ActiveMQDefaultConfiguration.getPropPasswordCodec());
enabledCipherSuites = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES, configuration);
enabledProtocols = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, TransportConstants.DEFAULT_ENABLED_PROTOCOLS, configuration);
verifyHost = ConfigurationHelper.getBooleanProperty(TransportConstants.VERIFY_HOST_PROP_NAME, TransportConstants.DEFAULT_VERIFY_HOST, configuration);
useDefaultSslContext = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_DEFAULT_SSL_CONTEXT_PROP_NAME, TransportConstants.DEFAULT_USE_DEFAULT_SSL_CONTEXT, configuration);
} else {
keyStoreProvider = TransportConstants.DEFAULT_KEYSTORE_PROVIDER;
keyStorePath = TransportConstants.DEFAULT_KEYSTORE_PATH;
keyStorePassword = TransportConstants.DEFAULT_KEYSTORE_PASSWORD;
trustStoreProvider = TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER;
trustStorePath = TransportConstants.DEFAULT_TRUSTSTORE_PATH;
trustStorePassword = TransportConstants.DEFAULT_TRUSTSTORE_PASSWORD;
enabledCipherSuites = TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES;
enabledProtocols = TransportConstants.DEFAULT_ENABLED_PROTOCOLS;
verifyHost = TransportConstants.DEFAULT_VERIFY_HOST;
useDefaultSslContext = TransportConstants.DEFAULT_USE_DEFAULT_SSL_CONTEXT;
}
tcpNoDelay = ConfigurationHelper.getBooleanProperty(TransportConstants.TCP_NODELAY_PROPNAME, TransportConstants.DEFAULT_TCP_NODELAY, configuration);
tcpSendBufferSize = ConfigurationHelper.getIntProperty(TransportConstants.TCP_SENDBUFFER_SIZE_PROPNAME, TransportConstants.DEFAULT_TCP_SENDBUFFER_SIZE, configuration);
tcpReceiveBufferSize = ConfigurationHelper.getIntProperty(TransportConstants.TCP_RECEIVEBUFFER_SIZE_PROPNAME, TransportConstants.DEFAULT_TCP_RECEIVEBUFFER_SIZE, configuration);
this.writeBufferLowWaterMark = ConfigurationHelper.getIntProperty(TransportConstants.WRITE_BUFFER_LOW_WATER_MARK_PROPNAME, TransportConstants.DEFAULT_WRITE_BUFFER_LOW_WATER_MARK, configuration);
this.writeBufferHighWaterMark = ConfigurationHelper.getIntProperty(TransportConstants.WRITE_BUFFER_HIGH_WATER_MARK_PROPNAME, TransportConstants.DEFAULT_WRITE_BUFFER_HIGH_WATER_MARK, configuration);
batchDelay = ConfigurationHelper.getLongProperty(TransportConstants.BATCH_DELAY, TransportConstants.DEFAULT_BATCH_DELAY, configuration);
connectTimeoutMillis = ConfigurationHelper.getIntProperty(TransportConstants.NETTY_CONNECT_TIMEOUT, TransportConstants.DEFAULT_NETTY_CONNECT_TIMEOUT, configuration);
this.closeExecutor = closeExecutor;
this.scheduledThreadPool = scheduledThreadPool;
}
@Override
public String toString() {
return "NettyConnector [host=" + host +
", port=" +
port +
", httpEnabled=" +
httpEnabled +
", httpUpgradeEnabled=" +
httpUpgradeEnabled +
", useServlet=" +
useServlet +
", servletPath=" +
servletPath +
", sslEnabled=" +
sslEnabled +
", useNio=" +
true +
getHttpUpgradeInfo() +
"]";
}
private String getHttpUpgradeInfo() {
if (!httpUpgradeEnabled) {
return "";
}
String serverName = ConfigurationHelper.getStringProperty(TransportConstants.ACTIVEMQ_SERVER_NAME, null, configuration);
String acceptor = ConfigurationHelper.getStringProperty(TransportConstants.HTTP_UPGRADE_ENDPOINT_PROP_NAME, null, configuration);
return ", activemqServerName=" + serverName + ", httpUpgradeEndpoint=" + acceptor;
}
@Override
public synchronized void start() {
if (channelClazz != null) {
return;
}
if (remotingThreads == -1) {
// Default to number of cores * 3
remotingThreads = Runtime.getRuntime().availableProcessors() * 3;
}
if (useEpoll && Epoll.isAvailable()) {
if (useGlobalWorkerPool) {
group = SharedEventLoopGroup.getInstance((threadFactory -> new EpollEventLoopGroup(remotingThreads, threadFactory)));
} else {
group = new EpollEventLoopGroup(remotingThreads);
}
channelClazz = EpollSocketChannel.class;
logger.debug("Connector " + this + " using native epoll");
} else {
if (useGlobalWorkerPool) {
channelClazz = NioSocketChannel.class;
group = SharedEventLoopGroup.getInstance((threadFactory -> new NioEventLoopGroup(remotingThreads, threadFactory)));
} else {
channelClazz = NioSocketChannel.class;
group = new NioEventLoopGroup(remotingThreads);
}
channelClazz = NioSocketChannel.class;
logger.debug("Connector + " + this + " using nio");
}
// if we are a servlet wrap the socketChannelFactory
bootstrap = new Bootstrap();
bootstrap.channel(channelClazz);
bootstrap.group(group);
bootstrap.option(ChannelOption.TCP_NODELAY, tcpNoDelay);
if (connectTimeoutMillis != -1) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
}
if (tcpReceiveBufferSize != -1) {
bootstrap.option(ChannelOption.SO_RCVBUF, tcpReceiveBufferSize);
}
if (tcpSendBufferSize != -1) {
bootstrap.option(ChannelOption.SO_SNDBUF, tcpSendBufferSize);
}
final int writeBufferLowWaterMark = this.writeBufferLowWaterMark != -1 ? this.writeBufferLowWaterMark : WriteBufferWaterMark.DEFAULT.low();
final int writeBufferHighWaterMark = this.writeBufferHighWaterMark != -1 ? this.writeBufferHighWaterMark : WriteBufferWaterMark.DEFAULT.high();
final WriteBufferWaterMark writeBufferWaterMark = new WriteBufferWaterMark(writeBufferLowWaterMark, writeBufferHighWaterMark);
bootstrap.option(ChannelOption.WRITE_BUFFER_WATER_MARK, writeBufferWaterMark);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.SO_REUSEADDR, true);
channelGroup = new DefaultChannelGroup("activemq-connector", GlobalEventExecutor.INSTANCE);
final SSLContext context;
if (sslEnabled) {
try {
if (useDefaultSslContext) {
context = SSLContext.getDefault();
} else {
// HORNETQ-680 - override the server-side config if client-side system properties are set
String realKeyStorePath = keyStorePath;
String realKeyStoreProvider = keyStoreProvider;
String realKeyStorePassword = keyStorePassword;
if (System.getProperty(JAVAX_KEYSTORE_PATH_PROP_NAME) != null) {
realKeyStorePath = System.getProperty(JAVAX_KEYSTORE_PATH_PROP_NAME);
}
if (System.getProperty(JAVAX_KEYSTORE_PASSWORD_PROP_NAME) != null) {
realKeyStorePassword = System.getProperty(JAVAX_KEYSTORE_PASSWORD_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_KEYSTORE_PROVIDER_PROP_NAME) != null) {
realKeyStoreProvider = System.getProperty(ACTIVEMQ_KEYSTORE_PROVIDER_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_KEYSTORE_PATH_PROP_NAME) != null) {
realKeyStorePath = System.getProperty(ACTIVEMQ_KEYSTORE_PATH_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_KEYSTORE_PASSWORD_PROP_NAME) != null) {
realKeyStorePassword = System.getProperty(ACTIVEMQ_KEYSTORE_PASSWORD_PROP_NAME);
}
String realTrustStorePath = trustStorePath;
String realTrustStoreProvider = trustStoreProvider;
String realTrustStorePassword = trustStorePassword;
if (System.getProperty(JAVAX_TRUSTSTORE_PATH_PROP_NAME) != null) {
realTrustStorePath = System.getProperty(JAVAX_TRUSTSTORE_PATH_PROP_NAME);
}
if (System.getProperty(JAVAX_TRUSTSTORE_PASSWORD_PROP_NAME) != null) {
realTrustStorePassword = System.getProperty(JAVAX_TRUSTSTORE_PASSWORD_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_TRUSTSTORE_PROVIDER_PROP_NAME) != null) {
realTrustStoreProvider = System.getProperty(ACTIVEMQ_TRUSTSTORE_PROVIDER_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_TRUSTSTORE_PATH_PROP_NAME) != null) {
realTrustStorePath = System.getProperty(ACTIVEMQ_TRUSTSTORE_PATH_PROP_NAME);
}
if (System.getProperty(ACTIVEMQ_TRUSTSTORE_PASSWORD_PROP_NAME) != null) {
realTrustStorePassword = System.getProperty(ACTIVEMQ_TRUSTSTORE_PASSWORD_PROP_NAME);
}
context = SSLSupport.createContext(realKeyStoreProvider, realKeyStorePath, realKeyStorePassword, realTrustStoreProvider, realTrustStorePath, realTrustStorePassword);
}
} catch (Exception e) {
close();
IllegalStateException ise = new IllegalStateException("Unable to create NettyConnector for " + host + ":" + port);
ise.initCause(e);
throw ise;
}
} else {
context = null; // Unused
}
if (context != null && useServlet) {
// TODO: Fix me
//bootstrap.setOption("sslContext", context);
}
bootstrap.handler(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel channel) throws Exception {
final ChannelPipeline pipeline = channel.pipeline();
if (sslEnabled && !useServlet) {
SSLEngine engine;
if (verifyHost) {
engine = context.createSSLEngine(host, port);
} else {
engine = context.createSSLEngine();
}
engine.setUseClientMode(true);
engine.setWantClientAuth(true);
// setting the enabled cipher suites resets the enabled protocols so we need
// to save the enabled protocols so that after the customer cipher suite is enabled
// we can reset the enabled protocols if a customer protocol isn't specified
String[] originalProtocols = engine.getEnabledProtocols();
if (enabledCipherSuites != null) {
try {
engine.setEnabledCipherSuites(SSLSupport.parseCommaSeparatedListIntoArray(enabledCipherSuites));
} catch (IllegalArgumentException e) {
ActiveMQClientLogger.LOGGER.invalidCipherSuite(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedCipherSuites()));
throw e;
}
}
if (enabledProtocols != null) {
try {
engine.setEnabledProtocols(SSLSupport.parseCommaSeparatedListIntoArray(enabledProtocols));
} catch (IllegalArgumentException e) {
ActiveMQClientLogger.LOGGER.invalidProtocol(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedProtocols()));
throw e;
}
} else {
engine.setEnabledProtocols(originalProtocols);
}
if (verifyHost) {
SSLParameters sslParameters = engine.getSSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
engine.setSSLParameters(sslParameters);
}
SslHandler handler = new SslHandler(engine);
pipeline.addLast(handler);
}
if (httpEnabled) {
pipeline.addLast(new HttpRequestEncoder());
pipeline.addLast(new HttpResponseDecoder());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
pipeline.addLast(new HttpHandler());
}
if (httpUpgradeEnabled) {
// prepare to handle a HTTP 101 response to upgrade the protocol.
final HttpClientCodec httpClientCodec = new HttpClientCodec();
pipeline.addLast(httpClientCodec);
pipeline.addLast("http-upgrade", new HttpUpgradeHandler(pipeline, httpClientCodec));
}
protocolManager.addChannelHandlers(pipeline);
pipeline.addLast(new ActiveMQClientChannelHandler(channelGroup, handler, new Listener()));
}
});
if (batchDelay > 0) {
flusher = new BatchFlusher();
batchFlusherFuture = scheduledThreadPool.scheduleWithFixedDelay(flusher, batchDelay, batchDelay, TimeUnit.MILLISECONDS);
}
logger.debug("Started Netty Connector version " + TransportConstants.NETTY_VERSION);
}
@Override
public synchronized void close() {
if (channelClazz == null) {
return;
}
if (batchFlusherFuture != null) {
batchFlusherFuture.cancel(false);
flusher.cancel();
flusher = null;
batchFlusherFuture = null;
}
bootstrap = null;
channelGroup.close().awaitUninterruptibly();
// Shutdown the EventLoopGroup if no new task was added for 100ms or if
// 3000ms elapsed.
group.shutdownGracefully(100, 3000, TimeUnit.MILLISECONDS);
channelClazz = null;
for (Connection connection : connections.values()) {
listener.connectionDestroyed(connection.getID());
}
connections.clear();
}
@Override
public boolean isStarted() {
return channelClazz != null;
}
@Override
public Connection createConnection() {
if (channelClazz == null) {
return null;
}
// HORNETQ-907 - strip off IPv6 scope-id (if necessary)
SocketAddress remoteDestination = new InetSocketAddress(host, port);
InetAddress inetAddress = ((InetSocketAddress) remoteDestination).getAddress();
if (inetAddress instanceof Inet6Address) {
Inet6Address inet6Address = (Inet6Address) inetAddress;
if (inet6Address.getScopeId() != 0) {
try {
remoteDestination = new InetSocketAddress(InetAddress.getByAddress(inet6Address.getAddress()), ((InetSocketAddress) remoteDestination).getPort());
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
}
logger.debug("Remote destination: " + remoteDestination);
ChannelFuture future;
//port 0 does not work so only use local address if set
if (localPort != 0) {
SocketAddress localDestination;
if (localAddress != null) {
localDestination = new InetSocketAddress(localAddress, localPort);
} else {
localDestination = new InetSocketAddress(localPort);
}
future = bootstrap.connect(remoteDestination, localDestination);
} else {
future = bootstrap.connect(remoteDestination);
}
future.awaitUninterruptibly();
if (future.isSuccess()) {
final Channel ch = future.channel();
SslHandler sslHandler = ch.pipeline().get(SslHandler.class);
if (sslHandler != null) {
Future<Channel> handshakeFuture = sslHandler.handshakeFuture();
if (handshakeFuture.awaitUninterruptibly(30000)) {
if (handshakeFuture.isSuccess()) {
ChannelPipeline channelPipeline = ch.pipeline();
ActiveMQChannelHandler channelHandler = channelPipeline.get(ActiveMQChannelHandler.class);
channelHandler.active = true;
} else {
ch.close().awaitUninterruptibly();
ActiveMQClientLogger.LOGGER.errorCreatingNettyConnection(handshakeFuture.cause());
return null;
}
} else {
//handshakeFuture.setFailure(new SSLException("Handshake was not completed in 30 seconds"));
ch.close().awaitUninterruptibly();
return null;
}
}
if (httpUpgradeEnabled) {
// Send a HTTP GET + Upgrade request that will be handled by the http-upgrade handler.
try {
//get this first incase it removes itself
HttpUpgradeHandler httpUpgradeHandler = (HttpUpgradeHandler) ch.pipeline().get("http-upgrade");
String scheme = "http";
if (sslEnabled) {
scheme = "https";
}
String ipv6Host = IPV6Util.encloseHost(host);
URI uri = new URI(scheme, null, ipv6Host, port, null, null, null);
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath());
request.headers().set(HttpHeaderNames.HOST, ipv6Host);
request.headers().set(HttpHeaderNames.UPGRADE, ACTIVEMQ_REMOTING);
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.UPGRADE);
final String serverName = ConfigurationHelper.getStringProperty(TransportConstants.ACTIVEMQ_SERVER_NAME, null, configuration);
if (serverName != null) {
request.headers().set(TransportConstants.ACTIVEMQ_SERVER_NAME, serverName);
}
final String endpoint = ConfigurationHelper.getStringProperty(TransportConstants.HTTP_UPGRADE_ENDPOINT_PROP_NAME, null, configuration);
if (endpoint != null) {
request.headers().set(TransportConstants.HTTP_UPGRADE_ENDPOINT_PROP_NAME, endpoint);
}
// Get 16 bit nonce and base 64 encode it
byte[] nonce = randomBytes(16);
String key = base64(nonce);
request.headers().set(SEC_ACTIVEMQ_REMOTING_KEY, key);
ch.attr(REMOTING_KEY).set(key);
logger.debugf("Sending HTTP request %s", request);
// Send the HTTP request.
ch.writeAndFlush(request);
if (!httpUpgradeHandler.awaitHandshake()) {
ch.close().awaitUninterruptibly();
return null;
}
} catch (URISyntaxException e) {
ActiveMQClientLogger.LOGGER.errorCreatingNettyConnection(e);
return null;
}
} else {
ChannelPipeline channelPipeline = ch.pipeline();
ActiveMQChannelHandler channelHandler = channelPipeline.get(ActiveMQChannelHandler.class);
channelHandler.active = true;
}
// No acceptor on a client connection
Listener connectionListener = new Listener();
NettyConnection conn = new NettyConnection(configuration, ch, connectionListener, !httpEnabled && batchDelay > 0, false);
connectionListener.connectionCreated(null, conn, protocolManager);
return conn;
} else {
Throwable t = future.cause();
if (t != null && !(t instanceof ConnectException)) {
ActiveMQClientLogger.LOGGER.errorCreatingNettyConnection(future.cause());
}
return null;
}
}
// Public --------------------------------------------------------
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
// Private -------------------------------------------------------
// Inner classes -------------------------------------------------
private static final class ActiveMQClientChannelHandler extends ActiveMQChannelHandler {
ActiveMQClientChannelHandler(final ChannelGroup group,
final BufferHandler handler,
final ClientConnectionLifeCycleListener listener) {
super(group, handler, listener);
}
}
private static class HttpUpgradeHandler extends SimpleChannelInboundHandler<HttpObject> {
private final ChannelPipeline pipeline;
private final HttpClientCodec httpClientCodec;
private final CountDownLatch latch = new CountDownLatch(1);
private boolean handshakeComplete = false;
private HttpUpgradeHandler(ChannelPipeline pipeline, HttpClientCodec httpClientCodec) {
this.pipeline = pipeline;
this.httpClientCodec = httpClientCodec;
}
/**
* HTTP upgrade response will be decode by Netty as 2 objects:
* - 1 HttpObject corresponding to the 101 SWITCHING PROTOCOL headers
* - 1 EMPTY_LAST_CONTENT
*
* The HTTP upgrade is successful whne the 101 SWITCHING PROTOCOL has been received (handshakeComplete = true)
* but the latch is count down only when the following EMPTY_LAST_CONTENT is also received.
* Otherwise this ChannelHandler would be removed too soon and the ActiveMQChannelHandler would handle the
* EMPTY_LAST_CONTENT (while it is expecitng only ByteBuf).
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Received msg=" + msg);
}
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
if (response.status().code() == HttpResponseStatus.SWITCHING_PROTOCOLS.code() && response.headers().get(HttpHeaderNames.UPGRADE).equals(ACTIVEMQ_REMOTING)) {
String accept = response.headers().get(SEC_ACTIVEMQ_REMOTING_ACCEPT);
String expectedResponse = createExpectedResponse(MAGIC_NUMBER, ctx.channel().attr(REMOTING_KEY).get());
if (expectedResponse.equals(accept)) {
// HTTP upgrade is successful but let's wait to receive the EMPTY_LAST_CONTENT to count down the latch
handshakeComplete = true;
} else {
// HTTP upgrade failed
ActiveMQClientLogger.LOGGER.httpHandshakeFailed(msg);
ctx.close();
latch.countDown();
}
return;
}
} else if (msg == LastHttpContent.EMPTY_LAST_CONTENT && handshakeComplete) {
// remove the http handlers and flag the activemq channel handler as active
pipeline.remove(httpClientCodec);
pipeline.remove(this);
ActiveMQChannelHandler channelHandler = pipeline.get(ActiveMQChannelHandler.class);
channelHandler.active = true;
}
if (!handshakeComplete) {
ActiveMQClientLogger.LOGGER.httpHandshakeFailed(msg);
ctx.close();
}
latch.countDown();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ActiveMQClientLogger.LOGGER.errorCreatingNettyConnection(cause);
ctx.close();
}
public boolean awaitHandshake() {
try {
if (!latch.await(30000, TimeUnit.MILLISECONDS)) {
return false;
}
} catch (InterruptedException e) {
return false;
}
return handshakeComplete;
}
}
class HttpHandler extends ChannelDuplexHandler {
private Channel channel;
private long lastSendTime = 0;
private boolean waitingGet = false;
private HttpIdleTimer task;
private final String url;
private final FutureLatch handShakeFuture = new FutureLatch();
private boolean active = false;
private boolean handshaking = false;
private String cookie;
HttpHandler() throws Exception {
url = new URI("http", null, host, port, servletPath, null, null).toString();
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
channel = ctx.channel();
if (httpClientIdleScanPeriod > 0) {
task = new HttpIdleTimer();
java.util.concurrent.Future<?> future = scheduledThreadPool.scheduleAtFixedRate(task, httpClientIdleScanPeriod, httpClientIdleScanPeriod, TimeUnit.MILLISECONDS);
task.setFuture(future);
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
if (task != null) {
task.close();
}
super.channelInactive(ctx);
}
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
FullHttpResponse response = (FullHttpResponse) msg;
if (httpRequiresSessionId && !active) {
final List<String> setCookieHeaderValues = response.headers().getAll(HttpHeaderNames.SET_COOKIE);
for (String setCookieHeaderValue : setCookieHeaderValues) {
final Cookie cookie = ClientCookieDecoder.LAX.decode(setCookieHeaderValue);
if ("JSESSIONID".equals(cookie.name())) {
this.cookie = setCookieHeaderValue;
break;
}
}
active = true;
handShakeFuture.run();
}
waitingGet = false;
ctx.fireChannelRead(response.content());
}
@Override
public void write(final ChannelHandlerContext ctx, final Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) {
if (httpRequiresSessionId && !active) {
if (handshaking) {
handshaking = true;
} else {
if (!handShakeFuture.await(5000)) {
throw new RuntimeException("Handshake failed after timeout");
}
}
}
ByteBuf buf = (ByteBuf) msg;
FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, url, buf);
httpRequest.headers().add(HttpHeaderNames.HOST, NettyConnector.this.host);
if (cookie != null) {
httpRequest.headers().add(HttpHeaderNames.COOKIE, cookie);
}
httpRequest.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(buf.readableBytes()));
ctx.write(httpRequest, promise);
lastSendTime = System.currentTimeMillis();
} else {
ctx.write(msg, promise);
lastSendTime = System.currentTimeMillis();
}
}
private class HttpIdleTimer implements Runnable {
private boolean closed = false;
private java.util.concurrent.Future<?> future;
@Override
public synchronized void run() {
if (closed) {
return;
}
if (!waitingGet && System.currentTimeMillis() > lastSendTime + httpMaxClientIdleTime) {
FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url);
httpRequest.headers().add(HttpHeaderNames.HOST, NettyConnector.this.host);
waitingGet = true;
channel.writeAndFlush(httpRequest);
}
}
public synchronized void setFuture(final java.util.concurrent.Future<?> future) {
this.future = future;
}
public void close() {
if (future != null) {
future.cancel(false);
}
closed = true;
}
}
}
private class Listener implements ClientConnectionLifeCycleListener {
@Override
public void connectionCreated(final ActiveMQComponent component,
final Connection connection,
final ClientProtocolManager protocol) {
if (connections.putIfAbsent(connection.getID(), connection) != null) {
throw ActiveMQClientMessageBundle.BUNDLE.connectionExists(connection.getID());
}
@SuppressWarnings("unchecked")
final BaseConnectionLifeCycleListener<ClientProtocolManager> clientListener = (BaseConnectionLifeCycleListener<ClientProtocolManager>) listener;
clientListener.connectionCreated(component, connection, protocol);
}
@Override
public void connectionDestroyed(final Object connectionID) {
if (connections.remove(connectionID) != null) {
// Execute on different thread to avoid deadlocks
closeExecutor.execute(new Runnable() {
@Override
public void run() {
listener.connectionDestroyed(connectionID);
}
});
}
}
@Override
public void connectionException(final Object connectionID, final ActiveMQException me) {
// Execute on different thread to avoid deadlocks
closeExecutor.execute(new Runnable() {
@Override
public void run() {
listener.connectionException(connectionID, me);
}
});
}
@Override
public void connectionReadyForWrites(Object connectionID, boolean ready) {
NettyConnection connection = (NettyConnection) connections.get(connectionID);
if (connection != null) {
connection.fireReady(ready);
}
listener.connectionReadyForWrites(connectionID, ready);
}
}
private class BatchFlusher implements Runnable {
private boolean cancelled;
@Override
public synchronized void run() {
if (!cancelled) {
for (Connection connection : connections.values()) {
connection.checkFlushBatchBuffer();
}
}
}
public synchronized void cancel() {
cancelled = true;
}
}
@Override
public boolean isEquivalent(Map<String, Object> configuration) {
Boolean httpUpgradeEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.HTTP_UPGRADE_ENABLED_PROP_NAME, TransportConstants.DEFAULT_HTTP_UPGRADE_ENABLED, configuration);
if (httpUpgradeEnabled) {
// we need to look at the activemqServerName to distinguish between ActiveMQ servers that could be proxied behind the same
// HTTP upgrade handler in the Web server
String otherActiveMQServerName = ConfigurationHelper.getStringProperty(TransportConstants.ACTIVEMQ_SERVER_NAME, null, configuration);
String activeMQServerName = ConfigurationHelper.getStringProperty(TransportConstants.ACTIVEMQ_SERVER_NAME, null, this.configuration);
boolean equivalent = isSameHostAndPort(configuration) && otherActiveMQServerName != null && otherActiveMQServerName.equals(activeMQServerName);
return equivalent;
} else {
return isSameHostAndPort(configuration);
}
}
private boolean isSameHostAndPort(Map<String, Object> configuration) {
//here we only check host and port because these two parameters
//is sufficient to determine the target host
String host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, configuration);
Integer port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, configuration);
if (!port.equals(this.port))
return false;
if (host.equals(this.host))
return true;
//The host may be an alias. We need to compare raw IP address.
boolean result = false;
try {
InetAddress inetAddr1 = InetAddress.getByName(host);
InetAddress inetAddr2 = InetAddress.getByName(this.host);
String ip1 = inetAddr1.getHostAddress();
String ip2 = inetAddr2.getHostAddress();
logger.debug(this + " host 1: " + host + " ip address: " + ip1 + " host 2: " + this.host + " ip address: " + ip2);
result = ip1.equals(ip2);
} catch (UnknownHostException e) {
ActiveMQClientLogger.LOGGER.error("Cannot resolve host", e);
}
return result;
}
@Override
public void finalize() throws Throwable {
close();
super.finalize();
}
//for test purpose only
public Bootstrap getBootStrap() {
return bootstrap;
}
public static void clearThreadPools() {
SharedEventLoopGroup.forceShutdown();
}
private static String base64(byte[] data) {
ByteBuf encodedData = Unpooled.wrappedBuffer(data);
ByteBuf encoded = Base64.encode(encodedData);
String encodedString = encoded.toString(StandardCharsets.UTF_8);
encoded.release();
return encodedString;
}
/**
* Creates an arbitrary number of random bytes
*
* @param size the number of random bytes to create
* @return An array of random bytes
*/
private static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
for (int index = 0; index < size; index++) {
bytes[index] = (byte) randomNumber(0, 255);
}
return bytes;
}
private static int randomNumber(int minimum, int maximum) {
return (int) (Math.random() * maximum + minimum);
}
public static String createExpectedResponse(final String magicNumber, final String secretKey) throws IOException {
try {
final String concat = secretKey + magicNumber;
final MessageDigest digest = MessageDigest.getInstance("SHA1");
digest.update(concat.getBytes(StandardCharsets.UTF_8));
final byte[] bytes = digest.digest();
return encodeBytes(bytes);
} catch (NoSuchAlgorithmException e) {
throw new IOException(e);
}
}
}