CLOVER🍀

That was when it all began.

Netty Http Snoop Clientを、SSL化する

せっかくなら、クライアント側もやってしまえ、ということで。
http://d.hatena.ne.jp/Kazuhira/20120610/1339323448

クライアント側のSSLContextを作成するために、HttpsContextFactoryに修正を加えます。
HttpsContextFactory.scala

import java.io.{BufferedInputStream, FileInputStream}
import java.security.{KeyStore, Security}

import javax.net.ssl.{KeyManager, KeyManagerFactory, SSLContext, SSLEngine, TrustManager}

object HttpsContextFactory {
  private val PROTOCOL: String = "TLS"

  private val SERVER_CONTEXT: SSLContext = {
    val algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm") match {
      case null => "SunX509"
      case alg => alg
    }

    try {
      val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
      using(new FileInputStream("keystore")) { fis =>
        using(new BufferedInputStream(fis)) { bis =>
          keyStore.load(bis, "changeit".toCharArray)

          val kmf = KeyManagerFactory.getInstance(algorithm)
          kmf.init(keyStore, "password".toCharArray)

          val serverContext = SSLContext.getInstance(PROTOCOL)
          serverContext.init(kmf.getKeyManagers, null, null)

          serverContext
        }
      }
    } catch {
      case e: Exception => throw new Error("Failed to initialize the server-side SSLContext", e)
    }
  }

  private val CLIENT_CONTEXT: SSLContext =
    try {
      val clientContext = SSLContext.getInstance(PROTOCOL)
      clientContext.init(null, HttpsTrustManagerFactory.getTrustManagers, null)
      clientContext
    } catch {
      case e: Exception => throw new Error("Failed to initialize the client-side SSLContext", e)
    }

  def getServerContext: SSLContext = SERVER_CONTEXT
  def getClientContext: SSLContext = CLIENT_CONTEXT

  private def using[A <: { def close(): Unit }, B](resource: A)(body: A => B): B =
    try {
      body(resource)
    } finally {
      if (resource != null) resource.close()
    }
}

クライアント側のSSLContextには、KeyStoreは不要です。代わりに、TrustManagerが必要になります。

というわけで、TrustManagerを返すファクトリを作成します。
HttpsTrustManagerFactory.scala

import java.security.{InvalidAlgorithmParameterException, KeyStore, KeyStoreException}
import java.security.cert.{CertificateException, X509Certificate}

import javax.net.ssl.{ManagerFactoryParameters, TrustManager, TrustManagerFactorySpi, X509TrustManager}

object HttpsTrustManagerFactory extends TrustManagerFactorySpi {
  private val DUMMY_TRUST_MANAGER: TrustManager = new X509TrustManager {
    def getAcceptedIssuers: Array[X509Certificate] = new Array[X509Certificate](0)

    @throws(classOf[CertificateException])
    def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit =
      System.err.println("UNKNOWN CLIENT CERTIFICATE: " + chain(0).getSubjectDN)

    @throws(classOf[CertificateException])
    def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit =
      System.err.println("UNKNOWN SERVER CERTIFICATE: " + chain(0).getSubjectDN)
  }

  def getTrustManagers: Array[TrustManager] = Array(DUMMY_TRUST_MANAGER)

  override protected def engineGetTrustManagers: Array[TrustManager] = getTrustManagers
  @throws(classOf[KeyStoreException])
  override protected def engineInit(keyStore: KeyStore): Unit = ()
  @throws(classOf[InvalidAlgorithmParameterException])
  override protected def engineInit(managerFactoryParameters: ManagerFactoryParameters): Unit = ()
}

要は、どんな証明書をもらってもエラーとしないTrustManagerです。

続いて、クライアント側のPipelineFactoryです。
HttpClientPipelineFactory.scala

import javax.net.ssl.SSLEngine

import org.jboss.netty.channel.{ChannelPipeline, ChannelPipelineFactory, Channels}
import org.jboss.netty.handler.codec.http.{HttpClientCodec, HttpContentDecompressor}
import org.jboss.netty.handler.ssl.SslHandler

