diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java new file mode 100644 index 0000000000..67cb476e98 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +/** + * A Web Socket echo server for running the autobahn + * test suite + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServer { + public static void main(String[] args) { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); + + //bootstrap.setOption("child.tcpNoDelay", true); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(9000)); + + System.out.println("Web Socket Server started on 9000. Open your browser and navigate to http://localhost:9000/"); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java new file mode 100644 index 0000000000..f3a084282f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.*; +import static org.jboss.netty.handler.codec.http.HttpMethod.*; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; +import static org.jboss.netty.handler.codec.http.HttpVersion.*; + +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +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.BinaryWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +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.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + * Handles handshakes and messages + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandler.class); + + private WebSocketServerHandshaker handshaker = null; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + handleHttpRequest(ctx, (HttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception { + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + this.getWebSocketLocation(req), null, false); + this.handshaker = wsFactory.newHandshaker(ctx, req); + if (this.handshaker == null) { + wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); + } else { + this.handshaker.executeOpeningHandshake(ctx, req); + } + return; + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + logger.debug(String + .format("Channel %s received %s", ctx.getChannel().getId(), frame.getClass().getSimpleName())); + + if (frame instanceof CloseWebSocketFrame) { + this.handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); + } else if (frame instanceof PingWebSocketFrame) { + ctx.getChannel().write( + new PongWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof TextWebSocketFrame) { + //String text = ((TextWebSocketFrame) frame).getText(); + ctx.getChannel().write(new TextWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof BinaryWebSocketFrame) { + ctx.getChannel().write( + new BinaryWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof ContinuationWebSocketFrame) { + ctx.getChannel().write( + new ContinuationWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof PongWebSocketFrame) { + // Ignore + } else { + throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() + .getName())); + } + } + + 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); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + private String getWebSocketLocation(HttpRequest req) { + return "ws://" + req.getHeader(HttpHeaders.Names.HOST); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java new file mode 100644 index 0000000000..e7bfd52999 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.channel.Channels.*; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; + +/** + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerPipelineFactory implements ChannelPipelineFactory { + @Override + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(65536)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", new WebSocketServerHandler()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java new file mode 100644 index 0000000000..46de920b06 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This package is intended for use with testing against the Python + * AutoBahn test suite. + * + *
01. Add ppa:twisted-dev/ppa to your system's Software Sources + * + *
02. Install Twisted V11: sudo apt-get install python-twisted + * + *
03. Intall Python Setup Tools: sudo apt-get install python-setuptools + * + *
04. Install AutoBahn: sudo easy_install Autobahn + * + *
05. Get AutoBahn testsuite source code: git clone git@github.com:oberstet/Autobahn.git + * + *
06. Go to AutoBahn directory: cd Autobahn + * + *
07. Checkout stable version: git checkout v0.4.2 + * + *
08. Go to test suite directory: cd testsuite/websockets + * + *
09. Edit fuzzing_clinet_spec.json and set the version to 10.
+ *
+ * {
+ * "servers": [{"agent": "Netty", "hostname": "localhost", "port": 9000, "version": 10}],
+ * "cases": ["*"]
+ * }
+ *
+ *
+ *
10. Run the test python fuzzing_client.py. Note that the actual test case python code is + * located in /usr/local/lib/python2.6/dist-packages/autobahn-0.4.2-py2.6.egg/autobahn/cases + * and not in the checked out git repository. + * + *
11. See the results in reports/servers/index.html
+ */
+package org.jboss.netty.example.http.websocketx.autobahn;
+
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java
new file mode 100644
index 0000000000..ec7db870f7
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2010 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.handler.codec.http.websocketx;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.util.CharsetUtil;
+
+/**
+ * Web Socket continuation frame containing continuation text or binary data.
+ * This is used for fragmented messages where the contents of a messages is
+ * contained more than 1 frame.
+ *
+ * @author Vibul Imtarnasan
+ */
+public class ContinuationWebSocketFrame extends WebSocketFrame {
+
+ private String aggregatedText = null;
+
+ /**
+ * Creates a new empty continuation frame.
+ */
+ public ContinuationWebSocketFrame() {
+ this.setBinaryData(ChannelBuffers.EMPTY_BUFFER);
+ }
+
+ /**
+ * Creates a new continuation frame with the specified binary data. The
+ * final fragment flag is set to true.
+ *
+ * @param binaryData
+ * the content of the frame.
+ */
+ public ContinuationWebSocketFrame(ChannelBuffer binaryData) {
+ this.setBinaryData(binaryData);
+ }
+
+ /**
+ * Creates a new continuation frame with the specified binary data
+ *
+ * @param finalFragment
+ * flag indicating if this frame is the final fragment
+ * @param rsv
+ * reserved bits used for protocol extensions
+ * @param binaryData
+ * the content of the frame.
+ */
+ public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) {
+ this.setFinalFragment(finalFragment);
+ this.setRsv(rsv);
+ this.setBinaryData(binaryData);
+ }
+
+ /**
+ * Creates a new continuation frame with the specified binary data
+ *
+ * @param finalFragment
+ * flag indicating if this frame is the final fragment
+ * @param rsv
+ * reserved bits used for protocol extensions
+ * @param binaryData
+ * the content of the frame.
+ * @param aggregatedText
+ * Aggregated text set by decoder on the final continuation frame
+ * of a fragmented text message
+ */
+ public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) {
+ this.setFinalFragment(finalFragment);
+ this.setRsv(rsv);
+ this.setBinaryData(binaryData);
+ this.aggregatedText = aggregatedText;
+ }
+
+ /**
+ * Creates a new continuation frame with the specified text data
+ *
+ * @param finalFragment
+ * flag indicating if this frame is the final fragment
+ * @param rsv
+ * reserved bits used for protocol extensions
+ * @param text
+ * text content of the frame.
+ */
+ public ContinuationWebSocketFrame(boolean finalFragment, int rsv, String text) {
+ this.setFinalFragment(finalFragment);
+ this.setRsv(rsv);
+ this.setText(text);
+ }
+
+ /**
+ * Returns the text data in this frame
+ */
+ public String getText() {
+ if (this.getBinaryData() == null) {
+ return null;
+ }
+ return this.getBinaryData().toString(CharsetUtil.UTF_8);
+ }
+
+ /**
+ * Sets the string for this frame
+ *
+ * @param text
+ * text to store
+ */
+ public void setText(String text) {
+ if (text == null || text.isEmpty()) {
+ this.setBinaryData(ChannelBuffers.EMPTY_BUFFER);
+ } else {
+ this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(data: " + getBinaryData() + ')';
+ }
+
+ /**
+ * Aggregated text returned by decoder on the final continuation frame of a
+ * fragmented text message
+ */
+ public String getAggregatedText() {
+ return aggregatedText;
+ }
+
+ public void setAggregatedText(String aggregatedText) {
+ this.aggregatedText = aggregatedText;
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java
new file mode 100644
index 0000000000..358f52684a
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java
@@ -0,0 +1,35 @@
+/*
+ * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ *
+ * Copyright (c) 2008-2009 Bjoern Hoehrmann