CLOVER🍀

That was when it all began.

Netty Http Snoop Serverを、SSL化する

そろそろ、Nettyを使ったSSLサーバの作成をやってみようと思います。ExampleのHTTP(Snoop)は、SecureChatで使用しているSSL系のクラスを使用していますが、今回はkeytoolで作った証明書を使って、SSL化します。

では、まずKeyStoreを作成します。KeyStoreのファイル名は「keystore」、作成する証明書のエイリアスは「ssltest」としています。パスワードは、KeyStoreのパスワードが「changeit」、証明書のパスワードは「password」です。
その他の情報は、超適当。

$ keytool -keystore keystore -genkey -alias ssltest
キーストアのパスワードを入力してください:  
新規パスワードを再入力してください: 
姓名を入力してください。
  [Unknown]:  localhost
組織単位名を入力してください。
  [Unknown]:  Little Wings   
組織名を入力してください。
  [Unknown]:  Kazuhira    
都市名または地域名を入力してください。
  [Unknown]:  test
都道府県名を入力してください。
  [Unknown]:  test
この単位に該当する2文字の国コードを入力してください。
  [Unknown]:  JP
CN=localhost, OU=Little Wings, O=Kazuhira, L=test, ST=test, C=JPでよろしいですか。
  [いいえ]:  はい

<ssltest>の鍵パスワードを入力してください
	(キーストアのパスワードと同じ場合はRETURNを押してください):  
新規パスワードを再入力してください: 

できあがったKeyStoreから、含まれている証明書を確認します。

$ keytool -list -keystore keystore -v
キーストアのパスワードを入力してください:  

キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには1エントリが含まれます

別名: ssltest
作成日: 2012/06/10
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: CN=localhost, OU=Little Wings, O=Kazuhira, L=test, ST=test, C=JP
発行者: CN=localhost, OU=Little Wings, O=Kazuhira, L=test, ST=test, C=JP
シリアル番号: 3e349c71
有効期間の開始日: Sun Jun 10 18:22:53 JST 2012終了日: Sat Sep 08 18:22:53 JST 2012
証明書のフィンガプリント:
	 MD5:  7D:57:24:44:19:1C:83:BD:7E:91:F4:CB:9B:F4:21:D8
	 SHA1: 0A:93:C4:F2:CF:79:C4:A1:28:0A:23:FF:62:A9:7D:5B:8B:A3:B8:6E
	 SHA256: B4:B6:36:69:4B:D6:BD:B9:98:4D:7C:5D:4E:93:C9:BA:02:63:CB:F3:02:F4:6E:C8:78:7B:CA:1B:C2:02:85:32
	 署名アルゴリズム名: SHA1withDSA
	 バージョン: 3

拡張: 

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: E0 9F EF 64 57 D4 38 DC   5F 9A 86 63 2D 2A 4D D2  ...dW.8._..c-*M.
0010: 8D D2 D0 CF                                        ....
]
]



*******************************************
*******************************************

では、こちらを使って以前のエントリで作成した単純なHTTPサーバをSSL化します。
http://d.hatena.ne.jp/Kazuhira/20110731/1312111672

今回は、差分だけ記載。

build.sbt

name := "netty-http-ssl"

version := "0.0.1"

scalaVersion := "2.9.2"

organization := "littlewings"

libraryDependencies += "io.netty" % "netty" % "3.5.0.Final"

今回は、3.5.0.Finalです。なお、元ネタを作成した時は、3.2.4.Finalでした。コンパイルは、1行たりとも変えずに通すことができました。素晴らしい!

んで、SSLContextを作成するクラス。SecureChatでは、KeyStoreをバイト配列で表現していますが、今回はファイルとしての実体を持ったKeyStoreを使用します。
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("JKS")
      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)
    }
  }

  def getServerContext: SSLContext = SERVER_CONTEXT

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

KeyStore#loadにKeyStoreをInputStream形式で渡し、一緒にKeyStoreのパスワードをcharの配列で渡します。証明書のパスワードは、KeyManagerFactory#initで使用します。

これを、PipelineFactoryに組み込みます。
HttpServerPipelineFactory.scala

import javax.net.ssl.SSLEngine

