@@ -147,7 +147,30 @@ def self.SSL_CTX_sess_set_cache_size(ssl_ctx, op)
147147 attach_function :SSL_CTX_set_cipher_list , [ :ssl_ctx , :string ] , :int
148148 attach_function :SSL_CTX_set_session_id_context , [ :ssl_ctx , :string , :buffer_length ] , :int
149149
150- # TODO:: SSL_CTX_set_alpn_protos
150+ # OpenSSL before 1.0.2 do not have these methods
151+ begin
152+ attach_function :SSL_CTX_set_alpn_protos , [ :ssl_ctx , :string , :uint ] , :int
153+
154+ SSL_TLSEXT_ERR_OK = 0
155+ SSL_TLSEXT_ERR_ALERT_WARNING = 1
156+ SSL_TLSEXT_ERR_ALERT_FATAL = 2
157+ SSL_TLSEXT_ERR_NOACK = 3
158+
159+ OPENSSL_NPN_UNSUPPORTED = 0
160+ OPENSSL_NPN_NEGOTIATED = 1
161+ OPENSSL_NPN_NO_OVERLAP = 2
162+
163+ attach_function :SSL_select_next_proto , [ :pointer , :pointer , :string , :uint , :string , :uint ] , :int
164+
165+ # array of str, unit8 out,uint8 in, *arg
166+ callback :alpn_select_cb , [ :ssl , :pointer , :pointer , :string , :uint , :pointer ] , :int
167+ attach_function :SSL_CTX_set_alpn_select_cb , [ :ssl_ctx , :alpn_select_cb , :pointer ] , :void
168+
169+ attach_function :SSL_get0_alpn_selected , [ :ssl , :pointer , :pointer ] , :void
170+ ALPN_SUPPORTED = true
171+ rescue FFI ::NotFoundError
172+ ALPN_SUPPORTED = false
173+ end
151174
152175
153176 # Deconstructor
@@ -201,19 +224,23 @@ def self.SSL_CTX_sess_set_cache_size(ssl_ctx, op)
201224 8
202225 end
203226
204- CRYPTO_LOCK = 0x1
205- LockingCB = FFI ::Function . new ( :void , [ :int , :int , :string , :int ] ) do |mode , type , file , line |
206- if ( mode & CRYPTO_LOCK ) != 0
207- SSL_LOCKS [ type ] . lock
208- else
209- # Unlock a lock
210- SSL_LOCKS [ type ] . unlock
211- end
212- end
227+ # Locking isn't provided as long as all writes are done on the same thread.
228+ # This is my main use case. Happy to enable it if someone requires it and can
229+ # get it to work on MRI Ruby (Currently only works on JRuby and Rubinius)
230+ # as MRI callbacks occur on a thread pool?
213231
214- ThreadIdCB = FFI ::Function . new ( :ulong , [ ] ) do
215- Thread . current . object_id
216- end
232+ #CRYPTO_LOCK = 0x1
233+ #LockingCB = FFI::Function.new(:void, [:int, :int, :string, :int]) do |mode, type, file, line|
234+ # if (mode & CRYPTO_LOCK) != 0
235+ # SSL_LOCKS[type].lock
236+ # else
237+ # Unlock a lock
238+ # SSL_LOCKS[type].unlock
239+ # end
240+ #end
241+ #ThreadIdCB = FFI::Function.new(:ulong, []) do
242+ # Thread.current.object_id
243+ #end
217244
218245
219246 # INIT CODE
@@ -262,6 +289,29 @@ class Context
262289 CIPHERS = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!CAMELLIA:@STRENGTH' . freeze
263290 SESSION = 'ruby-tls' . freeze
264291
292+
293+ ALPN_LOOKUP = ThreadSafe ::Cache . new
294+ ALPN_Select_CB = FFI ::Function . new ( :int , [
295+ # array of str, unit8 out,uint8 in, *arg
296+ :pointer , :pointer , :pointer , :string , :uint , :pointer
297+ ] ) do |ssl_p , out , outlen , inp , inlen , arg |
298+ ssl = Box ::InstanceLookup [ ssl_p . address ]
299+ protos = ssl . context . alpn_str
300+
301+ status = SSL . SSL_select_next_proto ( out , outlen , protos , protos . length , inp , inlen )
302+
303+ ssl . negotiated
304+
305+ case status
306+ when SSL ::OPENSSL_NPN_UNSUPPORTED
307+ SSL ::SSL_TLSEXT_ERR_ALERT_FATAL
308+ when SSL ::OPENSSL_NPN_NEGOTIATED
309+ SSL ::SSL_TLSEXT_ERR_OK
310+ when SSL ::OPENSSL_NPN_NO_OVERLAP
311+ SSL ::SSL_TLSEXT_ERR_ALERT_WARNING
312+ end
313+ end
314+
265315 def initialize ( server , options = { } )
266316 @is_server = server
267317 @ssl_ctx = SSL . SSL_CTX_new ( server ? SSL . SSLv23_server_method : SSL . SSLv23_client_method )
@@ -274,16 +324,27 @@ def initialize(server, options = {})
274324 end
275325
276326 SSL . SSL_CTX_set_cipher_list ( @ssl_ctx , options [ :ciphers ] || CIPHERS )
327+ @alpn_set = false
277328
278329 if @is_server
279330 SSL . SSL_CTX_sess_set_cache_size ( @ssl_ctx , 128 )
280331 SSL . SSL_CTX_set_session_id_context ( @ssl_ctx , SESSION , 8 )
332+
333+ if SSL ::ALPN_SUPPORTED && options [ :protocols ]
334+ @alpn_str = Context . build_alpn_string ( options [ :protocols ] )
335+ SSL . SSL_CTX_set_alpn_select_cb ( @ssl_ctx , ALPN_Select_CB , nil )
336+ @alpn_set = true
337+ end
281338 else
282339 set_private_key ( options [ :private_key ] )
283340 set_certificate ( options [ :cert_chain ] )
284- end
285341
286- # TODO:: Check for ALPN support
342+ # Check for ALPN support
343+ if SSL ::ALPN_SUPPORTED && options [ :protocols ]
344+ protocols = Context . build_alpn_string ( options [ :protocols ] )
345+ @alpn_set = SSL . SSL_CTX_set_alpn_protos ( @ssl_ctx , protocols , protocols . length ) == 0
346+ end
347+ end
287348 end
288349
289350 def cleanup
@@ -295,11 +356,23 @@ def cleanup
295356
296357 attr_reader :is_server
297358 attr_reader :ssl_ctx
359+ attr_reader :alpn_set
360+ attr_reader :alpn_str
298361
299362
300363 private
301364
302365
366+ def self . build_alpn_string ( protos )
367+ protocols = '' . force_encoding ( 'ASCII-8BIT' )
368+ protos . each do |prot |
369+ protocol = prot . to_s
370+ protocols << protocol . length
371+ protocols << protocol
372+ end
373+ protocols
374+ end
375+
303376 def set_private_key ( key )
304377 err = if key . is_a? FFI ::Pointer
305378 SSL . SSL_CTX_use_PrivateKey ( @ssl_ctx , key )
@@ -338,6 +411,8 @@ def set_certificate(cert)
338411
339412
340413 class Box
414+ InstanceLookup = ThreadSafe ::Cache . new
415+
341416 READ_BUFFER = 2048
342417
343418 SSL_VERIFY_PEER = 0x01
@@ -347,6 +422,7 @@ def initialize(server, transport, options = {})
347422
348423 @handshake_completed = false
349424 @handshake_signaled = false
425+ @negotiated = false
350426 @transport = transport
351427
352428 @read_buffer = FFI ::MemoryPointer . new ( :char , READ_BUFFER , false )
@@ -360,11 +436,9 @@ def initialize(server, transport, options = {})
360436
361437 @write_queue = [ ]
362438
363- # TODO:: if server && options[:alpn_string]
364- # SSL_CTX_set_alpn_select_cb
365-
366439 InstanceLookup [ @ssl . address ] = self
367440
441+ @alpn_fallback = options [ :fallback ]
368442 if options [ :verify_peer ]
369443 SSL . SSL_set_verify ( @ssl , SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE , VerifyCB )
370444 end
@@ -374,6 +448,7 @@ def initialize(server, transport, options = {})
374448
375449
376450 attr_reader :is_server
451+ attr_reader :context
377452 attr_reader :handshake_completed
378453
379454
@@ -382,6 +457,22 @@ def get_peer_cert
382457 SSL . SSL_get_peer_certificate ( @ssl )
383458 end
384459
460+ def negotiated_protocol
461+ return nil unless @context . alpn_set
462+
463+ proto = FFI ::MemoryPointer . new ( :pointer , 1 , true )
464+ len = FFI ::MemoryPointer . new ( :uint , 1 , true )
465+ SSL . SSL_get0_alpn_selected ( @ssl , proto , len )
466+
467+ resp = proto . get_pointer ( 0 )
468+ if resp . address == 0
469+ :failed
470+ else
471+ length = len . get_uint ( 0 )
472+ resp . read_string ( length ) . to_sym
473+ end
474+ end
475+
385476 def start
386477 return unless @ready
387478
@@ -435,7 +526,30 @@ def decrypt(data)
435526
436527 def signal_handshake
437528 @handshake_signaled = true
438- @transport . handshake_cb
529+
530+ # Check protocol support here
531+ if @context . alpn_set
532+ proto = negotiated_protocol
533+
534+ if proto == :failed
535+ if @negotiated
536+ # We should shutdown if this is the case
537+ @transport . close_cb
538+ return
539+ elsif @alpn_fallback
540+ # Client or Server with a client that doesn't support ALPN
541+ proto = @alpn_fallback . to_sym
542+ end
543+ end
544+ else
545+ proto = nil
546+ end
547+
548+ @transport . handshake_cb ( proto )
549+ end
550+
551+ def negotiated
552+ @negotiated = true
439553 end
440554
441555 SSL_RECEIVED_SHUTDOWN = 2
@@ -474,8 +588,6 @@ def get_plain_text(buffer, ready)
474588 end
475589 end
476590
477-
478- InstanceLookup = ThreadSafe ::Cache . new
479591 VerifyCB = FFI ::Function . new ( :int , [ :int , :pointer ] ) do |preverify_ok , x509_store |
480592 x509 = SSL . X509_STORE_CTX_get_current_cert ( x509_store )
481593 ssl = SSL . X509_STORE_CTX_get_ex_data ( x509_store , SSL . SSL_get_ex_data_X509_STORE_CTX_idx )
0 commit comments