about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2011-09-07 00:36:58 +0000
committerEric Wong <normalperson@yhbt.net>2011-09-15 21:37:40 +0000
commitac346b5abcfa6253bd792091e5fb011774c40d49 (patch)
treeb304b96f42c3ba2cde396de8ed626754ae9d78cc
parentb48c6659b294b37f2c6ff3e75c1c9245522d48d1 (diff)
downloadunicorn-ac346b5abcfa6253bd792091e5fb011774c40d49.tar.gz
This will also be the foundation of SSL support in Rainbows!
and Zbatery.  Some users may also want to use this in
Unicorn on LANs to meet certain security/auditing requirements.
Of course, Nightmare! (in whatever form) should also be able to
use it.
-rw-r--r--lib/unicorn/configurator.rb13
-rw-r--r--lib/unicorn/http_server.rb3
-rw-r--r--lib/unicorn/ssl_client.rb6
-rw-r--r--lib/unicorn/ssl_configurator.rb104
-rw-r--r--lib/unicorn/ssl_server.rb42
-rwxr-xr-xscript/isolate_for_tests1
-rw-r--r--t/.gitignore2
-rwxr-xr-xt/sslgen.sh63
-rwxr-xr-xt/t0600-https-server-basic.sh48
-rw-r--r--test/unit/test_sni_hostnames.rb47
10 files changed, 325 insertions, 4 deletions
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb
index 8816c85..a93c1dc 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -1,5 +1,6 @@
 # -*- encoding: binary -*-
 require 'logger'
+require 'unicorn/ssl_configurator'
 
 # Implements a simple DSL for configuring a \Unicorn server.
 #
@@ -12,6 +13,7 @@ require 'logger'
 # See the link:/TUNING.html document for more information on tuning unicorn.
 class Unicorn::Configurator
   include Unicorn
+  include Unicorn::SSLConfigurator
 
   # :stopdoc:
   attr_accessor :set, :config_file, :after_reload
@@ -563,13 +565,16 @@ private
     end
   end
 
-  def set_bool(var, bool) #:nodoc:
+  def check_bool(var, bool) # :nodoc:
     case bool
     when true, false
-      set[var] = bool
-    else
-      raise ArgumentError, "#{var}=#{bool.inspect} not a boolean"
+      return bool
     end
+    raise ArgumentError, "#{var}=#{bool.inspect} not a boolean"
+  end
+
+  def set_bool(var, bool) #:nodoc:
+    set[var] = check_bool(var, bool)
   end
 
   def set_hook(var, my_proc, req_arity = 2) #:nodoc:
diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb
index 65880d4..c78b094 100644
--- a/lib/unicorn/http_server.rb
+++ b/lib/unicorn/http_server.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+require "unicorn/ssl_server"
 
 # This is the process manager of Unicorn. This manages worker
 # processes which in turn handle the I/O and application process.
@@ -19,6 +20,7 @@ class Unicorn::HttpServer
   attr_reader :pid, :logger
   include Unicorn::SocketHelper
   include Unicorn::HttpResponse
+  include Unicorn::SSLServer
 
   # backwards compatibility with 1.x
   Worker = Unicorn::Worker
@@ -563,6 +565,7 @@ class Unicorn::HttpServer
     self.timeout /= 2.0 # halve it for select()
     @config = nil
     build_app! unless preload_app
+    ssl_enable!
     @after_fork = @listener_opts = @orig_app = nil
   end
 
