Java tutorial
/* * Copyright (c) 2016 Catavolt Inc. All rights reserved. * * This file is part of Ozy. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ozy.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import io.netty.util.CharsetUtil; import ozy.fp.Future; import ozy.klvm.ServiceRequest; import ozy.klvm.ServiceResponse; import ozy.util.ExceptionUtil; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import static io.netty.handler.codec.http.HttpHeaders.Names.*; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * This class was adapted from the Netty example at: * http://netty.io/4.0/xref/io/netty/example/http/snoop/HttpSnoopServerHandler.html */ class ServerHandler extends SimpleChannelInboundHandler<Object> { private HttpRequest _httpRequest; private final StringBuilder _httpContentBuf = new StringBuilder(); private Server _server; private Future<ServiceResponse> _serverResponseFr; private ServiceRequest _serviceRequest; //-------------------------------------------------------------------------- // CLASS METHODS //-------------------------------------------------------------------------- private static void send100Continue(ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE); ctx.write(response); } //-------------------------------------------------------------------------- // CONSTRUCTORS //-------------------------------------------------------------------------- ServerHandler(Server server) { _server = server; } //-------------------------------------------------------------------------- // INSTANCE METHODS //-------------------------------------------------------------------------- @Override public final void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, cause); } public final Router router() { return _server.router(); } // ~~~~~~~~~~~~~~~~~~~~~~~~~ PROTECTED ~~~~~~~~~~~~~~~~~~~~~~~~~ // public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); } @Override protected final void channelRead0(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpObject) { HttpObject object = (HttpObject) msg; DecoderResult result = object.getDecoderResult(); if (result.isFailure()) { sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, result.cause()); return; } } if (msg instanceof HttpRequest) { _serviceRequest = new ServiceRequest(); _httpRequest = (HttpRequest) msg; _httpContentBuf.setLength(0); if (HttpHeaders.is100ContinueExpected(_httpRequest)) { send100Continue(ctx); } receiveHeader(); } if (msg instanceof HttpContent) { HttpContent next = (HttpContent) msg; receiveContent(next); if (next instanceof LastHttpContent) { LastHttpContent last = (LastHttpContent) next; receiveTrailer(last); routeRequest(ctx); } } } // ~~~~~~~~~~~~~~~~~~~~~~~~~ PRIVATE ~~~~~~~~~~~~~~~~~~~~~~~~~ // private void encodeCookies(ChannelHandlerContext ctx, FullHttpResponse response) { String cookieString = _httpRequest.headers().get(COOKIE); if (cookieString != null) { Set<Cookie> cookies = ServerCookieDecoder.LAX.decode(cookieString); if (!cookies.isEmpty()) { for (Cookie cookie : cookies) { response.headers().add(SET_COOKIE, ServerCookieEncoder.LAX.encode(cookie)); } } } } private void receiveHeader() { QueryStringDecoder queryStringDecoder = new QueryStringDecoder(_httpRequest.getUri()); _serviceRequest.setMethod(_httpRequest.getMethod().toString()); _serviceRequest.setSenderHost(HttpHeaders.getHost(_httpRequest, "unknown")); _serviceRequest.setPath(queryStringDecoder.path()); HttpHeaders headers = _httpRequest.headers(); if (!headers.isEmpty()) { for (Map.Entry<String, String> h : headers) { _serviceRequest.headers().put(h.getKey(), h.getValue()); } } Map<String, List<String>> params = queryStringDecoder.parameters(); if (!params.isEmpty()) { for (Entry<String, List<String>> p : params.entrySet()) { _serviceRequest.params().put(p.getKey(), p.getValue()); } } } private void receiveContent(HttpContent content) { ByteBuf byteContent = content.content(); if (byteContent.isReadable()) { _httpContentBuf.append(byteContent.toString(CharsetUtil.UTF_8)); } } private void receiveTrailer(LastHttpContent trailer) { _serviceRequest.setBody(_httpContentBuf.toString()); HttpHeaders headers = trailer.trailingHeaders(); if (!headers.isEmpty()) { for (Map.Entry<String, String> h : headers) { _serviceRequest.trailingHeaders().put(h.getKey(), h.getValue()); } } } private void routeRequest(ChannelHandlerContext ctx) { if (router() == null) { sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, new NullPointerException("router")); } else if (!router().isRouterFor(_serviceRequest)) { sendErrorMessage(ctx, BAD_REQUEST, "[ServerHandler] invalid request"); } else { try { _serverResponseFr = router().route(_serviceRequest); _serverResponseFr.onComplete((responseTry) -> { if (responseTry.isSuccess()) { try { sendResponse(ctx, responseTry.success()); } catch (Exception exc) { sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, exc); } if (!HttpHeaders.isKeepAlive(_httpRequest)) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } } else { sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, responseTry.failure().toDebugString()); } }); } catch (Exception exc) { sendErrorMessage(ctx, INTERNAL_SERVER_ERROR, exc); } } } private void sendErrorMessage(ChannelHandlerContext ctx, HttpResponseStatus status, String message) { ByteBuf responseBytes = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, status, responseBytes); httpResponse.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); if (HttpHeaders.isKeepAlive(_httpRequest)) { HttpHeaders.setContentLength(httpResponse, httpResponse.content().readableBytes()); HttpHeaders.setKeepAlive(httpResponse, true); } encodeCookies(ctx, httpResponse); ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE); } private void sendErrorMessage(ChannelHandlerContext ctx, HttpResponseStatus status, Throwable cause) { String message = ExceptionUtil.formatAsString(cause); sendErrorMessage(ctx, status, message); } private void sendResponse(ChannelHandlerContext ctx, ServiceResponse serviceResponse) { ByteBuf responseBytes = Unpooled.copiedBuffer(serviceResponse.body(), CharsetUtil.UTF_8); HttpResponseStatus httpStatus = HttpResponseStatus.valueOf(serviceResponse.status()); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, httpStatus, responseBytes); httpResponse.headers().set(CONTENT_TYPE, serviceResponse.contentType()); if (HttpHeaders.isKeepAlive(_httpRequest)) { httpResponse.headers().set(CONTENT_LENGTH, httpResponse.content().readableBytes()); httpResponse.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE); } String cookieString = _httpRequest.headers().get(COOKIE); if (cookieString != null) { Set<Cookie> cookies = ServerCookieDecoder.LAX.decode(cookieString); if (!cookies.isEmpty()) { for (Cookie cookie : cookies) { httpResponse.headers().add(SET_COOKIE, ServerCookieEncoder.LAX.encode(cookie)); } } for (Map.Entry<String, String> c : serviceResponse.cookies().entrySet()) { httpResponse.headers().add(SET_COOKIE, ServerCookieEncoder.LAX.encode(c.getKey(), c.getValue())); } } ctx.write(httpResponse); } }