diff --git a/NOTICE.txt b/NOTICE.txt index a0b78e3202..02bb426a3d 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -96,3 +96,13 @@ framework implementation, which can be obtained at: * license/LICENSE.felix.txt (Apache License 2.0) * HOMEPAGE: * http://felix.apache.org/ + +This product optionally depends on 'Webbit', a Java event based +WebSocket and HTTP server: + + * LICENSE: + * license/LICENSE.webbit.txt (BSD License) + * HOMEPAGE: + * https://github.com/joewalnes/webbit + + \ No newline at end of file diff --git a/license/LICENSE.webbit.txt b/license/LICENSE.webbit.txt new file mode 100644 index 0000000000..05ae225fa3 --- /dev/null +++ b/license/LICENSE.webbit.txt @@ -0,0 +1,37 @@ +(BSD License: http://www.opensource.org/licenses/bsd-license) + +Copyright (c) 2011, Joe Walnes, Aslak Hellesøy and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the Webbit nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/docbook/en-US/module/architecture.xml b/src/docbook/en-US/module/architecture.xml index 44cbd0066b..5e4f883c91 100644 --- a/src/docbook/en-US/module/architecture.xml +++ b/src/docbook/en-US/module/architecture.xml @@ -41,20 +41,20 @@
true
*/
public ChannelLocal() {
- super();
+ this(true);
}
+
+ /**
+ * Creates a {@link Channel} local variable.
+ *
+ * @param removeOnClose if true
the {@link ChannelLocal} will remove a {@link Channel} from it own once the {@link Channel} was closed.
+ */
+ public ChannelLocal(boolean removeOnClose) {
+ this.removeOnClose = removeOnClose;
+ }
+
+
/**
* Returns the initial value of the variable. By default, it returns
* {@code null}. Override it to change the initial value.
*/
- protected T initialValue(@SuppressWarnings("unused") Channel channel) {
+ protected T initialValue(Channel channel) {
return null;
}
@@ -88,7 +108,11 @@ public class ChannelLocaltrue
as the last argument.
+ */
public DefaultFileRegion(FileChannel file, long position, long count) {
+ this(file, position, count, true);
+ }
+
+ public DefaultFileRegion(FileChannel file, long position, long count, boolean releaseAfterTransfer) {
this.file = file;
this.position = position;
this.count = count;
+ this.releaseAfterTransfer = releaseAfterTransfer;
}
@Override
@@ -31,6 +41,11 @@ public class DefaultFileRegion implements FileRegion {
return count;
}
+ @Override
+ public boolean releaseAfterTransfer() {
+ return releaseAfterTransfer;
+ }
+
@Override
public long transferTo(WritableByteChannel target, long position) throws IOException {
long count = this.count - position;
diff --git a/src/main/java/org/jboss/netty/channel/FileRegion.java b/src/main/java/org/jboss/netty/channel/FileRegion.java
index 7399453e4d..0780fb7a0d 100644
--- a/src/main/java/org/jboss/netty/channel/FileRegion.java
+++ b/src/main/java/org/jboss/netty/channel/FileRegion.java
@@ -73,6 +73,12 @@ public interface FileRegion extends ExternalResourceReleasable {
*/
long getCount();
+ /**
+ * Returns true
if {@link #releaseExternalResources()} has to
+ * be called after the transfer of this {@link FileRegion} is complete.
+ */
+ boolean releaseAfterTransfer();
+
/**
* Transfers the content of this file region to the specified channel.
*
diff --git a/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java b/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java
index 189269c46f..c942b8e7d7 100644
--- a/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java
+++ b/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java
@@ -151,7 +151,9 @@ public class SimpleChannelUpstreamHandler implements ChannelUpstreamHandler {
/**
* Invoked when a {@link Channel} is open, but not bound nor connected.
- */
+ * 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/example/http/websocketx/client/App.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java
new file mode 100644
index 0000000000..1112c45cf7
--- /dev/null
+++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java
@@ -0,0 +1,136 @@
+/*
+ * 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.client;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jboss.netty.buffer.ChannelBuffers;
+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.WebSocketSpecificationVersion;
+
+/**
+ * A HTTP client demo app
+ *
+ * @author Vibul Imtarnasan
+ *
+ * @version $Rev$, $Date$
+ */
+public class App {
+
+ public static void main(String[] args) throws Exception {
+ ConsoleHandler ch = new ConsoleHandler();
+ ch.setLevel(Level.FINE);
+ Logger.getLogger("").addHandler(ch);
+ Logger.getLogger("").setLevel(Level.FINE);
+
+ runClient();
+ System.exit(0);
+ }
+
+ /**
+ * Send and receive some messages using a web socket client
+ *
+ * @throws Exception
+ */
+ public static void runClient() throws Exception {
+
+ MyCallbackHandler callbackHandler = new MyCallbackHandler();
+ WebSocketClientFactory factory = new WebSocketClientFactory();
+
+ // Connect with spec version 10 (try changing it to V00 and it will
+ // still work ... fingers crossed ;-)
+ WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"),
+ WebSocketSpecificationVersion.V10, callbackHandler);
+
+ // Connect
+ System.out.println("WebSocket Client connecting");
+ client.connect().awaitUninterruptibly();
+ Thread.sleep(200);
+
+ // Send 10 messages and wait for responses
+ System.out.println("WebSocket Client sending message");
+ for (int i = 0; i < 10; i++) {
+ client.send(new TextWebSocketFrame("Message #" + i));
+ }
+ Thread.sleep(1000);
+
+ // Ping
+ System.out.println("WebSocket Client sending ping");
+ client.send(new PingWebSocketFrame(ChannelBuffers.copiedBuffer(new byte[] { 1, 2, 3, 4, 5, 6 })));
+ Thread.sleep(1000);
+
+ // Close
+ System.out.println("WebSocket Client sending close");
+ client.send(new CloseWebSocketFrame());
+ Thread.sleep(1000);
+
+ // Disconnect
+ client.disconnect();
+ }
+
+ /**
+ * Our web socket callback handler for this app
+ */
+ public static class MyCallbackHandler implements WebSocketCallback {
+ public boolean connected = false;
+ public ArrayList This is an example web service client.
+ * To run this example, you must first start
+ * org.jboss.netty.example.http.websocketx.server.WebSocketServer
+ * Next, run org.jboss.netty.example.http.websocketx.client.App.
+ */
+package org.jboss.netty.example.http.websocketx.client;
+
diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java
new file mode 100644
index 0000000000..37869480db
--- /dev/null
+++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java
@@ -0,0 +1,74 @@
+/*
+ * 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.server;
+
+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 HTTP server which serves Web Socket requests at:
+ *
+ * http://localhost:8080/websocket
+ *
+ * Open your browser at http://localhost:8080/, then the demo page will be
+ * loaded and a Web Socket connection will be made automatically.
+ *
+ * This server illustrates support for the different web socket specification
+ * versions and will work with:
+ *
+ * This package contains an example web socket web server.
+ * The web server only handles text, ping and closing frames. For text frames,
+ * it echoes the received text in upper case.
+ * Once started, you can test the web server against your browser by navigating
+ * to http://localhost:8080/
+ * You can also test it with a web socket client. Send web socket traffic to
+ * ws://localhost:8080/websocket.
+ */
+package org.jboss.netty.example.http.websocketx.server;
+
diff --git a/src/main/java/org/jboss/netty/example/iostream/IOStream.java b/src/main/java/org/jboss/netty/example/iostream/IOStream.java
new file mode 100755
index 0000000000..1abc192d2c
--- /dev/null
+++ b/src/main/java/org/jboss/netty/example/iostream/IOStream.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2011 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.iostream;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.channel.*;
+import org.jboss.netty.channel.iostream.IOStreamAddress;
+import org.jboss.netty.channel.iostream.IOStreamChannelFactory;
+import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
+import org.jboss.netty.handler.codec.frame.Delimiters;
+import org.jboss.netty.handler.codec.string.StringDecoder;
+import org.jboss.netty.handler.codec.string.StringEncoder;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * An example demonstrating the use of the {@link org.jboss.netty.channel.iostream.IOStreamChannel}.
+ *
+ * @author Daniel Bimschas
+ * @author Dennis Pfisterer
+ */
+public class IOStream {
+
+ private static volatile boolean running = true;
+
+ public static void main(String[] args) {
+
+ final ExecutorService executorService = Executors.newCachedThreadPool();
+ final ClientBootstrap bootstrap = new ClientBootstrap(new IOStreamChannelFactory(executorService));
+
+ // Configure the event pipeline factory.
+ bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
+ public ChannelPipeline getPipeline() throws Exception {
+ DefaultChannelPipeline pipeline = new DefaultChannelPipeline();
+ pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
+ pipeline.addLast("decoder", new StringDecoder());
+ pipeline.addLast("encoder", new StringEncoder());
+ pipeline.addLast("loggingHandler", new SimpleChannelHandler() {
+ @Override
+ public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e)
+ throws Exception {
+
+ final String message = (String) e.getMessage();
+ synchronized (System.out) {
+ e.getChannel().write("Message received: " + message);
+ }
+ if ("exit".equals(message)) {
+ IOStream.running = false;
+ }
+ super.messageReceived(ctx, e);
+ }
+ }
+ );
+ return pipeline;
+ }
+ });
+
+ // Make a new connection.
+ ChannelFuture connectFuture = bootstrap.connect(new IOStreamAddress(System.in, System.out));
+
+ // Wait until the connection is made successfully.
+ Channel channel = connectFuture.awaitUninterruptibly().getChannel();
+
+ while (running) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Close the connection.
+ channel.close().awaitUninterruptibly();
+
+ // Shut down all thread pools to exit.
+ bootstrap.releaseExternalResources();
+
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java
index a141402c29..313d38b7ce 100644
--- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java
+++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java
@@ -60,30 +60,34 @@ public class HttpContentCompressor extends HttpContentEncoder {
}
@Override
- protected EncoderEmbedder
* Please note that this is an abstract class. You have to extend this class
- * and implement {@link #newContentEncoder(String)} and {@link #getTargetContentEncoding(String)}
- * properly to make this class functional. For example, refer to the source
- * code of {@link HttpContentCompressor}.
+ * and implement {@link #beginEncode(HttpMessage, String)} properly to make
+ * this class functional. For example, refer to the source code of
+ * {@link HttpContentCompressor}.
*
* This handler must be placed after {@link HttpMessageEncoder} in the pipeline
* so that this handler can intercept HTTP responses before {@link HttpMessageEncoder}
@@ -103,26 +104,37 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler {
}
boolean hasContent = m.isChunked() || m.getContent().readable();
- if (hasContent && (encoder = newContentEncoder(acceptEncoding)) != null) {
- // Encode the content and remove or replace the existing headers
- // so that the message looks like a decoded message.
- m.setHeader(
- HttpHeaders.Names.CONTENT_ENCODING,
- getTargetContentEncoding(acceptEncoding));
+ if (!hasContent) {
+ ctx.sendDownstream(e);
+ return;
+ }
- if (!m.isChunked()) {
- ChannelBuffer content = m.getContent();
- // Encode the content.
- content = ChannelBuffers.wrappedBuffer(
- encode(content), finishEncode());
+ Result result = beginEncode(m, acceptEncoding);
+ if (result == null) {
+ ctx.sendDownstream(e);
+ return;
+ }
- // Replace the content.
- m.setContent(content);
- if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) {
- m.setHeader(
- HttpHeaders.Names.CONTENT_LENGTH,
- Integer.toString(content.readableBytes()));
- }
+ encoder = result.getContentEncoder();
+
+ // Encode the content and remove or replace the existing headers
+ // so that the message looks like a decoded message.
+ m.setHeader(
+ HttpHeaders.Names.CONTENT_ENCODING,
+ result.getTargetContentEncoding());
+
+ if (!m.isChunked()) {
+ ChannelBuffer content = m.getContent();
+ // Encode the content.
+ content = ChannelBuffers.wrappedBuffer(
+ encode(content), finishEncode());
+
+ // Replace the content.
+ m.setContent(content);
+ if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) {
+ m.setHeader(
+ HttpHeaders.Names.CONTENT_LENGTH,
+ Integer.toString(content.readableBytes()));
}
}
@@ -162,24 +174,20 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler {
}
/**
- * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
- * content.
+ * Prepare to encode the HTTP message content.
*
+ * @param msg
+ * the HTTP message whose content should be encoded
* @param acceptEncoding
* the value of the {@code "Accept-Encoding"} header
*
- * @return a new {@link EncoderEmbedder} if there is a supported encoding
- * in {@code acceptEncoding}. {@code null} otherwise.
+ * @return the result of preparation, which is composed of the determined
+ * target content encoding and a new {@link EncoderEmbedder} that
+ * encodes the content into the target content encoding.
+ * {@code null} if {@code acceptEncoding} is unsupported or rejected
+ * and thus the content should be handled as-is (i.e. no encoding).
*/
- protected abstract EncoderEmbedder
+ * For the detailed instruction on adding add Web Socket support to your HTTP
+ * server, take a look into the WebSocketServer example located in the
+ * {@code org.jboss.netty.example.http.websocket} package.
+ *
+ * @author The Netty Project
+ * @author Mike Heath (mheath@apache.org)
+ * @author Trustin Lee
+ * @version $Rev: 2342 $, $Date: 2010-07-07 14:07:39 +0900 (Wed, 07 Jul 2010) $
+ *
+ * @apiviz.landmark
+ * @apiviz.uses org.jboss.netty.handler.codec.http.websocket.WebSocketFrame
+ */
+public class WebSocket00FrameDecoder extends ReplayingDecoder
+ * For the detailed instruction on adding add Web Socket support to your HTTP
+ * server, take a look into the WebSocketServer example located in the
+ * {@code org.jboss.netty.example.http.websocket} package.
+ *
+ * @author The Netty Project
+ * @author Mike Heath (mheath@apache.org)
+ * @author Trustin Lee
+ * @version $Rev: 2362 $, $Date: 2010-09-09 19:59:22 +0900 (Thu, 09 Sep 2010) $
+ *
+ * @apiviz.landmark
+ * @apiviz.uses org.jboss.netty.handler.codec.http.websocket.WebSocketFrame
+ */
+@Sharable
+public class WebSocket00FrameEncoder extends OneToOneEncoder {
+
+ @Override
+ protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception {
+ if (msg instanceof WebSocketFrame) {
+ WebSocketFrame frame = (WebSocketFrame) msg;
+ if (frame instanceof TextWebSocketFrame) {
+ // Text frame
+ ChannelBuffer data = frame.getBinaryData();
+ ChannelBuffer encoded = channel.getConfig().getBufferFactory()
+ .getBuffer(data.order(), data.readableBytes() + 2);
+ encoded.writeByte((byte) 0x00);
+ encoded.writeBytes(data, data.readerIndex(), data.readableBytes());
+ encoded.writeByte((byte) 0xFF);
+ return encoded;
+ } else if (frame instanceof CloseWebSocketFrame) {
+ // Close frame
+ ChannelBuffer data = frame.getBinaryData();
+ ChannelBuffer encoded = channel.getConfig().getBufferFactory().getBuffer(data.order(), 2);
+ encoded.writeByte((byte) 0xFF);
+ encoded.writeByte((byte) 0x00);
+ return encoded;
+ } else {
+ // Binary frame
+ ChannelBuffer data = frame.getBinaryData();
+ int dataLen = data.readableBytes();
+ ChannelBuffer encoded = channel.getConfig().getBufferFactory().getBuffer(data.order(), dataLen + 5);
+
+ // Encode type.
+ encoded.writeByte((byte) 0x80);
+
+ // Encode length.
+ int b1 = dataLen >>> 28 & 0x7F;
+ int b2 = dataLen >>> 14 & 0x7F;
+ int b3 = dataLen >>> 7 & 0x7F;
+ int b4 = dataLen & 0x7F;
+ if (b1 == 0) {
+ if (b2 == 0) {
+ if (b3 == 0) {
+ encoded.writeByte(b4);
+ } else {
+ encoded.writeByte(b3 | 0x80);
+ encoded.writeByte(b4);
+ }
+ } else {
+ encoded.writeByte(b2 | 0x80);
+ encoded.writeByte(b3 | 0x80);
+ encoded.writeByte(b4);
+ }
+ } else {
+ encoded.writeByte(b1 | 0x80);
+ encoded.writeByte(b2 | 0x80);
+ encoded.writeByte(b3 | 0x80);
+ encoded.writeByte(b4);
+ }
+
+ // Encode binary data.
+ encoded.writeBytes(data, data.readerIndex(), dataLen);
+ return encoded;
+ }
+ }
+ return msg;
+ }
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
new file mode 100644
index 0000000000..2b76491502
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
@@ -0,0 +1,370 @@
+// (BSD License: http://www.opensource.org/licenses/bsd-license)
+//
+// Copyright (c) 2011, Joe Walnes and contributors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or
+// without modification, are permitted provided that the
+// following conditions are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the
+// following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the
+// following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// * Neither the name of the Webbit nor the names of
+// its contributors may be used to endorse or promote products
+// derived from this software without specific prior written
+// permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+package org.jboss.netty.handler.codec.http.websocketx;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFutureListener;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.frame.CorruptedFrameException;
+import org.jboss.netty.handler.codec.frame.TooLongFrameException;
+import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
+import org.jboss.netty.logging.InternalLogger;
+import org.jboss.netty.logging.InternalLoggerFactory;
+
+/**
+ * Decodes a web socket frame from wire protocol version 8 format. This code was
+ * forked from webbit and modified.
+ *
+ * @author Aslak Hellesøy
+ * @author Vibul Imtarnasan
+ */
+public class WebSocket08FrameDecoder extends ReplayingDecoder
+ * Encodes a web socket frame into wire protocol version 8 format. This code was
+ * forked from webbit and modified.
+ *
+ * Performs client side opening and closing handshakes for web socket
+ * specification version draft-ietf-hybi-thewebsocketprotocol- 00
+ *
+ * A very large portion of this code was taken from the Netty 3.2 HTTP example.
+ *
+ * Sends the opening request to the server:
+ *
+ * Process server response:
+ *
+ * Performs client side opening and closing handshakes for web socket
+ * specification version draft-ietf-hybi-thewebsocketprotocol- 10
+ *
+ * Sends the opening request to the server:
+ *
+ * Process server response:
+ *
+ * Performs server side opening and closing handshakes for web socket
+ * specification version draft-ietf-hybi-thewebsocketprotocol- 00
+ *
+ * A very large portion of this code was taken from the Netty 3.2 HTTP example.
+ *
+ * Handle the web socket handshake for the web socket specification HyBi
+ * version 0 and lower. This standard is really a rehash of hixie-76 and hixie-75.
+ *
+ * Browser request to the server:
+ *
+ * Server response:
+ *
+ * Performs server side opening and closing handshakes for web socket
+ * specification version draft-ietf-hybi-thewebsocketprotocol- 10
+ *
+ * Handle the web socket handshake for the web socket specification HyBi
+ * version 8 to 10. Version 8, 9 and 10 share the same wire protocol.
+ *
+ * Browser request to the server:
+ *
+ * Server response:
+ *
+ * Versions of the web socket specification.
+ *
+ * A specification is tied to one wire protocol version but a protocol version
+ * may have use by more than 1 version of the specification.
+ *
+ * This package supports different web socket specification versions (hence the X suffix).
+ * The specification current supported are:
+ *
+ * In the future, as the specification develops, more versions will be supported.
+ * This contrasts the org.jboss.netty.handler.codec.http.websocket package which only
+ * supports draft-ietf-hybi-thewebsocketprotocol-00.
+ *
+ * For the detailed instruction on adding add Web Socket support to your HTTP
+ * server, take a look into the WebSocketServerX example located in the
+ * {@code org.jboss.netty.example.http.websocket} package.
+ *
- * TLS renegotiation has been disabled by default due to a known security issue,
- * CVE-2009-3555.
- * You can re-enable renegotiation by calling {@link #setEnableRenegotiation(boolean)}
- * with {@code true} at your own risk.
- *
- * If {@link #isEnableRenegotiation() enableRenegotiation} is {@code true} and
- * the initial handshake has been done successfully, you can call
+ * If {@link #isEnableRenegotiation() enableRenegotiation} is {@code true}
+ * (default) and the initial handshake has been done successfully, you can call
* {@link #handshake()} to trigger the renegotiation.
*
* If {@link #isEnableRenegotiation() enableRenegotiation} is {@code false},
* an attempt to trigger renegotiation will result in the connection closure.
+ *
+ * Please note that TLS renegotiation had a security issue before. If your
+ * runtime environment did not fix it, please make sure to disable TLS
+ * renegotiation by calling {@link #setEnableRenegotiation(boolean)} with
+ * {@code false}. For more information, please refer to the following documents:
+ *
@@ -173,7 +178,7 @@ public class SslHandler extends FrameDecoder
private final Executor delegatedTaskExecutor;
private final boolean startTls;
- private volatile boolean enableRenegotiation;
+ private volatile boolean enableRenegotiation = true;
final Object handshakeLock = new Object();
private boolean handshaking;
@@ -866,46 +871,50 @@ public class SslHandler extends FrameDecoder
loop:
for (;;) {
SSLEngineResult result;
+ boolean needsHandshake = false;
synchronized (handshakeLock) {
if (!handshaken && !handshaking &&
!engine.getUseClientMode() &&
!engine.isInboundDone() && !engine.isOutboundDone()) {
- handshake();
- }
-
- try {
- result = engine.unwrap(inNetBuf, outAppBuf);
- } catch (SSLException e) {
- throw e;
- }
-
- final HandshakeStatus handshakeStatus = result.getHandshakeStatus();
- handleRenegotiation(handshakeStatus);
- switch (handshakeStatus) {
- case NEED_UNWRAP:
- if (inNetBuf.hasRemaining() && !engine.isInboundDone()) {
- break;
- } else {
- break loop;
- }
- case NEED_WRAP:
- wrapNonAppData(ctx, channel);
- break;
- case NEED_TASK:
- runDelegatedTasks();
- break;
- case FINISHED:
- setHandshakeSuccess(channel);
- needsWrap = true;
- break loop;
- case NOT_HANDSHAKING:
- needsWrap = true;
- break loop;
- default:
- throw new IllegalStateException(
- "Unknown handshake status: " + handshakeStatus);
+ needsHandshake = true;
+
}
}
+ if (needsHandshake) {
+ handshake();
+ }
+
+ synchronized (handshakeLock) {
+ result = engine.unwrap(inNetBuf, outAppBuf);
+ }
+
+ final HandshakeStatus handshakeStatus = result.getHandshakeStatus();
+ handleRenegotiation(handshakeStatus);
+ switch (handshakeStatus) {
+ case NEED_UNWRAP:
+ if (inNetBuf.hasRemaining() && !engine.isInboundDone()) {
+ break;
+ } else {
+ break loop;
+ }
+ case NEED_WRAP:
+ wrapNonAppData(ctx, channel);
+ break;
+ case NEED_TASK:
+ runDelegatedTasks();
+ break;
+ case FINISHED:
+ setHandshakeSuccess(channel);
+ needsWrap = true;
+ break loop;
+ case NOT_HANDSHAKING:
+ needsWrap = true;
+ break loop;
+ default:
+ throw new IllegalStateException(
+ "Unknown handshake status: " + handshakeStatus);
+ }
+
}
if (needsWrap) {
@@ -1037,29 +1046,59 @@ public class SslHandler extends FrameDecoder
if (handshakeFuture == null) {
handshakeFuture = future(channel);
}
+
+ // Release all resources such as internal buffers that SSLEngine
+ // is managing.
+
+ engine.closeOutbound();
+
+ try {
+ engine.closeInbound();
+ } catch (SSLException e) {
+ logger.debug(
+ "SSLEngine.closeInbound() raised an exception after " +
+ "a handshake failure.", e);
+ }
}
+
handshakeFuture.setFailure(cause);
}
private void closeOutboundAndChannel(
- final ChannelHandlerContext context, final ChannelStateEvent e) throws SSLException {
+ final ChannelHandlerContext context, final ChannelStateEvent e) {
if (!e.getChannel().isConnected()) {
context.sendDownstream(e);
return;
}
- unwrap(context, e.getChannel(), ChannelBuffers.EMPTY_BUFFER, 0, 0);
- if (!engine.isInboundDone()) {
- if (sentCloseNotify.compareAndSet(false, true)) {
- engine.closeOutbound();
- ChannelFuture closeNotifyFuture = wrapNonAppData(context, e.getChannel());
- closeNotifyFuture.addListener(
- new ClosingChannelFutureListener(context, e));
- return;
+ boolean success = false;
+ try {
+ try {
+ unwrap(context, e.getChannel(), ChannelBuffers.EMPTY_BUFFER, 0, 0);
+ } catch (SSLException ex) {
+ logger.debug("Failed to unwrap before sending a close_notify message", ex);
+ }
+
+ if (!engine.isInboundDone()) {
+ if (sentCloseNotify.compareAndSet(false, true)) {
+ engine.closeOutbound();
+ try {
+ ChannelFuture closeNotifyFuture = wrapNonAppData(context, e.getChannel());
+ closeNotifyFuture.addListener(
+ new ClosingChannelFutureListener(context, e));
+ success = true;
+ } catch (SSLException ex) {
+ logger.debug("Failed to encode a close_notify message", ex);
+ }
+ }
+ } else {
+ success = true;
+ }
+ } finally {
+ if (!success) {
+ context.sendDownstream(e);
}
}
-
- context.sendDownstream(e);
}
private static final class PendingWrite {
diff --git a/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java b/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java
index 27b6cee87e..7ba9e35663 100644
--- a/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java
+++ b/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java
@@ -154,34 +154,36 @@ public class ChunkedWriteHandler implements ChannelUpstreamHandler, ChannelDowns
private void discard(ChannelHandlerContext ctx) {
ClosedChannelException cause = null;
boolean fireExceptionCaught = false;
- synchronized (this) {
- for (;;) {
- if (currentEvent == null) {
- currentEvent = queue.poll();
- }
-
- if (currentEvent == null) {
- break;
- }
-
- MessageEvent currentEvent = this.currentEvent;
- this.currentEvent = null;
-
- Object m = currentEvent.getMessage();
- if (m instanceof ChunkedInput) {
- closeInput((ChunkedInput) m);
- }
-
- // Trigger a ClosedChannelException
- if (cause == null) {
- cause = new ClosedChannelException();
- }
- currentEvent.getFuture().setFailure(cause);
- fireExceptionCaught = true;
-
- currentEvent = null;
+
+ for (;;) {
+ MessageEvent currentEvent = this.currentEvent;
+
+ if (this.currentEvent == null) {
+ currentEvent = queue.poll();
+ } else {
+ this.currentEvent = null;
}
+
+ if (currentEvent == null) {
+ break;
+ }
+
+
+ Object m = currentEvent.getMessage();
+ if (m instanceof ChunkedInput) {
+ closeInput((ChunkedInput) m);
+ }
+
+ // Trigger a ClosedChannelException
+ if (cause == null) {
+ cause = new ClosedChannelException();
+ }
+ currentEvent.getFuture().setFailure(cause);
+ fireExceptionCaught = true;
+
+ currentEvent = null;
}
+
if (fireExceptionCaught) {
Channels.fireExceptionCaught(ctx.getChannel(), cause);
diff --git a/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java b/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java
index b7eabe2c09..9356554a48 100644
--- a/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java
+++ b/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java
@@ -136,7 +136,7 @@ public class WriteTimeoutHandler extends SimpleChannelDownstreamHandler
timer.stop();
}
- protected long getTimeoutMillis(@SuppressWarnings("unused") MessageEvent e) {
+ protected long getTimeoutMillis(MessageEvent e) {
return timeoutMillis;
}
+ *
+ *
+ * @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()));
+
+ // Set up the event pipeline factory.
+ bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory());
+
+ // Bind and start to accept incoming connections.
+ bootstrap.bind(new InetSocketAddress(8080));
+
+ System.out.println("Web Socket Server started on 8080. Open your browser and navigate to http://localhost:8080/");
+ }
+}
diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java
new file mode 100644
index 0000000000..d2b2d6ac85
--- /dev/null
+++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java
@@ -0,0 +1,153 @@
+/*
+ * 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.server;
+
+import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
+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.ChannelBuffer;
+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.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.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 static final String WEBSOCKET_PATH = "/websocket";
+
+ 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;
+ }
+
+ // Send the demo page and favicon.ico
+ if (req.getUri().equals("/")) {
+ HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK);
+
+ ChannelBuffer content = WebSocketServerIndexPage.getContent(getWebSocketLocation(req));
+
+ res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
+ setContentLength(res, content.readableBytes());
+
+ res.setContent(content);
+ sendHttpResponse(ctx, req, res);
+ return;
+ } else if (req.getUri().equals("/favicon.ico")) {
+ HttpResponse res = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND);
+ sendHttpResponse(ctx, req, res);
+ 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) {
+
+ // Check for closing frame
+ if (frame instanceof CloseWebSocketFrame) {
+ this.handshaker.executeClosingHandshake(ctx, (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()));
+ }
+
+ // Send the uppercase string back.
+ String request = ((TextWebSocketFrame) frame).getText();
+ logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), request));
+ ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase()));
+ }
+
+ 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) + WEBSOCKET_PATH;
+ }
+}
diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java
new file mode 100644
index 0000000000..179df6b633
--- /dev/null
+++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java
@@ -0,0 +1,73 @@
+/*
+ * 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.server;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.util.CharsetUtil;
+
+
+/**
+ * Generates the demo HTML page which is served at http://localhost:8080/
+ *
+ * @author The Netty Project
+ * @author Trustin Lee
+ * @author Vibul Imtarnasan
+ *
+ * @version $Rev$, $Date$
+ */
+public class WebSocketServerIndexPage {
+
+ private static final String NEWLINE = "\r\n";
+
+ public static ChannelBuffer getContent(String webSocketLocation) {
+ return ChannelBuffers.copiedBuffer(
+ "
+ * GET /demo HTTP/1.1
+ * Upgrade: WebSocket
+ * Connection: Upgrade
+ * Host: example.com
+ * Origin: http://example.com
+ * Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+ * Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+ *
+ * ^n:ds[4U
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param channel
+ * Channel into which we can write our request
+ */
+ @Override
+ public void beginOpeningHandshake(ChannelHandlerContext ctx, Channel channel) {
+ // Make keys
+ int spaces1 = createRandomNumber(1, 12);
+ int spaces2 = createRandomNumber(1, 12);
+
+ int max1 = Integer.MAX_VALUE / spaces1;
+ int max2 = Integer.MAX_VALUE / spaces2;
+
+ int number1 = createRandomNumber(0, max1);
+ int number2 = createRandomNumber(0, max2);
+
+ int product1 = number1 * spaces1;
+ int product2 = number2 * spaces2;
+
+ String key1 = Integer.toString(product1);
+ String key2 = Integer.toString(product2);
+
+ key1 = insertRandomCharacters(key1);
+ key2 = insertRandomCharacters(key2);
+
+ key1 = insertSpaces(key1, spaces1);
+ key2 = insertSpaces(key2, spaces2);
+
+ byte[] key3 = createRandomBytes(8);
+
+ ByteBuffer buffer = ByteBuffer.allocate(4);
+ buffer.putInt(number1);
+ byte[] number1Array = buffer.array();
+ buffer = ByteBuffer.allocate(4);
+ buffer.putInt(number2);
+ byte[] number2Array = buffer.array();
+
+ byte[] challenge = new byte[16];
+ System.arraycopy(number1Array, 0, challenge, 0, 4);
+ System.arraycopy(number2Array, 0, challenge, 4, 4);
+ System.arraycopy(key3, 0, challenge, 8, 8);
+ this.expectedChallengeResponseBytes = md5(challenge);
+
+ // Get path
+ URI wsURL = this.getWebSocketURL();
+ String path = wsURL.getPath();
+ if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
+ path = wsURL.getPath() + "?" + wsURL.getQuery();
+ }
+
+ // Format request
+ HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+ request.addHeader(Names.UPGRADE, Values.WEBSOCKET);
+ request.addHeader(Names.CONNECTION, Values.UPGRADE);
+ request.addHeader(Names.HOST, wsURL.getHost());
+ request.addHeader(Names.ORIGIN, "http://" + wsURL.getHost());
+ request.addHeader(Names.SEC_WEBSOCKET_KEY1, key1);
+ request.addHeader(Names.SEC_WEBSOCKET_KEY2, key2);
+ if (this.getSubProtocolRequest() != null && !this.getSubProtocolRequest().equals("")) {
+ request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, this.getSubProtocolRequest());
+ }
+ request.setContent(ChannelBuffers.copiedBuffer(key3));
+
+ channel.write(request);
+
+ ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket00FrameEncoder());
+ }
+
+ /**
+ *
+ * HTTP/1.1 101 WebSocket Protocol Handshake
+ * Upgrade: WebSocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Origin: http://example.com
+ * Sec-WebSocket-Location: ws://example.com/demo
+ * Sec-WebSocket-Protocol: sample
+ *
+ * 8jKS'y:G*Co,Wxa-
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param response
+ * HTTP response returned from the server for the request sent by
+ * beginOpeningHandshake00().
+ * @throws WebSocketHandshakeException
+ */
+ @Override
+ public void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response)
+ throws WebSocketHandshakeException {
+ final HttpResponseStatus status = new HttpResponseStatus(101, "WebSocket Protocol Handshake");
+
+ if (!response.getStatus().equals(status)) {
+ throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
+ }
+
+ String upgrade = response.getHeader(Names.UPGRADE);
+ if (upgrade == null || !upgrade.equals(Values.WEBSOCKET)) {
+ throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
+ + response.getHeader(Names.UPGRADE));
+ }
+
+ String connection = response.getHeader(Names.CONNECTION);
+ if (connection == null || !connection.equals(Values.UPGRADE)) {
+ throw new WebSocketHandshakeException("Invalid handshake response connection: "
+ + response.getHeader(Names.CONNECTION));
+ }
+
+ byte[] challenge = response.getContent().array();
+ if (!Arrays.equals(challenge, expectedChallengeResponseBytes)) {
+ throw new WebSocketHandshakeException("Invalid challenge");
+ }
+
+ String protocol = response.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
+ this.setSubProtocolResponse(protocol);
+
+ ctx.getPipeline().replace("decoder", "ws-decoder", new WebSocket00FrameDecoder());
+
+ this.setOpenningHandshakeCompleted(true);
+ return;
+ }
+
+ private String insertRandomCharacters(String key) {
+ int count = createRandomNumber(1, 12);
+
+ char[] randomChars = new char[count];
+ int randCount = 0;
+ while (randCount < count) {
+ int rand = (int) (Math.random() * 0x7e + 0x21);
+ if (((0x21 < rand) && (rand < 0x2f)) || ((0x3a < rand) && (rand < 0x7e))) {
+ randomChars[randCount] = (char) rand;
+ randCount += 1;
+ }
+ }
+
+ for (int i = 0; i < count; i++) {
+ int split = createRandomNumber(0, key.length());
+ String part1 = key.substring(0, split);
+ String part2 = key.substring(split);
+ key = part1 + randomChars[i] + part2;
+ }
+
+ return key;
+ }
+
+ private String insertSpaces(String key, int spaces) {
+ for (int i = 0; i < spaces; i++) {
+ int split = createRandomNumber(1, key.length() - 1);
+ String part1 = key.substring(0, split);
+ String part2 = key.substring(split);
+ key = part1 + " " + part2;
+ }
+
+ return key;
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java
new file mode 100644
index 0000000000..5a6db45850
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java
@@ -0,0 +1,194 @@
+/*
+ * 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 java.net.URI;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
+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.HttpMethod;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import org.jboss.netty.handler.codec.http.HttpVersion;
+import org.jboss.netty.logging.InternalLogger;
+import org.jboss.netty.logging.InternalLoggerFactory;
+import org.jboss.netty.util.CharsetUtil;
+
+/**
+ *
+ * GET /chat HTTP/1.1
+ * Host: server.example.com
+ * Upgrade: websocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
+ * Sec-WebSocket-Origin: http://example.com
+ * Sec-WebSocket-Protocol: chat, superchat
+ * Sec-WebSocket-Version: 8
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param channel
+ * Channel into which we can write our request
+ */
+ @Override
+ public void beginOpeningHandshake(ChannelHandlerContext ctx, Channel channel) {
+ // Get path
+ URI wsURL = this.getWebSocketURL();
+ String path = wsURL.getPath();
+ if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
+ path = wsURL.getPath() + "?" + wsURL.getQuery();
+ }
+
+ // Get 16 bit nonce and base 64 encode it
+ byte[] nonce = createRandomBytes(16);
+ String key = base64Encode(nonce);
+
+ String acceptSeed = key + MAGIC_GUID;
+ byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
+ this.expectedChallengeResponseString = base64Encode(sha1);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("HyBi10 Client Handshake key: %s. Expected response: %s.", key,
+ this.expectedChallengeResponseString));
+ }
+
+ // Format request
+ HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+ request.addHeader(Names.UPGRADE, Values.WEBSOCKET.toLowerCase());
+ request.addHeader(Names.CONNECTION, Values.UPGRADE);
+ request.addHeader(Names.SEC_WEBSOCKET_KEY, key);
+ request.addHeader(Names.HOST, wsURL.getHost());
+ request.addHeader(Names.ORIGIN, "http://" + wsURL.getHost());
+ if (protocol != null && !protocol.equals("")) {
+ request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol);
+ }
+ request.addHeader(Names.SEC_WEBSOCKET_VERSION, "8");
+
+ channel.write(request);
+
+ ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket08FrameEncoder(true));
+ return;
+ }
+
+ /**
+ *
+ * HTTP/1.1 101 Switching Protocols
+ * Upgrade: websocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+ * Sec-WebSocket-Protocol: chat
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param response
+ * HTTP response returned from the server for the request sent by
+ * beginOpeningHandshake00().
+ * @throws WebSocketHandshakeException
+ */
+ @Override
+ public void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response)
+ throws WebSocketHandshakeException {
+ final HttpResponseStatus status = new HttpResponseStatus(101, "Switching Protocols");
+
+ if (!response.getStatus().equals(status)) {
+ throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
+ }
+
+ String upgrade = response.getHeader(Names.UPGRADE);
+ if (upgrade == null || !upgrade.equals(Values.WEBSOCKET.toLowerCase())) {
+ throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
+ + response.getHeader(Names.UPGRADE));
+ }
+
+ String connection = response.getHeader(Names.CONNECTION);
+ if (connection == null || !connection.equals(Values.UPGRADE)) {
+ throw new WebSocketHandshakeException("Invalid handshake response connection: "
+ + response.getHeader(Names.CONNECTION));
+ }
+
+ String accept = response.getHeader(Names.SEC_WEBSOCKET_ACCEPT);
+ if (accept == null || !accept.equals(this.expectedChallengeResponseString)) {
+ throw new WebSocketHandshakeException(String.format("Invalid challenge. Actual: %s. Expected: %s", accept,
+ this.expectedChallengeResponseString));
+ }
+
+ ctx.getPipeline().replace("decoder", "ws-decoder", new WebSocket08FrameDecoder(false, this.allowExtensions));
+
+ this.setOpenningHandshakeCompleted(true);
+ return;
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
new file mode 100644
index 0000000000..341a20e676
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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 java.net.URI;
+
+/**
+ * Instances the appropriate handshake class to use for clients
+ *
+ * @author The Netty Project
+ */
+public class WebSocketClientHandshakerFactory {
+
+ /**
+ * Instances a new handshaker
+ *
+ * @param webSocketURL
+ * URL for web socket communications. e.g
+ * "ws://myhost.com/mypath". Subsequent web socket frames will be
+ * sent to this URL.
+ * @param version
+ * Version of web socket specification to use to connect to the
+ * server
+ * @param subProtocol
+ * Sub protocol request sent to the server. Null if no
+ * sub-protocol support is required.
+ * @param allowExtensions
+ * Allow extensions to be used in the reserved bits of the web socket frame
+ * @throws WebSocketHandshakeException
+ */
+ public WebSocketClientHandshaker newHandshaker(URI webSocketURL, WebSocketSpecificationVersion version,
+ String subProtocol, boolean allowExtensions) throws WebSocketHandshakeException {
+ if (version == WebSocketSpecificationVersion.V10) {
+ return new WebSocketClientHandshaker10(webSocketURL, version, subProtocol, allowExtensions);
+ }
+ if (version == WebSocketSpecificationVersion.V00) {
+ return new WebSocketClientHandshaker00(webSocketURL, version, subProtocol);
+ }
+
+ throw new WebSocketHandshakeException("Protocol version " + version.toString() + " not supported.");
+
+ }
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java
new file mode 100644
index 0000000000..2421a60850
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * Base class for web socket frames
+ *
+ * @author The Netty Project
+ */
+public abstract class WebSocketFrame {
+
+ /**
+ * Flag to indicate if this frame is the final fragment in a message. The
+ * first fragment (frame) may also be the final fragment.
+ */
+ private boolean finalFragment = true;
+
+ /**
+ * RSV1, RSV2, RSV3 used for extensions
+ */
+ private int rsv = 0;
+
+ /**
+ * Contents of this frame
+ */
+ private ChannelBuffer binaryData;
+
+ /**
+ * Returns binary data
+ */
+ public ChannelBuffer getBinaryData() {
+ return binaryData;
+ }
+
+ /**
+ * Sets the binary data for this frame
+ */
+ public void setBinaryData(ChannelBuffer binaryData) {
+ this.binaryData = binaryData;
+ }
+
+ /**
+ * Flag to indicate if this frame is the final fragment in a message. The
+ * first fragment (frame) may also be the final fragment.
+ */
+ public boolean isFinalFragment() {
+ return finalFragment;
+ }
+
+ public void setFinalFragment(boolean finalFragment) {
+ this.finalFragment = finalFragment;
+ }
+
+ /**
+ * Bits used for extensions to the standard.
+ */
+ public int getRsv() {
+ return rsv;
+ }
+
+ public void setRsv(int rsv) {
+ this.rsv = rsv;
+ }
+
+
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java
new file mode 100644
index 0000000000..8e80602045
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * Type of web socket frames
+ *
+ * @author The Netty Project
+ */
+public enum WebSocketFrameType {
+ TEXT, BINARY, PING, PONG, CLOSE, CONTINUATION
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java
new file mode 100644
index 0000000000..db9f768615
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Exception during handshaking process
+ *
+ * @author The Netty Project
+ */
+public class WebSocketHandshakeException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public WebSocketHandshakeException(String s) {
+ super(s);
+ }
+
+ public WebSocketHandshakeException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
new file mode 100644
index 0000000000..29656ae060
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
@@ -0,0 +1,191 @@
+/*
+ * 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 java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.base64.Base64;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.util.CharsetUtil;
+
+/**
+ * Base class for server side web socket opening and closing handshakes
+ *
+ * @author The Netty Project
+ */
+public abstract class WebSocketServerHandshaker {
+
+ private String webSocketURL;
+
+ private String subProtocols;
+
+ private String[] subProtocolsArray = null;
+
+ private WebSocketSpecificationVersion version = WebSocketSpecificationVersion.UNKNOWN;
+
+ /**
+ * Constructor specifying the destination web socket location
+ *
+ * @param webSocketURL
+ * URL for web socket communications. e.g
+ * "ws://myhost.com/mypath". Subsequent web socket frames will be
+ * sent to this URL.
+ * @param subProtocols
+ * CSV of supported protocols. Null if sub protocols not
+ * supported.
+ */
+ public WebSocketServerHandshaker(String webSocketURL, String subProtocols) {
+ this.webSocketURL = webSocketURL;
+ this.subProtocols = subProtocols;
+
+ if (this.subProtocols != null) {
+ this.subProtocolsArray = subProtocols.split(",");
+ for (int i = 0; i < this.subProtocolsArray.length; i++) {
+ this.subProtocolsArray[i] = this.subProtocolsArray[i].trim();
+ }
+ }
+ return;
+ }
+
+ /**
+ * Returns the URL of the web socket
+ */
+ public String getWebSocketURL() {
+ return webSocketURL;
+ }
+
+ public void setWebSocketURL(String webSocketURL) {
+ this.webSocketURL = webSocketURL;
+ }
+
+ /**
+ * Returns the CSV of supported sub protocols
+ */
+ public String getSubProtocols() {
+ return subProtocols;
+ }
+
+ public void setSubProtocols(String subProtocols) {
+ this.subProtocols = subProtocols;
+ }
+
+ /**
+ * Returns the version of the specification being supported
+ */
+ public WebSocketSpecificationVersion getVersion() {
+ return version;
+ }
+
+ public void setVersion(WebSocketSpecificationVersion version) {
+ this.version = version;
+ }
+
+ /**
+ * Performs the opening handshake
+ *
+ * @param ctx
+ * Context
+ * @param req
+ * HTTP Request
+ * @throws NoSuchAlgorithmException
+ */
+ public abstract void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req);
+
+ /**
+ * Performs the closing handshake
+ *
+ * @param ctx
+ * Context
+ * @param frame
+ * Closing Frame that was received
+ */
+ public abstract void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame);
+
+ /**
+ * Performs an MD5 hash
+ *
+ * @param bytes
+ * Data to hash
+ * @return Hashed data
+ */
+ protected byte[] md5(byte[] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ return md.digest(bytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new InternalError("MD5 not supported on this platform");
+ }
+ }
+
+ /**
+ * SHA-1 hashing. Instance this we think it is not thread safe
+ *
+ * @param bytes
+ * byte to hash
+ * @return hashed
+ */
+ protected byte[] sha1(byte[] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+ return md.digest(bytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new InternalError("SHA-1 not supported on this platform");
+ }
+ }
+
+ /**
+ * Base 64 encoding
+ *
+ * @param bytes
+ * Bytes to encode
+ * @return encoded string
+ */
+ protected String base64Encode(byte[] bytes) {
+ ChannelBuffer hashed = ChannelBuffers.wrappedBuffer(bytes);
+ return Base64.encode(hashed).toString(CharsetUtil.UTF_8);
+ }
+
+ /**
+ * Selects the first matching supported sub protocol
+ *
+ * @param requestedSubProtocol
+ * CSV of protocols to be supported. e.g. "chat, superchat"
+ * @return First matching supported sub protocol. Null if not found.
+ */
+ protected String selectSubProtocol(String requestedSubProtocol) {
+ if (requestedSubProtocol == null || this.subProtocolsArray == null) {
+ return null;
+ }
+
+ String[] requesteSubProtocolsArray = requestedSubProtocol.split(",");
+ for (int i = 0; i < requesteSubProtocolsArray.length; i++) {
+ String requesteSubProtocol = requesteSubProtocolsArray[i].trim();
+
+ for (String supportedSubProtocol : this.subProtocolsArray) {
+ if (requesteSubProtocol.equals(supportedSubProtocol)) {
+ return requesteSubProtocol;
+ }
+ }
+ }
+
+ // No match found
+ return null;
+ }
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java
new file mode 100644
index 0000000000..cfbbcd3f0a
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java
@@ -0,0 +1,205 @@
+/*
+ * 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 static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY1;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY2;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_LOCATION;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_ORIGIN;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_LOCATION;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_ORIGIN;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_PROTOCOL;
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET;
+import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import java.security.NoSuchAlgorithmException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
+import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
+import org.jboss.netty.logging.InternalLogger;
+import org.jboss.netty.logging.InternalLoggerFactory;
+
+/**
+ *
+ * GET /demo HTTP/1.1
+ * Upgrade: WebSocket
+ * Connection: Upgrade
+ * Host: example.com
+ * Origin: http://example.com
+ * Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+ * Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+ *
+ * ^n:ds[4U
+ *
+ *
+ *
+ * HTTP/1.1 101 WebSocket Protocol Handshake
+ * Upgrade: WebSocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Origin: http://example.com
+ * Sec-WebSocket-Location: ws://example.com/demo
+ * Sec-WebSocket-Protocol: sample
+ *
+ * 8jKS'y:G*Co,Wxa-
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param req
+ * HTTP request
+ * @throws NoSuchAlgorithmException
+ */
+ @Override
+ public void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req) {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Channel %s web socket spec version 00 handshake", ctx.getChannel().getId()));
+ }
+ this.setVersion(WebSocketSpecificationVersion.V00);
+
+ // Serve the WebSocket handshake request.
+ if (!Values.UPGRADE.equalsIgnoreCase(req.getHeader(CONNECTION))
+ || !WEBSOCKET.equalsIgnoreCase(req.getHeader(Names.UPGRADE))) {
+ return;
+ }
+
+ // Hixie 75 does not contain these headers while Hixie 76 does
+ boolean isHixie76 = req.containsHeader(SEC_WEBSOCKET_KEY1) && req.containsHeader(SEC_WEBSOCKET_KEY2);
+
+ // Create the WebSocket handshake response.
+ HttpResponse res = new DefaultHttpResponse(HTTP_1_1, new HttpResponseStatus(101,
+ isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake"));
+ res.addHeader(Names.UPGRADE, WEBSOCKET);
+ res.addHeader(CONNECTION, Values.UPGRADE);
+
+ // Fill in the headers and contents depending on handshake method.
+ if (isHixie76) {
+ // New handshake method with a challenge:
+ res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
+ res.addHeader(SEC_WEBSOCKET_LOCATION, this.getWebSocketURL());
+ String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
+ if (protocol != null) {
+ res.addHeader(SEC_WEBSOCKET_PROTOCOL, selectSubProtocol(protocol));
+ }
+
+ // Calculate the answer of the challenge.
+ String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
+ String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
+ int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
+ int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
+ long c = req.getContent().readLong();
+ ChannelBuffer input = ChannelBuffers.buffer(16);
+ input.writeInt(a);
+ input.writeInt(b);
+ input.writeLong(c);
+ ChannelBuffer output = ChannelBuffers.wrappedBuffer(this.md5(input.array()));
+ res.setContent(output);
+ } else {
+ // Old Hixie 75 handshake method with no challenge:
+ res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
+ res.addHeader(WEBSOCKET_LOCATION, this.getWebSocketURL());
+ String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
+ if (protocol != null) {
+ res.addHeader(WEBSOCKET_PROTOCOL, selectSubProtocol(protocol));
+ }
+ }
+
+ // Upgrade the connection and send the handshake response.
+ ChannelPipeline p = ctx.getChannel().getPipeline();
+ p.remove("aggregator");
+ p.replace("decoder", "wsdecoder", new WebSocket00FrameDecoder());
+
+ ctx.getChannel().write(res);
+
+ p.replace("encoder", "wsencoder", new WebSocket00FrameEncoder());
+ return;
+ }
+
+ /**
+ * Echo back the closing frame
+ *
+ * @param ctx
+ * Channel context
+ * @param frame
+ * Web Socket frame that was received
+ */
+ @Override
+ public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
+ ctx.getChannel().write(frame);
+ }
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java
new file mode 100644
index 0000000000..84467ab9e5
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java
@@ -0,0 +1,167 @@
+/*
+ * 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 static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET;
+import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import java.security.NoSuchAlgorithmException;
+
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
+import org.jboss.netty.logging.InternalLogger;
+import org.jboss.netty.logging.InternalLoggerFactory;
+import org.jboss.netty.util.CharsetUtil;
+
+/**
+ *
+ * GET /chat HTTP/1.1
+ * Host: server.example.com
+ * Upgrade: websocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
+ * Sec-WebSocket-Origin: http://example.com
+ * Sec-WebSocket-Protocol: chat, superchat
+ * Sec-WebSocket-Version: 8
+ *
+ *
+ *
+ * HTTP/1.1 101 Switching Protocols
+ * Upgrade: websocket
+ * Connection: Upgrade
+ * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+ * Sec-WebSocket-Protocol: chat
+ *
+ *
+ * @param ctx
+ * Channel context
+ * @param req
+ * HTTP request
+ * @throws NoSuchAlgorithmException
+ */
+ @Override
+ public void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req) {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Channel %s web socket spec version 10 handshake", ctx.getChannel().getId()));
+ }
+
+ HttpResponse res = new DefaultHttpResponse(HTTP_1_1, new HttpResponseStatus(101, "Switching Protocols"));
+ this.setVersion(WebSocketSpecificationVersion.V10);
+
+ String key = req.getHeader(Names.SEC_WEBSOCKET_KEY);
+ if (key == null) {
+ res.setStatus(HttpResponseStatus.BAD_REQUEST);
+ return;
+ }
+ String acceptSeed = key + WEBSOCKET_08_ACCEPT_GUID;
+ byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
+ String accept = base64Encode(sha1);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("HyBi10 Server Handshake key: %s. Response: %s.", key, accept));
+ }
+
+ res.setStatus(new HttpResponseStatus(101, "Switching Protocols"));
+ res.addHeader(Names.UPGRADE, WEBSOCKET.toLowerCase());
+ res.addHeader(Names.CONNECTION, Names.UPGRADE);
+ res.addHeader(Names.SEC_WEBSOCKET_ACCEPT, accept);
+ String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
+ if (protocol != null) {
+ res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, this.selectSubProtocol(protocol));
+ }
+
+ ctx.getChannel().write(res);
+
+ // Upgrade the connection and send the handshake response.
+ ChannelPipeline p = ctx.getChannel().getPipeline();
+ p.remove("aggregator");
+ p.replace("decoder", "wsdecoder", new WebSocket08FrameDecoder(true, this.allowExtensions));
+ p.replace("encoder", "wsencoder", new WebSocket08FrameEncoder(false));
+
+ return;
+ }
+
+ /**
+ * Echo back the closing frame
+ *
+ * @param ctx
+ * Channel context
+ * @param frame
+ * Web Socket frame that was received
+ */
+ @Override
+ public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
+ ctx.getChannel().write(frame);
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java
new file mode 100644
index 0000000000..ed5ff2ae52
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java
@@ -0,0 +1,108 @@
+/*
+ * 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.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import org.jboss.netty.handler.codec.http.HttpVersion;
+import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
+
+/**
+ * Instances the appropriate handshake class to use for clients
+ *
+ * @author The Netty Project
+ */
+public class WebSocketServerHandshakerFactory {
+
+ private String webSocketURL;
+
+ private String subProtocols;
+
+ private boolean allowExtensions = false;
+
+ /**
+ * Constructor specifying the destination web socket location
+ *
+ * @param webSocketURL
+ * URL for web socket communications. e.g
+ * "ws://myhost.com/mypath". Subsequent web socket frames will be
+ * sent to this URL.
+ * @param subProtocols
+ * CSV of supported protocols. Null if sub protocols not
+ * supported.
+ * @param allowExtensions
+ * Allow extensions to be used in the reserved bits of the web
+ * socket frame
+ */
+ public WebSocketServerHandshakerFactory(String webSocketURL, String subProtocols, boolean allowExtensions) {
+ this.webSocketURL = webSocketURL;
+ this.subProtocols = subProtocols;
+ this.allowExtensions = allowExtensions;
+ return;
+ }
+
+ /**
+ * Instances a new handshaker
+ *
+ * @param webSocketURL
+ * URL for web socket communications. e.g
+ * "ws://myhost.com/mypath". Subsequent web socket frames will be
+ * sent to this URL.
+ * @param version
+ * Version of web socket specification to use to connect to the
+ * server
+ * @param subProtocol
+ * Sub protocol request sent to the server. Null if no
+ * sub-protocol support is required.
+ * @return A new WebSocketServerHandshaker for the requested web socket
+ * version. Null if web socket version is not supported.
+ */
+ public WebSocketServerHandshaker newHandshaker(ChannelHandlerContext ctx, HttpRequest req) {
+
+ String version = req.getHeader(Names.SEC_WEBSOCKET_VERSION);
+ if (version != null) {
+ if (version.equals("8")) {
+ // Version 8 of the wire protocol - assume version 10 of the
+ // specification.
+ return new WebSocketServerHandshaker10(webSocketURL, subProtocols, this.allowExtensions);
+ } else {
+ return null;
+ }
+ } else {
+ // Assume version 00 where version header was not specified
+ return new WebSocketServerHandshaker00(webSocketURL, subProtocols);
+ }
+ }
+
+ /**
+ * Return that we need cannot not support the web socket version
+ *
+ * @param ctx
+ * Context
+ */
+ public void sendUnsupportedWebSocketVersionResponse(ChannelHandlerContext ctx) {
+ HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101,
+ "Switching Protocols"));
+ res.setStatus(HttpResponseStatus.UPGRADE_REQUIRED);
+ res.setHeader(Names.SEC_WEBSOCKET_VERSION, "8");
+ ctx.getChannel().write(res);
+ return;
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java
new file mode 100644
index 0000000000..c83779a7bf
--- /dev/null
+++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * Renegotiation
* Closing the session
*