/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2007, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.helios.apmrouter.server.unification.pipeline.http; import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive; import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; import static org.jboss.netty.handler.codec.http.HttpMethod.GET; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import org.helios.apmrouter.dataservice.json.JSONRequestRouter; import org.helios.apmrouter.dataservice.json.JsonResponse; import org.helios.apmrouter.dataservice.json.marshalling.JSONMarshaller; import org.helios.apmrouter.dataservice.json.marshalling.netty.ChannelBufferizable; import org.helios.apmrouter.server.ServerComponentBean; import org.helios.apmrouter.server.services.session.ChannelType; import org.helios.apmrouter.server.services.session.SharedChannelGroup; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelDownstreamHandler; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelLocal; import org.jboss.netty.channel.ChannelUpstreamHandler; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.DownstreamMessageEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import org.jboss.netty.util.CharsetUtil; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; /** * <p>Title: WebSocketServiceHandler</p> * <p>Description: WebSocket handler for fronting JSON based data-services</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.server.unification.pipeline.http.WebSocketServiceHandler</code></p> */ public class WebSocketServiceHandler extends ServerComponentBean implements ChannelUpstreamHandler, ChannelDownstreamHandler { /** The JSON Request Router */ protected JSONRequestRouter router = null; /** The JSON Marshaller */ protected JSONMarshaller marshaller = null; /** A channel local for the websocket handshaker */ protected final ChannelLocal<WebSocketServerHandshaker> wsHandShaker = new ChannelLocal<WebSocketServerHandshaker>(true); /** * {@inheritDoc} * @see org.jboss.netty.channel.ChannelUpstreamHandler#handleUpstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { if(e instanceof MessageEvent) { Object message = ((MessageEvent)e).getMessage(); if (message instanceof HttpRequest) { handleRequest(ctx, (HttpRequest) message, (MessageEvent)e); } else if (message instanceof WebSocketFrame) { handleRequest(ctx, (WebSocketFrame) message); } } else { ctx.sendUpstream(e); } } /** * {@inheritDoc} * @see org.jboss.netty.channel.ChannelDownstreamHandler#handleDownstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { Channel channel = e.getChannel(); if(!channel.isOpen()) return; if(!(e instanceof MessageEvent)) { ctx.sendDownstream(e); return; } Object message = ((MessageEvent)e).getMessage(); if((message instanceof ChannelBufferizable)) { ctx.sendDownstream(new DownstreamMessageEvent(channel, Channels.future(channel), new TextWebSocketFrame(((ChannelBufferizable)message).toChannelBuffer()), channel.getRemoteAddress())); } else if((message instanceof JsonResponse) || (message instanceof JSONObject) || (message instanceof CharSequence)) { ctx.sendDownstream(new DownstreamMessageEvent(channel, Channels.future(channel), new TextWebSocketFrame(marshaller.marshallToText(message)), channel.getRemoteAddress())); } else { ctx.sendDownstream(e); } } /** * Handles uncaught exceptions in the pipeline from this handler * @param ctx The channel context * @param ev The exception event */ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent ev) { error("Uncaught exception ", ev.getCause()); } /** * Processes a websocket request * @param ctx The channel handler context * @param frame The websocket frame request to process */ public void handleRequest(ChannelHandlerContext ctx, WebSocketFrame frame) { // Check for closing frame if (frame instanceof CloseWebSocketFrame) { wsHandShaker.get(ctx.getChannel()).close(ctx.getChannel(), (CloseWebSocketFrame) frame); return; } else if (frame instanceof PingWebSocketFrame) { ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); return; } else if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() .getName())); } String request = ((TextWebSocketFrame) frame).getText(); JSONObject wsRequest = null; try { wsRequest = new JSONObject(request); if("who".equals(wsRequest.get("t"))) { SocketAddress sa = ctx.getChannel().getRemoteAddress(); String host = "unknown"; String agent = "unknown"; if(sa!=null) { host = ((InetSocketAddress)sa).getHostName(); } if(wsRequest.get("agent")!=null) { agent = wsRequest.get("agent").toString(); } SharedChannelGroup.getInstance().add(ctx.getChannel(), ChannelType.WEBSOCKET_REMOTE, "ClientWebSocket", host, agent); } else { router.invoke(wsRequest, ctx.getChannel()); } } catch (Exception ex) { log.error("Failed to parse request [" + request + "]", ex); } } /** * Processes an HTTP request * @param ctx The channel handler context * @param req The HTTP request * @param me The message event that will be sent upstream if not handled here */ public void handleRequest(ChannelHandlerContext ctx, HttpRequest req, MessageEvent me) { if (req.getMethod() != GET) { sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); return; } String uri = req.getUri(); if(!"/ws".equals(uri)) { ctx.sendUpstream(me); return; } // Handshake WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false); WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel()); } else { wsHandShaker.set(ctx.getChannel(), handshaker); ChannelFuture cf = handshaker.handshake(ctx.getChannel(), req); cf.addListener(WebSocketServerHandshaker.HANDSHAKE_LISTENER); cf.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture f) throws Exception { if(f.isSuccess()) { Channel wsChannel = f.getChannel(); SharedChannelGroup.getInstance().add( f.getChannel(), ChannelType.WEBSOCKET_REMOTE, "WebSocketClient-" + f.getChannel().getId(), ((InetSocketAddress)wsChannel.getRemoteAddress()).getAddress().getCanonicalHostName(), "WebSock[" + wsChannel.getId() + "]" ); wsChannel.write(new JSONObject(Collections.singletonMap("sessionid", wsChannel.getId()))); //wsChannel.getPipeline().remove(DefaultChannelHandler.NAME); } } }); } } /** * Sends an HTTP response * @param ctx The channel handler context * @param req The HTTP request being responded to * @param res The HTTP response to send */ private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { // Generate an error page if response status code is not OK (200). if (res.getStatus().getCode() != 200) { res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); setContentLength(res, res.getContent().readableBytes()); } // Send the response and close the connection if necessary. ChannelFuture f = ctx.getChannel().write(res); if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } /** * Generates a websocket URL for the passed request * @param req The http request * @return The websocket URL */ private String getWebSocketLocation(HttpRequest req) { return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + "/ws"; } /** * Sets the json request router * @param router the router to set */ @Autowired(required=true) public void setRouter(JSONRequestRouter router) { this.router = router; } /** * Sets the JSONMarshaller * @param marshaller the marshaller to set */ @Autowired(required=true) public void setMarshaller(JSONMarshaller marshaller) { this.marshaller = marshaller; } }