File: ssl_socket.rb

package info (click to toggle)
ruby-excon 0.112.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,232 kB
  • sloc: ruby: 7,855; makefile: 5
file content (212 lines) | stat: -rw-r--r-- 7,858 bytes parent folder | download
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# frozen_string_literal: true
module Excon
  class SSLSocket < Socket
    HAVE_NONBLOCK = [:connect_nonblock, :read_nonblock, :write_nonblock].all? do |m|
      OpenSSL::SSL::SSLSocket.public_method_defined?(m)
    end

    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

    private

    def client_cert_data
      @client_cert_data ||= if (ccd = @data[:client_cert_data])
                              ccd
                            elsif (path = @data[:client_cert])
                              File.read path
                            elsif (path = @data[:certificate_path])
                              warn ":certificate_path is no longer supported and will be deprecated. Please use :client_cert or :client_cert_data"
                              File.read path
                            end
    end

    def client_chain_data
      @client_chain_data ||= if (ccd = @data[:client_chain_data])
                               ccd
                             elsif (path = @data[:client_chain])
                               File.read path
                             end
    end

    def connect
      # backwards compatability for things lacking nonblock
      @nonblock = HAVE_NONBLOCK && @nonblock
      super
    end

    def client_key_data
      @client_key_data ||= if (ckd = @data[:client_key_data])
                             ckd
                           elsif (path = @data[:client_key])
                             File.read path
                           elsif (path = @data[:private_key_path])
                             warn ":private_key_path is no longer supported and will be deprecated. Please use :client_key or :client_key_data"
                             File.read path
                           end
    end

    def client_key_pass
      @data[:client_key_pass] || @data[:private_key_pass]
    end

  end
end