diff --git a/pom.xml b/pom.xml index 7a7b9ab0d2..b279bfc7ae 100644 --- a/pom.xml +++ b/pom.xml @@ -177,6 +177,12 @@ 2.5.2 test + + org.jmock + jmock-junit4 + 2.5.1 + test + org.slf4j slf4j-simple @@ -254,7 +260,7 @@ maven-surefire-plugin 2.7.2 - never + once **/Abstract* **/TestUtil* diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelPipelineFactory.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelPipelineFactory.java new file mode 100644 index 0000000000..e5155eea0b --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelPipelineFactory.java @@ -0,0 +1,56 @@ +/* + * 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.httptunnel; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; + +/** + * Creates pipelines for incoming http tunnel connections, capable of decoding the incoming HTTP + * requests, determining their type (client sending data, client polling data, or unknown) and + * handling them appropriately. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class AcceptedServerChannelPipelineFactory implements ChannelPipelineFactory +{ + + private final ServerMessageSwitch messageSwitch; + + public AcceptedServerChannelPipelineFactory(ServerMessageSwitch messageSwitch) + { + this.messageSwitch = messageSwitch; + } + + public ChannelPipeline getPipeline() throws Exception + { + ChannelPipeline pipeline = Channels.pipeline(); + + pipeline.addLast("httpResponseEncoder", new HttpResponseEncoder()); + pipeline.addLast("httpRequestDecoder", new HttpRequestDecoder()); + pipeline.addLast("httpChunkAggregator", new HttpChunkAggregator(HttpTunnelMessageUtils.MAX_BODY_SIZE)); + pipeline.addLast("messageSwitchClient", new AcceptedServerChannelRequestDispatch(messageSwitch)); + + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatch.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatch.java new file mode 100644 index 0000000000..3dd5db3603 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatch.java @@ -0,0 +1,194 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Upstream handler which is responsible for determining whether a received HTTP request is a legal + * tunnel request, and if so, invoking the appropriate request method on the + * {@link ServerMessageSwitch} to service the request. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class AcceptedServerChannelRequestDispatch extends SimpleChannelUpstreamHandler +{ + + public static final String NAME = "AcceptedServerChannelRequestDispatch"; + + private static final InternalLogger LOG = InternalLoggerFactory + .getInstance(AcceptedServerChannelRequestDispatch.class); + + private final ServerMessageSwitchUpstreamInterface messageSwitch; + + public AcceptedServerChannelRequestDispatch(ServerMessageSwitchUpstreamInterface messageSwitch) + { + this.messageSwitch = messageSwitch; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception + { + HttpRequest request = (HttpRequest) e.getMessage(); + + if (HttpTunnelMessageUtils.isOpenTunnelRequest(request)) + { + handleOpenTunnel(ctx); + } + else if (HttpTunnelMessageUtils.isSendDataRequest(request)) + { + handleSendData(ctx, request); + } + else if (HttpTunnelMessageUtils.isReceiveDataRequest(request)) + { + handleReceiveData(ctx, request); + } + else if (HttpTunnelMessageUtils.isCloseTunnelRequest(request)) + { + handleCloseTunnel(ctx, request); + } + else + { + respondWithRejection(ctx, request, "invalid request to netty HTTP tunnel gateway"); + } + } + + private void handleOpenTunnel(ChannelHandlerContext ctx) + { + String tunnelId = messageSwitch.createTunnel((InetSocketAddress) ctx.getChannel().getRemoteAddress()); + if (LOG.isDebugEnabled()) + { + LOG.debug("open tunnel request received from " + ctx.getChannel().getRemoteAddress() + " - allocated ID " + + tunnelId); + } + respondWith(ctx, HttpTunnelMessageUtils.createTunnelOpenResponse(tunnelId)); + } + + private void handleCloseTunnel(ChannelHandlerContext ctx, HttpRequest request) + { + String tunnelId = checkTunnelId(request, ctx); + if (tunnelId == null) + { + return; + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("close tunnel request received for tunnel " + tunnelId); + } + messageSwitch.clientCloseTunnel(tunnelId); + respondWith(ctx, HttpTunnelMessageUtils.createTunnelCloseResponse()).addListener(ChannelFutureListener.CLOSE); + } + + private void handleSendData(ChannelHandlerContext ctx, HttpRequest request) + { + String tunnelId = checkTunnelId(request, ctx); + if (tunnelId == null) + { + return; + } + if (LOG.isDebugEnabled()) + { + LOG.debug("send data request received for tunnel " + tunnelId); + } + + if (HttpHeaders.getContentLength(request) == 0 || request.getContent() == null + || request.getContent().readableBytes() == 0) + { + respondWithRejection(ctx, request, "Send data requests must contain data"); + return; + } + + messageSwitch.routeInboundData(tunnelId, request.getContent()); + respondWith(ctx, HttpTunnelMessageUtils.createSendDataResponse()); + } + + private void handleReceiveData(ChannelHandlerContext ctx, HttpRequest request) + { + String tunnelId = checkTunnelId(request, ctx); + if (tunnelId == null) + { + return; + } + if (LOG.isDebugEnabled()) + { + LOG.debug("poll data request received for tunnel " + tunnelId); + } + messageSwitch.pollOutboundData(tunnelId, ctx.getChannel()); + } + + private String checkTunnelId(HttpRequest request, ChannelHandlerContext ctx) + { + String tunnelId = HttpTunnelMessageUtils.extractTunnelId(request); + if (tunnelId == null) + { + respondWithRejection(ctx, request, "no tunnel id specified in request"); + } + else if (!messageSwitch.isOpenTunnel(tunnelId)) + { + respondWithRejection(ctx, request, "specified tunnel is either closed or does not exist"); + return null; + } + + return tunnelId; + } + + /** + * Sends the provided response back on the channel, returning the created ChannelFuture + * for this operation. + */ + private ChannelFuture respondWith(ChannelHandlerContext ctx, HttpResponse response) + { + ChannelFuture writeFuture = Channels.future(ctx.getChannel()); + Channels.write(ctx, writeFuture, response); + return writeFuture; + } + + /** + * Sends an HTTP 400 message back to on the channel with the specified error message, and asynchronously + * closes the channel after this is successfully sent. + */ + private void respondWithRejection(ChannelHandlerContext ctx, HttpRequest rejectedRequest, String errorMessage) + { + if (LOG.isWarnEnabled()) + { + SocketAddress remoteAddress = ctx.getChannel().getRemoteAddress(); + String tunnelId = HttpTunnelMessageUtils.extractTunnelId(rejectedRequest); + if (tunnelId == null) + { + tunnelId = ""; + } + LOG.warn("Rejecting request from " + remoteAddress + " representing tunnel " + tunnelId + ": " + errorMessage); + } + HttpResponse rejection = HttpTunnelMessageUtils.createRejection(rejectedRequest, errorMessage); + respondWith(ctx, rejection).addListener(ChannelFutureListener.CLOSE); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/ChannelFutureAggregator.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ChannelFutureAggregator.java new file mode 100644 index 0000000000..bada6091cd --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ChannelFutureAggregator.java @@ -0,0 +1,78 @@ +/* + * 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.httptunnel; + +import java.util.HashSet; +import java.util.Set; + +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; + +/** + * Class which is used to consolidate multiple channel futures into one, by + * listening to the individual futures and producing an aggregated result + * (success/failure) when all futures have completed. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class ChannelFutureAggregator implements ChannelFutureListener +{ + + private final ChannelFuture aggregateFuture; + + private final Set pendingFutures; + + public ChannelFutureAggregator(ChannelFuture aggregateFuture) + { + this.aggregateFuture = aggregateFuture; + pendingFutures = new HashSet(); + } + + public void addFuture(ChannelFuture future) + { + pendingFutures.add(future); + future.addListener(this); + } + + public synchronized void operationComplete(ChannelFuture future) throws Exception + { + if (future.isCancelled()) + { + // TODO: what should the correct behaviour be when a fragment is cancelled? + // cancel all outstanding fragments and cancel the aggregate? + return; + } + + pendingFutures.remove(future); + if (!future.isSuccess()) + { + aggregateFuture.setFailure(future.getCause()); + for (ChannelFuture pendingFuture : pendingFutures) + { + pendingFuture.cancel(); + } + return; + } + + if (pendingFutures.isEmpty()) + { + aggregateFuture.setSuccess(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/DefaultTunnelIdGenerator.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/DefaultTunnelIdGenerator.java new file mode 100644 index 0000000000..041aaac744 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/DefaultTunnelIdGenerator.java @@ -0,0 +1,53 @@ +/* + * 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.httptunnel; + +import java.security.SecureRandom; + +/** + * Default implementation of TunnelIdGenerator, which uses a + * {@link java.security.SecureRandom SecureRandom} generator + * to produce 32-bit tunnel identifiers. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class DefaultTunnelIdGenerator implements TunnelIdGenerator +{ + + private SecureRandom generator; + + public DefaultTunnelIdGenerator() + { + this(new SecureRandom()); + } + + public DefaultTunnelIdGenerator(SecureRandom generator) + { + this.generator = generator; + } + + public synchronized String generateId() + { + // synchronized to ensure that this code is thread safe. The Sun + // standard implementations seem to be synchronized or lock free + // but are not documented as guaranteeing this + return Integer.toHexString(generator.nextInt()); + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannel.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannel.java new file mode 100644 index 0000000000..1d10936f5e --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannel.java @@ -0,0 +1,117 @@ +/* + * 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.httptunnel; + +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.buffer.ChannelBuffer; +import org.jboss.netty.channel.AbstractChannel; +import org.jboss.netty.channel.ChannelFactory; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.socket.SocketChannel; +import org.jboss.netty.channel.socket.SocketChannelConfig; + +/** + * Represents the server end of an HTTP tunnel, created after a legal tunnel creation + * request is received from a client. The server end of a tunnel does not have any + * directly related TCP connections - the connections used by a client are likely + * to change over the lifecycle of a tunnel, especially when an HTTP proxy is in + * use. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelAcceptedChannel extends AbstractChannel implements SocketChannel, HttpTunnelAcceptedChannelReceiver +{ + private final HttpTunnelAcceptedChannelConfig config; + + private final HttpTunnelAcceptedChannelSink sink; + + private final InetSocketAddress remoteAddress; + + protected HttpTunnelAcceptedChannel(HttpTunnelServerChannel parent, ChannelFactory factory, + ChannelPipeline pipeline, HttpTunnelAcceptedChannelSink sink, InetSocketAddress remoteAddress, HttpTunnelAcceptedChannelConfig config) + { + super(parent, factory, pipeline, sink); + this.config = config; + this.sink = sink; + this.remoteAddress = remoteAddress; + fireChannelOpen(this); + fireChannelBound(this, getLocalAddress()); + fireChannelConnected(this, getRemoteAddress()); + } + + public SocketChannelConfig getConfig() + { + return config; + } + + public InetSocketAddress getLocalAddress() + { + + return ((HttpTunnelServerChannel) getParent()).getLocalAddress(); + } + + public InetSocketAddress getRemoteAddress() + { + return remoteAddress; + } + + public boolean isBound() + { + return sink.isActive(); + } + + public boolean isConnected() + { + return sink.isActive(); + } + + public void clientClosed() + { + this.setClosed(); + Channels.fireChannelClosed(this); + } + + public void dataReceived(ChannelBuffer data) + { + Channels.fireMessageReceived(this, data); + } + + public void updateInterestOps(SaturationStateChange transition) { + switch(transition) { + case SATURATED: fireWriteEnabled(false); break; + case DESATURATED: fireWriteEnabled(true); break; + case NO_CHANGE: break; + } + } + + private void fireWriteEnabled(boolean enabled) { + int ops = OP_READ; + if(!enabled) { + ops |= OP_WRITE; + } + + setInterestOpsNow(ops); + Channels.fireChannelInterestChanged(this); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelConfig.java new file mode 100644 index 0000000000..bdfa60109e --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelConfig.java @@ -0,0 +1,130 @@ +/* + * 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.httptunnel; + +/** + * Configuration the server end of an http tunnel. + * + * These properties largely have no effect in the current implementation, and exist + * for API compatibility with TCP channels. With the exception of high / low water + * marks, any changes in the values will not be honoured. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelAcceptedChannelConfig extends HttpTunnelChannelConfig +{ + + private static final int SO_LINGER_DISABLED = -1; + + private static final int FAKE_SEND_BUFFER_SIZE = 16 * 1024; + + private static final int FAKE_RECEIVE_BUFFER_SIZE = 16 * 1024; + + // based on the values in RFC 791 + private static final int DEFAULT_TRAFFIC_CLASS = 0; + + @Override + public boolean isTcpNoDelay() + { + return true; + } + + @Override + public void setTcpNoDelay(boolean tcpNoDelay) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public int getSoLinger() + { + return SO_LINGER_DISABLED; + } + + @Override + public void setSoLinger(int soLinger) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public int getSendBufferSize() + { + return FAKE_SEND_BUFFER_SIZE; + } + + @Override + public void setSendBufferSize(int sendBufferSize) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public int getReceiveBufferSize() + { + return FAKE_RECEIVE_BUFFER_SIZE; + } + + @Override + public void setReceiveBufferSize(int receiveBufferSize) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public boolean isKeepAlive() + { + return true; + } + + @Override + public void setKeepAlive(boolean keepAlive) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public int getTrafficClass() + { + return DEFAULT_TRAFFIC_CLASS; + } + + @Override + public void setTrafficClass(int trafficClass) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public boolean isReuseAddress() + { + return false; + } + + @Override + public void setReuseAddress(boolean reuseAddress) + { + // we do not allow the value to be changed, as it will not be honoured + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) + { + // we do not allow the value to be changed, as it will not be honoured + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelFactory.java new file mode 100644 index 0000000000..600ac39851 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelFactory.java @@ -0,0 +1,34 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; + +/** + * Simple interface provided to a {@link ServerMessageSwitch}, allowing it to + * create the server end of tunnels in response to legal tunnel creation + * requests from clients. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface HttpTunnelAcceptedChannelFactory +{ + public HttpTunnelAcceptedChannelReceiver newChannel(String newTunnelId, InetSocketAddress remoteAddress); + + public String generateTunnelId(); +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelReceiver.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelReceiver.java new file mode 100644 index 0000000000..ab9852273a --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelReceiver.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.httptunnel; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * Interface from the server message switch and channel sink to an + * accepted channel. Exists primarily for mock testing purposes. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface HttpTunnelAcceptedChannelReceiver +{ + + public void updateInterestOps(SaturationStateChange transition); + + public void dataReceived(ChannelBuffer data); + + public void clientClosed(); + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSink.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSink.java new file mode 100644 index 0000000000..29e631576f --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSink.java @@ -0,0 +1,133 @@ +/* + * 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.httptunnel; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.AbstractChannelSink; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; + +/** + * Sink for the server end of an http tunnel. Data sent down through the server end is dispatched + * from here to the ServerMessageSwitch, which queues the data awaiting a poll request from the + * client end of the tunnel. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelAcceptedChannelSink extends AbstractChannelSink +{ + + private final SaturationManager saturationManager; + private final ServerMessageSwitchDownstreamInterface messageSwitch; + + private final String tunnelId; + + private AtomicBoolean active = new AtomicBoolean(false); + private HttpTunnelAcceptedChannelConfig config; + + public HttpTunnelAcceptedChannelSink(ServerMessageSwitchDownstreamInterface messageSwitch, String tunnelId, HttpTunnelAcceptedChannelConfig config) + { + this.messageSwitch = messageSwitch; + this.tunnelId = tunnelId; + this.config = config; + this.saturationManager = new SaturationManager(config.getWriteBufferLowWaterMark(), config.getWriteBufferHighWaterMark()); + } + + public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception + { + if (e instanceof MessageEvent) + { + handleMessageEvent((MessageEvent) e); + } + else if (e instanceof ChannelStateEvent) + { + handleStateEvent((ChannelStateEvent) e); + } + } + + private void handleMessageEvent(MessageEvent ev) + { + if (!(ev.getMessage() instanceof ChannelBuffer)) + { + throw new IllegalArgumentException("Attempt to send data which is not a ChannelBuffer:" + ev.getMessage()); + } + + final HttpTunnelAcceptedChannelReceiver channel = (HttpTunnelAcceptedChannelReceiver) ev.getChannel(); + final ChannelBuffer message = (ChannelBuffer) ev.getMessage(); + final int messageSize = message.readableBytes(); + final ChannelFuture future = ev.getFuture(); + + saturationManager.updateThresholds(config.getWriteBufferLowWaterMark(), config.getWriteBufferHighWaterMark()); + channel.updateInterestOps(saturationManager.queueSizeChanged(messageSize)); + future.addListener(new ChannelFutureListener() + { + + @Override + public void operationComplete(ChannelFuture future) throws Exception + { + channel.updateInterestOps(saturationManager.queueSizeChanged(-messageSize)); + } + }); + messageSwitch.routeOutboundData(tunnelId, message, future); + } + + private void handleStateEvent(ChannelStateEvent ev) + { + /* TODO: as any of disconnect, unbind or close destroys a server + channel, should we fire all three events always? */ + Channel owner = ev.getChannel(); + switch (ev.getState()) + { + case OPEN : + if (Boolean.FALSE.equals(ev.getValue())) + { + messageSwitch.serverCloseTunnel(tunnelId); + active.set(false); + Channels.fireChannelClosed(owner); + } + break; + case BOUND : + if (ev.getValue() == null) + { + messageSwitch.serverCloseTunnel(tunnelId); + active.set(false); + Channels.fireChannelUnbound(owner); + } + case CONNECTED : + if (ev.getValue() == null) + { + messageSwitch.serverCloseTunnel(tunnelId); + active.set(false); + Channels.fireChannelDisconnected(owner); + } + } + } + + public boolean isActive() + { + return active.get(); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelChannelConfig.java new file mode 100644 index 0000000000..d8821b3793 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelChannelConfig.java @@ -0,0 +1,164 @@ +/* + * 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.httptunnel; + +import org.jboss.netty.channel.DefaultChannelConfig; +import org.jboss.netty.channel.socket.SocketChannelConfig; + +/** + * Configuration for HTTP tunnels. Where possible, properties set on this configuration will + * be applied to the two channels that service sending and receiving data on this end of the + * tunnel. + *

+ * HTTP tunnel clients have the following additional options: + * + * + * + * + * + * + * + *
NameAssociated setter method
{@code "writeBufferHighWaterMark"}{@link #setWriteBufferHighWaterMark(int)}
{@code "writeBufferLowWaterMark"}{@link #setWriteBufferLowWaterMark(int)}
+ * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public abstract class HttpTunnelChannelConfig extends DefaultChannelConfig implements SocketChannelConfig +{ + + /** + * The minimum value that the high water mark may be set to, in addition to the + * constraint that the high water mark must be strictly greater than the low + * water mark. + */ + public static final int MIN_HIGH_WATER_MARK = 1; + + /** + * The minimum value that the low water mark may be set to. + */ + public static final int MIN_LOW_WATER_MARK = 0; + + /** + * The default level for the write buffer's high water mark, presently set to + * 64KByte. + */ + public static final int DEFAULT_HIGH_WATER_MARK = 64 * 1024; + + /** + * The default level for the write buffer's low water mark, presently set to + * 32KByte. + */ + public static final int DEFAULT_LOW_WATER_MARK = 32 * 1024; + + static final String HIGH_WATER_MARK_OPTION = "writeBufferhHighWaterMark"; + + static final String LOW_WATER_MARK_OPTION = "writeBufferLowWaterMark"; + + + protected volatile int writeBufferLowWaterMark = DEFAULT_LOW_WATER_MARK; + + protected volatile int writeBufferHighWaterMark = DEFAULT_HIGH_WATER_MARK; + + /** + * @return the current value (in bytes) of the high water mark. + */ + public int getWriteBufferHighWaterMark() + { + return writeBufferHighWaterMark; + } + + /** + * Similarly to {@link org.jboss.netty.channel.socket.nio.NioSocketChannelConfig#setWriteBufferHighWaterMark(int) + * NioSocketChannelConfig.setWriteBufferHighWaterMark()}, + * the high water mark refers to the buffer size at which a user of the channel should stop writing. When the + * number of queued bytes exceeds the high water mark, {@link org.jboss.netty.channel.Channel#isWritable() Channel.isWritable()} will + * return false. Once the number of queued bytes falls below the {@link #setWriteBufferLowWaterMark(int) low water mark}, + * {@link org.jboss.netty.channel.Channel#isWritable() Channel.isWritable()} will return true again, indicating that the client + * can begin to send more data. + * + * @param level the number of queued bytes required to flip {@link org.jboss.netty.channel.Channel#isWritable()} to + * false. + * + * @see {@link org.jboss.netty.channel.socket.nio.NioSocketChannelConfig#setWriteBufferHighWaterMark(int) NioSocketChannelConfig.setWriteBufferHighWaterMark()} + */ + public void setWriteBufferHighWaterMark(int level) + { + if (level <= writeBufferLowWaterMark) + { + throw new IllegalArgumentException( + "Write buffer high water mark must be strictly greater than the low water mark"); + } + + if (level < MIN_HIGH_WATER_MARK) + { + throw new IllegalArgumentException("Cannot set write buffer high water mark lower than " + MIN_HIGH_WATER_MARK); + } + + writeBufferHighWaterMark = level; + } + + /** + * @return the current value (in bytes) of the low water mark. + */ + public int getWriteBufferLowWaterMark() + { + return writeBufferLowWaterMark; + } + + /** + * The low water mark refers to the "safe" size of the queued byte buffer at which more data can be enqueued. When + * the {@link #setWriteBufferHighWaterMark(int) high water mark} is exceeded, {@link org.jboss.netty.channel.Channel#isWritable() Channel.isWriteable()} + * will return false until the buffer drops below this level. By creating a sufficient gap between the high and low + * water marks, rapid oscillation between "write enabled" and "write disabled" can be avoided. + * + * @see {@link org.jboss.netty.channel.socket.nio.NioSocketChannelConfig#setWriteBufferLowWaterMark(int) NioSocketChannelConfig.setWriteBufferLowWaterMark()} + */ + public void setWriteBufferLowWaterMark(int level) + { + if (level >= writeBufferHighWaterMark) + { + throw new IllegalArgumentException( + "Write buffer low water mark must be strictly less than the high water mark"); + } + + if (level < MIN_LOW_WATER_MARK) + { + throw new IllegalArgumentException("Cannot set write buffer low water mark lower than " + MIN_LOW_WATER_MARK); + } + + writeBufferLowWaterMark = level; + } + + @Override + public boolean setOption(String key, Object value) + { + if (HIGH_WATER_MARK_OPTION.equals(key)) + { + setWriteBufferHighWaterMark((Integer) value); + } + else if (LOW_WATER_MARK_OPTION.equals(key)) + { + setWriteBufferLowWaterMark((Integer) value); + } + else + { + return super.setOption(key, value); + } + + return true; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannel.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannel.java new file mode 100644 index 0000000000..70bb38d4f5 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannel.java @@ -0,0 +1,385 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.AbstractChannel; +import org.jboss.netty.channel.ChannelFactory; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.socket.ClientSocketChannelFactory; +import org.jboss.netty.channel.socket.SocketChannel; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpRequestEncoder; +import org.jboss.netty.handler.codec.http.HttpResponseDecoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * The client end of an HTTP tunnel, created by an {@link HttpTunnelClientChannelFactory}. Channels of + * this type are designed to emulate a normal TCP based socket channel as far as is feasible within the limitations + * of the HTTP 1.1 protocol, and the usage patterns permitted by commonly used HTTP proxies and firewalls. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelClientChannel extends AbstractChannel implements SocketChannel +{ + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(HttpTunnelClientChannel.class); + + private final HttpTunnelClientChannelConfig config; + + private final SocketChannel sendChannel; + + private final SocketChannel pollChannel; + + private volatile String tunnelId; + + private volatile ChannelFuture connectFuture; + + private volatile boolean connected; + + private volatile boolean bound; + + volatile InetSocketAddress serverAddress; + + private volatile String serverHostName; + + private final WorkerCallbacks callbackProxy; + + private final SaturationManager saturationManager; + + /** + * @see {@link HttpTunnelClientChannelFactory#newChannel(ChannelPipeline)} + */ + protected HttpTunnelClientChannel(ChannelFactory factory, ChannelPipeline pipeline, + HttpTunnelClientChannelSink sink, ClientSocketChannelFactory outboundFactory, ChannelGroup realConnections) + { + super(null, factory, pipeline, sink); + + this.callbackProxy = new WorkerCallbacks(); + + sendChannel = outboundFactory.newChannel(createSendPipeline()); + pollChannel = outboundFactory.newChannel(createPollPipeline()); + config = new HttpTunnelClientChannelConfig(sendChannel.getConfig(), pollChannel.getConfig()); + saturationManager = new SaturationManager(config.getWriteBufferLowWaterMark(), config.getWriteBufferHighWaterMark()); + serverAddress = null; + + realConnections.add(sendChannel); + realConnections.add(pollChannel); + + Channels.fireChannelOpen(this); + } + + public HttpTunnelClientChannelConfig getConfig() + { + return config; + } + + public boolean isBound() + { + return bound; + } + + public boolean isConnected() + { + return connected; + } + + public InetSocketAddress getLocalAddress() + { + return sendChannel.getLocalAddress(); + } + + public InetSocketAddress getRemoteAddress() + { + return serverAddress; + } + + void onConnectRequest(ChannelFuture connectFuture, InetSocketAddress remoteAddress) + { + this.connectFuture = connectFuture; + /* if we are using a proxy, the remoteAddress is swapped here for the address of the proxy. + * The send and poll channels can later ask for the correct server address using + * getServerHostName(). + */ + serverAddress = remoteAddress; + + SocketAddress connectTarget; + if (config.getProxyAddress() != null) + { + connectTarget = config.getProxyAddress(); + } + else + { + connectTarget = remoteAddress; + } + + Channels.connect(sendChannel, connectTarget); + } + + void onDisconnectRequest(final ChannelFuture disconnectFuture) + { + ChannelFutureListener disconnectListener = new ConsolidatingFutureListener(disconnectFuture, 2); + sendChannel.disconnect().addListener(disconnectListener); + pollChannel.disconnect().addListener(disconnectListener); + + disconnectFuture.addListener(new ChannelFutureListener() + { + public void operationComplete(ChannelFuture future) throws Exception + { + serverAddress = null; + } + }); + } + + void onBindRequest(InetSocketAddress localAddress, final ChannelFuture bindFuture) + { + ChannelFutureListener bindListener = new ConsolidatingFutureListener(bindFuture, 2); + // bind the send channel to the specified local address, and the poll channel to + // an ephemeral port on the same interface as the send channel + sendChannel.bind(localAddress).addListener(bindListener); + InetSocketAddress pollBindAddress; + if (localAddress.isUnresolved()) + { + pollBindAddress = InetSocketAddress.createUnresolved(localAddress.getHostName(), 0); + } + else + { + pollBindAddress = new InetSocketAddress(localAddress.getAddress(), 0); + } + pollChannel.bind(pollBindAddress).addListener(bindListener); + } + + void onUnbindRequest(final ChannelFuture unbindFuture) + { + ChannelFutureListener unbindListener = new ConsolidatingFutureListener(unbindFuture, 2); + sendChannel.unbind().addListener(unbindListener); + pollChannel.unbind().addListener(unbindListener); + } + + void onCloseRequest(final ChannelFuture closeFuture) + { + ChannelFutureListener closeListener = new CloseConsolidatingFutureListener(closeFuture, 2); + sendChannel.close().addListener(closeListener); + pollChannel.close().addListener(closeListener); + } + + private ChannelPipeline createSendPipeline() + { + ChannelPipeline pipeline = Channels.pipeline(); + + pipeline.addLast("reqencoder", new HttpRequestEncoder()); // downstream + pipeline.addLast("respdecoder", new HttpResponseDecoder()); // upstream + pipeline.addLast("aggregator", new HttpChunkAggregator(HttpTunnelMessageUtils.MAX_BODY_SIZE)); // upstream + pipeline.addLast("sendHandler", new HttpTunnelClientSendHandler(callbackProxy)); // both + pipeline.addLast("writeFragmenter", new WriteFragmenter(HttpTunnelMessageUtils.MAX_BODY_SIZE)); + + return pipeline; + } + + private ChannelPipeline createPollPipeline() + { + ChannelPipeline pipeline = Channels.pipeline(); + + pipeline.addLast("reqencoder", new HttpRequestEncoder()); // downstream + pipeline.addLast("respdecoder", new HttpResponseDecoder()); // upstream + pipeline.addLast("aggregator", new HttpChunkAggregator(HttpTunnelMessageUtils.MAX_BODY_SIZE)); // upstream + pipeline.addLast(HttpTunnelClientPollHandler.NAME, new HttpTunnelClientPollHandler(callbackProxy)); // both + + return pipeline; + } + + private void setTunnelIdForPollChannel() + { + HttpTunnelClientPollHandler pollHandler = pollChannel.getPipeline().get(HttpTunnelClientPollHandler.class); + pollHandler.setTunnelId(tunnelId); + } + + void sendData(final MessageEvent e) + { + saturationManager.updateThresholds(config.getWriteBufferLowWaterMark(), + config.getWriteBufferHighWaterMark()); + final ChannelFuture originalFuture = e.getFuture(); + final ChannelBuffer message = (ChannelBuffer) e.getMessage(); + final int messageSize = message.readableBytes(); + updateSaturationStatus(messageSize); + Channels.write(sendChannel, e.getMessage()).addListener(new ChannelFutureListener() + { + public void operationComplete(ChannelFuture future) throws Exception + { + if (future.isSuccess()) + { + originalFuture.setSuccess(); + } + else + { + originalFuture.setFailure(future.getCause()); + } + updateSaturationStatus(-messageSize); + } + }); + } + + private void updateSaturationStatus(int queueSizeDelta) { + SaturationStateChange transition = saturationManager.queueSizeChanged(queueSizeDelta); + switch(transition) { + case SATURATED: fireWriteEnabled(false); break; + case DESATURATED: fireWriteEnabled(true); break; + case NO_CHANGE: break; + } + } + + private void fireWriteEnabled(boolean enabled) { + int ops = OP_READ; + if(!enabled) { + ops |= OP_WRITE; + } + + setInterestOpsNow(ops); + Channels.fireChannelInterestChanged(this); + } + + private class ConsolidatingFutureListener implements ChannelFutureListener + { + + private final ChannelFuture completionFuture; + + private final AtomicInteger eventsLeft; + + public ConsolidatingFutureListener(ChannelFuture completionFuture, int numToConsolidate) + { + this.completionFuture = completionFuture; + eventsLeft = new AtomicInteger(numToConsolidate); + } + + public void operationComplete(ChannelFuture future) throws Exception + { + if (!future.isSuccess()) + { + futureFailed(future); + } + else + { + if (eventsLeft.decrementAndGet() == 0) + { + allFuturesComplete(); + } + } + } + + protected void allFuturesComplete() + { + completionFuture.setSuccess(); + } + + protected void futureFailed(ChannelFuture future) + { + completionFuture.setFailure(future.getCause()); + } + } + + /** + * Close futures are a special case, as marking them as successful or failed has no effect. + * Instead, we must call setClosed() on the channel itself, once all the child channels are + * closed or if we fail to close them for whatever reason. + */ + private final class CloseConsolidatingFutureListener extends ConsolidatingFutureListener + { + + public CloseConsolidatingFutureListener(ChannelFuture completionFuture, int numToConsolidate) + { + super(completionFuture, numToConsolidate); + } + + @Override + protected void futureFailed(ChannelFuture future) + { + LOG.warn("Failed to close one of the child channels of tunnel " + tunnelId); + HttpTunnelClientChannel.this.setClosed(); + } + + @Override + protected void allFuturesComplete() + { + if (LOG.isDebugEnabled()) + { + LOG.debug("Tunnel " + tunnelId + " closed"); + } + HttpTunnelClientChannel.this.setClosed(); + } + + } + + /** + * Contains the implementing methods of HttpTunnelClientWorkerOwner, so that these are hidden + * from the public API. + */ + private class WorkerCallbacks implements HttpTunnelClientWorkerOwner + { + + public void onConnectRequest(ChannelFuture connectFuture, InetSocketAddress remoteAddress) + { + HttpTunnelClientChannel.this.onConnectRequest(connectFuture, remoteAddress); + } + + public void onTunnelOpened(String tunnelId) + { + HttpTunnelClientChannel.this.tunnelId = tunnelId; + setTunnelIdForPollChannel(); + Channels.connect(pollChannel, sendChannel.getRemoteAddress()); + } + + public void fullyEstablished() + { + if (!bound) + { + bound = true; + Channels.fireChannelBound(HttpTunnelClientChannel.this, getLocalAddress()); + } + + connected = true; + connectFuture.setSuccess(); + Channels.fireChannelConnected(HttpTunnelClientChannel.this, getRemoteAddress()); + } + + public void onMessageReceived(ChannelBuffer content) + { + Channels.fireMessageReceived(HttpTunnelClientChannel.this, content); + } + + public String getServerHostName() + { + if (serverHostName == null) + { + serverHostName = HttpTunnelMessageUtils.convertToHostString(serverAddress); + } + + return serverHostName; + } + + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfig.java new file mode 100644 index 0000000000..12667632ed --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfig.java @@ -0,0 +1,180 @@ +/* + * 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.httptunnel; + +import java.net.SocketAddress; + +import org.jboss.netty.channel.socket.SocketChannelConfig; + +/** + * Configuration for the client end of an HTTP tunnel. Any socket channel properties set here + * will be applied uniformly to the underlying send and poll channels, created from the channel + * factory provided to the {@link HttpTunnelClientChannelFactory}. + *

+ * HTTP tunnel clients have the following additional options: + * + * + * + * + * + * + * + * + *
NameAssociated setter method
{@code "proxyAddress"}{@link #setProxyAddress(SocketAddress)}
{@code "writeBufferHighWaterMark"}{@link #setWriteBufferHighWaterMark(long)}
{@code "writeBufferLowWaterMark"}{@link #setWriteBufferLowWaterMark(long)}
+ * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelClientChannelConfig extends HttpTunnelChannelConfig +{ + + static final String PROXY_ADDRESS_OPTION = "proxyAddress"; + + private final SocketChannelConfig sendChannelConfig; + + private final SocketChannelConfig pollChannelConfig; + + private volatile SocketAddress proxyAddress; + + HttpTunnelClientChannelConfig(SocketChannelConfig sendChannelConfig, SocketChannelConfig pollChannelConfig) + { + this.sendChannelConfig = sendChannelConfig; + this.pollChannelConfig = pollChannelConfig; + } + + /* HTTP TUNNEL SPECIFIC CONFIGURATION */ + // TODO Support all options in the old tunnel (see HttpTunnelingSocketChannelConfig) + // Mostly SSL, virtual host, and URL prefix + @Override + public boolean setOption(String key, Object value) + { + if (PROXY_ADDRESS_OPTION.equals(key)) + { + setProxyAddress((SocketAddress) value); + } + else + { + return super.setOption(key, value); + } + + return true; + } + + /** + * @return the address of the http proxy. If this is null, then no proxy + * should be used. + */ + public SocketAddress getProxyAddress() + { + return proxyAddress; + } + + /** + * Specify a proxy to be used for the http tunnel. If this is null, then + * no proxy should be used, otherwise this should be a directly accessible IPv4/IPv6 + * address and port. + */ + public void setProxyAddress(SocketAddress proxyAddress) + { + this.proxyAddress = proxyAddress; + } + + /* GENERIC SOCKET CHANNEL CONFIGURATION */ + + public int getReceiveBufferSize() + { + return pollChannelConfig.getReceiveBufferSize(); + } + + public int getSendBufferSize() + { + return pollChannelConfig.getSendBufferSize(); + } + + public int getSoLinger() + { + return pollChannelConfig.getSoLinger(); + } + + public int getTrafficClass() + { + return pollChannelConfig.getTrafficClass(); + } + + public boolean isKeepAlive() + { + return pollChannelConfig.isKeepAlive(); + } + + public boolean isReuseAddress() + { + return pollChannelConfig.isReuseAddress(); + } + + public boolean isTcpNoDelay() + { + return pollChannelConfig.isTcpNoDelay(); + } + + public void setKeepAlive(boolean keepAlive) + { + pollChannelConfig.setKeepAlive(keepAlive); + sendChannelConfig.setKeepAlive(keepAlive); + } + + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) + { + pollChannelConfig.setPerformancePreferences(connectionTime, latency, bandwidth); + sendChannelConfig.setPerformancePreferences(connectionTime, latency, bandwidth); + } + + public void setReceiveBufferSize(int receiveBufferSize) + { + pollChannelConfig.setReceiveBufferSize(receiveBufferSize); + sendChannelConfig.setReceiveBufferSize(receiveBufferSize); + } + + public void setReuseAddress(boolean reuseAddress) + { + pollChannelConfig.setReuseAddress(reuseAddress); + sendChannelConfig.setReuseAddress(reuseAddress); + } + + public void setSendBufferSize(int sendBufferSize) + { + pollChannelConfig.setSendBufferSize(sendBufferSize); + sendChannelConfig.setSendBufferSize(sendBufferSize); + } + + public void setSoLinger(int soLinger) + { + pollChannelConfig.setSoLinger(soLinger); + sendChannelConfig.setSoLinger(soLinger); + } + + public void setTcpNoDelay(boolean tcpNoDelay) + { + pollChannelConfig.setTcpNoDelay(true); + sendChannelConfig.setTcpNoDelay(true); + } + + public void setTrafficClass(int trafficClass) + { + pollChannelConfig.setTrafficClass(1); + sendChannelConfig.setTrafficClass(1); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelFactory.java new file mode 100644 index 0000000000..5842ec0312 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelFactory.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.httptunnel; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.channel.socket.ClientSocketChannelFactory; + +/** + * Factory used to create new client channels. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelClientChannelFactory implements ClientSocketChannelFactory +{ + + private final ClientSocketChannelFactory factory; + + private final ChannelGroup realConnections = new DefaultChannelGroup(); + + public HttpTunnelClientChannelFactory(ClientSocketChannelFactory factory) + { + if (factory == null) + { + throw new NullPointerException("factory"); + } + this.factory = factory; + } + + public HttpTunnelClientChannel newChannel(ChannelPipeline pipeline) + { + return new HttpTunnelClientChannel(this, pipeline, new HttpTunnelClientChannelSink(), factory, realConnections); + } + + public void releaseExternalResources() + { + realConnections.close().awaitUninterruptibly(); + factory.releaseExternalResources(); + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelSink.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelSink.java new file mode 100644 index 0000000000..6c5fa3121a --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelSink.java @@ -0,0 +1,89 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; + +import org.jboss.netty.channel.AbstractChannelSink; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.MessageEvent; + +/** + * Sink of a client channel, deals with sunk events and then makes appropriate calls + * on the channel itself to push data. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientChannelSink extends AbstractChannelSink +{ + + public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception + { + if (e instanceof ChannelStateEvent) + { + handleChannelStateEvent((ChannelStateEvent) e); + } + else if (e instanceof MessageEvent) + { + handleMessageEvent((MessageEvent) e); + } + } + + private void handleMessageEvent(MessageEvent e) + { + HttpTunnelClientChannel channel = (HttpTunnelClientChannel) e.getChannel(); + channel.sendData(e); + } + + private void handleChannelStateEvent(ChannelStateEvent e) + { + HttpTunnelClientChannel channel = (HttpTunnelClientChannel) e.getChannel(); + + switch (e.getState()) + { + case CONNECTED : + if (e.getValue() != null) + { + channel.onConnectRequest(e.getFuture(), (InetSocketAddress) e.getValue()); + } + else + { + channel.onDisconnectRequest(e.getFuture()); + } + break; + case BOUND : + if (e.getValue() != null) + { + channel.onBindRequest((InetSocketAddress) e.getValue(), e.getFuture()); + } + else + { + channel.onUnbindRequest(e.getFuture()); + } + break; + case OPEN : + if (Boolean.FALSE.equals(e.getValue())) + { + channel.onCloseRequest(e.getFuture()); + } + break; + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandler.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandler.java new file mode 100644 index 0000000000..ce8cfa9bad --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandler.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.httptunnel; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Pipeline component which controls the client poll loop to the server. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientPollHandler extends SimpleChannelHandler +{ + + public static final String NAME = "server2client"; + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(HttpTunnelClientPollHandler.class); + + private String tunnelId; + + private final HttpTunnelClientWorkerOwner tunnelChannel; + + private long pollTime; + + public HttpTunnelClientPollHandler(HttpTunnelClientWorkerOwner tunnelChannel) + { + this.tunnelChannel = tunnelChannel; + } + + public void setTunnelId(String tunnelId) + { + this.tunnelId = tunnelId; + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception + { + if (LOG.isDebugEnabled()) + { + LOG.debug("Poll channel for tunnel " + tunnelId + " established"); + } + tunnelChannel.fullyEstablished(); + sendPoll(ctx); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception + { + HttpResponse response = (HttpResponse) e.getMessage(); + + if (HttpTunnelMessageUtils.isOKResponse(response)) + { + long rtTime = System.nanoTime() - pollTime; + if (LOG.isDebugEnabled()) + { + LOG.debug("OK response received for poll on tunnel " + tunnelId + " after " + rtTime + " ns"); + } + tunnelChannel.onMessageReceived(response.getContent()); + sendPoll(ctx); + } + else + { + if (LOG.isWarnEnabled()) + { + LOG.warn("non-OK response received for poll on tunnel " + tunnelId); + } + } + } + + private void sendPoll(ChannelHandlerContext ctx) + { + pollTime = System.nanoTime(); + if (LOG.isDebugEnabled()) + { + LOG.debug("sending poll request for tunnel " + tunnelId); + } + HttpRequest request = HttpTunnelMessageUtils + .createReceiveDataRequest(tunnelChannel.getServerHostName(), tunnelId); + Channels.write(ctx, Channels.future(ctx.getChannel()), request); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandler.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandler.java new file mode 100644 index 0000000000..4ef997fd54 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandler.java @@ -0,0 +1,267 @@ +/* + * 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.httptunnel; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.DownstreamMessageEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Pipeline component which deals with sending data from the client to server. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientSendHandler extends SimpleChannelHandler +{ + + public static final String NAME = "client2server"; + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(HttpTunnelClientSendHandler.class); + + private final HttpTunnelClientWorkerOwner tunnelChannel; + + private String tunnelId = null; + + private final AtomicBoolean disconnecting; + + private ChannelStateEvent postShutdownEvent; + + private final ConcurrentLinkedQueue queuedWrites; + + private final AtomicInteger pendingRequestCount; + + private long sendRequestTime; + + public HttpTunnelClientSendHandler(HttpTunnelClientWorkerOwner tunnelChannel) + { + this.tunnelChannel = tunnelChannel; + queuedWrites = new ConcurrentLinkedQueue(); + pendingRequestCount = new AtomicInteger(0); + disconnecting = new AtomicBoolean(false); + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception + { + if (tunnelId == null) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("connection to " + e.getValue() + " succeeded - sending open tunnel request"); + } + HttpRequest request = HttpTunnelMessageUtils.createOpenTunnelRequest(tunnelChannel.getServerHostName()); + Channel thisChannel = ctx.getChannel(); + DownstreamMessageEvent event = new DownstreamMessageEvent(thisChannel, Channels.future(thisChannel), request, + thisChannel.getRemoteAddress()); + queuedWrites.offer(event); + pendingRequestCount.incrementAndGet(); + sendQueuedData(ctx); + } + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception + { + HttpResponse response = (HttpResponse) e.getMessage(); + + if (HttpTunnelMessageUtils.isOKResponse(response)) + { + long roundTripTime = System.nanoTime() - sendRequestTime; + if (LOG.isDebugEnabled()) + { + LOG.debug("OK response received for tunnel " + tunnelId + ", after " + roundTripTime + " ns"); + } + sendNextAfterResponse(ctx); + } + else if (HttpTunnelMessageUtils.isTunnelOpenResponse(response)) + { + tunnelId = HttpTunnelMessageUtils.extractCookie(response); + if (LOG.isDebugEnabled()) + { + LOG.debug("tunnel open request accepted - id " + tunnelId); + } + tunnelChannel.onTunnelOpened(tunnelId); + sendNextAfterResponse(ctx); + } + else if (HttpTunnelMessageUtils.isTunnelCloseResponse(response)) + { + if (LOG.isDebugEnabled()) + { + if (disconnecting.get()) + { + LOG.debug("server acknowledged disconnect for tunnel " + tunnelId); + } + else + { + LOG.debug("server closed tunnel " + tunnelId); + } + } + ctx.sendDownstream(postShutdownEvent); + } + else + { + // TODO: kill connection + if (LOG.isWarnEnabled()) + { + LOG.warn("unknown response received for tunnel " + tunnelId + ", closing connection"); + } + Channels.close(ctx, ctx.getChannel().getCloseFuture()); + } + } + + private void sendNextAfterResponse(ChannelHandlerContext ctx) + { + if (pendingRequestCount.decrementAndGet() > 0) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("Immediately sending next send request for tunnel " + tunnelId); + } + sendQueuedData(ctx); + } + } + + private synchronized void sendQueuedData(ChannelHandlerContext ctx) + { + if (disconnecting.get()) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("sending close request for tunnel " + tunnelId); + } + HttpRequest closeRequest = HttpTunnelMessageUtils.createCloseTunnelRequest(tunnelChannel.getServerHostName(), + tunnelId); + Channels.write(ctx, Channels.future(ctx.getChannel()), closeRequest); + } + else + { + if (LOG.isDebugEnabled()) + { + LOG.debug("sending next request for tunnel " + tunnelId); + } + MessageEvent nextWrite = queuedWrites.poll(); + sendRequestTime = System.nanoTime(); + ctx.sendDownstream(nextWrite); + } + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception + { + if (LOG.isDebugEnabled()) + { + LOG.debug("request to send data for tunnel " + tunnelId); + } + if (disconnecting.get()) + { + if (LOG.isWarnEnabled()) + { + LOG.warn("rejecting write request for tunnel " + tunnelId + " received after disconnect requested"); + } + e.getFuture().setFailure(new IllegalStateException("tunnel is closing")); + return; + } + ChannelBuffer data = (ChannelBuffer) e.getMessage(); + HttpRequest request = HttpTunnelMessageUtils.createSendDataRequest(tunnelChannel.getServerHostName(), tunnelId, + data); + DownstreamMessageEvent translatedEvent = new DownstreamMessageEvent(ctx.getChannel(), e.getFuture(), request, ctx + .getChannel().getRemoteAddress()); + queuedWrites.offer(translatedEvent); + if (pendingRequestCount.incrementAndGet() == 1) + { + sendQueuedData(ctx); + } + else + { + if (LOG.isDebugEnabled()) + { + LOG.debug("write request for tunnel " + tunnelId + " queued"); + } + } + } + + @Override + public void closeRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception + { + shutdownTunnel(ctx, e); + } + + @Override + public void disconnectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception + { + shutdownTunnel(ctx, e); + } + + @Override + public void unbindRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception + { + shutdownTunnel(ctx, e); + } + + private void shutdownTunnel(ChannelHandlerContext ctx, ChannelStateEvent postShutdownEvent) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("tunnel shutdown requested for send channel of tunnel " + tunnelId); + } + if (!ctx.getChannel().isConnected()) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("send channel of tunnel " + tunnelId + " is already disconnected"); + } + ctx.sendDownstream(postShutdownEvent); + return; + } + + if (!disconnecting.compareAndSet(false, true)) + { + if (LOG.isWarnEnabled()) + { + LOG.warn("tunnel shutdown process already initiated for tunnel " + tunnelId); + } + return; + } + + this.postShutdownEvent = postShutdownEvent; + + // if the channel is idle, send a close request immediately + if (pendingRequestCount.incrementAndGet() == 1) + { + sendQueuedData(ctx); + } + } + + public String getTunnelId() + { + return tunnelId; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientWorkerOwner.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientWorkerOwner.java new file mode 100644 index 0000000000..c34ee3e2ee --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientWorkerOwner.java @@ -0,0 +1,68 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelFuture; + +/** + * Interface which is used by the send and poll "worker" channels + * to notify the virtual tunnel channel of key events, and to get + * access to higher level information required for correct + * operation. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface HttpTunnelClientWorkerOwner +{ + /** + * The HTTP tunnel client sink invokes this when the application code requests the connection + * of an HTTP tunnel to the specified remote address. + */ + public void onConnectRequest(ChannelFuture connectFuture, InetSocketAddress remoteAddress); + + /** + * The send channel handler calls this method when the server accepts the open tunnel request, + * returning a unique tunnel ID. + * + * @param tunnelId the server allocated tunnel ID + */ + public void onTunnelOpened(String tunnelId); + + /** + * The poll channel handler calls this method when the poll channel is connected, indicating + * that full duplex communications are now possible. + */ + public void fullyEstablished(); + + /** + * The poll handler calls this method when some data is received and decoded from the server. + * @param content the data received from the server + */ + public void onMessageReceived(ChannelBuffer content); + + /** + * @return the name of the server with whom we are communicating with - this is used within + * the HOST HTTP header for all requests. This is particularly important for operation behind + * a proxy, where the HOST string is used to route the request. + */ + public String getServerHostName(); + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelMessageUtils.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelMessageUtils.java new file mode 100644 index 0000000000..e5e28a9630 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelMessageUtils.java @@ -0,0 +1,363 @@ +/* + * 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.httptunnel; + +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.http.DefaultHttpRequest; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; + +/** + * Utility class for creating http requests for the operation of the full duplex + * http tunnel, and verifying that received requests are of the correct types. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelMessageUtils +{ + + private static final String HTTP_URL_PREFIX = "http://"; + + /** + * An upper bound is enforced on the size of message bodies, so as + * to ensure we do not dump large chunks of data on either peer. + */ + public static final int MAX_BODY_SIZE = 16 * 1024; + + /** + * The tunnel will only accept connections from this specific user agent. This + * allows us to distinguish a legitimate tunnel connection from someone pointing + * a web browser or robot at the tunnel URL. + */ + static final String USER_AGENT = "HttpTunnelClient"; + + static final String OPEN_TUNNEL_REQUEST_URI = "/http-tunnel/open"; + + static final String CLOSE_TUNNEL_REQUEST_URI = "/http-tunnel/close"; + + static final String CLIENT_SEND_REQUEST_URI = "/http-tunnel/send"; + + static final String CLIENT_RECV_REQUEST_URI = "/http-tunnel/poll"; + + static final String CONTENT_TYPE = "application/octet-stream"; + + public static HttpRequest createOpenTunnelRequest(SocketAddress host) + { + return createOpenTunnelRequest(convertToHostString(host)); + } + + public static HttpRequest createOpenTunnelRequest(String host) + { + HttpRequest request = createRequestTemplate(host, null, OPEN_TUNNEL_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isOpenTunnelRequest(HttpRequest request) + { + return isRequestTo(request, OPEN_TUNNEL_REQUEST_URI); + } + + public static boolean checkHost(HttpRequest request, SocketAddress expectedHost) + { + String host = request.getHeader(HttpHeaders.Names.HOST); + return expectedHost == null ? host == null : HttpTunnelMessageUtils.convertToHostString(expectedHost) + .equals(host); + } + + public static HttpRequest createSendDataRequest(SocketAddress host, String cookie, ChannelBuffer data) + { + return createSendDataRequest(convertToHostString(host), cookie, data); + } + + public static HttpRequest createSendDataRequest(String host, String cookie, ChannelBuffer data) + { + HttpRequest request = createRequestTemplate(host, cookie, CLIENT_SEND_REQUEST_URI); + request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, Long.toString(data.readableBytes())); + request.setContent(data); + + return request; + } + + public static boolean isSendDataRequest(HttpRequest request) + { + return isRequestTo(request, CLIENT_SEND_REQUEST_URI); + } + + public static HttpRequest createReceiveDataRequest(SocketAddress host, String tunnelId) + { + return createReceiveDataRequest(convertToHostString(host), tunnelId); + } + + public static HttpRequest createReceiveDataRequest(String host, String tunnelId) + { + HttpRequest request = createRequestTemplate(host, tunnelId, CLIENT_RECV_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isReceiveDataRequest(HttpRequest request) + { + return isRequestTo(request, CLIENT_RECV_REQUEST_URI); + } + + public static HttpRequest createCloseTunnelRequest(String host, String tunnelId) + { + HttpRequest request = createRequestTemplate(host, tunnelId, CLOSE_TUNNEL_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isCloseTunnelRequest(HttpRequest request) + { + return isRequestTo(request, CLOSE_TUNNEL_REQUEST_URI); + } + + public static boolean isServerToClientRequest(HttpRequest request) + { + return isRequestTo(request, CLIENT_RECV_REQUEST_URI); + } + + public static String convertToHostString(SocketAddress hostAddress) + { + StringWriter host = new StringWriter(); + InetSocketAddress inetSocketAddr = (InetSocketAddress) hostAddress; + InetAddress addr = inetSocketAddr.getAddress(); + if (addr instanceof Inet6Address) + { + host.append('['); + host.append(addr.getHostAddress()); + host.append(']'); + } + else if (addr != null) + { + host.append(addr.getHostAddress()); + } + else + { + host.append(inetSocketAddr.getHostName()); + } + + host.append(':'); + host.append(Integer.toString(inetSocketAddr.getPort())); + return host.toString(); + } + + private static HttpRequest createRequestTemplate(String host, String tunnelId, String uri) + { + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, createCompleteUri(host, uri)); + request.setHeader(HttpHeaders.Names.HOST, host); + request.setHeader(HttpHeaders.Names.USER_AGENT, USER_AGENT); + if (tunnelId != null) + { + request.setHeader(HttpHeaders.Names.COOKIE, tunnelId); + } + + return request; + } + + private static String createCompleteUri(String host, String uri) + { + StringBuilder builder = new StringBuilder(HTTP_URL_PREFIX.length() + host.length() + uri.length()); + builder.append(HTTP_URL_PREFIX); + builder.append(host); + builder.append(uri); + + return builder.toString(); + } + + private static boolean isRequestTo(HttpRequest request, String uri) + { + URI decodedUri; + try + { + decodedUri = new URI(request.getUri()); + } + catch (URISyntaxException e) + { + return false; + } + + return HttpVersion.HTTP_1_1.equals(request.getProtocolVersion()) + && USER_AGENT.equals(request.getHeader(HttpHeaders.Names.USER_AGENT)) + && HttpMethod.POST.equals(request.getMethod()) && uri.equals(decodedUri.getPath()); + } + + private static void setNoData(HttpRequest request) + { + request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); + request.setContent(null); + } + + public static String extractTunnelId(HttpRequest request) + { + return request.getHeader(HttpHeaders.Names.COOKIE); + } + + private static byte[] toBytes(String string) + { + try + { + return string.getBytes("UTF-8"); + } + catch (UnsupportedEncodingException e) + { + // UTF-8 is meant to be supported on all platforms + throw new RuntimeException("UTF-8 encoding not supported!"); + } + } + + public static HttpResponse createTunnelOpenResponse(String tunnelId) + { + HttpResponse response = createResponseTemplate(HttpResponseStatus.CREATED, null); + response.setHeader(HttpHeaders.Names.SET_COOKIE, tunnelId); + return response; + } + + public static boolean isTunnelOpenResponse(HttpResponse response) + { + return isResponseWithCode(response, HttpResponseStatus.CREATED); + } + + public static boolean isOKResponse(HttpResponse response) + { + return isResponseWithCode(response, HttpResponseStatus.OK); + } + + public static boolean hasContents(HttpResponse response, byte[] expectedContents) + { + if (response.getContent() != null && HttpHeaders.getContentLength(response) == expectedContents.length + && response.getContent().readableBytes() == expectedContents.length) + { + byte[] compareBytes = new byte[expectedContents.length]; + response.getContent().readBytes(compareBytes); + return Arrays.equals(expectedContents, compareBytes); + } + + return false; + } + + public static HttpResponse createTunnelCloseResponse() + { + HttpResponse response = createResponseTemplate(HttpResponseStatus.RESET_CONTENT, null); + return response; + } + + public static boolean isTunnelCloseResponse(HttpResponse response) + { + return isResponseWithCode(response, HttpResponseStatus.RESET_CONTENT); + } + + public static String extractCookie(HttpResponse response) + { + if (response.containsHeader(HttpHeaders.Names.SET_COOKIE)) + { + return response.getHeader(HttpHeaders.Names.SET_COOKIE); + } + + return null; + } + + public static HttpResponse createSendDataResponse() + { + return createOKResponseTemplate(null); + } + + public static HttpResponse createRecvDataResponse(ChannelBuffer data) + { + return createOKResponseTemplate(data); + } + + public static HttpResponse createRejection(HttpRequest request, String reason) + { + HttpVersion version = request != null ? request.getProtocolVersion() : HttpVersion.HTTP_1_1; + HttpResponse response = new DefaultHttpResponse(version, HttpResponseStatus.BAD_REQUEST); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=\"utf-8\""); + ChannelBuffer reasonBuffer = ChannelBuffers.wrappedBuffer(toBytes(reason)); + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, Integer.toString(reasonBuffer.readableBytes())); + response.setContent(reasonBuffer); + return response; + } + + public static boolean isRejection(HttpResponse response) + { + return !HttpResponseStatus.OK.equals(response.getStatus()); + } + + public static Object extractErrorMessage(HttpResponse response) + { + if (response.getContent() == null || HttpHeaders.getContentLength(response) == 0) + { + return ""; + } + + byte[] bytes = new byte[response.getContent().readableBytes()]; + response.getContent().readBytes(bytes); + try + { + return new String(bytes, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + return ""; + } + } + + private static boolean isResponseWithCode(HttpResponse response, HttpResponseStatus status) + { + return HttpVersion.HTTP_1_1.equals(response.getProtocolVersion()) && status.equals(response.getStatus()); + } + + private static HttpResponse createOKResponseTemplate(ChannelBuffer data) + { + return createResponseTemplate(HttpResponseStatus.OK, data); + } + + private static HttpResponse createResponseTemplate(HttpResponseStatus status, ChannelBuffer data) + { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); + if (data != null) + { + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, Integer.toString(data.readableBytes())); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream"); + response.setContent(data); + } + else + { + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); + response.setContent(null); + } + return response; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannel.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannel.java new file mode 100644 index 0000000000..e0efaa8422 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannel.java @@ -0,0 +1,113 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; + +import org.jboss.netty.channel.AbstractServerChannel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineException; +import org.jboss.netty.channel.Channels; +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) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannel extends AbstractServerChannel implements ServerSocketChannel +{ + + private final ServerSocketChannel realChannel; + + private final HttpTunnelServerChannelConfig config; + + private final ServerMessageSwitch messageSwitch; + + private final ChannelFutureListener CLOSE_FUTURE_PROXY = new ChannelFutureListener() + { + public void operationComplete(ChannelFuture future) throws Exception + { + HttpTunnelServerChannel.this.setClosed(); + } + }; + + protected HttpTunnelServerChannel(HttpTunnelServerChannelFactory factory, ChannelPipeline pipeline) + { + super(factory, pipeline, new HttpTunnelServerChannelSink()); + + messageSwitch = new ServerMessageSwitch(new TunnelCreator()); + realChannel = factory.createRealChannel(this, messageSwitch); + HttpTunnelServerChannelSink sink = (HttpTunnelServerChannelSink) getPipeline().getSink(); + sink.setRealChannel(realChannel); + sink.setCloseListener(CLOSE_FUTURE_PROXY); + config = new HttpTunnelServerChannelConfig(realChannel); + Channels.fireChannelOpen(this); + } + + public ServerSocketChannelConfig getConfig() + { + return config; + } + + public InetSocketAddress getLocalAddress() + { + return realChannel.getLocalAddress(); + } + + public InetSocketAddress getRemoteAddress() + { + // server channels never have a remote address + return null; + } + + public boolean isBound() + { + return realChannel.isBound(); + } + + /** + * Used to hide the newChannel method from the public API. + */ + private final class TunnelCreator implements HttpTunnelAcceptedChannelFactory + { + + public HttpTunnelAcceptedChannelReceiver newChannel(String newTunnelId, InetSocketAddress remoteAddress) + { + ChannelPipeline childPipeline = null; + try + { + childPipeline = getConfig().getPipelineFactory().getPipeline(); + } + catch (Exception e) + { + throw new ChannelPipelineException("Failed to initialize a pipeline.", e); + } + HttpTunnelAcceptedChannelConfig config = new HttpTunnelAcceptedChannelConfig(); + HttpTunnelAcceptedChannelSink sink = new HttpTunnelAcceptedChannelSink(messageSwitch, newTunnelId, config); + return new HttpTunnelAcceptedChannel(HttpTunnelServerChannel.this, getFactory(), childPipeline, sink, + remoteAddress, config); + } + + public String generateTunnelId() + { + return config.getTunnelIdGenerator().generateId(); + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelConfig.java new file mode 100644 index 0000000000..b41bd5a845 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelConfig.java @@ -0,0 +1,150 @@ +/* + * 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.httptunnel; + +import java.util.Map; +import java.util.Map.Entry; + +import org.jboss.netty.buffer.ChannelBufferFactory; +import org.jboss.netty.channel.ChannelPipelineFactory; +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) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannelConfig implements ServerSocketChannelConfig +{ + + private ChannelPipelineFactory pipelineFactory; + + private final ServerSocketChannel realChannel; + + private TunnelIdGenerator tunnelIdGenerator = new DefaultTunnelIdGenerator(); + + public HttpTunnelServerChannelConfig(ServerSocketChannel realChannel) + { + this.realChannel = realChannel; + } + + private ServerSocketChannelConfig getWrappedConfig() + { + return realChannel.getConfig(); + } + + public int getBacklog() + { + return getWrappedConfig().getBacklog(); + } + + public int getReceiveBufferSize() + { + return getWrappedConfig().getReceiveBufferSize(); + } + + public boolean isReuseAddress() + { + return getWrappedConfig().isReuseAddress(); + } + + public void setBacklog(int backlog) + { + getWrappedConfig().setBacklog(backlog); + } + + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) + { + getWrappedConfig().setPerformancePreferences(connectionTime, latency, bandwidth); + } + + public void setReceiveBufferSize(int receiveBufferSize) + { + getWrappedConfig().setReceiveBufferSize(receiveBufferSize); + } + + public void setReuseAddress(boolean reuseAddress) + { + getWrappedConfig().setReuseAddress(reuseAddress); + } + + public ChannelBufferFactory getBufferFactory() + { + return getWrappedConfig().getBufferFactory(); + } + + public int getConnectTimeoutMillis() + { + return getWrappedConfig().getConnectTimeoutMillis(); + } + + public ChannelPipelineFactory getPipelineFactory() + { + return pipelineFactory; + } + + public void setBufferFactory(ChannelBufferFactory bufferFactory) + { + getWrappedConfig().setBufferFactory(bufferFactory); + } + + public void setConnectTimeoutMillis(int connectTimeoutMillis) + { + getWrappedConfig().setConnectTimeoutMillis(connectTimeoutMillis); + } + + public boolean setOption(String name, Object value) + { + if (name.equals("pipelineFactory")) + { + setPipelineFactory((ChannelPipelineFactory) value); + return true; + } + else if (name.equals("tunnelIdGenerator")) + { + setTunnelIdGenerator((TunnelIdGenerator) value); + return true; + } + else + { + return getWrappedConfig().setOption(name, value); + } + } + + public void setOptions(Map options) + { + for (Entry e : options.entrySet()) + { + setOption(e.getKey(), e.getValue()); + } + } + + public void setPipelineFactory(ChannelPipelineFactory pipelineFactory) + { + this.pipelineFactory = pipelineFactory; + } + + public void setTunnelIdGenerator(TunnelIdGenerator tunnelIdGenerator) + { + this.tunnelIdGenerator = tunnelIdGenerator; + } + + public TunnelIdGenerator getTunnelIdGenerator() + { + return tunnelIdGenerator; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactory.java new file mode 100644 index 0000000000..322fe7d6bb --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactory.java @@ -0,0 +1,65 @@ +/* + * 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.httptunnel; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.DefaultChannelGroup; +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) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannelFactory implements ServerSocketChannelFactory +{ + + private final ServerSocketChannelFactory realConnectionFactory; + + private final ChannelGroup realConnections; + + public HttpTunnelServerChannelFactory(ServerSocketChannelFactory realConnectionFactory) + { + this.realConnectionFactory = realConnectionFactory; + realConnections = new DefaultChannelGroup(); + } + + public HttpTunnelServerChannel newChannel(ChannelPipeline pipeline) + { + return new HttpTunnelServerChannel(this, pipeline); + } + + ServerSocketChannel createRealChannel(HttpTunnelServerChannel channel, ServerMessageSwitch messageSwitch) + { + ChannelPipeline realChannelPipeline = Channels.pipeline(); + AcceptedServerChannelPipelineFactory realPipelineFactory = new AcceptedServerChannelPipelineFactory(messageSwitch); + realChannelPipeline.addFirst(TunnelWrappedServerChannelHandler.NAME, new TunnelWrappedServerChannelHandler( + channel, realPipelineFactory, realConnections)); + ServerSocketChannel newChannel = realConnectionFactory.newChannel(realChannelPipeline); + realConnections.add(newChannel); + return newChannel; + } + + public void releaseExternalResources() + { + realConnections.close().awaitUninterruptibly(); + realConnectionFactory.releaseExternalResources(); + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSink.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSink.java new file mode 100644 index 0000000000..39551409db --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSink.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.httptunnel; + +import java.net.SocketAddress; + +import org.jboss.netty.channel.AbstractChannelSink; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.socket.ServerSocketChannel; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelServerChannelSink extends AbstractChannelSink +{ + + private ChannelFutureListener closeHook; + + private ServerSocketChannel realChannel; + + public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception + { + + if (e instanceof ChannelStateEvent) + { + ChannelStateEvent ev = (ChannelStateEvent) e; + switch (ev.getState()) + { + case OPEN : + if (Boolean.FALSE.equals(ev.getValue())) + { + realChannel.close().addListener(closeHook); + } + break; + case BOUND : + if (ev.getValue() != null) + { + realChannel.bind((SocketAddress) ev.getValue()).addListener(new ChannelFutureProxy(e.getFuture())); + } + else + { + realChannel.unbind().addListener(new ChannelFutureProxy(e.getFuture())); + } + break; + } + } + } + + private final class ChannelFutureProxy implements ChannelFutureListener + { + private final ChannelFuture upstreamFuture; + + ChannelFutureProxy(ChannelFuture upstreamFuture) + { + this.upstreamFuture = upstreamFuture; + } + + public void operationComplete(ChannelFuture future) throws Exception + { + if (future.isSuccess()) + { + upstreamFuture.setSuccess(); + } + else + { + upstreamFuture.setFailure(future.getCause()); + } + } + } + + public void setRealChannel(ServerSocketChannel realChannel) + { + this.realChannel = realChannel; + } + + public void setCloseListener(ChannelFutureListener closeHook) + { + this.closeHook = closeHook; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationManager.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationManager.java new file mode 100644 index 0000000000..c54d9472ae --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationManager.java @@ -0,0 +1,68 @@ +/* + * 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.httptunnel; + +import static org.jboss.netty.channel.socket.httptunnel.SaturationStateChange.DESATURATED; +import static org.jboss.netty.channel.socket.httptunnel.SaturationStateChange.NO_CHANGE; +import static org.jboss.netty.channel.socket.httptunnel.SaturationStateChange.SATURATED; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used to monitor the amount of data that has yet to be pushed to + * the underlying socket, in order to implement the "high/low water mark" facility + * that controls Channel.isWritable() and the interest ops of http tunnels. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class SaturationManager +{ + private AtomicLong desaturationPoint; + private AtomicLong saturationPoint; + private final AtomicLong queueSize; + private final AtomicBoolean saturated; + + public SaturationManager(long desaturationPoint, long saturationPoint) + { + this.desaturationPoint = new AtomicLong(desaturationPoint); + this.saturationPoint = new AtomicLong(saturationPoint); + queueSize = new AtomicLong(0); + saturated = new AtomicBoolean(false); + } + + public SaturationStateChange queueSizeChanged(long sizeDelta) { + long newQueueSize = queueSize.addAndGet(sizeDelta); + if(newQueueSize <= desaturationPoint.get()) { + if(saturated.compareAndSet(true, false)) { + return DESATURATED; + } + } else if(newQueueSize > saturationPoint.get()) { + if(saturated.compareAndSet(false, true)) { + return SATURATED; + } + } + + return NO_CHANGE; + } + + public void updateThresholds(long desaturationPoint, long saturationPoint) { + this.desaturationPoint.set(desaturationPoint); + this.saturationPoint.set(saturationPoint); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationStateChange.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationStateChange.java new file mode 100644 index 0000000000..3173ef8706 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/SaturationStateChange.java @@ -0,0 +1,31 @@ +/* + * 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.httptunnel; + +/** + * Represents the state change of a chanel in response in the amount of pending data to be + * sent - either no change occurs, the channel becomes desaturated (indicating that writing + * can safely commence) or it becomes saturated (indicating that writing should cease). + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +enum SaturationStateChange { + NO_CHANGE, + DESATURATED, + SATURATED +} diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitch.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitch.java new file mode 100644 index 0000000000..31918020f7 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitch.java @@ -0,0 +1,268 @@ +/* + * 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.httptunnel; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * This is the gateway between the accepted TCP channels that are used to communicate with the client + * ends of the http tunnel and the virtual server accepted tunnel. As a tunnel can last for longer than + * the lifetime of the client channels that are used to service it, this layer of abstraction is + * necessary. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class ServerMessageSwitch implements ServerMessageSwitchUpstreamInterface, ServerMessageSwitchDownstreamInterface +{ + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(ServerMessageSwitch.class.getName()); + + private final String tunnelIdPrefix; + + private final HttpTunnelAcceptedChannelFactory newChannelFactory; + + private final ConcurrentHashMap tunnelsById; + + public ServerMessageSwitch(HttpTunnelAcceptedChannelFactory newChannelFactory) + { + this.newChannelFactory = newChannelFactory; + tunnelIdPrefix = Long.toHexString(new Random().nextLong()); + tunnelsById = new ConcurrentHashMap(); + } + + public String createTunnel(InetSocketAddress remoteAddress) + { + String newTunnelId = String.format("%s_%s", tunnelIdPrefix, newChannelFactory.generateTunnelId()); + TunnelInfo newTunnel = new TunnelInfo(); + newTunnel.tunnelId = newTunnelId; + tunnelsById.put(newTunnelId, newTunnel); + newTunnel.localChannel = newChannelFactory.newChannel(newTunnelId, remoteAddress); + return newTunnelId; + } + + public boolean isOpenTunnel(String tunnelId) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + return tunnel != null; + } + + public void pollOutboundData(String tunnelId, Channel channel) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) + { + if (LOG.isWarnEnabled()) + { + LOG.warn("Poll request for tunnel " + tunnelId + " which does not exist or already closed"); + } + respondAndClose(channel, + HttpTunnelMessageUtils.createRejection(null, "Unknown tunnel, possibly already closed")); + return; + } + + if (!tunnel.responseChannel.compareAndSet(null, channel)) + { + if (LOG.isWarnEnabled()) + { + LOG.warn("Duplicate poll request detected for tunnel " + tunnelId); + } + respondAndClose(channel, + HttpTunnelMessageUtils.createRejection(null, "Only one poll request at a time per tunnel allowed")); + return; + } + + sendQueuedData(tunnel); + } + + private void respondAndClose(Channel channel, HttpResponse response) + { + Channels.write(channel, response).addListener(ChannelFutureListener.CLOSE); + } + + private void sendQueuedData(TunnelInfo state) + { + Queue queuedData = state.queuedResponses; + Channel responseChannel = state.responseChannel.getAndSet(null); + if (responseChannel == null) + { + // no response channel, or another thread has already used it + return; + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("sending response for tunnel id " + state.tunnelId + " to " + responseChannel.getRemoteAddress()); + } + QueuedResponse messageToSend = queuedData.poll(); + if (messageToSend == null) + { + // no data to send, restore the response channel and bail out + state.responseChannel.set(responseChannel); + return; + } + + HttpResponse response = HttpTunnelMessageUtils.createRecvDataResponse(messageToSend.data); + final ChannelFuture originalFuture = messageToSend.writeFuture; + Channels.write(responseChannel, response).addListener(new RelayedChannelFutureListener(originalFuture)); + } + + public TunnelStatus routeInboundData(String tunnelId, ChannelBuffer inboundData) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) + { + return TunnelStatus.CLOSED; + } + + if (tunnel.closing.get()) + { + // client has now been notified, forget the tunnel + tunnelsById.remove(tunnel); + return TunnelStatus.CLOSED; + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("routing inbound data for tunnel " + tunnelId); + } + tunnel.localChannel.dataReceived(inboundData); + return TunnelStatus.ALIVE; + } + + public void clientCloseTunnel(String tunnelId) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + tunnel.localChannel.clientClosed(); + tunnelsById.remove(tunnelId); + } + + public void serverCloseTunnel(String tunnelId) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + tunnel.closing.set(true); + + Channel responseChannel = tunnel.responseChannel.getAndSet(null); + if (responseChannel == null) + { + // response channel is already in use, client will be notified + // of close at next opportunity + return; + } + + respondAndClose(responseChannel, HttpTunnelMessageUtils.createTunnelCloseResponse()); + // client has been notified, forget the tunnel + tunnelsById.remove(tunnelId); + } + + public void routeOutboundData(String tunnelId, ChannelBuffer data, ChannelFuture writeFuture) + { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) + { + // tunnel is closed + if (LOG.isWarnEnabled()) + { + LOG.warn("attempt made to send data out on tunnel id " + tunnelId + " which is unknown or closed"); + } + return; + } + + ChannelFutureAggregator aggregator = new ChannelFutureAggregator(writeFuture); + List fragments = WriteSplitter.split(data, HttpTunnelMessageUtils.MAX_BODY_SIZE); + + if (LOG.isDebugEnabled()) + { + LOG.debug("routing outbound data for tunnel " + tunnelId); + } + for (ChannelBuffer fragment : fragments) + { + ChannelFuture fragmentFuture = Channels.future(writeFuture.getChannel()); + aggregator.addFuture(fragmentFuture); + tunnel.queuedResponses.offer(new QueuedResponse(fragment, fragmentFuture)); + } + + sendQueuedData(tunnel); + } + + /** + * Used to pass the result received from one ChannelFutureListener to another verbatim. + */ + private final class RelayedChannelFutureListener implements ChannelFutureListener + { + private final ChannelFuture originalFuture; + + private RelayedChannelFutureListener(ChannelFuture originalFuture) + { + this.originalFuture = originalFuture; + } + + public void operationComplete(ChannelFuture future) throws Exception + { + if (future.isSuccess()) + { + originalFuture.setSuccess(); + } + else + { + originalFuture.setFailure(future.getCause()); + } + } + } + + private static final class TunnelInfo + { + public String tunnelId; + + public HttpTunnelAcceptedChannelReceiver localChannel; + + public final AtomicReference responseChannel = new AtomicReference(null); + + public final Queue queuedResponses = new ConcurrentLinkedQueue(); + + 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/httptunnel/ServerMessageSwitchDownstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchDownstreamInterface.java new file mode 100644 index 0000000000..6e4e8552c1 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/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.httptunnel; + +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/httptunnel/ServerMessageSwitchUpstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchUpstreamInterface.java new file mode 100644 index 0000000000..9491f6bd46 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/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.httptunnel; + +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/httptunnel/TunnelIdGenerator.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelIdGenerator.java new file mode 100644 index 0000000000..75a5388917 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelIdGenerator.java @@ -0,0 +1,38 @@ +/* + * 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.httptunnel; + +/** + * 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/httptunnel/TunnelWrappedServerChannelHandler.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelWrappedServerChannelHandler.java new file mode 100644 index 0000000000..6f3ba2796a --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/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.httptunnel; + +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/httptunnel/WriteFragmenter.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenter.java new file mode 100644 index 0000000000..5236f3497e --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenter.java @@ -0,0 +1,78 @@ +/* + * 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.httptunnel; + +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/httptunnel/WriteSplitter.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitter.java new file mode 100644 index 0000000000..e3cf664f9d --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitter.java @@ -0,0 +1,59 @@ +/* + * 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.httptunnel; + +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/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java new file mode 100644 index 0000000000..b219566a49 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java @@ -0,0 +1,247 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelConfig.java new file mode 100644 index 0000000000..1b853da685 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelConfig.java @@ -0,0 +1,237 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeChannelSink.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelSink.java new file mode 100644 index 0000000000..7ed63f9ac8 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelSink.java @@ -0,0 +1,40 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeClientSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeClientSocketChannelFactory.java new file mode 100644 index 0000000000..3958b01171 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeClientSocketChannelFactory.java @@ -0,0 +1,52 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeServerSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannel.java new file mode 100644 index 0000000000..0bda1e4f6a --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannel.java @@ -0,0 +1,92 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeServerSocketChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelConfig.java new file mode 100644 index 0000000000..539456c343 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelConfig.java @@ -0,0 +1,80 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeServerSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelFactory.java new file mode 100644 index 0000000000..1da5623cc9 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelFactory.java @@ -0,0 +1,46 @@ +/* + * 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.httptunnel; + +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/httptunnel/FakeSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeSocketChannel.java new file mode 100644 index 0000000000..1cc94eb760 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeSocketChannel.java @@ -0,0 +1,115 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelAcceptedChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSinkTest.java new file mode 100644 index 0000000000..123e85af4f --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSinkTest.java @@ -0,0 +1,94 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelClientChannelConfigTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfigTest.java new file mode 100644 index 0000000000..c2308957db --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfigTest.java @@ -0,0 +1,329 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelClientChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelTest.java new file mode 100644 index 0000000000..a7ed487015 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelTest.java @@ -0,0 +1,255 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelClientPollHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandlerTest.java new file mode 100644 index 0000000000..3db51162cc --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandlerTest.java @@ -0,0 +1,126 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelClientSendHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandlerTest.java new file mode 100644 index 0000000000..361d143f88 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandlerTest.java @@ -0,0 +1,217 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelServerChannelFactoryTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactoryTest.java new file mode 100644 index 0000000000..1483b86725 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactoryTest.java @@ -0,0 +1,111 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelServerChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSinkTest.java new file mode 100644 index 0000000000..2beee03e9c --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSinkTest.java @@ -0,0 +1,173 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelServerChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelTest.java new file mode 100644 index 0000000000..bbe8d63218 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelTest.java @@ -0,0 +1,242 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelSoakTester.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelSoakTester.java new file mode 100644 index 0000000000..034c6e9488 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelSoakTester.java @@ -0,0 +1,487 @@ +/* + * 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.httptunnel; + +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/httptunnel/HttpTunnelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelTest.java new file mode 100644 index 0000000000..6209b1eb9b --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelTest.java @@ -0,0 +1,228 @@ +/* + * 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.httptunnel; + +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/httptunnel/MockChannelStateListener.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/MockChannelStateListener.java new file mode 100644 index 0000000000..fd5cb10ec3 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/MockChannelStateListener.java @@ -0,0 +1,66 @@ +/* + * 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.httptunnel; + +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/httptunnel/NettyTestUtils.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtils.java new file mode 100644 index 0000000000..1358aa9bdb --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtils.java @@ -0,0 +1,200 @@ +/* + * 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.httptunnel; + +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/httptunnel/NettyTestUtilsTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtilsTest.java new file mode 100644 index 0000000000..2b9987993d --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtilsTest.java @@ -0,0 +1,144 @@ +/* + * 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.httptunnel; + +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/httptunnel/NullChannelHandler.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NullChannelHandler.java new file mode 100644 index 0000000000..1ece636a9f --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/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.httptunnel; + +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/httptunnel/SaturationManagerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/SaturationManagerTest.java new file mode 100644 index 0000000000..d60340d92f --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/SaturationManagerTest.java @@ -0,0 +1,34 @@ +package org.jboss.netty.channel.socket.httptunnel; + +import static org.junit.Assert.*; +import static org.jboss.netty.channel.socket.httptunnel.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/httptunnel/ServerMessageSwitchTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchTest.java new file mode 100644 index 0000000000..8851804ffc --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchTest.java @@ -0,0 +1,181 @@ +/* + * 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.httptunnel; + +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.httptunnel.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/httptunnel/UpstreamEventCatcher.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/UpstreamEventCatcher.java new file mode 100644 index 0000000000..91fa22b833 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/UpstreamEventCatcher.java @@ -0,0 +1,42 @@ +/* + * 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.httptunnel; + +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/httptunnel/WriteFragmenterTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenterTest.java new file mode 100644 index 0000000000..3aa294f445 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenterTest.java @@ -0,0 +1,156 @@ +/* + * 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.httptunnel; + +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/httptunnel/WriteSplitterTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitterTest.java new file mode 100644 index 0000000000..c4453b14e0 --- /dev/null +++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitterTest.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.httptunnel; + +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); + } + +}