From 07803b3e2c463b9ad5c1b82c747cb04e7bffb4c0 Mon Sep 17 00:00:00 2001 From: pfisterer Date: Mon, 1 Aug 2011 10:07:34 +0200 Subject: [PATCH 01/22] Updated version of netty-iostream channels --- .../channel/iostream/IOStreamAddress.java | 48 +++++ .../channel/iostream/IOStreamChannel.java | 73 +++++++ .../iostream/IOStreamChannelFactory.java | 60 ++++++ .../channel/iostream/IOStreamChannelSink.java | 178 ++++++++++++++++++ .../netty/example/iostream/IOStream.java | 94 +++++++++ 5 files changed, 453 insertions(+) create mode 100755 src/main/java/org/jboss/netty/channel/iostream/IOStreamAddress.java create mode 100755 src/main/java/org/jboss/netty/channel/iostream/IOStreamChannel.java create mode 100755 src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelFactory.java create mode 100755 src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelSink.java create mode 100755 src/main/java/org/jboss/netty/example/iostream/IOStream.java diff --git a/src/main/java/org/jboss/netty/channel/iostream/IOStreamAddress.java b/src/main/java/org/jboss/netty/channel/iostream/IOStreamAddress.java new file mode 100755 index 0000000000..e85c90e939 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/iostream/IOStreamAddress.java @@ -0,0 +1,48 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.iostream; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketAddress; + +/** + * A {@link java.net.SocketAddress} implementation holding an {@link java.io.InputStream} and an {@link java.io.OutputStream} instance used as + * "remote" address to connect to with a {@link IOStreamChannel}. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStreamAddress extends SocketAddress { + + private final InputStream inputStream; + + private final OutputStream outputStream; + + public IOStreamAddress(final InputStream inputStream, final OutputStream outputStream) { + + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + public InputStream getInputStream() { + return inputStream; + } + + public OutputStream getOutputStream() { + return outputStream; + } +} diff --git a/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannel.java b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannel.java new file mode 100755 index 0000000000..67bb4b7a97 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannel.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.iostream; + + +import org.jboss.netty.channel.*; + +import java.net.SocketAddress; + +/** + * A channel to an {@link java.io.InputStream} and an {@link java.io.OutputStream}. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStreamChannel extends AbstractChannel { + + IOStreamChannel(final ChannelFactory factory, final ChannelPipeline pipeline, final ChannelSink sink) { + super(null, factory, pipeline, sink); + } + + @Override + public ChannelConfig getConfig() { + return ((IOStreamChannelSink) getPipeline().getSink()).getConfig(); + } + + @Override + public boolean isBound() { + return ((IOStreamChannelSink) getPipeline().getSink()).isBound(); + } + + @Override + public boolean isConnected() { + return ((IOStreamChannelSink) getPipeline().getSink()).isConnected(); + } + + @Override + public SocketAddress getLocalAddress() { + return null; + } + + @Override + public SocketAddress getRemoteAddress() { + return ((IOStreamChannelSink) getPipeline().getSink()).getRemoteAddress(); + } + + @Override + public ChannelFuture bind(final SocketAddress localAddress) { + throw new UnsupportedOperationException(); + } + + @Override + public ChannelFuture unbind() { + throw new UnsupportedOperationException(); + } + + void doSetClosed() { + setClosed(); + } +} diff --git a/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelFactory.java b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelFactory.java new file mode 100755 index 0000000000..5b7c9548cd --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.iostream; + + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFactory; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.ChannelGroupFuture; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.util.internal.ExecutorUtil; + +import java.util.concurrent.ExecutorService; + +/** + * A {@link org.jboss.netty.channel.ChannelFactory} for creating {@link IOStreamChannel} instances. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStreamChannelFactory implements ChannelFactory { + + private final ChannelGroup channels = new DefaultChannelGroup("IOStreamChannelFactory-ChannelGroup"); + + private final ExecutorService executorService; + + public IOStreamChannelFactory(ExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public Channel newChannel(final ChannelPipeline pipeline) { + IOStreamChannelSink sink = new IOStreamChannelSink(executorService); + IOStreamChannel channel = new IOStreamChannel(this, pipeline, sink); + sink.setChannel(channel); + channels.add(channel); + return channel; + } + + @Override + public void releaseExternalResources() { + ChannelGroupFuture close = channels.close(); + close.awaitUninterruptibly(); + ExecutorUtil.terminate(executorService); + } +} diff --git a/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelSink.java b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelSink.java new file mode 100755 index 0000000000..ecd14eb1d9 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/iostream/IOStreamChannelSink.java @@ -0,0 +1,178 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.iostream; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.*; + +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.util.concurrent.ExecutorService; + +import static org.jboss.netty.channel.Channels.*; + +/** + * A {@link org.jboss.netty.channel.ChannelSink} implementation which reads from an {@link java.io.InputStream} and + * writes to an {@link java.io.OutputStream}. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStreamChannelSink extends AbstractChannelSink { + + private static class ReadRunnable implements Runnable { + + private final IOStreamChannelSink channelSink; + + public ReadRunnable(final IOStreamChannelSink channelSink) { + this.channelSink = channelSink; + } + + @Override + public void run() { + + PushbackInputStream in = channelSink.inputStream; + + while (channelSink.channel.isOpen()) { + + byte[] buf; + int readBytes; + try { + int bytesToRead = in.available(); + if (bytesToRead > 0) { + buf = new byte[bytesToRead]; + readBytes = in.read(buf); + } else { + // peek into the stream if it was closed (value=-1) + int b = in.read(); + if (b < 0) { + break; + } + // push back the byte which was read too much + in.unread(b); + continue; + } + } catch (Throwable t) { + if (!channelSink.channel.getCloseFuture().isDone()) { + fireExceptionCaught(channelSink.channel, t); + } + break; + } + + fireMessageReceived(channelSink.channel, ChannelBuffers.wrappedBuffer(buf, 0, readBytes)); + } + + // Clean up. + close(channelSink.channel); + } + } + + private final ExecutorService executorService; + + private IOStreamChannel channel; + + public IOStreamChannelSink(final ExecutorService executorService) { + this.executorService = executorService; + } + + public boolean isConnected() { + return inputStream != null && outputStream != null; + } + + public IOStreamAddress getRemoteAddress() { + return remoteAddress; + } + + public boolean isBound() { + return false; + } + + public ChannelConfig getConfig() { + return config; + } + + public void setChannel(final IOStreamChannel channel) { + this.channel = channel; + } + + private IOStreamAddress remoteAddress; + + private OutputStream outputStream; + + private PushbackInputStream inputStream; + + private ChannelConfig config = new DefaultChannelConfig(); + + @Override + public void eventSunk(final ChannelPipeline pipeline, final ChannelEvent e) throws Exception { + + final ChannelFuture future = e.getFuture(); + + if (e instanceof ChannelStateEvent) { + + final ChannelStateEvent stateEvent = (ChannelStateEvent) e; + final ChannelState state = stateEvent.getState(); + final Object value = stateEvent.getValue(); + + switch (state) { + + case OPEN: + if (Boolean.FALSE.equals(value)) { + outputStream = null; + inputStream = null; + ((IOStreamChannel) e.getChannel()).doSetClosed(); + } + break; + + case BOUND: + throw new UnsupportedOperationException(); + + case CONNECTED: + if (value != null) { + remoteAddress = (IOStreamAddress) value; + outputStream = remoteAddress.getOutputStream(); + inputStream = new PushbackInputStream(remoteAddress.getInputStream()); + executorService.execute(new ReadRunnable(this)); + future.setSuccess(); + } + break; + + case INTEREST_OPS: + // TODO implement + throw new UnsupportedOperationException(); + + } + + } else if (e instanceof MessageEvent) { + + final MessageEvent event = (MessageEvent) e; + if (event.getMessage() instanceof ChannelBuffer) { + + final ChannelBuffer buffer = (ChannelBuffer) event.getMessage(); + buffer.readBytes(outputStream, buffer.readableBytes()); + outputStream.flush(); + future.setSuccess(); + + } else { + throw new IllegalArgumentException( + "Only ChannelBuffer objects are supported to be written onto the IOStreamChannelSink! " + + "Please check if the encoder pipeline is configured correctly." + ); + } + } + } +} diff --git a/src/main/java/org/jboss/netty/example/iostream/IOStream.java b/src/main/java/org/jboss/netty/example/iostream/IOStream.java new file mode 100755 index 0000000000..1abc192d2c --- /dev/null +++ b/src/main/java/org/jboss/netty/example/iostream/IOStream.java @@ -0,0 +1,94 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.iostream; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.*; +import org.jboss.netty.channel.iostream.IOStreamAddress; +import org.jboss.netty.channel.iostream.IOStreamChannelFactory; +import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; +import org.jboss.netty.handler.codec.frame.Delimiters; +import org.jboss.netty.handler.codec.string.StringDecoder; +import org.jboss.netty.handler.codec.string.StringEncoder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * An example demonstrating the use of the {@link org.jboss.netty.channel.iostream.IOStreamChannel}. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStream { + + private static volatile boolean running = true; + + public static void main(String[] args) { + + final ExecutorService executorService = Executors.newCachedThreadPool(); + final ClientBootstrap bootstrap = new ClientBootstrap(new IOStreamChannelFactory(executorService)); + + // Configure the event pipeline factory. + bootstrap.setPipelineFactory(new ChannelPipelineFactory() { + public ChannelPipeline getPipeline() throws Exception { + DefaultChannelPipeline pipeline = new DefaultChannelPipeline(); + pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast("decoder", new StringDecoder()); + pipeline.addLast("encoder", new StringEncoder()); + pipeline.addLast("loggingHandler", new SimpleChannelHandler() { + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) + throws Exception { + + final String message = (String) e.getMessage(); + synchronized (System.out) { + e.getChannel().write("Message received: " + message); + } + if ("exit".equals(message)) { + IOStream.running = false; + } + super.messageReceived(ctx, e); + } + } + ); + return pipeline; + } + }); + + // Make a new connection. + ChannelFuture connectFuture = bootstrap.connect(new IOStreamAddress(System.in, System.out)); + + // Wait until the connection is made successfully. + Channel channel = connectFuture.awaitUninterruptibly().getChannel(); + + while (running) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // Close the connection. + channel.close().awaitUninterruptibly(); + + // Shut down all thread pools to exit. + bootstrap.releaseExternalResources(); + + } + +} From aa72c987e73827687b9c5921c20afabccc9c03e8 Mon Sep 17 00:00:00 2001 From: Veebs Date: Mon, 26 Sep 2011 22:51:15 +1000 Subject: [PATCH 02/22] WebSocket hybi 10 support --- .../example/http/websocketx/client/App.java | 172 ++++++++++ .../websocketx/client/WebSocketCallback.java | 71 +++++ .../websocketx/client/WebSocketClient.java | 57 ++++ .../client/WebSocketClientFactory.java | 86 +++++ .../client/WebSocketClientHandler.java | 124 ++++++++ .../websocketx/client/WebSocketException.java | 47 +++ .../client/WebSocketHttpResponseDecoder.java | 53 ++++ .../websocketx/server/WebSocketServer.java | 74 +++++ .../server/WebSocketServerHandler.java | 153 +++++++++ .../server/WebSocketServerIndexPage.java | 70 +++++ .../WebSocketServerPipelineFactory.java | 44 +++ .../netty/handler/codec/http/HttpHeaders.java | 12 + .../handler/codec/http/websocketx/Base64.java | 115 +++++++ .../http/websocketx/BinaryWebSocketFrame.java | 55 ++++ .../http/websocketx/CloseWebSocketFrame.java | 38 +++ .../http/websocketx/PingWebSocketFrame.java | 55 ++++ .../http/websocketx/PongWebSocketFrame.java | 55 ++++ .../http/websocketx/TextWebSocketFrame.java | 89 ++++++ .../websocketx/WebSocket00FrameDecoder.java | 140 +++++++++ .../websocketx/WebSocket00FrameEncoder.java | 103 ++++++ .../websocketx/WebSocket08FrameDecoder.java | 293 ++++++++++++++++++ .../websocketx/WebSocket08FrameEncoder.java | 152 +++++++++ .../websocketx/WebSocketClientHandshaker.java | 195 ++++++++++++ .../WebSocketClientHandshaker00.java | 249 +++++++++++++++ .../WebSocketClientHandshaker10.java | 188 +++++++++++ .../WebSocketClientHandshakerFactory.java | 54 ++++ .../codec/http/websocketx/WebSocketFrame.java | 51 +++ .../http/websocketx/WebSocketFrameType.java | 25 ++ .../WebSocketHandshakeException.java | 34 ++ .../websocketx/WebSocketServerHandshaker.java | 175 +++++++++++ .../WebSocketServerHandshaker00.java | 205 ++++++++++++ .../WebSocketServerHandshaker10.java | 161 ++++++++++ .../WebSocketServerHandshakerFactory.java | 102 ++++++ .../WebSocketSpecificationVersion.java | 45 +++ .../codec/http/websocketx/package-info.java | 44 +++ 35 files changed, 3586 insertions(+) create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/App.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClient.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientFactory.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientHandler.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketException.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketHttpResponseDecoder.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java create mode 100644 src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerPipelineFactory.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/Base64.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java create mode 100644 src/main/java/org/jboss/netty/handler/codec/http/websocketx/package-info.java 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..afce21acd5 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java @@ -0,0 +1,172 @@ +/* + * 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.InetSocketAddress; +import java.net.URI; +import java.util.ArrayList; +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.Channel; +import org.jboss.netty.channel.ChannelFactory; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.ChannelGroupFuture; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; +import org.jboss.netty.example.http.websocketx.server.WebSocketServerPipelineFactory; +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; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * A HTTP client demo app + * + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class App { + + private static final ChannelGroup allChannels = new DefaultChannelGroup("App"); + private static ChannelFactory channelFactory = null; + + 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); + + startServer(); + + runClient(); + + stopServer(); + } + + /** + * Starts our web socket server + */ + public static void startServer() { + // Configure the server. + channelFactory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), + Executors.newCachedThreadPool()); + + ServerBootstrap bootstrap = new ServerBootstrap(channelFactory); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory()); + + // Bind and start to accept incoming connections. + Channel channel = bootstrap.bind(new InetSocketAddress(8080)); + allChannels.add(channel); + + System.out + .println("Web Socket Server started on 8080. Open your browser and navigate to http://localhost:8080/"); + } + + /** + * Send and receive some messages using a web socket client + * + * @throws Exception + */ + public static void runClient() throws Exception { + + MyCallbackHandler callbackHandler = new MyCallbackHandler(); + WebSocketClientFactory factory = new WebSocketClientFactory(); + + // Connect with spec version 10 (try changing it to V00 and it will still work ... fingers crossed ;-) + WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"), + WebSocketSpecificationVersion.V10, callbackHandler); + + // Connect + client.connect().awaitUninterruptibly(); + Thread.sleep(500); + + // Send 10 messages and wait for responses + for (int i = 0; i < 10; i++) { + client.send(new TextWebSocketFrame("Message #" + i)); + } + Thread.sleep(1000); + + // Close - this throws ClosedChannelException. Not sure why. Just as easy to just disconnect. + //client.send(new CloseWebSocketFrame()); + + // Disconnect + client.disconnect(); + } + + /** + * Stops the server + */ + public static void stopServer() { + ChannelGroupFuture future = allChannels.close(); + future.awaitUninterruptibly(); + + channelFactory.releaseExternalResources(); + channelFactory = null; + + } + + /** + * Our web socket callback handler for this app + */ + public static class MyCallbackHandler implements WebSocketCallback { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(MyCallbackHandler.class); + + public boolean connected = false; + public ArrayList messagesReceived = new ArrayList(); + + public MyCallbackHandler() { + return; + } + + @Override + public void onConnect(WebSocketClient client) { + logger.debug("WebSocket Client connected!"); + connected = true; + } + + @Override + public void onDisconnect(WebSocketClient client) { + logger.debug("WebSocket Client disconnected!"); + connected = false; + } + + @Override + public void onMessage(WebSocketClient client, WebSocketFrame frame) { + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + logger.debug("WebSocket Client Received Message:" + textFrame.getText()); + messagesReceived.add(textFrame.getText()); + } + } + + @Override + public void onError(Throwable t) { + logger.error("WebSocket Client error", t); + } + + } + +} 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..b076701bca --- /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 + */ + public void onConnect(WebSocketClient client); + + /** + * Called when the client got disconnected from the server. + * + * @param client + * Current client that was disconnected + */ + public void onDisconnect(WebSocketClient client); + + /** + * Called when a message arrives from the server. + * + * @param client + * Current client connected + * @param frame + * Data received from server + */ + public void onMessage(WebSocketClient client, WebSocketFrame frame); + + /** + * Called when an unhandled errors occurs. + * + * @param t + * The causing error + */ + public 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..430b28b6fd --- /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. + */ + public ChannelFuture connect(); + + /** + * Disconnect from the server + * + * @return Disconnect future. Fires when disconnected. + */ + public ChannelFuture disconnect(); + + /** + * Send data to server + * + * @param frame + * Data for sending + * @return Write future. Will fire when the data is sent. + */ + public 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..4ef12bc07a --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientFactory.java @@ -0,0 +1,86 @@ +//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 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() { + + 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..e87cb37431 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketClientHandler.java @@ -0,0 +1,124 @@ +//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 ClientBootstrap bootstrap; + private URI url; + private WebSocketCallback callback; + private Channel channel; + private WebSocketClientHandshaker handshaker = null; + private 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); + 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(); + } + + public ChannelFuture connect() { + return bootstrap.connect(new InetSocketAddress(url.getHost(), url.getPort())); + } + + public ChannelFuture disconnect() { + return channel.close(); + } + + public ChannelFuture send(WebSocketFrame frame) { + return channel.write(frame); + } + + public URI getUrl() { + return url; + } + + public void setUrl(URI url) { + this.url = url; + } +} \ No newline at end of file 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/server/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java new file mode 100644 index 0000000000..37869480db --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.server; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +/** + * A HTTP server which serves Web Socket requests at: + * + * http://localhost:8080/websocket + * + * Open your browser at http://localhost:8080/, then the demo page will be + * loaded and a Web Socket connection will be made automatically. + * + * This server illustrates support for the different web socket specification + * versions and will work with: + * + *
    + *
  • Safari 5+ (draft-ietf-hybi-thewebsocketprotocol-00) + *
  • + *
  • Chrome 6-13 (draft-ietf-hybi-thewebsocketprotocol-00) + *
  • + *
  • Chrome 14+ (draft-ietf-hybi-thewebsocketprotocol-10) + *
  • + *
  • Firefox 7+ (draft-ietf-hybi-thewebsocketprotocol-10) + *
  • + *
+ * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServer { + public static void main(String[] args) { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(8080)); + + System.out.println("Web Socket Server started on 8080. Open your browser and navigate to http://localhost:8080/"); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java new file mode 100644 index 0000000000..12e22936a2 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java @@ -0,0 +1,153 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.server; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.*; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; +import static org.jboss.netty.handler.codec.http.HttpMethod.*; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; +import static org.jboss.netty.handler.codec.http.HttpVersion.*; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + * Handles handshakes and messages + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandler.class); + + private static final String WEBSOCKET_PATH = "/websocket"; + + private WebSocketServerHandshaker handshaker = null; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + handleHttpRequest(ctx, (HttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception { + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Send the demo page and favicon.ico + if (req.getUri().equals("/")) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK); + + ChannelBuffer content = WebSocketServerIndexPage.getContent(getWebSocketLocation(req)); + + res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8"); + setContentLength(res, content.readableBytes()); + + res.setContent(content); + sendHttpResponse(ctx, req, res); + return; + } else if (req.getUri().equals("/favicon.ico")) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND); + sendHttpResponse(ctx, req, res); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + this.getWebSocketLocation(req), null); + this.handshaker = wsFactory.newHandshaker(ctx, req); + if (this.handshaker == null) { + wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); + } else { + this.handshaker.executeOpeningHandshake(ctx, req); + } + return; + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + + // Check for closing frame + if (frame instanceof CloseWebSocketFrame) { + this.handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); + return; + } else if (frame instanceof PingWebSocketFrame) { + ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); + return; + } else if (!(frame instanceof TextWebSocketFrame)) { + throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() + .getName())); + } + + // Send the uppercase string back. + String request = ((TextWebSocketFrame) frame).getText(); + logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), request)); + ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase())); + } + + private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { + // Generate an error page if response status code is not OK (200). + if (res.getStatus().getCode() != 200) { + res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); + setContentLength(res, res.getContent().readableBytes()); + } + + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.getChannel().write(res); + if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + private String getWebSocketLocation(HttpRequest req) { + return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java new file mode 100644 index 0000000000..1865ad5a7a --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.server; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + + +/** + * Generates the demo HTML page which is served at http://localhost:8080/ + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerIndexPage { + + private static final String NEWLINE = "\r\n"; + + public static ChannelBuffer getContent(String webSocketLocation) { + return ChannelBuffers.copiedBuffer( + "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..49167bae22 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerPipelineFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.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 + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerPipelineFactory implements ChannelPipelineFactory { + @Override + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(65536)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", new WebSocketServerHandler()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java index 68e15757db..7e2f2c4b41 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 @@ -236,6 +236,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/Base64.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/Base64.java new file mode 100644 index 0000000000..33880b60b6 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/Base64.java @@ -0,0 +1,115 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/** + * Encodes binary data to plain text as Base64. + * + * Despite there being a gazillion other Base64 implementations out there, this + * has been written as part of XStream as it forms a core part but is too + * trivial to warrant an extra dependency. + * + * This meets the standard as described in RFC 1521, section 5.2 + * , allowing other Base64 tools to + * manipulate the data. + * + * This code originally came from the XStream http://xstream.codehaus.org + * project by Joe Walnes. Relicensed to Webbit. + */ +public class Base64 { + + // Here's how encoding works: + // + // 1) Incoming bytes are broken up into groups of 3 (each byte having 8 + // bits). + // + // 2) The combined 24 bits (3 * 8) are split into 4 groups of 6 bits. + // + // input |------||------||------| (3 values each with 8 bits) + // 101010101010101010101010 + // output |----||----||----||----| (4 values each with 6 bits) + // + // 3) Each of these 4 groups of 6 bits are converted back to a number, which + // will fall in the range of 0 - 63. + // + // 4) Each of these 4 numbers are converted to an alphanumeric char in a + // specified mapping table, to create + // a 4 character string. + // + // 5) This is repeated for all groups of three bytes. + // + // 6) Special padding is done at the end of the stream using the '=' char. + + private static final char[] SIXTY_FOUR_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .toCharArray(); + private static final int[] REVERSE_MAPPING = new int[123]; + + static { + for (int i = 0; i < SIXTY_FOUR_CHARS.length; i++) + REVERSE_MAPPING[SIXTY_FOUR_CHARS[i]] = i + 1; + } + + public static String encode(byte[] input) { + StringBuilder result = new StringBuilder(); + int outputCharCount = 0; + for (int i = 0; i < input.length; i += 3) { + int remaining = Math.min(3, input.length - i); + int oneBigNumber = (input[i] & 0xff) << 16 | (remaining <= 1 ? 0 : input[i + 1] & 0xff) << 8 + | (remaining <= 2 ? 0 : input[i + 2] & 0xff); + for (int j = 0; j < 4; j++) + result.append(remaining + 1 > j ? SIXTY_FOUR_CHARS[0x3f & oneBigNumber >> 6 * (3 - j)] : '='); + if ((outputCharCount += 4) % 76 == 0) + result.append('\n'); + } + return result.toString(); + } + + public static byte[] decode(String input) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + StringReader in = new StringReader(input); + for (int i = 0; i < input.length(); i += 4) { + int a[] = { mapCharToInt(in), mapCharToInt(in), mapCharToInt(in), mapCharToInt(in) }; + int oneBigNumber = (a[0] & 0x3f) << 18 | (a[1] & 0x3f) << 12 | (a[2] & 0x3f) << 6 | (a[3] & 0x3f); + for (int j = 0; j < 3; j++) { + if (a[j + 1] >= 0) { + out.write(0xff & oneBigNumber >> 8 * (2 - j)); + } + } + } + return out.toByteArray(); + } catch (IOException e) { + throw new Error(e + ": " + e.getMessage()); + } + } + + private static int mapCharToInt(Reader input) throws IOException { + int c; + while ((c = input.read()) != -1) { + int result = REVERSE_MAPPING[c]; + if (result != 0) + return result - 1; + if (c == '=') + return -1; + } + return -1; + } +} 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..beccd509e9 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java @@ -0,0 +1,55 @@ +/* + * 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 { + + @Override + public WebSocketFrameType getType() { + return WebSocketFrameType.BINARY; + } + + /** + * Creates a new empty binary frame. + */ + public BinaryWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(type: " + getType() + ", " + "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..d4d7b6b447 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java @@ -0,0 +1,38 @@ +/* + * 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 { + + @Override + public WebSocketFrameType getType() { + return WebSocketFrameType.CLOSE; + } + + /** + * Creates a new empty binary frame. + */ + public CloseWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } +} 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..f4a31a0862 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java @@ -0,0 +1,55 @@ +/* + * 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 { + + @Override + public WebSocketFrameType getType() { + return WebSocketFrameType.PING; + } + + /** + * Creates a new empty binary frame. + */ + public PingWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(type: " + getType() + ", " + "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..4743d10553 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java @@ -0,0 +1,55 @@ +/* + * 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 { + + @Override + public WebSocketFrameType getType() { + return WebSocketFrameType.PONG; + } + + /** + * Creates a new empty binary frame. + */ + public PongWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(type: " + getType() + ", " + "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..f6f7d827cd --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java @@ -0,0 +1,89 @@ +/* + * 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 { + + @Override + public WebSocketFrameType getType() { + return WebSocketFrameType.TEXT; + } + + /** + * Creates a new empty text frame. + */ + public TextWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new text frame with the specified text string. + * + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(String text) { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + + /** + * Creates a new frame with the specified binary data. + * + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(ChannelBuffer binaryData) { + 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/WebSocket00FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java new file mode 100644 index 0000000000..4698d32b31 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java @@ -0,0 +1,140 @@ +/* + * 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 + * @version $Rev: 2342 $, $Date: 2010-07-07 14:07:39 +0900 (Wed, 07 Jul 2010) $ + * + * @apiviz.landmark + * @apiviz.uses org.jboss.netty.handler.codec.http.websocket.WebSocketFrame + */ +public class WebSocket00FrameDecoder extends ReplayingDecoder { + + 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..e3883eb1e4 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java @@ -0,0 +1,103 @@ +/* + * 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 + * @version $Rev: 2362 $, $Date: 2010-09-09 19:59:22 +0900 (Thu, 09 Sep 2010) $ + * + * @apiviz.landmark + * @apiviz.uses org.jboss.netty.handler.codec.http.websocket.WebSocketFrame + */ +@Sharable +public class WebSocket00FrameEncoder extends OneToOneEncoder { + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception { + if (msg instanceof WebSocketFrame) { + WebSocketFrame frame = (WebSocketFrame) msg; + if (frame.getType() == WebSocketFrameType.TEXT) { + // 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.getType() == WebSocketFrameType.CLOSE) { + // 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..6579bd03d2 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -0,0 +1,293 @@ +// (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.channel.Channel; +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 java.util.ArrayList; +import java.util.List; + +/** + * Decodes a web socket frame from wire protocol version 8 format. This code was + * originally taken from webbit and modified. + * + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameDecoder extends ReplayingDecoder { + + 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; + + public static final int MAX_LENGTH = 16384; + + private Byte fragmentOpcode; + private Byte opcode = null; + private int currentFrameLength; + private ChannelBuffer maskingKey; + private int currentPayloadBytesRead = 0; + private ChannelBuffer currentPayload = null; + private List frames = new ArrayList(); + private boolean maskedPayload = false; + private boolean receivedClosingHandshake = false; + + public static enum State { + FRAME_START, PARSING_LENGTH, MASKING_KEY, PARSING_LENGTH_2, PARSING_LENGTH_3, PAYLOAD + } + + /** + * Constructor + * + * @param maskedPayload + * Web socket servers must set this to true processed incoming + * masked payload. Client implementations must set this to false. + */ + public WebSocket08FrameDecoder(boolean maskedPayload) { + super(State.FRAME_START); + this.maskedPayload = maskedPayload; + } + + @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: + currentPayloadBytesRead = 0; + currentFrameLength = -1; + currentPayload = null; + + byte b = buffer.readByte(); + byte fin = (byte) (b & 0x80); + byte reserved = (byte) (b & 0x70); + byte opcode = (byte) (b & 0x0F); + + if (reserved != 0) { + throw new CorruptedFrameException("Reserved bits set: " + bits(reserved)); + } + if (!isOpcode(opcode)) { + throw new CorruptedFrameException("Invalid opcode " + hex(opcode)); + } + + if (fin == 0) { + if (fragmentOpcode == null) { + if (!isDataOpcode(opcode)) { + throw new CorruptedFrameException("Fragmented frame with invalid opcode " + hex(opcode)); + } + fragmentOpcode = opcode; + } else if (opcode != OPCODE_CONT) { + throw new CorruptedFrameException("Continuation frame with invalid opcode " + hex(opcode)); + } + } else { + if (fragmentOpcode != null) { + if (!isControlOpcode(opcode) && opcode != OPCODE_CONT) { + throw new CorruptedFrameException("Final frame with invalid opcode " + hex(opcode)); + } + } else if (opcode == OPCODE_CONT) { + throw new CorruptedFrameException("Final frame with invalid opcode " + hex(opcode)); + } + this.opcode = opcode; + } + + checkpoint(State.PARSING_LENGTH); + case PARSING_LENGTH: + b = buffer.readByte(); + int length = (byte) (b); + + if (this.maskedPayload) { + byte masked = (byte) (b & 0x80); + if (masked == 0) { + throw new CorruptedFrameException("Unmasked frame received"); + } + length = (byte) (b & 0x7F); + } + + if (length < 126) { + currentFrameLength = length; + if (currentFrameLength == 0) { + checkpoint(State.PAYLOAD); + } else { + checkpoint(this.maskedPayload ? State.MASKING_KEY : State.PAYLOAD); + } + } else if (length == 126) { + checkpoint(State.PARSING_LENGTH_2); + } else if (length == 127) { + checkpoint(State.PARSING_LENGTH_3); + } + return null; + case PARSING_LENGTH_2: + currentFrameLength = buffer.readShort(); + checkpoint(this.maskedPayload ? State.MASKING_KEY : State.PAYLOAD); + return null; + case PARSING_LENGTH_3: + currentFrameLength = buffer.readInt(); + checkpoint(this.maskedPayload ? State.MASKING_KEY : State.PAYLOAD); + return null; + case MASKING_KEY: + maskingKey = buffer.readBytes(4); + checkpoint(State.PAYLOAD); + return null; + case PAYLOAD: + // Some times, 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 payload = null; + + int willHaveReadByteCount = currentPayloadBytesRead + rbytes; + if (willHaveReadByteCount == currentFrameLength) { + // We have all our content so proceed to process + payload = buffer.readBytes(rbytes); + } else if (willHaveReadByteCount < currentFrameLength) { + // We don't have all our content so accumulate payload. + // Returning null means we will get called back + payload = buffer.readBytes(rbytes); + if (currentPayload == null) { + currentPayload = channel.getConfig().getBufferFactory().getBuffer(currentFrameLength); + } + currentPayload.writeBytes(payload); + currentPayloadBytesRead = currentPayloadBytesRead + rbytes; + + // Return null to wait for more bytes to arrive + return null; + } else if (willHaveReadByteCount > currentFrameLength) { + // We have more than what we need so read up to the end of frame + // Leave the remainder in the buffer for next frame + payload = buffer.readBytes(currentFrameLength - currentPayloadBytesRead); + } + + // 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 (currentPayload == null) { + currentPayload = payload; + } else { + currentPayload.writeBytes(payload); + } + + // Unmask data if needed + if (this.maskedPayload) { + unmask(currentPayload); + } + + // Accumulate fragments + if (this.opcode == OPCODE_CONT) { + this.opcode = fragmentOpcode; + frames.add(currentPayload); + + int totalBytes = 0; + for (ChannelBuffer channelBuffer : frames) { + totalBytes += channelBuffer.readableBytes(); + } + + currentPayload = channel.getConfig().getBufferFactory().getBuffer(totalBytes); + for (ChannelBuffer channelBuffer : frames) { + currentPayload.writeBytes(channelBuffer); + } + + this.fragmentOpcode = null; + frames.clear(); + } + + if (this.opcode == OPCODE_TEXT) { + if (currentPayload.readableBytes() > MAX_LENGTH) { + throw new TooLongFrameException(); + } + return new TextWebSocketFrame(currentPayload); + } else if (this.opcode == OPCODE_BINARY) { + return new BinaryWebSocketFrame(currentPayload); + } else if (this.opcode == OPCODE_PING) { + return new PingWebSocketFrame(currentPayload); + } else if (this.opcode == OPCODE_PONG) { + return new PongWebSocketFrame(currentPayload); + } else if (this.opcode == OPCODE_CLOSE) { + this.receivedClosingHandshake = true; + return new CloseWebSocketFrame(); + } else { + throw new UnsupportedOperationException("Cannot decode opcode: " + this.opcode); + } + 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 String bits(byte b) { + return Integer.toBinaryString(b).substring(24); + } + + private String hex(byte b) { + return Integer.toHexString(b); + } + + private boolean isOpcode(int opcode) { + return opcode == OPCODE_CONT || opcode == OPCODE_TEXT || opcode == OPCODE_BINARY || opcode == OPCODE_CLOSE + || opcode == OPCODE_PING || opcode == OPCODE_PONG; + } + + private boolean isControlOpcode(int opcode) { + return opcode == OPCODE_CLOSE || opcode == OPCODE_PING || opcode == OPCODE_PONG; + } + + private boolean isDataOpcode(int opcode) { + return opcode == OPCODE_TEXT || opcode == OPCODE_BINARY; + } +} \ No newline at end of file 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..7b9dcee5cd --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java @@ -0,0 +1,152 @@ +// (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.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; + +/** + *

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

+ *

+ * Currently fragmentation is not supported. Data is always sent in 1 frame. + *

+ * + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameEncoder extends OneToOneEncoder { + + 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(); + + // Create buffer with 10 extra bytes for: + // 1 byte opCode + // 5 bytes length in worst case scenario + // 4 bites mask + ChannelBuffer encoded = channel.getConfig().getBufferFactory() + .getBuffer(data.order(), data.readableBytes() + 10); + + // Write opcode and length + 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 { + throw new UnsupportedOperationException("Cannot encode frame of type: " + frame.getClass().getName()); + } + encoded.writeByte(0x80 | opcode); // Fragmentation currently not + // supported + + int length = data.readableBytes(); + if (length < 126) { + byte b = (byte) (this.maskPayload ? (0x80 | (byte) length) : (byte) length); + encoded.writeByte(b); + } else if (length < 65535) { + byte b = (byte) (this.maskPayload ? (0xFE) : 126); + encoded.writeByte(b); + encoded.writeShort(length); + } else { + byte b = (byte) (this.maskPayload ? (0xFF) : 127); + encoded.writeByte(b); + encoded.writeInt(length); + } + + // Write payload + if (this.maskPayload) { + Integer random = (int) (Math.random() * Integer.MAX_VALUE); + mask = ByteBuffer.allocate(4).putInt(random).array(); + + encoded.writeBytes(mask); + + int counter = 0; + while (data.readableBytes() > 0) { + byte byteData = data.readByte(); + encoded.writeByte(byteData ^ mask[+counter++ % 4]); + } + + counter++; + } else { + encoded.writeBytes(data, data.readerIndex(), data.readableBytes()); + encoded = encoded.slice(0, encoded.writerIndex()); + } + + return encoded; + } + return msg; + } +} \ No newline at end of file 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..aa778165d7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java @@ -0,0 +1,195 @@ +/* + * 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.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.http.HttpResponse; + +/** + * 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"); + } + } + + /** + * 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) { + int rand = (int) (Math.random() * max + min); + return rand; + } +} 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..524abb6932 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java @@ -0,0 +1,249 @@ +/* + * 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); + return; + } + + /** + *

+ * 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); + return; + } + + private String insertRandomCharacters(String key) { + int count = createRandomNumber(1, 12); + + char[] randomChars = new char[count]; + int randCount = 0; + while (randCount < count) { + int rand = (int) (Math.random() * 0x7e + 0x21); + if (((0x21 < rand) && (rand < 0x2f)) || ((0x3a < rand) && (rand < 0x7e))) { + randomChars[randCount] = (char) rand; + randCount += 1; + } + } + + for (int i = 0; i < count; i++) { + int split = createRandomNumber(0, key.length()); + String part1 = key.substring(0, split); + String part2 = key.substring(split); + key = part1 + randomChars[i] + part2; + } + + return key; + } + + private String insertSpaces(String key, int spaces) { + for (int i = 0; i < spaces; i++) { + int split = createRandomNumber(1, key.length() - 1); + String part1 = key.substring(0, split); + String part2 = key.substring(split); + key = part1 + " " + part2; + } + + return key; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java new file mode 100644 index 0000000000..5ce6547a02 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java @@ -0,0 +1,188 @@ +/* + * 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 String protocol = 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 WebSocketClientHandshaker10(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol) { + super(webSocketURL, version, subProtocol); + return; + } + + /** + * /** + *

+ * 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 = Base64.encode(nonce); + + String acceptSeed = key + MAGIC_GUID; + byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); + this.expectedChallengeResponseString = Base64.encode(sha1); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("HyBi10 Client Handshake key: %s. Expected response: %s.", key, + this.expectedChallengeResponseString)); + } + + // Format request + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + request.addHeader(Names.UPGRADE, Values.WEBSOCKET.toLowerCase()); + request.addHeader(Names.CONNECTION, Values.UPGRADE); + request.addHeader(Names.SEC_WEBSOCKET_KEY, key); + request.addHeader(Names.HOST, wsURL.getHost()); + request.addHeader(Names.ORIGIN, "http://" + wsURL.getHost()); + if (protocol != null && !protocol.equals("")) { + request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol); + } + request.addHeader(Names.SEC_WEBSOCKET_VERSION, "8"); + + channel.write(request); + + ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket08FrameEncoder(true)); + return; + } + + /** + *

+ * 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.setOpenningHandshakeCompleted(true); + return; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java new file mode 100644 index 0000000000..739ad48560 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java @@ -0,0 +1,54 @@ +/* + * 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. + * @throws WebSocketHandshakeException + */ + public WebSocketClientHandshaker newHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, + String subProtocol) throws WebSocketHandshakeException { + if (version == WebSocketSpecificationVersion.V10) { + return new WebSocketClientHandshaker10(webSocketURL, version, subProtocol); + } + 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..3893a701dd --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.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.ChannelBuffer; + +/** + * Base class for web socket frames + * + * @author The Netty Project + */ +public abstract class WebSocketFrame { + + /** + * Contents of this frame + */ + private ChannelBuffer binaryData; + + /** + * Returns the type of this frame. + */ + public abstract WebSocketFrameType getType(); + + /** + * Returns binary data + */ + public ChannelBuffer getBinaryData() { + return binaryData; + } + + /** + * Sets the binary data for this frame + */ + public void setBinaryData(ChannelBuffer binaryData) { + this.binaryData = binaryData; + } + +} 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..e0650f8621 --- /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 +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java new file mode 100644 index 0000000000..db9f768615 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Exception during handshaking process + * + * @author The Netty Project + */ +public class WebSocketHandshakeException extends Exception { + + private static final long serialVersionUID = 1L; + + public WebSocketHandshakeException(String s) { + super(s); + } + + public WebSocketHandshakeException(String s, Throwable throwable) { + super(s, throwable); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java new file mode 100644 index 0000000000..ace5ecafab --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java @@ -0,0 +1,175 @@ +/* + * 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.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.http.HttpRequest; + +/** + * Base class for server side web socket opening and closing handshakes + * + * @author The Netty Project + */ +public abstract class WebSocketServerHandshaker { + + private String webSocketURL; + + private String subProtocols; + + private String[] subProtocolsArray = null; + + private WebSocketSpecificationVersion version = WebSocketSpecificationVersion.UNKNOWN; + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subProtocols + * CSV of supported protocols. Null if sub protocols not + * supported. + */ + public WebSocketServerHandshaker(String webSocketURL, String subProtocols) { + this.webSocketURL = webSocketURL; + this.subProtocols = subProtocols; + + if (this.subProtocols != null) { + this.subProtocolsArray = subProtocols.split(","); + for (int i = 0; i < this.subProtocolsArray.length; i++) { + this.subProtocolsArray[i] = this.subProtocolsArray[i].trim(); + } + } + return; + } + + /** + * Returns the URL of the web socket + */ + public String getWebSocketURL() { + return webSocketURL; + } + + public void setWebSocketURL(String webSocketURL) { + this.webSocketURL = webSocketURL; + } + + /** + * Returns the CSV of supported sub protocols + */ + public String getSubProtocols() { + return subProtocols; + } + + public void setSubProtocols(String subProtocols) { + this.subProtocols = subProtocols; + } + + /** + * Returns the version of the specification being supported + */ + public WebSocketSpecificationVersion getVersion() { + return version; + } + + public void setVersion(WebSocketSpecificationVersion version) { + this.version = version; + } + + /** + * Performs the opening handshake + * + * @param ctx + * Context + * @param req + * HTTP Request + * @throws NoSuchAlgorithmException + */ + public abstract void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req); + + /** + * Performs the closing handshake + * + * @param ctx + * Context + * @param frame + * Closing Frame that was received + */ + public abstract void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame); + + /** + * Performs an MD5 hash + * + * @param bytes + * Data to hash + * @return Hashed data + */ + protected byte[] md5(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 not supported on this platform"); + } + } + + /** + * SHA-1 hashing. Instance this we think it is not thread safe + * + * @param bytes + * byte to hash + * @return hashed + */ + protected byte[] sha1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-1 not supported on this platform"); + } + } + + /** + * Selects the first matching supported sub protocol + * + * @param requestedSubProtocol + * CSV of protocols to be supported. e.g. "chat, superchat" + * @return First matching supported sub protocol. Null if not found. + */ + protected String selectSubProtocol(String requestedSubProtocol) { + if (requestedSubProtocol == null || this.subProtocolsArray == null) { + return null; + } + + String[] requesteSubProtocolsArray = requestedSubProtocol.split(","); + for (int i = 0; i < requesteSubProtocolsArray.length; i++) { + String requesteSubProtocol = requesteSubProtocolsArray[i].trim(); + + for (String supportedSubProtocol : this.subProtocolsArray) { + if (requesteSubProtocol.equals(supportedSubProtocol)) { + return requesteSubProtocol; + } + } + } + + // No match found + return null; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java new file mode 100644 index 0000000000..cfbbcd3f0a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java @@ -0,0 +1,205 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY1; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY2; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_LOCATION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_LOCATION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_PROTOCOL; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET; +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpHeaders.Values; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + *

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

+ * 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()); + return; + } + + /** + * Echo back the closing frame + * + * @param ctx + * Channel context + * @param frame + * Web Socket frame that was received + */ + @Override + public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) { + ctx.getChannel().write(frame); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java new file mode 100644 index 0000000000..fed122013c --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java @@ -0,0 +1,161 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET; +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + *

+ * 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"; + + /** + * 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 WebSocketServerHandshaker10(String webSocketURL, String subProtocols) { + super(webSocketURL, subProtocols); + return; + } + + /** + *

+ * 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 = Base64.encode(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)); + p.replace("encoder", "wsencoder", new WebSocket08FrameEncoder(false)); + + return; + } + + /** + * Echo back the closing frame + * + * @param ctx + * Channel context + * @param frame + * Web Socket frame that was received + */ + @Override + public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) { + ctx.getChannel().write(frame); + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java new file mode 100644 index 0000000000..64d83be35b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; + +/** + * Instances the appropriate handshake class to use for clients + * + * @author The Netty Project + */ +public class WebSocketServerHandshakerFactory { + + private String webSocketURL; + + private String subProtocols; + + /** + * 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 WebSocketServerHandshakerFactory(String webSocketURL, String subProtocols) { + this.webSocketURL = webSocketURL; + this.subProtocols = subProtocols; + return; + } + + /** + * Instances a new handshaker + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the + * server + * @param subProtocol + * Sub protocol request sent to the server. Null if no + * sub-protocol support is required. + * @return A new WebSocketServerHandshaker for the requested web socket + * version. Null if web socket version is not supported. + */ + public WebSocketServerHandshaker newHandshaker(ChannelHandlerContext ctx, HttpRequest req) { + + String version = req.getHeader(Names.SEC_WEBSOCKET_VERSION); + if (version != null) { + if (version.equals("8")) { + // Version 8 of the wire protocol - assume version 10 of the + // specification. + return new WebSocketServerHandshaker10(webSocketURL, subProtocols); + } else { + return null; + } + } else { + // Assume version 00 where version header was not specified + return new WebSocketServerHandshaker00(webSocketURL, subProtocols); + } + } + + /** + * Return that we need cannot not support the web socket version + * + * @param ctx + * Context + */ + public void sendUnsupportedWebSocketVersionResponse(ChannelHandlerContext ctx) { + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, + "Switching Protocols")); + res.setStatus(HttpResponseStatus.UPGRADE_REQUIRED); + res.setHeader(Names.SEC_WEBSOCKET_VERSION, "8"); + ctx.getChannel().write(res); + return; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java new file mode 100644 index 0000000000..c83779a7bf --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + *

+ * 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 +} 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; + From 33f548a8ae2b988c8d316de8b4d3e6957c32b446 Mon Sep 17 00:00:00 2001 From: Veebs Date: Mon, 26 Sep 2011 23:07:22 +1000 Subject: [PATCH 03/22] Added example ping/pong and closing frame. --- .../example/http/websocketx/client/App.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 index afce21acd5..422b4fe94f 100644 --- 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 @@ -25,6 +25,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFactory; import org.jboss.netty.channel.group.ChannelGroup; @@ -32,6 +33,9 @@ import org.jboss.netty.channel.group.ChannelGroupFuture; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.example.http.websocketx.server.WebSocketServerPipelineFactory; +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; @@ -94,13 +98,14 @@ public class App { MyCallbackHandler callbackHandler = new MyCallbackHandler(); WebSocketClientFactory factory = new WebSocketClientFactory(); - // Connect with spec version 10 (try changing it to V00 and it will still work ... fingers crossed ;-) + // Connect with spec version 10 (try changing it to V00 and it will + // still work ... fingers crossed ;-) WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"), WebSocketSpecificationVersion.V10, callbackHandler); // Connect client.connect().awaitUninterruptibly(); - Thread.sleep(500); + Thread.sleep(200); // Send 10 messages and wait for responses for (int i = 0; i < 10; i++) { @@ -108,8 +113,11 @@ public class App { } Thread.sleep(1000); - // Close - this throws ClosedChannelException. Not sure why. Just as easy to just disconnect. - //client.send(new CloseWebSocketFrame()); + // Close - this throws ClosedChannelException. Not sure why. Just as + // easy to just disconnect. + client.send(new PingWebSocketFrame(ChannelBuffers.copiedBuffer(new byte[] { 1, 2, 3, 4, 5, 6 }))); + client.send(new CloseWebSocketFrame()); + Thread.sleep(200); // Disconnect client.disconnect(); @@ -159,6 +167,10 @@ public class App { TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; logger.debug("WebSocket Client Received Message:" + textFrame.getText()); messagesReceived.add(textFrame.getText()); + } else if (frame instanceof PongWebSocketFrame) { + logger.debug("WebSocket Client ping/pong"); + } else if (frame instanceof CloseWebSocketFrame) { + logger.debug("WebSocket Client closing"); } } From f8b96fe6b818048e233eccc571736430dfe967ea Mon Sep 17 00:00:00 2001 From: Veebs Date: Thu, 13 Oct 2011 10:17:49 +1100 Subject: [PATCH 04/22] Support for FireFox websockets. They use window.MozWebSocket rather than window.WebSocket. --- .../http/websocketx/server/WebSocketServerIndexPage.java | 3 +++ 1 file changed, 3 insertions(+) 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 index 1865ad5a7a..179df6b633 100644 --- 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 @@ -39,6 +39,9 @@ public class WebSocketServerIndexPage { "" + NEWLINE + "