1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
|
# frozen_string_literal: true
require "openssl"
module HTTPX
TLSError = OpenSSL::SSL::SSLError
class SSL < TCP
# rubocop:disable Style/MutableConstant
TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
# https://github.com/jruby/jruby-openssl/issues/284
# TODO: remove when dropping support for jruby-openssl < 0.15.4
TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby" && JOpenSSL::VERSION < "0.15.4"
# rubocop:enable Style/MutableConstant
TLS_OPTIONS.freeze
attr_writer :ssl_session
def initialize(_, _, options)
super
@ssl_session = nil
ctx_options = TLS_OPTIONS.merge(options.ssl)
@sni_hostname = ctx_options.delete(:hostname) || @hostname
if @keep_open && @io.is_a?(OpenSSL::SSL::SSLSocket)
# externally initiated ssl socket
@ctx = @io.context
@state = :negotiated
else
@ctx = OpenSSL::SSL::SSLContext.new
@ctx.set_params(ctx_options) unless ctx_options.empty?
unless @ctx.session_cache_mode.nil? # a dummy method on JRuby
@ctx.session_cache_mode =
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
end
yield(self) if block_given?
end
@verify_hostname = @ctx.verify_hostname
end
if OpenSSL::SSL::SSLContext.method_defined?(:session_new_cb=)
def session_new_cb(&pr)
@ctx.session_new_cb = proc { |_, sess| pr.call(sess) }
end
else
# session_new_cb not implemented under JRuby
def session_new_cb; end
end
def protocol
@io.alpn_protocol || super
rescue StandardError
super
end
if RUBY_ENGINE == "jruby"
# in jruby, alpn_protocol may return ""
# https://github.com/jruby/jruby-openssl/issues/287
def protocol
proto = @io.alpn_protocol
return super if proto.nil? || proto.empty?
proto
rescue StandardError
super
end
end
def can_verify_peer?
@ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
end
def verify_hostname(host)
return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
OpenSSL::SSL.verify_certificate_identity(@io.peer_cert, host)
end
def connected?
@state == :negotiated
end
def ssl_session_expired?
@ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (@ssl_session.time.to_f + @ssl_session.timeout)
end
def connect
return if @state == :negotiated
unless @state == :connected
super
return unless @state == :connected
end
unless @io.is_a?(OpenSSL::SSL::SSLSocket)
if (hostname_is_ip = (@ip == @sni_hostname)) && @ctx.verify_hostname
# IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
@sni_hostname = @ip.to_string
# IP addresses in SNI is not valid per RFC 6066, section 3.
@ctx.verify_hostname = false
end
@io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
@io.hostname = @sni_hostname unless hostname_is_ip
@io.session = @ssl_session unless ssl_session_expired?
@io.sync_close = true
end
try_ssl_connect
end
def try_ssl_connect
ret = @io.connect_nonblock(exception: false)
log(level: 3, color: :cyan) { "TLS CONNECT: #{ret}..." }
case ret
when :wait_readable
@interests = :r
return
when :wait_writable
@interests = :w
return
end
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
transition(:negotiated)
@interests = :w
end
private
def transition(nextstate)
case nextstate
when :negotiated
return unless @state == :connected
when :closed
return unless @state == :negotiated ||
@state == :connected
end
do_transition(nextstate)
end
def log_transition_state(nextstate)
return super unless nextstate == :negotiated
server_cert = @io.peer_cert
"#{super}\n\n" \
"SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
"ALPN, server accepted to use #{protocol}\n" \
"Server certificate:\n " \
"subject: #{server_cert.subject}\n " \
"start date: #{server_cert.not_before}\n " \
"expire date: #{server_cert.not_after}\n " \
"issuer: #{server_cert.issuer}\n " \
"SSL certificate verify ok."
end
end
end
|