diff --git a/lib/unicorn/ssl_client.rb b/lib/unicorn/ssl_client.rb
new file mode 100644
index 0000000..7b41cd2
--- /dev/null
+++ b/lib/unicorn/ssl_client.rb
@@ -0,0 +1,6 @@
+# -*- encoding: binary -*-
+# :stopdoc:
+class Unicorn::SSLClient < Kgio::SSL
+  alias write kgio_write
+  alias close kgio_close
+end
diff --git a/lib/unicorn/ssl_configurator.rb b/lib/unicorn/ssl_configurator.rb
new file mode 100644
index 0000000..c92c85e
--- /dev/null
+++ b/lib/unicorn/ssl_configurator.rb
@@ -0,0 +1,104 @@
+# -*- encoding: binary -*-
+# :stopdoc:
+# This module is included in Unicorn::Configurator
+# :startdoc:
+#
+module Unicorn::SSLConfigurator
+  def ssl(&block)
+    ssl_require!
+    before = @set[:listeners].dup
+    opts = @set[:ssl_opts] = {}
+    yield
+    (@set[:listeners] - before).each do |address|
+      (@set[:listener_opts][address] ||= {})[:ssl_opts] = opts
+    end
+    ensure
+      @set.delete(:ssl_opts)
+  end
+
+  def ssl_certificate(file)
+    ssl_set(:ssl_certificate, file)
+  end
+
+  def ssl_certificate_key(file)
+    ssl_set(:ssl_certificate_key, file)
+  end
+
+  def ssl_client_certificate(file)
+    ssl_set(:ssl_client_certificate, file)
+  end
+
+  def ssl_dhparam(file)
+    ssl_set(:ssl_dhparam, file)
+  end
+
+  def ssl_ciphers(openssl_cipherlist_spec)
+    ssl_set(:ssl_ciphers, openssl_cipherlist_spec)
+  end
+
+  def ssl_crl(file)
+    ssl_set(:ssl_crl, file)
+  end
+
+  def ssl_prefer_server_ciphers(bool)
+    ssl_set(:ssl_prefer_server_ciphers, check_bool(bool))
+  end
+
+  def ssl_protocols(list)
+    ssl_set(:ssl_protocols, list)
+  end
+
+  def ssl_verify_client(on_off_optional)
+    ssl_set(:ssl_verify_client, on_off_optional)
+  end
+
+  def ssl_session_timeout(seconds)
+    ssl_set(:ssl_session_timeout, seconds)
+  end
+
+  def ssl_verify_depth(depth)
+    ssl_set(:ssl_verify_depth, depth)
+  end
+
+  # Allows specifying an engine for OpenSSL to use.  We have not been
+  # able to successfully test this feature due to a lack of hardware,
+  # Reports of success or patches to mongrel-unicorn@rubyforge.org is
+  # greatly appreciated.
+  def ssl_engine(engine)
+    ssl_warn_global(:ssl_engine)
+    ssl_require!
+    OpenSSL::Engine.load
+    OpenSSL::Engine.by_id(engine)
+    @set[:ssl_engine] = engine
+  end
+
+  def ssl_compression(bool)
+    # OpenSSL uses the SSL_OP_NO_COMPRESSION flag, Flipper follows suit
+    # with :ssl_no_compression, but we negate it to avoid exposing double
+    # negatives to the user.
+    ssl_set(:ssl_no_compression, check_bool(:ssl_compression, ! bool))
+  end
+
+private
+
+  def ssl_warn_global(func) # :nodoc:
+    Hash === @set[:ssl_opts] or return
+    warn("`#{func}' affects all SSL contexts in this process, " \
+         "not just this block")
+  end
+
+  def ssl_set(key, value) # :nodoc:
+    cur = @set[:ssl_opts]
+    Hash === cur or
+             raise ArgumentError, "#{key} must be called inside an `ssl' block"
+    cur[key] = value
+  end
+
+  def ssl_require! # :nodoc:
+    require "flipper"
+    require "unicorn/ssl_client"
+    rescue LoadError
+      warn "install 'kgio-monkey' for SSL support"
+      raise
+  end
+end
diff --git a/lib/unicorn/ssl_server.rb b/lib/unicorn/ssl_server.rb
new file mode 100644
index 0000000..c00c3ae
--- /dev/null
+++ b/lib/unicorn/ssl_server.rb
@@ -0,0 +1,42 @@
+# -*- encoding: binary -*-
+# :stopdoc:
+# this module is meant to be included in Unicorn::HttpServer
+# It is an implementation detail and NOT meant for users.
+module Unicorn::SSLServer
+  attr_accessor :ssl_engine
+
+  def ssl_enable!
+    sni_hostnames = rack_sni_hostnames(@app)
+    seen = {} # we map a single SSLContext to multiple listeners
+    listener_ctx = {}
+    @listener_opts.each do |address, address_opts|
+      ssl_opts = address_opts[:ssl_opts] or next
+      listener_ctx[address] = seen[ssl_opts.object_id] ||= begin
+        unless sni_hostnames.empty?
+          ssl_opts = ssl_opts.dup
+          ssl_opts[:sni_hostnames] = sni_hostnames
+        end
+        ctx = Flipper.ssl_context(ssl_opts)
+        # FIXME: make configurable
+        ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF
+        ctx
+      end
+    end
+    Unicorn::HttpServer::LISTENERS.each do |listener|
+      ctx = listener_ctx[sock_name(listener)] or next
+      listener.extend(Kgio::SSLServer)
+      listener.ssl_ctx = ctx
+      listener.kgio_ssl_class = Unicorn::SSLClient
+    end
+  end
+
+  # ugh, this depends on Rack internals...
+  def rack_sni_hostnames(rack_app) # :nodoc:
+    hostnames = {}
+    if Rack::URLMap === rack_app
+      mapping = rack_app.instance_variable_get(:@mapping)
+      mapping.each { |hostname,_,_,_| hostnames[hostname] = true }
+    end
+    hostnames.keys
+  end
+end
diff --git a/script/isolate_for_tests b/script/isolate_for_tests
index 1dbe769..cd4a2b2 100755
--- a/script/isolate_for_tests
+++ b/script/isolate_for_tests
@@ -18,6 +18,7 @@ pid = fork do
   Isolate.now!(opts) do
     gem 'sqlite3-ruby', '1.2.5'
     gem 'raindrops', '0.7.0'
+    gem 'kgio-monkey', '0.3.0'
     gem 'kgio', '2.6.0'
     gem 'rack', '1.3.2'
   end
diff --git a/t/.gitignore b/t/.gitignore
index a0c1c36..2312321 100644
--- a/t/.gitignore
+++ b/t/.gitignore
@@ -1,2 +1,4 @@
 /random_blob
 /.dep+*
