そろそろ、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