Class: Excon::SSLSocket

Inherits:
Socket
  • Object
show all
Defined in:
lib/excon/ssl_socket.rb

Constant Summary collapse

HAVE_NONBLOCK =
[:connect_nonblock, :read_nonblock, :write_nonblock].all? do |m|
  OpenSSL::SSL::SSLSocket.public_method_defined?(m)
end

Constants inherited from Socket

Excon::Socket::CONNECT_RETRY_EXCEPTION_CLASSES, Excon::Socket::OPERATION_TO_TIMEOUT, Excon::Socket::READ_RETRY_EXCEPTION_CLASSES, Excon::Socket::WRITE_RETRY_EXCEPTION_CLASSES

Constants included from Utils

Utils::CONTROL, Utils::DELIMS, Utils::ESCAPED, Utils::NONASCII, Utils::UNESCAPED, Utils::UNWISE

Instance Attribute Summary

Attributes inherited from Socket

#data, #remote_ip

Instance Method Summary collapse

Methods inherited from Socket

#local_address, #local_port, #params, #params=, #read, #readline, #write

Methods included from Utils

#binary_encode, #connection_uri, #escape_uri, #headers_hash_to_s, #port_string, #query_string, #redact, #request_uri, #split_header_value, #unescape_form, #unescape_uri

Constructor Details

#initialize(data = {}) ⇒ SSLSocket

Returns a new instance of SSLSocket.



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
164
165
166
167
# File 'lib/excon/ssl_socket.rb', line 8

def initialize(data = {})
  @port = data[:port] || 443
  super

  # create ssl context
  ssl_context = OpenSSL::SSL::SSLContext.new

  # set the security level before setting other parameters affected by it
  if @data[:ssl_security_level]
    ssl_context.security_level = @data[:ssl_security_level]
  end

  # disable less secure options, when supported
  ssl_context_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
  if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS)
    ssl_context_options &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
  end

  if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
    ssl_context_options |= OpenSSL::SSL::OP_NO_COMPRESSION
  end
  ssl_context.options = ssl_context_options

  ssl_context.ciphers = @data[:ciphers]
  if @data[:ssl_version]
    ssl_context.ssl_version = @data[:ssl_version]
  end
  if @data[:ssl_min_version]
    ssl_context.min_version = @data[:ssl_min_version]
  end
  if @data[:ssl_max_version]
    ssl_context.max_version = @data[:ssl_max_version]
  end


  if @data[:ssl_verify_peer]
    # turn verification on
    ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER

    if (ca_file = @data[:ssl_ca_file] || ENV['SSL_CERT_FILE'])
      ssl_context.ca_file = ca_file
    end
    if (ca_path = @data[:ssl_ca_path] || ENV['SSL_CERT_DIR'])
      ssl_context.ca_path = ca_path
    end
    if (cert_store = @data[:ssl_cert_store])
      ssl_context.cert_store = cert_store
    end

    if cert_store.nil?
      ssl_context.cert_store = OpenSSL::X509::Store.new
      ssl_context.cert_store.set_default_paths
    end

    # no defaults, fallback to bundled
    unless ca_file || ca_path || cert_store
      # workaround issue #257 (JRUBY-6970)
      ca_file = DEFAULT_CA_FILE
      ca_file = ca_file.gsub(/^jar:/, '') if ca_file =~ /^jar:file:\//

      begin
        ssl_context.cert_store.add_file(ca_file)
      rescue
        Excon.display_warning("Excon unable to add file to cert store, ignoring: #{ca_file}\n[#{$!.class}] #{$!.message}")
      end
    end

    if (verify_callback = @data[:ssl_verify_callback])
      ssl_context.verify_callback = verify_callback
    end
  else
    # turn verification off
    ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end

  # Verify certificate hostname if supported (ruby >= 2.4.0)
  ssl_context.verify_hostname = @data[:ssl_verify_hostname] if ssl_context.respond_to?(:verify_hostname=)

  if client_cert_data && client_key_data
    ssl_context.cert = OpenSSL::X509::Certificate.new client_cert_data
    if OpenSSL::PKey.respond_to? :read
      ssl_context.key = OpenSSL::PKey.read(client_key_data, client_key_pass)
    else
      ssl_context.key = OpenSSL::PKey::RSA.new(client_key_data, client_key_pass)
    end
    if client_chain_data && OpenSSL::X509::Certificate.respond_to?(:load)
      ssl_context.extra_chain_cert = OpenSSL::X509::Certificate.load(client_chain_data)
    elsif client_chain_data
      certs = client_chain_data.scan(/-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----/)
      ssl_context.extra_chain_cert = certs.map do |cert|
        OpenSSL::X509::Certificate.new(cert)
      end
    end
  elsif @data.key?(:certificate) && @data.key?(:private_key)
    ssl_context.cert = OpenSSL::X509::Certificate.new(@data[:certificate])
    if OpenSSL::PKey.respond_to? :read
      ssl_context.key = OpenSSL::PKey.read(@data[:private_key], client_key_pass)
    else
      ssl_context.key = OpenSSL::PKey::RSA.new(@data[:private_key], client_key_pass)
    end
  end

  if @data[:proxy]
    request = "CONNECT #{@data[:host]}#{port_string(@data.merge(:omit_default_port => false))}#{Excon::HTTP_1_1}" +
              "Host: #{@data[:host]}#{port_string(@data)}#{Excon::CR_NL}"

    if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
      user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
      auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
      request += "Proxy-Authorization: Basic #{auth}#{Excon::CR_NL}"
    end

    request += "Proxy-Connection: Keep-Alive#{Excon::CR_NL}"

    if @data[:ssl_proxy_headers]
      request << Utils.headers_hash_to_s(@data[:ssl_proxy_headers])
    end

    request += Excon::CR_NL

    # write out the proxy setup request
    @socket.write(request)

    # eat the proxy's connection response
    response = Excon::Response.parse(self,  :expects => 200, :method => 'CONNECT')
    if response[:response][:status] != 200
      raise(Excon::Errors::ProxyConnectionError.new("proxy connection could not be established", request, response))
    end
  end

  # convert Socket to OpenSSL::SSL::SSLSocket
  @socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context)
  @socket.sync_close = true

  # Server Name Indication (SNI) RFC 3546
  if @socket.respond_to?(:hostname=)
    @socket.hostname = @data[:ssl_verify_peer_host] || @data[:host]
  end

  begin
    if @nonblock
      begin
        @socket.connect_nonblock
      rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
        select_with_timeout(@socket, :connect_read) && retry
      rescue IO::WaitWritable
        select_with_timeout(@socket, :connect_write) && retry
      end
    else
      @socket.connect
    end
  rescue Errno::ETIMEDOUT, Timeout::Error
    raise Excon::Errors::Timeout.new('connect timeout reached')
  end

  # verify connection
  if @data[:ssl_verify_peer]
    @socket.post_connection_check(@data[:ssl_verify_peer_host] || @data[:host])
  end
end