+/*.crt
+/*.key
diff --git a/t/sslgen.sh b/t/sslgen.sh
new file mode 100755
index 0000000..3fd070a
--- /dev/null
+++ b/t/sslgen.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+set -e
+set -x
+
+certinfo() {
+        echo US
+        echo Hell
+        echo A Very Special Place
+        echo Monkeys
+        echo Poo-Flingers
+        echo 127.0.0.1
+        echo kgio@bogomips.org
+}
+
+certinfo2() {
+        certinfo
+        echo
+        echo
+}
+
+ca_certinfo () {
+        echo US
+        echo Hell
+        echo An Even More Special Place
+        echo Deranged Monkeys
+        echo Poo-Hurlers
+        echo 127.6.6.6
+        echo unicorn@bogomips.org
+}
+
+openssl genrsa -out ca.key 512
+ca_certinfo | openssl req -new -x509 -days 666 -key ca.key -out ca.crt
+
+openssl genrsa -out bad-ca.key 512
+ca_certinfo | openssl req -new -x509 -days 666 -key bad-ca.key -out bad-ca.crt
+
+openssl genrsa -out server.key 512
+certinfo2 | openssl req -new -key server.key -out server.csr
+
+openssl x509 -req -days 666 \
+        -in server.csr -CA ca.crt -CAkey ca.key -set_serial 1 -out server.crt
+n=2
+mk_client_cert () {
+        CLIENT=$1
+        openssl genrsa -out $CLIENT.key 512
+        certinfo2 | openssl req -new -key $CLIENT.key -out $CLIENT.csr
+
+        openssl x509 -req -days 666 \
+                -in $CLIENT.csr -CA $CA.crt -CAkey $CA.key -set_serial $n \
+                -out $CLIENT.crt
+        rm -f $CLIENT.csr
+        n=$(($n + 1))
+}
+
+CA=ca
+mk_client_cert client1
+mk_client_cert client2
+
+CA=bad-ca mk_client_cert bad-client
+
+rm -f server.csr
+
+echo OK
diff --git a/t/t0600-https-server-basic.sh b/t/t0600-https-server-basic.sh
new file mode 100755
index 0000000..5dd0d65
--- /dev/null
+++ b/t/t0600-https-server-basic.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+. ./test-lib.sh
+t_plan 7 "simple HTTPS connection tests"
+
+t_begin "setup and start" && {
+        rtmpfiles curl_err
+        unicorn_setup
+cat > $unicorn_config <<EOF
+ssl do
+  listen "$listen"
+  ssl_certificate "server.crt"
+  ssl_certificate_key "server.key"
+end
+pid "$pid"
+stderr_path "$r_err"
+stdout_path "$r_out"
+EOF
+        unicorn -D -c $unicorn_config env.ru
+        unicorn_wait_start
+}
+
+t_begin "single request" && {
+        curl -sSfv --cacert ca.crt https://$listen/
+}
+
+t_begin "check stderr has no errors" && {
+        check_stderr
+}
+
+t_begin "multiple requests" && {
+        curl -sSfv --no-keepalive --cacert ca.crt \
+                https://$listen/ https://$listen/ 2>> $curl_err >> $tmp
+                dbgcat curl_err
+}
+
+t_begin "check stderr has no errors" && {
+        check_stderr
+}
+
+t_begin "killing succeeds" && {
+        kill $unicorn_pid
+}
+
+t_begin "check stderr has no errors" && {
+        check_stderr
+}
+
+t_done
diff --git a/test/unit/test_sni_hostnames.rb b/test/unit/test_sni_hostnames.rb
new file mode 100644
index 0000000..457afee
--- /dev/null
+++ b/test/unit/test_sni_hostnames.rb
@@ -0,0 +1,47 @@
+# -*- encoding: binary -*-
+require "test/unit"
+require "unicorn"
+
+# this tests an implementation detail, it may change so this test
+# can be removed later.
+class TestSniHostnames < Test::Unit::TestCase
+  include Unicorn::SSLServer
+
+  def setup
+    GC.start
+  end
+
+  def teardown
+    GC.start
+  end
+
+  def test_host_name_detect_one
+    app = Rack::Builder.new do
+      map "http://sni1.example.com/" do
+        use Rack::ContentLength
+        use Rack::ContentType, "text/plain"
+        run lambda { |env| [ 200, {}, [] ] }
+      end
+    end.to_app
+    hostnames = rack_sni_hostnames(app)
+    assert hostnames.include?("sni1.example.com")
+  end
+
+  def test_host_name_detect_multiple
+    app = Rack::Builder.new do
+      map "http://sni2.example.com/" do
+        use Rack::ContentLength
+        use Rack::ContentType, "text/plain"
+        run lambda { |env| [ 200, {}, [] ] }
+      end
+      map "http://sni3.example.com/" do
+        use Rack::ContentLength
+        use Rack::ContentType, "text/plain"
+        run lambda { |env| [ 200, {}, [] ] }
+      end
+    end.to_app
+    hostnames = rack_sni_hostnames(app)
+    assert hostnames.include?("sni2.example.com")
+    assert hostnames.include?("sni3.example.com")
+  end
+end