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.{ChannelClosedException, ProxyConnectException}
import com.twitter.finagle.client.Transporter
import com.twitter.finagle.client.Transporter.Credentials
import com.twitter.finagle.netty4.channel.BufferingChannelOutboundHandler
import com.twitter.finagle.netty4.channel.ConnectPromiseDelayListeners._
import com.twitter.util.Base64StringEncoder
import io.netty.channel._
import io.netty.handler.codec.http._
import io.netty.util.ReferenceCountUtil
import io.netty.util.concurrent.{GenericFutureListener, Future => NettyFuture}
import java.nio.charset.StandardCharsets.UTF_8
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]: https://tools.ietf.org/html/draft-luotonen-web-proxy-tunneling-01
 * [2]: https://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] final def httpCodecKey = "httpProxyClientCodec"

  private[this] var connectPromise: ChannelPromise = _
  private[this] var proxyResponseStatus: HttpResponseStatus = _

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

  private[this] final def fail(t: Throwable): Unit = {
    // We "try" because it might be already cancelled and we don't need to handle
    // cancellations here - it's already done by `proxyCancellationsTo`.
    connectPromise.tryFailure(t)
    failPendingWrites(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.
    // NOTE: We don't worry about cancelling/failing pending writes here since it will happen
    // automatically on channel closure.
    promise.addListener(proxyCancellationsTo(proxyConnectPromise, ctx))

    // 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)
          readIfNeeded(ctx)
        } 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 = {
    def finishOrRead(o: HttpObject): Unit = {
      ReferenceCountUtil.release(o)

      if (o.isInstanceOf[LastHttpContent]) finishProxyHandshake(ctx)
      else readIfNeeded(ctx)
    }

    msg match {
      case rep: HttpResponse =>
        // The first portion of a response has arrived so we capture the status.
        proxyResponseStatus = rep.status()
        finishOrRead(rep)

      case chunk: HttpContent =>
        // Some proxy servers send their error messages as HTML formatted documents so
        // we need to swallow them.
        finishOrRead(chunk)

      case other => ctx.fireChannelRead(other)
    }
  }

  override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = {
    fail(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(new ChannelClosedException(ctx.channel().remoteAddress()))
    ctx.fireChannelInactive()
  }

  private[this] final def finishProxyHandshake(ctx: ChannelHandlerContext): Unit = {
    // A remote HTTP proxy is ready to proxy traffic to an ultimate destination. We no longer
    // need HTTP proxy pieces in the pipeline.
    if (proxyResponseStatus == HttpResponseStatus.OK) {
      ctx.pipeline().remove(httpCodecKey)
      ctx.pipeline().remove(self) // drains pending writes when removed

      connectPromise.trySuccess()
    } else {
      val failure =
        new ProxyConnectException(
          s"Unexpected status returned from an HTTP proxy server: $proxyResponseStatus.",
          ctx.channel().remoteAddress()
        )

      fail(failure)
      ctx.close()
    }
  }

  private[this] final def readIfNeeded(ctx: ChannelHandlerContext): Unit = {
    // We issue a read request if auto-read is disabled.
    if (!ctx.channel().config().isAutoRead) {
      ctx.read()
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy