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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +79 -0
- data/.travis.yml +10 -7
- data/CHANGELOG.md +32 -1
- data/Gemfile +15 -1
- data/README.md +80 -30
- data/ext/patron/extconf.rb +7 -4
- data/ext/patron/session_ext.c +360 -155
- data/lib/patron/error.rb +4 -0
- data/lib/patron/request.rb +8 -6
- data/lib/patron/response.rb +5 -0
- data/lib/patron/session.rb +19 -4
- data/lib/patron/version.rb +1 -1
- data/patron.gemspec +8 -15
- data/script/generate_client_certs +65 -0
- data/spec/request_spec.rb +12 -0
- data/spec/session_spec.rb +287 -67
- data/spec/session_ssl_spec.rb +163 -27
- data/spec/spec_helper.rb +16 -7
- data/spec/support/certs/client.p12 +0 -0
- data/spec/support/certs/client_ca.pem +20 -0
- data/spec/support/certs/client_cert.pem +20 -0
- data/spec/support/certs/client_key.pem +30 -0
- data/spec/support/certs/rogue_client.p12 +0 -0
- data/spec/support/config.ru +18 -3
- data/spec/support/fork.rb +70 -0
- data/spec/support/raw_request_capture.rb +93 -0
- data/spec/support/test_server.rb +6 -3
- data/spec/support/valgrind.supp +2 -0
- metadata +22 -125
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
|
data/lib/patron/request.rb
CHANGED
|
@@ -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
|
|
38
|
-
attr_writer
|
|
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
|
|
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
|
data/lib/patron/response.rb
CHANGED
|
@@ -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
|
data/lib/patron/session.rb
CHANGED
|
@@ -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.
|
|
160
|
+
if !File.exist?(file_path) && !File.writable?(path.dirname)
|
|
150
161
|
raise ArgumentError, "Can't create file #{path} (permission error)"
|
|
151
|
-
elsif File.
|
|
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
|
data/lib/patron/version.rb
CHANGED
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 = ["
|
|
11
|
-
spec.email = ["
|
|
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
|
|
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
|
|
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 = ">=
|
|
25
|
-
spec.
|
|
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
|
|
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
|