/* * Copyright (c) 2013-2015 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 io.werval.server.netty; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.math.BigDecimal; import io.werval.api.Mode; import io.werval.api.events.HttpEvent; import io.werval.api.http.ProtocolVersion; import io.werval.api.http.Request; import io.werval.api.http.RequestHeader; import io.werval.api.http.ResponseHeader; import io.werval.api.http.Status; import io.werval.api.outcomes.Outcome; import io.werval.runtime.outcomes.ChunkedInputOutcome; import io.werval.runtime.outcomes.InputStreamOutcome; import io.werval.runtime.outcomes.SimpleOutcome; import io.werval.spi.ApplicationSPI; import io.werval.spi.dev.DevShellRebuildException; import io.werval.spi.dev.DevShellSPI; import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.stream.ChunkedStream; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.WriteTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.werval.api.http.Headers.Names.CONTENT_LENGTH; import static io.werval.api.http.Headers.Names.TRAILER; import static io.werval.api.http.Headers.Names.TRANSFER_ENCODING; import static io.werval.api.http.Headers.Names.X_WERVAL_CONTENT_LENGTH; import static io.werval.api.http.Headers.Values.CHUNKED; import static io.werval.util.Charsets.UTF_8; import static io.werval.server.netty.NettyHttpFactories.remoteAddressOf; import static io.werval.server.netty.NettyHttpFactories.requestOf; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * Handle HTTP Requests. * * Any HTTP request message is allowed to contain a message body, and thus must be parsed with that in mind. * This implementation consume the request body for any requests methods but it is only parsed for POST, PUT * and PATCH methods. Parsing is done only for URL-encoded forms and multipart form data. For other request body * types, it's the application responsibility to do the parsing. */ // TODO WebSocket UPGRADE public final class WervalHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final Logger LOG = LoggerFactory.getLogger( WervalHttpHandler.class ); private final class HttpRequestCompleteChannelFutureListener implements ChannelFutureListener { private final RequestHeader requestHeader; private HttpRequestCompleteChannelFutureListener( RequestHeader requestHeader ) { this.requestHeader = requestHeader; } @Override public void operationComplete( ChannelFuture future ) throws Exception { if( future.isSuccess() ) { LOG.trace( "{} Request completed successfully", requestIdentity ); app.onHttpRequestComplete( requestHeader ); } } } private final ApplicationSPI app; private final DevShellSPI devSpi; private String requestIdentity; private RequestHeader requestHeader; public WervalHttpHandler( ApplicationSPI app, DevShellSPI devSpi ) { super(); this.app = app; this.devSpi = devSpi; } @Override protected void channelRead0( ChannelHandlerContext nettyContext, FullHttpRequest nettyRequest ) throws Exception { // Get the request unique identifier requestIdentity = nettyContext.channel().attr( Attrs.REQUEST_IDENTITY ).get(); assert requestIdentity != null; if( LOG.isTraceEnabled() ) { LOG.trace( "{} Received a FullHttpRequest:\n{}", requestIdentity, nettyRequest.toString() ); } // Return 503 to incoming requests while shutting down if( nettyContext.executor().isShuttingDown() ) { app.shuttingDownOutcome( ProtocolVersion.valueOf( nettyRequest.getProtocolVersion().text() ), requestIdentity ).thenAcceptAsync( shuttingDownOutcome -> { writeOutcome( nettyContext, shuttingDownOutcome ) .addListeners( new HttpRequestCompleteChannelFutureListener( requestHeader ), f -> app.events().emit( new HttpEvent.ResponseSent( requestIdentity, shuttingDownOutcome.responseHeader().status() ) ) ); }, app.executor() ); return; } // In development mode, rebuild application source if needed if( devSpi != null && devSpi.isSourceChanged() ) { devSpi.rebuild(); } // Create Request Instance // Can throw HttpRequestParsingException Request request = requestOf( app.defaultCharset(), app.httpBuilders(), remoteAddressOf( nettyContext.channel() ), requestIdentity, nettyRequest ); requestHeader = request; // Handle Request app.handleRequest( request ).thenAcceptAsync( outcome -> { // Write Outcome ChannelFuture writeFuture = writeOutcome( nettyContext, outcome ); // Listen to request completion writeFuture.addListeners( f -> app.events().emit( new HttpEvent.ResponseSent( requestIdentity, outcome.responseHeader().status() ) ), new HttpRequestCompleteChannelFutureListener( requestHeader ) ); }, app.executor() ); } @Override public void exceptionCaught( ChannelHandlerContext nettyContext, Throwable cause ) throws IOException { if( cause instanceof ReadTimeoutException ) { LOG.trace( "{} Read timeout, connection has been closed.", requestIdentity ); } else if( cause instanceof WriteTimeoutException ) { LOG.trace( "{} Write timeout, connection has been closed.", requestIdentity ); } else if( cause instanceof DevShellRebuildException ) { byte[] htmlErrorPage = ( (DevShellRebuildException) cause ).htmlErrorPage().getBytes( UTF_8 ); DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse( HTTP_1_1, INTERNAL_SERVER_ERROR ); nettyResponse.headers().set( CONTENT_LENGTH, htmlErrorPage.length ); ( (ByteBufHolder) nettyResponse ).content().writeBytes( htmlErrorPage ); nettyContext.writeAndFlush( nettyResponse ) .addListeners( f -> app.events().emit( new HttpEvent.ResponseSent( requestIdentity, Status.INTERNAL_SERVER_ERROR ) ), new HttpRequestCompleteChannelFutureListener( requestHeader ), ChannelFutureListener.CLOSE ); } else if( requestHeader != null ) { // Write Outcome Outcome errorOutcome = app.handleError( requestHeader, cause ); ChannelFuture writeFuture = writeOutcome( nettyContext, errorOutcome ); // Listen to request completion writeFuture.addListeners( f -> app.events().emit( new HttpEvent.ResponseSent( requestIdentity, errorOutcome.responseHeader().status() ) ), new HttpRequestCompleteChannelFutureListener( requestHeader ) ); } else if( cause instanceof HttpRequestParsingException ) { if( app.mode() == Mode.PROD ) { LOG.trace( "HTTP request parsing error, returning 400, was: {}", cause.getMessage(), cause ); } else { LOG.warn( "HTTP request parsing error, returning 400, was: {}", cause.getMessage(), cause ); } DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse( HTTP_1_1, BAD_REQUEST ); nettyResponse.headers().set( CONTENT_LENGTH, 0 ); nettyContext.writeAndFlush( nettyResponse ) .addListeners( f -> app.events().emit( new HttpEvent.ResponseSent( requestIdentity, Status.BAD_REQUEST ) ), new HttpRequestCompleteChannelFutureListener( requestHeader ), ChannelFutureListener.CLOSE ); } else { LOG.error( "HTTP Server encountered an unexpected error, please raise an issue with the complete stacktrace", cause ); nettyContext.close(); } } private ChannelFuture writeOutcome( ChannelHandlerContext nettyContext, Outcome outcome ) { // == Build the Netty Response ResponseHeader responseHeader = outcome.responseHeader(); // Netty Version & Status HttpVersion responseVersion = HttpVersion.valueOf( responseHeader.version().toString() ); HttpResponseStatus responseStatus = HttpResponseStatus.valueOf( responseHeader.status().code() ); // Netty Headers & Body output final HttpResponse nettyResponse; final ChannelFuture writeFuture; if( outcome instanceof ChunkedInputOutcome ) { ChunkedInputOutcome chunkedOutcome = (ChunkedInputOutcome) outcome; nettyResponse = new DefaultHttpResponse( responseVersion, responseStatus ); // Headers applyResponseHeader( responseHeader, nettyResponse ); nettyResponse.headers().set( TRANSFER_ENCODING, CHUNKED ); nettyResponse.headers().set( TRAILER, X_WERVAL_CONTENT_LENGTH ); // Body nettyContext.write( nettyResponse ); writeFuture = nettyContext.writeAndFlush( new HttpChunkedBodyEncoder( new ChunkedStream( chunkedOutcome.inputStream(), chunkedOutcome.chunkSize() ) ) ); } else if( outcome instanceof InputStreamOutcome ) { InputStreamOutcome streamOutcome = (InputStreamOutcome) outcome; nettyResponse = new DefaultFullHttpResponse( responseVersion, responseStatus ); // Headers applyResponseHeader( responseHeader, nettyResponse ); nettyResponse.headers().set( CONTENT_LENGTH, streamOutcome.contentLength() ); // Body try( InputStream bodyInputStream = streamOutcome.bodyInputStream() ) { ( (ByteBufHolder) nettyResponse ).content().writeBytes( bodyInputStream, new BigDecimal( streamOutcome.contentLength() ).intValueExact() ); } catch( IOException ex ) { throw new UncheckedIOException( ex ); } writeFuture = nettyContext.writeAndFlush( nettyResponse ); } else if( outcome instanceof SimpleOutcome ) { SimpleOutcome simpleOutcome = (SimpleOutcome) outcome; byte[] body = simpleOutcome.body().asBytes(); nettyResponse = new DefaultFullHttpResponse( responseVersion, responseStatus ); // Headers applyResponseHeader( responseHeader, nettyResponse ); nettyResponse.headers().set( CONTENT_LENGTH, body.length ); // Body ( (ByteBufHolder) nettyResponse ).content().writeBytes( body ); writeFuture = nettyContext.writeAndFlush( nettyResponse ); } else { LOG.warn( "{} Unhandled Outcome type '{}', no response body.", requestIdentity, outcome.getClass() ); nettyResponse = new DefaultFullHttpResponse( responseVersion, responseStatus ); applyResponseHeader( responseHeader, nettyResponse ); writeFuture = nettyContext.writeAndFlush( nettyResponse ); } if( LOG.isTraceEnabled() ) { LOG.trace( "{} Sent a HttpResponse:\n{}", requestIdentity, nettyResponse.toString() ); } // Close the connection as soon as the response is sent if not keep alive if( !outcome.responseHeader().isKeepAlive() || nettyContext.executor().isShuttingDown() ) { writeFuture.addListener( ChannelFutureListener.CLOSE ); } // Done! return writeFuture; } /** * Apply Headers and Cookies into Netty HttpResponse. * * @param response Werval ResponseHeader * @param nettyResponse Netty HttpResponse */ private void applyResponseHeader( ResponseHeader response, HttpResponse nettyResponse ) { for( String name : response.headers().keys() ) { nettyResponse.headers().add( name, response.headers().values( name ) ); } } }