/*
* Copyright 2013 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 ratpack.server.internal;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ratpack.exec.ExecController;
import ratpack.func.Action;
import ratpack.handling.Handler;
import ratpack.handling.Handlers;
import ratpack.handling.internal.ChainHandler;
import ratpack.handling.internal.DefaultContext;
import ratpack.handling.internal.DescribingHandler;
import ratpack.handling.internal.DescribingHandlers;
import ratpack.http.Headers;
import ratpack.http.MutableHeaders;
import ratpack.http.Response;
import ratpack.http.internal.*;
import ratpack.registry.Registry;
import ratpack.render.internal.DefaultRenderController;
import ratpack.server.ServerConfig;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.security.cert.X509Certificate;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicBoolean;
@ChannelHandler.Sharable
public class NettyHandlerAdapter extends ChannelInboundHandlerAdapter {
private static final AttributeKey<Action<Object>> CHANNEL_SUBSCRIBER_ATTRIBUTE_KEY = AttributeKey.valueOf(NettyHandlerAdapter.class, "subscriber");
private static final AttributeKey<RequestBodyAccumulator> BODY_ACCUMULATOR_KEY = AttributeKey.valueOf(NettyHandlerAdapter.class, "requestBody");
private static final AttributeKey<X509Certificate> CLIENT_CERT_KEY = AttributeKey.valueOf(NettyHandlerAdapter.class, "principal");
private final static Logger LOGGER = LoggerFactory.getLogger(NettyHandlerAdapter.class);
private final Handler[] handlers;
private final DefaultContext.ApplicationConstants applicationConstants;
private final Registry serverRegistry;
private final boolean development;
public NettyHandlerAdapter(Registry serverRegistry, Handler handler) throws Exception {
this.handlers = ChainHandler.unpack(handler);
this.serverRegistry = serverRegistry;
this.applicationConstants = new DefaultContext.ApplicationConstants(this.serverRegistry, new DefaultRenderController(), serverRegistry.get(ExecController.class), Handlers.notFound());
this.development = serverRegistry.get(ServerConfig.class).isDevelopment();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.read();
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
newRequest(ctx, (HttpRequest) msg);
} else if (msg instanceof HttpContent) {
((HttpContent) msg).touch();
RequestBodyAccumulator bodyAccumulator = ctx.channel().attr(BODY_ACCUMULATOR_KEY).get();
if (bodyAccumulator == null) {
((HttpContent) msg).release();
} else {
bodyAccumulator.add((HttpContent) msg);
}
// Read for the next request proactively so that we
// detect if the client closes the connection.
if (msg instanceof LastHttpContent) {
ctx.channel().read();
}
} else {
Action<Object> subscriber = ctx.channel().attr(CHANNEL_SUBSCRIBER_ATTRIBUTE_KEY).get();
if (subscriber == null) {
super.channelRead(ctx, ReferenceCountUtil.touch(msg));
} else {
subscriber.execute(ReferenceCountUtil.touch(msg));
}
}
}
private void newRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) throws Exception {
if (!nettyRequest.decoderResult().isSuccess()) {
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
Headers requestHeaders = new NettyHeadersBackedHeaders(nettyRequest.headers());
//Find the content length we will use this as an indicator of a body
Long contentLength = HttpUtil.getContentLength(nettyRequest, -1L);
String transferEncoding = requestHeaders.get(HttpHeaderNames.TRANSFER_ENCODING);
//If there is a content length or transfer encoding that indicates there is a body
boolean hasBody = (contentLength > 0) || (transferEncoding != null);
RequestBody requestBody = hasBody ? new RequestBody(contentLength, nettyRequest, ctx) : null;
Channel channel = ctx.channel();
if (requestBody != null) {
channel.attr(BODY_ACCUMULATOR_KEY).set(requestBody);
}
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
InetSocketAddress socketAddress = (InetSocketAddress) channel.localAddress();
ConnectionIdleTimeout connectionIdleTimeout = ConnectionIdleTimeout.of(channel);
DefaultRequest request = new DefaultRequest(
Instant.now(),
requestHeaders,
nettyRequest.method(),
nettyRequest.protocolVersion(),
nettyRequest.uri(),
remoteAddress,
socketAddress,
serverRegistry.get(ServerConfig.class),
requestBody,
connectionIdleTimeout,
channel.attr(CLIENT_CERT_KEY).get()
);
HttpHeaders nettyHeaders = new DefaultHttpHeaders(false);
MutableHeaders responseHeaders = new NettyHeadersBackedMutableHeaders(nettyHeaders);
AtomicBoolean transmitted = new AtomicBoolean(false);
DefaultResponseTransmitter responseTransmitter = new DefaultResponseTransmitter(transmitted, channel, nettyRequest, request, nettyHeaders, requestBody);
ctx.channel().attr(DefaultResponseTransmitter.ATTRIBUTE_KEY).set(responseTransmitter);
Action<Action<Object>> subscribeHandler = thing -> {
transmitted.set(true);
ctx.channel().attr(CHANNEL_SUBSCRIBER_ATTRIBUTE_KEY).set(thing);
};
DefaultContext.RequestConstants requestConstants = new DefaultContext.RequestConstants(
applicationConstants,
request,
channel,
responseTransmitter,
subscribeHandler
);
Response response = new DefaultResponse(responseHeaders, ctx.alloc(), responseTransmitter);
requestConstants.response = response;
DefaultContext.start(channel.eventLoop(), requestConstants, serverRegistry, handlers, execution -> {
if (!transmitted.get()) {
Handler lastHandler = requestConstants.handler;
StringBuilder description = new StringBuilder();
description
.append("No response sent for ")
.append(request.getMethod().getName())
.append(" request to ")
.append(request.getUri());
if (lastHandler != null) {
description.append(" (last handler: ");
if (lastHandler instanceof DescribingHandler) {
((DescribingHandler) lastHandler).describeTo(description);
} else {
DescribingHandlers.describeTo(lastHandler, description);
}
description.append(")");
}
String message = description.toString();
LOGGER.warn(message);
response.getHeaders().clear();
ByteBuf body;
if (development) {
CharBuffer charBuffer = CharBuffer.wrap(message);
body = ByteBufUtil.encodeString(ctx.alloc(), charBuffer, CharsetUtil.UTF_8);
response.contentType(HttpHeaderConstants.PLAIN_TEXT_UTF8);
} else {
body = Unpooled.EMPTY_BUFFER;
}
response.getHeaders().set(HttpHeaderConstants.CONTENT_LENGTH, body.readableBytes());
responseTransmitter.transmit(HttpResponseStatus.INTERNAL_SERVER_ERROR, body);
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (!isIgnorableException(cause)) {
LOGGER.error("", cause);
if (ctx.channel().isActive()) {
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ConnectionClosureReason.setIdle(ctx.channel());
ctx.close();
}
if (evt instanceof SslHandshakeCompletionEvent && ((SslHandshakeCompletionEvent) evt).isSuccess()) {
SSLEngine engine = ctx.pipeline().get(SslHandler.class).engine();
if (engine.getWantClientAuth() || engine.getNeedClientAuth()) {
try {
X509Certificate clientCert = engine.getSession().getPeerCertificateChain()[0];
ctx.channel().attr(CLIENT_CERT_KEY).set(clientCert);
} catch (SSLPeerUnverifiedException ignore) {
// ignore - there is no way to avoid this exception that I can determine
}
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(DefaultResponseTransmitter.ATTRIBUTE_KEY).get().writabilityChanged();
}
private static boolean isIgnorableException(Throwable throwable) {
if (throwable instanceof ClosedChannelException) {
return true;
} else if (throwable instanceof IOException) {
// There really does not seem to be a better way of detecting this kind of exception
String message = throwable.getMessage();
return message != null && message.endsWith("Connection reset by peer");
} else {
return false;
}
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderConstants.CONTENT_TYPE, HttpHeaderConstants.PLAIN_TEXT_UTF8);
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}