diff --git a/NOTICE.txt b/NOTICE.txt index a0b78e3202..02bb426a3d 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -96,3 +96,13 @@ framework implementation, which can be obtained at: * license/LICENSE.felix.txt (Apache License 2.0) * HOMEPAGE: * http://felix.apache.org/ + +This product optionally depends on 'Webbit', a Java event based +WebSocket and HTTP server: + + * LICENSE: + * license/LICENSE.webbit.txt (BSD License) + * HOMEPAGE: + * https://github.com/joewalnes/webbit + + \ No newline at end of file diff --git a/license/LICENSE.webbit.txt b/license/LICENSE.webbit.txt new file mode 100644 index 0000000000..05ae225fa3 --- /dev/null +++ b/license/LICENSE.webbit.txt @@ -0,0 +1,37 @@ +(BSD License: http://www.opensource.org/licenses/bsd-license) + +Copyright (c) 2011, Joe Walnes, Aslak Hellesøy and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the Webbit nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/docbook/en-US/module/architecture.xml b/src/docbook/en-US/module/architecture.xml index 44cbd0066b..5e4f883c91 100644 --- a/src/docbook/en-US/module/architecture.xml +++ b/src/docbook/en-US/module/architecture.xml @@ -41,20 +41,20 @@ Rich Buffer Data Structure Netty uses its own buffer API instead of NIO ByteBuffer - to represent a sequence of bytes. This approach has significant advantage + to represent a sequence of bytes. This approach has significant advantages over using ByteBuffer. Netty's new buffer type, - &ChannelBuffer; has been designed from ground up to address the problems + &ChannelBuffer; has been designed from the ground up to address the problems of ByteBuffer and to meet the daily needs of network application developers. To list a few cool features: - You can define your buffer type if necessary. + You can define your own buffer type if necessary. - Transparent zero copy is achieved by built-in composite buffer type. + Transparent zero copy is achieved by a built-in composite buffer type. @@ -84,7 +84,7 @@
Universal Asynchronous I/O API - Traditional I/O APIs in Java provided different types and methods for + Traditional I/O APIs in Java provide different types and methods for different transport types. For example, java.net.Socket and java.net.DatagramSocket do not have any common @@ -93,32 +93,33 @@ This mismatch makes porting a network application from one transport to - the other tedious and difficult. The lack of portability between - transports becomes a problem when you need to support more transports not - rewriting the network layer of the application. Logically, many protocols - can run on more than one transport such as TCP/IP, UDP/IP, SCTP, and - serial port communication. + another tedious and difficult. The lack of portability between + transports becomes a problem when you need to support additional + transports, as this often entails rewriting the network layer of the + application. Logically, many protocols can run on more than one + transport such as TCP/IP, UDP/IP, SCTP, and serial port communication. - To make the matter worse, Java New I/O (NIO) API introduced the - incompatibility with the old blocking I/O (OIO) API, and so will NIO.2 - (AIO). Because all these APIs are different from each other in design - and performance characteristics, you are often forced to determine which - API your application will depend on before you even begin the - implementation phase. + To make matters worse, Java's New I/O (NIO) API introduced + incompatibilities with the old blocking I/O (OIO) API and will continue + to do so in the next release, NIO.2 (AIO). Because all these APIs are + different from each other in design and performance characteristics, you + are often forced to determine which API your application will depend on + before you even begin the implementation phase. For instance, you might want to start with OIO because the number of clients you are going to serve will be very small and writing a socket server using OIO is much easier than using NIO. However, you are going - to be in trouble when your business grows up exponentially and your server - starts to serve tens of thousand clients simultaneously. You could - start with NIO, but it might take much longer time to implement due to - the complexity of the NIO Selector API, hindering rapid development. + to be in trouble when your business grows exponentially and your server + needs to serve tens of thousands of clients simultaneously. You could + start with NIO, but doing so may hinder rapid development by greatly + increasing development time due to the complexity of the NIO Selector + API. - Netty has a universal asynchronous I/O interface called &Channel;, which - abstracts away all operations required to point-to-point communication. + Netty has a universal asynchronous I/O interface called a &Channel;, which + abstracts away all operations required for point-to-point communication. That is, once you wrote your application on one Netty transport, your application can run on other Netty transports. Netty provides a number of essential transports via one universal API: @@ -144,30 +145,29 @@ - Switching from one transport to the other usually takes just a couple + Switching from one transport to another usually takes just a couple lines of changes such as choosing a different &ChannelFactory; implementation. - Also, you are even able to take advantage of a new transport which is - not written yet, serial port communication transport for instance, again + Also, you are even able to take advantage of new transports which aren't + yet written (such as serial port communication transport), again by replacing just a couple lines of constructor calls. Moreover, you can - write your own transport by extending the core API because it is highly - extensible. + write your own transport by extending the core API.
Event Model based on the Interceptor Chain Pattern - Well-defined and extensible event model is a must for an event-driven - application. Netty does have a well-defined event model focused on I/O. - It also allows you to implement your own event type without breaking the - existing code at all because each event type is distinguished from - each other by strict type hierarchy. This is another differentiator - against other frameworks. Many NIO frameworks have no or very limited - notion of event model; they often break the existing code when you try - to add a new custom event type, or just do not allow extension. + A well-defined and extensible event model is a must for an event-driven + application. Netty has a well-defined event model focused on I/O. It + also allows you to implement your own event type without breaking the + existing code because each event type is distinguished from another by + a strict type hierarchy. This is another differentiator against other + frameworks. Many NIO frameworks have no or a very limited notion of an + event model. If they offer extension at all, they often break the + existing code when you try to add custom event types A &ChannelEvent; is handled by a list of &ChannelHandler;s in a @@ -175,7 +175,7 @@ Intercepting Filter pattern to give a user full control over how an event is handled and how the handlers in the pipeline interact with each other. For example, - you can define what to do when a data is read from a socket: + you can define what to do when data is read from a socket: public class MyReadHandler implements &SimpleChannelHandler; { public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; evt) { @@ -188,8 +188,7 @@ } } - You can also define what to do when other handler requested a write - operation: + You can also define what to do when a handler receives a write request: public class MyWriteHandler implements &SimpleChannelHandler; { public void writeRequested(&ChannelHandlerContext; ctx, &MessageEvent; evt) { @@ -202,7 +201,7 @@ } } - For more information about the event model, please refer to the + For more information on the event model, please refer to the API documentation of &ChannelEvent; and &ChannelPipeline;.
@@ -212,29 +211,28 @@ On top of the core components mentioned above, that already enable the implementation of all types of network applications, Netty provides a set - of advanced features to accelerate the development pace even more. + of advanced features to accelerate the page of development even more.
Codec framework As demonstrated in , it is always a good - idea to separate a protocol codec from a business logic. However, there + idea to separate a protocol codec from business logic. However, there are some complications when implementing this idea from scratch. You have to deal with the fragmentation of messages. Some protocols are - multi-layered (i.e. built on top of other lower level protocol). Some + multi-layered (i.e. built on top of other lower level protocols). Some are too complicated to be implemented in a single state machine. Consequently, a good network application framework should provide an extensible, reusable, unit-testable, and multi-layered codec framework - that generates maintainable user codec. + that generates maintainable user codecs. - Netty provides a number of basic and advanced codecs built on top of - its core to address most issues you will encounter when you write a - protocol codec regardless if it is simple or not, binary or text - - simply whatever. + Netty provides a number of basic and advanced codecs to address most + issues you will encounter when you write a protocol codec regardless + if it is simple or not, binary or text - simply whatever.
@@ -245,16 +243,16 @@ You can't simply wrap a stream to encrypt or decrypt data but you have to use javax.net.ssl.SSLEngine. SSLEngine is a state machine which is as complex - as SSL is. You have to manage all possible states such as cipher suite - and encryption key negotiation (or re-negotiation), certificate - exchange and validation. Moreover, SSLEngine is - not even completely thread-safe unlike usual expectation. + as SSL itself. You have to manage all possible states such as cipher + suite and encryption key negotiation (or re-negotiation), certificate + exchange, and validation. Moreover, SSLEngine is + not even completely thread-safe, as one would expect.
In Netty, &SslHandler; takes care of all the gory details and pitfalls of SSLEngine. All you need to do is to configure - and insert the &SslHandler; to your &ChannelPipeline;. It also allows - you to implement advanced features like + the &SslHandler; and insert it into your &ChannelPipeline;. It also + allows you to implement advanced features like StartTLS very easily. @@ -269,13 +267,13 @@ Netty's HTTP support is very different from the existing HTTP libraries. - It gives you complete control over how HTTP messages are exchanged in a - low level. Because it is basically the combination of HTTP codec and - HTTP message classes, there is no restriction such as enforced thread + It gives you complete control over how HTTP messages are exchanged at a + low level. Because it is basically the combination of an HTTP codec and + HTTP message classes, there is no restriction such as an enforced thread model. That is, you can write your own HTTP client or server that works - exactly the way you want. You have full control over thread model, - connection life cycle, chunked encoding, and as much as what HTTP - specification allows you to do. + exactly the way you want. You have full control over everything that's + in the HTTP specification, including the thread model, connection life + cycle, and chunked encoding. Thanks to its highly customizable nature, you can write a very efficient @@ -291,18 +289,18 @@ Media streaming server that needs to keep the connection open - until the whole media is streamed (e.g. 2 hours of movie) + until the whole media is streamed (e.g. 2 hours of video) - File server that allows the upload of large files without memory - pressure (e.g. uploading 1GB per request) + File server that allows the uploading of large files without + memory pressure (e.g. uploading 1GB per request) - Scalable mash-up client that connects to tens of thousand 3rd + Scalable mash-up client that connects to tens of thousands of 3rd party web services asynchronously @@ -315,10 +313,11 @@ Google Protocol Buffers are an ideal solution for the rapid implementation of a highly efficient - binary protocol that evolves over time. With &ProtobufEncoder; and - &ProtobufDecoder;, you can turn the message classes generated by Google - Protocol Buffers Compiler (protoc) into Netty codec. Please take a look - into the 'LocalTime' example + binary protocols that evolve over time. With &ProtobufEncoder; and + &ProtobufDecoder;, you can turn the message classes generated by the + Google Protocol Buffers Compiler (protoc) into Netty codec. Please take + a look into the + 'LocalTime' example that shows how easily you can create a high-performing binary protocol client and server from the sample protocol definition. @@ -330,18 +329,18 @@ Summary In this chapter, we reviewed the overall architecture of Netty from the - feature-wise standpoint. Netty has simple yet powerful architecture. + feature standpoint. Netty has a simple, yet powerful architecture. It is composed of three components - buffer, channel, and event model - and all advanced features are built on top of the three core components. Once you understood how these three work together, it should not be - difficult to understand more advanced features which were covered briefly - in this chapter. + difficult to understand the more advanced features which were covered + briefly in this chapter. - You might still have an unanswered question about what the overall - architecture looks exactly like and how each feature work together. - If so, it is a good idea to talk to us - to improve this guide. + You might still have unanswered questions about what the overall + architecture looks like exactly and how each of the features work + together. If so, it is a good idea to + talk to us to improve this guide. diff --git a/src/main/java/org/jboss/netty/channel/ChannelEvent.java b/src/main/java/org/jboss/netty/channel/ChannelEvent.java index ca566d5022..2ab0b0b851 100644 --- a/src/main/java/org/jboss/netty/channel/ChannelEvent.java +++ b/src/main/java/org/jboss/netty/channel/ChannelEvent.java @@ -71,6 +71,7 @@ import org.jboss.netty.channel.socket.ServerSocketChannel; * {@code "channelOpen"} * {@link ChannelStateEvent}
(state = {@link ChannelState#OPEN OPEN}, value = {@code true}) * a {@link Channel} is open, but not bound nor connected + * Be aware that this event is fired from within the Boss-Thread so you should not execute any heavy operation in there as it will block the dispatching to other workers! * * * {@code "channelClosed"} @@ -81,6 +82,7 @@ import org.jboss.netty.channel.socket.ServerSocketChannel; * {@code "channelBound"} * {@link ChannelStateEvent}
(state = {@link ChannelState#BOUND BOUND}, value = {@link SocketAddress}) * a {@link Channel} is open and bound to a local address, but not connected + * Be aware that this event is fired from within the Boss-Thread so you should not execute any heavy operation in there as it will block the dispatching to other workers! * * * {@code "channelUnbound"} diff --git a/src/main/java/org/jboss/netty/channel/ChannelLocal.java b/src/main/java/org/jboss/netty/channel/ChannelLocal.java index 4328003e55..cbba433044 100644 --- a/src/main/java/org/jboss/netty/channel/ChannelLocal.java +++ b/src/main/java/org/jboss/netty/channel/ChannelLocal.java @@ -40,18 +40,38 @@ public class ChannelLocal { private final ConcurrentMap map = new ConcurrentIdentityWeakKeyHashMap(); + private final ChannelFutureListener remover = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + remove(future.getChannel()); + } + }; + + private final boolean removeOnClose; + /** - * Creates a {@link Channel} local variable. + * Creates a {@link Channel} local variable by calling {@link #ChannelLocal(boolean)} with true */ public ChannelLocal() { - super(); + this(true); } + + /** + * Creates a {@link Channel} local variable. + * + * @param removeOnClose if true the {@link ChannelLocal} will remove a {@link Channel} from it own once the {@link Channel} was closed. + */ + public ChannelLocal(boolean removeOnClose) { + this.removeOnClose = removeOnClose; + } + + /** * Returns the initial value of the variable. By default, it returns * {@code null}. Override it to change the initial value. */ - protected T initialValue(@SuppressWarnings("unused") Channel channel) { + protected T initialValue(Channel channel) { return null; } @@ -88,7 +108,11 @@ public class ChannelLocal { if (channel == null) { throw new NullPointerException("channel"); } - return map.put(channel, value); + T old = map.put(channel, value); + if (removeOnClose) { + channel.getCloseFuture().addListener(remover); + } + return old; } } @@ -105,7 +129,12 @@ public class ChannelLocal { if (channel == null) { throw new NullPointerException("channel"); } - return map.putIfAbsent(channel, value); + T mapping = map.putIfAbsent(channel, value); + + if (removeOnClose && mapping == null) { + channel.getCloseFuture().addListener(remover); + } + return mapping; } } @@ -126,6 +155,9 @@ public class ChannelLocal { if (removed == null) { return initialValue(channel); } else { + if (removeOnClose) { + channel.getCloseFuture().removeListener(remover); + } return removed; } } diff --git a/src/main/java/org/jboss/netty/channel/DefaultFileRegion.java b/src/main/java/org/jboss/netty/channel/DefaultFileRegion.java index 9a8bc100ee..d4f335211e 100644 --- a/src/main/java/org/jboss/netty/channel/DefaultFileRegion.java +++ b/src/main/java/org/jboss/netty/channel/DefaultFileRegion.java @@ -14,11 +14,21 @@ public class DefaultFileRegion implements FileRegion { private final FileChannel file; private final long position; private final long count; + private final boolean releaseAfterTransfer; + /** + * Calls {@link #DefaultFileRegion(FileChannel, long, long, boolean)} + * with true as the last argument. + */ public DefaultFileRegion(FileChannel file, long position, long count) { + this(file, position, count, true); + } + + public DefaultFileRegion(FileChannel file, long position, long count, boolean releaseAfterTransfer) { this.file = file; this.position = position; this.count = count; + this.releaseAfterTransfer = releaseAfterTransfer; } @Override @@ -31,6 +41,11 @@ public class DefaultFileRegion implements FileRegion { return count; } + @Override + public boolean releaseAfterTransfer() { + return releaseAfterTransfer; + } + @Override public long transferTo(WritableByteChannel target, long position) throws IOException { long count = this.count - position; diff --git a/src/main/java/org/jboss/netty/channel/FileRegion.java b/src/main/java/org/jboss/netty/channel/FileRegion.java index 7399453e4d..0780fb7a0d 100644 --- a/src/main/java/org/jboss/netty/channel/FileRegion.java +++ b/src/main/java/org/jboss/netty/channel/FileRegion.java @@ -73,6 +73,12 @@ public interface FileRegion extends ExternalResourceReleasable { */ long getCount(); + /** + * Returns true if {@link #releaseExternalResources()} has to + * be called after the transfer of this {@link FileRegion} is complete. + */ + boolean releaseAfterTransfer(); + /** * Transfers the content of this file region to the specified channel. * diff --git a/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java b/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java index 189269c46f..c942b8e7d7 100644 --- a/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java +++ b/src/main/java/org/jboss/netty/channel/SimpleChannelUpstreamHandler.java @@ -151,7 +151,9 @@ public class SimpleChannelUpstreamHandler implements ChannelUpstreamHandler { /** * Invoked when a {@link Channel} is open, but not bound nor connected. - */ + *
+ * Be aware that this event is fired from within the Boss-Thread so you should not execute any heavy operation in there as it will block the dispatching to other workers! + */ public void channelOpen( ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { ctx.sendUpstream(e); @@ -160,6 +162,8 @@ public class SimpleChannelUpstreamHandler implements ChannelUpstreamHandler { /** * Invoked when a {@link Channel} is open and bound to a local address, * but not connected. + *
+ * Be aware that this event is fired from within the Boss-Thread so you should not execute any heavy operation in there as it will block the dispatching to other workers! */ public void channelBound( ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { 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/channel/socket/nio/NioAcceptedSocketChannel.java b/src/main/java/org/jboss/netty/channel/socket/nio/NioAcceptedSocketChannel.java index ad7571ba8a..3b7205595f 100644 --- a/src/main/java/org/jboss/netty/channel/socket/nio/NioAcceptedSocketChannel.java +++ b/src/main/java/org/jboss/netty/channel/socket/nio/NioAcceptedSocketChannel.java @@ -47,7 +47,5 @@ final class NioAcceptedSocketChannel extends NioSocketChannel { setConnected(); fireChannelOpen(this); - fireChannelBound(this, getLocalAddress()); - fireChannelConnected(this, getRemoteAddress()); } } diff --git a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannel.java b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannel.java index 4e0dac2697..4e35bc17c8 100644 --- a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannel.java +++ b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannel.java @@ -29,7 +29,6 @@ import org.jboss.netty.channel.AbstractChannel; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFactory; import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelSink; import org.jboss.netty.channel.MessageEvent; diff --git a/src/main/java/org/jboss/netty/channel/socket/nio/NioWorker.java b/src/main/java/org/jboss/netty/channel/socket/nio/NioWorker.java index 0bf1dc51c9..39cedc3691 100644 --- a/src/main/java/org/jboss/netty/channel/socket/nio/NioWorker.java +++ b/src/main/java/org/jboss/netty/channel/socket/nio/NioWorker.java @@ -776,12 +776,10 @@ class NioWorker implements Runnable { } } - if (!server) { - if (!((NioClientSocketChannel) channel).boundManually) { - fireChannelBound(channel, localAddress); - } - fireChannelConnected(channel, remoteAddress); + if (server || !((NioClientSocketChannel) channel).boundManually) { + fireChannelBound(channel, localAddress); } + fireChannelConnected(channel, remoteAddress); } } } diff --git a/src/main/java/org/jboss/netty/channel/socket/nio/SocketSendBufferPool.java b/src/main/java/org/jboss/netty/channel/socket/nio/SocketSendBufferPool.java index ba1ab5de83..36606ce784 100644 --- a/src/main/java/org/jboss/netty/channel/socket/nio/SocketSendBufferPool.java +++ b/src/main/java/org/jboss/netty/channel/socket/nio/SocketSendBufferPool.java @@ -306,7 +306,10 @@ final class SocketSendBufferPool { @Override public void release() { - // Unpooled. + if (file.releaseAfterTransfer()) { + // Make sure the FileRegion resource are released otherwise it may cause a FD leak or something similar + file.releaseExternalResources(); + } } } diff --git a/src/main/java/org/jboss/netty/channel/socket/oio/OioAcceptedSocketChannel.java b/src/main/java/org/jboss/netty/channel/socket/oio/OioAcceptedSocketChannel.java index d42cb6f0e7..16781e5c1c 100644 --- a/src/main/java/org/jboss/netty/channel/socket/oio/OioAcceptedSocketChannel.java +++ b/src/main/java/org/jboss/netty/channel/socket/oio/OioAcceptedSocketChannel.java @@ -63,7 +63,6 @@ class OioAcceptedSocketChannel extends OioSocketChannel { fireChannelOpen(this); fireChannelBound(this, getLocalAddress()); - fireChannelConnected(this, getRemoteAddress()); } @Override diff --git a/src/main/java/org/jboss/netty/channel/socket/oio/OioWorker.java b/src/main/java/org/jboss/netty/channel/socket/oio/OioWorker.java index 945927f178..69814d0a48 100644 --- a/src/main/java/org/jboss/netty/channel/socket/oio/OioWorker.java +++ b/src/main/java/org/jboss/netty/channel/socket/oio/OioWorker.java @@ -20,12 +20,15 @@ import static org.jboss.netty.channel.Channels.*; import java.io.OutputStream; import java.io.PushbackInputStream; import java.net.SocketException; +import java.nio.channels.Channels; import java.nio.channels.ClosedChannelException; +import java.nio.channels.WritableByteChannel; import java.util.regex.Pattern; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.FileRegion; /** * @@ -50,8 +53,13 @@ class OioWorker implements Runnable { public void run() { channel.workerThread = Thread.currentThread(); final PushbackInputStream in = channel.getInputStream(); + boolean fireOpen = channel instanceof OioAcceptedSocketChannel; while (channel.isOpen()) { + if (fireOpen) { + fireOpen = false; + fireChannelConnected(channel, channel.getRemoteAddress()); + } synchronized (channel.interestOpsLock) { while (!channel.isReadable()) { try { @@ -114,13 +122,41 @@ class OioWorker implements Runnable { } try { - ChannelBuffer a = (ChannelBuffer) message; - int length = a.readableBytes(); - synchronized (out) { - a.getBytes(a.readerIndex(), out, length); + int length = 0; + + // Add support to write a FileRegion. This in fact will not give any performance gain but at least it not fail and + // we did the best to emulate it + if (message instanceof FileRegion) { + FileRegion fr = (FileRegion) message; + try { + synchronized (out) { + WritableByteChannel bchannel = Channels.newChannel(out); + + long i = 0; + while ((i = fr.transferTo(bchannel, length)) > 0) { + length += i; + if (length >= fr.getCount()) { + break; + } + } + } + } finally { + if (fr.releaseAfterTransfer()) { + fr.releaseExternalResources(); + } + + } + } else { + ChannelBuffer a = (ChannelBuffer) message; + length = a.readableBytes(); + synchronized (out) { + a.getBytes(a.readerIndex(), out, length); + } } + fireWriteComplete(channel, length); future.setSuccess(); + } catch (Throwable t) { // Convert 'SocketException: Socket closed' to // ClosedChannelException. diff --git a/src/main/java/org/jboss/netty/example/http/file/HttpStaticFileServerHandler.java b/src/main/java/org/jboss/netty/example/http/file/HttpStaticFileServerHandler.java index 922f0be479..f3cf358bcd 100644 --- a/src/main/java/org/jboss/netty/example/http/file/HttpStaticFileServerHandler.java +++ b/src/main/java/org/jboss/netty/example/http/file/HttpStaticFileServerHandler.java @@ -142,8 +142,11 @@ public class HttpStaticFileServerHandler extends SimpleChannelUpstreamHandler { { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); - if (ifModifiedSinceDate.getTime() == file.lastModified()) - { + + // Only compare up to the second because the datetime format we send to the client does not have milliseconds + long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; + long fileLastModifiedSeconds = file.lastModified() / 1000; + if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { sendNotModified(ctx); return; } diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java new file mode 100644 index 0000000000..67cb476e98 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +/** + * A Web Socket echo server for running the autobahn + * test suite + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServer { + public static void main(String[] args) { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); + + //bootstrap.setOption("child.tcpNoDelay", true); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(9000)); + + System.out.println("Web Socket Server started on 9000. Open your browser and navigate to http://localhost:9000/"); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java new file mode 100644 index 0000000000..f3a084282f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.*; +import static org.jboss.netty.handler.codec.http.HttpMethod.*; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; +import static org.jboss.netty.handler.codec.http.HttpVersion.*; + +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + * Handles handshakes and messages + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandler.class); + + private WebSocketServerHandshaker handshaker = null; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + handleHttpRequest(ctx, (HttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception { + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + this.getWebSocketLocation(req), null, false); + this.handshaker = wsFactory.newHandshaker(ctx, req); + if (this.handshaker == null) { + wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); + } else { + this.handshaker.executeOpeningHandshake(ctx, req); + } + return; + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + logger.debug(String + .format("Channel %s received %s", ctx.getChannel().getId(), frame.getClass().getSimpleName())); + + if (frame instanceof CloseWebSocketFrame) { + this.handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); + } else if (frame instanceof PingWebSocketFrame) { + ctx.getChannel().write( + new PongWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof TextWebSocketFrame) { + //String text = ((TextWebSocketFrame) frame).getText(); + ctx.getChannel().write(new TextWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof BinaryWebSocketFrame) { + ctx.getChannel().write( + new BinaryWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof ContinuationWebSocketFrame) { + ctx.getChannel().write( + new ContinuationWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof PongWebSocketFrame) { + // Ignore + } else { + throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() + .getName())); + } + } + + private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { + // Generate an error page if response status code is not OK (200). + if (res.getStatus().getCode() != 200) { + res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); + setContentLength(res, res.getContent().readableBytes()); + } + + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.getChannel().write(res); + if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + private String getWebSocketLocation(HttpRequest req) { + return "ws://" + req.getHeader(HttpHeaders.Names.HOST); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java new file mode 100644 index 0000000000..e7bfd52999 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.channel.Channels.*; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; + +/** + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerPipelineFactory implements ChannelPipelineFactory { + @Override + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(65536)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", new WebSocketServerHandler()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java new file mode 100644 index 0000000000..46de920b06 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This package is intended for use with testing against the Python + * AutoBahn test suite. + * + *

How to run the tests on Ubuntu

+ * + *

01. Add ppa:twisted-dev/ppa to your system's Software Sources + * + *

02. Install Twisted V11: sudo apt-get install python-twisted + * + *

03. Intall Python Setup Tools: sudo apt-get install python-setuptools + * + *

04. Install AutoBahn: sudo easy_install Autobahn + * + *

05. Get AutoBahn testsuite source code: git clone git@github.com:oberstet/Autobahn.git + * + *

06. Go to AutoBahn directory: cd Autobahn + * + *

07. Checkout stable version: git checkout v0.4.2 + * + *

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

09. Edit fuzzing_clinet_spec.json and set the version to 10. + * + * { + * "servers": [{"agent": "Netty", "hostname": "localhost", "port": 9000, "version": 10}], + * "cases": ["*"] + * } + * + * + *

10. Run the test python fuzzing_client.py. Note that the actual test case python code is + * located in /usr/local/lib/python2.6/dist-packages/autobahn-0.4.2-py2.6.egg/autobahn/cases + * and not in the checked out git repository. + * + *

11. See the results in reports/servers/index.html + */ +package org.jboss.netty.example.http.websocketx.autobahn; + diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java new file mode 100644 index 0000000000..1112c45cf7 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/App.java @@ -0,0 +1,136 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.jboss.netty.example.http.websocketx.client; + +import java.net.URI; +import java.util.ArrayList; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketSpecificationVersion; + +/** + * A HTTP client demo app + * + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class App { + + public static void main(String[] args) throws Exception { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + runClient(); + System.exit(0); + } + + /** + * Send and receive some messages using a web socket client + * + * @throws Exception + */ + public static void runClient() throws Exception { + + MyCallbackHandler callbackHandler = new MyCallbackHandler(); + WebSocketClientFactory factory = new WebSocketClientFactory(); + + // Connect with spec version 10 (try changing it to V00 and it will + // still work ... fingers crossed ;-) + WebSocketClient client = factory.newClient(new URI("ws://localhost:8080/websocket"), + WebSocketSpecificationVersion.V10, callbackHandler); + + // Connect + System.out.println("WebSocket Client connecting"); + client.connect().awaitUninterruptibly(); + Thread.sleep(200); + + // Send 10 messages and wait for responses + System.out.println("WebSocket Client sending message"); + for (int i = 0; i < 10; i++) { + client.send(new TextWebSocketFrame("Message #" + i)); + } + Thread.sleep(1000); + + // Ping + System.out.println("WebSocket Client sending ping"); + client.send(new PingWebSocketFrame(ChannelBuffers.copiedBuffer(new byte[] { 1, 2, 3, 4, 5, 6 }))); + Thread.sleep(1000); + + // Close + System.out.println("WebSocket Client sending close"); + client.send(new CloseWebSocketFrame()); + Thread.sleep(1000); + + // Disconnect + client.disconnect(); + } + + /** + * Our web socket callback handler for this app + */ + public static class MyCallbackHandler implements WebSocketCallback { + public boolean connected = false; + public ArrayList messagesReceived = new ArrayList(); + + public MyCallbackHandler() { + return; + } + + @Override + public void onConnect(WebSocketClient client) { + System.out.println("WebSocket Client connected!"); + connected = true; + } + + @Override + public void onDisconnect(WebSocketClient client) { + System.out.println("WebSocket Client disconnected!"); + connected = false; + } + + @Override + public void onMessage(WebSocketClient client, WebSocketFrame frame) { + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + System.out.println("WebSocket Client received message:" + textFrame.getText()); + messagesReceived.add(textFrame.getText()); + } else if (frame instanceof PongWebSocketFrame) { + System.out.println("WebSocket Client received pong"); + } else if (frame instanceof CloseWebSocketFrame) { + System.out.println("WebSocket Client received closing"); + } + } + + @Override + public void onError(Throwable t) { + System.out.println("WebSocket Client error " + t.toString()); + } + + } + +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/WebSocketCallback.java new file mode 100644 index 0000000000..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..07b5615cd5 --- /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, false); + handshaker.beginOpeningHandshake(ctx, channel); + } + + @Override + public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + callback.onDisconnect(this); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + if (!handshaker.isOpeningHandshakeCompleted()) { + handshaker.endOpeningHandshake(ctx, (HttpResponse) e.getMessage()); + callback.onConnect(this); + return; + } + + if (e.getMessage() instanceof HttpResponse) { + HttpResponse response = (HttpResponse) e.getMessage(); + throw new WebSocketException("Unexpected HttpResponse (status=" + response.getStatus() + ", content=" + + response.getContent().toString(CharsetUtil.UTF_8) + ")"); + } + + WebSocketFrame frame = (WebSocketFrame) e.getMessage(); + callback.onMessage(this, frame); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + final Throwable t = e.getCause(); + callback.onError(t); + e.getChannel().close(); + } + + 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/client/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/client/package-info.java new file mode 100644 index 0000000000..05da751a10 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/client/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + *

This is an example web service client. + *

To run this example, you must first start + * org.jboss.netty.example.http.websocketx.server.WebSocketServer + *

Next, run org.jboss.netty.example.http.websocketx.client.App. + */ +package org.jboss.netty.example.http.websocketx.client; + diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServer.java new file mode 100644 index 0000000000..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..d2b2d6ac85 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerHandler.java @@ -0,0 +1,153 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.server; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.*; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; +import static org.jboss.netty.handler.codec.http.HttpMethod.*; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; +import static org.jboss.netty.handler.codec.http.HttpVersion.*; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + * Handles handshakes and messages + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandler.class); + + private static final String WEBSOCKET_PATH = "/websocket"; + + private WebSocketServerHandshaker handshaker = null; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + handleHttpRequest(ctx, (HttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception { + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Send the demo page and favicon.ico + if (req.getUri().equals("/")) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK); + + ChannelBuffer content = WebSocketServerIndexPage.getContent(getWebSocketLocation(req)); + + res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8"); + setContentLength(res, content.readableBytes()); + + res.setContent(content); + sendHttpResponse(ctx, req, res); + return; + } else if (req.getUri().equals("/favicon.ico")) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND); + sendHttpResponse(ctx, req, res); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + this.getWebSocketLocation(req), null, false); + this.handshaker = wsFactory.newHandshaker(ctx, req); + if (this.handshaker == null) { + wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); + } else { + this.handshaker.executeOpeningHandshake(ctx, req); + } + return; + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + + // Check for closing frame + if (frame instanceof CloseWebSocketFrame) { + this.handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); + return; + } else if (frame instanceof PingWebSocketFrame) { + ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); + return; + } else if (!(frame instanceof TextWebSocketFrame)) { + throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() + .getName())); + } + + // Send the uppercase string back. + String request = ((TextWebSocketFrame) frame).getText(); + logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), request)); + ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase())); + } + + private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { + // Generate an error page if response status code is not OK (200). + if (res.getStatus().getCode() != 200) { + res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); + setContentLength(res, res.getContent().readableBytes()); + } + + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.getChannel().write(res); + if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + private String getWebSocketLocation(HttpRequest req) { + return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java new file mode 100644 index 0000000000..179df6b633 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/WebSocketServerIndexPage.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.http.websocketx.server; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + + +/** + * Generates the demo HTML page which is served at http://localhost:8080/ + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerIndexPage { + + private static final String NEWLINE = "\r\n"; + + public static ChannelBuffer getContent(String webSocketLocation) { + return ChannelBuffers.copiedBuffer( + "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/example/http/websocketx/server/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/server/package-info.java new file mode 100644 index 0000000000..ade9c22eb1 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/server/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + *

This package contains an example web socket web server. + *

The web server only handles text, ping and closing frames. For text frames, + * it echoes the received text in upper case. + *

Once started, you can test the web server against your browser by navigating + * to http://localhost:8080/ + *

You can also test it with a web socket client. Send web socket traffic to + * ws://localhost:8080/websocket. + */ +package org.jboss.netty.example.http.websocketx.server; + diff --git a/src/main/java/org/jboss/netty/example/iostream/IOStream.java b/src/main/java/org/jboss/netty/example/iostream/IOStream.java new file mode 100755 index 0000000000..1abc192d2c --- /dev/null +++ b/src/main/java/org/jboss/netty/example/iostream/IOStream.java @@ -0,0 +1,94 @@ +/* + * Copyright 2011 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.example.iostream; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.*; +import org.jboss.netty.channel.iostream.IOStreamAddress; +import org.jboss.netty.channel.iostream.IOStreamChannelFactory; +import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; +import org.jboss.netty.handler.codec.frame.Delimiters; +import org.jboss.netty.handler.codec.string.StringDecoder; +import org.jboss.netty.handler.codec.string.StringEncoder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * An example demonstrating the use of the {@link org.jboss.netty.channel.iostream.IOStreamChannel}. + * + * @author Daniel Bimschas + * @author Dennis Pfisterer + */ +public class IOStream { + + private static volatile boolean running = true; + + public static void main(String[] args) { + + final ExecutorService executorService = Executors.newCachedThreadPool(); + final ClientBootstrap bootstrap = new ClientBootstrap(new IOStreamChannelFactory(executorService)); + + // Configure the event pipeline factory. + bootstrap.setPipelineFactory(new ChannelPipelineFactory() { + public ChannelPipeline getPipeline() throws Exception { + DefaultChannelPipeline pipeline = new DefaultChannelPipeline(); + pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast("decoder", new StringDecoder()); + pipeline.addLast("encoder", new StringEncoder()); + pipeline.addLast("loggingHandler", new SimpleChannelHandler() { + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) + throws Exception { + + final String message = (String) e.getMessage(); + synchronized (System.out) { + e.getChannel().write("Message received: " + message); + } + if ("exit".equals(message)) { + IOStream.running = false; + } + super.messageReceived(ctx, e); + } + } + ); + return pipeline; + } + }); + + // Make a new connection. + ChannelFuture connectFuture = bootstrap.connect(new IOStreamAddress(System.in, System.out)); + + // Wait until the connection is made successfully. + Channel channel = connectFuture.awaitUninterruptibly().getChannel(); + + while (running) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // Close the connection. + channel.close().awaitUninterruptibly(); + + // Shut down all thread pools to exit. + bootstrap.releaseExternalResources(); + + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java index a141402c29..313d38b7ce 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java @@ -60,30 +60,34 @@ public class HttpContentCompressor extends HttpContentEncoder { } @Override - protected EncoderEmbedder newContentEncoder(String acceptEncoding) throws Exception { - ZlibWrapper wrapper = determineWrapper(acceptEncoding); - if (wrapper == null) { - return null; - } - - return new EncoderEmbedder(new ZlibEncoder(wrapper, compressionLevel)); - } - - @Override - protected String getTargetContentEncoding(String acceptEncoding) throws Exception { + protected Result beginEncode(HttpMessage msg, String acceptEncoding) throws Exception { + String contentEncoding = msg.getHeader(HttpHeaders.Names.CONTENT_ENCODING); + if (contentEncoding != null && + !HttpHeaders.Values.IDENTITY.equalsIgnoreCase(contentEncoding)) { + return null; + } + ZlibWrapper wrapper = determineWrapper(acceptEncoding); if (wrapper == null) { return null; } + String targetContentEncoding; switch (wrapper) { case GZIP: - return "gzip"; + targetContentEncoding = "gzip"; + break; case ZLIB: - return "deflate"; + targetContentEncoding = "deflate"; + break; default: throw new Error(); } + + return new Result( + targetContentEncoding, + new EncoderEmbedder( + new ZlibEncoder(wrapper, compressionLevel))); } private ZlibWrapper determineWrapper(String acceptEncoding) { diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java index 807b4e8a27..c07855c9b9 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java @@ -29,19 +29,20 @@ import org.jboss.netty.util.internal.LinkedTransferQueue; /** * Encodes the content of the outbound {@link HttpResponse} and {@link HttpChunk}. * The original content is replaced with the new content encoded by the - * {@link EncoderEmbedder}, which is created by {@link #newContentEncoder(String)}. + * {@link EncoderEmbedder}, which is created by {@link #beginEncode(HttpMessage, String)}. * Once encoding is finished, the value of the 'Content-Encoding' header - * is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}. + * is set to the target content encoding, as returned by + * {@link #beginEncode(HttpMessage, String)}. * Also, the 'Content-Length' header is updated to the length of the - * encoded content. If there is no supported encoding in the + * encoded content. If there is no supported or allowed encoding in the * corresponding {@link HttpRequest}'s {@code "Accept-Encoding"} header, - * {@link #newContentEncoder(String)} should return {@code null} so that no - * encoding occurs (i.e. pass-through). + * {@link #beginEncode(HttpMessage, String)} should return {@code null} so that + * no encoding occurs (i.e. pass-through). *

* Please note that this is an abstract class. You have to extend this class - * and implement {@link #newContentEncoder(String)} and {@link #getTargetContentEncoding(String)} - * properly to make this class functional. For example, refer to the source - * code of {@link HttpContentCompressor}. + * and implement {@link #beginEncode(HttpMessage, String)} properly to make + * this class functional. For example, refer to the source code of + * {@link HttpContentCompressor}. *

* This handler must be placed after {@link HttpMessageEncoder} in the pipeline * so that this handler can intercept HTTP responses before {@link HttpMessageEncoder} @@ -103,26 +104,37 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler { } boolean hasContent = m.isChunked() || m.getContent().readable(); - if (hasContent && (encoder = newContentEncoder(acceptEncoding)) != null) { - // Encode the content and remove or replace the existing headers - // so that the message looks like a decoded message. - m.setHeader( - HttpHeaders.Names.CONTENT_ENCODING, - getTargetContentEncoding(acceptEncoding)); + if (!hasContent) { + ctx.sendDownstream(e); + return; + } - if (!m.isChunked()) { - ChannelBuffer content = m.getContent(); - // Encode the content. - content = ChannelBuffers.wrappedBuffer( - encode(content), finishEncode()); + Result result = beginEncode(m, acceptEncoding); + if (result == null) { + ctx.sendDownstream(e); + return; + } - // Replace the content. - m.setContent(content); - if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) { - m.setHeader( - HttpHeaders.Names.CONTENT_LENGTH, - Integer.toString(content.readableBytes())); - } + encoder = result.getContentEncoder(); + + // Encode the content and remove or replace the existing headers + // so that the message looks like a decoded message. + m.setHeader( + HttpHeaders.Names.CONTENT_ENCODING, + result.getTargetContentEncoding()); + + if (!m.isChunked()) { + ChannelBuffer content = m.getContent(); + // Encode the content. + content = ChannelBuffers.wrappedBuffer( + encode(content), finishEncode()); + + // Replace the content. + m.setContent(content); + if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) { + m.setHeader( + HttpHeaders.Names.CONTENT_LENGTH, + Integer.toString(content.readableBytes())); } } @@ -162,24 +174,20 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler { } /** - * Returns a new {@link EncoderEmbedder} that encodes the HTTP message - * content. + * Prepare to encode the HTTP message content. * + * @param msg + * the HTTP message whose content should be encoded * @param acceptEncoding * the value of the {@code "Accept-Encoding"} header * - * @return a new {@link EncoderEmbedder} if there is a supported encoding - * in {@code acceptEncoding}. {@code null} otherwise. + * @return the result of preparation, which is composed of the determined + * target content encoding and a new {@link EncoderEmbedder} that + * encodes the content into the target content encoding. + * {@code null} if {@code acceptEncoding} is unsupported or rejected + * and thus the content should be handled as-is (i.e. no encoding). */ - protected abstract EncoderEmbedder newContentEncoder(String acceptEncoding) throws Exception; - - /** - * Returns the expected content encoding of the encoded content. - * - * @param acceptEncoding the value of the {@code "Accept-Encoding"} header - * @return the expected content encoding of the new content - */ - protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception; + protected abstract Result beginEncode(HttpMessage msg, String acceptEncoding) throws Exception; private ChannelBuffer encode(ChannelBuffer buf) { encoder.offer(buf); @@ -196,4 +204,29 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler { encoder = null; return result; } + + public static final class Result { + private final String targetContentEncoding; + private final EncoderEmbedder contentEncoder; + + public Result(String targetContentEncoding, EncoderEmbedder contentEncoder) { + if (targetContentEncoding == null) { + throw new NullPointerException("targetContentEncoding"); + } + if (contentEncoder == null) { + throw new NullPointerException("contentEncoder"); + } + + this.targetContentEncoding = targetContentEncoding; + this.contentEncoder = contentEncoder; + } + + public String getTargetContentEncoding() { + return targetContentEncoding; + } + + public EncoderEmbedder getContentEncoder() { + return contentEncoder; + } + } } 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/BinaryWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java new file mode 100644 index 0000000000..2e04c3ce25 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class BinaryWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty binary frame. + */ + public BinaryWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new binary frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new binary frame with the specified binary data and the final + * fragment flag. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java new file mode 100644 index 0000000000..cc607df36c --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket Frame for closing the connection + * + * @author Vibul Imtarnasan + */ +public class CloseWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty close frame. + */ + public CloseWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new close frame + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + */ + public CloseWebSocketFrame(boolean finalFragment, int rsv) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java new file mode 100644 index 0000000000..ec7db870f7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + +/** + * Web Socket continuation frame containing continuation text or binary data. + * This is used for fragmented messages where the contents of a messages is + * contained more than 1 frame. + * + * @author Vibul Imtarnasan + */ +public class ContinuationWebSocketFrame extends WebSocketFrame { + + private String aggregatedText = null; + + /** + * Creates a new empty continuation frame. + */ + public ContinuationWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new continuation frame with the specified binary data. The + * final fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + * @param aggregatedText + * Aggregated text set by decoder on the final continuation frame + * of a fragmented text message + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + this.aggregatedText = aggregatedText; + } + + /** + * Creates a new continuation frame with the specified text data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * text content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setText(text); + } + + /** + * Returns the text data in this frame + */ + public String getText() { + if (this.getBinaryData() == null) { + return null; + } + return this.getBinaryData().toString(CharsetUtil.UTF_8); + } + + /** + * Sets the string for this frame + * + * @param text + * text to store + */ + public void setText(String text) { + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + + /** + * Aggregated text returned by decoder on the final continuation frame of a + * fragmented text message + */ + public String getAggregatedText() { + return aggregatedText; + } + + public void setAggregatedText(String aggregatedText) { + this.aggregatedText = aggregatedText; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java new file mode 100644 index 0000000000..857afd4484 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PingWebSocketFrame.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PingWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty ping frame. + */ + public PingWebSocketFrame() { + this.setFinalFragment(true); + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new ping frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new ping frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java new file mode 100644 index 0000000000..19ea1350f8 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/PongWebSocketFrame.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PongWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty pong frame. + */ + public PongWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new pong frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new pong frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java new file mode 100644 index 0000000000..fa16845e0d --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/TextWebSocketFrame.java @@ -0,0 +1,128 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + +/** + * Web Socket text frame with assumed UTF-8 encoding + * + * @author Vibul Imtarnasan + * + */ +public class TextWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty text frame. + */ + public TextWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(String text) { + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * Returns the text data in this frame + */ + public String getText() { + if (this.getBinaryData() == null) { + return null; + } + return this.getBinaryData().toString(CharsetUtil.UTF_8); + } + + /** + * Sets the string for this frame + * + * @param text + * text to store + */ + public void setText(String text) { + if (text == null) { + throw new NullPointerException("text"); + } + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(text: " + getText() + ')'; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java new file mode 100644 index 0000000000..358f52684a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java @@ -0,0 +1,35 @@ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Invalid UTF8 bytes encountered + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Exception extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UTF8Exception(String reason) { + super(reason); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java new file mode 100644 index 0000000000..9e33df9bc0 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java @@ -0,0 +1,84 @@ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Checks UTF8 bytes for validity before converting it into a string + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Output { + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + + private static final byte[] TYPES = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, + 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 }; + + private static final byte[] STATES = { 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12 }; + + private int state = UTF8_ACCEPT; + private int codep = 0; + + private final StringBuilder stringBuilder; + + public UTF8Output(byte[] bytes) { + stringBuilder = new StringBuilder(bytes.length); + write(bytes); + } + + public void write(byte[] bytes) { + for (byte b : bytes) { + write(b); + } + } + + public void write(int b) { + byte type = TYPES[b & 0xFF]; + + codep = (state != UTF8_ACCEPT) ? (b & 0x3f) | (codep << 6) : (0xff >> type) & (b); + + state = STATES[state + type]; + + if (state == UTF8_ACCEPT) { + stringBuilder.append((char) codep); + } else if (state == UTF8_REJECT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + } + + public String toString() { + if (state != UTF8_ACCEPT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java new file mode 100644 index 0000000000..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..0ad6025e03 --- /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 instanceof TextWebSocketFrame) { + // Text frame + ChannelBuffer data = frame.getBinaryData(); + ChannelBuffer encoded = channel.getConfig().getBufferFactory() + .getBuffer(data.order(), data.readableBytes() + 2); + encoded.writeByte((byte) 0x00); + encoded.writeBytes(data, data.readerIndex(), data.readableBytes()); + encoded.writeByte((byte) 0xFF); + return encoded; + } else if (frame instanceof CloseWebSocketFrame) { + // Close frame + ChannelBuffer data = frame.getBinaryData(); + ChannelBuffer encoded = channel.getConfig().getBufferFactory().getBuffer(data.order(), 2); + encoded.writeByte((byte) 0xFF); + encoded.writeByte((byte) 0x00); + return encoded; + } else { + // Binary frame + ChannelBuffer data = frame.getBinaryData(); + int dataLen = data.readableBytes(); + ChannelBuffer encoded = channel.getConfig().getBufferFactory().getBuffer(data.order(), dataLen + 5); + + // Encode type. + encoded.writeByte((byte) 0x80); + + // Encode length. + int b1 = dataLen >>> 28 & 0x7F; + int b2 = dataLen >>> 14 & 0x7F; + int b3 = dataLen >>> 7 & 0x7F; + int b4 = dataLen & 0x7F; + if (b1 == 0) { + if (b2 == 0) { + if (b3 == 0) { + encoded.writeByte(b4); + } else { + encoded.writeByte(b3 | 0x80); + encoded.writeByte(b4); + } + } else { + encoded.writeByte(b2 | 0x80); + encoded.writeByte(b3 | 0x80); + encoded.writeByte(b4); + } + } else { + encoded.writeByte(b1 | 0x80); + encoded.writeByte(b2 | 0x80); + encoded.writeByte(b3 | 0x80); + encoded.writeByte(b4); + } + + // Encode binary data. + encoded.writeBytes(data, data.readerIndex(), dataLen); + return encoded; + } + } + return msg; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java new file mode 100644 index 0000000000..2b76491502 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -0,0 +1,370 @@ +// (BSD License: http://www.opensource.org/licenses/bsd-license) +// +// Copyright (c) 2011, Joe Walnes and contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the +// following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of the Webbit nor the names of +// its contributors may be used to endorse or promote products +// derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.CorruptedFrameException; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.replay.ReplayingDecoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Decodes a web socket frame from wire protocol version 8 format. This code was + * forked from webbit and modified. + * + * @author Aslak Hellesøy + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameDecoder extends ReplayingDecoder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameDecoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private UTF8Output fragmentedFramesText = null; + private int fragmentedFramesCount = 0; + + private boolean frameFinalFlag; + private int frameRsv; + private int frameOpcode; + private long framePayloadLength; + private ChannelBuffer framePayload = null; + private int framePayloadBytesRead = 0; + private ChannelBuffer maskingKey; + + private boolean allowExtensions = false; + private boolean maskedPayload = false; + private boolean receivedClosingHandshake = false; + + public static enum State { + FRAME_START, MASKING_KEY, PAYLOAD, CORRUPT + } + + /** + * Constructor + * + * @param maskedPayload + * Web socket servers must set this to true processed incoming + * masked payload. Client implementations must set this to false. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + */ + public WebSocket08FrameDecoder(boolean maskedPayload, boolean allowExtensions) { + super(State.FRAME_START); + this.maskedPayload = maskedPayload; + this.allowExtensions = allowExtensions; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) + throws Exception { + + // Discard all data received if closing handshake was received before. + if (receivedClosingHandshake) { + buffer.skipBytes(actualReadableBytes()); + return null; + } + + switch (state) { + case FRAME_START: + framePayloadBytesRead = 0; + framePayloadLength = -1; + framePayload = null; + + // FIN, RSV, OPCODE + byte b = buffer.readByte(); + frameFinalFlag = (b & 0x80) != 0; + frameRsv = (b & 0x70) >> 4; + frameOpcode = (b & 0x0F); + + logger.debug("Decoding WebSocket Frame opCode=" + frameOpcode); + + // MASK, PAYLOAD LEN 1 + b = buffer.readByte(); + boolean frameMasked = (b & 0x80) != 0; + int framePayloadLen1 = (b & 0x7F); + + if (frameRsv != 0 && !this.allowExtensions) { + protocolViolation(channel, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); + return null; + } + + if (this.maskedPayload && !frameMasked) { + protocolViolation(channel, "unmasked client to server frame"); + return null; + } + if (frameOpcode > 7) { // control frame (have MSB in opcode set) + + // control frames MUST NOT be fragmented + if (!frameFinalFlag) { + protocolViolation(channel, "fragmented control frame"); + return null; + } + + // control frames MUST have payload 125 octets or less + if (framePayloadLen1 > 125) { + protocolViolation(channel, "control frame with payload length > 125 octets"); + return null; + } + + // check for reserved control frame opcodes + if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { + protocolViolation(channel, "control frame using reserved opcode " + frameOpcode); + return null; + } + + // close frame : if there is a body, the first two bytes of the + // body MUST be a 2-byte + // unsigned integer (in network byte order) representing a + // status code + if (frameOpcode == 8 && framePayloadLen1 == 1) { + protocolViolation(channel, "received close control frame with payload len 1"); + return null; + } + } else { // data frame + // check for reserved data frame opcodes + if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { + protocolViolation(channel, "data frame using reserved opcode " + frameOpcode); + return null; + } + + // check opcode vs message fragmentation state 1/2 + if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { + protocolViolation(channel, "received continuation data frame outside fragmented message"); + return null; + } + + // check opcode vs message fragmentation state 2/2 + if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { + protocolViolation(channel, "received non-continuation data frame while inside fragmented message"); + return null; + } + } + + if (framePayloadLen1 == 126) { + framePayloadLength = buffer.readUnsignedShort(); + if (framePayloadLength < 126) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else if (framePayloadLen1 == 127) { + framePayloadLength = buffer.readLong(); + // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe + // just check if it's negative? + + if (framePayloadLength < 65536) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else { + framePayloadLength = framePayloadLen1; + } + + //logger.debug("Frame length=" + framePayloadLength); + checkpoint(State.MASKING_KEY); + case MASKING_KEY: + if (this.maskedPayload){ + maskingKey = buffer.readBytes(4); + } + checkpoint(State.PAYLOAD); + 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 payloadBuffer = null; + + int willHaveReadByteCount = framePayloadBytesRead + rbytes; + //logger.debug("Frame rbytes=" + rbytes + " willHaveReadByteCount=" + willHaveReadByteCount + " framePayloadLength=" + framePayloadLength); + if (willHaveReadByteCount == framePayloadLength) { + // We have all our content so proceed to process + payloadBuffer = buffer.readBytes(rbytes); + } else if (willHaveReadByteCount < framePayloadLength) { + // We don't have all our content so accumulate payload. + // Returning null means we will get called back + payloadBuffer = buffer.readBytes(rbytes); + if (framePayload == null) { + framePayload = channel.getConfig().getBufferFactory().getBuffer(toFrameLength(framePayloadLength)); + } + framePayload.writeBytes(payloadBuffer); + framePayloadBytesRead = framePayloadBytesRead + rbytes; + + // Return null to wait for more bytes to arrive + return null; + } else if (willHaveReadByteCount > framePayloadLength) { + // We have more than what we need so read up to the end of frame + // Leave the remainder in the buffer for next frame + payloadBuffer = buffer.readBytes(toFrameLength(framePayloadLength - framePayloadBytesRead)); + } + + // Now we have all the data, the next checkpoint must be the next + // frame + checkpoint(State.FRAME_START); + + // Take the data that we have in this packet + if (framePayload == null) { + framePayload = payloadBuffer; + } else { + framePayload.writeBytes(payloadBuffer); + } + + // Unmask data if needed + if (this.maskedPayload) { + unmask(framePayload); + } + + // Processing for fragmented messages + String aggregatedText = null; + if (frameFinalFlag) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (frameOpcode != OPCODE_PING) { + fragmentedFramesCount = 0; + + // Check text for UTF8 correctness + if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) { + // Check UTF-8 correctness for this payload + checkUTF8String(channel, framePayload.array()); + + // This does a second check to make sure UTF-8 + // correctness for entire text message + aggregatedText = fragmentedFramesText.toString(); + + fragmentedFramesText = null; + } + } + } else { + // Not final frame so we can expect more frames in the + // fragmented sequence + if (fragmentedFramesCount == 0) { + // First text or binary frame for a fragmented set + fragmentedFramesText = null; + if (frameOpcode == OPCODE_TEXT) { + checkUTF8String(channel, framePayload.array()); + } + } else { + // Subsequent frames - only check if init frame is text + if (fragmentedFramesText != null) { + checkUTF8String(channel, framePayload.array()); + } + } + + // Increment counter + fragmentedFramesCount++; + } + + // Return the frame + if (frameOpcode == OPCODE_TEXT) { + return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_BINARY) { + return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_PING) { + return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_PONG) { + return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_CONT) { + return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText); + } else if (frameOpcode == OPCODE_CLOSE) { + this.receivedClosingHandshake = true; + return new CloseWebSocketFrame(frameFinalFlag, frameRsv); + } else { + throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode); + } + case CORRUPT: + // If we don't keep reading Netty will throw an exception saying + // we can't return null if no bytes read and state not changed. + buffer.readByte(); + return null; + default: + throw new Error("Shouldn't reach here."); + } + } + + private void unmask(ChannelBuffer frame) { + byte[] bytes = frame.array(); + for (int i = 0; i < bytes.length; i++) { + frame.setByte(i, frame.getByte(i) ^ maskingKey.getByte(i % 4)); + } + } + + private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException { + checkpoint(State.CORRUPT); + if (channel.isConnected()) { + channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + channel.close().awaitUninterruptibly(); + } + throw new CorruptedFrameException(reason); + } + + private int toFrameLength(long l) throws TooLongFrameException { + if (l > Integer.MAX_VALUE) { + throw new TooLongFrameException("Length:" + l); + } else { + return (int) l; + } + } + + private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException { + try { + // StringBuilder sb = new StringBuilder("UTF8 " + bytes.length + + // " bytes: "); + // for (byte b : bytes) { + // sb.append(Integer.toHexString(b)).append(" "); + // } + // logger.debug(sb.toString()); + + if (fragmentedFramesText == null) { + fragmentedFramesText = new UTF8Output(bytes); + } else { + fragmentedFramesText.write(bytes); + } + } catch (UTF8Exception ex) { + protocolViolation(channel, "invalid UTF-8 bytes"); + } + } +} \ 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..10005c55ab --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java @@ -0,0 +1,174 @@ +// (BSD License: http://www.opensource.org/licenses/bsd-license) +// +// Copyright (c) 2011, Joe Walnes and contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the +// following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of the Webbit nor the names of +// its contributors may be used to endorse or promote products +// derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package org.jboss.netty.handler.codec.http.websocketx; + +import java.nio.ByteBuffer; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + *

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

+ * + * @author Aslak Hellesøy + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameEncoder extends OneToOneEncoder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameEncoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private boolean maskPayload = false; + + /** + * Constructor + * + * @param maskPayload + * Web socket clients must set this to true to mask payload. + * Server implementations must set this to false. + */ + public WebSocket08FrameEncoder(boolean maskPayload) { + this.maskPayload = maskPayload; + } + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception { + + byte[] mask = null; + + if (msg instanceof WebSocketFrame) { + WebSocketFrame frame = (WebSocketFrame) msg; + ChannelBuffer data = frame.getBinaryData(); + if (data == null) { + data = ChannelBuffers.EMPTY_BUFFER; + } + + byte opcode; + if (frame instanceof TextWebSocketFrame) { + opcode = OPCODE_TEXT; + } else if (frame instanceof PingWebSocketFrame) { + opcode = OPCODE_PING; + } else if (frame instanceof PongWebSocketFrame) { + opcode = OPCODE_PONG; + } else if (frame instanceof CloseWebSocketFrame) { + opcode = OPCODE_CLOSE; + } else if (frame instanceof BinaryWebSocketFrame) { + opcode = OPCODE_BINARY; + } else if (frame instanceof ContinuationWebSocketFrame) { + opcode = OPCODE_CONT; + } else { + throw new UnsupportedOperationException("Cannot encode frame of type: " + frame.getClass().getName()); + } + + int length = data.readableBytes(); + + logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length); + + int b0 = 0; + if (frame.isFinalFragment()) { + b0 |= (1 << 7); + } + b0 |= (frame.getRsv() % 8) << 4; + b0 |= opcode % 128; + + ChannelBuffer header; + ChannelBuffer body; + + if (opcode == OPCODE_PING && length > 125) { + throw new TooLongFrameException("invalid payload for PING (payload length must be <= 125, was " + + length); + } + + int maskLength = this.maskPayload ? 4 : 0; + if (length <= 125) { + header = ChannelBuffers.buffer(2 + maskLength); + header.writeByte(b0); + byte b = (byte) (this.maskPayload ? (0x80 | (byte) length) : (byte) length); + header.writeByte(b); + } else if (length <= 0xFFFF) { + header = ChannelBuffers.buffer(4 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFE) : 126); + header.writeByte((length >>> 8) & 0xFF); + header.writeByte((length) & 0xFF); + } else { + header = ChannelBuffers.buffer(10 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFF) : 127); + header.writeLong(length); + } + + // Write payload + if (this.maskPayload) { + Integer random = (int) (Math.random() * Integer.MAX_VALUE); + mask = ByteBuffer.allocate(4).putInt(random).array(); + header.writeBytes(mask); + + body = ChannelBuffers.buffer(length); + int counter = 0; + while (data.readableBytes() > 0) { + byte byteData = data.readByte(); + body.writeByte(byteData ^ mask[+counter++ % 4]); + } + } else { + body = data; + } + return ChannelBuffers.wrappedBuffer(header, body); + } + + // If not websocket, then just return the message + return msg; + } + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java new file mode 100644 index 0000000000..4ee63b0bc6 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java @@ -0,0 +1,211 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.base64.Base64; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.util.CharsetUtil; + +/** + * Base class for web socket client handshake implementations + * + * @author Vibul Imtarnasan + */ +public abstract class WebSocketClientHandshaker { + + private URI webSocketURL; + + private WebSocketSpecificationVersion version = WebSocketSpecificationVersion.UNKNOWN; + + private boolean openingHandshakeCompleted = false; + + private String subProtocolRequest = null; + + private String subProtocolResponse = null; + + /** + * + * @param webSocketURL + * @param version + * @param subProtocol + */ + public WebSocketClientHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol) { + this.webSocketURL = webSocketURL; + this.version = version; + this.subProtocolRequest = subProtocol; + } + + /** + * Returns the URI to the web socket. e.g. "ws://myhost.com/path" + */ + public URI getWebSocketURL() { + return webSocketURL; + } + + protected void setWebSocketURL(URI webSocketURL) { + this.webSocketURL = webSocketURL; + } + + /** + * Version of the web socket specification that is being used + */ + public WebSocketSpecificationVersion getVersion() { + return version; + } + + protected void setVersion(WebSocketSpecificationVersion version) { + this.version = version; + } + + /** + * Flag to indicate if the opening handshake is complete + */ + public boolean isOpeningHandshakeCompleted() { + return openingHandshakeCompleted; + } + + protected void setOpenningHandshakeCompleted(boolean openningHandshakeCompleted) { + this.openingHandshakeCompleted = openningHandshakeCompleted; + } + + /** + * Returns the sub protocol request sent to the server as specified in the + * constructor + */ + public String getSubProtocolRequest() { + return subProtocolRequest; + } + + protected void setSubProtocolRequest(String subProtocolRequest) { + this.subProtocolRequest = subProtocolRequest; + } + + /** + * Returns the sub protocol response and sent by the server. Only available + * after end of handshake. + */ + public String getSubProtocolResponse() { + return subProtocolResponse; + } + + protected void setSubProtocolResponse(String subProtocolResponse) { + this.subProtocolResponse = subProtocolResponse; + } + + /** + * Performs the opening handshake + * + * @param ctx + * Channel context + * @param channel + * Channel + */ + public abstract void beginOpeningHandshake(ChannelHandlerContext ctx, Channel channel); + + /** + * Performs the closing handshake + * + * @param ctx + * Channel context + * @param response + * HTTP response containing the closing handshake details + */ + public abstract void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response) + throws WebSocketHandshakeException; + + /** + * Performs an MD5 hash + * + * @param bytes + * Data to hash + * @return Hashed data + */ + protected byte[] md5(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 not supported on this platform"); + } + } + + /** + * Performs an SHA-1 hash + * + * @param bytes + * Data to hash + * @return Hashed data + */ + protected byte[] sha1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-1 not supported on this platform"); + } + } + + /** + * Base 64 encoding + * + * @param bytes + * Bytes to encode + * @return encoded string + */ + protected String base64Encode(byte[] bytes) { + ChannelBuffer hashed = ChannelBuffers.wrappedBuffer(bytes); + return Base64.encode(hashed).toString(CharsetUtil.UTF_8); + } + + /** + * Creates some random bytes + * + * @param size + * Number of random bytes to create + * @return random bytes + */ + protected byte[] createRandomBytes(int size) { + byte[] bytes = new byte[size]; + + for (int i = 0; i < size; i++) { + bytes[i] = (byte) createRandomNumber(0, 255); + } + + return bytes; + } + + /** + * Generates a random number + * + * @param min + * Minimum value + * @param max + * Maximum value + * @return Random number + */ + protected int createRandomNumber(int min, int max) { + 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..5a6db45850 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshaker10.java @@ -0,0 +1,194 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import java.net.URI; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.http.DefaultHttpRequest; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpHeaders.Values; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + *

+ * 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; + + private boolean allowExtensions = false; + + /** + * Constructor specifying the destination web socket location and version to + * initiate + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the + * server + * @param subProtocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + */ + public WebSocketClientHandshaker10(URI webSocketURL, WebSocketSpecificationVersion version, String subProtocol, + boolean allowExtensions) { + super(webSocketURL, version, subProtocol); + this.allowExtensions = allowExtensions; + 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 = base64Encode(nonce); + + String acceptSeed = key + MAGIC_GUID; + byte[] sha1 = sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); + this.expectedChallengeResponseString = base64Encode(sha1); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("HyBi10 Client Handshake key: %s. Expected response: %s.", key, + this.expectedChallengeResponseString)); + } + + // Format request + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + request.addHeader(Names.UPGRADE, Values.WEBSOCKET.toLowerCase()); + request.addHeader(Names.CONNECTION, Values.UPGRADE); + request.addHeader(Names.SEC_WEBSOCKET_KEY, key); + request.addHeader(Names.HOST, wsURL.getHost()); + request.addHeader(Names.ORIGIN, "http://" + wsURL.getHost()); + if (protocol != null && !protocol.equals("")) { + request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol); + } + request.addHeader(Names.SEC_WEBSOCKET_VERSION, "8"); + + channel.write(request); + + ctx.getPipeline().replace("encoder", "ws-encoder", new WebSocket08FrameEncoder(true)); + return; + } + + /** + *

+ * Process server response: + *

+ * + *
+	 * HTTP/1.1 101 Switching Protocols
+	 * Upgrade: websocket
+	 * Connection: Upgrade
+	 * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+	 * Sec-WebSocket-Protocol: chat
+	 * 
+ * + * @param ctx + * Channel context + * @param response + * HTTP response returned from the server for the request sent by + * beginOpeningHandshake00(). + * @throws WebSocketHandshakeException + */ + @Override + public void endOpeningHandshake(ChannelHandlerContext ctx, HttpResponse response) + throws WebSocketHandshakeException { + final HttpResponseStatus status = new HttpResponseStatus(101, "Switching Protocols"); + + if (!response.getStatus().equals(status)) { + throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus()); + } + + String upgrade = response.getHeader(Names.UPGRADE); + if (upgrade == null || !upgrade.equals(Values.WEBSOCKET.toLowerCase())) { + throw new WebSocketHandshakeException("Invalid handshake response upgrade: " + + response.getHeader(Names.UPGRADE)); + } + + String connection = response.getHeader(Names.CONNECTION); + if (connection == null || !connection.equals(Values.UPGRADE)) { + throw new WebSocketHandshakeException("Invalid handshake response connection: " + + response.getHeader(Names.CONNECTION)); + } + + String accept = response.getHeader(Names.SEC_WEBSOCKET_ACCEPT); + if (accept == null || !accept.equals(this.expectedChallengeResponseString)) { + throw new WebSocketHandshakeException(String.format("Invalid challenge. Actual: %s. Expected: %s", accept, + this.expectedChallengeResponseString)); + } + + ctx.getPipeline().replace("decoder", "ws-decoder", new WebSocket08FrameDecoder(false, this.allowExtensions)); + + this.setOpenningHandshakeCompleted(true); + return; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java new file mode 100644 index 0000000000..341a20e676 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import java.net.URI; + +/** + * Instances the appropriate handshake class to use for clients + * + * @author The Netty Project + */ +public class WebSocketClientHandshakerFactory { + + /** + * Instances a new handshaker + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the + * server + * @param subProtocol + * Sub protocol request sent to the server. Null if no + * sub-protocol support is required. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @throws WebSocketHandshakeException + */ + public WebSocketClientHandshaker newHandshaker(URI webSocketURL, WebSocketSpecificationVersion version, + String subProtocol, boolean allowExtensions) throws WebSocketHandshakeException { + if (version == WebSocketSpecificationVersion.V10) { + return new WebSocketClientHandshaker10(webSocketURL, version, subProtocol, allowExtensions); + } + if (version == WebSocketSpecificationVersion.V00) { + return new WebSocketClientHandshaker00(webSocketURL, version, subProtocol); + } + + throw new WebSocketHandshakeException("Protocol version " + version.toString() + " not supported."); + + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java new file mode 100644 index 0000000000..2421a60850 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrame.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * Base class for web socket frames + * + * @author The Netty Project + */ +public abstract class WebSocketFrame { + + /** + * Flag to indicate if this frame is the final fragment in a message. The + * first fragment (frame) may also be the final fragment. + */ + private boolean finalFragment = true; + + /** + * RSV1, RSV2, RSV3 used for extensions + */ + private int rsv = 0; + + /** + * Contents of this frame + */ + private ChannelBuffer binaryData; + + /** + * Returns binary data + */ + public ChannelBuffer getBinaryData() { + return binaryData; + } + + /** + * Sets the binary data for this frame + */ + public void setBinaryData(ChannelBuffer binaryData) { + this.binaryData = binaryData; + } + + /** + * Flag to indicate if this frame is the final fragment in a message. The + * first fragment (frame) may also be the final fragment. + */ + public boolean isFinalFragment() { + return finalFragment; + } + + public void setFinalFragment(boolean finalFragment) { + this.finalFragment = finalFragment; + } + + /** + * Bits used for extensions to the standard. + */ + public int getRsv() { + return rsv; + } + + public void setRsv(int rsv) { + this.rsv = rsv; + } + + + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java new file mode 100644 index 0000000000..8e80602045 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameType.java @@ -0,0 +1,25 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Type of web socket frames + * + * @author The Netty Project + */ +public enum WebSocketFrameType { + TEXT, BINARY, PING, PONG, CLOSE, CONTINUATION +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java new file mode 100644 index 0000000000..db9f768615 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketHandshakeException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Exception during handshaking process + * + * @author The Netty Project + */ +public class WebSocketHandshakeException extends Exception { + + private static final long serialVersionUID = 1L; + + public WebSocketHandshakeException(String s) { + super(s); + } + + public WebSocketHandshakeException(String s, Throwable throwable) { + super(s, throwable); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java new file mode 100644 index 0000000000..29656ae060 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java @@ -0,0 +1,191 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.base64.Base64; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.util.CharsetUtil; + +/** + * Base class for server side web socket opening and closing handshakes + * + * @author The Netty Project + */ +public abstract class WebSocketServerHandshaker { + + private String webSocketURL; + + private String subProtocols; + + private String[] subProtocolsArray = null; + + private WebSocketSpecificationVersion version = WebSocketSpecificationVersion.UNKNOWN; + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subProtocols + * CSV of supported protocols. Null if sub protocols not + * supported. + */ + public WebSocketServerHandshaker(String webSocketURL, String subProtocols) { + this.webSocketURL = webSocketURL; + this.subProtocols = subProtocols; + + if (this.subProtocols != null) { + this.subProtocolsArray = subProtocols.split(","); + for (int i = 0; i < this.subProtocolsArray.length; i++) { + this.subProtocolsArray[i] = this.subProtocolsArray[i].trim(); + } + } + return; + } + + /** + * Returns the URL of the web socket + */ + public String getWebSocketURL() { + return webSocketURL; + } + + public void setWebSocketURL(String webSocketURL) { + this.webSocketURL = webSocketURL; + } + + /** + * Returns the CSV of supported sub protocols + */ + public String getSubProtocols() { + return subProtocols; + } + + public void setSubProtocols(String subProtocols) { + this.subProtocols = subProtocols; + } + + /** + * Returns the version of the specification being supported + */ + public WebSocketSpecificationVersion getVersion() { + return version; + } + + public void setVersion(WebSocketSpecificationVersion version) { + this.version = version; + } + + /** + * Performs the opening handshake + * + * @param ctx + * Context + * @param req + * HTTP Request + * @throws NoSuchAlgorithmException + */ + public abstract void executeOpeningHandshake(ChannelHandlerContext ctx, HttpRequest req); + + /** + * Performs the closing handshake + * + * @param ctx + * Context + * @param frame + * Closing Frame that was received + */ + public abstract void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame); + + /** + * Performs an MD5 hash + * + * @param bytes + * Data to hash + * @return Hashed data + */ + protected byte[] md5(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 not supported on this platform"); + } + } + + /** + * SHA-1 hashing. Instance this we think it is not thread safe + * + * @param bytes + * byte to hash + * @return hashed + */ + protected byte[] sha1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-1 not supported on this platform"); + } + } + + /** + * Base 64 encoding + * + * @param bytes + * Bytes to encode + * @return encoded string + */ + protected String base64Encode(byte[] bytes) { + ChannelBuffer hashed = ChannelBuffers.wrappedBuffer(bytes); + return Base64.encode(hashed).toString(CharsetUtil.UTF_8); + } + + /** + * Selects the first matching supported sub protocol + * + * @param requestedSubProtocol + * CSV of protocols to be supported. e.g. "chat, superchat" + * @return First matching supported sub protocol. Null if not found. + */ + protected String selectSubProtocol(String requestedSubProtocol) { + if (requestedSubProtocol == null || this.subProtocolsArray == null) { + return null; + } + + String[] requesteSubProtocolsArray = requestedSubProtocol.split(","); + for (int i = 0; i < requesteSubProtocolsArray.length; i++) { + String requesteSubProtocol = requesteSubProtocolsArray[i].trim(); + + for (String supportedSubProtocol : this.subProtocolsArray) { + if (requesteSubProtocol.equals(supportedSubProtocol)) { + return requesteSubProtocol; + } + } + } + + // No match found + return null; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java new file mode 100644 index 0000000000..cfbbcd3f0a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java @@ -0,0 +1,205 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY1; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY2; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_LOCATION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_LOCATION; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_ORIGIN; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_PROTOCOL; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET; +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpHeaders.Values; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + *

+ * 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..84467ab9e5 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshaker10.java @@ -0,0 +1,167 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.WEBSOCKET; +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.security.NoSuchAlgorithmException; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + *

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

+ * + * @author The Netty Project + */ +public class WebSocketServerHandshaker10 extends WebSocketServerHandshaker { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker10.class); + + public static final String WEBSOCKET_08_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private boolean allowExtensions = false; + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subProtocols + * CSV of supported protocols + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketServerHandshaker10(String webSocketURL, String subProtocols, boolean allowExtensions) { + super(webSocketURL, subProtocols); + this.allowExtensions = allowExtensions; + 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 = base64Encode(sha1); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("HyBi10 Server Handshake key: %s. Response: %s.", key, accept)); + } + + res.setStatus(new HttpResponseStatus(101, "Switching Protocols")); + res.addHeader(Names.UPGRADE, WEBSOCKET.toLowerCase()); + res.addHeader(Names.CONNECTION, Names.UPGRADE); + res.addHeader(Names.SEC_WEBSOCKET_ACCEPT, accept); + String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL); + if (protocol != null) { + res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, this.selectSubProtocol(protocol)); + } + + ctx.getChannel().write(res); + + // Upgrade the connection and send the handshake response. + ChannelPipeline p = ctx.getChannel().getPipeline(); + p.remove("aggregator"); + p.replace("decoder", "wsdecoder", new WebSocket08FrameDecoder(true, this.allowExtensions)); + p.replace("encoder", "wsencoder", new WebSocket08FrameEncoder(false)); + + return; + } + + /** + * Echo back the closing frame + * + * @param ctx + * Channel context + * @param frame + * Web Socket frame that was received + */ + @Override + public void executeClosingHandshake(ChannelHandlerContext ctx, CloseWebSocketFrame frame) { + ctx.getChannel().write(frame); + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java new file mode 100644 index 0000000000..ed5ff2ae52 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java @@ -0,0 +1,108 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; + +/** + * Instances the appropriate handshake class to use for clients + * + * @author The Netty Project + */ +public class WebSocketServerHandshakerFactory { + + private String webSocketURL; + + private String subProtocols; + + private boolean allowExtensions = false; + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subProtocols + * CSV of supported protocols. Null if sub protocols not + * supported. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web + * socket frame + */ + public WebSocketServerHandshakerFactory(String webSocketURL, String subProtocols, boolean allowExtensions) { + this.webSocketURL = webSocketURL; + this.subProtocols = subProtocols; + this.allowExtensions = allowExtensions; + return; + } + + /** + * Instances a new handshaker + * + * @param webSocketURL + * URL for web socket communications. e.g + * "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the + * server + * @param subProtocol + * Sub protocol request sent to the server. Null if no + * sub-protocol support is required. + * @return A new WebSocketServerHandshaker for the requested web socket + * version. Null if web socket version is not supported. + */ + public WebSocketServerHandshaker newHandshaker(ChannelHandlerContext ctx, HttpRequest req) { + + String version = req.getHeader(Names.SEC_WEBSOCKET_VERSION); + if (version != null) { + if (version.equals("8")) { + // Version 8 of the wire protocol - assume version 10 of the + // specification. + return new WebSocketServerHandshaker10(webSocketURL, subProtocols, this.allowExtensions); + } else { + return null; + } + } else { + // Assume version 00 where version header was not specified + return new WebSocketServerHandshaker00(webSocketURL, subProtocols); + } + } + + /** + * Return that we need cannot not support the web socket version + * + * @param ctx + * Context + */ + public void sendUnsupportedWebSocketVersionResponse(ChannelHandlerContext ctx) { + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, + "Switching Protocols")); + res.setStatus(HttpResponseStatus.UPGRADE_REQUIRED); + res.setHeader(Names.SEC_WEBSOCKET_VERSION, "8"); + ctx.getChannel().write(res); + return; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java new file mode 100644 index 0000000000..c83779a7bf --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketSpecificationVersion.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + *

+ * 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; + diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/CIDR4.java b/src/main/java/org/jboss/netty/handler/ipfilter/CIDR4.java index 80b42fd90f..ffebaeae34 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/CIDR4.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/CIDR4.java @@ -71,7 +71,7 @@ public class CIDR4 extends CIDR } @Override -public int compareTo(CIDR arg) + public int compareTo(CIDR arg) { if (arg instanceof CIDR6) { diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/CIDR6.java b/src/main/java/org/jboss/netty/handler/ipfilter/CIDR6.java index 160a86e43e..93c2c64a90 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/CIDR6.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/CIDR6.java @@ -78,7 +78,7 @@ public class CIDR6 extends CIDR } @Override -public int compareTo(CIDR arg) + public int compareTo(CIDR arg) { if (arg instanceof CIDR4) { diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/IpFilteringHandlerImpl.java b/src/main/java/org/jboss/netty/handler/ipfilter/IpFilteringHandlerImpl.java index b0cb22c4a8..1768321f1a 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/IpFilteringHandlerImpl.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/IpFilteringHandlerImpl.java @@ -113,7 +113,7 @@ public abstract class IpFilteringHandlerImpl implements ChannelUpstreamHandler, * @see org.jboss.netty.channel.ChannelUpstreamHandler#handleUpstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override -public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception + public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { if (e instanceof ChannelStateEvent) { @@ -189,7 +189,7 @@ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exc * @see org.jboss.netty.handler.ipfilter.IpFilteringHandler#setIpFilterListener(org.jboss.netty.handler.ipfilter.IpFilterListener) */ @Override -public void setIpFilterListener(IpFilterListener listener) + public void setIpFilterListener(IpFilterListener listener) { this.listener = listener; } @@ -198,7 +198,7 @@ public void setIpFilterListener(IpFilterListener listener) * @see org.jboss.netty.handler.ipfilter.IpFilteringHandler#removeIpFilterListener() */ @Override -public void removeIpFilterListener() + public void removeIpFilterListener() { this.listener = null; diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnet.java b/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnet.java index 85d4be9079..4f15c1fbb5 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnet.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnet.java @@ -159,7 +159,7 @@ public boolean contains(InetAddress inetAddress) * Compare two IpSubnet */ @Override -public int compareTo(IpSubnet o) + public int compareTo(IpSubnet o) { return cidr.toString().compareTo(o.cidr.toString()); } diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnetFilterRule.java b/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnetFilterRule.java index d4009c09b3..e197ae1fff 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnetFilterRule.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/IpSubnetFilterRule.java @@ -79,13 +79,13 @@ public class IpSubnetFilterRule extends IpSubnet implements IpFilterRule } @Override -public boolean isAllowRule() + public boolean isAllowRule() { return isAllowRule; } @Override -public boolean isDenyRule() + public boolean isDenyRule() { return !isAllowRule; } diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/IpV4Subnet.java b/src/main/java/org/jboss/netty/handler/ipfilter/IpV4Subnet.java index 80374a9eb2..18ae5bda36 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/IpV4Subnet.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/IpV4Subnet.java @@ -278,7 +278,7 @@ public boolean contains(InetAddress inetAddress1) * Compare two IpV4Subnet */ @Override -public int compareTo(IpV4Subnet o) + public int compareTo(IpV4Subnet o) { if (o.subnet == subnet && o.cidrMask == cidrMask) { diff --git a/src/main/java/org/jboss/netty/handler/ipfilter/IpV4SubnetFilterRule.java b/src/main/java/org/jboss/netty/handler/ipfilter/IpV4SubnetFilterRule.java index 106e9c72ac..5705d97dcb 100644 --- a/src/main/java/org/jboss/netty/handler/ipfilter/IpV4SubnetFilterRule.java +++ b/src/main/java/org/jboss/netty/handler/ipfilter/IpV4SubnetFilterRule.java @@ -74,13 +74,13 @@ public class IpV4SubnetFilterRule extends IpV4Subnet implements IpFilterRule } @Override -public boolean isAllowRule() + public boolean isAllowRule() { return isAllowRule; } @Override -public boolean isDenyRule() + public boolean isDenyRule() { return !isAllowRule; } diff --git a/src/main/java/org/jboss/netty/handler/ssl/SslHandler.java b/src/main/java/org/jboss/netty/handler/ssl/SslHandler.java index a636ca8131..bf6926a710 100644 --- a/src/main/java/org/jboss/netty/handler/ssl/SslHandler.java +++ b/src/main/java/org/jboss/netty/handler/ssl/SslHandler.java @@ -69,17 +69,22 @@ import org.jboss.netty.util.internal.NonReentrantLock; * *

Renegotiation

*

- * TLS renegotiation has been disabled by default due to a known security issue, - * CVE-2009-3555. - * You can re-enable renegotiation by calling {@link #setEnableRenegotiation(boolean)} - * with {@code true} at your own risk. - *

- * If {@link #isEnableRenegotiation() enableRenegotiation} is {@code true} and - * the initial handshake has been done successfully, you can call + * If {@link #isEnableRenegotiation() enableRenegotiation} is {@code true} + * (default) and the initial handshake has been done successfully, you can call * {@link #handshake()} to trigger the renegotiation. *

* If {@link #isEnableRenegotiation() enableRenegotiation} is {@code false}, * an attempt to trigger renegotiation will result in the connection closure. + *

+ * Please note that TLS renegotiation had a security issue before. If your + * runtime environment did not fix it, please make sure to disable TLS + * renegotiation by calling {@link #setEnableRenegotiation(boolean)} with + * {@code false}. For more information, please refer to the following documents: + *

* *

Closing the session

*

@@ -173,7 +178,7 @@ public class SslHandler extends FrameDecoder private final Executor delegatedTaskExecutor; private final boolean startTls; - private volatile boolean enableRenegotiation; + private volatile boolean enableRenegotiation = true; final Object handshakeLock = new Object(); private boolean handshaking; @@ -866,46 +871,50 @@ public class SslHandler extends FrameDecoder loop: for (;;) { SSLEngineResult result; + boolean needsHandshake = false; synchronized (handshakeLock) { if (!handshaken && !handshaking && !engine.getUseClientMode() && !engine.isInboundDone() && !engine.isOutboundDone()) { - handshake(); - } - - try { - result = engine.unwrap(inNetBuf, outAppBuf); - } catch (SSLException e) { - throw e; - } - - final HandshakeStatus handshakeStatus = result.getHandshakeStatus(); - handleRenegotiation(handshakeStatus); - switch (handshakeStatus) { - case NEED_UNWRAP: - if (inNetBuf.hasRemaining() && !engine.isInboundDone()) { - break; - } else { - break loop; - } - case NEED_WRAP: - wrapNonAppData(ctx, channel); - break; - case NEED_TASK: - runDelegatedTasks(); - break; - case FINISHED: - setHandshakeSuccess(channel); - needsWrap = true; - break loop; - case NOT_HANDSHAKING: - needsWrap = true; - break loop; - default: - throw new IllegalStateException( - "Unknown handshake status: " + handshakeStatus); + needsHandshake = true; + } } + if (needsHandshake) { + handshake(); + } + + synchronized (handshakeLock) { + result = engine.unwrap(inNetBuf, outAppBuf); + } + + final HandshakeStatus handshakeStatus = result.getHandshakeStatus(); + handleRenegotiation(handshakeStatus); + switch (handshakeStatus) { + case NEED_UNWRAP: + if (inNetBuf.hasRemaining() && !engine.isInboundDone()) { + break; + } else { + break loop; + } + case NEED_WRAP: + wrapNonAppData(ctx, channel); + break; + case NEED_TASK: + runDelegatedTasks(); + break; + case FINISHED: + setHandshakeSuccess(channel); + needsWrap = true; + break loop; + case NOT_HANDSHAKING: + needsWrap = true; + break loop; + default: + throw new IllegalStateException( + "Unknown handshake status: " + handshakeStatus); + } + } if (needsWrap) { @@ -1037,29 +1046,59 @@ public class SslHandler extends FrameDecoder if (handshakeFuture == null) { handshakeFuture = future(channel); } + + // Release all resources such as internal buffers that SSLEngine + // is managing. + + engine.closeOutbound(); + + try { + engine.closeInbound(); + } catch (SSLException e) { + logger.debug( + "SSLEngine.closeInbound() raised an exception after " + + "a handshake failure.", e); + } } + handshakeFuture.setFailure(cause); } private void closeOutboundAndChannel( - final ChannelHandlerContext context, final ChannelStateEvent e) throws SSLException { + final ChannelHandlerContext context, final ChannelStateEvent e) { if (!e.getChannel().isConnected()) { context.sendDownstream(e); return; } - unwrap(context, e.getChannel(), ChannelBuffers.EMPTY_BUFFER, 0, 0); - if (!engine.isInboundDone()) { - if (sentCloseNotify.compareAndSet(false, true)) { - engine.closeOutbound(); - ChannelFuture closeNotifyFuture = wrapNonAppData(context, e.getChannel()); - closeNotifyFuture.addListener( - new ClosingChannelFutureListener(context, e)); - return; + boolean success = false; + try { + try { + unwrap(context, e.getChannel(), ChannelBuffers.EMPTY_BUFFER, 0, 0); + } catch (SSLException ex) { + logger.debug("Failed to unwrap before sending a close_notify message", ex); + } + + if (!engine.isInboundDone()) { + if (sentCloseNotify.compareAndSet(false, true)) { + engine.closeOutbound(); + try { + ChannelFuture closeNotifyFuture = wrapNonAppData(context, e.getChannel()); + closeNotifyFuture.addListener( + new ClosingChannelFutureListener(context, e)); + success = true; + } catch (SSLException ex) { + logger.debug("Failed to encode a close_notify message", ex); + } + } + } else { + success = true; + } + } finally { + if (!success) { + context.sendDownstream(e); } } - - context.sendDownstream(e); } private static final class PendingWrite { diff --git a/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java b/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java index 27b6cee87e..7ba9e35663 100644 --- a/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java +++ b/src/main/java/org/jboss/netty/handler/stream/ChunkedWriteHandler.java @@ -154,34 +154,36 @@ public class ChunkedWriteHandler implements ChannelUpstreamHandler, ChannelDowns private void discard(ChannelHandlerContext ctx) { ClosedChannelException cause = null; boolean fireExceptionCaught = false; - synchronized (this) { - for (;;) { - if (currentEvent == null) { - currentEvent = queue.poll(); - } - - if (currentEvent == null) { - break; - } - - MessageEvent currentEvent = this.currentEvent; - this.currentEvent = null; - - Object m = currentEvent.getMessage(); - if (m instanceof ChunkedInput) { - closeInput((ChunkedInput) m); - } - - // Trigger a ClosedChannelException - if (cause == null) { - cause = new ClosedChannelException(); - } - currentEvent.getFuture().setFailure(cause); - fireExceptionCaught = true; - - currentEvent = null; + + for (;;) { + MessageEvent currentEvent = this.currentEvent; + + if (this.currentEvent == null) { + currentEvent = queue.poll(); + } else { + this.currentEvent = null; } + + if (currentEvent == null) { + break; + } + + + Object m = currentEvent.getMessage(); + if (m instanceof ChunkedInput) { + closeInput((ChunkedInput) m); + } + + // Trigger a ClosedChannelException + if (cause == null) { + cause = new ClosedChannelException(); + } + currentEvent.getFuture().setFailure(cause); + fireExceptionCaught = true; + + currentEvent = null; } + if (fireExceptionCaught) { Channels.fireExceptionCaught(ctx.getChannel(), cause); diff --git a/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java b/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java index b7eabe2c09..9356554a48 100644 --- a/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java +++ b/src/main/java/org/jboss/netty/handler/timeout/WriteTimeoutHandler.java @@ -136,7 +136,7 @@ public class WriteTimeoutHandler extends SimpleChannelDownstreamHandler timer.stop(); } - protected long getTimeoutMillis(@SuppressWarnings("unused") MessageEvent e) { + protected long getTimeoutMillis(MessageEvent e) { return timeoutMillis; }