NETTY-246: adding implementation to netty trunk for the 4.0.0 release. In order to get the tests to pass,
I had to change the surefire fork policy from "never" to "once", as it seems the classloader settings used by the maven process were preventing jmock from mocking package private interfaces.
This commit is contained in:
parent
4aef19e947
commit
12d2c8ad2e
8
pom.xml
8
pom.xml
@ -177,6 +177,12 @@
|
||||
<version>2.5.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jmock</groupId>
|
||||
<artifactId>jmock-junit4</artifactId>
|
||||
<version>2.5.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
@ -254,7 +260,7 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.7.2</version>
|
||||
<configuration>
|
||||
<forkMode>never</forkMode>
|
||||
<forkMode>once</forkMode>
|
||||
<excludes>
|
||||
<exclude>**/Abstract*</exclude>
|
||||
<exclude>**/TestUtil*</exclude>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 = "<UNKNOWN>";
|
||||
}
|
||||
LOG.warn("Rejecting request from " + remoteAddress + " representing tunnel " + tunnelId + ": " + errorMessage);
|
||||
}
|
||||
HttpResponse rejection = HttpTunnelMessageUtils.createRejection(rejectedRequest, errorMessage);
|
||||
respondWith(ctx, rejection).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
@ -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<ChannelFuture> pendingFutures;
|
||||
|
||||
public ChannelFutureAggregator(ChannelFuture aggregateFuture)
|
||||
{
|
||||
this.aggregateFuture = aggregateFuture;
|
||||
pendingFutures = new HashSet<ChannelFuture>();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* HTTP tunnel clients have the following additional options:
|
||||
*
|
||||
* <table border="1" cellspacing="0" cellpadding="6">
|
||||
* <tr>
|
||||
* <th>Name</th><th>Associated setter method</th>
|
||||
* </tr>
|
||||
* <tr><td>{@code "writeBufferHighWaterMark"}</td><td>{@link #setWriteBufferHighWaterMark(int)}</td></tr>
|
||||
* <tr><td>{@code "writeBufferLowWaterMark"}</td><td>{@link #setWriteBufferLowWaterMark(int)}</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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}.
|
||||
* <p>
|
||||
* HTTP tunnel clients have the following additional options:
|
||||
*
|
||||
* <table border="1" cellspacing="0" cellpadding="6">
|
||||
* <tr>
|
||||
* <th>Name</th><th>Associated setter method</th>
|
||||
* </tr>
|
||||
* <tr><td>{@code "proxyAddress"}</td><td>{@link #setProxyAddress(SocketAddress)}</td></tr>
|
||||
* <tr><td>{@code "writeBufferHighWaterMark"}</td><td>{@link #setWriteBufferHighWaterMark(long)}</td></tr>
|
||||
* <tr><td>{@code "writeBufferLowWaterMark"}</td><td>{@link #setWriteBufferLowWaterMark(long)}</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<MessageEvent> queuedWrites;
|
||||
|
||||
private final AtomicInteger pendingRequestCount;
|
||||
|
||||
private long sendRequestTime;
|
||||
|
||||
public HttpTunnelClientSendHandler(HttpTunnelClientWorkerOwner tunnelChannel)
|
||||
{
|
||||
this.tunnelChannel = tunnelChannel;
|
||||
queuedWrites = new ConcurrentLinkedQueue<MessageEvent>();
|
||||
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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, Object> options)
|
||||
{
|
||||
for (Entry<String, Object> 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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<String, TunnelInfo> tunnelsById;
|
||||
|
||||
public ServerMessageSwitch(HttpTunnelAcceptedChannelFactory newChannelFactory)
|
||||
{
|
||||
this.newChannelFactory = newChannelFactory;
|
||||
tunnelIdPrefix = Long.toHexString(new Random().nextLong());
|
||||
tunnelsById = new ConcurrentHashMap<String, TunnelInfo>();
|
||||
}
|
||||
|
||||
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<QueuedResponse> 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<ChannelBuffer> 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<Channel> responseChannel = new AtomicReference<Channel>(null);
|
||||
|
||||
public final Queue<QueuedResponse> queuedResponses = new ConcurrentLinkedQueue<QueuedResponse>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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<ChannelBuffer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ChannelBuffer> split(ChannelBuffer buffer, int splitThreshold)
|
||||
{
|
||||
int listSize = (int) ((float) buffer.readableBytes() / splitThreshold);
|
||||
ArrayList<ChannelBuffer> fragmentList = new ArrayList<ChannelBuffer>(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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<String, Object> options)
|
||||
{
|
||||
for (Entry<String, Object> e : options.entrySet())
|
||||
{
|
||||
setOption(e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<ChannelEvent> events = new LinkedList<ChannelEvent>();
|
||||
|
||||
public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception
|
||||
{
|
||||
events.add(e);
|
||||
}
|
||||
|
||||
}
|
@ -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<FakeSocketChannel> createdChannels;
|
||||
|
||||
public FakeClientSocketChannelFactory()
|
||||
{
|
||||
createdChannels = new ArrayList<FakeSocketChannel>();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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> channel = new AtomicReference<Channel>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ChannelBuffer> messages = new ArrayList<ChannelBuffer>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -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<ChannelBuffer> splitIntoChunks(int chunkSize, ChannelBuffer... buffers)
|
||||
{
|
||||
LinkedList<ChannelBuffer> chunks = new LinkedList<ChannelBuffer>();
|
||||
|
||||
ArrayList<ChannelBuffer> sourceBuffers = new ArrayList<ChannelBuffer>();
|
||||
Collections.addAll(sourceBuffers, buffers);
|
||||
Iterator<ChannelBuffer> 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> T checkIsUpstreamMessageEvent(ChannelEvent event, Class<T> expectedMessageType)
|
||||
{
|
||||
assertTrue(event instanceof UpstreamMessageEvent);
|
||||
UpstreamMessageEvent messageEvent = (UpstreamMessageEvent) event;
|
||||
assertTrue(expectedMessageType.isInstance(messageEvent.getMessage()));
|
||||
return expectedMessageType.cast(messageEvent.getMessage());
|
||||
}
|
||||
|
||||
public static <T> T checkIsDownstreamMessageEvent(ChannelEvent event, Class<T> 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;
|
||||
}
|
||||
}
|
@ -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<ChannelBuffer> 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<ChannelBuffer> 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<ChannelBuffer> 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<ChannelBuffer> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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<ChannelEvent> events = new LinkedList<ChannelEvent>();
|
||||
|
||||
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception
|
||||
{
|
||||
events.add(e);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ChannelBuffer> fragments = WriteSplitter.split(buffer, SPLIT_THRESHOLD);
|
||||
assertNotNull(fragments);
|
||||
assertEquals(1, fragments.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSplit_bufferMatchesThreshold()
|
||||
{
|
||||
ChannelBuffer buffer = createBufferWithContents(SPLIT_THRESHOLD);
|
||||
List<ChannelBuffer> 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<ChannelBuffer> 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<ChannelBuffer> 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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user