Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

add preliminary SSL support

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 should also be able to use it.
  • Loading branch information...
commit 4fd73ff182d4e4e1c7f9490d4e54e16152a05baf 1 parent 93875f3
Eric Wong authored
View
13 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 @@
# 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
@@ -569,13 +571,16 @@ def set_path(var, path) #:nodoc:
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:
View
3  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
@@ -582,6 +584,7 @@ def init_worker_process(worker)
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
View
6 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
View
104 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
View
42 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
View
2  t/.gitignore
@@ -1,2 +1,4 @@
/random_blob
/.dep+*
+/*.crt
+/*.key
View
63 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
View
50 t/t0600-https-server-basic.sh
@@ -0,0 +1,50 @@
+#!/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
+ # curl -sSfv --no-keepalive --cacert ca.crt \
+ # https://$listen/ 2>> $curl_err >> $tmp
+}
+
+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
View
47 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
Please sign in to comment.
Something went wrong with that request. Please try again.