();
+
+ public final AtomicBoolean closing = new AtomicBoolean(false);
+ }
+
+ private static final class QueuedResponse {
+ public ChannelBuffer data;
+
+ public ChannelFuture writeFuture;
+
+ QueuedResponse(ChannelBuffer data, ChannelFuture writeFuture) {
+ this.data = data;
+ this.writeFuture = writeFuture;
+ }
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java
new file mode 100644
index 00000000000..cc29a3c2868
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.channel.socket.http;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+
+/**
+ * The interface from a HttpTunnelAcceptedChannel to the ServerMessageSwitch.
+ * This primarily exists for mock object testing purposes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+interface ServerMessageSwitchDownstreamInterface {
+
+ public void serverCloseTunnel(String tunnelId);
+
+ public void routeOutboundData(String tunnelId, ChannelBuffer data,
+ ChannelFuture writeFuture);
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java
new file mode 100644
index 00000000000..436f68ecf06
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.channel.socket.http;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.Channel;
+
+/**
+ * The interface from a TCP channel which is being used to communicate with the client
+ * end of an http tunnel and the server message switch.
+ *
+ * This primarily exists for mock testing purposes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+interface ServerMessageSwitchUpstreamInterface {
+
+ public String createTunnel(InetSocketAddress remoteAddress);
+
+ public boolean isOpenTunnel(String tunnelId);
+
+ public void clientCloseTunnel(String tunnelId);
+
+ /**
+ * Passes some received data from a client for forwarding to the server's view
+ * of the tunnel.
+ * @return the current status of the tunnel. ALIVE indicates the tunnel is still
+ * functional, CLOSED indicates it is closed and the client should be notified
+ * of this (and will be forgotten after this notification).
+ */
+ public TunnelStatus routeInboundData(String tunnelId,
+ ChannelBuffer inboundData);
+
+ public void pollOutboundData(String tunnelId, Channel responseChannel);
+
+ public static enum TunnelStatus {
+ ALIVE, CLOSED
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java b/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java
new file mode 100644
index 00000000000..9eafdf5cceb
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+/**
+ * This interface is used by the server end of an http tunnel to generate new
+ * tunnel ids for accepted client connections.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public interface TunnelIdGenerator {
+
+ /**
+ * Generates the next tunnel ID to be used, which must be unique
+ * (i.e. ensure with high probability that it will not clash with
+ * an existing tunnel ID). This method must be thread safe, and
+ * preferably lock free.
+ */
+ public String generateId();
+
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java b/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java
new file mode 100644
index 00000000000..37943a7e4a6
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.channel.socket.http;
+
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.ChildChannelStateEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+class TunnelWrappedServerChannelHandler extends SimpleChannelUpstreamHandler {
+
+ public static final String NAME = "TunnelWrappedServerChannelHandler";
+
+ private final HttpTunnelServerChannel tunnelChannel;
+
+ private final AcceptedServerChannelPipelineFactory pipelineFactory;
+
+ private final ChannelGroup allChannels;
+
+ public TunnelWrappedServerChannelHandler(
+ HttpTunnelServerChannel tunnelChannel,
+ AcceptedServerChannelPipelineFactory pipelineFactory,
+ ChannelGroup allChannels) {
+ this.tunnelChannel = tunnelChannel;
+ this.pipelineFactory = pipelineFactory;
+ this.allChannels = allChannels;
+ }
+
+ @Override
+ public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e)
+ throws Exception {
+ e.getChannel().getConfig().setPipelineFactory(pipelineFactory);
+ super.channelOpen(ctx, e);
+ }
+
+ @Override
+ public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e)
+ throws Exception {
+ Channels.fireChannelBound(tunnelChannel, (SocketAddress) e.getValue());
+ super.channelBound(ctx, e);
+ }
+
+ @Override
+ public void channelUnbound(ChannelHandlerContext ctx, ChannelStateEvent e)
+ throws Exception {
+ Channels.fireChannelUnbound(tunnelChannel);
+ super.channelUnbound(ctx, e);
+ }
+
+ @Override
+ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e)
+ throws Exception {
+ Channels.fireChannelClosed(tunnelChannel);
+ super.channelClosed(ctx, e);
+ }
+
+ @Override
+ public void childChannelOpen(ChannelHandlerContext ctx,
+ ChildChannelStateEvent e) throws Exception {
+ allChannels.add(e.getChildChannel());
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java b/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java
new file mode 100644
index 00000000000..94380e96cba
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.channel.socket.http;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelDownstreamHandler;
+
+/**
+ * Downstream handler which places an upper bound on the size of written
+ * {@link ChannelBuffer ChannelBuffers}. If a buffer
+ * is bigger than the specified upper bound, the buffer is broken up
+ * into two or more smaller pieces.
+ *
+ * This is utilised by the http tunnel to smooth out the per-byte latency,
+ * by placing an upper bound on HTTP request / response body sizes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public class WriteFragmenter extends SimpleChannelDownstreamHandler {
+
+ public static final String NAME = "writeFragmenter";
+
+ private int splitThreshold;
+
+ public WriteFragmenter(int splitThreshold) {
+ this.splitThreshold = splitThreshold;
+ }
+
+ public void setSplitThreshold(int splitThreshold) {
+ this.splitThreshold = splitThreshold;
+ }
+
+ @Override
+ public void writeRequested(ChannelHandlerContext ctx, MessageEvent e)
+ throws Exception {
+ ChannelBuffer data = (ChannelBuffer) e.getMessage();
+
+ if (data.readableBytes() <= splitThreshold) {
+ super.writeRequested(ctx, e);
+ } else {
+ List fragments =
+ WriteSplitter.split(data, splitThreshold);
+ ChannelFutureAggregator aggregator =
+ new ChannelFutureAggregator(e.getFuture());
+ for (ChannelBuffer fragment: fragments) {
+ ChannelFuture fragmentFuture =
+ Channels.future(ctx.getChannel(), true);
+ aggregator.addFuture(fragmentFuture);
+ Channels.write(ctx, fragmentFuture, fragment);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java b/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java
new file mode 100644
index 00000000000..37a9f772e5b
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jboss.netty.channel.socket.http;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+
+/**
+ * Provides functionality to split a provided ChannelBuffer into multiple fragments which fit
+ * under a specified size threshold.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public final class WriteSplitter {
+
+ public static List split(ChannelBuffer buffer,
+ int splitThreshold) {
+ int listSize = (int) ((float) buffer.readableBytes() / splitThreshold);
+ ArrayList fragmentList =
+ new ArrayList(listSize);
+
+ if (buffer.readableBytes() > splitThreshold) {
+ int slicePosition = buffer.readerIndex();
+ while (slicePosition < buffer.writerIndex()) {
+ int chunkSize =
+ Math.min(splitThreshold, buffer.writerIndex() -
+ slicePosition);
+ ChannelBuffer chunk = buffer.slice(slicePosition, chunkSize);
+ fragmentList.add(chunk);
+ slicePosition += chunkSize;
+ }
+ } else {
+ fragmentList.add(ChannelBuffers.wrappedBuffer(buffer));
+ }
+
+ return fragmentList;
+ }
+
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java
index abf7142ae34..35ffccc8ed4 100644
--- a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java
+++ b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java
@@ -144,5 +144,6 @@ public interface NioSocketChannelConfig extends SocketChannelConfig {
* will be called with the new predictor. The default factory is
* {@link AdaptiveReceiveBufferSizePredictorFactory}(64, 1024, 65536).
*/
- void setReceiveBufferSizePredictorFactory(ReceiveBufferSizePredictorFactory predictorFactory);
+ void setReceiveBufferSizePredictorFactory(
+ ReceiveBufferSizePredictorFactory predictorFactory);
}
diff --git a/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java b/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java
deleted file mode 100644
index 0561a92dc6b..00000000000
--- a/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2009 Red Hat, Inc.
- *
- * Red Hat licenses this file to you under the Apache License, version 2.0
- * (the "License"); you may not use this file except in compliance with the
- * License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package org.jboss.netty.example.http.tunnel;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.util.concurrent.Executors;
-
-import org.jboss.netty.bootstrap.ClientBootstrap;
-import org.jboss.netty.channel.ChannelFuture;
-import org.jboss.netty.channel.ChannelPipeline;
-import org.jboss.netty.channel.ChannelPipelineFactory;
-import org.jboss.netty.channel.Channels;
-import org.jboss.netty.channel.socket.http.HttpTunnelingClientSocketChannelFactory;
-import org.jboss.netty.channel.socket.oio.OioClientSocketChannelFactory;
-import org.jboss.netty.example.securechat.SecureChatSslContextFactory;
-import org.jboss.netty.handler.codec.string.StringDecoder;
-import org.jboss.netty.handler.codec.string.StringEncoder;
-import org.jboss.netty.handler.logging.LoggingHandler;
-import org.jboss.netty.logging.InternalLogLevel;
-
-/**
- * An HTTP tunneled version of the telnet client example. Please refer to the
- * API documentation of the org.jboss.netty.channel.socket.http package
- * for the detailed instruction on how to deploy the server-side HTTP tunnel in
- * your Servlet container.
- *
- * @author The Netty Project
- * @author Andy Taylor (andy.taylor@jboss.org)
- * @version $Rev$, $Date$
- */
-public class HttpTunnelingClientExample {
-
- public static void main(String[] args) throws Exception {
- if (args.length != 1) {
- System.err.println(
- "Usage: " + HttpTunnelingClientExample.class.getSimpleName() +
- " ");
- System.err.println(
- "Example: " + HttpTunnelingClientExample.class.getSimpleName() +
- " http://localhost:8080/netty-tunnel");
- return;
- }
-
- URI uri = new URI(args[0]);
- String scheme = uri.getScheme() == null? "http" : uri.getScheme();
-
- // Configure the client.
- ClientBootstrap b = new ClientBootstrap(
- new HttpTunnelingClientSocketChannelFactory(
- new OioClientSocketChannelFactory(Executors.newCachedThreadPool())));
-
- b.setPipelineFactory(new ChannelPipelineFactory() {
- @Override
- public ChannelPipeline getPipeline() throws Exception {
- return Channels.pipeline(
- new StringDecoder(),
- new StringEncoder(),
- new LoggingHandler(InternalLogLevel.INFO));
- }
- });
-
- // Set additional options required by the HTTP tunneling transport.
- b.setOption("serverName", uri.getHost());
- b.setOption("serverPath", uri.getRawPath());
-
- // Configure SSL if necessary
- if (scheme.equals("https")) {
- b.setOption("sslContext", SecureChatSslContextFactory.getClientContext());
- } else if (!scheme.equals("http")) {
- // Only HTTP and HTTPS are supported.
- System.err.println("Only HTTP(S) is supported.");
- return;
- }
-
- // Make the connection attempt.
- ChannelFuture channelFuture = b.connect(
- new InetSocketAddress(uri.getHost(), uri.getPort()));
- channelFuture.awaitUninterruptibly();
-
- // Read commands from the stdin.
- System.out.println("Enter text ('quit' to exit)");
- ChannelFuture lastWriteFuture = null;
- BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
- for (; ;) {
- String line = in.readLine();
- if (line == null || "quit".equalsIgnoreCase(line)) {
- break;
- }
-
- // Sends the received line to the server.
- lastWriteFuture = channelFuture.getChannel().write(line);
- }
-
- // Wait until all messages are flushed before closing the channel.
- if (lastWriteFuture != null) {
- lastWriteFuture.awaitUninterruptibly();
- }
-
- channelFuture.getChannel().close();
- // Wait until the connection is closed or the connection attempt fails.
- channelFuture.getChannel().getCloseFuture().awaitUninterruptibly();
-
- // Shut down all threads.
- b.releaseExternalResources();
- }
-}
diff --git a/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java b/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java
deleted file mode 100644
index cf49ae23883..00000000000
--- a/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2009 Red Hat, Inc.
- *
- * Red Hat licenses this file to you under the Apache License, version 2.0
- * (the "License"); you may not use this file except in compliance with the
- * License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package org.jboss.netty.example.http.tunnel;
-
-import org.jboss.netty.bootstrap.ServerBootstrap;
-import org.jboss.netty.channel.Channel;
-import org.jboss.netty.channel.ChannelFactory;
-import org.jboss.netty.channel.local.DefaultLocalServerChannelFactory;
-import org.jboss.netty.channel.local.LocalAddress;
-import org.jboss.netty.example.echo.EchoServerHandler;
-
-/**
- * Deploy this in JBossAS 5 or other IoC container by adding the following bean.
- *
- *
- * <bean name="org.jboss.netty.example.http.tunnel.LocalEchoServerRegistration"
- * class="org.jboss.netty.example.http.tunnel.LocalEchoServerRegistration" />
- *
- *
- * @author The Netty Project
- * @author Andy Taylor (andy.taylor@jboss.org)
- * @version $Rev$, $Date$
- */
-public class LocalEchoServerRegistration {
-
- private final ChannelFactory factory = new DefaultLocalServerChannelFactory();
- private volatile Channel serverChannel;
-
- public void start() {
- ServerBootstrap serverBootstrap = new ServerBootstrap(factory);
- EchoServerHandler handler = new EchoServerHandler();
- serverBootstrap.getPipeline().addLast("handler", handler);
-
- // Note that "myLocalServer" is the endpoint which was specified in web.xml.
- serverChannel = serverBootstrap.bind(new LocalAddress("myLocalServer"));
- }
-
- public void stop() {
- serverChannel.close();
- }
-}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/AcceptedServerChannelRequestDispatchTest.java b/src/test/java/org/jboss/netty/channel/socket/http/AcceptedServerChannelRequestDispatchTest.java
new file mode 100644
index 00000000000..f5114c42aca
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/AcceptedServerChannelRequestDispatchTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class AcceptedServerChannelRequestDispatchTest {
+
+ private static final String HOST = "test.server.com";
+
+ private static final String KNOWN_TUNNEL_ID = "1";
+
+ protected static final String UNKNOWN_TUNNEL_ID = "unknownTunnel";
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private AcceptedServerChannelRequestDispatch handler;
+
+ FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ ServerMessageSwitchUpstreamInterface messageSwitch;
+
+ @Before
+ public void setUp() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ messageSwitch =
+ mockContext.mock(ServerMessageSwitchUpstreamInterface.class);
+ handler = new AcceptedServerChannelRequestDispatch(messageSwitch);
+ pipeline.addLast(AcceptedServerChannelRequestDispatch.NAME, handler);
+ sink = new FakeChannelSink();
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress =
+ InetSocketAddress.createUnresolved("test.client.com", 51231);
+
+ mockContext.checking(new Expectations() {
+ {
+ ignoring(messageSwitch).isOpenTunnel(KNOWN_TUNNEL_ID);
+ will(returnValue(true));
+ }
+ });
+ }
+
+ @Test
+ public void testTunnelOpenRequest() {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).createTunnel(channel.remoteAddress);
+ will(returnValue(KNOWN_TUNNEL_ID));
+ }
+ });
+
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createOpenTunnelRequest(HOST));
+ assertEquals(1, sink.events.size());
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ sink.events.poll(), HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isTunnelOpenResponse(response));
+ }
+
+ @Test
+ public void testTunnelCloseRequest() {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).clientCloseTunnel(KNOWN_TUNNEL_ID);
+ }
+ });
+
+ HttpRequest request =
+ HttpTunnelMessageUtils.createCloseTunnelRequest(HOST,
+ KNOWN_TUNNEL_ID);
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent = sink.events.poll();
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(responseEvent,
+ HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isTunnelCloseResponse(response));
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ @Test
+ public void testTunnelCloseRequestWithoutTunnelIdRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createCloseTunnelRequest(HOST, null);
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testTunnelCloseRequestWithUnknownTunnelId() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createCloseTunnelRequest(HOST,
+ UNKNOWN_TUNNEL_ID);
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testSendDataRequest() {
+ final ChannelBuffer expectedData = ChannelBuffers.dynamicBuffer();
+ expectedData.writeLong(1234L);
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).routeInboundData(KNOWN_TUNNEL_ID,
+ expectedData);
+ }
+ });
+
+ HttpRequest request =
+ HttpTunnelMessageUtils.createSendDataRequest(HOST,
+ KNOWN_TUNNEL_ID, expectedData);
+ Channels.fireMessageReceived(channel, request);
+
+ assertEquals(1, sink.events.size());
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ sink.events.poll(), HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isOKResponse(response));
+ }
+
+ @Test
+ public void testSendDataRequestWithNoContentRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createSendDataRequest(HOST,
+ KNOWN_TUNNEL_ID, ChannelBuffers.dynamicBuffer());
+ Channels.fireMessageReceived(channel, request);
+
+ assertEquals(1, sink.events.size());
+ checkResponseIsRejection("Send data requests must contain data");
+ }
+
+ @Test
+ public void testSendDataRequestForUnknownTunnelIdRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createSendDataRequest(HOST,
+ UNKNOWN_TUNNEL_ID, ChannelBuffers.dynamicBuffer());
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testSendDataRequestWithoutTunnelIdRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createSendDataRequest(HOST, null,
+ ChannelBuffers.dynamicBuffer());
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testReceiveDataRequest() {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).pollOutboundData(KNOWN_TUNNEL_ID, channel);
+ }
+ });
+ HttpRequest request =
+ HttpTunnelMessageUtils.createReceiveDataRequest(HOST,
+ KNOWN_TUNNEL_ID);
+ Channels.fireMessageReceived(channel, request);
+ }
+
+ @Test
+ public void testReceiveDataRequestWithoutTunnelIdRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createReceiveDataRequest(HOST, null);
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testReceiveDataRequestForUnknownTunnelIdRejected() {
+ HttpRequest request =
+ HttpTunnelMessageUtils.createReceiveDataRequest(HOST,
+ UNKNOWN_TUNNEL_ID);
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ private void checkRequestWithoutTunnelIdIsRejected(HttpRequest request) {
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent =
+ checkResponseIsRejection("no tunnel id specified in request");
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ private void checkRequestWithUnknownTunnelIdIsRejected(HttpRequest request) {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).isOpenTunnel(UNKNOWN_TUNNEL_ID);
+ will(returnValue(false));
+ }
+ });
+
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent =
+ checkResponseIsRejection("specified tunnel is either closed or does not exist");
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ private ChannelEvent checkResponseIsRejection(String errorMessage) {
+ ChannelEvent responseEvent = sink.events.poll();
+
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(responseEvent,
+ HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isRejection(response));
+ assertEquals(errorMessage,
+ HttpTunnelMessageUtils.extractErrorMessage(response));
+
+ return responseEvent;
+ }
+
+ private void checkClosesAfterWrite(ChannelEvent responseEvent) {
+ responseEvent.getFuture().setSuccess();
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(), ChannelState.OPEN,
+ false);
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelConfig.java
new file mode 100644
index 00000000000..b56b9b781a9
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelConfig.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jboss.netty.buffer.ChannelBufferFactory;
+import org.jboss.netty.buffer.HeapChannelBufferFactory;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+import org.jboss.netty.util.internal.ConversionUtil;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeChannelConfig implements SocketChannelConfig {
+
+ private int receiveBufferSize = 1024;
+
+ private int sendBufferSize = 1024;
+
+ private int soLinger = 500;
+
+ private int trafficClass = 0;
+
+ private boolean keepAlive = true;
+
+ private boolean reuseAddress = true;
+
+ private boolean tcpNoDelay = false;
+
+ private ChannelBufferFactory bufferFactory = new HeapChannelBufferFactory();
+
+ private int connectTimeout = 5000;
+
+ private ChannelPipelineFactory pipelineFactory =
+ new ChannelPipelineFactory() {
+ public ChannelPipeline getPipeline() throws Exception {
+ return Channels.pipeline();
+ }
+ };
+
+ private int writeTimeout = 3000;
+
+ public int getReceiveBufferSize() {
+ return receiveBufferSize;
+ }
+
+ public void setReceiveBufferSize(int receiveBufferSize) {
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+ public int getSendBufferSize() {
+ return sendBufferSize;
+ }
+
+ public void setSendBufferSize(int sendBufferSize) {
+ this.sendBufferSize = sendBufferSize;
+ }
+
+ public int getSoLinger() {
+ return soLinger;
+ }
+
+ public void setSoLinger(int soLinger) {
+ this.soLinger = soLinger;
+ }
+
+ public int getTrafficClass() {
+ return trafficClass;
+ }
+
+ public void setTrafficClass(int trafficClass) {
+ this.trafficClass = trafficClass;
+ }
+
+ public boolean isKeepAlive() {
+ return keepAlive;
+ }
+
+ public void setKeepAlive(boolean keepAlive) {
+ this.keepAlive = keepAlive;
+ }
+
+ public boolean isReuseAddress() {
+ return reuseAddress;
+ }
+
+ public void setReuseAddress(boolean reuseAddress) {
+ this.reuseAddress = reuseAddress;
+ }
+
+ public boolean isTcpNoDelay() {
+ return tcpNoDelay;
+ }
+
+ public void setTcpNoDelay(boolean tcpNoDelay) {
+ this.tcpNoDelay = tcpNoDelay;
+ }
+
+ public void setPerformancePreferences(int connectionTime, int latency,
+ int bandwidth) {
+ // do nothing
+ }
+
+ public ChannelBufferFactory getBufferFactory() {
+ return bufferFactory;
+ }
+
+ public void setBufferFactory(ChannelBufferFactory bufferFactory) {
+ this.bufferFactory = bufferFactory;
+ }
+
+ public int getConnectTimeoutMillis() {
+ return connectTimeout;
+ }
+
+ public void setConnectTimeoutMillis(int connectTimeoutMillis) {
+ connectTimeout = connectTimeoutMillis;
+ }
+
+ public ChannelPipelineFactory getPipelineFactory() {
+ return pipelineFactory;
+ }
+
+ public void setPipelineFactory(ChannelPipelineFactory pipelineFactory) {
+ this.pipelineFactory = pipelineFactory;
+ }
+
+ public int getWriteTimeoutMillis() {
+ return writeTimeout;
+ }
+
+ public void setWriteTimeoutMillis(int writeTimeoutMillis) {
+ writeTimeout = writeTimeoutMillis;
+ }
+
+ public boolean setOption(String key, Object value) {
+ if (key.equals("pipelineFactory")) {
+ setPipelineFactory((ChannelPipelineFactory) value);
+ } else if (key.equals("connectTimeoutMillis")) {
+ setConnectTimeoutMillis(ConversionUtil.toInt(value));
+ } else if (key.equals("bufferFactory")) {
+ setBufferFactory((ChannelBufferFactory) value);
+ } else if (key.equals("receiveBufferSize")) {
+ setReceiveBufferSize(ConversionUtil.toInt(value));
+ } else if (key.equals("sendBufferSize")) {
+ setSendBufferSize(ConversionUtil.toInt(value));
+ } else if (key.equals("tcpNoDelay")) {
+ setTcpNoDelay(ConversionUtil.toBoolean(value));
+ } else if (key.equals("keepAlive")) {
+ setKeepAlive(ConversionUtil.toBoolean(value));
+ } else if (key.equals("reuseAddress")) {
+ setReuseAddress(ConversionUtil.toBoolean(value));
+ } else if (key.equals("soLinger")) {
+ setSoLinger(ConversionUtil.toInt(value));
+ } else if (key.equals("trafficClass")) {
+ setTrafficClass(ConversionUtil.toInt(value));
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ public void setOptions(Map options) {
+ for (Entry e: options.entrySet()) {
+ setOption(e.getKey(), e.getValue());
+ }
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelSink.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelSink.java
new file mode 100644
index 00000000000..aa5b1254f52
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeChannelSink.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.jboss.netty.channel.AbstractChannelSink;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeChannelSink extends AbstractChannelSink {
+
+ public Queue events = new LinkedList();
+
+ public void eventSunk(ChannelPipeline pipeline, ChannelEvent e)
+ throws Exception {
+ events.add(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeClientSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeClientSocketChannelFactory.java
new file mode 100644
index 00000000000..31799022522
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeClientSocketChannelFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.SocketChannel;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeClientSocketChannelFactory implements
+ ClientSocketChannelFactory {
+
+ public List createdChannels;
+
+ public FakeClientSocketChannelFactory() {
+ createdChannels = new ArrayList();
+ }
+
+ public SocketChannel newChannel(ChannelPipeline pipeline) {
+ FakeSocketChannel channel =
+ new FakeSocketChannel(null, this, pipeline,
+ new FakeChannelSink());
+ createdChannels.add(channel);
+ return channel;
+ }
+
+ public void releaseExternalResources() {
+ // nothing to do
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannel.java
new file mode 100644
index 00000000000..fe7fc82ee62
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannel.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.jboss.netty.channel.Channels.fireChannelBound;
+import static org.jboss.netty.channel.Channels.fireChannelConnected;
+import static org.jboss.netty.channel.Channels.fireChannelOpen;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.channel.AbstractChannel;
+import org.jboss.netty.channel.ChannelFactory;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannel extends AbstractChannel implements
+ ServerSocketChannel {
+
+ public boolean bound;
+
+ public boolean connected;
+
+ public InetSocketAddress remoteAddress;
+
+ public InetSocketAddress localAddress;
+
+ public ServerSocketChannelConfig config =
+ new FakeServerSocketChannelConfig();
+
+ public FakeServerSocketChannel(ChannelFactory factory,
+ ChannelPipeline pipeline, ChannelSink sink) {
+ super(null, factory, pipeline, sink);
+ }
+
+ public ServerSocketChannelConfig getConfig() {
+ return config;
+ }
+
+ public InetSocketAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ public InetSocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public boolean isBound() {
+ return bound;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public FakeSocketChannel acceptNewConnection(
+ InetSocketAddress remoteAddress, ChannelSink sink) throws Exception {
+ ChannelPipeline newPipeline =
+ getConfig().getPipelineFactory().getPipeline();
+ FakeSocketChannel newChannel =
+ new FakeSocketChannel(this, getFactory(), newPipeline, sink);
+ newChannel.localAddress = localAddress;
+ newChannel.remoteAddress = remoteAddress;
+ fireChannelOpen(newChannel);
+ fireChannelBound(newChannel, newChannel.localAddress);
+ fireChannelConnected(this, newChannel.remoteAddress);
+
+ return newChannel;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelConfig.java
new file mode 100644
index 00000000000..28d2cb2ef68
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelConfig.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import org.jboss.netty.buffer.ChannelBufferFactory;
+import org.jboss.netty.buffer.HeapChannelBufferFactory;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.DefaultChannelConfig;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannelConfig extends DefaultChannelConfig
+ implements ServerSocketChannelConfig {
+
+ public int backlog = 5;
+
+ public int receiveBufferSize = 1024;
+
+ public boolean reuseAddress = false;
+
+ public int connectionTimeout = 5000;
+
+ public ChannelPipelineFactory pipelineFactory;
+
+ public int writeTimeout = 5000;
+
+ public ChannelBufferFactory bufferFactory = new HeapChannelBufferFactory();
+
+ public int getBacklog() {
+ return backlog;
+ }
+
+ public void setBacklog(int backlog) {
+ this.backlog = backlog;
+ }
+
+ public int getReceiveBufferSize() {
+ return receiveBufferSize;
+ }
+
+ public void setReceiveBufferSize(int receiveBufferSize) {
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+ public boolean isReuseAddress() {
+ return reuseAddress;
+ }
+
+ public void setReuseAddress(boolean reuseAddress) {
+ this.reuseAddress = reuseAddress;
+ }
+
+ public void setPerformancePreferences(int connectionTime, int latency,
+ int bandwidth) {
+ // ignore
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelFactory.java
new file mode 100644
index 00000000000..704ba958fff
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeServerSocketChannelFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannelFactory implements
+ ServerSocketChannelFactory {
+
+ public ChannelSink sink = new FakeChannelSink();
+
+ public FakeServerSocketChannel createdChannel;
+
+ public ServerSocketChannel newChannel(ChannelPipeline pipeline) {
+ createdChannel = new FakeServerSocketChannel(this, pipeline, sink);
+ return createdChannel;
+ }
+
+ public void releaseExternalResources() {
+ // nothing to do
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/FakeSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/http/FakeSocketChannel.java
new file mode 100644
index 00000000000..6849873cb96
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/FakeSocketChannel.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.net.InetSocketAddress;
+
+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.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.SocketChannel;
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeSocketChannel extends AbstractChannel implements SocketChannel {
+
+ public InetSocketAddress localAddress;
+
+ public InetSocketAddress remoteAddress;
+
+ public SocketChannelConfig config = new FakeChannelConfig();
+
+ public boolean bound = false;
+
+ public boolean connected = false;
+
+ public ChannelSink sink;
+
+ public FakeSocketChannel(Channel parent, ChannelFactory factory,
+ ChannelPipeline pipeline, ChannelSink sink) {
+ super(parent, factory, pipeline, sink);
+ this.sink = sink;
+ }
+
+ public InetSocketAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ public SocketChannelConfig getConfig() {
+ return config;
+ }
+
+ public InetSocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public boolean isBound() {
+ return bound;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void emulateConnected(InetSocketAddress localAddress,
+ InetSocketAddress remoteAddress, ChannelFuture connectedFuture) {
+ if (connected) {
+ return;
+ }
+
+ emulateBound(localAddress, null);
+ this.remoteAddress = remoteAddress;
+ connected = true;
+ Channels.fireChannelConnected(this, remoteAddress);
+ if (connectedFuture != null) {
+ connectedFuture.setSuccess();
+ }
+ }
+
+ public void emulateBound(InetSocketAddress localAddress,
+ ChannelFuture boundFuture) {
+ if (bound) {
+ return;
+ }
+
+ bound = true;
+ this.localAddress = localAddress;
+ Channels.fireChannelBound(this, localAddress);
+ if (boundFuture != null) {
+ boundFuture.setSuccess();
+ }
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelAcceptedChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelAcceptedChannelSinkTest.java
new file mode 100644
index 00000000000..14dfe3ca64a
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelAcceptedChannelSinkTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelAcceptedChannelSinkTest {
+
+ private static final String TUNNEL_ID = "1";
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ ServerMessageSwitchDownstreamInterface messageSwitch;
+
+ private HttpTunnelAcceptedChannelSink sink;
+
+ private FakeSocketChannel channel;
+
+ private UpstreamEventCatcher upstreamCatcher;
+
+ @Before
+ public void setUp() throws Exception {
+ messageSwitch =
+ mockContext.mock(ServerMessageSwitchDownstreamInterface.class);
+ sink =
+ new HttpTunnelAcceptedChannelSink(messageSwitch, TUNNEL_ID,
+ new HttpTunnelAcceptedChannelConfig());
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamCatcher = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamCatcher);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ upstreamCatcher.events.clear();
+ }
+
+ @Test
+ public void testSendInvalidDataType() {
+ Channels.write(channel, new Object());
+ assertEquals(1, upstreamCatcher.events.size());
+ NettyTestUtils.checkIsExceptionEvent(upstreamCatcher.events.poll());
+ }
+
+ @Test
+ public void testUnbind() {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).serverCloseTunnel(TUNNEL_ID);
+ }
+ });
+ Channels.unbind(channel);
+ }
+
+ @Test
+ public void testDisconnect() {
+ mockContext.checking(new Expectations() {
+ {
+ one(messageSwitch).serverCloseTunnel(TUNNEL_ID);
+ }
+ });
+
+ Channels.disconnect(channel);
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelConfigTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelConfigTest.java
new file mode 100644
index 00000000000..5b9b535c198
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelConfigTest.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelClientChannelConfigTest {
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ SocketChannelConfig sendChannelConfig;
+
+ SocketChannelConfig pollChannelConfig;
+
+ HttpTunnelClientChannelConfig config;
+
+ @Before
+ public void setUp() {
+ sendChannelConfig =
+ mockContext
+ .mock(SocketChannelConfig.class, "sendChannelConfig");
+ pollChannelConfig =
+ mockContext
+ .mock(SocketChannelConfig.class, "pollChannelConfig");
+
+ config =
+ new HttpTunnelClientChannelConfig(sendChannelConfig,
+ pollChannelConfig);
+ }
+
+ @Test
+ public void testGetReceiveBufferSize() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).getReceiveBufferSize();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getReceiveBufferSize());
+ }
+
+ @Test
+ public void testGetSendBufferSize() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).getSendBufferSize();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getSendBufferSize());
+ }
+
+ @Test
+ public void testGetSoLinger() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).getSoLinger();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getSoLinger());
+ }
+
+ @Test
+ public void testTrafficClass() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).getTrafficClass();
+ will(returnValue(1));
+ }
+ });
+
+ assertEquals(1, config.getTrafficClass());
+ }
+
+ @Test
+ public void testIsKeepAlive() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).isKeepAlive();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isKeepAlive());
+ }
+
+ @Test
+ public void testIsReuseAddress() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).isReuseAddress();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isReuseAddress());
+ }
+
+ @Test
+ public void testIsTcpNoDelay() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).isTcpNoDelay();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isTcpNoDelay());
+ }
+
+ @Test
+ public void testSetKeepAlive() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setKeepAlive(true);
+ one(sendChannelConfig).setKeepAlive(true);
+ }
+ });
+
+ config.setKeepAlive(true);
+ }
+
+ @Test
+ public void testSetPerformancePreferences() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setPerformancePreferences(100, 200, 300);
+ one(sendChannelConfig).setPerformancePreferences(100, 200, 300);
+ }
+ });
+
+ config.setPerformancePreferences(100, 200, 300);
+ }
+
+ @Test
+ public void testSetReceiveBufferSize() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setReceiveBufferSize(100);
+ one(sendChannelConfig).setReceiveBufferSize(100);
+ }
+ });
+
+ config.setReceiveBufferSize(100);
+ }
+
+ @Test
+ public void testSetReuseAddress() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setReuseAddress(true);
+ one(sendChannelConfig).setReuseAddress(true);
+ }
+ });
+
+ config.setReuseAddress(true);
+ }
+
+ @Test
+ public void testSetSendBufferSize() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setSendBufferSize(100);
+ one(sendChannelConfig).setSendBufferSize(100);
+ }
+ });
+
+ config.setSendBufferSize(100);
+ }
+
+ @Test
+ public void testSetSoLinger() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setSoLinger(100);
+ one(sendChannelConfig).setSoLinger(100);
+ }
+ });
+
+ config.setSoLinger(100);
+ }
+
+ @Test
+ public void testTcpNoDelay() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setTcpNoDelay(true);
+ one(sendChannelConfig).setTcpNoDelay(true);
+ }
+ });
+
+ config.setTcpNoDelay(true);
+ }
+
+ @Test
+ public void testSetTrafficClass() {
+ mockContext.checking(new Expectations() {
+ {
+ one(pollChannelConfig).setTrafficClass(1);
+ one(sendChannelConfig).setTrafficClass(1);
+ }
+ });
+
+ config.setTrafficClass(1);
+ }
+
+ @Test
+ public void testSetHighWaterMark() {
+ config.setWriteBufferHighWaterMark(128 * 1024);
+ assertEquals(128 * 1024, config.getWriteBufferHighWaterMark());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetHighWaterMark_negative() {
+ config.setWriteBufferHighWaterMark(-1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetHighWaterMark_zero() {
+ config.setWriteBufferHighWaterMark(0);
+ }
+
+ @Test
+ public void testSetLowWaterMark() {
+ config.setWriteBufferLowWaterMark(100);
+ assertEquals(100, config.getWriteBufferLowWaterMark());
+ }
+
+ @Test
+ public void testSetLowWaterMark_zero() {
+ // zero is permitted for the low water mark, unlike high water mark
+ config.setWriteBufferLowWaterMark(0);
+ assertEquals(0, config.getWriteBufferLowWaterMark());
+ }
+
+ @Test
+ public void testSetHighWaterMark_lowerThanLow() {
+ config.setWriteBufferLowWaterMark(100);
+ try {
+ config.setWriteBufferHighWaterMark(80);
+ fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Write buffer high water mark must be strictly greater than the low water mark",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSetLowWaterMark_higherThanHigh() {
+ config.setWriteBufferHighWaterMark(128 * 1024);
+ try {
+ config.setWriteBufferLowWaterMark(256 * 1024);
+ fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Write buffer low water mark must be strictly less than the high water mark",
+ e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelTest.java
new file mode 100644
index 00000000000..ccc185ca2a3
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientChannelTest {
+
+ public static final int LOCAL_PORT = 50123;
+
+ /** used to emulate the selection of a random port in response to a bind request
+ * on an ephemeral port.
+ */
+ public static final int OTHER_LOCAL_PORT = 40652;
+
+ public static final InetSocketAddress LOCAL_ADDRESS = InetSocketAddress
+ .createUnresolved("localhost", LOCAL_PORT);
+
+ public static final InetSocketAddress LOCAL_ADDRESS_EPHEMERAL_PORT =
+ InetSocketAddress.createUnresolved("localhost", 0);
+
+ public static final InetSocketAddress REMOTE_ADDRESS = InetSocketAddress
+ .createUnresolved("test.server.com", 12345);
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT;
+
+ static {
+ try {
+ InetAddress localhostIPV4 =
+ InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 });
+ InetAddress localhostIPV6 =
+ InetAddress.getByAddress(new byte[] { 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 1 });
+ RESOLVED_LOCAL_ADDRESS_IPV4 =
+ new InetSocketAddress(localhostIPV4, LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV6 =
+ new InetSocketAddress(localhostIPV6, LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT =
+ new InetSocketAddress(localhostIPV4, 0);
+ RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT =
+ new InetSocketAddress(localhostIPV6, 0);
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT =
+ new InetSocketAddress(localhostIPV4, OTHER_LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT =
+ new InetSocketAddress(localhostIPV6, OTHER_LOCAL_PORT);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(
+ "Creation of InetAddresses should not fail when explicitly specified and the correct length",
+ e);
+ }
+ }
+
+ private UpstreamEventCatcher upstreamCatcher;
+
+ private HttpTunnelClientChannel channel;
+
+ private FakeClientSocketChannelFactory outboundFactory;
+
+ private FakeSocketChannel sendChannel;
+
+ private FakeSocketChannel pollChannel;
+
+ private FakeChannelSink sendSink;
+
+ private FakeChannelSink pollSink;
+
+ @Before
+ public void setUp() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamCatcher = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamCatcher);
+
+ outboundFactory = new FakeClientSocketChannelFactory();
+
+ HttpTunnelClientChannelFactory factory =
+ new HttpTunnelClientChannelFactory(outboundFactory);
+ channel = factory.newChannel(pipeline);
+
+ assertEquals(2, outboundFactory.createdChannels.size());
+
+ sendChannel = outboundFactory.createdChannels.get(0);
+ pollChannel = outboundFactory.createdChannels.get(1);
+ sendSink = (FakeChannelSink) sendChannel.sink;
+ pollSink = (FakeChannelSink) pollChannel.sink;
+ }
+
+ @Test
+ public void testConnect() {
+ Channels.connect(channel, REMOTE_ADDRESS);
+
+ // this should result in a CONNECTED state event on the send channel, but not on the poll
+ // channel just yet
+ assertEquals(1, sendSink.events.size());
+ assertEquals(0, pollSink.events.size());
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent,
+ ChannelState.CONNECTED, REMOTE_ADDRESS);
+
+ // once the send channel indicates that it is connected, we should see the tunnel open request
+ // being sent
+ sendChannel.emulateConnected(LOCAL_ADDRESS, REMOTE_ADDRESS,
+ ((ChannelStateEvent) sendChannelEvent).getFuture());
+ assertEquals(1, sendSink.events.size());
+ ChannelEvent openTunnelRequest = sendSink.events.poll();
+ NettyTestUtils.checkIsDownstreamMessageEvent(openTunnelRequest,
+ ChannelBuffer.class);
+ }
+
+ @Test
+ public void testBind_unresolvedAddress() {
+ // requesting a binding with an unresolved local address
+ // should attempt to bind the send channel with that address unaltered
+ // and attempt to bind the poll address with the same host name but
+ // an ephemeral port. We emulate a resolved IPV4 address for the bind
+ // response.
+ checkBinding(LOCAL_ADDRESS, LOCAL_ADDRESS,
+ LOCAL_ADDRESS_EPHEMERAL_PORT, RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT);
+ }
+
+ @Test
+ public void testBind_resolvedAddress_ipv4() {
+ // variant that uses resolved addresses. The bind request
+ // for the poll channel should also use a resolved address,
+ // built from the provided resolved address.
+ checkBinding(RESOLVED_LOCAL_ADDRESS_IPV4, RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT,
+ RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT);
+ }
+
+ @Test
+ public void testBind_resolvedAddress_ipv6() {
+ // variant that uses a resolved IPV6 address.
+ // bind request on the poll channel should use the same
+ // IPv6 host, with an ephemeral port.
+ checkBinding(RESOLVED_LOCAL_ADDRESS_IPV6, RESOLVED_LOCAL_ADDRESS_IPV6,
+ RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT,
+ RESOLVED_LOCAL_ADDRESS_IPV6,
+ RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT);
+ }
+
+ private void checkBinding(InetSocketAddress requestedBindAddress,
+ InetSocketAddress expectedPollBindRequest,
+ InetSocketAddress expectedSendBindRequest,
+ InetSocketAddress emulatedPollBindAddress,
+ InetSocketAddress emulatedSendBindAddress) {
+
+ ChannelFuture bindFuture = Channels.bind(channel, requestedBindAddress);
+ assertFalse(bindFuture.isDone());
+
+ assertEquals(1, sendSink.events.size());
+ assertEquals(1, pollSink.events.size());
+
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent, ChannelState.BOUND,
+ expectedPollBindRequest);
+ ChannelEvent pollChannelEvent = pollSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(pollChannelEvent, ChannelState.BOUND,
+ expectedSendBindRequest);
+
+ sendChannel.emulateBound(emulatedPollBindAddress,
+ sendChannelEvent.getFuture());
+ assertFalse(bindFuture.isDone());
+ pollChannel.emulateBound(emulatedSendBindAddress,
+ pollChannelEvent.getFuture());
+ assertTrue(bindFuture.isDone());
+ assertTrue(bindFuture.isSuccess());
+
+ assertEquals(channel.getLocalAddress(), emulatedPollBindAddress);
+ }
+
+ @Test
+ public void testBind_preResolvedAddress_ipv6() {
+ ChannelFuture bindFuture =
+ Channels.bind(channel, RESOLVED_LOCAL_ADDRESS_IPV6);
+ assertFalse(bindFuture.isDone());
+
+ assertEquals(1, sendSink.events.size());
+ assertEquals(1, pollSink.events.size());
+
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent, ChannelState.BOUND,
+ RESOLVED_LOCAL_ADDRESS_IPV6);
+ ChannelEvent pollChannelEvent = pollSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(pollChannelEvent, ChannelState.BOUND,
+ RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT);
+
+ sendChannel.emulateBound(RESOLVED_LOCAL_ADDRESS_IPV6,
+ sendChannelEvent.getFuture());
+ assertFalse(bindFuture.isDone());
+ pollChannel.emulateBound(RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT,
+ pollChannelEvent.getFuture());
+ assertTrue(bindFuture.isDone());
+ assertTrue(bindFuture.isSuccess());
+
+ assertEquals(channel.getLocalAddress(), RESOLVED_LOCAL_ADDRESS_IPV6);
+ }
+
+ @Test
+ public void testBind_sendBindFails() {
+ ChannelFuture bindFuture = Channels.bind(channel, LOCAL_ADDRESS);
+ assertFalse(bindFuture.isDone());
+
+ Exception bindFailureReason = new Exception("could not bind");
+ ((ChannelStateEvent) sendSink.events.poll()).getFuture().setFailure(
+ bindFailureReason);
+ assertTrue(bindFuture.isDone());
+ assertFalse(bindFuture.isSuccess());
+ assertSame(bindFailureReason, bindFuture.getCause());
+ }
+
+ @Test
+ public void testBind_pollBindFails() {
+ ChannelFuture bindFuture = Channels.bind(channel, LOCAL_ADDRESS);
+ assertFalse(bindFuture.isDone());
+
+ Exception bindFailureReason = new Exception("could not bind");
+ ((ChannelStateEvent) pollSink.events.poll()).getFuture().setFailure(
+ bindFailureReason);
+ assertTrue(bindFuture.isDone());
+ assertFalse(bindFuture.isSuccess());
+ assertSame(bindFailureReason, bindFuture.getCause());
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandlerTest.java
new file mode 100644
index 00000000000..fa8685d1b99
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandlerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientPollHandlerTest {
+
+ private static final String TUNNEL_ID = "1";
+
+ private static final InetSocketAddress SERVER_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 3 }, 12345);
+
+ private static final InetSocketAddress PROXY_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 2 }, 8888);
+
+ private static final InetSocketAddress LOCAL_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 1 }, 54321);
+
+ private FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ private HttpTunnelClientPollHandler handler;
+
+ private MockChannelStateListener listener;
+
+ private static InetSocketAddress createAddress(byte[] addr, int port) {
+ try {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sink = new FakeChannelSink();
+
+ ChannelPipeline pipeline = Channels.pipeline();
+ listener = new MockChannelStateListener();
+ listener.serverHostName =
+ HttpTunnelMessageUtils.convertToHostString(SERVER_ADDRESS);
+ handler = new HttpTunnelClientPollHandler(listener);
+ handler.setTunnelId(TUNNEL_ID);
+ pipeline.addLast(HttpTunnelClientPollHandler.NAME, handler);
+
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress = PROXY_ADDRESS;
+ channel.localAddress = LOCAL_ADDRESS;
+ }
+
+ @Test
+ public void testSendsRequestOnConnect() {
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ HttpRequest request =
+ checkIsMessageEventContainingHttpRequest(sink.events.poll());
+ assertTrue(HttpTunnelMessageUtils.isServerToClientRequest(request));
+ assertTrue(HttpTunnelMessageUtils.checkHost(request, SERVER_ADDRESS));
+ assertTrue(listener.fullyEstablished);
+ }
+
+ @Test
+ public void testSendsReceivedDataSentUpstream() {
+ HttpResponse response =
+ HttpTunnelMessageUtils.createRecvDataResponse(NettyTestUtils
+ .createData(1234L));
+ Channels.fireMessageReceived(channel, response);
+ assertEquals(1, listener.messages.size());
+ assertEquals(1234L, listener.messages.get(0).readLong());
+ }
+
+ @Test
+ public void testSendsAnotherRequestAfterResponse() {
+ HttpResponse response =
+ HttpTunnelMessageUtils.createRecvDataResponse(NettyTestUtils
+ .createData(1234L));
+ Channels.fireMessageReceived(channel, response);
+ assertEquals(1, sink.events.size());
+ checkIsMessageEventContainingHttpRequest(sink.events.poll());
+ }
+
+ private HttpRequest checkIsMessageEventContainingHttpRequest(
+ ChannelEvent event) {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(messageEvent.getMessage() instanceof HttpRequest);
+ return (HttpRequest) messageEvent.getMessage();
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandlerTest.java
new file mode 100644
index 00000000000..943789a58e8
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandlerTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.handler.codec.http.HttpHeaders;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientSendHandlerTest {
+
+ private static final InetSocketAddress SERVER_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 3 }, 12345);
+
+ private static final InetSocketAddress PROXY_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 2 }, 8888);
+
+ private static final InetSocketAddress LOCAL_ADDRESS = createAddress(
+ new byte[] { 10, 0, 0, 1 }, 54321);
+
+ private FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ private HttpTunnelClientSendHandler handler;
+
+ private MockChannelStateListener listener;
+
+ @Before
+ public void setUp() {
+ sink = new FakeChannelSink();
+ ChannelPipeline pipeline = Channels.pipeline();
+ listener = new MockChannelStateListener();
+ listener.serverHostName =
+ HttpTunnelMessageUtils.convertToHostString(SERVER_ADDRESS);
+ handler = new HttpTunnelClientSendHandler(listener);
+ pipeline.addLast(HttpTunnelClientSendHandler.NAME, handler);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress = PROXY_ADDRESS;
+ channel.localAddress = LOCAL_ADDRESS;
+ }
+
+ private static InetSocketAddress createAddress(byte[] addr, int port) {
+ try {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ @Test
+ public void testSendsTunnelOpen() throws Exception {
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ HttpRequest request =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isOpenTunnelRequest(request));
+ assertTrue(HttpTunnelMessageUtils.checkHost(request, SERVER_ADDRESS));
+ }
+
+ @Test
+ public void testStoresTunnelId() throws Exception {
+ emulateConnect();
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createTunnelOpenResponse("newTunnel"));
+ assertEquals("newTunnel", handler.getTunnelId());
+ assertEquals("newTunnel", listener.tunnelId);
+ }
+
+ @Test
+ public void testSendData() {
+ emulateConnectAndOpen();
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ ChannelEvent sentEvent = sink.events.poll();
+ checkIsSendDataRequestWithData(sentEvent,
+ NettyTestUtils.createData(1234L));
+ }
+
+ @Test
+ public void testWillNotSendDataUntilTunnelIdSet() {
+ emulateConnect();
+ channel.write(NettyTestUtils.createData(1234L));
+
+ assertEquals(0, sink.events.size());
+
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ }
+
+ @Test
+ public void testOnlyOneRequestAtATime() {
+ emulateConnectAndOpen();
+
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(),
+ NettyTestUtils.createData(1234L));
+
+ channel.write(NettyTestUtils.createData(5678L));
+ assertEquals(0, sink.events.size());
+
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createSendDataResponse());
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(),
+ NettyTestUtils.createData(5678L));
+ }
+
+ @Test
+ public void testDisconnect() {
+ emulateConnectAndOpen();
+
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(),
+ NettyTestUtils.createData(1234L));
+
+ channel.disconnect();
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createSendDataResponse());
+ assertEquals(1, sink.events.size());
+
+ HttpRequest request =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isCloseTunnelRequest(request));
+ assertEquals("newTunnel",
+ HttpTunnelMessageUtils.extractTunnelId(request));
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createTunnelCloseResponse());
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(),
+ ChannelState.CONNECTED, null);
+ }
+
+ @Test
+ public void testClose() {
+ emulateConnectAndOpen();
+
+ channel.close();
+ assertEquals(1, sink.events.size());
+ HttpRequest request =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isCloseTunnelRequest(request));
+ assertEquals("newTunnel",
+ HttpTunnelMessageUtils.extractTunnelId(request));
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createTunnelCloseResponse());
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(), ChannelState.OPEN,
+ false);
+ }
+
+ @Test
+ public void testWritesAfterCloseAreRejected() {
+ emulateConnectAndOpen();
+
+ channel.close();
+ assertFalse(channel.write(NettyTestUtils.createData(1234L)).isSuccess());
+ }
+
+ private void checkIsSendDataRequestWithData(ChannelEvent event,
+ ChannelBuffer data) {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(messageEvent.getMessage() instanceof HttpRequest);
+ HttpRequest request = (HttpRequest) messageEvent.getMessage();
+ assertTrue(HttpTunnelMessageUtils.isSendDataRequest(request));
+ assertEquals(data.readableBytes(),
+ HttpHeaders.getContentLength(request));
+
+ ChannelBuffer content = request.getContent();
+ NettyTestUtils.assertEquals(data, content);
+ }
+
+ private void emulateConnect() {
+ channel.emulateConnected(LOCAL_ADDRESS, PROXY_ADDRESS, null);
+ sink.events.clear();
+ }
+
+ private void emulateConnectAndOpen() {
+ emulateConnect();
+ Channels.fireMessageReceived(channel,
+ HttpTunnelMessageUtils.createTunnelOpenResponse("newTunnel"));
+
+ sink.events.clear();
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactoryTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactoryTest.java
new file mode 100644
index 00000000000..cb592a096b1
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactoryTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import org.jboss.netty.channel.ChannelException;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelFactoryTest {
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ ServerSocketChannelFactory realChannelFactory;
+
+ private HttpTunnelServerChannelFactory factory;
+
+ ServerSocketChannel realChannel;
+
+ @Before
+ public void setUp() throws Exception {
+ realChannelFactory = mockContext.mock(ServerSocketChannelFactory.class);
+ factory = new HttpTunnelServerChannelFactory(realChannelFactory);
+ ChannelPipeline pipeline = Channels.pipeline();
+ realChannel =
+ new FakeServerSocketChannel(factory, pipeline,
+ new FakeChannelSink());
+ }
+
+ @Test
+ public void testNewChannel() {
+ mockContext.checking(new Expectations() {
+ {
+ one(realChannelFactory).newChannel(
+ with(any(ChannelPipeline.class)));
+ will(returnValue(realChannel));
+ }
+ });
+ ChannelPipeline pipeline = Channels.pipeline();
+ HttpTunnelServerChannel newChannel = factory.newChannel(pipeline);
+ assertNotNull(newChannel);
+ assertSame(pipeline, newChannel.getPipeline());
+ }
+
+ @Test
+ public void testNewChannel_forwardsWrappedFactoryFailure() {
+ final ChannelException innerException = new ChannelException();
+ mockContext.checking(new Expectations() {
+ {
+ one(realChannelFactory).newChannel(
+ with(any(ChannelPipeline.class)));
+ will(throwException(innerException));
+ }
+ });
+
+ try {
+ factory.newChannel(Channels.pipeline());
+ fail("Expected ChannelException");
+ } catch (ChannelException e) {
+ assertSame(innerException, e);
+ }
+ }
+
+ // @Test
+ // public void testChannelCreation_withServerBootstrap() {
+ // mockContext.checking(new Expectations() {{
+ // one(realChannelFactory).newChannel(with(any(ChannelPipeline.class))); will(returnValue(realChannel));
+ // }});
+ //
+ // ServerBootstrap bootstrap = new ServerBootstrap(factory);
+ // Channel newChannel = bootstrap.bind(new InetSocketAddress(80));
+ // assertNotNull(newChannel);
+ //
+ // }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSinkTest.java
new file mode 100644
index 00000000000..57b00307034
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSinkTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelSinkTest {
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private HttpTunnelServerChannelSink sink;
+
+ private ChannelPipeline pipeline;
+
+ private FakeSocketChannel channel;
+
+ ServerSocketChannel realChannel;
+
+ ChannelFuture realFuture;
+
+ Throwable exceptionInPipeline;
+
+ @Before
+ public void setUp() throws Exception {
+ realChannel = mockContext.mock(ServerSocketChannel.class);
+ pipeline = Channels.pipeline();
+ pipeline.addLast("exceptioncatcher", new ExceptionCatcher());
+ sink = new HttpTunnelServerChannelSink();
+ sink.setRealChannel(realChannel);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ realFuture = Channels.future(realChannel);
+ }
+
+ @After
+ public void teardown() throws Exception {
+ assertTrue("exception caught in pipeline: " + exceptionInPipeline,
+ exceptionInPipeline == null);
+ }
+
+ public void testCloseRequest() throws Exception {
+ mockContext.checking(new Expectations() {
+ {
+ one(realChannel).close();
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture1 = Channels.close(channel);
+ mockContext.assertIsSatisfied();
+ ChannelFuture virtualFuture = virtualFuture1;
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testUnbindRequest_withSuccess() throws Exception {
+ ChannelFuture virtualFuture = checkUnbind();
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testUnbindRequest_withFailure() throws Exception {
+ ChannelFuture virtualFuture = checkUnbind();
+ realFuture.setFailure(new Exception("Something bad happened"));
+ assertFalse(virtualFuture.isSuccess());
+ }
+
+ private ChannelFuture checkUnbind() {
+ mockContext.checking(new Expectations() {
+ {
+ one(realChannel).unbind();
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture = Channels.unbind(channel);
+ mockContext.assertIsSatisfied();
+ return virtualFuture;
+ }
+
+ @Test
+ public void testBindRequest_withSuccess() {
+ ChannelFuture virtualFuture = checkBind();
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testBindRequest_withFailure() {
+ ChannelFuture virtualFuture = checkBind();
+ realFuture.setFailure(new Exception("Something bad happened"));
+ assertFalse(virtualFuture.isSuccess());
+ }
+
+ private ChannelFuture checkBind() {
+ final SocketAddress toAddress = new InetSocketAddress(80);
+ mockContext.checking(new Expectations() {
+ {
+ one(realChannel).bind(toAddress);
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture = Channels.bind(channel, toAddress);
+ return virtualFuture;
+ }
+
+ private final class ExceptionCatcher extends SimpleChannelUpstreamHandler {
+
+ ExceptionCatcher() {
+ super();
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
+ throws Exception {
+ exceptionInPipeline = e.getCause();
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelTest.java
new file mode 100644
index 00000000000..baaaa9c41d3
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.UpstreamChannelStateEvent;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelTest {
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private HttpTunnelServerChannel virtualChannel;
+
+ private UpstreamEventCatcher upstreamEvents;
+
+ private FakeServerSocketChannelFactory realChannelFactory;
+
+ @Before
+ public void setUp() throws Exception {
+ realChannelFactory = new FakeServerSocketChannelFactory();
+ realChannelFactory.sink = new FakeChannelSink();
+
+ HttpTunnelServerChannelFactory factory =
+ new HttpTunnelServerChannelFactory(realChannelFactory);
+ virtualChannel = factory.newChannel(createVirtualChannelPipeline());
+ }
+
+ private ChannelPipeline createVirtualChannelPipeline() {
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamEvents = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamEvents);
+ return pipeline;
+ }
+
+ @Test
+ public void testGetLocalAddress_delegatedToRealChannel() {
+ realChannelFactory.createdChannel.localAddress =
+ InetSocketAddress.createUnresolved("mycomputer", 80);
+ SocketAddress returned = virtualChannel.getLocalAddress();
+ assertSame(realChannelFactory.createdChannel.localAddress, returned);
+ }
+
+ @Test
+ public void testGetRemoteAddress_returnsNull() {
+ assertNull(virtualChannel.getRemoteAddress());
+ }
+
+ @Test
+ public void testIsBound_delegatedToRealChannel() {
+ realChannelFactory.createdChannel.bound = true;
+ assertTrue(virtualChannel.isBound());
+ realChannelFactory.createdChannel.bound = false;
+ assertFalse(virtualChannel.isBound());
+ }
+
+ @Test
+ public void testConstruction_firesOpenEvent() {
+ assertTrue(upstreamEvents.events.size() > 0);
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(),
+ virtualChannel, ChannelState.OPEN, Boolean.TRUE);
+ }
+
+ @Test
+ public void testChannelBoundEventFromReal_replicatedOnVirtual() {
+ upstreamEvents.events.clear();
+ InetSocketAddress boundAddr =
+ InetSocketAddress.createUnresolved("mycomputer", 12345);
+ Channels.fireChannelBound(realChannelFactory.createdChannel, boundAddr);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(),
+ virtualChannel, ChannelState.BOUND, boundAddr);
+ }
+
+ @Test
+ public void testChannelUnboundEventFromReal_replicatedOnVirtual() {
+ upstreamEvents.events.clear();
+ Channels.fireChannelUnbound(realChannelFactory.createdChannel);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(),
+ virtualChannel, ChannelState.BOUND, null);
+ }
+
+ @Test
+ public void testChannelClosedEventFromReal_replicatedOnVirtual() {
+ upstreamEvents.events.clear();
+ Channels.fireChannelClosed(realChannelFactory.createdChannel);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(),
+ virtualChannel, ChannelState.OPEN, Boolean.FALSE);
+ }
+
+ @Test
+ public void testHasConfiguration() {
+ assertNotNull(virtualChannel.getConfig());
+ }
+
+ @Test
+ public void testChangePipelineFactoryDoesNotAffectRealChannel() {
+ ChannelPipelineFactory realPipelineFactory =
+ realChannelFactory.createdChannel.getConfig()
+ .getPipelineFactory();
+ ChannelPipelineFactory virtualPipelineFactory =
+ mockContext.mock(ChannelPipelineFactory.class);
+ virtualChannel.getConfig().setPipelineFactory(virtualPipelineFactory);
+ assertSame(virtualPipelineFactory, virtualChannel.getConfig()
+ .getPipelineFactory());
+
+ // channel pipeline factory is a special case: we do not want it set on the configuration
+ // of the underlying factory
+ assertSame(realPipelineFactory, realChannelFactory.createdChannel
+ .getConfig().getPipelineFactory());
+ }
+
+ @Test
+ public void testChangingBacklogAffectsRealChannel() {
+ virtualChannel.getConfig().setBacklog(1234);
+ assertEquals(1234, realChannelFactory.createdChannel.getConfig()
+ .getBacklog());
+ }
+
+ @Test
+ public void testChangingConnectTimeoutMillisAffectsRealChannel() {
+ virtualChannel.getConfig().setConnectTimeoutMillis(54321);
+ assertEquals(54321, realChannelFactory.createdChannel.getConfig()
+ .getConnectTimeoutMillis());
+ }
+
+ @Test
+ public void testChangingPerformancePreferencesAffectsRealChannel() {
+ final ServerSocketChannelConfig mockConfig =
+ mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+ mockContext.checking(new Expectations() {
+ {
+ one(mockConfig).setPerformancePreferences(100, 200, 300);
+ }
+ });
+ virtualChannel.getConfig().setPerformancePreferences(100, 200, 300);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testChangingReceiveBufferSizeAffectsRealChannel() {
+ virtualChannel.getConfig().setReceiveBufferSize(10101);
+ assertEquals(10101, realChannelFactory.createdChannel.getConfig()
+ .getReceiveBufferSize());
+ }
+
+ @Test
+ public void testChangingReuseAddressAffectsRealChannel() {
+ virtualChannel.getConfig().setReuseAddress(true);
+ assertEquals(true, realChannelFactory.createdChannel.getConfig()
+ .isReuseAddress());
+ }
+
+ @Test
+ public void testSetChannelPipelineFactoryViaOption() {
+ final ServerSocketChannelConfig mockConfig =
+ mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+
+ mockContext.checking(new Expectations() {
+ {
+ never(mockConfig);
+ }
+ });
+
+ ChannelPipelineFactory factory =
+ mockContext.mock(ChannelPipelineFactory.class);
+ virtualChannel.getConfig().setOption("pipelineFactory", factory);
+ assertSame(factory, virtualChannel.getConfig().getPipelineFactory());
+ }
+
+ @Test
+ public void testSetOptionAffectsRealChannel() {
+ final ServerSocketChannelConfig mockConfig =
+ mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+
+ mockContext.checking(new Expectations() {
+ {
+ one(mockConfig).setOption("testOption", "testValue");
+ }
+ });
+
+ virtualChannel.getConfig().setOption("testOption", "testValue");
+ }
+
+ private void checkIsUpstreamChannelStateEvent(ChannelEvent ev,
+ Channel expectedChannel, ChannelState expectedState,
+ Object expectedValue) {
+ assertTrue(ev instanceof UpstreamChannelStateEvent);
+ UpstreamChannelStateEvent checkedEv = (UpstreamChannelStateEvent) ev;
+ assertSame(expectedChannel, checkedEv.getChannel());
+ assertEquals(expectedState, checkedEv.getState());
+ assertEquals(expectedValue, checkedEv.getValue());
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelSoakTester.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelSoakTester.java
new file mode 100644
index 00000000000..0a271b7c294
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelSoakTester.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.bootstrap.ServerBootstrap;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+import org.jboss.netty.channel.group.DefaultChannelGroup;
+import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+import org.jboss.netty.channel.socket.SocketChannel;
+import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelSoakTester {
+
+ private static final int SERVER_PORT = 20100;
+
+ static final Logger LOG = Logger.getLogger(HttpTunnelSoakTester.class
+ .getName());
+
+ private static final long BYTES_TO_SEND = 1024 * 1024 * 1024;
+
+ private static final int MAX_WRITE_SIZE = 64 * 1024;
+
+ private final ServerBootstrap serverBootstrap;
+
+ private final ClientBootstrap clientBootstrap;
+
+ final ChannelGroup channels;
+
+ private final ExecutorService executor;
+
+ final ScheduledExecutorService scheduledExecutor;
+
+ final DataSender c2sDataSender = new DataSender("C2S");
+
+ final DataSender s2cDataSender = new DataSender("S2C");
+
+ private DataVerifier c2sVerifier = new DataVerifier("C2S-Verifier");
+
+ private DataVerifier s2cVerifier = new DataVerifier("S2C-Verifier");
+
+ private static byte[] SEND_STREAM;
+
+ static {
+ SEND_STREAM = new byte[MAX_WRITE_SIZE + 127];
+ for (int i = 0; i < SEND_STREAM.length; i ++) {
+ SEND_STREAM[i] = (byte) (i % 127);
+ }
+ }
+
+ public HttpTunnelSoakTester() {
+ scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
+ executor = Executors.newCachedThreadPool();
+ ServerSocketChannelFactory serverChannelFactory =
+ new NioServerSocketChannelFactory(executor, executor);
+ HttpTunnelServerChannelFactory serverTunnelFactory =
+ new HttpTunnelServerChannelFactory(serverChannelFactory);
+
+ serverBootstrap = new ServerBootstrap(serverTunnelFactory);
+ serverBootstrap.setPipelineFactory(createServerPipelineFactory());
+
+ ClientSocketChannelFactory clientChannelFactory =
+ new NioClientSocketChannelFactory(executor, executor);
+ HttpTunnelClientChannelFactory clientTunnelFactory =
+ new HttpTunnelClientChannelFactory(clientChannelFactory);
+
+ clientBootstrap = new ClientBootstrap(clientTunnelFactory);
+ clientBootstrap.setPipelineFactory(createClientPipelineFactory());
+ configureProxy();
+
+ channels = new DefaultChannelGroup();
+ }
+
+ private void configureProxy() {
+ String proxyHost = System.getProperty("http.proxyHost");
+ if (proxyHost != null && proxyHost.length() != 0) {
+ int proxyPort = Integer.getInteger("http.proxyPort", 80);
+ InetAddress chosenAddress = chooseAddress(proxyHost);
+ InetSocketAddress proxyAddress =
+ new InetSocketAddress(chosenAddress, proxyPort);
+ if (!proxyAddress.isUnresolved()) {
+ clientBootstrap.setOption(
+ HttpTunnelClientChannelConfig.PROXY_ADDRESS_OPTION,
+ proxyAddress);
+ System.out.println("Using " + proxyAddress +
+ " as a proxy for this test run");
+ } else {
+ System.err.println("Failed to resolve proxy address " +
+ proxyAddress);
+ }
+ } else {
+ System.out
+ .println("No proxy specified, will connect to server directly");
+ }
+ }
+
+ private InetAddress chooseAddress(String proxyHost) {
+ try {
+ InetAddress[] allByName = InetAddress.getAllByName(proxyHost);
+ for (InetAddress address: allByName) {
+ if (address.isAnyLocalAddress() || address.isLinkLocalAddress()) {
+ continue;
+ }
+
+ return address;
+ }
+
+ return null;
+ } catch (UnknownHostException e) {
+ return null;
+ }
+ }
+
+ protected ChannelPipelineFactory createClientPipelineFactory() {
+ return new ChannelPipelineFactory() {
+
+ public ChannelPipeline getPipeline() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("s2cVerifier", s2cVerifier);
+ pipeline.addLast("throttleControl", new SendThrottle(
+ c2sDataSender));
+ return pipeline;
+ }
+ };
+ }
+
+ protected ChannelPipelineFactory createServerPipelineFactory() {
+ return new ChannelPipelineFactory() {
+
+ public ChannelPipeline getPipeline() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("c2sVerifier", c2sVerifier);
+ pipeline.addLast("throttleControl", new SendThrottle(
+ s2cDataSender));
+ pipeline.addLast("sendStarter",
+ new SimpleChannelUpstreamHandler() {
+ public void channelConnected(
+ ChannelHandlerContext ctx,
+ ChannelStateEvent e) throws Exception {
+ Channel childChannel = e.getChannel();
+ channels.add(childChannel);
+ s2cDataSender.setChannel(childChannel);
+ executor.execute(s2cDataSender);
+ };
+ });
+ return pipeline;
+ }
+ };
+ }
+
+ public void run() throws InterruptedException {
+ LOG.info("binding server channel");
+ Channel serverChannel =
+ serverBootstrap.bind(new InetSocketAddress(SERVER_PORT));
+ channels.add(serverChannel);
+ LOG.log(Level.INFO, "server channel bound to {0}",
+ serverChannel.getLocalAddress());
+
+ SocketChannel clientChannel = createClientChannel();
+ if (clientChannel == null) {
+ LOG.severe("no client channel - bailing out");
+ return;
+ }
+
+ channels.add(clientChannel);
+ c2sDataSender.setChannel(clientChannel);
+
+ executor.execute(c2sDataSender);
+
+ if (!c2sDataSender.waitForFinish(5, TimeUnit.MINUTES)) {
+ LOG.severe("Data send from client to server failed");
+ }
+
+ if (!s2cDataSender.waitForFinish(5, TimeUnit.MINUTES)) {
+ LOG.severe("Data send from server to client failed");
+ }
+
+ LOG.log(Level.INFO, "Waiting for verification to complete");
+ if (!c2sVerifier.waitForCompletion(30L, TimeUnit.SECONDS)) {
+ LOG.warning("Timed out waiting for verification of client-to-server stream");
+ }
+
+ if (!s2cVerifier.waitForCompletion(30L, TimeUnit.SECONDS)) {
+ LOG.warning("Timed out waiting for verification of server-to-client stream");
+ }
+
+ LOG.info("closing client channel");
+ closeChannel(clientChannel);
+ LOG.info("server channel status: " +
+ (serverChannel.isOpen()? "open" : "closed"));
+ LOG.info("closing server channel");
+ closeChannel(serverChannel);
+ }
+
+ private void closeChannel(Channel channel) {
+ try {
+ if (!channel.close().await(5L, TimeUnit.SECONDS)) {
+ LOG.warning("Failed to close connection within reasonable amount of time");
+ }
+ } catch (InterruptedException e) {
+ LOG.severe("Interrupted while closing connection");
+ }
+
+ }
+
+ private SocketChannel createClientChannel() {
+ InetSocketAddress serverAddress =
+ new InetSocketAddress("localhost", SERVER_PORT);
+ ChannelFuture clientChannelFuture =
+ clientBootstrap.connect(serverAddress);
+ try {
+ if (!clientChannelFuture.await(1000, TimeUnit.MILLISECONDS)) {
+ LOG.severe("did not connect within acceptable time period");
+ return null;
+ }
+ } catch (InterruptedException e) {
+ LOG.severe("Interrupted while waiting for client connect to be established");
+ return null;
+ }
+
+ if (!clientChannelFuture.isSuccess()) {
+ LOG.log(Level.SEVERE, "did not connect successfully",
+ clientChannelFuture.getCause());
+ return null;
+ }
+
+ HttpTunnelClientChannelConfig config =
+ ((HttpTunnelClientChannelConfig) clientChannelFuture
+ .getChannel().getConfig());
+ config.setWriteBufferHighWaterMark(2 * 1024 * 1024);
+ config.setWriteBufferLowWaterMark(1024 * 1024);
+
+ return (SocketChannel) clientChannelFuture.getChannel();
+ }
+
+ private ChannelBuffer createRandomSizeBuffer(AtomicInteger nextWriteByte) {
+ Random random = new Random();
+ int arraySize = random.nextInt(MAX_WRITE_SIZE) + 1;
+
+ // cheaply create the buffer by wrapping an appropriately sized section of the pre-built array
+ ChannelBuffer buffer =
+ ChannelBuffers.wrappedBuffer(SEND_STREAM, nextWriteByte.get(),
+ arraySize);
+ nextWriteByte.set((nextWriteByte.get() + arraySize) % 127);
+
+ return buffer;
+ }
+
+ public static void main(String[] args) throws Exception {
+ HttpTunnelSoakTester soakTester = new HttpTunnelSoakTester();
+ try {
+ soakTester.run();
+ } finally {
+ soakTester.shutdown();
+ }
+ }
+
+ private void shutdown() {
+ serverBootstrap.releaseExternalResources();
+ clientBootstrap.releaseExternalResources();
+ executor.shutdownNow();
+ scheduledExecutor.shutdownNow();
+ }
+
+ private class DataVerifier extends SimpleChannelUpstreamHandler {
+ private String name;
+
+ private int expectedNext = 0;
+
+ private int verifiedBytes = 0;
+
+ private CountDownLatch completionLatch = new CountDownLatch(1);
+
+ public DataVerifier(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
+ throws Exception {
+ ChannelBuffer bytesToVerify = (ChannelBuffer) e.getMessage();
+
+ while (bytesToVerify.readable()) {
+ byte readByte = bytesToVerify.readByte();
+ if (readByte != expectedNext) {
+ LOG.log(Level.SEVERE,
+ "{0}: received a byte out of sequence. Expected {1}, got {2}",
+ new Object[] { name, expectedNext, readByte });
+ System.exit(-1);
+ return;
+ }
+
+ expectedNext = (expectedNext + 1) % 127;
+ verifiedBytes ++;
+ }
+
+ if (verifiedBytes >= BYTES_TO_SEND) {
+ completionLatch.countDown();
+ return;
+ }
+ }
+
+ @Override
+ public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e)
+ throws Exception {
+ channels.add(ctx.getChannel());
+ }
+
+ public boolean waitForCompletion(long timeout, TimeUnit timeoutUnit)
+ throws InterruptedException {
+ return completionLatch.await(timeout, timeoutUnit);
+ }
+ }
+
+ private class SendThrottle extends SimpleChannelUpstreamHandler {
+ private final DataSender sender;
+
+ public SendThrottle(DataSender sender) {
+ this.sender = sender;
+ }
+
+ @Override
+ public void channelInterestChanged(ChannelHandlerContext ctx,
+ ChannelStateEvent e) throws Exception {
+ boolean writeEnabled = ctx.getChannel().isWritable();
+ sender.setWriteEnabled(writeEnabled);
+
+ }
+ }
+
+ private class DataSender implements Runnable {
+
+ private AtomicReference channel =
+ new AtomicReference();
+
+ private long totalBytesSent = 0;
+
+ private long numWrites = 0;
+
+ private long runStartTime = System.currentTimeMillis();
+
+ private boolean firstRun = true;
+
+ private AtomicBoolean writeEnabled = new AtomicBoolean(true);
+
+ private AtomicBoolean running = new AtomicBoolean(false);
+
+ private CountDownLatch finishLatch = new CountDownLatch(1);
+
+ private String name;
+
+ private AtomicInteger nextWriteByte = new AtomicInteger(0);
+
+ public DataSender(String name) {
+ this.name = name;
+ }
+
+ public void setChannel(Channel channel) {
+ this.channel.set(channel);
+ }
+
+ public void setWriteEnabled(boolean enabled) {
+ writeEnabled.set(enabled);
+ if (enabled && !this.isRunning() && finishLatch.getCount() > 0) {
+ executor.execute(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!running.compareAndSet(false, true)) {
+ LOG.log(Level.WARNING,
+ "{0}: Attempt made to run duplicate sender!", name);
+ return;
+ }
+
+ if (finishLatch.getCount() == 0) {
+ LOG.log(Level.SEVERE,
+ "{0}: Attempt made to run after completion!", name);
+ }
+
+ if (firstRun) {
+ firstRun = false;
+ runStartTime = System.currentTimeMillis();
+ LOG.log(Level.INFO, "{0}: sending data", name);
+ }
+
+ while (totalBytesSent < BYTES_TO_SEND) {
+ if (!writeEnabled.get()) {
+ running.set(false);
+ return;
+ }
+
+ ChannelBuffer randomBytesForSend =
+ createRandomSizeBuffer(nextWriteByte);
+ totalBytesSent += randomBytesForSend.readableBytes();
+
+ channel.get().write(
+ ChannelBuffers.wrappedBuffer(randomBytesForSend));
+
+ numWrites ++;
+ if (numWrites % 100 == 0) {
+ LOG.log(Level.INFO,
+ "{0}: {1} writes dispatched, totalling {2} bytes",
+ new Object[] { name, numWrites, totalBytesSent });
+ }
+ }
+
+ LOG.log(Level.INFO, "{0}: completed send cycle", name);
+
+ long runEndTime = System.currentTimeMillis();
+ long totalTime = runEndTime - runStartTime;
+ long totalKB = totalBytesSent / 1024;
+ double rate = totalKB / (totalTime / 1000.0);
+ LOG.log(Level.INFO, "{0}: Sent {1} bytes", new Object[] { name,
+ totalBytesSent });
+ LOG.log(Level.INFO, "{0}: Average throughput: {1} KB/s",
+ new Object[] { name, rate });
+
+ finishLatch.countDown();
+ running.set(false);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public boolean waitForFinish(long timeout, TimeUnit timeoutUnit)
+ throws InterruptedException {
+ return finishLatch.await(timeout, timeoutUnit);
+ }
+
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelTest.java b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelTest.java
new file mode 100644
index 00000000000..c000fe425cf
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/HttpTunnelTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.bootstrap.ServerBootstrap;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandler;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+import org.jboss.netty.channel.group.DefaultChannelGroup;
+import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelTest {
+
+ private HttpTunnelClientChannelFactory clientFactory;
+
+ private HttpTunnelServerChannelFactory serverFactory;
+
+ private ClientBootstrap clientBootstrap;
+
+ private ServerBootstrap serverBootstrap;
+
+ ChannelGroup activeConnections;
+
+ ChannelHandler clientCaptureHandler;
+
+ ServerEndHandler connectionCaptureHandler;
+
+ Channel serverEnd;
+
+ CountDownLatch serverEndLatch;
+
+ ChannelBuffer receivedBytes;
+
+ CountDownLatch messageReceivedLatch;
+
+ ChannelBuffer clientReceivedBytes;
+
+ CountDownLatch clientMessageReceivedLatch;
+
+ private Channel serverChannel;
+
+ @Before
+ public void setUp() throws UnknownHostException {
+ activeConnections = new DefaultChannelGroup();
+ clientFactory =
+ new HttpTunnelClientChannelFactory(
+ new NioClientSocketChannelFactory(
+ Executors.newCachedThreadPool(),
+ Executors.newCachedThreadPool()));
+ serverFactory =
+ new HttpTunnelServerChannelFactory(
+ new NioServerSocketChannelFactory(
+ Executors.newCachedThreadPool(),
+ Executors.newCachedThreadPool()));
+
+ clientBootstrap = new ClientBootstrap(clientFactory);
+
+ clientCaptureHandler = new ClientEndHandler();
+ clientBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
+
+ public ChannelPipeline getPipeline() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("clientCapture", clientCaptureHandler);
+ return pipeline;
+ }
+ });
+
+ clientReceivedBytes = ChannelBuffers.dynamicBuffer();
+ clientMessageReceivedLatch = new CountDownLatch(1);
+
+ serverBootstrap = new ServerBootstrap(serverFactory);
+
+ connectionCaptureHandler = new ServerEndHandler();
+ serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
+
+ public ChannelPipeline getPipeline() throws Exception {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("capture", connectionCaptureHandler);
+ return pipeline;
+ }
+ });
+
+ serverEndLatch = new CountDownLatch(1);
+ receivedBytes = ChannelBuffers.dynamicBuffer();
+ messageReceivedLatch = new CountDownLatch(1);
+
+ serverChannel =
+ serverBootstrap.bind(new InetSocketAddress(InetAddress
+ .getLocalHost(), 12345));
+ activeConnections.add(serverChannel);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ activeConnections.disconnect().await(1000L);
+ clientBootstrap.releaseExternalResources();
+ serverBootstrap.releaseExternalResources();
+ }
+
+ @Test(timeout = 2000)
+ public void testConnectClientToServer() throws Exception {
+ ChannelFuture connectFuture =
+ clientBootstrap.connect(new InetSocketAddress(InetAddress
+ .getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+ assertTrue(connectFuture.isSuccess());
+ assertNotNull(connectFuture.getChannel());
+
+ Channel clientChannel = connectFuture.getChannel();
+ activeConnections.add(clientChannel);
+ assertEquals(serverChannel.getLocalAddress(),
+ clientChannel.getRemoteAddress());
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+ assertNotNull(serverEnd);
+ assertEquals(clientChannel.getLocalAddress(),
+ serverEnd.getRemoteAddress());
+ }
+
+ @Test
+ public void testSendDataFromClientToServer() throws Exception {
+ ChannelFuture connectFuture =
+ clientBootstrap.connect(new InetSocketAddress(InetAddress
+ .getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+
+ Channel clientEnd = connectFuture.getChannel();
+ activeConnections.add(clientEnd);
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+
+ ChannelFuture writeFuture =
+ Channels.write(clientEnd, NettyTestUtils.createData(100L));
+ assertTrue(writeFuture.await(1000L));
+ assertTrue(writeFuture.isSuccess());
+
+ assertTrue(messageReceivedLatch.await(1000L, TimeUnit.MILLISECONDS));
+ assertEquals(100L, receivedBytes.readLong());
+ }
+
+ @Test
+ public void testSendDataFromServerToClient() throws Exception {
+ ChannelFuture connectFuture =
+ clientBootstrap.connect(new InetSocketAddress(InetAddress
+ .getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+
+ Channel clientEnd = connectFuture.getChannel();
+ activeConnections.add(clientEnd);
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+
+ ChannelFuture writeFuture =
+ Channels.write(serverEnd, NettyTestUtils.createData(4321L));
+ assertTrue(writeFuture.await(1000L));
+ assertTrue(writeFuture.isSuccess());
+
+ assertTrue(clientMessageReceivedLatch
+ .await(1000, TimeUnit.MILLISECONDS));
+ assertEquals(4321L, clientReceivedBytes.readLong());
+ }
+
+ class ServerEndHandler extends SimpleChannelUpstreamHandler {
+
+ @Override
+ public void channelConnected(ChannelHandlerContext ctx,
+ ChannelStateEvent e) throws Exception {
+ serverEnd = e.getChannel();
+ activeConnections.add(serverEnd);
+ serverEndLatch.countDown();
+ super.channelConnected(ctx, e);
+ }
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
+ throws Exception {
+ receivedBytes.writeBytes((ChannelBuffer) e.getMessage());
+ messageReceivedLatch.countDown();
+ }
+ }
+
+ class ClientEndHandler extends SimpleChannelUpstreamHandler {
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
+ throws Exception {
+ clientReceivedBytes.writeBytes((ChannelBuffer) e.getMessage());
+ clientMessageReceivedLatch.countDown();
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/MockChannelStateListener.java b/src/test/java/org/jboss/netty/channel/socket/http/MockChannelStateListener.java
new file mode 100644
index 00000000000..73645f089d8
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/MockChannelStateListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class MockChannelStateListener implements HttpTunnelClientWorkerOwner {
+
+ public boolean fullyEstablished = false;
+
+ public List messages = new ArrayList();
+
+ public String tunnelId = null;
+
+ public String serverHostName = null;
+
+ public void fullyEstablished() {
+ fullyEstablished = true;
+ }
+
+ public void onConnectRequest(ChannelFuture connectFuture,
+ InetSocketAddress remoteAddress) {
+ // not relevant for test
+ }
+
+ public void onMessageReceived(ChannelBuffer content) {
+ messages.add(content);
+ }
+
+ public void onTunnelOpened(String tunnelId) {
+ this.tunnelId = tunnelId;
+ }
+
+ public String getServerHostName() {
+ return serverHostName;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtils.java b/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtils.java
new file mode 100644
index 00000000000..d896cacdfc2
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtils.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import junit.framework.Assert;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.jboss.netty.channel.UpstreamMessageEvent;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NettyTestUtils {
+
+ public static ByteBuffer convertReadable(ChannelBuffer b) {
+ int startIndex = b.readerIndex();
+ ByteBuffer converted = ByteBuffer.allocate(b.readableBytes());
+ b.readBytes(converted);
+ b.readerIndex(startIndex);
+ converted.flip();
+ return converted;
+ }
+
+ public static void assertEquals(ChannelBuffer expected, ChannelBuffer actual) {
+ if (expected.readableBytes() != actual.readableBytes()) {
+ Assert.failNotEquals(
+ "channel buffers have differing readable sizes",
+ expected.readableBytes(), actual.readableBytes());
+ }
+
+ int startPositionExpected = expected.readerIndex();
+ int startPositionActual = actual.readerIndex();
+ int position = 0;
+ while (expected.readable()) {
+ byte expectedByte = expected.readByte();
+ byte actualByte = actual.readByte();
+ if (expectedByte != actualByte) {
+ Assert.failNotEquals("channel buffers differ at position " +
+ position, expectedByte, actualByte);
+ }
+
+ position ++;
+ }
+
+ expected.readerIndex(startPositionExpected);
+ actual.readerIndex(startPositionActual);
+ }
+
+ public static boolean checkEquals(ChannelBuffer expected,
+ ChannelBuffer actual) {
+ if (expected.readableBytes() != actual.readableBytes()) {
+ return false;
+ }
+
+ int position = 0;
+ while (expected.readable()) {
+ byte expectedByte = expected.readByte();
+ byte actualByte = actual.readByte();
+ if (expectedByte != actualByte) {
+ return false;
+ }
+
+ position ++;
+ }
+
+ return true;
+ }
+
+ public static List splitIntoChunks(int chunkSize,
+ ChannelBuffer... buffers) {
+ LinkedList chunks = new LinkedList();
+
+ ArrayList sourceBuffers = new ArrayList();
+ Collections.addAll(sourceBuffers, buffers);
+ Iterator sourceIter = sourceBuffers.iterator();
+ ChannelBuffer chunk = ChannelBuffers.buffer(chunkSize);
+ while (sourceIter.hasNext()) {
+ ChannelBuffer source = sourceIter.next();
+
+ int index = source.readerIndex();
+ while (source.writerIndex() > index) {
+ int fragmentSize =
+ Math.min(source.writerIndex() - index,
+ chunk.writableBytes());
+ chunk.writeBytes(source, index, fragmentSize);
+ if (!chunk.writable()) {
+ chunks.add(chunk);
+ chunk = ChannelBuffers.buffer(chunkSize);
+ }
+ index += fragmentSize;
+ }
+ }
+
+ if (chunk.readable()) {
+ chunks.add(chunk);
+ }
+
+ return chunks;
+ }
+
+ public static ChannelBuffer createData(long containedNumber) {
+ ChannelBuffer data = ChannelBuffers.dynamicBuffer();
+ data.writeLong(containedNumber);
+ return data;
+ }
+
+ public static void checkIsUpstreamMessageEventContainingData(
+ ChannelEvent event, ChannelBuffer expectedData) {
+ ChannelBuffer data =
+ checkIsUpstreamMessageEvent(event, ChannelBuffer.class);
+ assertEquals(expectedData, data);
+ }
+
+ public static T checkIsUpstreamMessageEvent(ChannelEvent event,
+ Class expectedMessageType) {
+ assertTrue(event instanceof UpstreamMessageEvent);
+ UpstreamMessageEvent messageEvent = (UpstreamMessageEvent) event;
+ assertTrue(expectedMessageType.isInstance(messageEvent.getMessage()));
+ return expectedMessageType.cast(messageEvent.getMessage());
+ }
+
+ public static T checkIsDownstreamMessageEvent(ChannelEvent event,
+ Class expectedMessageType) {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(expectedMessageType.isInstance(messageEvent.getMessage()));
+ return expectedMessageType.cast(messageEvent.getMessage());
+ }
+
+ public static InetSocketAddress createAddress(byte[] addr, int port) {
+ try {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ public static Throwable checkIsExceptionEvent(ChannelEvent ev) {
+ assertTrue(ev instanceof ExceptionEvent);
+ ExceptionEvent exceptionEv = (ExceptionEvent) ev;
+ return exceptionEv.getCause();
+ }
+
+ public static ChannelStateEvent checkIsStateEvent(ChannelEvent event,
+ ChannelState expectedState, Object expectedValue) {
+ assertTrue(event instanceof ChannelStateEvent);
+ ChannelStateEvent stateEvent = (ChannelStateEvent) event;
+ Assert.assertEquals(expectedState, stateEvent.getState());
+ Assert.assertEquals(expectedValue, stateEvent.getValue());
+ return stateEvent;
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtilsTest.java b/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtilsTest.java
new file mode 100644
index 00000000000..2f71b647d86
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/NettyTestUtilsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NettyTestUtilsTest {
+
+ @Test
+ public void testSplitIntoChunks() {
+ ChannelBuffer a = createFullBuffer(20, (byte) 0);
+ ChannelBuffer b = createFullBuffer(20, (byte) 1);
+ ChannelBuffer c = createFullBuffer(20, (byte) 2);
+
+ List chunks =
+ NettyTestUtils.splitIntoChunks(10, a, b, c);
+ assertEquals(6, chunks.size());
+ for (ChannelBuffer chunk: chunks) {
+ assertEquals(10, chunk.readableBytes());
+ }
+
+ // reader index should not be modified by splitIntoChunks()
+ assertEquals(0, a.readerIndex());
+ assertEquals(0, b.readerIndex());
+ assertEquals(0, c.readerIndex());
+ }
+
+ @Test
+ public void testSplitIntoChunks_chunksCrossBoundaries() {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ List chunks = NettyTestUtils.splitIntoChunks(4, a, b, c);
+ assertEquals(4, chunks.size());
+ checkBufferContains(chunks.get(0), new byte[] { 0, 0, 0, 0 });
+ checkBufferContains(chunks.get(1), new byte[] { 0, 1, 1, 1 });
+ checkBufferContains(chunks.get(2), new byte[] { 1, 1, 2, 2 });
+ checkBufferContains(chunks.get(3), new byte[] { 2, 2, 2 });
+ }
+
+ @Test
+ public void testSplitIntoChunks_smallestChunksPossible() {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ List chunks = NettyTestUtils.splitIntoChunks(1, a, b, c);
+ assertEquals(15, chunks.size());
+ checkBufferContains(chunks.get(0), new byte[] { 0 });
+ checkBufferContains(chunks.get(5), new byte[] { 1 });
+ checkBufferContains(chunks.get(10), new byte[] { 2 });
+ }
+
+ @Test
+ public void testSplitIntoChunks_sourceBuffersArePartiallyRead() {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ a.readerIndex(1);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ b.readerIndex(2);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ // will be ignored, as fully read
+ ChannelBuffer d = createFullBuffer(5, (byte) 3);
+ d.readerIndex(5);
+ ChannelBuffer e = createFullBuffer(5, (byte) 4);
+ e.readerIndex(4);
+
+ List chunks =
+ NettyTestUtils.splitIntoChunks(3, a, b, c, d, e);
+ checkBufferContains(chunks.get(0), new byte[] { 0, 0, 0 });
+ checkBufferContains(chunks.get(1), new byte[] { 0, 1, 1 });
+ checkBufferContains(chunks.get(2), new byte[] { 1, 2, 2 });
+ checkBufferContains(chunks.get(3), new byte[] { 2, 2, 2 });
+ checkBufferContains(chunks.get(4), new byte[] { 4 });
+ }
+
+ private void checkBufferContains(ChannelBuffer channelBuffer, byte[] bs) {
+ if (channelBuffer.readableBytes() != bs.length) {
+ fail("buffer does not have enough bytes");
+ }
+
+ for (int i = 0; i < bs.length; i ++) {
+ assertEquals("byte at position " + i + " does not match", bs[i],
+ channelBuffer.getByte(i));
+ }
+ }
+
+ private ChannelBuffer createFullBuffer(int size, byte value) {
+ ChannelBuffer buffer = ChannelBuffers.buffer(size);
+ byte[] contents = new byte[size];
+ for (int i = 0; i < contents.length; i ++) {
+ contents[i] = value;
+ }
+ buffer.writeBytes(contents);
+ return buffer;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/NullChannelHandler.java b/src/test/java/org/jboss/netty/channel/socket/http/NullChannelHandler.java
new file mode 100644
index 00000000000..1c080255ec3
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/NullChannelHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import org.jboss.netty.channel.ChannelDownstreamHandler;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelUpstreamHandler;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NullChannelHandler implements ChannelUpstreamHandler,
+ ChannelDownstreamHandler {
+
+ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
+ throws Exception {
+ ctx.sendUpstream(e);
+ }
+
+ public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e)
+ throws Exception {
+ ctx.sendDownstream(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/SaturationManagerTest.java b/src/test/java/org/jboss/netty/channel/socket/http/SaturationManagerTest.java
new file mode 100644
index 00000000000..fcd4dca6974
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/SaturationManagerTest.java
@@ -0,0 +1,32 @@
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.*;
+import static org.jboss.netty.channel.socket.http.SaturationStateChange.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SaturationManagerTest {
+
+ private SaturationManager manager;
+
+ @Before
+ public void setUp() {
+ manager = new SaturationManager(100L, 200L);
+ }
+
+ @Test
+ public void testQueueSizeChanged() {
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(100L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(99L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(1L));
+ assertEquals(SATURATED, manager.queueSizeChanged(1L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(10L));
+
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-10L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-1L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-1L));
+ assertEquals(DESATURATED, manager.queueSizeChanged(-99L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-100L));
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchTest.java b/src/test/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchTest.java
new file mode 100644
index 00000000000..142cb440ac9
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.http.ServerMessageSwitchUpstreamInterface.TunnelStatus;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class ServerMessageSwitchTest {
+
+ public static final InetSocketAddress REMOTE_ADDRESS = InetSocketAddress
+ .createUnresolved("test.client.com", 52354);
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private ServerMessageSwitch messageSwitch;
+
+ HttpTunnelAcceptedChannelFactory newChannelFactory;
+
+ private FakeChannelSink responseCatcher;
+
+ private FakeSocketChannel htunChannel;
+
+ private FakeSocketChannel requesterChannel;
+
+ private HttpTunnelAcceptedChannelReceiver htunAcceptedChannel;
+
+ @Before
+ public void setUp() throws Exception {
+ newChannelFactory =
+ mockContext.mock(HttpTunnelAcceptedChannelFactory.class);
+ messageSwitch = new ServerMessageSwitch(newChannelFactory);
+
+ htunAcceptedChannel =
+ mockContext.mock(HttpTunnelAcceptedChannelReceiver.class);
+ createRequesterChannel();
+
+ mockContext.checking(new Expectations() {
+ {
+ one(newChannelFactory).newChannel(with(any(String.class)),
+ with(equal(REMOTE_ADDRESS)));
+ will(returnValue(htunAcceptedChannel));
+ ignoring(newChannelFactory).generateTunnelId();
+ will(returnValue("TEST_TUNNEL"));
+ }
+ });
+ }
+
+ private FakeSocketChannel createRequesterChannel() {
+ ChannelPipeline requesterChannelPipeline = Channels.pipeline();
+ responseCatcher = new FakeChannelSink();
+ requesterChannel =
+ new FakeSocketChannel(null, null, requesterChannelPipeline,
+ responseCatcher);
+ responseCatcher.events.clear();
+
+ return requesterChannel;
+ }
+
+ @Test
+ public void testRouteInboundData() {
+ final ChannelBuffer inboundData = ChannelBuffers.dynamicBuffer();
+ inboundData.writeLong(1234L);
+
+ mockContext.checking(new Expectations() {
+ {
+ one(htunAcceptedChannel).dataReceived(with(same(inboundData)));
+ }
+ });
+
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.routeInboundData(tunnelId, inboundData);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testRouteOutboundData_onPoll() {
+ ChannelBuffer outboundData = ChannelBuffers.dynamicBuffer();
+ outboundData.writeLong(1234L);
+
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.routeOutboundData(tunnelId, outboundData,
+ Channels.future(htunChannel));
+ messageSwitch.pollOutboundData(tunnelId, requesterChannel);
+
+ assertEquals(1, responseCatcher.events.size());
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ responseCatcher.events.poll(), HttpResponse.class);
+ NettyTestUtils.assertEquals(outboundData, response.getContent());
+ }
+
+ @Test
+ public void testRouteOutboundData_withDanglingRequest() {
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.pollOutboundData(tunnelId, requesterChannel);
+ assertEquals(0, responseCatcher.events.size());
+
+ ChannelBuffer outboundData = ChannelBuffers.dynamicBuffer();
+ outboundData.writeLong(1234L);
+
+ messageSwitch.routeOutboundData(tunnelId, outboundData,
+ Channels.future(htunChannel));
+ assertEquals(1, responseCatcher.events.size());
+ HttpResponse response =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ responseCatcher.events.poll(), HttpResponse.class);
+ NettyTestUtils.assertEquals(outboundData, response.getContent());
+ }
+
+ @Test
+ public void testCloseTunnel() {
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+ assertEquals(
+ TunnelStatus.CLOSED,
+ messageSwitch.routeInboundData(tunnelId,
+ ChannelBuffers.dynamicBuffer()));
+ }
+
+ /* TODO: require tests that check the various permutations of a client sending or polling
+ data after the server has closed the connection */
+
+ /* TODO: require tests that check what happens when a client closes a connection */
+
+ @Test
+ public void testRouteInboundDataIgnoredAfterClose() {
+ ChannelBuffer data = NettyTestUtils.createData(1234L);
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+
+ mockContext.checking(new Expectations() {
+ {
+ never(htunAcceptedChannel).dataReceived(
+ with(any(ChannelBuffer.class)));
+ }
+ });
+
+ messageSwitch.routeInboundData(tunnelId, data);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testRouteOutboundDataIgnoredAfterClose() {
+ ChannelBuffer data = NettyTestUtils.createData(1234L);
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+ messageSwitch.routeOutboundData(tunnelId, data,
+ Channels.future(htunChannel));
+ assertEquals(0, responseCatcher.events.size());
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/UpstreamEventCatcher.java b/src/test/java/org/jboss/netty/channel/socket/http/UpstreamEventCatcher.java
new file mode 100644
index 00000000000..95401bf8ff7
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/UpstreamEventCatcher.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelUpstreamHandler;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class UpstreamEventCatcher implements ChannelUpstreamHandler {
+
+ public static final String NAME = "upstreamCatcher";
+
+ public Queue events = new LinkedList();
+
+ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
+ throws Exception {
+ events.add(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/WriteFragmenterTest.java b/src/test/java/org/jboss/netty/channel/socket/http/WriteFragmenterTest.java
new file mode 100644
index 00000000000..cf03851be09
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/WriteFragmenterTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class WriteFragmenterTest {
+
+ private FakeSocketChannel channel;
+
+ private WriteFragmenter fragmenter;
+
+ private FakeChannelSink downstreamCatcher;
+
+ @Before
+ public void setUp() throws Exception {
+ fragmenter = new WriteFragmenter(100);
+
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast(WriteFragmenter.NAME, fragmenter);
+ downstreamCatcher = new FakeChannelSink();
+ channel =
+ new FakeSocketChannel(null, null, pipeline, downstreamCatcher);
+ }
+
+ @Test
+ public void testLeavesWritesBeneathThresholdUntouched() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[99]);
+ Channels.write(channel, data);
+
+ assertEquals(1, downstreamCatcher.events.size());
+ ChannelBuffer sentData =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(), ChannelBuffer.class);
+ assertSame(data, sentData);
+ }
+
+ @Test
+ public void testLeavesMessagesOnThresholdUntouched() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[100]);
+ Channels.write(channel, data);
+
+ assertEquals(1, downstreamCatcher.events.size());
+ ChannelBuffer sentData =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(), ChannelBuffer.class);
+ assertSame(data, sentData);
+ }
+
+ @Test
+ public void testSplitsMessagesAboveThreshold_twoChunks() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[101]);
+ Channels.write(channel, data);
+
+ assertEquals(2, downstreamCatcher.events.size());
+ ChannelBuffer chunk1 =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(), ChannelBuffer.class);
+ ChannelBuffer chunk2 =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(), ChannelBuffer.class);
+ assertEquals(100, chunk1.readableBytes());
+ assertEquals(1, chunk2.readableBytes());
+ }
+
+ @Test
+ public void testSplitsMessagesAboveThreshold_multipleChunks() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 25; i ++) {
+ ChannelBuffer chunk =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertEquals(100, chunk.readableBytes());
+ }
+
+ ChannelBuffer endChunk =
+ NettyTestUtils.checkIsDownstreamMessageEvent(
+ downstreamCatcher.events.poll(), ChannelBuffer.class);
+ assertEquals(40, endChunk.readableBytes());
+ }
+
+ @Test
+ public void testChannelFutureTriggeredOnlyWhenAllChunksWritten() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ ChannelFuture mainWriteFuture = Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 25; i ++) {
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture()
+ .setSuccess();
+ assertFalse(mainWriteFuture.isDone());
+ }
+
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture()
+ .setSuccess();
+ assertTrue(mainWriteFuture.isDone());
+ assertTrue(mainWriteFuture.isSuccess());
+ }
+
+ @Test
+ public void testChannelFutureFailsOnFirstWriteFailure() {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ ChannelFuture mainWriteFuture = Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 10; i ++) {
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture()
+ .setSuccess();
+ assertFalse(mainWriteFuture.isDone());
+ }
+
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture()
+ .setFailure(new Exception("Something bad happened"));
+ assertTrue(mainWriteFuture.isDone());
+ assertFalse(mainWriteFuture.isSuccess());
+
+ // check all the subsequent writes got cancelled
+ for (int i = 0; i < 15; i ++) {
+ assertTrue(((MessageEvent) downstreamCatcher.events.poll())
+ .getFuture().isCancelled());
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/http/WriteSplitterTest.java b/src/test/java/org/jboss/netty/channel/socket/http/WriteSplitterTest.java
new file mode 100644
index 00000000000..d0258f54614
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/http/WriteSplitterTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jboss.netty.channel.socket.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class WriteSplitterTest {
+
+ private static final int SPLIT_THRESHOLD = 1024;
+
+ @Test
+ public void testSplit_bufferUnderThreshold() {
+ ChannelBuffer buffer = createBufferWithContents(800);
+ List fragments =
+ WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(1, fragments.size());
+ }
+
+ @Test
+ public void testSplit_bufferMatchesThreshold() {
+ ChannelBuffer buffer = createBufferWithContents(SPLIT_THRESHOLD);
+ List fragments =
+ WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(1, fragments.size());
+ }
+
+ @Test
+ public void testSplit_bufferOverThreshold() {
+ ChannelBuffer buffer =
+ createBufferWithContents((int) (SPLIT_THRESHOLD * 1.5));
+ List fragments =
+ WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(2, fragments.size());
+
+ ChannelBuffer fragment1 = fragments.get(0);
+ checkMatches(buffer, fragment1);
+ ChannelBuffer fragment2 = fragments.get(1);
+ checkMatches(buffer, fragment2);
+ }
+
+ @Test
+ public void testSplit_largeNumberOfFragments() {
+ ChannelBuffer buffer = createBufferWithContents(SPLIT_THRESHOLD * 250);
+ List fragments =
+ WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(250, fragments.size());
+
+ for (ChannelBuffer fragment: fragments) {
+ checkMatches(buffer, fragment);
+ }
+ }
+
+ private void checkMatches(ChannelBuffer mainBuffer, ChannelBuffer fragment) {
+ assertTrue(mainBuffer.readableBytes() >= fragment.readableBytes());
+ while (fragment.readable()) {
+ assertEquals(mainBuffer.readByte(), fragment.readByte());
+ }
+ }
+
+ private ChannelBuffer createBufferWithContents(int size) {
+ byte[] contents = new byte[size];
+ for (int i = 0; i < contents.length; i ++) {
+ contents[i] = (byte) (i % 10);
+ }
+
+ return ChannelBuffers.copiedBuffer(contents);
+ }
+
+}