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

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

+ * HTTP tunnel clients have the following additional options: + * + * + * + * + * + * + * + * + *
NameAssociated setter method
{@code "proxyAddress"}{@link #setProxyAddress(SocketAddress)}
{@code "writeBufferHighWaterMark"}{@link #setWriteBufferHighWaterMark(long)}
{@code "writeBufferLowWaterMark"}{@link #setWriteBufferLowWaterMark(long)}
+ * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelClientChannelConfig extends HttpTunnelChannelConfig { + + static final String PROXY_ADDRESS_OPTION = "proxyAddress"; + + private final SocketChannelConfig sendChannelConfig; + + private final SocketChannelConfig pollChannelConfig; + + private volatile SocketAddress proxyAddress; + + HttpTunnelClientChannelConfig(SocketChannelConfig sendChannelConfig, + SocketChannelConfig pollChannelConfig) { + this.sendChannelConfig = sendChannelConfig; + this.pollChannelConfig = pollChannelConfig; + } + + /* HTTP TUNNEL SPECIFIC CONFIGURATION */ + // TODO Support all options in the old tunnel (see HttpTunnelingSocketChannelConfig) + // Mostly SSL, virtual host, and URL prefix + @Override + public boolean setOption(String key, Object value) { + if (PROXY_ADDRESS_OPTION.equals(key)) { + setProxyAddress((SocketAddress) value); + } else { + return super.setOption(key, value); + } + + return true; + } + + /** + * @return the address of the http proxy. If this is null, then no proxy + * should be used. + */ + public SocketAddress getProxyAddress() { + return proxyAddress; + } + + /** + * Specify a proxy to be used for the http tunnel. If this is null, then + * no proxy should be used, otherwise this should be a directly accessible IPv4/IPv6 + * address and port. + */ + public void setProxyAddress(SocketAddress proxyAddress) { + this.proxyAddress = proxyAddress; + } + + /* GENERIC SOCKET CHANNEL CONFIGURATION */ + + public int getReceiveBufferSize() { + return pollChannelConfig.getReceiveBufferSize(); + } + + public int getSendBufferSize() { + return pollChannelConfig.getSendBufferSize(); + } + + public int getSoLinger() { + return pollChannelConfig.getSoLinger(); + } + + public int getTrafficClass() { + return pollChannelConfig.getTrafficClass(); + } + + public boolean isKeepAlive() { + return pollChannelConfig.isKeepAlive(); + } + + public boolean isReuseAddress() { + return pollChannelConfig.isReuseAddress(); + } + + public boolean isTcpNoDelay() { + return pollChannelConfig.isTcpNoDelay(); + } + + public void setKeepAlive(boolean keepAlive) { + pollChannelConfig.setKeepAlive(keepAlive); + sendChannelConfig.setKeepAlive(keepAlive); + } + + public void setPerformancePreferences(int connectionTime, int latency, + int bandwidth) { + pollChannelConfig.setPerformancePreferences(connectionTime, latency, + bandwidth); + sendChannelConfig.setPerformancePreferences(connectionTime, latency, + bandwidth); + } + + public void setReceiveBufferSize(int receiveBufferSize) { + pollChannelConfig.setReceiveBufferSize(receiveBufferSize); + sendChannelConfig.setReceiveBufferSize(receiveBufferSize); + } + + public void setReuseAddress(boolean reuseAddress) { + pollChannelConfig.setReuseAddress(reuseAddress); + sendChannelConfig.setReuseAddress(reuseAddress); + } + + public void setSendBufferSize(int sendBufferSize) { + pollChannelConfig.setSendBufferSize(sendBufferSize); + sendChannelConfig.setSendBufferSize(sendBufferSize); + } + + public void setSoLinger(int soLinger) { + pollChannelConfig.setSoLinger(soLinger); + sendChannelConfig.setSoLinger(soLinger); + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + pollChannelConfig.setTcpNoDelay(true); + sendChannelConfig.setTcpNoDelay(true); + } + + public void setTrafficClass(int trafficClass) { + pollChannelConfig.setTrafficClass(1); + sendChannelConfig.setTrafficClass(1); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelFactory.java new file mode 100644 index 00000000000..78d2ce8a623 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.channel.socket.ClientSocketChannelFactory; + +/** + * Factory used to create new client channels. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelClientChannelFactory implements + ClientSocketChannelFactory { + + private final ClientSocketChannelFactory factory; + + private final ChannelGroup realConnections = new DefaultChannelGroup(); + + public HttpTunnelClientChannelFactory(ClientSocketChannelFactory factory) { + if (factory == null) { + throw new NullPointerException("factory"); + } + this.factory = factory; + } + + public HttpTunnelClientChannel newChannel(ChannelPipeline pipeline) { + return new HttpTunnelClientChannel(this, pipeline, + new HttpTunnelClientChannelSink(), factory, realConnections); + } + + public void releaseExternalResources() { + realConnections.close().awaitUninterruptibly(); + factory.releaseExternalResources(); + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelSink.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelSink.java new file mode 100644 index 00000000000..5284691a830 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientChannelSink.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.InetSocketAddress; + +import org.jboss.netty.channel.AbstractChannelSink; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.MessageEvent; + +/** + * Sink of a client channel, deals with sunk events and then makes appropriate calls + * on the channel itself to push data. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientChannelSink extends AbstractChannelSink { + + public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) + throws Exception { + if (e instanceof ChannelStateEvent) { + handleChannelStateEvent((ChannelStateEvent) e); + } else if (e instanceof MessageEvent) { + handleMessageEvent((MessageEvent) e); + } + } + + private void handleMessageEvent(MessageEvent e) { + HttpTunnelClientChannel channel = + (HttpTunnelClientChannel) e.getChannel(); + channel.sendData(e); + } + + private void handleChannelStateEvent(ChannelStateEvent e) { + HttpTunnelClientChannel channel = + (HttpTunnelClientChannel) e.getChannel(); + + switch (e.getState()) { + case CONNECTED: + if (e.getValue() != null) { + channel.onConnectRequest(e.getFuture(), + (InetSocketAddress) e.getValue()); + } else { + channel.onDisconnectRequest(e.getFuture()); + } + break; + case BOUND: + if (e.getValue() != null) { + channel.onBindRequest((InetSocketAddress) e.getValue(), + e.getFuture()); + } else { + channel.onUnbindRequest(e.getFuture()); + } + break; + case OPEN: + if (Boolean.FALSE.equals(e.getValue())) { + channel.onCloseRequest(e.getFuture()); + } + break; + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandler.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandler.java new file mode 100644 index 00000000000..1ee05563163 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientPollHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Pipeline component which controls the client poll loop to the server. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientPollHandler extends SimpleChannelHandler { + + public static final String NAME = "server2client"; + + private static final InternalLogger LOG = InternalLoggerFactory + .getInstance(HttpTunnelClientPollHandler.class); + + private String tunnelId; + + private final HttpTunnelClientWorkerOwner tunnelChannel; + + private long pollTime; + + public HttpTunnelClientPollHandler(HttpTunnelClientWorkerOwner tunnelChannel) { + this.tunnelChannel = tunnelChannel; + } + + public void setTunnelId(String tunnelId) { + this.tunnelId = tunnelId; + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("Poll channel for tunnel " + tunnelId + " established"); + } + tunnelChannel.fullyEstablished(); + sendPoll(ctx); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + HttpResponse response = (HttpResponse) e.getMessage(); + + if (HttpTunnelMessageUtils.isOKResponse(response)) { + long rtTime = System.nanoTime() - pollTime; + if (LOG.isDebugEnabled()) { + LOG.debug("OK response received for poll on tunnel " + + tunnelId + " after " + rtTime + " ns"); + } + tunnelChannel.onMessageReceived(response.getContent()); + sendPoll(ctx); + } else { + if (LOG.isWarnEnabled()) { + LOG.warn("non-OK response received for poll on tunnel " + + tunnelId); + } + } + } + + private void sendPoll(ChannelHandlerContext ctx) { + pollTime = System.nanoTime(); + if (LOG.isDebugEnabled()) { + LOG.debug("sending poll request for tunnel " + tunnelId); + } + HttpRequest request = + HttpTunnelMessageUtils.createReceiveDataRequest( + tunnelChannel.getServerHostName(), tunnelId); + Channels.write(ctx, Channels.future(ctx.getChannel()), request); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandler.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandler.java new file mode 100644 index 00000000000..5a44364e4fa --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientSendHandler.java @@ -0,0 +1,245 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.DownstreamMessageEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Pipeline component which deals with sending data from the client to server. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelClientSendHandler extends SimpleChannelHandler { + + public static final String NAME = "client2server"; + + private static final InternalLogger LOG = InternalLoggerFactory + .getInstance(HttpTunnelClientSendHandler.class); + + private final HttpTunnelClientWorkerOwner tunnelChannel; + + private String tunnelId = null; + + private final AtomicBoolean disconnecting; + + private ChannelStateEvent postShutdownEvent; + + private final ConcurrentLinkedQueue queuedWrites; + + private final AtomicInteger pendingRequestCount; + + private long sendRequestTime; + + public HttpTunnelClientSendHandler(HttpTunnelClientWorkerOwner tunnelChannel) { + this.tunnelChannel = tunnelChannel; + queuedWrites = new ConcurrentLinkedQueue(); + pendingRequestCount = new AtomicInteger(0); + disconnecting = new AtomicBoolean(false); + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + if (tunnelId == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("connection to " + e.getValue() + + " succeeded - sending open tunnel request"); + } + HttpRequest request = + HttpTunnelMessageUtils + .createOpenTunnelRequest(tunnelChannel + .getServerHostName()); + Channel thisChannel = ctx.getChannel(); + DownstreamMessageEvent event = + new DownstreamMessageEvent(thisChannel, + Channels.future(thisChannel), request, + thisChannel.getRemoteAddress()); + queuedWrites.offer(event); + pendingRequestCount.incrementAndGet(); + sendQueuedData(ctx); + } + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + HttpResponse response = (HttpResponse) e.getMessage(); + + if (HttpTunnelMessageUtils.isOKResponse(response)) { + long roundTripTime = System.nanoTime() - sendRequestTime; + if (LOG.isDebugEnabled()) { + LOG.debug("OK response received for tunnel " + tunnelId + + ", after " + roundTripTime + " ns"); + } + sendNextAfterResponse(ctx); + } else if (HttpTunnelMessageUtils.isTunnelOpenResponse(response)) { + tunnelId = HttpTunnelMessageUtils.extractCookie(response); + if (LOG.isDebugEnabled()) { + LOG.debug("tunnel open request accepted - id " + tunnelId); + } + tunnelChannel.onTunnelOpened(tunnelId); + sendNextAfterResponse(ctx); + } else if (HttpTunnelMessageUtils.isTunnelCloseResponse(response)) { + if (LOG.isDebugEnabled()) { + if (disconnecting.get()) { + LOG.debug("server acknowledged disconnect for tunnel " + + tunnelId); + } else { + LOG.debug("server closed tunnel " + tunnelId); + } + } + ctx.sendDownstream(postShutdownEvent); + } else { + // TODO: kill connection + if (LOG.isWarnEnabled()) { + LOG.warn("unknown response received for tunnel " + tunnelId + + ", closing connection"); + } + Channels.close(ctx, ctx.getChannel().getCloseFuture()); + } + } + + private void sendNextAfterResponse(ChannelHandlerContext ctx) { + if (pendingRequestCount.decrementAndGet() > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("Immediately sending next send request for tunnel " + + tunnelId); + } + sendQueuedData(ctx); + } + } + + private synchronized void sendQueuedData(ChannelHandlerContext ctx) { + if (disconnecting.get()) { + if (LOG.isDebugEnabled()) { + LOG.debug("sending close request for tunnel " + tunnelId); + } + HttpRequest closeRequest = + HttpTunnelMessageUtils.createCloseTunnelRequest( + tunnelChannel.getServerHostName(), tunnelId); + Channels.write(ctx, Channels.future(ctx.getChannel()), closeRequest); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("sending next request for tunnel " + tunnelId); + } + MessageEvent nextWrite = queuedWrites.poll(); + sendRequestTime = System.nanoTime(); + ctx.sendDownstream(nextWrite); + } + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("request to send data for tunnel " + tunnelId); + } + if (disconnecting.get()) { + if (LOG.isWarnEnabled()) { + LOG.warn("rejecting write request for tunnel " + tunnelId + + " received after disconnect requested"); + } + e.getFuture().setFailure( + new IllegalStateException("tunnel is closing")); + return; + } + ChannelBuffer data = (ChannelBuffer) e.getMessage(); + HttpRequest request = + HttpTunnelMessageUtils.createSendDataRequest( + tunnelChannel.getServerHostName(), tunnelId, data); + DownstreamMessageEvent translatedEvent = + new DownstreamMessageEvent(ctx.getChannel(), e.getFuture(), + request, ctx.getChannel().getRemoteAddress()); + queuedWrites.offer(translatedEvent); + if (pendingRequestCount.incrementAndGet() == 1) { + sendQueuedData(ctx); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("write request for tunnel " + tunnelId + " queued"); + } + } + } + + @Override + public void closeRequested(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + shutdownTunnel(ctx, e); + } + + @Override + public void disconnectRequested(ChannelHandlerContext ctx, + ChannelStateEvent e) throws Exception { + shutdownTunnel(ctx, e); + } + + @Override + public void unbindRequested(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + shutdownTunnel(ctx, e); + } + + private void shutdownTunnel(ChannelHandlerContext ctx, + ChannelStateEvent postShutdownEvent) { + if (LOG.isDebugEnabled()) { + LOG.debug("tunnel shutdown requested for send channel of tunnel " + + tunnelId); + } + if (!ctx.getChannel().isConnected()) { + if (LOG.isDebugEnabled()) { + LOG.debug("send channel of tunnel " + tunnelId + + " is already disconnected"); + } + ctx.sendDownstream(postShutdownEvent); + return; + } + + if (!disconnecting.compareAndSet(false, true)) { + if (LOG.isWarnEnabled()) { + LOG.warn("tunnel shutdown process already initiated for tunnel " + + tunnelId); + } + return; + } + + this.postShutdownEvent = postShutdownEvent; + + // if the channel is idle, send a close request immediately + if (pendingRequestCount.incrementAndGet() == 1) { + sendQueuedData(ctx); + } + } + + public String getTunnelId() { + return tunnelId; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientWorkerOwner.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientWorkerOwner.java new file mode 100644 index 00000000000..59f3d161544 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelClientWorkerOwner.java @@ -0,0 +1,68 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.InetSocketAddress; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelFuture; + +/** + * Interface which is used by the send and poll "worker" channels + * to notify the virtual tunnel channel of key events, and to get + * access to higher level information required for correct + * operation. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface HttpTunnelClientWorkerOwner { + /** + * The HTTP tunnel client sink invokes this when the application code requests the connection + * of an HTTP tunnel to the specified remote address. + */ + public void onConnectRequest(ChannelFuture connectFuture, + InetSocketAddress remoteAddress); + + /** + * The send channel handler calls this method when the server accepts the open tunnel request, + * returning a unique tunnel ID. + * + * @param tunnelId the server allocated tunnel ID + */ + public void onTunnelOpened(String tunnelId); + + /** + * The poll channel handler calls this method when the poll channel is connected, indicating + * that full duplex communications are now possible. + */ + public void fullyEstablished(); + + /** + * The poll handler calls this method when some data is received and decoded from the server. + * @param content the data received from the server + */ + public void onMessageReceived(ChannelBuffer content); + + /** + * @return the name of the server with whom we are communicating with - this is used within + * the HOST HTTP header for all requests. This is particularly important for operation behind + * a proxy, where the HOST string is used to route the request. + */ + public String getServerHostName(); + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelMessageUtils.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelMessageUtils.java new file mode 100644 index 00000000000..2d9c45da789 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelMessageUtils.java @@ -0,0 +1,342 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.http.DefaultHttpRequest; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; + +/** + * Utility class for creating http requests for the operation of the full duplex + * http tunnel, and verifying that received requests are of the correct types. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelMessageUtils { + + private static final String HTTP_URL_PREFIX = "http://"; + + /** + * An upper bound is enforced on the size of message bodies, so as + * to ensure we do not dump large chunks of data on either peer. + */ + public static final int MAX_BODY_SIZE = 16 * 1024; + + /** + * The tunnel will only accept connections from this specific user agent. This + * allows us to distinguish a legitimate tunnel connection from someone pointing + * a web browser or robot at the tunnel URL. + */ + static final String USER_AGENT = "HttpTunnelClient"; + + static final String OPEN_TUNNEL_REQUEST_URI = "/http-tunnel/open"; + + static final String CLOSE_TUNNEL_REQUEST_URI = "/http-tunnel/close"; + + static final String CLIENT_SEND_REQUEST_URI = "/http-tunnel/send"; + + static final String CLIENT_RECV_REQUEST_URI = "/http-tunnel/poll"; + + static final String CONTENT_TYPE = "application/octet-stream"; + + public static HttpRequest createOpenTunnelRequest(SocketAddress host) { + return createOpenTunnelRequest(convertToHostString(host)); + } + + public static HttpRequest createOpenTunnelRequest(String host) { + HttpRequest request = + createRequestTemplate(host, null, OPEN_TUNNEL_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isOpenTunnelRequest(HttpRequest request) { + return isRequestTo(request, OPEN_TUNNEL_REQUEST_URI); + } + + public static boolean checkHost(HttpRequest request, + SocketAddress expectedHost) { + String host = request.getHeader(HttpHeaders.Names.HOST); + return expectedHost == null? host == null : HttpTunnelMessageUtils + .convertToHostString(expectedHost).equals(host); + } + + public static HttpRequest createSendDataRequest(SocketAddress host, + String cookie, ChannelBuffer data) { + return createSendDataRequest(convertToHostString(host), cookie, data); + } + + public static HttpRequest createSendDataRequest(String host, String cookie, + ChannelBuffer data) { + HttpRequest request = + createRequestTemplate(host, cookie, CLIENT_SEND_REQUEST_URI); + request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, + Long.toString(data.readableBytes())); + request.setContent(data); + + return request; + } + + public static boolean isSendDataRequest(HttpRequest request) { + return isRequestTo(request, CLIENT_SEND_REQUEST_URI); + } + + public static HttpRequest createReceiveDataRequest(SocketAddress host, + String tunnelId) { + return createReceiveDataRequest(convertToHostString(host), tunnelId); + } + + public static HttpRequest createReceiveDataRequest(String host, + String tunnelId) { + HttpRequest request = + createRequestTemplate(host, tunnelId, CLIENT_RECV_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isReceiveDataRequest(HttpRequest request) { + return isRequestTo(request, CLIENT_RECV_REQUEST_URI); + } + + public static HttpRequest createCloseTunnelRequest(String host, + String tunnelId) { + HttpRequest request = + createRequestTemplate(host, tunnelId, CLOSE_TUNNEL_REQUEST_URI); + setNoData(request); + return request; + } + + public static boolean isCloseTunnelRequest(HttpRequest request) { + return isRequestTo(request, CLOSE_TUNNEL_REQUEST_URI); + } + + public static boolean isServerToClientRequest(HttpRequest request) { + return isRequestTo(request, CLIENT_RECV_REQUEST_URI); + } + + public static String convertToHostString(SocketAddress hostAddress) { + StringWriter host = new StringWriter(); + InetSocketAddress inetSocketAddr = (InetSocketAddress) hostAddress; + InetAddress addr = inetSocketAddr.getAddress(); + if (addr instanceof Inet6Address) { + host.append('['); + host.append(addr.getHostAddress()); + host.append(']'); + } else if (addr != null) { + host.append(addr.getHostAddress()); + } else { + host.append(inetSocketAddr.getHostName()); + } + + host.append(':'); + host.append(Integer.toString(inetSocketAddr.getPort())); + return host.toString(); + } + + private static HttpRequest createRequestTemplate(String host, + String tunnelId, String uri) { + HttpRequest request = + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, + createCompleteUri(host, uri)); + request.setHeader(HttpHeaders.Names.HOST, host); + request.setHeader(HttpHeaders.Names.USER_AGENT, USER_AGENT); + if (tunnelId != null) { + request.setHeader(HttpHeaders.Names.COOKIE, tunnelId); + } + + return request; + } + + private static String createCompleteUri(String host, String uri) { + StringBuilder builder = + new StringBuilder(HTTP_URL_PREFIX.length() + host.length() + + uri.length()); + builder.append(HTTP_URL_PREFIX); + builder.append(host); + builder.append(uri); + + return builder.toString(); + } + + private static boolean isRequestTo(HttpRequest request, String uri) { + URI decodedUri; + try { + decodedUri = new URI(request.getUri()); + } catch (URISyntaxException e) { + return false; + } + + return HttpVersion.HTTP_1_1.equals(request.getProtocolVersion()) && + USER_AGENT.equals(request + .getHeader(HttpHeaders.Names.USER_AGENT)) && + HttpMethod.POST.equals(request.getMethod()) && + uri.equals(decodedUri.getPath()); + } + + private static void setNoData(HttpRequest request) { + request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); + request.setContent(null); + } + + public static String extractTunnelId(HttpRequest request) { + return request.getHeader(HttpHeaders.Names.COOKIE); + } + + private static byte[] toBytes(String string) { + try { + return string.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // UTF-8 is meant to be supported on all platforms + throw new RuntimeException("UTF-8 encoding not supported!"); + } + } + + public static HttpResponse createTunnelOpenResponse(String tunnelId) { + HttpResponse response = + createResponseTemplate(HttpResponseStatus.CREATED, null); + response.setHeader(HttpHeaders.Names.SET_COOKIE, tunnelId); + return response; + } + + public static boolean isTunnelOpenResponse(HttpResponse response) { + return isResponseWithCode(response, HttpResponseStatus.CREATED); + } + + public static boolean isOKResponse(HttpResponse response) { + return isResponseWithCode(response, HttpResponseStatus.OK); + } + + public static boolean hasContents(HttpResponse response, + byte[] expectedContents) { + if (response.getContent() != null && + HttpHeaders.getContentLength(response) == expectedContents.length && + response.getContent().readableBytes() == expectedContents.length) { + byte[] compareBytes = new byte[expectedContents.length]; + response.getContent().readBytes(compareBytes); + return Arrays.equals(expectedContents, compareBytes); + } + + return false; + } + + public static HttpResponse createTunnelCloseResponse() { + HttpResponse response = + createResponseTemplate(HttpResponseStatus.RESET_CONTENT, null); + return response; + } + + public static boolean isTunnelCloseResponse(HttpResponse response) { + return isResponseWithCode(response, HttpResponseStatus.RESET_CONTENT); + } + + public static String extractCookie(HttpResponse response) { + if (response.containsHeader(HttpHeaders.Names.SET_COOKIE)) { + return response.getHeader(HttpHeaders.Names.SET_COOKIE); + } + + return null; + } + + public static HttpResponse createSendDataResponse() { + return createOKResponseTemplate(null); + } + + public static HttpResponse createRecvDataResponse(ChannelBuffer data) { + return createOKResponseTemplate(data); + } + + public static HttpResponse createRejection(HttpRequest request, + String reason) { + HttpVersion version = + request != null? request.getProtocolVersion() + : HttpVersion.HTTP_1_1; + HttpResponse response = + new DefaultHttpResponse(version, HttpResponseStatus.BAD_REQUEST); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, + "text/plain; charset=\"utf-8\""); + ChannelBuffer reasonBuffer = + ChannelBuffers.wrappedBuffer(toBytes(reason)); + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, + Integer.toString(reasonBuffer.readableBytes())); + response.setContent(reasonBuffer); + return response; + } + + public static boolean isRejection(HttpResponse response) { + return !HttpResponseStatus.OK.equals(response.getStatus()); + } + + public static Object extractErrorMessage(HttpResponse response) { + if (response.getContent() == null || + HttpHeaders.getContentLength(response) == 0) { + return ""; + } + + byte[] bytes = new byte[response.getContent().readableBytes()]; + response.getContent().readBytes(bytes); + try { + return new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + } + + private static boolean isResponseWithCode(HttpResponse response, + HttpResponseStatus status) { + return HttpVersion.HTTP_1_1.equals(response.getProtocolVersion()) && + status.equals(response.getStatus()); + } + + private static HttpResponse createOKResponseTemplate(ChannelBuffer data) { + return createResponseTemplate(HttpResponseStatus.OK, data); + } + + private static HttpResponse createResponseTemplate( + HttpResponseStatus status, ChannelBuffer data) { + HttpResponse response = + new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); + if (data != null) { + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, + Integer.toString(data.readableBytes())); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, + "application/octet-stream"); + response.setContent(data); + } else { + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); + response.setContent(null); + } + return response; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannel.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannel.java new file mode 100644 index 00000000000..53764759e17 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannel.java @@ -0,0 +1,110 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.InetSocketAddress; + +import org.jboss.netty.channel.AbstractServerChannel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineException; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.socket.ServerSocketChannel; +import org.jboss.netty.channel.socket.ServerSocketChannelConfig; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannel extends AbstractServerChannel implements + ServerSocketChannel { + + private final ServerSocketChannel realChannel; + + private final HttpTunnelServerChannelConfig config; + + private final ServerMessageSwitch messageSwitch; + + private final ChannelFutureListener CLOSE_FUTURE_PROXY = + new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) + throws Exception { + HttpTunnelServerChannel.this.setClosed(); + } + }; + + protected HttpTunnelServerChannel(HttpTunnelServerChannelFactory factory, + ChannelPipeline pipeline) { + super(factory, pipeline, new HttpTunnelServerChannelSink()); + + messageSwitch = new ServerMessageSwitch(new TunnelCreator()); + realChannel = factory.createRealChannel(this, messageSwitch); + HttpTunnelServerChannelSink sink = + (HttpTunnelServerChannelSink) getPipeline().getSink(); + sink.setRealChannel(realChannel); + sink.setCloseListener(CLOSE_FUTURE_PROXY); + config = new HttpTunnelServerChannelConfig(realChannel); + Channels.fireChannelOpen(this); + } + + public ServerSocketChannelConfig getConfig() { + return config; + } + + public InetSocketAddress getLocalAddress() { + return realChannel.getLocalAddress(); + } + + public InetSocketAddress getRemoteAddress() { + // server channels never have a remote address + return null; + } + + public boolean isBound() { + return realChannel.isBound(); + } + + /** + * Used to hide the newChannel method from the public API. + */ + private final class TunnelCreator implements + HttpTunnelAcceptedChannelFactory { + + public HttpTunnelAcceptedChannelReceiver newChannel(String newTunnelId, + InetSocketAddress remoteAddress) { + ChannelPipeline childPipeline = null; + try { + childPipeline = getConfig().getPipelineFactory().getPipeline(); + } catch (Exception e) { + throw new ChannelPipelineException( + "Failed to initialize a pipeline.", e); + } + HttpTunnelAcceptedChannelConfig config = + new HttpTunnelAcceptedChannelConfig(); + HttpTunnelAcceptedChannelSink sink = + new HttpTunnelAcceptedChannelSink(messageSwitch, + newTunnelId, config); + return new HttpTunnelAcceptedChannel(HttpTunnelServerChannel.this, + getFactory(), childPipeline, sink, remoteAddress, config); + } + + public String generateTunnelId() { + return config.getTunnelIdGenerator().generateId(); + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelConfig.java new file mode 100644 index 00000000000..db54ef039ad --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelConfig.java @@ -0,0 +1,127 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.util.Map; +import java.util.Map.Entry; + +import org.jboss.netty.buffer.ChannelBufferFactory; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.socket.ServerSocketChannel; +import org.jboss.netty.channel.socket.ServerSocketChannelConfig; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannelConfig implements ServerSocketChannelConfig { + + private ChannelPipelineFactory pipelineFactory; + + private final ServerSocketChannel realChannel; + + private TunnelIdGenerator tunnelIdGenerator = + new DefaultTunnelIdGenerator(); + + public HttpTunnelServerChannelConfig(ServerSocketChannel realChannel) { + this.realChannel = realChannel; + } + + private ServerSocketChannelConfig getWrappedConfig() { + return realChannel.getConfig(); + } + + public int getBacklog() { + return getWrappedConfig().getBacklog(); + } + + public int getReceiveBufferSize() { + return getWrappedConfig().getReceiveBufferSize(); + } + + public boolean isReuseAddress() { + return getWrappedConfig().isReuseAddress(); + } + + public void setBacklog(int backlog) { + getWrappedConfig().setBacklog(backlog); + } + + public void setPerformancePreferences(int connectionTime, int latency, + int bandwidth) { + getWrappedConfig().setPerformancePreferences(connectionTime, latency, + bandwidth); + } + + public void setReceiveBufferSize(int receiveBufferSize) { + getWrappedConfig().setReceiveBufferSize(receiveBufferSize); + } + + public void setReuseAddress(boolean reuseAddress) { + getWrappedConfig().setReuseAddress(reuseAddress); + } + + public ChannelBufferFactory getBufferFactory() { + return getWrappedConfig().getBufferFactory(); + } + + public int getConnectTimeoutMillis() { + return getWrappedConfig().getConnectTimeoutMillis(); + } + + public ChannelPipelineFactory getPipelineFactory() { + return pipelineFactory; + } + + public void setBufferFactory(ChannelBufferFactory bufferFactory) { + getWrappedConfig().setBufferFactory(bufferFactory); + } + + public void setConnectTimeoutMillis(int connectTimeoutMillis) { + getWrappedConfig().setConnectTimeoutMillis(connectTimeoutMillis); + } + + public boolean setOption(String name, Object value) { + if (name.equals("pipelineFactory")) { + setPipelineFactory((ChannelPipelineFactory) value); + return true; + } else if (name.equals("tunnelIdGenerator")) { + setTunnelIdGenerator((TunnelIdGenerator) value); + return true; + } else { + return getWrappedConfig().setOption(name, value); + } + } + + public void setOptions(Map options) { + for (Entry e: options.entrySet()) { + setOption(e.getKey(), e.getValue()); + } + } + + public void setPipelineFactory(ChannelPipelineFactory pipelineFactory) { + this.pipelineFactory = pipelineFactory; + } + + public void setTunnelIdGenerator(TunnelIdGenerator tunnelIdGenerator) { + this.tunnelIdGenerator = tunnelIdGenerator; + } + + public TunnelIdGenerator getTunnelIdGenerator() { + return tunnelIdGenerator; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactory.java new file mode 100644 index 00000000000..616f8e58e8e --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.channel.socket.ServerSocketChannel; +import org.jboss.netty.channel.socket.ServerSocketChannelFactory; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class HttpTunnelServerChannelFactory implements + ServerSocketChannelFactory { + + private final ServerSocketChannelFactory realConnectionFactory; + + private final ChannelGroup realConnections; + + public HttpTunnelServerChannelFactory( + ServerSocketChannelFactory realConnectionFactory) { + this.realConnectionFactory = realConnectionFactory; + realConnections = new DefaultChannelGroup(); + } + + public HttpTunnelServerChannel newChannel(ChannelPipeline pipeline) { + return new HttpTunnelServerChannel(this, pipeline); + } + + ServerSocketChannel createRealChannel(HttpTunnelServerChannel channel, + ServerMessageSwitch messageSwitch) { + ChannelPipeline realChannelPipeline = Channels.pipeline(); + AcceptedServerChannelPipelineFactory realPipelineFactory = + new AcceptedServerChannelPipelineFactory(messageSwitch); + realChannelPipeline.addFirst(TunnelWrappedServerChannelHandler.NAME, + new TunnelWrappedServerChannelHandler(channel, + realPipelineFactory, realConnections)); + ServerSocketChannel newChannel = + realConnectionFactory.newChannel(realChannelPipeline); + realConnections.add(newChannel); + return newChannel; + } + + public void releaseExternalResources() { + realConnections.close().awaitUninterruptibly(); + realConnectionFactory.releaseExternalResources(); + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSink.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSink.java new file mode 100644 index 00000000000..d00b1c56de8 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelServerChannelSink.java @@ -0,0 +1,86 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.SocketAddress; + +import org.jboss.netty.channel.AbstractChannelSink; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.socket.ServerSocketChannel; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class HttpTunnelServerChannelSink extends AbstractChannelSink { + + private ChannelFutureListener closeHook; + + private ServerSocketChannel realChannel; + + public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) + throws Exception { + + if (e instanceof ChannelStateEvent) { + ChannelStateEvent ev = (ChannelStateEvent) e; + switch (ev.getState()) { + case OPEN: + if (Boolean.FALSE.equals(ev.getValue())) { + realChannel.close().addListener(closeHook); + } + break; + case BOUND: + if (ev.getValue() != null) { + realChannel.bind((SocketAddress) ev.getValue()) + .addListener(new ChannelFutureProxy(e.getFuture())); + } else { + realChannel.unbind().addListener( + new ChannelFutureProxy(e.getFuture())); + } + break; + } + } + } + + private final class ChannelFutureProxy implements ChannelFutureListener { + private final ChannelFuture upstreamFuture; + + ChannelFutureProxy(ChannelFuture upstreamFuture) { + this.upstreamFuture = upstreamFuture; + } + + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + upstreamFuture.setSuccess(); + } else { + upstreamFuture.setFailure(future.getCause()); + } + } + } + + public void setRealChannel(ServerSocketChannel realChannel) { + this.realChannel = realChannel; + } + + public void setCloseListener(ChannelFutureListener closeHook) { + this.closeHook = closeHook; + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannel.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannel.java deleted file mode 100644 index bcd93b326b2..00000000000 --- a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannel.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.channel.socket.http; - -import static org.jboss.netty.channel.Channels.*; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.channels.NotYetConnectedException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -import org.jboss.netty.buffer.ChannelBuffer; -import org.jboss.netty.buffer.ChannelBuffers; -import org.jboss.netty.channel.AbstractChannel; -import org.jboss.netty.channel.ChannelException; -import org.jboss.netty.channel.ChannelFactory; -import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelFutureListener; -import org.jboss.netty.channel.ChannelHandlerContext; -import org.jboss.netty.channel.ChannelPipeline; -import org.jboss.netty.channel.ChannelSink; -import org.jboss.netty.channel.ChannelStateEvent; -import org.jboss.netty.channel.DefaultChannelPipeline; -import org.jboss.netty.channel.ExceptionEvent; -import org.jboss.netty.channel.MessageEvent; -import org.jboss.netty.channel.SimpleChannelUpstreamHandler; -import org.jboss.netty.channel.socket.ClientSocketChannelFactory; -import org.jboss.netty.channel.socket.SocketChannel; -import org.jboss.netty.handler.codec.http.DefaultHttpChunk; -import org.jboss.netty.handler.codec.http.DefaultHttpRequest; -import org.jboss.netty.handler.codec.http.HttpChunk; -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.HttpRequestEncoder; -import org.jboss.netty.handler.codec.http.HttpResponse; -import org.jboss.netty.handler.codec.http.HttpResponseDecoder; -import org.jboss.netty.handler.codec.http.HttpResponseStatus; -import org.jboss.netty.handler.codec.http.HttpVersion; -import org.jboss.netty.handler.ssl.SslHandler; - -/** - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @author Trustin Lee - * @version $Rev$, $Date$ - */ -class HttpTunnelingClientSocketChannel extends AbstractChannel - implements org.jboss.netty.channel.socket.SocketChannel { - - final HttpTunnelingSocketChannelConfig config; - - volatile boolean requestHeaderWritten; - - final Object interestOpsLock = new Object(); - - final SocketChannel realChannel; - - private final HttpTunnelingClientSocketChannel.ServletChannelHandler handler = new ServletChannelHandler(); - - HttpTunnelingClientSocketChannel( - ChannelFactory factory, - ChannelPipeline pipeline, - ChannelSink sink, ClientSocketChannelFactory clientSocketChannelFactory) { - - super(null, factory, pipeline, sink); - - config = new HttpTunnelingSocketChannelConfig(this); - DefaultChannelPipeline channelPipeline = new DefaultChannelPipeline(); - channelPipeline.addLast("decoder", new HttpResponseDecoder()); - channelPipeline.addLast("encoder", new HttpRequestEncoder()); - channelPipeline.addLast("handler", handler); - realChannel = clientSocketChannelFactory.newChannel(channelPipeline); - - fireChannelOpen(this); - } - - @Override - public HttpTunnelingSocketChannelConfig getConfig() { - return config; - } - - @Override - public InetSocketAddress getLocalAddress() { - return realChannel.getLocalAddress(); - } - - @Override - public InetSocketAddress getRemoteAddress() { - return realChannel.getRemoteAddress(); - } - - @Override - public boolean isBound() { - return realChannel.isBound(); - } - - @Override - public boolean isConnected() { - return realChannel.isConnected(); - } - - @Override - public int getInterestOps() { - return realChannel.getInterestOps(); - } - - @Override - public boolean isWritable() { - return realChannel.isWritable(); - } - - @Override - protected boolean setClosed() { - return super.setClosed(); - } - - @Override - public ChannelFuture write(Object message, SocketAddress remoteAddress) { - if (remoteAddress == null || remoteAddress.equals(getRemoteAddress())) { - return super.write(message, null); - } - else { - return getUnsupportedOperationFuture(); - } - } - - void bindReal(final SocketAddress localAddress, final ChannelFuture future) { - realChannel.bind(localAddress).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - future.setSuccess(); - } else { - future.setFailure(f.getCause()); - } - } - }); - } - - void connectReal(final SocketAddress remoteAddress, final ChannelFuture future) { - final SocketChannel virtualChannel = this; - realChannel.connect(remoteAddress).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - final String serverName = config.getServerName(); - final int serverPort = ((InetSocketAddress) remoteAddress).getPort(); - final String serverPath = config.getServerPath(); - - if (f.isSuccess()) { - // Configure SSL - SSLContext sslContext = config.getSslContext(); - ChannelFuture sslHandshakeFuture = null; - if (sslContext != null) { - // Create a new SSLEngine from the specified SSLContext. - SSLEngine engine; - if (serverName != null) { - engine = sslContext.createSSLEngine(serverName, serverPort); - } else { - engine = sslContext.createSSLEngine(); - } - - // Configure the SSLEngine. - engine.setUseClientMode(true); - engine.setEnableSessionCreation(config.isEnableSslSessionCreation()); - String[] enabledCipherSuites = config.getEnabledSslCipherSuites(); - if (enabledCipherSuites != null) { - engine.setEnabledCipherSuites(enabledCipherSuites); - } - String[] enabledProtocols = config.getEnabledSslProtocols(); - if (enabledProtocols != null) { - engine.setEnabledProtocols(enabledProtocols); - } - - SslHandler sslHandler = new SslHandler(engine); - realChannel.getPipeline().addFirst("ssl", sslHandler); - sslHandshakeFuture = sslHandler.handshake(); - } - - // Send the HTTP request. - final HttpRequest req = new DefaultHttpRequest( - HttpVersion.HTTP_1_1, HttpMethod.POST, serverPath); - if (serverName != null) { - req.setHeader(HttpHeaders.Names.HOST, serverName); - } - req.setHeader(HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream"); - req.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); - req.setHeader(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, HttpHeaders.Values.BINARY); - req.setHeader(HttpHeaders.Names.USER_AGENT, HttpTunnelingClientSocketChannel.class.getName()); - - if (sslHandshakeFuture == null) { - realChannel.write(req); - requestHeaderWritten = true; - future.setSuccess(); - fireChannelConnected(virtualChannel, remoteAddress); - } else { - sslHandshakeFuture.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - realChannel.write(req); - requestHeaderWritten = true; - future.setSuccess(); - fireChannelConnected(virtualChannel, remoteAddress); - } else { - future.setFailure(f.getCause()); - fireExceptionCaught(virtualChannel, f.getCause()); - } - } - }); - } - } else { - future.setFailure(f.getCause()); - fireExceptionCaught(virtualChannel, f.getCause()); - } - } - }); - } - - void writeReal(final ChannelBuffer a, final ChannelFuture future) { - if (!requestHeaderWritten) { - throw new NotYetConnectedException(); - } - - final int size = a.readableBytes(); - final ChannelFuture f; - - if (size == 0) { - f = realChannel.write(ChannelBuffers.EMPTY_BUFFER); - } else { - f = realChannel.write(new DefaultHttpChunk(a)); - } - - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - future.setSuccess(); - if (size != 0) { - fireWriteComplete(HttpTunnelingClientSocketChannel.this, size); - } - } else { - future.setFailure(f.getCause()); - } - } - }); - } - - private ChannelFuture writeLastChunk() { - if (!requestHeaderWritten) { - throw new NotYetConnectedException(); - } else { - return realChannel.write(HttpChunk.LAST_CHUNK); - } - } - - void setInterestOpsReal(final int interestOps, final ChannelFuture future) { - realChannel.setInterestOps(interestOps).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - future.setSuccess(); - } else { - future.setFailure(f.getCause()); - } - } - }); - } - - void disconnectReal(final ChannelFuture future) { - writeLastChunk().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - realChannel.disconnect().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - future.setSuccess(); - } else { - future.setFailure(f.getCause()); - } - } - }); - } - }); - } - - void unbindReal(final ChannelFuture future) { - writeLastChunk().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - realChannel.unbind().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - if (f.isSuccess()) { - future.setSuccess(); - } else { - future.setFailure(f.getCause()); - } - } - }); - } - }); - } - - void closeReal(final ChannelFuture future) { - writeLastChunk().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - realChannel.close().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) { - // Note: If 'future' refers to the closeFuture, - // setSuccess() and setFailure() do nothing. - // AbstractChannel.setClosed() should be called instead. - // (See AbstractChannel.ChannelCloseFuture) - - if (f.isSuccess()) { - future.setSuccess(); - } else { - future.setFailure(f.getCause()); - } - - // Notify the closeFuture. - setClosed(); - } - }); - } - }); - } - - final class ServletChannelHandler extends SimpleChannelUpstreamHandler { - - private volatile boolean readingChunks; - final SocketChannel virtualChannel = HttpTunnelingClientSocketChannel.this; - - @Override - public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e) - throws Exception { - fireChannelBound(virtualChannel, (SocketAddress) e.getValue()); - } - - @Override - public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { - if (!readingChunks) { - HttpResponse res = (HttpResponse) e.getMessage(); - if (res.getStatus().getCode() != HttpResponseStatus.OK.getCode()) { - throw new ChannelException("Unexpected HTTP response status: " + res.getStatus()); - } - - if (res.isChunked()) { - readingChunks = true; - } else { - ChannelBuffer content = res.getContent(); - if (content.readable()) { - fireMessageReceived(HttpTunnelingClientSocketChannel.this, content); - } - // Reached to the end of response - close the request. - closeReal(succeededFuture(virtualChannel)); - } - } else { - HttpChunk chunk = (HttpChunk) e.getMessage(); - if (!chunk.isLast()) { - fireMessageReceived(HttpTunnelingClientSocketChannel.this, chunk.getContent()); - } else { - readingChunks = false; - // Reached to the end of response - close the request. - closeReal(succeededFuture(virtualChannel)); - } - } - } - - @Override - public void channelInterestChanged(ChannelHandlerContext ctx, - ChannelStateEvent e) throws Exception { - fireChannelInterestChanged(virtualChannel); - } - - @Override - public void channelDisconnected(ChannelHandlerContext ctx, - ChannelStateEvent e) throws Exception { - fireChannelDisconnected(virtualChannel); - } - - @Override - public void channelUnbound(ChannelHandlerContext ctx, - ChannelStateEvent e) throws Exception { - fireChannelUnbound(virtualChannel); - } - - @Override - public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) - throws Exception { - fireChannelClosed(virtualChannel); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { - fireExceptionCaught(virtualChannel, e.getCause()); - realChannel.close(); - } - } -} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannelFactory.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannelFactory.java deleted file mode 100644 index a82a5db1748..00000000000 --- a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketChannelFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.channel.socket.http; - -import org.jboss.netty.channel.ChannelPipeline; -import org.jboss.netty.channel.ChannelSink; -import org.jboss.netty.channel.socket.ClientSocketChannelFactory; -import org.jboss.netty.channel.socket.SocketChannel; - -/** - * Creates a client-side {@link SocketChannel} which connects to an - * {@link HttpTunnelingServlet} to communicate with the server application - * behind the {@link HttpTunnelingServlet}. Please refer to the - * package summary for - * the detailed usage. - * - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @author Trustin Lee - * @version $Rev$, $Date$ - * - * @apiviz.landmark - */ -public class HttpTunnelingClientSocketChannelFactory implements ClientSocketChannelFactory { - - private final ChannelSink sink = new HttpTunnelingClientSocketPipelineSink(); - private final ClientSocketChannelFactory clientSocketChannelFactory; - - /** - * Creates a new instance. - */ - public HttpTunnelingClientSocketChannelFactory(ClientSocketChannelFactory clientSocketChannelFactory) { - this.clientSocketChannelFactory = clientSocketChannelFactory; - } - - @Override - public SocketChannel newChannel(ChannelPipeline pipeline) { - return new HttpTunnelingClientSocketChannel(this, pipeline, sink, clientSocketChannelFactory); - } - - @Override - public void releaseExternalResources() { - clientSocketChannelFactory.releaseExternalResources(); - } -} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketPipelineSink.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketPipelineSink.java deleted file mode 100644 index f4774d5bebe..00000000000 --- a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingClientSocketPipelineSink.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.channel.socket.http; - -import java.net.SocketAddress; - -import org.jboss.netty.buffer.ChannelBuffer; -import org.jboss.netty.channel.AbstractChannelSink; -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.MessageEvent; - -/** - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @author Trustin Lee - * @version $Rev$, $Date$ - */ -final class HttpTunnelingClientSocketPipelineSink extends AbstractChannelSink { - - HttpTunnelingClientSocketPipelineSink() { - super(); - } - - @Override - public void eventSunk( - ChannelPipeline pipeline, ChannelEvent e) throws Exception { - HttpTunnelingClientSocketChannel channel = (HttpTunnelingClientSocketChannel) e.getChannel(); - ChannelFuture future = e.getFuture(); - if (e instanceof ChannelStateEvent) { - ChannelStateEvent stateEvent = (ChannelStateEvent) e; - ChannelState state = stateEvent.getState(); - Object value = stateEvent.getValue(); - switch (state) { - case OPEN: - if (Boolean.FALSE.equals(value)) { - channel.closeReal(future); - } - break; - case BOUND: - if (value != null) { - channel.bindReal((SocketAddress) value, future); - } else { - channel.unbindReal(future); - } - break; - case CONNECTED: - if (value != null) { - channel.connectReal((SocketAddress) value, future); - } else { - channel.closeReal(future); - } - break; - case INTEREST_OPS: - channel.setInterestOpsReal(((Integer) value).intValue(), future); - break; - } - } else if (e instanceof MessageEvent) { - channel.writeReal(((ChannelBuffer) ((MessageEvent) e).getMessage()), future); - } - } -} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingServlet.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingServlet.java deleted file mode 100644 index 8e93c6f1e2f..00000000000 --- a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingServlet.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.channel.socket.http; - -import java.io.EOFException; -import java.io.IOException; -import java.io.PushbackInputStream; -import java.net.SocketAddress; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.jboss.netty.buffer.ChannelBuffer; -import org.jboss.netty.buffer.ChannelBuffers; -import org.jboss.netty.channel.Channel; -import org.jboss.netty.channel.ChannelFactory; -import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelFutureListener; -import org.jboss.netty.channel.ChannelHandlerContext; -import org.jboss.netty.channel.ChannelPipeline; -import org.jboss.netty.channel.Channels; -import org.jboss.netty.channel.ExceptionEvent; -import org.jboss.netty.channel.MessageEvent; -import org.jboss.netty.channel.SimpleChannelUpstreamHandler; -import org.jboss.netty.channel.local.DefaultLocalClientChannelFactory; -import org.jboss.netty.channel.local.LocalAddress; -import org.jboss.netty.handler.codec.http.HttpHeaders; -import org.jboss.netty.logging.InternalLogger; -import org.jboss.netty.logging.InternalLoggerFactory; - -/** - * An {@link HttpServlet} that proxies an incoming data to the actual server - * and vice versa. Please refer to the - * package summary for - * the detailed usage. - * - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @author Trustin Lee - * @version $Rev$, $Date$ - * - * @apiviz.landmark - */ -public class HttpTunnelingServlet extends HttpServlet { - - private static final long serialVersionUID = 4259910275899756070L; - - private static final String ENDPOINT = "endpoint"; - - static final InternalLogger logger = InternalLoggerFactory.getInstance(HttpTunnelingServlet.class); - - private volatile SocketAddress remoteAddress; - private volatile ChannelFactory channelFactory; - - @Override - public void init() throws ServletException { - ServletConfig config = getServletConfig(); - String endpoint = config.getInitParameter(ENDPOINT); - if (endpoint == null) { - throw new ServletException("init-param '" + ENDPOINT + "' must be specified."); - } - - try { - remoteAddress = parseEndpoint(endpoint.trim()); - } catch (ServletException e) { - throw e; - } catch (Exception e) { - throw new ServletException("Failed to parse an endpoint.", e); - } - - try { - channelFactory = createChannelFactory(remoteAddress); - } catch (ServletException e) { - throw e; - } catch (Exception e) { - throw new ServletException("Failed to create a channel factory.", e); - } - - // Stuff for testing purpose - //ServerBootstrap b = new ServerBootstrap(new DefaultLocalServerChannelFactory()); - //b.getPipeline().addLast("logger", new LoggingHandler(getClass(), InternalLogLevel.INFO, true)); - //b.getPipeline().addLast("handler", new EchoHandler()); - //b.bind(remoteAddress); - } - - protected SocketAddress parseEndpoint(String endpoint) throws Exception { - if (endpoint.startsWith("local:")) { - return new LocalAddress(endpoint.substring(6).trim()); - } else { - throw new ServletException( - "Invalid or unknown endpoint: " + endpoint); - } - } - - protected ChannelFactory createChannelFactory(SocketAddress remoteAddress) throws Exception { - if (remoteAddress instanceof LocalAddress) { - return new DefaultLocalClientChannelFactory(); - } else { - throw new ServletException( - "Unsupported remote address type: " + - remoteAddress.getClass().getName()); - } - } - - @Override - public void destroy() { - try { - destroyChannelFactory(channelFactory); - } catch (Exception e) { - logger.warn("Failed to destroy a channel factory.", e); - } - } - - protected void destroyChannelFactory(ChannelFactory factory) throws Exception { - factory.releaseExternalResources(); - } - - @Override - protected void service(HttpServletRequest req, HttpServletResponse res) - throws ServletException, IOException { - if (!"POST".equalsIgnoreCase(req.getMethod())) { - logger.warn("Unallowed method: " + req.getMethod()); - res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - - final ChannelPipeline pipeline = Channels.pipeline(); - final ServletOutputStream out = res.getOutputStream(); - final OutboundConnectionHandler handler = new OutboundConnectionHandler(out); - pipeline.addLast("handler", handler); - - Channel channel = channelFactory.newChannel(pipeline); - ChannelFuture future = channel.connect(remoteAddress).awaitUninterruptibly(); - if (!future.isSuccess()) { - Throwable cause = future.getCause(); - logger.warn("Endpoint unavailable: " + cause.getMessage(), cause); - res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return; - } - - ChannelFuture lastWriteFuture = null; - try { - res.setStatus(HttpServletResponse.SC_OK); - res.setHeader(HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream"); - res.setHeader(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, HttpHeaders.Values.BINARY); - - // Initiate chunked encoding by flushing the headers. - out.flush(); - - PushbackInputStream in = - new PushbackInputStream(req.getInputStream()); - while (channel.isConnected()) { - ChannelBuffer buffer; - try { - buffer = read(in); - } catch (EOFException e) { - break; - } - if (buffer == null) { - break; - } - lastWriteFuture = channel.write(buffer); - } - } finally { - if (lastWriteFuture == null) { - channel.close(); - } else { - lastWriteFuture.addListener(ChannelFutureListener.CLOSE); - } - } - } - - private static ChannelBuffer read(PushbackInputStream in) throws IOException { - byte[] buf; - int readBytes; - - int bytesToRead = in.available(); - if (bytesToRead > 0) { - buf = new byte[bytesToRead]; - readBytes = in.read(buf); - } else if (bytesToRead == 0) { - int b = in.read(); - if (b < 0 || in.available() < 0) { - return null; - } - in.unread(b); - bytesToRead = in.available(); - buf = new byte[bytesToRead]; - readBytes = in.read(buf); - } else { - return null; - } - - assert readBytes > 0; - - ChannelBuffer buffer; - if (readBytes == buf.length) { - buffer = ChannelBuffers.wrappedBuffer(buf); - } else { - // A rare case, but it sometimes happen. - buffer = ChannelBuffers.wrappedBuffer(buf, 0, readBytes); - } - return buffer; - } - - private static final class OutboundConnectionHandler extends SimpleChannelUpstreamHandler { - - private final ServletOutputStream out; - - public OutboundConnectionHandler(ServletOutputStream out) { - this.out = out; - } - - @Override - public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { - ChannelBuffer buffer = (ChannelBuffer) e.getMessage(); - synchronized (this) { - buffer.readBytes(out, buffer.readableBytes()); - out.flush(); - } - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { - logger.warn("Unexpected exception while HTTP tunneling", e.getCause()); - e.getChannel().close(); - } - } -} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingSocketChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingSocketChannelConfig.java deleted file mode 100644 index 8c688ed85bc..00000000000 --- a/src/main/java/org/jboss/netty/channel/socket/http/HttpTunnelingSocketChannelConfig.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.channel.socket.http; - -import java.util.Map; -import java.util.Map.Entry; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; - -import org.jboss.netty.buffer.ChannelBufferFactory; -import org.jboss.netty.channel.ChannelConfig; -import org.jboss.netty.channel.ChannelPipelineFactory; -import org.jboss.netty.channel.socket.SocketChannel; -import org.jboss.netty.channel.socket.SocketChannelConfig; -import org.jboss.netty.util.internal.ConversionUtil; - -/** - * The {@link ChannelConfig} of a client-side HTTP tunneling - * {@link SocketChannel}. A {@link SocketChannel} created by - * {@link HttpTunnelingClientSocketChannelFactory} will return an instance of - * this configuration type for {@link SocketChannel#getConfig()}. - * - *

Available options

- * - * In addition to the options provided by {@link SocketChannelConfig}, - * {@link HttpTunnelingSocketChannelConfig} allows the following options in - * the option map: - * - * - * - * - * - * - * - * - * - * - * - * - * - *
NameAssociated setter method
{@code "sslContext"}{@link #setSslContext(SSLContext)}
{@code "enabledSslCiperSuites"}{@link #setEnabledSslCipherSuites(String[])}
{@code "enabledSslProtocols"}{@link #setEnabledSslProtocols(String[])}
{@code "enableSslSessionCreation"}{@link #setEnableSslSessionCreation(boolean)}
- * - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @author Trustin Lee - * @version $Rev$, $Date$ - * - * @apiviz.landmark - */ -public final class HttpTunnelingSocketChannelConfig implements SocketChannelConfig { - - private final HttpTunnelingClientSocketChannel channel; - private volatile String serverName; - private volatile String serverPath = "/netty-tunnel"; - private volatile SSLContext sslContext; - private volatile String[] enabledSslCipherSuites; - private volatile String[] enabledSslProtocols; - private volatile boolean enableSslSessionCreation = true; - - /** - * Creates a new instance. - */ - HttpTunnelingSocketChannelConfig(HttpTunnelingClientSocketChannel channel) { - this.channel = channel; - } - - /** - * Returns the host name of the HTTP server. If {@code null}, the - * {@code "Host"} header is not sent by the HTTP tunneling client. - */ - public String getServerName() { - return serverName; - } - - /** - * Sets the host name of the HTTP server. If {@code null}, the - * {@code "Host"} header is not sent by the HTTP tunneling client. - */ - public void setServerName(String serverName) { - this.serverName = serverName; - } - - /** - * Returns the path where the {@link HttpTunnelingServlet} is mapped to. - * The default value is {@code "/netty-tunnel"}. - */ - public String getServerPath() { - return serverPath; - } - - /** - * Sets the path where the {@link HttpTunnelingServlet} is mapped to. - * The default value is {@code "/netty-tunnel"}. - */ - public void setServerPath(String serverPath) { - if (serverPath == null) { - throw new NullPointerException("serverPath"); - } - this.serverPath = serverPath; - } - - /** - * Returns the {@link SSLContext} which is used to establish an HTTPS - * connection. If {@code null}, a plain-text HTTP connection is established. - */ - public SSLContext getSslContext() { - return sslContext; - } - - /** - * Sets the {@link SSLContext} which is used to establish an HTTPS connection. - * If {@code null}, a plain-text HTTP connection is established. - */ - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } - - /** - * Returns the cipher suites enabled for use on an {@link SSLEngine}. - * If {@code null}, the default value will be used. - * - * @see SSLEngine#getEnabledCipherSuites() - */ - public String[] getEnabledSslCipherSuites() { - String[] suites = enabledSslCipherSuites; - if (suites == null) { - return null; - } else { - return suites.clone(); - } - } - - /** - * Sets the cipher suites enabled for use on an {@link SSLEngine}. - * If {@code null}, the default value will be used. - * - * @see SSLEngine#setEnabledCipherSuites(String[]) - */ - public void setEnabledSslCipherSuites(String[] suites) { - if (suites == null) { - enabledSslCipherSuites = null; - } else { - enabledSslCipherSuites = suites.clone(); - } - } - - /** - * Returns the protocol versions enabled for use on an {@link SSLEngine}. - * - * @see SSLEngine#getEnabledProtocols() - */ - public String[] getEnabledSslProtocols() { - String[] protocols = enabledSslProtocols; - if (protocols == null) { - return null; - } else { - return protocols.clone(); - } - } - - /** - * Sets the protocol versions enabled for use on an {@link SSLEngine}. - * - * @see SSLEngine#setEnabledProtocols(String[]) - */ - public void setEnabledSslProtocols(String[] protocols) { - if (protocols == null) { - enabledSslProtocols = null; - } else { - enabledSslProtocols = protocols.clone(); - } - } - - /** - * Returns {@code true} if new {@link SSLSession}s may be established by - * an {@link SSLEngine}. - * - * @see SSLEngine#getEnableSessionCreation() - */ - public boolean isEnableSslSessionCreation() { - return enableSslSessionCreation; - } - - /** - * Sets whether new {@link SSLSession}s may be established by an - * {@link SSLEngine}. - * - * @see SSLEngine#setEnableSessionCreation(boolean) - */ - public void setEnableSslSessionCreation(boolean flag) { - enableSslSessionCreation = flag; - } - - @Override - public void setOptions(Map options) { - for (Entry e: options.entrySet()) { - setOption(e.getKey(), e.getValue()); - } - } - - @Override - public boolean setOption(String key, Object value) { - if (channel.realChannel.getConfig().setOption(key, value)) { - return true; - } - - if (key.equals("serverName")){ - setServerName(String.valueOf(value)); - } else if (key.equals("serverPath")){ - setServerPath(String.valueOf(value)); - } else if (key.equals("sslContext")) { - setSslContext((SSLContext) value); - } else if (key.equals("enabledSslCipherSuites")){ - setEnabledSslCipherSuites(ConversionUtil.toStringArray(value)); - } else if (key.equals("enabledSslProtocols")){ - setEnabledSslProtocols(ConversionUtil.toStringArray(value)); - } else if (key.equals("enableSslSessionCreation")){ - setEnableSslSessionCreation(ConversionUtil.toBoolean(value)); - } else { - return false; - } - - return true; - } - - @Override - public int getReceiveBufferSize() { - return channel.realChannel.getConfig().getReceiveBufferSize(); - } - - @Override - public int getSendBufferSize() { - return channel.realChannel.getConfig().getSendBufferSize(); - } - - @Override - public int getSoLinger() { - return channel.realChannel.getConfig().getSoLinger(); - } - - @Override - public int getTrafficClass() { - return channel.realChannel.getConfig().getTrafficClass(); - } - - @Override - public boolean isKeepAlive() { - return channel.realChannel.getConfig().isKeepAlive(); - } - - @Override - public boolean isReuseAddress() { - return channel.realChannel.getConfig().isReuseAddress(); - } - - @Override - public boolean isTcpNoDelay() { - return channel.realChannel.getConfig().isTcpNoDelay(); - } - - @Override - public void setKeepAlive(boolean keepAlive) { - channel.realChannel.getConfig().setKeepAlive(keepAlive); - } - - @Override - public void setPerformancePreferences( - int connectionTime, int latency, int bandwidth) { - channel.realChannel.getConfig().setPerformancePreferences(connectionTime, latency, bandwidth); - } - - @Override - public void setReceiveBufferSize(int receiveBufferSize) { - channel.realChannel.getConfig().setReceiveBufferSize(receiveBufferSize); - } - - @Override - public void setReuseAddress(boolean reuseAddress) { - channel.realChannel.getConfig().setReuseAddress(reuseAddress); - } - - @Override - public void setSendBufferSize(int sendBufferSize) { - channel.realChannel.getConfig().setSendBufferSize(sendBufferSize); - - } - - @Override - public void setSoLinger(int soLinger) { - channel.realChannel.getConfig().setSoLinger(soLinger); - } - - @Override - public void setTcpNoDelay(boolean tcpNoDelay) { - channel.realChannel.getConfig().setTcpNoDelay(tcpNoDelay); - } - - @Override - public void setTrafficClass(int trafficClass) { - channel.realChannel.getConfig().setTrafficClass(trafficClass); - } - - @Override - public ChannelBufferFactory getBufferFactory() { - return channel.realChannel.getConfig().getBufferFactory(); - } - - @Override - public int getConnectTimeoutMillis() { - return channel.realChannel.getConfig().getConnectTimeoutMillis(); - } - - @Override - public ChannelPipelineFactory getPipelineFactory() { - return channel.realChannel.getConfig().getPipelineFactory(); - } - - @Override - public void setBufferFactory(ChannelBufferFactory bufferFactory) { - channel.realChannel.getConfig().setBufferFactory(bufferFactory); - } - - @Override - public void setConnectTimeoutMillis(int connectTimeoutMillis) { - channel.realChannel.getConfig().setConnectTimeoutMillis(connectTimeoutMillis); - } - - @Override - public void setPipelineFactory(ChannelPipelineFactory pipelineFactory) { - channel.realChannel.getConfig().setPipelineFactory(pipelineFactory); - } -} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/SaturationManager.java b/src/main/java/org/jboss/netty/channel/socket/http/SaturationManager.java new file mode 100644 index 00000000000..a9be1df55c8 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/SaturationManager.java @@ -0,0 +1,69 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import static org.jboss.netty.channel.socket.http.SaturationStateChange.DESATURATED; +import static org.jboss.netty.channel.socket.http.SaturationStateChange.NO_CHANGE; +import static org.jboss.netty.channel.socket.http.SaturationStateChange.SATURATED; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used to monitor the amount of data that has yet to be pushed to + * the underlying socket, in order to implement the "high/low water mark" facility + * that controls Channel.isWritable() and the interest ops of http tunnels. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class SaturationManager { + private AtomicLong desaturationPoint; + + private AtomicLong saturationPoint; + + private final AtomicLong queueSize; + + private final AtomicBoolean saturated; + + public SaturationManager(long desaturationPoint, long saturationPoint) { + this.desaturationPoint = new AtomicLong(desaturationPoint); + this.saturationPoint = new AtomicLong(saturationPoint); + queueSize = new AtomicLong(0); + saturated = new AtomicBoolean(false); + } + + public SaturationStateChange queueSizeChanged(long sizeDelta) { + long newQueueSize = queueSize.addAndGet(sizeDelta); + if (newQueueSize <= desaturationPoint.get()) { + if (saturated.compareAndSet(true, false)) { + return DESATURATED; + } + } else if (newQueueSize > saturationPoint.get()) { + if (saturated.compareAndSet(false, true)) { + return SATURATED; + } + } + + return NO_CHANGE; + } + + public void updateThresholds(long desaturationPoint, long saturationPoint) { + this.desaturationPoint.set(desaturationPoint); + this.saturationPoint.set(saturationPoint); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/SaturationStateChange.java b/src/main/java/org/jboss/netty/channel/socket/http/SaturationStateChange.java new file mode 100644 index 00000000000..bef8741d6cd --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/SaturationStateChange.java @@ -0,0 +1,29 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +/** + * Represents the state change of a chanel in response in the amount of pending data to be + * sent - either no change occurs, the channel becomes desaturated (indicating that writing + * can safely commence) or it becomes saturated (indicating that writing should cease). + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +enum SaturationStateChange { + NO_CHANGE, DESATURATED, SATURATED +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitch.java b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitch.java new file mode 100644 index 00000000000..36fa17e2f6a --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitch.java @@ -0,0 +1,257 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * This is the gateway between the accepted TCP channels that are used to communicate with the client + * ends of the http tunnel and the virtual server accepted tunnel. As a tunnel can last for longer than + * the lifetime of the client channels that are used to service it, this layer of abstraction is + * necessary. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class ServerMessageSwitch implements ServerMessageSwitchUpstreamInterface, + ServerMessageSwitchDownstreamInterface { + + private static final InternalLogger LOG = InternalLoggerFactory + .getInstance(ServerMessageSwitch.class.getName()); + + private final String tunnelIdPrefix; + + private final HttpTunnelAcceptedChannelFactory newChannelFactory; + + private final ConcurrentHashMap tunnelsById; + + public ServerMessageSwitch( + HttpTunnelAcceptedChannelFactory newChannelFactory) { + this.newChannelFactory = newChannelFactory; + tunnelIdPrefix = Long.toHexString(new Random().nextLong()); + tunnelsById = new ConcurrentHashMap(); + } + + public String createTunnel(InetSocketAddress remoteAddress) { + String newTunnelId = + String.format("%s_%s", tunnelIdPrefix, + newChannelFactory.generateTunnelId()); + TunnelInfo newTunnel = new TunnelInfo(); + newTunnel.tunnelId = newTunnelId; + tunnelsById.put(newTunnelId, newTunnel); + newTunnel.localChannel = + newChannelFactory.newChannel(newTunnelId, remoteAddress); + return newTunnelId; + } + + public boolean isOpenTunnel(String tunnelId) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + return tunnel != null; + } + + public void pollOutboundData(String tunnelId, Channel channel) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) { + if (LOG.isWarnEnabled()) { + LOG.warn("Poll request for tunnel " + tunnelId + + " which does not exist or already closed"); + } + respondAndClose(channel, HttpTunnelMessageUtils.createRejection( + null, "Unknown tunnel, possibly already closed")); + return; + } + + if (!tunnel.responseChannel.compareAndSet(null, channel)) { + if (LOG.isWarnEnabled()) { + LOG.warn("Duplicate poll request detected for tunnel " + + tunnelId); + } + respondAndClose(channel, HttpTunnelMessageUtils.createRejection( + null, "Only one poll request at a time per tunnel allowed")); + return; + } + + sendQueuedData(tunnel); + } + + private void respondAndClose(Channel channel, HttpResponse response) { + Channels.write(channel, response).addListener( + ChannelFutureListener.CLOSE); + } + + private void sendQueuedData(TunnelInfo state) { + Queue queuedData = state.queuedResponses; + Channel responseChannel = state.responseChannel.getAndSet(null); + if (responseChannel == null) { + // no response channel, or another thread has already used it + return; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("sending response for tunnel id " + state.tunnelId + + " to " + responseChannel.getRemoteAddress()); + } + QueuedResponse messageToSend = queuedData.poll(); + if (messageToSend == null) { + // no data to send, restore the response channel and bail out + state.responseChannel.set(responseChannel); + return; + } + + HttpResponse response = + HttpTunnelMessageUtils + .createRecvDataResponse(messageToSend.data); + final ChannelFuture originalFuture = messageToSend.writeFuture; + Channels.write(responseChannel, response).addListener( + new RelayedChannelFutureListener(originalFuture)); + } + + public TunnelStatus routeInboundData(String tunnelId, + ChannelBuffer inboundData) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) { + return TunnelStatus.CLOSED; + } + + if (tunnel.closing.get()) { + // client has now been notified, forget the tunnel + tunnelsById.remove(tunnel); + return TunnelStatus.CLOSED; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("routing inbound data for tunnel " + tunnelId); + } + tunnel.localChannel.dataReceived(inboundData); + return TunnelStatus.ALIVE; + } + + public void clientCloseTunnel(String tunnelId) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + tunnel.localChannel.clientClosed(); + tunnelsById.remove(tunnelId); + } + + public void serverCloseTunnel(String tunnelId) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + tunnel.closing.set(true); + + Channel responseChannel = tunnel.responseChannel.getAndSet(null); + if (responseChannel == null) { + // response channel is already in use, client will be notified + // of close at next opportunity + return; + } + + respondAndClose(responseChannel, + HttpTunnelMessageUtils.createTunnelCloseResponse()); + // client has been notified, forget the tunnel + tunnelsById.remove(tunnelId); + } + + public void routeOutboundData(String tunnelId, ChannelBuffer data, + ChannelFuture writeFuture) { + TunnelInfo tunnel = tunnelsById.get(tunnelId); + if (tunnel == null) { + // tunnel is closed + if (LOG.isWarnEnabled()) { + LOG.warn("attempt made to send data out on tunnel id " + + tunnelId + " which is unknown or closed"); + } + return; + } + + ChannelFutureAggregator aggregator = + new ChannelFutureAggregator(writeFuture); + List fragments = + WriteSplitter.split(data, HttpTunnelMessageUtils.MAX_BODY_SIZE); + + if (LOG.isDebugEnabled()) { + LOG.debug("routing outbound data for tunnel " + tunnelId); + } + for (ChannelBuffer fragment: fragments) { + ChannelFuture fragmentFuture = + Channels.future(writeFuture.getChannel()); + aggregator.addFuture(fragmentFuture); + tunnel.queuedResponses.offer(new QueuedResponse(fragment, + fragmentFuture)); + } + + sendQueuedData(tunnel); + } + + /** + * Used to pass the result received from one ChannelFutureListener to another verbatim. + */ + private final class RelayedChannelFutureListener implements + ChannelFutureListener { + private final ChannelFuture originalFuture; + + private RelayedChannelFutureListener(ChannelFuture originalFuture) { + this.originalFuture = originalFuture; + } + + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + originalFuture.setSuccess(); + } else { + originalFuture.setFailure(future.getCause()); + } + } + } + + private static final class TunnelInfo { + public String tunnelId; + + public HttpTunnelAcceptedChannelReceiver localChannel; + + public final AtomicReference responseChannel = + new AtomicReference(null); + + public final Queue queuedResponses = + new ConcurrentLinkedQueue(); + + public final AtomicBoolean closing = new AtomicBoolean(false); + } + + private static final class QueuedResponse { + public ChannelBuffer data; + + public ChannelFuture writeFuture; + + QueuedResponse(ChannelBuffer data, ChannelFuture writeFuture) { + this.data = data; + this.writeFuture = writeFuture; + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java new file mode 100644 index 00000000000..cc29a3c2868 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchDownstreamInterface.java @@ -0,0 +1,36 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelFuture; + +/** + * The interface from a HttpTunnelAcceptedChannel to the ServerMessageSwitch. + * This primarily exists for mock object testing purposes. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface ServerMessageSwitchDownstreamInterface { + + public void serverCloseTunnel(String tunnelId); + + public void routeOutboundData(String tunnelId, ChannelBuffer data, + ChannelFuture writeFuture); + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java new file mode 100644 index 00000000000..436f68ecf06 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/ServerMessageSwitchUpstreamInterface.java @@ -0,0 +1,57 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.InetSocketAddress; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; + +/** + * The interface from a TCP channel which is being used to communicate with the client + * end of an http tunnel and the server message switch. + * + * This primarily exists for mock testing purposes. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +interface ServerMessageSwitchUpstreamInterface { + + public String createTunnel(InetSocketAddress remoteAddress); + + public boolean isOpenTunnel(String tunnelId); + + public void clientCloseTunnel(String tunnelId); + + /** + * Passes some received data from a client for forwarding to the server's view + * of the tunnel. + * @return the current status of the tunnel. ALIVE indicates the tunnel is still + * functional, CLOSED indicates it is closed and the client should be notified + * of this (and will be forgotten after this notification). + */ + public TunnelStatus routeInboundData(String tunnelId, + ChannelBuffer inboundData); + + public void pollOutboundData(String tunnelId, Channel responseChannel); + + public static enum TunnelStatus { + ALIVE, CLOSED + } + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java b/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java new file mode 100644 index 00000000000..9eafdf5cceb --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/TunnelIdGenerator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.jboss.netty.channel.socket.http; + +/** + * This interface is used by the server end of an http tunnel to generate new + * tunnel ids for accepted client connections. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public interface TunnelIdGenerator { + + /** + * Generates the next tunnel ID to be used, which must be unique + * (i.e. ensure with high probability that it will not clash with + * an existing tunnel ID). This method must be thread safe, and + * preferably lock free. + */ + public String generateId(); + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java b/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java new file mode 100644 index 00000000000..37943a7e4a6 --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/TunnelWrappedServerChannelHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.net.SocketAddress; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.ChildChannelStateEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.channel.group.ChannelGroup; + +/** + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +class TunnelWrappedServerChannelHandler extends SimpleChannelUpstreamHandler { + + public static final String NAME = "TunnelWrappedServerChannelHandler"; + + private final HttpTunnelServerChannel tunnelChannel; + + private final AcceptedServerChannelPipelineFactory pipelineFactory; + + private final ChannelGroup allChannels; + + public TunnelWrappedServerChannelHandler( + HttpTunnelServerChannel tunnelChannel, + AcceptedServerChannelPipelineFactory pipelineFactory, + ChannelGroup allChannels) { + this.tunnelChannel = tunnelChannel; + this.pipelineFactory = pipelineFactory; + this.allChannels = allChannels; + } + + @Override + public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + e.getChannel().getConfig().setPipelineFactory(pipelineFactory); + super.channelOpen(ctx, e); + } + + @Override + public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + Channels.fireChannelBound(tunnelChannel, (SocketAddress) e.getValue()); + super.channelBound(ctx, e); + } + + @Override + public void channelUnbound(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + Channels.fireChannelUnbound(tunnelChannel); + super.channelUnbound(ctx, e); + } + + @Override + public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + Channels.fireChannelClosed(tunnelChannel); + super.channelClosed(ctx, e); + } + + @Override + public void childChannelOpen(ChannelHandlerContext ctx, + ChildChannelStateEvent e) throws Exception { + allChannels.add(e.getChildChannel()); + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java b/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java new file mode 100644 index 00000000000..94380e96cba --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/WriteFragmenter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.util.List; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; + +/** + * Downstream handler which places an upper bound on the size of written + * {@link ChannelBuffer ChannelBuffers}. If a buffer + * is bigger than the specified upper bound, the buffer is broken up + * into two or more smaller pieces. + *

+ * This is utilised by the http tunnel to smooth out the per-byte latency, + * by placing an upper bound on HTTP request / response body sizes. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public class WriteFragmenter extends SimpleChannelDownstreamHandler { + + public static final String NAME = "writeFragmenter"; + + private int splitThreshold; + + public WriteFragmenter(int splitThreshold) { + this.splitThreshold = splitThreshold; + } + + public void setSplitThreshold(int splitThreshold) { + this.splitThreshold = splitThreshold; + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + ChannelBuffer data = (ChannelBuffer) e.getMessage(); + + if (data.readableBytes() <= splitThreshold) { + super.writeRequested(ctx, e); + } else { + List fragments = + WriteSplitter.split(data, splitThreshold); + ChannelFutureAggregator aggregator = + new ChannelFutureAggregator(e.getFuture()); + for (ChannelBuffer fragment: fragments) { + ChannelFuture fragmentFuture = + Channels.future(ctx.getChannel(), true); + aggregator.addFuture(fragmentFuture); + Channels.write(ctx, fragmentFuture, fragment); + } + } + } +} diff --git a/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java b/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java new file mode 100644 index 00000000000..37a9f772e5b --- /dev/null +++ b/src/main/java/org/jboss/netty/channel/socket/http/WriteSplitter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.channel.socket.http; + +import java.util.ArrayList; +import java.util.List; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Provides functionality to split a provided ChannelBuffer into multiple fragments which fit + * under a specified size threshold. + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Iain McGinniss (iain.mcginniss@onedrum.com) + * @author OneDrum Ltd. + */ +public final class WriteSplitter { + + public static List split(ChannelBuffer buffer, + int splitThreshold) { + int listSize = (int) ((float) buffer.readableBytes() / splitThreshold); + ArrayList fragmentList = + new ArrayList(listSize); + + if (buffer.readableBytes() > splitThreshold) { + int slicePosition = buffer.readerIndex(); + while (slicePosition < buffer.writerIndex()) { + int chunkSize = + Math.min(splitThreshold, buffer.writerIndex() - + slicePosition); + ChannelBuffer chunk = buffer.slice(slicePosition, chunkSize); + fragmentList.add(chunk); + slicePosition += chunkSize; + } + } else { + fragmentList.add(ChannelBuffers.wrappedBuffer(buffer)); + } + + return fragmentList; + } + +} diff --git a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java index abf7142ae34..35ffccc8ed4 100644 --- a/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java +++ b/src/main/java/org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.java @@ -144,5 +144,6 @@ public interface NioSocketChannelConfig extends SocketChannelConfig { * will be called with the new predictor. The default factory is * {@link AdaptiveReceiveBufferSizePredictorFactory}(64, 1024, 65536). */ - void setReceiveBufferSizePredictorFactory(ReceiveBufferSizePredictorFactory predictorFactory); + void setReceiveBufferSizePredictorFactory( + ReceiveBufferSizePredictorFactory predictorFactory); } diff --git a/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java b/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java deleted file mode 100644 index 0561a92dc6b..00000000000 --- a/src/main/java/org/jboss/netty/example/http/tunnel/HttpTunnelingClientExample.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.example.http.tunnel; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.concurrent.Executors; - -import org.jboss.netty.bootstrap.ClientBootstrap; -import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelPipeline; -import org.jboss.netty.channel.ChannelPipelineFactory; -import org.jboss.netty.channel.Channels; -import org.jboss.netty.channel.socket.http.HttpTunnelingClientSocketChannelFactory; -import org.jboss.netty.channel.socket.oio.OioClientSocketChannelFactory; -import org.jboss.netty.example.securechat.SecureChatSslContextFactory; -import org.jboss.netty.handler.codec.string.StringDecoder; -import org.jboss.netty.handler.codec.string.StringEncoder; -import org.jboss.netty.handler.logging.LoggingHandler; -import org.jboss.netty.logging.InternalLogLevel; - -/** - * An HTTP tunneled version of the telnet client example. Please refer to the - * API documentation of the org.jboss.netty.channel.socket.http package - * for the detailed instruction on how to deploy the server-side HTTP tunnel in - * your Servlet container. - * - * @author The Netty Project - * @author Andy Taylor (andy.taylor@jboss.org) - * @version $Rev$, $Date$ - */ -public class HttpTunnelingClientExample { - - public static void main(String[] args) throws Exception { - if (args.length != 1) { - System.err.println( - "Usage: " + HttpTunnelingClientExample.class.getSimpleName() + - " "); - System.err.println( - "Example: " + HttpTunnelingClientExample.class.getSimpleName() + - " http://localhost:8080/netty-tunnel"); - return; - } - - URI uri = new URI(args[0]); - String scheme = uri.getScheme() == null? "http" : uri.getScheme(); - - // Configure the client. - ClientBootstrap b = new ClientBootstrap( - new HttpTunnelingClientSocketChannelFactory( - new OioClientSocketChannelFactory(Executors.newCachedThreadPool()))); - - b.setPipelineFactory(new ChannelPipelineFactory() { - @Override - public ChannelPipeline getPipeline() throws Exception { - return Channels.pipeline( - new StringDecoder(), - new StringEncoder(), - new LoggingHandler(InternalLogLevel.INFO)); - } - }); - - // Set additional options required by the HTTP tunneling transport. - b.setOption("serverName", uri.getHost()); - b.setOption("serverPath", uri.getRawPath()); - - // Configure SSL if necessary - if (scheme.equals("https")) { - b.setOption("sslContext", SecureChatSslContextFactory.getClientContext()); - } else if (!scheme.equals("http")) { - // Only HTTP and HTTPS are supported. - System.err.println("Only HTTP(S) is supported."); - return; - } - - // Make the connection attempt. - ChannelFuture channelFuture = b.connect( - new InetSocketAddress(uri.getHost(), uri.getPort())); - channelFuture.awaitUninterruptibly(); - - // Read commands from the stdin. - System.out.println("Enter text ('quit' to exit)"); - ChannelFuture lastWriteFuture = null; - BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); - for (; ;) { - String line = in.readLine(); - if (line == null || "quit".equalsIgnoreCase(line)) { - break; - } - - // Sends the received line to the server. - lastWriteFuture = channelFuture.getChannel().write(line); - } - - // Wait until all messages are flushed before closing the channel. - if (lastWriteFuture != null) { - lastWriteFuture.awaitUninterruptibly(); - } - - channelFuture.getChannel().close(); - // Wait until the connection is closed or the connection attempt fails. - channelFuture.getChannel().getCloseFuture().awaitUninterruptibly(); - - // Shut down all threads. - b.releaseExternalResources(); - } -} diff --git a/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java b/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java deleted file mode 100644 index cf49ae23883..00000000000 --- a/src/main/java/org/jboss/netty/example/http/tunnel/LocalEchoServerRegistration.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jboss.netty.example.http.tunnel; - -import org.jboss.netty.bootstrap.ServerBootstrap; -import org.jboss.netty.channel.Channel; -import org.jboss.netty.channel.ChannelFactory; -import org.jboss.netty.channel.local.DefaultLocalServerChannelFactory; -import org.jboss.netty.channel.local.LocalAddress; -import org.jboss.netty.example.echo.EchoServerHandler; - -/** - * Deploy this in JBossAS 5 or other IoC container by adding the following bean. - * - *

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