/** * 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.services.handlergroups.websockets; 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.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import org.apache.log4j.Logger; import org.helios.apmrouter.catalog.domain.DomainObject; import org.helios.apmrouter.dataservice.json.JSONRequestRouter; import org.helios.apmrouter.dataservice.json.marshalling.JSONMarshaller; 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.ChannelUpstreamHandler; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.DownstreamMessageEvent; 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: WebSocketServerHandler</p> * <p>Description: The push handler for websockets.</p> * <p>Based on {@literal org.jboss.netty.example.http.websocketx.server.WebSocketServerHandler} * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.server.services.handlergroups.websockets.WebSocketServerHandler</code></p> */ public class WebSocketServerHandler implements ChannelUpstreamHandler, ChannelDownstreamHandler { /** The websocket handshaker */ private WebSocketServerHandshaker handshaker; /** Instance logger */ protected final Logger log = Logger.getLogger(getClass()); /** The JSON Request Router */ protected JSONRequestRouter router = null; /** The Json marshaller */ protected JSONMarshaller marshaller = null; /** * {@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 DomainObject || (message.getClass().isArray() && DomainObject.class.isAssignableFrom(message.getClass().getComponentType()))) { WebSocketFrame frame = new TextWebSocketFrame(marshaller.marshallToText(message)); ctx.sendDownstream(new DownstreamMessageEvent(channel, Channels.future(channel), frame, channel.getRemoteAddress())); return; } if(!(message instanceof JSONObject) && !(message instanceof CharSequence)) { ctx.sendDownstream(e); return; } WebSocketFrame frame = new TextWebSocketFrame(message.toString()); ctx.sendDownstream(new DownstreamMessageEvent(channel, Channels.future(channel), frame, channel.getRemoteAddress())); } /** * {@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); } else if (message instanceof WebSocketFrame) { handleRequest(ctx, (WebSocketFrame) message); } } else { ctx.sendUpstream(e); } } /** * Processes an HTTP request * @param ctx The channel handler context * @param req The HTTP request */ public void handleRequest(ChannelHandlerContext ctx, HttpRequest req) { if (req.getMethod() != GET) { sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); return; } //protected final WebSocketServerHandshakerFactory wsFactory new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false); // Handshake WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false); this.handshaker = wsFactory.newHandshaker(req); if (this.handshaker == null) { wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel()); } else { 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()); wsChannel.write(new JSONObject(Collections.singletonMap("sessionid", wsChannel.getId()))); //wsChannel.getPipeline().remove(DefaultChannelHandler.NAME); } } }); } } 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); } } /** * 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) { this.handshaker.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); } // log.info(String.format("Channel %s received %s", ctx.getChannel().getId(), request)); // ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase())); } /** * Generates a websocket URL * @param req The http request * @return The websocket URL */ private String getWebSocketLocation(HttpRequest req) { return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + "/ws"; } /** * Returns the websocket handshaker * @return the handshaker */ public WebSocketServerHandshaker getHandshaker() { return handshaker; } /** * Sets the json request router * @param router the router to set */ @Autowired(required=true) public void setRouter(JSONRequestRouter router) { this.router = router; } /** * Sets the object Json marshaller * @param marshaller the object Json marshaller */ @Autowired(required=true) public void setMarshaller(JSONMarshaller marshaller) { this.marshaller = marshaller; } }