/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed 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.springframework.integration.x.http;
import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.DefaultChannelPipeline;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
import org.jboss.netty.handler.logging.LoggingHandler;
import org.jboss.netty.handler.ssl.SslHandler;
import org.jboss.netty.logging.CommonsLoggerFactory;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.jboss.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @author Mark Fisher
* @author Jennifer Hickey
* @author Gary Russell
* @author Marius Bogoevici
* @author David Turanski
*/
public class NettyHttpInboundChannelAdapter extends MessageProducerSupport {
private static Logger logger = LoggerFactory.getLogger(NettyHttpInboundChannelAdapter.class);
/**
* Default max number of threads for the default {@link Executor}
*/
private static final int DEFAULT_CORE_POOL_SIZE = 16;
/**
* Default max total size of queued events per channel for the default {@link Executor} (in bytes)
*/
private static final long DEFAULT_MAX_CHANNEL_MEMORY_SIZE = 1048576;
/**
* Default max total size of queued events for the whole pool for the default {@link Executor} (in bytes)
*/
private static final long DEFAULT_MAX_TOTAL_MEMORY_SIZE = 1048576;
/**
* Default max content length
*/
private static final int DEFAULT_MAX_CONTENT_LENGTH = 1048576;
private final int port;
private final boolean ssl;
private volatile String keyStore;
private volatile String keyStorePassphrase;
private volatile ServerBootstrap bootstrap;
private volatile ExecutionHandler executionHandler;
private volatile Executor executor = new OrderedMemoryAwareThreadPoolExecutor(DEFAULT_CORE_POOL_SIZE,
DEFAULT_MAX_CHANNEL_MEMORY_SIZE, DEFAULT_MAX_TOTAL_MEMORY_SIZE);
private volatile MessageConverter messageConverter;
private volatile int maxContentLength = DEFAULT_MAX_CONTENT_LENGTH;
static {
// Use commons-logging for Netty logging
InternalLoggerFactory.setDefaultFactory(new CommonsLoggerFactory());
}
/**
* Properties file containing keyStore=[resource], keyStore.passPhrase=[passPhrase]
*/
private volatile Resource sslPropertiesLocation;
private SSLContext sslContext;
public NettyHttpInboundChannelAdapter(int port) {
this(port, false);
}
public NettyHttpInboundChannelAdapter(int port, boolean ssl) {
this.port = port;
this.ssl = ssl;
}
/**
*
* @param executor The {@link Executor} to use with the Netty {@link ExecutionHandler} in the pipeline. This allows
* any potential blocking operations done by message consumers to be removed from the I/O thread. The default
* executor is an {@link OrderedMemoryAwareThreadPoolExecutor}, which is highly recommended because it
* guarantees order of execution within a channel.
*/
public void setExecutor(Executor executor) {
Assert.notNull(executor, "A non-null executor is required");
this.executor = executor;
}
/**
* @param sslPropertiesLocation A properties resource containing a resource with key 'keyStore' and
* a pass phrase with key 'keyStore.passPhrase'.
*/
public void setSslPropertiesLocation(Resource sslPropertiesLocation) {
this.sslPropertiesLocation = sslPropertiesLocation;
}
/**
* Set the message converter; defaults to {@link NettyInboundMessageConverter}.
* @param messageConverter the converter.
*/
public void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
/**
* Set the keyStore location directly as an alternative to using
* sslPropertiesLocation. If sslPropertiesLocation is set, this value will be ignored.
* @param keyStore
*/
public void setKeyStore(String keyStore) {
this.keyStore = keyStore;
}
/**
* Set the keyStore passphrase directly as an alternative to using
* sslPropertiesLocation. If sslPropertiesLocation is set, this value will be ignored.
* @param keyStorePassphrase
*/
public void setKeyStorePassphrase(String keyStorePassphrase) {
this.keyStorePassphrase = keyStorePassphrase;
}
/**
* Set the max content length; default 1Mb.
* @param maxContentLength the max content length.
*/
public void setMaxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
}
@Override
protected void onInit() {
try {
if (this.ssl) {
this.sslContext = initializeSSLContext();
}
}
catch (Exception e) {
throw new BeanInitializationException("failed to initialize", e);
}
super.onInit();
}
@Override
protected void doStart() {
if (this.messageConverter == null) {
this.messageConverter = new NettyInboundMessageConverter(getMessageBuilderFactory());
}
executionHandler = new ExecutionHandler(executor);
bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setPipelineFactory(new PipelineFactory());
bootstrap.bind(new InetSocketAddress(this.port));
}
@Override
protected void doStop() {
if (bootstrap != null) {
bootstrap.shutdown();
}
}
private SSLContext initializeSSLContext() throws Exception {
Assert.state(this.sslPropertiesLocation != null || (StringUtils.hasText
(keyStore) && StringUtils.hasText(this.keyStorePassphrase))
,"either 'sslPropertiesLocation' or 'keyStore' and 'keyStorePassphrase' "
+ "must be set.");
Assert.state( this.sslPropertiesLocation == null || (StringUtils.isEmpty
(keyStore) && StringUtils.isEmpty(keyStorePassphrase)),
"either 'sslPropertiesLocation' or 'keyStore' and 'keyStorePassphrase' "
+ "must be set.");
String keyStoreName = this.keyStore;
String keyStorePassphrase = this.keyStorePassphrase;
if (this.sslPropertiesLocation != null) {
Properties sslProperties = new Properties();
sslProperties.load(this.sslPropertiesLocation.getInputStream());
keyStoreName = sslProperties.getProperty("keyStore");
//For consistency, respect new inline property name and fall back to original
keyStorePassphrase = sslProperties.getProperty("keyStorePassphrase");
if (StringUtils.isEmpty(keyStorePassphrase)) {
keyStorePassphrase = sslProperties.getProperty("keyStore.passPhrase");
}
}
return createSSLContext(keyStoreName, keyStorePassphrase);
}
private SSLContext createSSLContext(String keyStoreName, String keyStorePassPhrase)
throws Exception {
Assert.state(StringUtils.hasText(keyStoreName), "keyStore property cannot be null");
Assert.state(StringUtils.hasText(keyStorePassPhrase),
"keyStorePassPhrase property cannot be null");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource keyStore = resolver.getResource(keyStoreName);
SSLContext sslContext = SSLContext.getInstance("TLS");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(keyStore.getInputStream(), keyStorePassPhrase.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, keyStorePassPhrase.toCharArray());
sslContext.init(kmf.getKeyManagers(), null, null);
return sslContext;
}
private class PipelineFactory implements ChannelPipelineFactory {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = new DefaultChannelPipeline();
if (NettyHttpInboundChannelAdapter.this.ssl) {
SSLEngine engine = sslContext.createSSLEngine();
engine.setUseClientMode(false);
pipeline.addLast("ssl", new SslHandler(engine));
}
LoggingHandler loggingHandler = new LoggingHandler();
if (loggingHandler.getLogger().isDebugEnabled()) {
pipeline.addLast("logger", loggingHandler);
}
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("aggregator", new HttpChunkAggregator(maxContentLength));
pipeline.addLast("errorHandler", new SimpleChannelHandler() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
throws Exception {
if (e.getCause() instanceof TooLongFrameException) {
HttpResponse err = new DefaultHttpResponse(HTTP_1_1,
REQUEST_ENTITY_TOO_LARGE);
e.getChannel().write(err).addListener(ChannelFutureListener.CLOSE);
}
}
});
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("compressor", new HttpContentCompressor() {
/*
* Required because the content compressor rejects a reply when no request
* has yet been processed. Even through the exception is caused further down
* the pipleline, writes go through all handlers.
*/
@Override
public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object msg = e.getMessage();
if (msg instanceof HttpResponse
&& ((HttpResponse) e.getMessage()).getStatus().equals(REQUEST_ENTITY_TOO_LARGE)) {
ctx.sendDownstream(e);
}
else {
super.writeRequested(ctx, e);
}
}
});
pipeline.addLast("executionHandler", executionHandler);
pipeline.addLast("handler", new Handler(messageConverter));
return pipeline;
}
}
private class Handler extends SimpleChannelUpstreamHandler {
private final MessageConverter messageConverter;
public Handler(MessageConverter messageConverter) {
Assert.notNull(messageConverter, "'messageConverter' must not be null");
this.messageConverter = messageConverter;
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Assert.isInstanceOf(HttpRequest.class, e.getMessage());
HttpRequest request = (HttpRequest) e.getMessage();
if (logger.isDebugEnabled()) {
logger.debug("Received HTTP request:\n" + indent(e.getMessage().toString()));
}
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
Message<?> message = null;
try {
message = this.messageConverter.toMessage(request, null);
}
catch (MessageConversionException ex) {
logger.error("Failed to convert message", ex);
response = new DefaultHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
}
if (message != null) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Sending message: " + message);
}
sendMessage(message);
}
catch (Exception ex) {
logger.error("Error sending message", ex);
response = new DefaultHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
}
}
writeResponse(request, response, e.getChannel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
logger.error("Unhandled exception, closing channel", e.getCause());
e.getChannel().close();
}
private void writeResponse(HttpRequest request, HttpResponse response, Channel channel) {
boolean keepAlive = isKeepAlive(request);
if (keepAlive) {
response.setHeader(CONTENT_LENGTH, response.getContent().readableBytes());
response.setHeader(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
if (logger.isDebugEnabled()) {
logger.debug("Sending HTTP response:\n" + indent(response.toString()));
}
ChannelFuture future = channel.write(response);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}
/**
* Indents the content of a multi-line string - used mainly for allowing the pretty display of Netty {@code toString()} output/
*/
private static String indent(String s) {
return "\t" + s.replace(StringUtil.NEWLINE, StringUtil.NEWLINE + "\t");
}
}