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..30610be285 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.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.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 + */ +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..b51517760f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.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.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 + */ +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); + } + } + + 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..e1bdd1c959 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java @@ -0,0 +1,42 @@ +/* + * 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 + */ +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..3d010b0da2 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java @@ -0,0 +1,57 @@ +/* + * 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. + * + *

How to run the tests on Ubuntu

+ * + *

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.3 + * + *

08. Go to test suite directory: cd testsuite/websockets + * + *

09. Edit fuzzing_clinet_spec.json and set the version to 10 or 17. + * + * { + * "options": {"failByDrop": false}, + * "servers": [{"agent": "Netty", "url": "ws://localhost:9000", "options": {"version": 17}}], + * "cases": ["*"], + * "exclude-cases": [], + * "exclude-agent-cases": {"FoobarServer*": ["4.*", "1.1.3"]} + * } + * + * + *

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.3-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..ba8ea8681a --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java @@ -0,0 +1,133 @@ +/* + * 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 + */ +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 17 (try changing it to V10 or V00 and it will + // still work ... fingers crossed ;-) + WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"), + WebSocketSpecificationVersion.V17, 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 messagesReceived = new ArrayList(); + + public MyCallbackHandler() { + } + + @Override + public void onConnect(WebSocketClient client) { + System.out.println("WebSocket Client connected!"); + connected = true; + } + + @Override + public void onDisconnect(WebSocketClient client) { + System.out.println("WebSocket Client disconnected!"); + connected = false; + } + + @Override + public void onMessage(WebSocketClient client, WebSocketFrame frame) { + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + System.out.println("WebSocket Client received message:" + textFrame.getText()); + messagesReceived.add(textFrame.getText()); + } else if (frame instanceof PongWebSocketFrame) { + System.out.println("WebSocket Client received pong"); + } else if (frame instanceof CloseWebSocketFrame) { + System.out.println("WebSocket Client received closing"); + } + } + + @Override + public void onError(Throwable t) { + System.out.println("WebSocket Client error " + t.toString()); + } + + } + +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java new file mode 100644 index 0000000000..26c1d2edd2 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java @@ -0,0 +1,71 @@ +//The MIT License +// +//Copyright (c) 2009 Carl Bystršm +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. + +package org.jboss.netty.example.http.websocketx.client; + +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; + + +/** + * Copied from https://github.com/cgbystrom/netty-tools + * + * Callbacks for the {@link WebSocketClient}. Implement and get notified when events happen. + * + * @author Carl Byström + * @author Vibul Imtarnasan + */ +public interface WebSocketCallback { + + /** + * Called when the client is connected to the server + * + * @param client + * Current client used to connect + */ + void onConnect(WebSocketClient client); + + /** + * Called when the client got disconnected from the server. + * + * @param client + * Current client that was disconnected + */ + void onDisconnect(WebSocketClient client); + + /** + * Called when a message arrives from the server. + * + * @param client + * Current client connected + * @param frame + * Data received from server + */ + void onMessage(WebSocketClient client, WebSocketFrame frame); + + /** + * Called when an unhandled errors occurs. + * + * @param t + * The causing error + */ + void onError(Throwable t); +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClient.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClient.java new file mode 100644 index 0000000000..a992cdc646 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClient.java @@ -0,0 +1,57 @@ +//The MIT License +// +//Copyright (c) 2009 Carl Bystršm +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. +package org.jboss.netty.example.http.websocketx.client; + +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; + +/** + * Copied from https://github.com/cgbystrom/netty-tools + * + * @author Carl Byström + * @author Vibul Imtarnasan + */ +public interface WebSocketClient { + + /** + * Connect to server Host and port is setup by the factory. + * + * @return Connect future. Fires when connected. + */ + ChannelFuture connect(); + + /** + * Disconnect from the server + * + * @return Disconnect future. Fires when disconnected. + */ + ChannelFuture disconnect(); + + /** + * Send data to server + * + * @param frame + * Data for sending + * @return Write future. Will fire when the data is sent. + */ + ChannelFuture send(WebSocketFrame frame); +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientFactory.java new file mode 100644 index 0000000000..253d7dbe2f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientFactory.java @@ -0,0 +1,87 @@ +//The MIT License +// +//Copyright (c) 2009 Carl Bystršm +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. + +package org.jboss.netty.example.http.websocketx.client; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; +import org.jboss.netty.handler.codec.http.HttpRequestEncoder; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketSpecificationVersion; + +import java.net.URI; +import java.util.concurrent.Executors; + +/** + * Copied from https://github.com/cgbystrom/netty-tools + * + * A factory for creating WebSocket clients. The entry point for creating and connecting a client. Can and should be + * used to create multiple instances. + * + * @author Carl Byström + * @author Vibul Imtarnasan + */ +public class WebSocketClientFactory { + + private final NioClientSocketChannelFactory socketChannelFactory = new NioClientSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); + + /** + * Create a new WebSocket client + * + * @param url + * URL to connect to. + * @param version + * Web Socket version to support + * @param callback + * Callback interface to receive events + * @return A WebSocket client. Call {@link WebSocketClient#connect()} to connect. + */ + public WebSocketClient newClient(final URI url, + final WebSocketSpecificationVersion version, + final WebSocketCallback callback) { + ClientBootstrap bootstrap = new ClientBootstrap(socketChannelFactory); + + String protocol = url.getScheme(); + if (!protocol.equals("ws") && !protocol.equals("wss")) { + throw new IllegalArgumentException("Unsupported protocol: " + protocol); + } + + final WebSocketClientHandler clientHandler = new WebSocketClientHandler(bootstrap, url, version, callback); + + bootstrap.setPipelineFactory(new ChannelPipelineFactory() { + + @Override + public ChannelPipeline getPipeline() throws Exception { + ChannelPipeline pipeline = Channels.pipeline(); + pipeline.addLast("decoder", new WebSocketHttpResponseDecoder()); + pipeline.addLast("encoder", new HttpRequestEncoder()); + pipeline.addLast("ws-handler", clientHandler); + return pipeline; + } + }); + + return clientHandler; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientHandler.java new file mode 100644 index 0000000000..58725d3ece --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientHandler.java @@ -0,0 +1,127 @@ +//The MIT License +// +//Copyright (c) 2009 Carl Bystršm +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. + +package org.jboss.netty.example.http.websocketx.client; + +import java.net.InetSocketAddress; +import java.net.URI; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketSpecificationVersion; +import org.jboss.netty.util.CharsetUtil; + +/** + * Copied from https://github.com/cgbystrom/netty-tools + * + * Handles socket communication for a connected WebSocket client Not intended for end-users. Please use + * {@link WebSocketClient} or {@link WebSocketCallback} for controlling your client. + * + * @author Carl Byström + * @author Vibul Imtarnasan + */ +public class WebSocketClientHandler extends SimpleChannelUpstreamHandler implements WebSocketClient { + + private final ClientBootstrap bootstrap; + private URI url; + private final WebSocketCallback callback; + private Channel channel; + private WebSocketClientHandshaker handshaker = null; + private final WebSocketSpecificationVersion version; + + public WebSocketClientHandler(ClientBootstrap bootstrap, URI url, WebSocketSpecificationVersion version, WebSocketCallback callback) { + this.bootstrap = bootstrap; + this.url = url; + this.version = version; + this.callback = callback; + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + channel = e.getChannel(); + this.handshaker = new WebSocketClientHandshakerFactory().newHandshaker(url, version, null, false); + handshaker.beginOpeningHandshake(ctx, channel); + } + + @Override + public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + callback.onDisconnect(this); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + if (!handshaker.isOpeningHandshakeCompleted()) { + handshaker.endOpeningHandshake(ctx, (HttpResponse) e.getMessage()); + callback.onConnect(this); + return; + } + + if (e.getMessage() instanceof HttpResponse) { + HttpResponse response = (HttpResponse) e.getMessage(); + throw new WebSocketException("Unexpected HttpResponse (status=" + response.getStatus() + ", content=" + + response.getContent().toString(CharsetUtil.UTF_8) + ")"); + } + + WebSocketFrame frame = (WebSocketFrame) e.getMessage(); + callback.onMessage(this, frame); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + final Throwable t = e.getCause(); + callback.onError(t); + e.getChannel().close(); + } + + @Override + public ChannelFuture connect() { + return bootstrap.connect(new InetSocketAddress(url.getHost(), url.getPort())); + } + + @Override + public ChannelFuture disconnect() { + return channel.close(); + } + + @Override + public ChannelFuture send(WebSocketFrame frame) { + return channel.write(frame); + } + + public URI getUrl() { + return url; + } + + public void setUrl(URI url) { + this.url = url; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketException.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketException.java new file mode 100644 index 0000000000..aa0c439ec0 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketException.java @@ -0,0 +1,47 @@ +//The MIT License +// +//Copyright (c) 2009 Carl Bystršm +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. +package org.jboss.netty.example.http.websocketx.client; + +import java.io.IOException; + +/** + * Copied from https://github.com/cgbystrom/netty-tools + * + * A WebSocket related exception + * + * @author Carl Byström + */ +public class WebSocketException extends IOException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public WebSocketException(String s) { + super(s); + } + + public WebSocketException(String s, Throwable throwable) { + super(s, throwable); + } +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketHttpResponseDecoder.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketHttpResponseDecoder.java new file mode 100644 index 0000000000..ec9bb20fb8 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketHttpResponseDecoder.java @@ -0,0 +1,53 @@ +/* + * 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 org.jboss.netty.handler.codec.http.HttpMessage; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseDecoder; + +/** + * Fix bug in standard HttpResponseDecoder for web socket clients. When status 101 is received for Hybi00, there are 16 + * bytes of contents expected + * + * @author Vibul Imtarnasan + */ +public class WebSocketHttpResponseDecoder extends HttpResponseDecoder { + + @Override + protected boolean isContentAlwaysEmpty(HttpMessage msg) { + if (msg instanceof HttpResponse) { + HttpResponse res = (HttpResponse) msg; + int code = res.getStatus().getCode(); + + // FIX force reading of protocol upgrade challenge data into the content buffer + if (code == 101) { + return false; + } + + if (code < 200) { + return true; + } + switch (code) { + case 204: + case 205: + case 304: + return true; + } + } + return false; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/package-info.java new file mode 100644 index 0000000000..05da751a10 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 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..c32c710e04 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java @@ -0,0 +1,72 @@ +/* + * 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: + * + *

+ * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + */ +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..98aff22afb --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java @@ -0,0 +1,150 @@ +/* + * 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 + */ +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); + } + } + + 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..2b0db7b3c3 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java @@ -0,0 +1,71 @@ +/* + * 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 + */ +public class WebSocketServerIndexPage { + + private static final String NEWLINE = "\r\n"; + + public static ChannelBuffer getContent(String webSocketLocation) { + return ChannelBuffers.copiedBuffer( + "Web Socket Test" + NEWLINE + + "" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + "" + + "" + NEWLINE + + "

Output

" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + "" + NEWLINE + + "" + NEWLINE, + CharsetUtil.US_ASCII); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerPipelineFactory.java new file mode 100644 index 0000000000..ae9d62af6b --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerPipelineFactory.java @@ -0,0 +1,42 @@ +/* + * 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.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 + */ +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/server/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/package-info.java new file mode 100644 index 0000000000..ade9c22eb1 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/package-info.java @@ -0,0 +1,27 @@ +/* + * 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 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/http/websocketx/sslserver/WebSocketSslServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServer.java new file mode 100644 index 0000000000..a6642c4754 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServer.java @@ -0,0 +1,84 @@ +/* + * 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.sslserver; + +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: + * + * https://localhost:8081/websocket + * + * Open your browser at https://localhost:8081/, 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: + * + *

+ * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + */ +public class WebSocketSslServer { + public static void main(String[] args) { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + String keyStoreFilePath = System.getProperty("keystore.file.path"); + if (keyStoreFilePath == null || keyStoreFilePath.isEmpty()) { + System.out.println("ERROR: System property keystore.file.path not set. Exiting now!"); + System.exit(1); + } + + String keyStoreFilePassword = System.getProperty("keystore.file.password"); + if (keyStoreFilePassword == null || keyStoreFilePassword.isEmpty()) { + System.out.println("ERROR: System property keystore.file.password not set. Exiting now!"); + System.exit(1); + } + + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketSslServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(8081)); + + System.out.println("Web Socket Server started on 8081. Open your browser and navigate to https://localhost:8081/"); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerHandler.java new file mode 100644 index 0000000000..ac17f32a0e --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerHandler.java @@ -0,0 +1,150 @@ +/* + * 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.sslserver; + +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 + */ +public class WebSocketSslServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketSslServerHandler.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 = WebSocketSslServerIndexPage.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); + } + } + + 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 "wss://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerIndexPage.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerIndexPage.java new file mode 100644 index 0000000000..bfc4099099 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerIndexPage.java @@ -0,0 +1,71 @@ +/* + * 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.sslserver; + +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 + */ +public class WebSocketSslServerIndexPage { + + private static final String NEWLINE = "\r\n"; + + public static ChannelBuffer getContent(String webSocketLocation) { + return ChannelBuffers.copiedBuffer( + "Web Socket Test" + NEWLINE + + "" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + "" + + "" + NEWLINE + + "

Output

" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + "" + NEWLINE + + "" + NEWLINE, + CharsetUtil.US_ASCII); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerPipelineFactory.java new file mode 100644 index 0000000000..00f89ad0ab --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerPipelineFactory.java @@ -0,0 +1,50 @@ +/* + * 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.sslserver; + +import static org.jboss.netty.channel.Channels.*; + +import javax.net.ssl.SSLEngine; + +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; +import org.jboss.netty.handler.ssl.SslHandler; + +/** + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + */ +public class WebSocketSslServerPipelineFactory implements ChannelPipelineFactory { + @Override + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + + SSLEngine engine = WebSocketSslServerSslContext.getInstance().getServerContext().createSSLEngine(); + engine.setUseClientMode(false); + pipeline.addLast("ssl", new SslHandler(engine)); + + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(65536)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", new WebSocketSslServerHandler()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerSslContext.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerSslContext.java new file mode 100644 index 0000000000..e7f8588ce8 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/WebSocketSslServerSslContext.java @@ -0,0 +1,107 @@ +/* + * 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. + */ +package org.jboss.netty.example.http.websocketx.sslserver; + +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.Security; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Creates a {@link SSLContext} for just server certificates. + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + */ +public class WebSocketSslServerSslContext { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketSslServerSslContext.class); + private static final String PROTOCOL = "TLS"; + private SSLContext _serverContext; + + /** + * Returns the singleton instance for this class + */ + public static WebSocketSslServerSslContext getInstance() { + return SingletonHolder.INSTANCE; + } + + /** + * SingletonHolder is loaded on the first execution of + * Singleton.getInstance() or the first access to SingletonHolder.INSTANCE, + * not before. + * + * See http://en.wikipedia.org/wiki/Singleton_pattern + */ + private static class SingletonHolder { + + public static final WebSocketSslServerSslContext INSTANCE = new WebSocketSslServerSslContext(); + } + + /** + * Constructor for singleton + */ + private WebSocketSslServerSslContext() { + try { + // Key store (Server side certificate) + String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); + if (algorithm == null) { + algorithm = "SunX509"; + } + + SSLContext serverContext = null; + try { + String keyStoreFilePath = System.getProperty("keystore.file.path"); + String keyStoreFilePassword = System.getProperty("keystore.file.password"); + + KeyStore ks = KeyStore.getInstance("JKS"); + FileInputStream fin = new FileInputStream(keyStoreFilePath); + ks.load(fin, keyStoreFilePassword.toCharArray()); + + // Set up key manager factory to use our key store + // Assume key password is the same as the key store file + // password + KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + kmf.init(ks, keyStoreFilePassword.toCharArray()); + + // Initialise the SSLContext to work with our key managers. + serverContext = SSLContext.getInstance(PROTOCOL); + serverContext.init(kmf.getKeyManagers(), null, null); + } catch (Exception e) { + throw new Error("Failed to initialize the server-side SSLContext", e); + } + _serverContext = serverContext; + } catch (Exception ex) { + logger.error("Error initializing SslContextManager. " + ex.getMessage(), ex); + System.exit(1); + + } + } + + /** + * Returns the server context with server side key store + */ + public SSLContext getServerContext() { + return _serverContext; + } + +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/package-info.java new file mode 100644 index 0000000000..3e91cb30a8 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/sslserver/package-info.java @@ -0,0 +1,39 @@ +/* + * 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 contains an example web socket web server with server SSL. + *

To run this example, follow the steps below: + *

+ *
Step 1. Generate Your Key
+ *
+ * keytool -genkey -keystore mySrvKeystore -keyalg RSA. + * Make sure that you set the key password to be the same the key file password. + *
+ *
Step 2. Specify your key store file and password as system properties
+ *
+ * -Dkeystore.file.path=<path to mySrvKeystore> -Dkeystore.file.password=<password> + *
+ *
Step 3. Run WebSocketSslServer as a Java application
+ *
+ * Once started, you can test the web server against your browser by navigating to https://localhost:8081/ + *
+ *
+ *

To find out more about setting up key stores, refer to this + * giude. + */ +package org.jboss.netty.example.http.websocketx.sslserver; + diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java index bfcc0f9fd9..f80b0529b2 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java @@ -233,6 +233,18 @@ public class HttpHeaders { * {@code "Sec-WebSocket-Protocol"} */ public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + /** + * {@code "Sec-WebSocket-Version"} + */ + public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + /** + * {@code "Sec-WebSocket-Key"} + */ + public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + /** + * {@code "Sec-WebSocket-Accept"} + */ + public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; /** * {@code "Server"} */ diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java new file mode 100644 index 0000000000..fd52192ba5 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java @@ -0,0 +1,68 @@ +/* + * 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; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class BinaryWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty binary frame. + */ + public BinaryWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new binary frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new binary frame with the specified binary data and the final + * fragment flag. + * + * @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 BinaryWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java new file mode 100644 index 0000000000..732893c715 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java @@ -0,0 +1,51 @@ +/* + * 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.ChannelBuffers; + +/** + * Web Socket Frame for closing the connection + * + * @author Vibul Imtarnasan + */ +public class CloseWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty close frame. + */ + public CloseWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new close frame + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + */ + public CloseWebSocketFrame(boolean finalFragment, int rsv) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} 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..fd613fb97c --- /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/PingWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java new file mode 100644 index 0000000000..48c9ea4f9c --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java @@ -0,0 +1,67 @@ +/* + * 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; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PingWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty ping frame. + */ + public PingWebSocketFrame() { + this.setFinalFragment(true); + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new ping frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new ping 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 PingWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java new file mode 100644 index 0000000000..123000964f --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java @@ -0,0 +1,66 @@ +/* + * 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; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PongWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty pong frame. + */ + public PongWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new pong frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new pong 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 PongWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java new file mode 100644 index 0000000000..97e0d45ebc --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java @@ -0,0 +1,128 @@ +/* + * 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 text frame with assumed UTF-8 encoding + * + * @author Vibul Imtarnasan + * + */ +public class TextWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty text frame. + */ + public TextWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(String text) { + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @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. Must be UTF-8 encoded + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * 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) { + throw new NullPointerException("text"); + } + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(text: " + getText() + ')'; + } +} 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..fbffd9c7eb --- /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 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Invalid UTF8 bytes encountered + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Exception extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UTF8Exception(String reason) { + super(reason); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java new file mode 100644 index 0000000000..70e2bec308 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java @@ -0,0 +1,77 @@ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Checks UTF8 bytes for validity before converting it into a string + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Output { + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + + private static final byte[] TYPES = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 }; + + private static final byte[] STATES = { 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, + 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 }; + + private int state = UTF8_ACCEPT; + private int codep = 0; + + private final StringBuilder stringBuilder; + + public UTF8Output(byte[] bytes) { + stringBuilder = new StringBuilder(bytes.length); + write(bytes); + } + + public void write(byte[] bytes) { + for (byte b : bytes) { + write(b); + } + } + + public void write(int b) { + byte type = TYPES[b & 0xFF]; + + codep = (state != UTF8_ACCEPT) ? (b & 0x3f) | (codep << 6) : (0xff >> type) & (b); + + state = STATES[state + type]; + + if (state == UTF8_ACCEPT) { + stringBuilder.append((char) codep); + } else if (state == UTF8_REJECT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + } + + @Override + public String toString() { + if (state != UTF8_ACCEPT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java new file mode 100644 index 0000000000..8e3e215486 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java @@ -0,0 +1,138 @@ +/* + * 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.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.replay.ReplayingDecoder; +import org.jboss.netty.handler.codec.replay.VoidEnum; + +/** + * Decodes {@link ChannelBuffer}s into {@link WebSocketFrame}s. + *

+ * 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 + * + * @apiviz.landmark + * @apiviz.uses org.jboss.netty.handler.codec.http.websocket.WebSocketFrame + */ +public class WebSocket00FrameDecoder extends ReplayingDecoder { + + public static final int DEFAULT_MAX_FRAME_SIZE = 16384; + + private final int maxFrameSize; + private boolean receivedClosingHandshake; + + public WebSocket00FrameDecoder() { + this(DEFAULT_MAX_FRAME_SIZE); + } + + /** + * Creates a new instance of {@code WebSocketFrameDecoder} with the + * specified {@code maxFrameSize}. If the client sends a frame size larger + * than {@code maxFrameSize}, the channel will be closed. + * + * @param maxFrameSize + * the maximum frame size to decode + */ + public WebSocket00FrameDecoder(int maxFrameSize) { + this.maxFrameSize = maxFrameSize; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, VoidEnum state) throws Exception { + + // Discard all data received if closing handshake was received before. + if (receivedClosingHandshake) { + buffer.skipBytes(actualReadableBytes()); + return null; + } + + // Decode a frame otherwise. + byte type = buffer.readByte(); + if ((type & 0x80) == 0x80) { + // If the MSB on type is set, decode the frame length + return decodeBinaryFrame(type, buffer); + } else { + // Decode a 0xff terminated UTF-8 string + return decodeTextFrame(type, buffer); + } + } + + private WebSocketFrame decodeBinaryFrame(byte type, ChannelBuffer buffer) throws TooLongFrameException { + long frameSize = 0; + int lengthFieldSize = 0; + byte b; + do { + b = buffer.readByte(); + frameSize <<= 7; + frameSize |= b & 0x7f; + if (frameSize > maxFrameSize) { + throw new TooLongFrameException(); + } + lengthFieldSize++; + if (lengthFieldSize > 8) { + // Perhaps a malicious peer? + throw new TooLongFrameException(); + } + } while ((b & 0x80) == 0x80); + + if (type == ((byte) 0xFF) && frameSize == 0) { + receivedClosingHandshake = true; + return new CloseWebSocketFrame(); + } + + return new BinaryWebSocketFrame(buffer.readBytes((int) frameSize)); + } + + private WebSocketFrame decodeTextFrame(byte type, ChannelBuffer buffer) throws TooLongFrameException { + int ridx = buffer.readerIndex(); + int rbytes = actualReadableBytes(); + int delimPos = buffer.indexOf(ridx, ridx + rbytes, (byte) 0xFF); + if (delimPos == -1) { + // Frame delimiter (0xFF) not found + if (rbytes > maxFrameSize) { + // Frame length exceeded the maximum + throw new TooLongFrameException(); + } else { + // Wait until more data is received + return null; + } + } + + int frameSize = delimPos - ridx; + if (frameSize > maxFrameSize) { + throw new TooLongFrameException(); + } + + ChannelBuffer binaryData = buffer.readBytes(frameSize); + buffer.skipBytes(1); + + int ffDelimPos = binaryData.indexOf(binaryData.readerIndex(), binaryData.writerIndex(), (byte) 0xFF); + if (ffDelimPos >= 0) { + throw new IllegalArgumentException("a text frame should not contain 0xFF."); + } + + return new TextWebSocketFrame(binaryData); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java new file mode 100644 index 0000000000..8735911d48 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java @@ -0,0 +1,101 @@ +/* + * 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.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelHandler.Sharable; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; + +/** + * Encodes a {@link WebSocketFrame} into a {@link ChannelBuffer}. + *

+ * 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 + * + * @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..76d0988b8d --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -0,0 +1,383 @@ +// (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 { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameDecoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private UTF8Output fragmentedFramesText = null; + private int fragmentedFramesCount = 0; + + private boolean frameFinalFlag; + private int frameRsv; + private int frameOpcode; + private long framePayloadLength; + private ChannelBuffer framePayload = null; + private int framePayloadBytesRead = 0; + private ChannelBuffer maskingKey; + + private boolean allowExtensions = false; + private boolean maskedPayload = false; + private boolean receivedClosingHandshake = false; + + public enum State { + FRAME_START, MASKING_KEY, PAYLOAD, CORRUPT + } + + /** + * Constructor + * + * @param maskedPayload + * Web socket servers must set this to true processed incoming + * masked payload. Client implementations must set this to false. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + */ + public WebSocket08FrameDecoder(boolean maskedPayload, boolean allowExtensions) { + super(State.FRAME_START); + this.maskedPayload = maskedPayload; + this.allowExtensions = allowExtensions; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) + throws Exception { + + // Discard all data received if closing handshake was received before. + if (receivedClosingHandshake) { + buffer.skipBytes(actualReadableBytes()); + return null; + } + + switch (state) { + case FRAME_START: + framePayloadBytesRead = 0; + framePayloadLength = -1; + framePayload = null; + + // FIN, RSV, OPCODE + byte b = buffer.readByte(); + frameFinalFlag = (b & 0x80) != 0; + frameRsv = (b & 0x70) >> 4; + frameOpcode = (b & 0x0F); + + if (logger.isDebugEnabled()) { + logger.debug("Decoding WebSocket Frame opCode=" + frameOpcode); + } + + // MASK, PAYLOAD LEN 1 + b = buffer.readByte(); + boolean frameMasked = (b & 0x80) != 0; + int framePayloadLen1 = (b & 0x7F); + + if (frameRsv != 0 && !this.allowExtensions) { + protocolViolation(channel, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); + return null; + } + + if (this.maskedPayload && !frameMasked) { + protocolViolation(channel, "unmasked client to server frame"); + return null; + } + if (frameOpcode > 7) { // control frame (have MSB in opcode set) + + // control frames MUST NOT be fragmented + if (!frameFinalFlag) { + protocolViolation(channel, "fragmented control frame"); + return null; + } + + // control frames MUST have payload 125 octets or less + if (framePayloadLen1 > 125) { + protocolViolation(channel, "control frame with payload length > 125 octets"); + return null; + } + + // check for reserved control frame opcodes + if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { + protocolViolation(channel, "control frame using reserved opcode " + frameOpcode); + return null; + } + + // close frame : if there is a body, the first two bytes of the + // body MUST be a 2-byte unsigned integer (in network byte + // order) representing a status code + if (frameOpcode == 8 && framePayloadLen1 == 1) { + protocolViolation(channel, "received close control frame with payload len 1"); + return null; + } + } else { // data frame + // check for reserved data frame opcodes + if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { + protocolViolation(channel, "data frame using reserved opcode " + frameOpcode); + return null; + } + + // check opcode vs message fragmentation state 1/2 + if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { + protocolViolation(channel, "received continuation data frame outside fragmented message"); + return null; + } + + // check opcode vs message fragmentation state 2/2 + if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { + protocolViolation(channel, "received non-continuation data frame while inside fragmented message"); + return null; + } + } + + // Read frame payload length + if (framePayloadLen1 == 126) { + framePayloadLength = buffer.readUnsignedShort(); + if (framePayloadLength < 126) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else if (framePayloadLen1 == 127) { + framePayloadLength = buffer.readLong(); + // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe + // just check if it's negative? + + if (framePayloadLength < 65536) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else { + framePayloadLength = framePayloadLen1; + } + + if (logger.isDebugEnabled()) { + logger.debug("Decoding WebSocket Frame length=" + framePayloadLength); + } + + checkpoint(State.MASKING_KEY); + case MASKING_KEY: + if (this.maskedPayload) { + maskingKey = buffer.readBytes(4); + } + checkpoint(State.PAYLOAD); + case PAYLOAD: + // Sometimes, the payload may not be delivered in 1 nice packet + // We need to accumulate the data until we have it all + int rbytes = actualReadableBytes(); + ChannelBuffer payloadBuffer = null; + + int willHaveReadByteCount = framePayloadBytesRead + rbytes; + // logger.debug("Frame rbytes=" + rbytes + " willHaveReadByteCount=" + // + willHaveReadByteCount + " framePayloadLength=" + + // framePayloadLength); + if (willHaveReadByteCount == framePayloadLength) { + // We have all our content so proceed to process + payloadBuffer = buffer.readBytes(rbytes); + } else if (willHaveReadByteCount < framePayloadLength) { + // We don't have all our content so accumulate payload. + // Returning null means we will get called back + payloadBuffer = buffer.readBytes(rbytes); + if (framePayload == null) { + framePayload = channel.getConfig().getBufferFactory().getBuffer(toFrameLength(framePayloadLength)); + } + framePayload.writeBytes(payloadBuffer); + framePayloadBytesRead = framePayloadBytesRead + rbytes; + + // Return null to wait for more bytes to arrive + return null; + } else if (willHaveReadByteCount > framePayloadLength) { + // We have more than what we need so read up to the end of frame + // Leave the remainder in the buffer for next frame + payloadBuffer = buffer.readBytes(toFrameLength(framePayloadLength - framePayloadBytesRead)); + } + + // Now we have all the data, the next checkpoint must be the next + // frame + checkpoint(State.FRAME_START); + + // Take the data that we have in this packet + if (framePayload == null) { + framePayload = payloadBuffer; + } else { + framePayload.writeBytes(payloadBuffer); + } + + // Unmask data if needed + if (this.maskedPayload) { + unmask(framePayload); + } + + // Processing ping/pong/close frames because they cannot be + // fragmented + if (frameOpcode == OPCODE_PING) { + return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_PONG) { + return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_CLOSE) { + this.receivedClosingHandshake = true; + return new CloseWebSocketFrame(frameFinalFlag, frameRsv); + } + + // Processing for possible fragmented messages for text and binary + // frames + String aggregatedText = null; + if (frameFinalFlag) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (frameOpcode != OPCODE_PING) { + fragmentedFramesCount = 0; + + // Check text for UTF8 correctness + if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) { + // Check UTF-8 correctness for this payload + checkUTF8String(channel, framePayload.array()); + + // This does a second check to make sure UTF-8 + // correctness for entire text message + aggregatedText = fragmentedFramesText.toString(); + + fragmentedFramesText = null; + } + } + } else { + // Not final frame so we can expect more frames in the + // fragmented sequence + if (fragmentedFramesCount == 0) { + // First text or binary frame for a fragmented set + fragmentedFramesText = null; + if (frameOpcode == OPCODE_TEXT) { + checkUTF8String(channel, framePayload.array()); + } + } else { + // Subsequent frames - only check if init frame is text + if (fragmentedFramesText != null) { + checkUTF8String(channel, framePayload.array()); + } + } + + // Increment counter + fragmentedFramesCount++; + } + + // Return the frame + if (frameOpcode == OPCODE_TEXT) { + return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_BINARY) { + return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_CONT) { + return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText); + } else { + throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode); + } + case CORRUPT: + // If we don't keep reading Netty will throw an exception saying + // we can't return null if no bytes read and state not changed. + buffer.readByte(); + return null; + default: + throw new Error("Shouldn't reach here."); + } + } + + private void unmask(ChannelBuffer frame) { + byte[] bytes = frame.array(); + for (int i = 0; i < bytes.length; i++) { + frame.setByte(i, frame.getByte(i) ^ maskingKey.getByte(i % 4)); + } + } + + private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException { + checkpoint(State.CORRUPT); + if (channel.isConnected()) { + channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + channel.close().awaitUninterruptibly(); + } + throw new CorruptedFrameException(reason); + } + + private int toFrameLength(long l) throws TooLongFrameException { + if (l > Integer.MAX_VALUE) { + throw new TooLongFrameException("Length:" + l); + } else { + return (int) l; + } + } + + private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException { + try { + // StringBuilder sb = new StringBuilder("UTF8 " + bytes.length + + // " bytes: "); + // for (byte b : bytes) { + // sb.append(Integer.toHexString(b)).append(" "); + // } + // logger.debug(sb.toString()); + + if (fragmentedFramesText == null) { + fragmentedFramesText = new UTF8Output(bytes); + } else { + fragmentedFramesText.write(bytes); + } + } catch (UTF8Exception ex) { + protocolViolation(channel, "invalid UTF-8 bytes"); + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java new file mode 100644 index 0000000000..9270a2f7c7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java @@ -0,0 +1,176 @@ +// (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 java.nio.ByteBuffer; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + *

+ * Encodes a web socket frame into wire protocol version 8 format. This code was + * forked from webbit and + * modified. + *

+ * + * @author Aslak Hellesøy + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameEncoder extends OneToOneEncoder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameEncoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private boolean maskPayload = false; + + /** + * Constructor + * + * @param maskPayload + * Web socket clients must set this to true to mask payload. + * Server implementations must set this to false. + */ + public WebSocket08FrameEncoder(boolean maskPayload) { + this.maskPayload = maskPayload; + } + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception { + + byte[] mask = null; + + if (msg instanceof WebSocketFrame) { + WebSocketFrame frame = (WebSocketFrame) msg; + ChannelBuffer data = frame.getBinaryData(); + if (data == null) { + data = ChannelBuffers.EMPTY_BUFFER; + } + + byte opcode; + if (frame instanceof TextWebSocketFrame) { + opcode = OPCODE_TEXT; + } else if (frame instanceof PingWebSocketFrame) { + opcode = OPCODE_PING; + } else if (frame instanceof PongWebSocketFrame) { + opcode = OPCODE_PONG; + } else if (frame instanceof CloseWebSocketFrame) { + opcode = OPCODE_CLOSE; + } else if (frame instanceof BinaryWebSocketFrame) { + opcode = OPCODE_BINARY; + } else if (frame instanceof ContinuationWebSocketFrame) { + opcode = OPCODE_CONT; + } else { + throw new UnsupportedOperationException("Cannot encode frame of type: " + frame.getClass().getName()); + } + + int length = data.readableBytes(); + + if (logger.isDebugEnabled()) { + logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length); + } + + int b0 = 0; + if (frame.isFinalFragment()) { + b0 |= (1 << 7); + } + b0 |= (frame.getRsv() % 8) << 4; + b0 |= opcode % 128; + + ChannelBuffer header; + ChannelBuffer body; + + if (opcode == OPCODE_PING && length > 125) { + throw new TooLongFrameException("invalid payload for PING (payload length must be <= 125, was " + length); + } + + int maskLength = this.maskPayload ? 4 : 0; + if (length <= 125) { + header = ChannelBuffers.buffer(2 + maskLength); + header.writeByte(b0); + byte b = (byte) (this.maskPayload ? (0x80 | (byte) length) : (byte) length); + header.writeByte(b); + } else if (length <= 0xFFFF) { + header = ChannelBuffers.buffer(4 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFE) : 126); + header.writeByte((length >>> 8) & 0xFF); + header.writeByte((length) & 0xFF); + } else { + header = ChannelBuffers.buffer(10 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFF) : 127); + header.writeLong(length); + } + + // Write payload + if (this.maskPayload) { + Integer random = (int) (Math.random() * Integer.MAX_VALUE); + mask = ByteBuffer.allocate(4).putInt(random).array(); + header.writeBytes(mask); + + body = ChannelBuffers.buffer(length); + int counter = 0; + while (data.readableBytes() > 0) { + byte byteData = data.readByte(); + body.writeByte(byteData ^ mask[+counter++ % 4]); + } + } else { + body = data; + } + return ChannelBuffers.wrappedBuffer(header, body); + } + + // If not websocket, then just return the message + return msg; + } + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java new file mode 100644 index 0000000000..5cf746952a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java @@ -0,0 +1,61 @@ +// (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; + +/** + * Decodes a web socket frame from wire protocol version 13 format. + * V13 is essentially the same as V8. + * + * @author Vibul Imtarnasan + */ +public class WebSocket13FrameDecoder extends WebSocket08FrameDecoder { + + /** + * Constructor + * + * @param maskedPayload + * Web socket servers must set this to true processed incoming + * masked payload. Client implementations must set this to false. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + */ + public WebSocket13FrameDecoder(boolean maskedPayload, boolean allowExtensions) { + super(maskedPayload, allowExtensions); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameEncoder.java new file mode 100644 index 0000000000..fb63448a77 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket13FrameEncoder.java @@ -0,0 +1,62 @@ +// (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; + + +/** + *

+ * Encodes a web socket frame into wire protocol version 13 format. V13 is essentially the same + * as V8. + *

+ * + * @author Vibul Imtarnasan + */ +public class WebSocket13FrameEncoder extends WebSocket08FrameEncoder { + + /** + * Constructor + * + * @param maskPayload + * Web socket clients must set this to true to mask payload. + * Server implementations must set this to false. + */ + public WebSocket13FrameEncoder(boolean maskPayload) { + super(maskPayload); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java new file mode 100644 index 0000000000..c362dba8d5 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java @@ -0,0 +1,209 @@ +/* + * 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 java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.base64.Base64; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.util.CharsetUtil; + +/** + * Base class for web socket client handshake implementations + * + * @author Vibul Imtarnasan + */ +public abstract class WebSocketClientHandshaker { + + private URI webSocketURL; + + private WebSocketSpecificationVersion version = WebSocketSpecificationVersion.UNKNOWN; + + private boolean openingHandshakeCompleted = false; + + private String subProtocolRequest = null; + + private String subProtocolResponse = null; + + /** + * + * @param webSocketURL + * @param version + * @param subProtocol + */ + public WebSocketClientHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol) { + this.webSocketURL = webSocketURL; + this.version = version; + this.subProtocolRequest = subProtocol; + } + + /** + * Returns the URI to the web socket. e.g. "ws://myhost.com/path" + */ + public URI getWebSocketURL() { + return webSocketURL; + } + + protected void setWebSocketURL(URI webSocketURL) { + this.webSocketURL = webSocketURL; + } + + /** + * Version of the web socket specification that is being used + */ + public WebSocketSpecificationVersion getVersion() { + return version; + } + + protected void setVersion(WebSocketSpecificationVersion version) { + this.version = version; + } + + /** + * Flag to indicate if the opening handshake is complete + */ + public boolean isOpeningHandshakeCompleted() { + return openingHandshakeCompleted; + } + + protected void setOpenningHandshakeCompleted(boolean openningHandshakeCompleted) { + this.openingHandshakeCompleted = openningHandshakeCompleted; + } + + /** + * Returns the sub protocol request sent to the server as specified in the + * constructor + */ + public String getSubProtocolRequest() { + return subProtocolRequest; + } + + protected void setSubProtocolRequest(String subProtocolRequest) { + this.subProtocolRequest = subProtocolRequest; + } + + /** + * Returns the sub protocol response and sent by the server. Only available + * after end of handshake. + */ + public String getSubProtocolResponse() { + return subProtocolResponse; + } + + protected void setSubProtocolResponse(String subProtocolResponse) { + this.subProtocolResponse = subProtocolResponse; + } + + /** + * Performs the opening handshake + * + * @param ctx + * Channel context + * @param channel + * Channel + */ + public abstract void beginOpeningHandshake(ChannelHandlerContext ctx, Channel channel); + + /** + * Performs the closing handshake + * + * @param ctx + * Channel context + * @param response + * HTTP response containing the closing handshake details + */ + public abstract void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response) throws WebSocketHandshakeException; + + /** + * 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"); + } + } + + /** + * Performs an SHA-1 hash + * + * @param bytes + * Data to hash + * @return Hashed data + */ + 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); + } + + /** + * Creates some random bytes + * + * @param size + * Number of random bytes to create + * @return random bytes + */ + protected byte[] createRandomBytes(int size) { + byte[] bytes = new byte[size]; + + for (int i = 0; i < size; i++) { + bytes[i] = (byte) createRandomNumber(0, 255); + } + + return bytes; + } + + /** + * Generates a random number + * + * @param min + * Minimum value + * @param max + * Maximum value + * @return Random number + */ + protected int createRandomNumber(int min, int max) { + return (int) (Math.random() * max + min); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java new file mode 100644 index 0000000000..51f27c4978 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java @@ -0,0 +1,244 @@ +/* + * 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 java.nio.ByteBuffer; +import java.util.Arrays; + +import org.jboss.netty.buffer.ChannelBuffers; +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; + +/** + *

+ * 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. + *

+ * + * @author The Netty Project + * @author Vibul Imtarnasan + */ +public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { + + private byte[] expectedChallengeResponseBytes = null; + + /** + * Constructor specifying the destination web socket location and version to + * initiate + * + * @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. + */ + public WebSocketClientHandshaker00(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol) { + super(webSocketURL, version, subProtocol); + } + + /** + *

+ * Sends the opening request to the server: + *

+ * + *
+     * 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()); + } + + /** + *

+ * Process server response: + *

+ * + *
+     * 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); + } + + 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..b68c97fc7f --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java @@ -0,0 +1,186 @@ +/* + * 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; + +/** + *

+ * Performs client side opening and closing handshakes for web socket + * specification version draft-ietf-hybi-thewebsocketprotocol- 10 + *

+ * + * @author The Netty Project + */ +public class WebSocketClientHandshaker10 extends WebSocketClientHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker10.class); + + public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private String expectedChallengeResponseString = null; + + private static final String protocol = null; + + private boolean allowExtensions = false; + + /** + * Constructor specifying the destination web socket location and version to + * initiate + * + * @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. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketClientHandshaker10(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, boolean allowExtensions) { + super(webSocketURL, version, subProtocol); + this.allowExtensions = allowExtensions; + } + + /** + * /** + *

+ * Sends the opening request to the server: + *

+ * + *
+     * 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)); + } + + /** + *

+ * Process server response: + *

+ * + *
+     * 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); + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker17.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker17.java new file mode 100644 index 0000000000..8d7b2caf37 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker17.java @@ -0,0 +1,186 @@ +/* + * 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; + +/** + *

+ * Performs client side opening and closing handshakes for web socket + * specification version draft-ietf-hybi-thewebsocketprotocol- 17 + *

+ * + * @author The Netty Project + */ +public class WebSocketClientHandshaker17 extends WebSocketClientHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker17.class); + + public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private String expectedChallengeResponseString = null; + + private static final String protocol = null; + + private boolean allowExtensions = false; + + /** + * Constructor specifying the destination web socket location and version to + * initiate + * + * @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. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketClientHandshaker17(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, boolean allowExtensions) { + super(webSocketURL, version, subProtocol); + this.allowExtensions = allowExtensions; + } + + /** + * /** + *

+ * Sends the opening request to the server: + *

+ * + *
+     * 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: 13
+     * 
+ * + * @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("HyBi17 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, "13"); + + channel.write(request); + + ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket13FrameEncoder(true)); + } + + /** + *

+ * Process server response: + *

+ * + *
+     * 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 WebSocket13FrameDecoder(false, this.allowExtensions)); + + this.setOpenningHandshakeCompleted(true); + } + +} 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..c908b13f6e --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java @@ -0,0 +1,59 @@ +/* + * 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.V17) { + return new WebSocketClientHandshaker17(webSocketURL, version, subProtocol, allowExtensions); + } + 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..c9245fa68b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java @@ -0,0 +1,80 @@ +/* + * 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..385a2fd5cb --- /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..13b3f5b56e --- /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..9d8c7a7a5f --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java @@ -0,0 +1,190 @@ +/* + * 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(); + } + } + } + + /** + * 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..ea63380d04 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java @@ -0,0 +1,201 @@ +/* + * 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; + +/** + *

+ * 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. + *

+ * + * @author The Netty Project + * @author Vibul Imtarnasan + */ +public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker00.class); + + /** + * 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 + */ + public WebSocketServerHandshaker00(String webSocketURL, String subProtocols) { + super(webSocketURL, subProtocols); + } + + /** + *

+ * 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: + *

+ * + *
+     * 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
+     * 
+ * + *

+ * Server response: + *

+ * + *
+     * 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()); + } + + /** + * 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..40d5dfa032 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java @@ -0,0 +1,168 @@ +/* + * 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.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +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; + +/** + *

+ * Performs server side opening and closing handshakes for web socket + * specification version draft-ietf-hybi-thewebsocketprotocol- 10 + *

+ * + * @author The Netty Project + */ +public class WebSocketServerHandshaker10 extends WebSocketServerHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker10.class); + + public static final String WEBSOCKET_08_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + 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 + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketServerHandshaker10(String webSocketURL, String subProtocols, boolean allowExtensions) { + super(webSocketURL, subProtocols); + this.allowExtensions = allowExtensions; + } + + /** + *

+ * 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: + *

+ * + *
+     * 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
+     * 
+ * + *

+ * Server response: + *

+ * + *
+     * 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)); + + } + + /** + * Echo back the closing frame and close the connection + * + * @param ctx + * Channel context + * @param frame + * Web Socket frame that was received + */ + @Override + public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) { + ChannelFuture f = ctx.getChannel().write(frame); + f.addListener(ChannelFutureListener.CLOSE); + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker17.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker17.java new file mode 100644 index 0000000000..8a9ac4aeb5 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker17.java @@ -0,0 +1,168 @@ +/* + * 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.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +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; + +/** + *

+ * Performs server side opening and closing handshakes for web socket + * specification version draft-ietf-hybi-thewebsocketprotocol- 17 + *

+ * + * @author The Netty Project + */ +public class WebSocketServerHandshaker17 extends WebSocketServerHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker17.class); + + public static final String WEBSOCKET_17_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + 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 + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketServerHandshaker17(String webSocketURL, String subProtocols, boolean allowExtensions) { + super(webSocketURL, subProtocols); + this.allowExtensions = allowExtensions; + } + + /** + *

+ * Handle the web socket handshake for the web socket specification HyBi + * versions 13-17. Versions 13-17 share the same wire protocol. + *

+ * + *

+ * Browser request to the server: + *

+ * + *
+     * 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: 13
+     * 
+ * + *

+ * Server response: + *

+ * + *
+     * 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 17 handshake", ctx.getChannel().getId())); + } + + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, new HttpResponseStatus(101, "Switching Protocols")); + this.setVersion(WebSocketSpecificationVersion.V17); + + String key = req.getHeader(Names.SEC_WEBSOCKET_KEY); + if (key == null) { + res.setStatus(HttpResponseStatus.BAD_REQUEST); + return; + } + String acceptSeed = key + WEBSOCKET_17_ACCEPT_GUID; + byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); + String accept = base64Encode(sha1); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("HyBi17 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 WebSocket13FrameDecoder(true, this.allowExtensions)); + p.replace("encoder", "wsencoder", new WebSocket13FrameEncoder(false)); + + } + + /** + * Echo back the closing frame and close the connection + * + * @param ctx + * Channel context + * @param frame + * Web Socket frame that was received + */ + @Override + public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) { + ChannelFuture f = ctx.getChannel().write(frame); + f.addListener(ChannelFutureListener.CLOSE); + } + +} 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..f1306b572e --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java @@ -0,0 +1,100 @@ +/* + * 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 final String webSocketURL; + + private final 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; + } + + /** + * Instances a new handshaker + * + * @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("13")) { + // Version 13 of the wire protocol - assume version 17 of the + // specification. + return new WebSocketServerHandshaker17(webSocketURL, subProtocols, this.allowExtensions); + } else 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, "13"); + ctx.getChannel().write(res); + } + +} 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..58a160a998 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + *

+ * 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. + *

+ * + * @author The Netty Project + */ +public enum WebSocketSpecificationVersion { + UNKNOWN, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 00. + */ + V00, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 10 + */ + V10, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 17 + */ + V17 + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/package-info.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/package-info.java new file mode 100644 index 0000000000..8f642dcff2 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/package-info.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. + */ + +/** + * Encoder, decoder, handshakers and their related message types for + * Web Socket data frames. + *

+ * 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. + *

+ * + * @apiviz.exclude OneToOne(Encoder|Decoder)$ + * @apiviz.exclude \.codec\.replay\. + * @apiviz.exclude \.Default + */ +package org.jboss.netty.handler.codec.http.websocketx; +