patron 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/patron/error.rb CHANGED
@@ -36,6 +36,10 @@ module Patron
36
36
  # Gets raised if the progress callback, or an interrupt, aborts the Curl perform() call
37
37
  class Aborted < Error; end
38
38
 
39
+ # Gets raised when the client certificate or its private key could not be used,
40
+ # for example when the key password is wrong or the certificate file is missing.
41
+ class ClientCertificateError < Error; end
42
+
39
43
  # Gets raised when the server specifies an encoding that could not be found, or has an invalid name,
40
44
  # or when the server "lies" about the encoding of the response body (such as can be the case
41
45
  # when the server specifies an encoding in `Content-Type`) which the HTML generator then overrides
@@ -25,17 +25,17 @@ module Patron
25
25
  :ignore_content_length, :multipart, :action, :timeout, :connect_timeout, :dns_cache_timeout,
26
26
  :max_redirects, :headers, :auth_type, :upload_data, :buffer_size, :cacert,
27
27
  :ssl_version, :http_version, :automatic_content_encoding, :force_ipv4, :download_byte_limit,
28
- :low_speed_time, :low_speed_limit, :progress_callback
28
+ :low_speed_time, :low_speed_limit, :progress_callback, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
29
29
  ]
30
30
 
31
31
  WRITER_VARS = [
32
32
  :url, :username, :password, :file_name, :proxy, :proxy_type, :insecure, :dns_cache_timeout,
33
33
  :ignore_content_length, :multipart, :cacert, :ssl_version, :http_version, :automatic_content_encoding, :force_ipv4, :download_byte_limit,
34
- :low_speed_time, :low_speed_limit, :progress_callback
34
+ :low_speed_time, :low_speed_limit, :progress_callback, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
35
35
  ]
36
36
 
37
- attr_reader *READER_VARS
38
- attr_writer *WRITER_VARS
37
+ attr_reader(*READER_VARS)
38
+ attr_writer(*WRITER_VARS)
39
39
 
40
40
  # Set the type of authentication to use for this request.
41
41
  #
@@ -181,7 +181,8 @@ module Patron
181
181
  def marshal_dump
182
182
  [ @url, @username, @password, @file_name, @proxy, @proxy_type, @insecure,
183
183
  @ignore_content_length, @multipart, @action, @timeout, @connect_timeout,
184
- @max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert ]
184
+ @max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert,
185
+ @ssl_cert, @ssl_cert_type, @ssl_key, @ssl_key_password ]
185
186
  end
186
187
 
187
188
  # Reinstates instance variables from a marshaled representation
@@ -190,7 +191,8 @@ module Patron
190
191
  def marshal_load(data)
191
192
  @url, @username, @password, @file_name, @proxy, @proxy_type, @insecure,
192
193
  @ignore_content_length, @multipart, @action, @timeout, @connect_timeout,
193
- @max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert = data
194
+ @max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert,
195
+ @ssl_cert, @ssl_cert_type, @ssl_key, @ssl_key_password = data
194
196
  end
195
197
 
196
198
  end
@@ -28,6 +28,11 @@ module Patron
28
28
  # to be a valid charset name, just stored. To check the charset for validity, use #body_decodable?
29
29
  attr_reader :charset
30
30
 
31
+ # @return [Hash{Symbol => Float}, nil] curl's cumulative transfer marks in seconds since the request
32
+ # started: `:namelookup`, `:connect`, `:appconnect`, `:pretransfer`, `:starttransfer`, `:redirect`
33
+ # and `:total`. `nil` when the response was not built from a curl transfer (e.g. a stub).
34
+ attr_reader :timings
35
+
31
36
  # Overridden so that the output is shorter and there is no response body printed
32
37
  def inspect
33
38
  # Avoid spamming the console with the header and body data
@@ -67,6 +67,19 @@ module Patron
67
67
  # @return [String] path to the CA file used for certificate verification, or `nil` if CURL default is used
68
68
  attr_accessor :cacert
69
69
 
70
+ # @return [String] path to the client SSL certificate file, or `nil` if no client certificate is used
71
+ attr_accessor :ssl_cert
72
+
73
+ # @return [String] the client SSL certificate file type: "PEM" (the libcurl default), "DER" or "P12" (PKCS#12)
74
+ attr_accessor :ssl_cert_type
75
+
76
+ # @return [String] path to the PEM private key for the client SSL certificate, if it is not bundled with
77
+ # the certificate itself (as it is with PKCS#12)
78
+ attr_accessor :ssl_key
79
+
80
+ # @return [String] the password for the client SSL certificate private key
81
+ attr_accessor :ssl_key_password
82
+
70
83
  # @return [Boolean] whether Content-Range and Content-Length headers should be ignored