class HttpClientPipelineFactory(ssl: Boolean) extends ChannelPipelineFactory {
  @throws(classOf[Exception])
  def getPipeline: ChannelPipeline = {
    val pipeline = Channels.pipeline()

    if (ssl) {
      val engine = HttpsContextFactory.getClientContext.createSSLEngine()
      engine.setUseClientMode(true)
      pipeline.addLast("ssl", new SslHandler(engine))
    }

    pipeline.addLast("codec", new HttpClientCodec)
    pipeline.addLast("inflater", new HttpContentDecompressor)
    pipeline.addLast("handler", new HttpResponseHandler)

    pipeline
  }
}

こちらでは、SSLEngine#setUseClientModeをtrueに設定します。

あとは、SSLをサポートするようにHttpClientを修正。

HttpClient.scala 
import java.net.{InetSocketAddress, URI}
import java.util.concurrent.Executors

import org.jboss.netty.bootstrap.ClientBootstrap
import org.jboss.netty.channel.{Channel, ChannelFuture}
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory
import org.jboss.netty.handler.codec.http.CookieEncoder
import org.jboss.netty.handler.codec.http.DefaultHttpRequest
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.HttpVersion

object HttpClient {
  @throws(classOf[Exception])
  def main(args: Array[String]): Unit = {
    args.length match {
      case 1 =>
      case _ =>
	Console.err.println("Usage: %s <URL>".format(this.getClass.getSimpleName))
	sys.exit(1)
    }

    val uri = new URI(args(0))
    val scheme = uri.getScheme match {
      case null => "http"
      case s => s
    }
    val host = uri.getHost match {
      case null => "localhost"
      case h => h
    }
    val port = uri.getPort match {
      case -1 if scheme == "http" => 80
      case -1 if scheme == "https" => 443
      case p => p
    }

    if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
      Console.err.println("Only HTTP(S) is supported.")
      sys.exit(1)
    }

    val ssl = scheme.equalsIgnoreCase("https")

    val bootstrap = new ClientBootstrap(
      new NioClientSocketChannelFactory(Executors.newCachedThreadPool, Executors.newCachedThreadPool))

    bootstrap.setPipelineFactory(new HttpClientPipelineFactory(ssl))

    val future = bootstrap.connect(new InetSocketAddress(host, port))

    val channel = future.awaitUninterruptibly().getChannel
    if (!future.isSuccess) {
      future.getCause.printStackTrace()
      bootstrap.releaseExternalResources()
      sys.exit(1)
    }

    val request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toASCIIString)
    request.setHeader(HttpHeaders.Names.HOST, host)
    request.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)
    request.setHeader(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP)

    val httpCookieEncoder = new CookieEncoder(false)
    httpCookieEncoder.addCookie("my-cookie", "foo")
    httpCookieEncoder.addCookie("anothor-cookie", "bar")
    request.setHeader(HttpHeaders.Names.COOKIE, httpCookieEncoder.encode)

    channel.write(request)

    channel.getCloseFuture.awaitUninterruptibly()

    bootstrap.releaseExternalResources()
  }
}

sbtで実行すると、こんな感じで出力されます。

> run https://localhost:8443/

Multiple main classes detected, select one to run:

 [1] HttpServer
 [2] HttpClient

Enter number: 2

[info] Running HttpClient https://localhost:8443/
UNKNOWN SERVER CERTIFICATE: CN=localhost, OU=Little Wings, O=Kazuhira, L=test, ST=test, C=JP
STATUS: 200 OK
VERSION: HTTP/1.1

HEADER: Content-Encoding = identity
HEADER: Content-Type = text/plain; charset=UTF-8
HEADER: Set-Cookie = anothor-cookie=bar;my-cookie=foo

CHUNKED CONTENT {
WELCOME TO THE WILD WILD WEB SERVER
===================================
VERSION: HTTP/1.1
HOSTNAME: localhost
REQUEST_URI: https://localhost:8443/

HEADER: Host = localhost
HEADER: Connection = close
HEADER: Accept-Encoding = gzip
HEADER: Cookie = anothor-cookie=bar;my-cookie=foo

} END OF CHUNKED CONTENT
[success] Total time: 2 s, completed 2012/06/10 22:54:11

さっきのエントリが、サーバ側だけとちょっと中途半端だったので、ここまで一応やってみましたと。