せっかくなら、クライアント側もやってしまえ、ということで。
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
さっきのエントリが、サーバ側だけとちょっと中途半端だったので、ここまで一応やってみましたと。