package com.alecgorge.minecraft.jsonapi.packets.netty; 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.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.util.CharsetUtil; import java.io.IOException; import com.alecgorge.minecraft.jsonapi.JSONAPI; import com.alecgorge.minecraft.jsonapi.streams.StreamingResponse; class JSONAPIHandler extends SimpleChannelInboundHandler<Object> { private static final String WEBSOCKET_PATH = "/api/2/websocket"; private WebSocketServerHandshaker handshaker; private boolean continueSending = true; private JSONAPI api; public JSONAPIHandler(JSONAPI api) { this.api = api; } @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) { handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // Handle a bad request. if (!req.getDecoderResult().isSuccess()) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } QueryStringDecoder uri = new QueryStringDecoder(req.getUri()); if (uri.path().equals(WEBSOCKET_PATH)) { // Handshake WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } else { api.getRouter().serveRequest(ctx, req); } } private void handleWebSocketFrame(final ChannelHandlerContext ctx, WebSocketFrame frame) { // Check for closing frame if (frame instanceof CloseWebSocketFrame) { continueSending = false; handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return; } if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName())); } TextWebSocketFrame txt = ((TextWebSocketFrame) frame); FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, txt.text()); if (APIv2Handler.canServe(new QueryStringDecoder(req.getUri()))) { APIv2Handler h = new APIv2Handler(req); if (h.isStream()) { final StreamingResponse s = h.subscribe(); (new Thread(new Runnable() { @Override public void run() { String line = ""; JSONAPI.dbug("starting streaming response"); while (continueSending && (line = s.nextLine()) != null) { ctx.channel().write(new TextWebSocketFrame(line.trim())); } try { JSONAPI.dbug("closing streaming response continueSending: " + continueSending); s.close(); } catch (IOException e) { e.printStackTrace(); } } })).start(); } else { ctx.channel().write(new TextWebSocketFrame(h.serve().content())); } } else { FullHttpResponse resp = api.getRouter().getResponse(ctx, req); ctx.channel().write(new TextWebSocketFrame(resp.content().toString(CharsetUtil.UTF_8))); } } private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // Send the response and close the connection if necessary. ChannelFuture f = ctx.channel().writeAndFlush(res); if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); JSONAPI.dbug("exception caught"); continueSending = false; ctx.close(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); JSONAPI.dbug("channel inactive"); continueSending = false; } private static String getWebSocketLocation(FullHttpRequest req) { return "ws://" + req.headers().get(HttpHeaders.Names.HOST) + WEBSOCKET_PATH; } }