/* * Copyright 2010 Red Hat, Inc. * * Red Hat licenses this file to you under the Apache License, version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package org.texai.network.netty.handler; import java.util.Collection; import org.apache.log4j.Logger; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.handler.codec.frame.FrameDecoder; import org.jboss.netty.handler.codec.http.HttpChunkAggregator; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.ssl.SslHandler; import org.texai.network.netty.NetworkConstants; import org.texai.torrent.support.BitTorrentUtils; import org.texai.util.StringUtils; import org.texai.util.TexaiException; /** Manipulates the current pipeline dynamically to switch protocols that share the single port. Web socket protocol * switching is performed within the HTTP request handler. * * @author reed * */ public class PortUnificationHandler extends FrameDecoder { /** the logger */ private static final Logger LOGGER = Logger.getLogger(PortUnificationHandler.class); // dependency injection of the business logic handlers enables the substitution of stubs for unit testing this class /** the Albus hierarchical control network channel handler */ private AbstractAlbusHCSMessageHandler albusHCNMessageHandler; /** the HTTP request handler */ private AbstractHTTPRequestHandler httpRequestHandler; /** the bit torrent handshake handler */ private AbstractBitTorrentHandler bitTorrentHandler; /** Constructs a new PortUnificationHandler instance. */ public PortUnificationHandler() { } /** Recognizes the protocol and switches the pipeline to handle it. * * @param channelHandlerContext the context of this handler * @param channel the current channel * @param channelBuffer the cumulative buffer of received packets so far. * * @return the channel buffer after reading a possible object serialization protocol identification byte. * {@code null} if there's not enough data in the buffer to recognize the protocol. */ @Override protected Object decode( final ChannelHandlerContext channelHandlerContext, final Channel channel, final ChannelBuffer channelBuffer) { final int readableBytes = channelBuffer.readableBytes(); LOGGER.info("readable bytes: " + readableBytes); // use the first five bytes of the channel buffer to detect the protocol if (readableBytes < 5) { return null; } final int magic1 = channelBuffer.getUnsignedByte(channelBuffer.readerIndex()); final int magic2 = channelBuffer.getUnsignedByte(channelBuffer.readerIndex() + 1); final int magic3 = channelBuffer.getUnsignedByte(channelBuffer.readerIndex() + 2); final int magic4 = channelBuffer.getUnsignedByte(channelBuffer.readerIndex() + 3); final int magic5 = channelBuffer.getUnsignedByte(channelBuffer.readerIndex() + 4); if (isSerializedObject(magic1)) { switchToAlbusHCN(channelHandlerContext); } else if (isHttp(magic1, magic2)) { switchToHttp(channelHandlerContext); } else if (BitTorrentUtils.isBitTorrentHandshake(magic1) || BitTorrentUtils.isBitTorrentKeepAlive(magic1, magic2, magic3, magic4) || BitTorrentUtils.isBitTorrentChoke(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentUnchoke(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentInterested(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentNotInterested(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentHave(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentCancel(magic1, magic2, magic3, magic4, magic5) || BitTorrentUtils.isBitTorrentBitfield(magic5) || BitTorrentUtils.isBitTorrentPiece(magic5)) { switchToBitTorrent(channelHandlerContext); } else { LOGGER.info("unknown protocol"); // unknown protocol; discard everything and close the connection channelBuffer.skipBytes(channelBuffer.readableBytes()); channelHandlerContext.getChannel().close(); return null; } // forward the current read buffer as-is to the new handlers return channelBuffer.readBytes(channelBuffer.readableBytes()); } /** Returns whether this is a serialized object, e.g. a Texai node-to-node message. * * @param magic1 the first byte of the message * @return whether this is a serialized object */ private boolean isSerializedObject(final int magic1) { return magic1 == NetworkConstants.OBJECT_SERIALIZATION_PROTOCOL; } /** Returns whether this is an HTTP message. * * @param magic1 the first byte of the message * @param magic2 the second byte of the message * @return whether this is an HTTP message */ private boolean isHttp(final int magic1, final int magic2) { return magic1 == 'G' && magic2 == 'E' || // GET magic1 == 'P' && magic2 == 'O' || // POST magic1 == 'P' && magic2 == 'U' || // PUT magic1 == 'H' && magic2 == 'E' || // HEAD magic1 == 'O' && magic2 == 'P' || // OPTIONS magic1 == 'P' && magic2 == 'A' || // PATCH magic1 == 'D' && magic2 == 'E' || // DELETE magic1 == 'T' && magic2 == 'R' || // TRACE magic1 == 'C' && magic2 == 'O'; // CONNECT } /** Dynamically switches the channel pipeline to handle an HTTP message. * * @param channelHandlerContext the channel handler context */ private void switchToHttp(final ChannelHandlerContext channelHandlerContext) { //Preconditions assert channelHandlerContext != null : "channelHandlerContext must not be null"; final ChannelPipeline channelPipeline = channelHandlerContext.getPipeline(); LOGGER.info("switching to HTTP channel pipeline from: " + channelPipeline); final Collection<ChannelHandler> channelHandlers = channelPipeline.toMap().values(); for (final ChannelHandler channelHandler : channelHandlers) { if (!(channelHandler instanceof SslHandler || channelHandler instanceof PortUnificationHandler)) { channelPipeline.remove(channelHandler); } } channelPipeline.addLast("encoder", new HttpResponseEncoder()); channelPipeline.addLast("decoder", new HttpRequestDecoder()); channelPipeline.addLast("aggregator", new HttpChunkAggregator(1048576)); channelPipeline.addLast("http-request-handler", httpRequestHandler); channelPipeline.remove(this); LOGGER.info("HTTP channel pipeline: " + channelPipeline); } /** Dynamically switches the channel pipeline to handle a serialized object message sent from one node to another * in an Albus hierarchical control network. * * @param channelHandlerContext the channel handler context */ private void switchToAlbusHCN(final ChannelHandlerContext channelHandlerContext) { //Preconditions assert channelHandlerContext != null : "channelHandlerContext must not be null"; assert albusHCNMessageHandler != null : "albusHCNMessageHandler must not be null"; final ChannelPipeline channelPipeline = channelHandlerContext.getPipeline(); LOGGER.info("switching to Albus HCN channel pipeline from: " + channelPipeline); final Collection<ChannelHandler> channelHandlers = channelPipeline.toMap().values(); for (final ChannelHandler channelHandler : channelHandlers) { if (!(channelHandler instanceof SslHandler || channelHandler instanceof PortUnificationHandler)) { channelPipeline.remove(channelHandler); } } channelPipeline.addLast("decoder", new TaggedObjectDecoder()); channelPipeline.addLast("encoder", new TaggedObjectEncoder()); channelPipeline.addLast("albus-handler", albusHCNMessageHandler); channelPipeline.remove(this); LOGGER.info("Albus HCN pipeline: " + channelPipeline.toString()); } /** Dynamically switches the channel pipeline to handle a bit torrent message. * * @param channelHandlerContext the channel handler context */ private void switchToBitTorrent(final ChannelHandlerContext channelHandlerContext) { //Preconditions assert channelHandlerContext != null : "channelHandlerContext must not be null"; final ChannelPipeline channelPipeline = channelHandlerContext.getPipeline(); LOGGER.info("switching to bit torrent channel pipeline from: " + channelPipeline); final Collection<ChannelHandler> channelHandlers = channelPipeline.toMap().values(); for (final ChannelHandler channelHandler : channelHandlers) { if (!(channelHandler instanceof SslHandler || channelHandler instanceof PortUnificationHandler)) { channelPipeline.remove(channelHandler); } } channelPipeline.addLast("torrent-decoder", new BitTorrentDecoder()); channelPipeline.addLast("torrent-encoder", new BitTorrentEncoder()); channelPipeline.addLast("torrent-handler", bitTorrentHandler); channelPipeline.remove(this); LOGGER.info("bit torrent pipeline: " + channelPipeline.toString()); } /** Handles a caught exception. * * @param channelHandlerContext the channel handler context * @param exceptionEvent the exception event */ @Override public void exceptionCaught( final ChannelHandlerContext channelHandlerContext, final ExceptionEvent exceptionEvent) { //Preconditions assert channelHandlerContext != null : "channelHandlerContext must not be null"; assert exceptionEvent != null : "exceptionEvent must not be null"; final Throwable throwable = exceptionEvent.getCause(); switch (throwable.getMessage()) { case "Received close_notify during handshake": LOGGER.info("ignoring warning: " + throwable.getMessage()); break; case "Connection reset by peer": LOGGER.info("connection reset by client web browser"); break; default: LOGGER.error(StringUtils.getStackTraceAsString(throwable)); LOGGER.error("exception: " + throwable.getMessage()); throw new TexaiException(throwable); } } /** Sets the Albus hierarchical control network channel handler. * * @param albusHCNMessageHandler the Albus hierarchical control network channel handler */ public void setAlbusHCNMessageHandler(final AbstractAlbusHCSMessageHandler albusHCNMessageHandler) { //Preconditions assert albusHCNMessageHandler != null : "albusHCNMessageHandler must not be null"; this.albusHCNMessageHandler = albusHCNMessageHandler; } /** Sets the HTTP request handler. * * @param httpRequestHandler the HTTP request handler */ public void setHttpRequestHandler(final AbstractHTTPRequestHandler httpRequestHandler) { //Preconditions assert httpRequestHandler != null : "httpRequestHandler must not be null"; this.httpRequestHandler = httpRequestHandler; } /** Sets the bit torrent handshake handler. * * @param bitTorrentHandler the bit torrent handshake handler */ public void setBitTorrentHandler(final AbstractBitTorrentHandler bitTorrentHandler) { //Preconditions assert bitTorrentHandler != null : "bitTorrentHandler must not be null"; this.bitTorrentHandler = bitTorrentHandler; } }