1 # -*- encoding: binary -*-
4 # This is a library for an experimental SSL proxy similar to
5 # {stunnel}[http://stunnel.org/] or {stud}[git://github.com/bumptech/stud].
6 # It does not use threads internally and uses as little memory as
7 # possible. It supports both client and server proxies in a single event
8 # loop and allows multiple event loops to be used.
10 # It is intended as a demo and testbed of kgio-monkey functionality,
11 # but maybe useful in production. It only supports TCP <-> TCP
14 # Most of Flipper is undocumented and the API is subject to change.
16 autoload :SSLAccepted, "flipper/ssl_accepted"
17 autoload :SSLUpstream, "flipper/ssl_upstream"
18 autoload :Poller, "flipper/poller"
19 autoload :FakePoll, "flipper/fake_poll"
20 autoload :Configurator, "flipper/configurator"
21 autoload :SNI, "flipper/sni"
22 autoload :TDBSessionCache, "flipper/tdb_session_cache"
23 autoload :MemcacheCommon, "flipper/memcache_common"
24 autoload :DalliSessionCache, "flipper/dalli_session_cache"
25 autoload :MemcachedSessionCache, "flipper/memcached_session_cache"
27 @proto_user = { :SSLv2 => 2, :SSLv3 => 4, :TLSv1 => 8 }
29 OpenSSL::SSL::OP_NO_SSLv2 |
30 OpenSSL::SSL::OP_NO_SSLv3 |
31 OpenSSL::SSL::OP_NO_TLSv1,
32 OpenSSL::SSL::OP_NO_SSLv3|OpenSSL::SSL::OP_NO_TLSv1,
33 OpenSSL::SSL::OP_NO_SSLv2|OpenSSL::SSL::OP_NO_TLSv1,
34 OpenSSL::SSL::OP_NO_TLSv1,
35 OpenSSL::SSL::OP_NO_SSLv2|OpenSSL::SSL::OP_NO_SSLv3,
36 OpenSSL::SSL::OP_NO_SSLv3,
37 OpenSSL::SSL::OP_NO_SSLv2
40 # we try to emulate the nginx configuration format/terminology as much
41 # as possible to make it easy for users to learn.
42 # ref: http://wiki.nginx.org/NginxHttpSslModule
43 def self.ssl_context(opts)
44 ctx = OpenSSL::SSL::SSLContext.new
46 :options => OpenSSL::SSL::OP_ALL,
47 :session_id_context => "kgio",
50 # same default ecdh curve as nginx (as of r3961)
51 :ssl_ecdh_curve => opts[:ssl_ecdh_curve] || "prime256v1",
54 tmp = opts[:ssl_certificate] and
55 monkey_opts[:ssl_certificate] = File.expand_path(tmp)
56 tmp = opts[:ssl_certificate_key] and
57 monkey_opts[:ssl_certificate_key] = File.expand_path(tmp)
59 tmp = opts[:ssl_version] and params[:ssl_version] = tmp
60 tmp = opts[:ssl_ciphers] and params[:ciphers] = tmp
61 tmp = opts[:ssl_verify_depth] and params[:verify_depth] = tmp
62 tmp = opts[:ssl_session_timeout] and params[:timeout] = tmp
63 tmp = opts[:ssl_ca_file] and params[:ca_file] = File.expand_path(tmp)
65 if tmp = opts[:ssl_client_certificate]
66 params[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
67 tmp = params[:ca_file] = File.expand_path(tmp)
69 params[:client_ca] = Kgio::SSL.split_pem(OpenSSL::X509::Certificate, tmp)
72 case tmp = opts[:ssl_verify_mode]
74 params[:verify_mode] = tmp.inject(0) do |mode,f|
75 mode |= OpenSSL::SSL.const_get("VERIFY_#{f.to_s.upcase}")
78 params[:verify_mode] = OpenSSL::SSL.const_get("VERIFY_#{tmp.to_s.upcase}")
80 params[:verify_mode] = tmp
83 if tmp = opts[:ssl_client]
84 params[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
85 tmp = opts[:ssl_ca_path] and params[:ca_path] = File.expand_path(tmp)
88 params[:ciphers] ||= "HIGH:!aNULL:!MD5" # ref: nginx 1.0.5
89 params[:verify_depth] ||= 1 if opts[:ssl_client_certificate]
91 if tmp = opts[:ssl_verify_client]
92 opts[:ssl_verify_mode] and
94 ":ssl_verify_mode and :ssl_verify_client are incompatible"
95 params[:verify_mode] = case tmp
97 OpenSSL::SSL::VERIFY_PEER |
98 OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT |
99 OpenSSL::SSL::VERIFY_CLIENT_ONCE
100 when "off", false, :off
101 OpenSSL::SSL::VERIFY_NONE
102 when "optional", :optional
103 OpenSSL::SSL::VERIFY_PEER
106 raise ArgumentError, ":ssl_verify_client=#{tmp} " \
107 "must be one of: 'on', 'off', or 'optional'"
112 if tmp = opts[:ssl_dhparam]
113 dhparam = OpenSSL::PKey::DH.new(IO.read(File.expand_path(tmp)))
114 ctx.tmp_dh_callback = __dhparam_proc(dhparam)
117 opts[:ssl_prefer_server_ciphers] and
118 params[:options] |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE
120 if opts[:ssl_no_compression]
121 # ref: http://redmine.ruby-lang.org/issues/5183
122 if defined?(Kgio::SSL::OP_NO_COMPRESSION)
123 params[:options] |= Kgio::SSL::OP_NO_COMPRESSION
125 warn ":ssl_no_compression not supported per-context with old OpenSSL"
126 warn "disabling OpenSSL compression globally"
127 Kgio::SSL.compression = false
131 # map an array of symbols (in @proto_user)
132 if tmp = opts[:ssl_protocols]
133 proto = Array(tmp).inject(0) do |m,k|
134 flag = @proto_user[k.to_sym] or raise ArgumentError,
135 "#{k.inspect} not one of #{@proto_user.keys.inspect}"
138 op = @proto_map[proto >> 1] and params[:options] |= op
141 ctx.set_params(params)
143 if tmp = opts[:sni_hostnames]
144 sni = opts[:flipper_sni] || Flipper::SNI.new
145 Array(tmp).each { |h| sni.register(h, ctx) }
146 ctx.servername_cb = sni.servername_cb
149 opts.key?(:ssl_session_cache) and
150 __setup_cache(ctx, opts[:ssl_session_cache])
152 Kgio::Monkey!(ctx, monkey_opts)
153 if tmp = opts[:ssl_crl]
154 store = ctx.cert_store or raise ArgumentError, "cert_store required"
160 def self.__dhparam_proc(dhparam) # :nodoc:
164 def self.__setup_cache(ctx, tmp) # :nodoc:
165 cCTX = OpenSSL::SSL::SSLContext
168 ctx.session_cache_mode = cCTX::SESSION_CACHE_OFF
169 when nil # soft off, some mail clients may require it
170 ctx.session_cache_mode = cCTX::SESSION_CACHE_SERVER |
171 cCTX::SESSION_CACHE_NO_AUTO_CLEAR |
172 cCTX::SESSION_CACHE_NO_INTERNAL_STORE
173 ctx.session_cache_size = 1 # 0 is unlimited
175 ctx.session_cache_mode = cCTX::SESSION_CACHE_SERVER
176 tmp = [ tmp ] if String === tmp || (Array === tmp && Symbol === tmp[0])
177 caches = tmp.map { |cache| parse_cache_directive(ctx, cache) }
178 caches.compact! or # compact! returns nil unless builtin:\d+ is present
179 ctx.session_cache_mode |= cCTX::SESSION_CACHE_NO_INTERNAL_STORE
180 if caches.size > 0 && RUBY_VERSION <= "1.9.2"
181 warn("The OpenSSL Ruby extension does not properly support " \
182 "external caches in Ruby <= 1.9.3")
186 %w(session_new_cb session_remove_cb session_get_cb).each do |m|
187 ctx.__send__("#{m}=", caches[0].__send__(m))
190 __assign_caches(ctx, caches)
195 def self.parse_cache_directive(ctx, cache) # :nodoc:
197 when /\Abuiltin:(\d+)\z/
198 ctx.session_cache_size = $1.to_i
199 nil # causes caches.compact! to return the array
201 Flipper::TDBSessionCache.new(nil, $1.to_i)
202 when /\Atdb:(.+):(\d+)\z/
203 Flipper::TDBSessionCache.new($1, $2.to_i)
204 when /\Adalli:\[(.+)\]\z/
205 servers = $1.split(/\s*,\s*/)
207 opts[:expires_in] = ctx.timeout if ctx.timeout && ctx.timeout > 0
208 Flipper::DalliSessionCache.new(servers, opts)
209 when /\Amemcached:\[(.+)\]\z/
210 servers = $1.split(/\s*,\s*/)
212 opts[:default_ttl] = ctx.timeout if ctx.timeout && ctx.timeout > 0
213 Flipper::MemcachedSessionCache.new(servers, opts)
217 servers, opts = cache[1], cache[2] || {}
218 opts[:expires_in] ||= ctx.timeout if ctx.timeout && ctx.timeout > 0
219 Flipper::DalliSessionCache.new(servers, opts)
221 servers, opts = cache[1], cache[2] || {}
222 opts[:default_ttl] ||= ctx.timeout if ctx.timeout && ctx.timeout > 0
223 Flipper::MemcachedSessionCache.new(servers, opts)
225 raise ArgumentError, "unknown cache directive: #{cache.inspect}"
228 raise ArgumentError, "unknown cache directive: #{cache.inspect}"
232 def self.session_get_cb(callbacks) # :nodoc:
234 callbacks.each { |c| sess = c.call(a) and return sess }
238 def self.session_new_cb(callbacks) # :nodoc:
240 callbacks.each { |c| c.call(a) }
244 def self.session_remove_cb(callbacks) # :nodoc:
247 callbacks.each do |c|
255 def self.__assign_caches(ctx, caches) # :nodoc:
256 ctx.session_get_cb = session_get_cb(caches.map { |c| c.session_get_cb })
257 ctx.session_new_cb = session_new_cb(caches.map { |c| c.session_new_cb })
258 caches.map! { |c| c.session_remove_cb }
259 ctx.session_remove_cb = session_remove_cb(caches)
262 require "flipper/base"
263 require "flipper/socket"
264 require "flipper/tcp_proxy"