import org.jboss.netty.channel.{ChannelPipeline, ChannelPipelineFactory, Channels}
import org.jboss.netty.handler.codec.http.{HttpContentCompressor, HttpRequestDecoder, HttpResponseEncoder}
import org.jboss.netty.handler.ssl.SslHandler

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

    val engine = HttpsContextFactory.getServerContext.createSSLEngine()
    engine.setUseClientMode(false)

    pipeline.addLast("ssl", new SslHandler(engine))
    pipeline.addLast("decoder", new HttpRequestDecoder)
    pipeline.addLast("encoder", new HttpResponseEncoder)
    pipeline.addLast("deflater", new HttpContentCompressor)
    pipeline.addLast("handler", new HttpRequestHandler)

    pipeline
  }
}

新規作成したクラスから、SSLContextを受け取り、そこからSSLEngineを作成します。それを、SslHandlerに渡してPipelineに追加。

ここから先は、SSLにはあまり関係がありません。サーバのシャットダウンコードを追加しています。
HttpServer.scala

import java.net.InetSocketAddress
import java.util.concurrent.Executors

import org.jboss.netty.bootstrap.ServerBootstrap
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory
import org.jboss.netty.channel.group.{ChannelGroup, DefaultChannelGroup}

object HttpServer {
  val channels: ChannelGroup = new DefaultChannelGroup

  def main(args: Array[String]): Unit = {
    val bootstrap = new ServerBootstrap(
      new NioServerSocketChannelFactory(Executors.newCachedThreadPool, Executors.newCachedThreadPool))

    bootstrap.setPipelineFactory(new HttpServerPipelineFactory)

    val channel = bootstrap.bind(new InetSocketAddress(8443))
    channels.add(channel)

    waitForShutdown()
    val future = channels.close()
    future.awaitUninterruptibly()
    bootstrap.releaseExternalResources()
  }

  private def waitForShutdown(): Unit = {
    readLine()
    println("Shutdown Https Server...")
  }
}

しれっとリッスンポートが、8443になっています。

HttpRequestHandler.scala

import scala.collection.JavaConverters._

import java.util.Map.Entry

import org.jboss.netty.buffer.{ChannelBuffer, ChannelBuffers}
import org.jboss.netty.channel.ChannelFuture
import org.jboss.netty.channel.ChannelFutureListener
import org.jboss.netty.channel.ChannelHandlerContext
import org.jboss.netty.channel.ChannelStateEvent
import org.jboss.netty.channel.ExceptionEvent
import org.jboss.netty.channel.MessageEvent
import org.jboss.netty.channel.SimpleChannelUpstreamHandler
import org.jboss.netty.handler.codec.http.Cookie
import org.jboss.netty.handler.codec.http.CookieDecoder
import org.jboss.netty.handler.codec.http.CookieEncoder
import org.jboss.netty.handler.codec.http.DefaultHttpResponse
import org.jboss.netty.handler.codec.http.HttpChunk
import org.jboss.netty.handler.codec.http.HttpChunkTrailer
import org.jboss.netty.handler.codec.http.HttpHeaders
import org.jboss.netty.handler.codec.http.HttpHeaders.Names
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
import org.jboss.netty.handler.codec.http.QueryStringDecoder
import org.jboss.netty.util.CharsetUtil

class HttpRequestHandler extends SimpleChannelUpstreamHandler {
  private var request: HttpRequest = _
  private var readingChunks: Boolean = _
  private val buf: StringBuilder = new StringBuilder

  @throws(classOf[Exception])
  override def channelConnected(ctx: ChannelHandlerContext, e: ChannelStateEvent): Unit =
    HttpServer.channels.add(e.getChannel)

  @throws(classOf[Exception])
  override def channelDisconnected(ctx: ChannelHandlerContext, e: ChannelStateEvent): Unit =
    HttpServer.channels.remove(e.getChannel)

  〜省略〜

}

これでサーバを起動して、Firefoxなどのブラウザで
https://localhost:8443/
でアクセスすると、以下のようなレスポンスが返るはずです。いわゆるオレオレ証明書なので、最初に信頼するかどうか聞かれるはずなので、例外として許可してあげてくださいね。

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

HEADER: Host = localhost:8443
HEADER: User-Agent = Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0
HEADER: Accept = text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HEADER: Accept-Language = ja,en-us;q=0.7,en;q=0.3
HEADER: Accept-Encoding = gzip, deflate
HEADER: Connection = keep-alive
HEADER: Cache-Control = max-age=0