71
84
  attr_accessor :ignore_content_length
72
85
 
@@ -103,8 +116,6 @@ module Patron
103
116
  # @see low_speed_time
104
117
  attr_accessor :low_speed_limit
105
118
 
106
- private :handle_request, :add_cookie_file, :set_debug_file
107
-
108
119
  # @return [#call, nil] callable object that will be called with 4 arguments
109
120
  # during request/response execution - `dltotal`, `dlnow`, `ultotal`, `ulnow`.
110
121
  # All these arguments are in bytes.
@@ -146,9 +157,9 @@ module Patron
146
157
  if file_path
147
158
  path = Pathname(file_path).expand_path
148
159
 
149
- if !File.exists?(file_path) && !File.writable?(path.dirname)
160
+ if !File.exist?(file_path) && !File.writable?(path.dirname)
150
161
  raise ArgumentError, "Can't create file #{path} (permission error)"
151
- elsif File.exists?(file_path) && !File.writable?(file_path)
162
+ elsif File.exist?(file_path) && !File.writable?(file_path)
152
163
  raise ArgumentError, "Can't read or write file #{path} (permission error)"
153
164
  end
154
165
  else
@@ -378,6 +389,10 @@ module Patron
378
389
  req.ssl_version = options.fetch :ssl_version, self.ssl_version
379
390
  req.http_version = options.fetch :http_version, self.http_version
380
391
  req.cacert = options.fetch :cacert, self.cacert
392
+ req.ssl_cert = options.fetch :ssl_cert, self.ssl_cert
393
+ req.ssl_cert_type = options.fetch :ssl_cert_type, self.ssl_cert_type
394
+ req.ssl_key = options.fetch :ssl_key, self.ssl_key
395
+ req.ssl_key_password = options.fetch :ssl_key_password, self.ssl_key_password
381
396
  req.ignore_content_length = options.fetch :ignore_content_length, self.ignore_content_length
382
397
  req.buffer_size = options.fetch :buffer_size, self.buffer_size
383
398
  req.download_byte_limit = options.fetch :download_byte_limit, self.download_byte_limit
@@ -1,3 +1,3 @@
1
1
  module Patron
2
- VERSION = "0.13.3"
2
+ VERSION = "0.14.0"
3
3
  end
data/patron.gemspec CHANGED
@@ -6,23 +6,24 @@ require 'patron/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "patron"
8
8
  spec.version = Patron::VERSION
9
+ spec.licenses = ["MIT"]
9
10
  spec.platform = Gem::Platform::RUBY
10
- spec.authors = ["Phillip Toland"]
11
- spec.email = ["phil.toland@gmail.com"]
11
+ spec.authors = ["Aeryn Riley Dowling-Toland"]
12
+ spec.email = ["aeryn.toland@gmail.com"]
12
13
  spec.homepage = "https://github.com/toland/patron"
13
14
  spec.summary = "Patron HTTP Client"
14
15
  spec.description = "Ruby HTTP client library based on libcurl"
15
16
 
16
- # Prevent pushing this gem to RubyGemspec.org. To allow pushes either set the 'allowed_push_host'
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
18
19
  if spec.respond_to?(:metadata)
19
20
  spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
21
  else
21
- raise "RubyGems 2.0 or newer is required to protect against public gem pushespec."
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
23
  end
23
24
 
24
- spec.required_rubygems_version = ">= 1.2.0"
25
- spec.rubyforge_project = "patron"
25
+ spec.required_rubygems_version = ">= 2.5.0"
26
+ spec.required_ruby_version = ">= 2.3"
26
27
 
27
28
  spec.files = `git ls-files -z`.split("\x0")
28
29
  spec.bindir = "exe"
@@ -31,16 +32,8 @@ Gem::Specification.new do |spec|
31
32
  spec.extensions = ["ext/patron/extconf.rb"]
32
33
  spec.post_install_message = %q{
33
34
  Thank you for installing Patron. On OSX, make sure you are using libCURL with OpenSSL.
34
- SecureTransport-based builds might cause crashes in forking environment.
35
+ SecureTransport-based builds might cause crashes in forking environments.
35
36
 
36
37
  For more info see https://github.com/curl/curl/issues/788
37
38
  }
