package com.webobjects.appserver; import static org.jboss.netty.channel.Channels.pipeline; import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.concurrent.Executors; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFactory; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.handler.codec.http.HttpChunkAggregator; import org.jboss.netty.handler.codec.http.HttpHeaders.Names; import org.jboss.netty.handler.codec.http.HttpHeaders.Values; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; 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.WebSocketFrame; import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver._private.WOProperties; import com.webobjects.foundation.NSDelayedCallbackCenter; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSNotificationCenter; import er.woadaptor.ERWOAdaptorUtilities; import er.woadaptor.websockets.WebSocket; import er.woadaptor.websockets.WebSocketFactory; import er.woadaptor.websockets.WebSocketStore; /** * How to use the WONettyAdaptor: * * 1. Build/Install ERWOAdaptor framework * 2. Include ERWOAdaptor framework in your app/project * 3. Run your app with the property: * * -WOAdaptor er.woadaptor.ERWOAdaptor * * OR: * * -WOAdaptor WONettyAdaptor * * 4. (Optional) If developing with the WONettyAdaptor set the following properties as well: * * -WODirectConnectEnabled false * * AND (maybe) * * -WOAllowRapidTurnaround false * * @author ravim * @author ramsey (WebSocket support) * * @version 2.0 */ public class WONettyAdaptor extends WOAdaptor { private static final Logger log = LoggerFactory.getLogger(WONettyAdaptor.class); private int _port; private String _hostname; private ChannelFactory channelFactory; private Channel channel; private String hostname() { if (_hostname == null) { try { InetAddress _host = InetAddress.getLocalHost(); _hostname = _host.getHostName(); } catch (UnknownHostException e) { log.error("Failed to get localhost address.", e); } } return _hostname; } public WONettyAdaptor(String name, NSDictionary<String, Object> config) { super(name, config); Number number = (Number) config.objectForKey(WOProperties._PortKey); if (number != null) _port = number.intValue(); if (_port < 0) _port = 0; _hostname = (String) config.objectForKey(WOProperties._HostKey); WOApplication.application()._setHost(hostname()); } @Override public void registerForEvents() { // Configure the server. channelFactory = new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); ServerBootstrap bootstrap = new ServerBootstrap(channelFactory); // Set up the event pipeline factory. bootstrap.setPipelineFactory(new PipelineFactory()); // Bind and start to accept incoming connections. channel = bootstrap.bind(new InetSocketAddress(hostname(), _port)); log.debug("Binding adaptor to address: {}", channel.getLocalAddress()); _port = ((InetSocketAddress) channel.getLocalAddress()).getPort(); System.setProperty(WOProperties._PortKey, Integer.toString(_port)); } @Override public void unregisterForEvents() { ChannelFuture future = channel.close(); future.awaitUninterruptibly(); channelFactory.releaseExternalResources(); } @Override public int port() { return _port; } @Override public boolean dispatchesRequestsConcurrently() { return true; } /** * Originally inspired by: * * @author <a href="http://www.jboss.org/netty/">The Netty Project</a> * @author Andy Taylor (andy.taylor@jboss.org) * @author <a href="http://gleamynode.net/">Trustin Lee</a> * * @see <a href="http://docs.jboss.org/netty/3.2/xref/org/jboss/netty/example/http/snoop/HttpServerPipelineFactory.html">HttpServerPipelineFactory</a> * * @author ravim ERWOAdaptor/WONettyAdaptor * * @property WOMaxIOBufferSize Max http chunking size. Defaults to WO default 8196 * @see <a href="http://docs.jboss.org/netty/3.2/xref/org/jboss/netty/handler/codec/http/HttpMessageDecoder.html">HttpMessageDecoder</a> * * @property WOFileUpload.sizeLimit Max file upload size permitted */ protected static class PipelineFactory implements ChannelPipelineFactory { // TODO ravi: CHECKME Netty default is 8192; WO default is 8196(!?) public final Integer maxChunkSize = Integer.getInteger("WOMaxIOBufferSize", 8196); public final Integer maxFileSize = Integer.getInteger("WOFileUpload.sizeLimit", 1024*1024*100); public ChannelPipeline getPipeline() throws Exception { // Create a default pipeline implementation. ChannelPipeline pipeline = pipeline(); pipeline.addLast("decoder", new HttpRequestDecoder(4096, 8192, maxChunkSize)); pipeline.addLast("aggregator", new HttpChunkAggregator(maxFileSize)); pipeline.addLast("encoder", new HttpResponseEncoder()); pipeline.addLast("handler", new RequestHandler()); return pipeline; } } /** * Originally inspired by: * * @author <a href="http://www.jboss.org/netty/">The Netty Project</a> * @author Andy Taylor (andy.taylor@jboss.org) * @author <a href="http://gleamynode.net/">Trustin Lee</a> * * @see <a href="http://docs.jboss.org/netty/3.2/xref/org/jboss/netty/example/http/snoop/HttpRequestHandler.html">HttpRequestHandler</a> * * @author ravim ERWOAdaptor/WONettyAdaptor version */ protected static class RequestHandler extends SimpleChannelUpstreamHandler { private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); private WebSocketServerHandshaker handshaker; @Override public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { super.channelClosed(ctx, e); NSNotificationCenter.defaultCenter().postNotification(WebSocketStore.CHANNEL_CLOSED_NOTIFICATION, ctx.getChannel()); } /** * @see <a href="http://docs.jboss.org/netty/3.2/api/org/jboss/netty/channel/SimpleChannelUpstreamHandler.html#messageReceived(org.jboss.netty.channel.ChannelHandlerContext,%20org.jboss.netty.channel.MessageEvent)">SimpleChannelUpstreamHandler</a> */ @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Object msg = e.getMessage(); if(msg instanceof WebSocketFrame) { handleWebSocketFrame(ctx, e, (WebSocketFrame)msg); } else if(msg instanceof HttpRequest) { handleHTTPRequest(ctx, e, (HttpRequest)msg); } } protected void handleWebSocketFrame(ChannelHandlerContext ctx, MessageEvent e, WebSocketFrame frame) { WebSocket socket = WebSocketStore.defaultWebSocketStore().socketForChannel(e.getChannel()); if (frame instanceof CloseWebSocketFrame) { //TODO remove from store? handshaker.close(ctx.getChannel(), (CloseWebSocketFrame) frame); } else if (frame instanceof PingWebSocketFrame) { ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); } else if(socket != null) { socket.receiveFrame(frame); } } protected void handleHTTPRequest(ChannelHandlerContext ctx, MessageEvent e, HttpRequest _request) throws IOException { if(_request.getHeader(Names.SEC_WEBSOCKET_VERSION) != null || (Values.UPGRADE.equalsIgnoreCase(_request.getHeader(Names.CONNECTION)) && Values.WEBSOCKET.equalsIgnoreCase(_request.getHeader(Names.UPGRADE))) ) { handleUpgradeRequest(ctx, _request); } else { WORequest worequest = ERWOAdaptorUtilities.asWORequest(_request); worequest._setOriginatingAddress(((InetSocketAddress) ctx.getChannel().getRemoteAddress()).getAddress()); WOResponse woresponse = WOApplication.application().dispatchRequest(worequest); // send a response NSDelayedCallbackCenter.defaultCenter().eventEnded(); // Decide whether to close the connection or not. boolean keepAlive = isKeepAlive(_request); //For reasons that escape me, empty responses fail to close properly. boolean close = !(woresponse._contentLength() > 0 || woresponse.contentInputStream() != null); // Write the response. HttpResponse response = ERWOAdaptorUtilities.asHttpResponse(woresponse); ChannelFuture future = e.getChannel().write(response); // Close the non-keep-alive connection after the write operation is done. if (close || !keepAlive) { future.addListener(ChannelFutureListener.CLOSE); } } } protected void handleUpgradeRequest(ChannelHandlerContext ctx, HttpRequest req) { //If factory doesn't exist, close the upgrade request channel WebSocketFactory factory = WebSocketStore.defaultWebSocketStore().factory(); if(factory == null) { ctx.getChannel().close(); return; } WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( ERWOAdaptorUtilities.getWebSocketLocation(req), null, false); handshaker = wsFactory.newHandshaker(req); Channel socketChannel = ctx.getChannel(); if(handshaker == null) { wsFactory.sendUnsupportedWebSocketVersionResponse(socketChannel); } else { ChannelFuture future = handshaker.handshake(socketChannel, req); //TODO tie this to the channel future result? //Create a WebSocket instance to handle frames WebSocket socket = factory.create(socketChannel, req); WebSocketStore.defaultWebSocketStore().takeSocketForChannel(socket, socketChannel); socket.didUpgrade(); } } /** * @see <a href="http://docs.jboss.org/netty/3.2/api/org/jboss/netty/channel/SimpleChannelUpstreamHandler.html#exceptionCaught(org.jboss.netty.channel.ChannelHandlerContext,%20org.jboss.netty.channel.ExceptionEvent)">SimpleChannelUpstreamHandler</a> */ @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Throwable cause = e.getCause(); log.warn(cause.getMessage()); e.getChannel().close(); } } }