update mailing list address
[kgio-monkey.git] / lib / flipper.rb
blob92ab881d7d3711bbda1e023fa2336dc4154294f2
1 # -*- encoding: binary -*-
2 require "kgio/monkey"
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
12 # connections.
14 # Most of Flipper is undocumented and the API is subject to change.
15 module Flipper
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 }
28   @proto_map = [
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
38   ]
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
45     params = {
46       :options => OpenSSL::SSL::OP_ALL,
47       :session_id_context => "kgio",
48     }
49     monkey_opts = {
50       # same default ecdh curve as nginx (as of r3961)
51       :ssl_ecdh_curve => opts[:ssl_ecdh_curve] || "prime256v1",
52     }
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)
68       # load names
69       params[:client_ca] = Kgio::SSL.split_pem(OpenSSL::X509::Certificate, tmp)
70     end
72     case tmp = opts[:ssl_verify_mode]
73     when Array
74       params[:verify_mode] = tmp.inject(0) do |mode,f|
75         mode |= OpenSSL::SSL.const_get("VERIFY_#{f.to_s.upcase}")
76       end
77     when String, Symbol
78       params[:verify_mode] = OpenSSL::SSL.const_get("VERIFY_#{tmp.to_s.upcase}")
79     when Integer
80       params[:verify_mode] = tmp
81     end
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)
86     else # server mode:
87       # nginx defaults:
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
93           raise ArgumentError,
94                 ":ssl_verify_mode and :ssl_verify_client are incompatible"
95         params[:verify_mode] = case tmp
96         when "on", true, :on
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
104         else
105           tmp = tmp.inspect
106           raise ArgumentError, ":ssl_verify_client=#{tmp} " \
107                                "must be one of: 'on', 'off', or 'optional'"
108         end
109       end
110     end
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)
115     end
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
124       else
125         warn ":ssl_no_compression not supported per-context with old OpenSSL"
126         warn "disabling OpenSSL compression globally"
127         Kgio::SSL.compression = false
128       end
129     end
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}"
136         m |= flag
137       end
138       op = @proto_map[proto >> 1] and params[:options] |= op
139     end
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
147     end
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"
155       store.add_file(tmp)
156     end
157     ctx
158   end
160   def self.__dhparam_proc(dhparam) # :nodoc:
161     proc { dhparam }
162   end
164   def self.__setup_cache(ctx, tmp) # :nodoc:
165     cCTX = OpenSSL::SSL::SSLContext
166     case tmp
167     when false
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
174     else
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")
183         return
184       end
185       if caches.size == 1
186         %w(session_new_cb session_remove_cb session_get_cb).each do |m|
187           ctx.__send__("#{m}=", caches[0].__send__(m))
188         end
189       else
190         __assign_caches(ctx, caches)
191       end
192     end
193   end
195   def self.parse_cache_directive(ctx, cache) # :nodoc:
196     case cache
197     when /\Abuiltin:(\d+)\z/
198       ctx.session_cache_size = $1.to_i
199       nil # causes caches.compact! to return the array
200     when /\Atdb:(\d+)\z/
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*/)
206       opts = {}
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*/)
211       opts = {}
212       opts[:default_ttl] = ctx.timeout if ctx.timeout && ctx.timeout > 0
213       Flipper::MemcachedSessionCache.new(servers, opts)
214     when Array
215       case cache[0]
216       when :dalli
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)
220       when :memcached
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)
224       else
225         raise ArgumentError, "unknown cache directive: #{cache.inspect}"
226       end
227     else
228       raise ArgumentError, "unknown cache directive: #{cache.inspect}"
229     end
230   end
232   def self.session_get_cb(callbacks) # :nodoc:
233     lambda do |a|
234       callbacks.each { |c| sess = c.call(a) and return sess }
235     end
236   end
238   def self.session_new_cb(callbacks) # :nodoc:
239     lambda do |a|
240       callbacks.each { |c| c.call(a) }
241     end
242   end
244   def self.session_remove_cb(callbacks) # :nodoc:
245     lambda do |a|
246       rv = nil
247       callbacks.each do |c|
248         c = c.call(a)
249         rv ||= c
250       end
251       rv
252     end
253   end
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)
260   end
262 require "flipper/base"
263 require "flipper/socket"
264 require "flipper/tcp_proxy"