38
- spec.add_development_dependency "rake", "~> 10"
39
- spec.add_development_dependency "bundler"
40
- spec.add_development_dependency "rspec", ">= 2.3.0"
41
- spec.add_development_dependency "simplecov", "~> 0.10"
42
- spec.add_development_dependency "yard", "~> 0.9.11"
43
- spec.add_development_dependency "rack", "~> 1"
44
- spec.add_development_dependency "puma", '~> 3.11'
45
- spec.add_development_dependency "rake-compiler"
46
39
  end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # Regenerates the client certificate fixtures used by the mutual TLS specs
3
+ require 'openssl'
4
+
5
+ DIR = File.expand_path('../spec/support/certs', __dir__)
6
+ PASSWORD = 'patron'
7
+ NOT_BEFORE = Time.utc(2025, 1, 1)
8
+ NOT_AFTER = Time.utc(2045, 1, 1)
9
+
10
+ def name(cn)
11
+ OpenSSL::X509::Name.parse("/C=US/O=Patron Test Certificate/CN=#{cn}")
12
+ end
13
+
14
+ def make_ca(cn)
15
+ key = OpenSSL::PKey::RSA.new(2048)
16
+ cert = OpenSSL::X509::Certificate.new
17
+ cert.version = 2
18
+ cert.serial = 1
19
+ cert.subject = name(cn)
20
+ cert.issuer = cert.subject
21
+ cert.public_key = key.public_key
22
+ cert.not_before = NOT_BEFORE
23
+ cert.not_after = NOT_AFTER
24
+ ef = OpenSSL::X509::ExtensionFactory.new(cert, cert)
25
+ cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true))
26
+ cert.add_extension(ef.create_extension('keyUsage', 'keyCertSign,cRLSign', true))
27
+ cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash'))
28
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
29
+ [cert, key]
30
+ end
31
+
32
+ def make_client(ca, ca_key)
33
+ key = OpenSSL::PKey::RSA.new(2048)
34
+ cert = OpenSSL::X509::Certificate.new
35
+ cert.version = 2
36
+ cert.serial = 2
37
+ cert.subject = name('patron-test-client')
38
+ cert.issuer = ca.subject
39
+ cert.public_key = key.public_key
40
+ cert.not_before = NOT_BEFORE
41
+ cert.not_after = NOT_AFTER
42
+ ef = OpenSSL::X509::ExtensionFactory.new(ca, cert)
43
+ cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', true))
44
+ cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature,keyEncipherment', true))
45
+ cert.add_extension(ef.create_extension('extendedKeyUsage', 'clientAuth'))
46
+ cert.sign(ca_key, OpenSSL::Digest::SHA256.new)
47
+ [cert, key]
48
+ end
49
+
50
+ def write_p12(filename, cert, key)
51
+ p12 = OpenSSL::PKCS12.create(PASSWORD, 'patron-test-client', key, cert, nil, 'PBE-SHA1-3DES', 'PBE-SHA1-3DES')
52
+ File.binwrite(File.join(DIR, filename), p12.to_der)
53
+ end
54
+
55
+ ca, ca_key = make_ca('Patron Test Client CA')
56
+ client, client_key = make_client(ca, ca_key)
57
+ File.write(File.join(DIR, 'client_ca.pem'), ca.to_pem)
58
+ File.write(File.join(DIR, 'client_cert.pem'), client.to_pem)
59
+ File.write(File.join(DIR, 'client_key.pem'), client_key.export(OpenSSL::Cipher.new('aes-256-cbc'), PASSWORD))
60
+ write_p12('client.p12', client, client_key)
61
+
62
+ # Same subject as the real client certificate, but signed by a CA the test server does not trust
63
+ rogue_ca, rogue_ca_key = make_ca('Patron Rogue CA')
64
+ rogue, rogue_key = make_client(rogue_ca, rogue_ca_key)
65
+ write_p12('rogue_client.p12', rogue, rogue_key)
data/spec/request_spec.rb CHANGED
@@ -74,4 +74,16 @@ describe Patron::Request do
74
74
  it "should be able to serialize and deserialize itself" do
75
75
  expect(Marshal.load(Marshal.dump(@request))).to eql(@request)
76
76
  end
77
+
78
+ it "should preserve the client certificate settings when serialized" do
79
+ @request.ssl_cert = "client.pem"
80
+ @request.ssl_cert_type = "PEM"
81
+ @request.ssl_key = "client_key.pem"
82
+ @request.ssl_key_password = "secret"
83
+ restored = Marshal.load(Marshal.dump(@request))
84
+ expect(restored.ssl_cert).to eq("client.pem")
85
+ expect(restored.ssl_cert_type).to eq("PEM")
86
+ expect(restored.ssl_key).to eq("client_key.pem")
87
+ expect(restored.ssl_key_password).to eq("secret")
88
+ end
77
89
  end