All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.netty4.proxy.HttpProxyConnectHandler.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.finagle.netty4.proxy

import com.twitter.finagle.{
  CancelledConnectionException, ChannelClosedException, Failure, ConnectionFailedException
}
import com.twitter.finagle.client.Transporter
import com.twitter.finagle.client.Transporter.Credentials
import com.twitter.finagle.netty4.channel.BufferingChannelOutboundHandler
import com.twitter.io.Charsets
import com.twitter.util.Base64StringEncoder
import io.netty.channel._
import io.netty.handler.codec.http._
import io.netty.util.concurrent.{Future => NettyFuture, GenericFutureListener}
import java.net.SocketAddress

/**
 * An internal handler that upgrades the pipeline to delay connect-promise satisfaction until the
 * remote HTTP proxy server is ready to proxy traffic to an ultimate destination represented as
 * `host` (i.e., HTTP proxy connect procedure is successful).
 *
 * This enables "Tunneling TCP-based protocols (i.e., TLS/SSL) through Web proxy servers" [1] and
 * may be used with any TCP traffic, not only HTTP(S). See Squid documentation on this feature [2].
 *
 * @note We don't use Netty's implementation [3] here because it supports an opposite direction: the
 *       destination passed to `Channel.connect` is an ultimate target and the `HttpProxyHandler`
 *       is supposed to replace it with proxy addr (represented as a `SocketAddress`). This is the
 *       exact approach we used for Netty 3 implementation, but we don't do that anymore because we
 *       don't want to bypass Finagle's load balancers while resolving the proxy endpoint.
 *
 * @note This mixes in a [[BufferingChannelOutboundHandler]] so we can protect ourselves from
 *       channel handlers that write on `channelAdded` or `channelActive`.
 *
 * [1]: http://www.web-cache.com/Writings/Internet-Drafts/draft-luotonen-web-proxy-tunneling-01.txt
 * [2]: http://wiki.squid-cache.org/Features/HTTPS
 * [3]: https://github.com/netty/netty/blob/4.1/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java
 *
 * @param host the ultimate host a remote proxy server connects to
 *
 * @param credentialsOption optional credentials for a proxy server
 */
private[netty4] class HttpProxyConnectHandler(
  host: String,
  credentialsOption: Option[Transporter.Credentials],
  httpClientCodec: ChannelHandler = new HttpClientCodec() // exposed for testing
) extends ChannelDuplexHandler with BufferingChannelOutboundHandler { self =>

  private[this] val httpCodecKey: String = "http proxy client codec"
  private[this] var connectPromise: ChannelPromise = _

  private[this] def proxyAuthorizationHeader(c: Credentials): String = {
    val bytes = "%s:%s".format(c.username, c.password).getBytes(Charsets.Utf8)
    "Basic " + Base64StringEncoder.encode(bytes)
  }

  private[this] def fail(ctx: ChannelHandlerContext, t: Throwable): Unit = {
    connectPromise.tryFailure(t)
    failPendingWrites(ctx, t)
  }

  override def connect(
    ctx: ChannelHandlerContext,
    remote: SocketAddress,
    local: SocketAddress,
    promise: ChannelPromise
  ): Unit = {
    val proxyConnectPromise = ctx.newPromise()

    // Cancel new promise if an original one is canceled.
    promise.addListener(new GenericFutureListener[NettyFuture[Any]] {
      override def operationComplete(f: NettyFuture[Any]): Unit =
        if (f.isCancelled) {
          if (!proxyConnectPromise.cancel(true) && proxyConnectPromise.isSuccess) {
            // New connect promise wasn't cancelled because it was already satisfied (connected) so
            // we need to close the channel to prevent resource leaks.
            // See https://github.com/twitter/finagle/issues/345
            failPendingWrites(ctx, new CancelledConnectionException())
            ctx.close()
          }
        }
    })

    // Fail old promise if a new one is failed.
    proxyConnectPromise.addListener(new GenericFutureListener[NettyFuture[Any]] {
      override def operationComplete(f: NettyFuture[Any]): Unit =
        if (f.isSuccess) {
          // Add HTTP client codec so we can talk to an HTTP proxy.
          ctx.pipeline().addBefore(ctx.name(), httpCodecKey, httpClientCodec)

          // Create new connect HTTP proxy connect request.
          val req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, host)
          req.headers().set(HttpHeaderNames.HOST, host)
          credentialsOption.foreach(c =>
            req.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, proxyAuthorizationHeader(c))
          )

          ctx.writeAndFlush(req)

          // We issue a read request if auto-read is disabled.
          if (!ctx.channel().config().isAutoRead) {
            ctx.read()
          }
        } else {
          // The connect request was cancelled or failed so the channel was never active. Since no
          // writes are expected from the previous handler, no need to fail the pending writes.
          if (!f.isCancelled) {
            promise.setFailure(f.cause())
          }
        }
    })

    // We propagate the pipeline with a new promise thereby delaying the original connect's
    // satisfaction.
    connectPromise = promise
    ctx.connect(remote, local, proxyConnectPromise)
  }

  override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = msg match {
    case rep: FullHttpResponse =>
      // A remote HTTP proxy is ready to proxy traffic to an ultimate destination. We no longer
      // need HTTP proxy pieces in the pipeline.
      if (rep.status() == HttpResponseStatus.OK) {
        ctx.pipeline().remove(httpCodecKey)
        ctx.pipeline().remove(self) // drains pending writes when removed

        connectPromise.trySuccess()
        // We don't release `req` since by specs, we don't expect any payload sent back from a
        // a web proxy server.
      } else {
        val failure = new ConnectionFailedException(
          Failure(s"Unexpected status returned from an HTTP proxy server: ${rep.status()}."),
          ctx.channel().remoteAddress()
        )

        fail(ctx, failure)
        ctx.close()
      }
    case other => ctx.fireChannelRead(other)
  }

  override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = {
    fail(ctx, cause)
    ctx.fireExceptionCaught(cause) // we don't call super.exceptionCaught since we've already filed
                                   // both connect promise and pending writes in `fail`
    ctx.close() // close a channel since we've failed to perform an HTTP proxy handshake
  }

  override def channelInactive(ctx: ChannelHandlerContext): Unit = {
    fail(ctx, new ChannelClosedException(ctx.channel().remoteAddress()))
    ctx.fireChannelInactive()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy