patron 0.14.0 → 1.0.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +83 -49
  4. data/ext/patron/extconf.rb +54 -23
  5. data/ext/patron/session_ext.c +1044 -923
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +21 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -144
  10. data/lib/patron/response.rb +28 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +114 -108
  13. data/lib/patron/util.rb +14 -12
  14. data/lib/patron/version.rb +3 -1
  15. data/lib/patron.rb +9 -15
  16. metadata +12 -58
  17. data/.autotest +0 -15
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -79
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -0
  22. data/.travis.yml +0 -13
  23. data/.yardopts +0 -1
  24. data/Gemfile +0 -18
  25. data/Rakefile +0 -31
  26. data/ext/patron/.gitignore +0 -4
  27. data/ext/patron/membuffer.c +0 -85
  28. data/ext/patron/membuffer.h +0 -78
  29. data/ext/patron/sglib.h +0 -1952
  30. data/lib/patron/proxy_type.rb +0 -11
  31. data/patron.gemspec +0 -39
  32. data/pic.png +0 -0
  33. data/script/console +0 -11
  34. data/script/generate_client_certs +0 -65
  35. data/script/test_server +0 -7
  36. data/spec/header_parser_spec.rb +0 -101
  37. data/spec/patron_spec.rb +0 -28
  38. data/spec/request_spec.rb +0 -89
  39. data/spec/response_spec.rb +0 -145
  40. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  41. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  42. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  43. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  44. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  45. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  46. data/spec/session_spec.rb +0 -865
  47. data/spec/session_ssl_spec.rb +0 -431
  48. data/spec/spec_helper.rb +0 -40
  49. data/spec/support/certs/cacert.pem +0 -36
  50. data/spec/support/certs/client.p12 +0 -0
  51. data/spec/support/certs/client_ca.pem +0 -20
  52. data/spec/support/certs/client_cert.pem +0 -20
  53. data/spec/support/certs/client_key.pem +0 -30
  54. data/spec/support/certs/privkey.pem +0 -51
  55. data/spec/support/certs/rogue_client.p12 +0 -0
  56. data/spec/support/config.ru +0 -153
  57. data/spec/support/fork.rb +0 -70
  58. data/spec/support/raw_request_capture.rb +0 -93
  59. data/spec/support/test_server.rb +0 -29
  60. data/spec/support/valgrind.supp +0 -2
  61. data/spec/util_spec.rb +0 -71
@@ -1,153 +0,0 @@
1
- require 'yaml'
2
- require 'ostruct'
3
- require 'zlib'
4
-
5
- ## HTTP test server for integration tests
6
-
7
- Readback = Proc.new {|env|
8
- # The Patron test suite is originally written to enable the following
9
- # testing pattern:
10
- # * Patron does a request
11
- # * The request gets captured by Webrick
12
- # * Webrick then takes it's Request object and YAMLs it wholesale (body, headers and all)
13
- # * ...and sends it back to the client
14
- # * The test suite then de-YAMLs the request object from the response body and does assertions on it
15
- #
16
- # The easiest way to fake it, preserving the entire test suite intact, is to simulate a Webrick
17
- # request object using an ostruct. Of note is the following:
18
- #
19
- # 1) Webrick returns headers in arrays by default, accounting for repeats like with Cookie
20
- # 2) Webrick does not convert headers to ENV_VARS_FOR_FCI it only downcases them. We have to match that.
21
- req = Rack::Request.new(env)
22
- req_headers = env.to_a.select do |(k,v)|
23
- k.is_a?(String) && v.is_a?(String)
24
- end.map do |(k,v)|
25
- [k.downcase.gsub(/^http_/, '').gsub(/_/o, '-'), [v]]
26
- end
27
-
28
- fake_webrick_request_object = OpenStruct.new({
29
- :fullpath => req.fullpath,
30
- :path => req.path,
31
- :query_string => req.query_string,
32
- :request_method => req.request_method.to_s,
33
- :header => Hash[req_headers],
34
- :body => env['rack.input'].read,
35
- })
36
-
37
- body_str = fake_webrick_request_object.to_yaml
38
- [200, {'Content-Type' => 'text/plain', 'Content-Length' => body_str.bytesize.to_s}, [body_str]]
39
- }
40
-
41
- GzipServlet = Proc.new {|env|
42
- raise "Need to have the right Accept-Encoding: header" unless env['HTTP_ACCEPT_ENCODING']
43
-
44
- body = []
45
- content_length = 0
46
- z = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION)
47
- 1024.times do
48
- chunk = z.deflate('Some highly compressible data')
49
- content_length += chunk.bytesize
50
- body << chunk
51
- end
52
- fin = z.finish.to_s
53
- body << fin
54
- content_length += fin.bytesize
55
- z.close
56
-
57
- [200, {'Content-Encoding' => 'deflate', 'Content-Length' => content_length.to_s, 'Vary' => 'Accept-Encoding'}, body]
58
- }
59
-
60
- TimeoutServlet = Proc.new {|env|
61
- query_vars = Rack::Utils.parse_nested_query(env.fetch('QUERY_STRING'))
62
- query_millis = query_vars.fetch('millis').to_i
63
- sleep(query_millis / 1000.0)
64
- [200, {'Content-Type' => 'text/plain'}, ['That took a while']]
65
- }
66
-
67
- SlowServlet = Proc.new {|env|
68
- body = Enumerator.new do |y|
69
- y.yield 'x'
70
- sleep 5
71
- y.yield 'rest of body'
72
- end
73
- [200, {'Content-Type' => 'text/plain'}, body]
74
- }
75
-
76
- RedirectServlet = Proc.new {|env|
77
- url_scheme = env.fetch('rack.url_scheme')
78
- port = env.fetch('SERVER_PORT')
79
- [301, {'Location' => "#{url_scheme}://localhost:#{port}/test"}, []]
80
- }
81
-
82
- EvilRedirectServlet = Proc.new {|env|
83
- [301, {'Location' => "smtp://mailbox:secret@localhost"}, []]
84
- }
85
-
86
- BodyReadback = Proc.new {|env|
87
- readback = {'method' => env['REQUEST_METHOD'], 'body' => env['rack.input'].read, 'content_type' => env.fetch('CONTENT_TYPE')}
88
- [200, {'Content-Type' => 'text/plain'}, [OpenStruct.new(readback).to_yaml]]
89
- }
90
-
91
- TestPatchBodyServlet = BodyReadback
92
-
93
- SetCookieServlet = Proc.new {|env|
94
- [301, {'Set-Cookie' => 'session_id=foo123', 'Location' => 'http://localhost:9001/test'}, []]
95
- }
96
-
97
- RepetitiveHeaderServlet = Proc.new {|env|
98
- # The values of the header must be Strings,
99
- # consisting of lines (for multiple header values, e.g. multiple
100
- # <tt>Set-Cookie</tt> values) separated by "\\n".
101
- [200, {'Set-Cookie' => "a=1\nb=2", 'Content-Type' => 'text/plain'}, ['Hi.']]
102
- }
103
-
104
- PictureServlet = Proc.new {|env|
105
- # Rack::File allows us to test Range support as well
106
- env_with_adjusted_path = env.merge('PATH_INFO' => Rack::Utils.escape('/pic.png'))
107
- Rack::File.new('./').call(env_with_adjusted_path)
108
- }
109
-
110
- RedirectToPictureServlet = Proc.new {|env|
111
- [307, {'Location' => '/picture'}, []]
112
- }
113
-
114
- WrongContentLengthServlet = Proc.new {|env|
115
- [200, {'Content-Length' => '1024', 'Content-Type' => 'text/plain'}, ['Hello.']]
116
- }
117
-
118
- # Serves a substantial amount of data
119
- LargeServlet = Proc.new {|env|
120
- rng = Random.new
121
- len = 15 * 1024 * 1024
122
- body = Enumerator.new do |y|
123
- 15.times do
124
- y.yield(rng.bytes(1024 * 1024))
125
- end
126
- end
127
- [200, {'Content-Type' => 'binary/octet-stream', 'Content-Length' => len.to_s}, body]
128
- }
129
-
130
- # Echoes the subject of the client certificate Puma verified, if any
131
- PeerCertServlet = Proc.new {|env|
132
- subject = env['puma.peercert'] ? env['puma.peercert'].subject.to_s : 'none'
133
- [200, {'Content-Type' => 'text/plain', 'Content-Length' => subject.bytesize.to_s}, [subject]]
134
- }
135
-
136
- run Rack::URLMap.new({
137
- "/" => Proc.new {|env| [200, {'Content-Length' => '2'}, ['Welcome']]},
138
- "/test" => Readback,
139
- "/testpost" => BodyReadback,
140
- "/testpatch" => BodyReadback,
141
- "/timeout" => TimeoutServlet,
142
- "/slow" => SlowServlet,
143
- "/redirect" => RedirectServlet,
144
- "/evil-redirect" => EvilRedirectServlet,
145
- "/picture" => PictureServlet,
146
- "/redirect-to-picture" => RedirectToPictureServlet,
147
- "/very-large" => LargeServlet,
148
- "/setcookie" => SetCookieServlet,
149
- "/repetitiveheader" => RepetitiveHeaderServlet,
150
- "/wrongcontentlength" => WrongContentLengthServlet,
151
- "/gzip-compressed" => GzipServlet,
152
- "/peercert" => PeerCertServlet,
153
- })
data/spec/support/fork.rb DELETED
@@ -1,70 +0,0 @@
1
- require 'thread'
2
-
3
- class Fork
4
- def initialize(&block)
5
- @mu = Mutex.new
6
- @cond = ConditionVariable.new
7
- @pstate = nil
8
- @pid = Process.fork(&block)
9
- @killed = false
10
-
11
- # Start monitoring the PID.
12
- Thread.new { monitor }
13
-
14
- # Kill the process anyway when the program exits.
15
- ppid = Process.pid
16
- at_exit do
17
- if ppid == Process.pid # Make sure we are not inside another fork spawned by rspec example.
18
- do_kill("KILL")
19
- end
20
- end
21
- end
22
-
23
- # Wait for process to exit.
24
- def wait(timeout = nil)
25
- @mu.synchronize do
26
- next @pstate unless @pstate.nil?
27
-
28
- @cond.wait(@mu, timeout)
29
- @pstate
30
- end
31
- end
32
-
33
- # Signal the process.
34
- def kill(sig)
35
- already_killed = @mu.synchronize do
36
- old = @killed
37
- @killed = true
38
- old
39
- end
40
- signaled = do_kill(sig)
41
- Thread.new { reaper } if signaled && !already_killed
42
- signaled
43
- end
44
-
45
- private
46
-
47
- # Signal the process.
48
- def do_kill(sig)
49
- Process.kill(sig, @pid)
50
- true
51
- rescue Errno::ESRCH # No such process
52
- false
53
- end
54
-
55
- # Monitor the process state.
56
- def monitor
57
- _, pstate = Process.wait2(@pid)
58
-
59
- @mu.synchronize do
60
- @pstate = pstate
61
- @cond.broadcast
62
- end
63
- end
64
-
65
- # Wait 500 milliseconds and force terminate.
66
- def reaper
67
- pstate = wait(0.5)
68
- do_kill("KILL") unless pstate
69
- end
70
- end
@@ -1,93 +0,0 @@
1
- require 'socket'
2
- require 'openssl'
3
- require 'stringio'
4
-
5
- # Captures the raw HTTP request a client puts on the wire, independent of any
6
- # Rack/HTTP server. This lets us assert on hop-by-hop details such as
7
- # Transfer-Encoding which a conformant server (webrick, puma, ...) decodes and
8
- # never exposes to the application.
9
- module RawRequestCapture
10
- CapturedRequest = Struct.new(:request_line, :headers, :body)
11
-
12
- CERT_DIR = File.join(__dir__, 'certs')
13
-
14
- # Boots a one-shot capture server on an ephemeral port, yields its base URL to
15
- # the block (which must perform a single request against it) and returns the
16
- # CapturedRequest. Pass ssl: true to terminate TLS using the test certificates.
17
- def capture_request(ssl: false)
18
- tcp = TCPServer.new('127.0.0.1', 0)
19
- port = tcp.addr[1]
20
- server = ssl ? wrap_tls(tcp) : tcp
21
- captured = nil
22
- acceptor = Thread.new do
23
- conn = server.accept
24
- captured = read_request(conn)
25
- conn.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
26
- conn.close
27
- end
28
- yield "#{ssl ? 'https' : 'http'}://127.0.0.1:#{port}"
29
- acceptor.join(5)
30
- captured
31
- ensure
32
- tcp.close if tcp
33
- end
34
-
35
- private
36
-
37
- def wrap_tls(tcp)
38
- ctx = OpenSSL::SSL::SSLContext.new
39
- ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERT_DIR, 'cacert.pem')))
40
- ctx.key = OpenSSL::PKey.read(File.read(File.join(CERT_DIR, 'privkey.pem')))
41
- OpenSSL::SSL::SSLServer.new(tcp, ctx)
42
- end
43
-
44
- def read_request(conn)
45
- buf = +''
46
- buf << conn.readpartial(4096) until buf.include?("\r\n\r\n")
47
- head, rest = buf.split("\r\n\r\n", 2)
48
- request_line, *header_lines = head.split("\r\n")
49
- headers = header_lines.each_with_object({}) do |line, acc|
50
- name, value = line.split(': ', 2)
51
- acc[name.downcase] = value
52
- end
53
- # Honour Expect: 100-continue so the client proceeds to send the body.
54
- conn.write("HTTP/1.1 100 Continue\r\n\r\n") if headers['expect'].to_s =~ /100-continue/i
55
- CapturedRequest.new(request_line, headers, read_body(conn, headers, rest))
56
- end
57
-
58
- def read_body(conn, headers, rest)
59
- if headers['transfer-encoding'] == 'chunked'
60
- dechunk(drain_chunked(conn, rest))
61
- elsif (len = headers['content-length'])
62
- read_exactly(conn, len.to_i, rest)
63
- else
64
- rest
65
- end
66
- end
67
-
68
- def drain_chunked(conn, rest)
69
- buf = rest.dup
70
- buf << conn.readpartial(4096) until buf.include?("0\r\n\r\n")
71
- buf
72
- end
73
-
74
- def read_exactly(conn, n, rest)
75
- buf = rest.dup
76
- buf << conn.readpartial(4096) while buf.bytesize < n
77
- buf.byteslice(0, n)
78
- end
79
-
80
- def dechunk(buf)
81
- io = StringIO.new(buf)
82
- out = +''
83
- while (size_line = io.gets("\r\n"))
84
- size = size_line.strip.split(';', 2).first.to_i(16)
85
- break if size.zero?
86
- out << io.read(size).to_s
87
- io.read(2) # trailing CRLF
88
- end
89
- out
90
- end
91
- end
92
-
93
- RSpec.configure { |c| c.include RawRequestCapture }
@@ -1,29 +0,0 @@
1
- require 'rack'
2
- require 'puma'
3
- require 'rack/handler/puma'
4
-
5
- class PatronTestServer
6
- APP = Rack::Builder.new { eval(File.read(File.join(__dir__, 'config.ru'))) }
7
-
8
- def self.start(ssl = false, port = 9001, client_ca: nil)
9
- # Reset the RSpec's SIGINT handler that does not really terminate after
10
- # the first Ctrl+C pressed.
11
- # Useful to terminate the forked process before Puma is actually started:
12
- # it happens when running one particular example that does not need Puma
13
- # so the specs are in fact finished before the Puma started.
14
- Signal.trap('INT', 'EXIT')
15
- @ssl = ssl
16
- keypath = File.join(__dir__, 'certs', 'privkey.pem')
17
- certpath = File.join(__dir__, 'certs', 'cacert.pem')
18
-
19
- host = if ssl && client_ca
20
- # force_peer makes Puma drop the handshake when no valid client certificate is presented
21
- 'ssl://0.0.0.0:%d?key=%s&cert=%s&ca=%s&verify_mode=force_peer' % [port, keypath, certpath, client_ca]
22
- elsif ssl
23
- 'ssl://0.0.0.0:%d?key=%s&cert=%s' % [port, keypath, certpath]
24
- else
25
- '0.0.0.0'
26
- end
27
- Rack::Handler::Puma.run(APP, :Port => port.to_i, :Verbose => true, :Host => host)
28
- end
29
- end
@@ -1,2 +0,0 @@
1
- # Suppressions for the valgrind CI job. Keep this to things that are demonstrably
2
- # not Patron's: nothing from session_ext.c or membuffer.c may be suppressed here.
data/spec/util_spec.rb DELETED
@@ -1,71 +0,0 @@
1
- require File.expand_path("./spec") + '/spec_helper.rb'
2
-
3
- describe Patron::Util do
4
-
5
- describe :build_query_pairs_from_hash do
6
-
7
- it "correctly serializes a simple hash" do
8
- hash = {:foo => "bar", "baz" => 42}
9
- array = Patron::Util.build_query_pairs_from_hash(hash)
10
- expect(array.size).to be == 2
11
- expect(array).to include("foo=bar")
12
- expect(array).to include("baz=42")
13
- end
14
-
15
- it "correctly serializes a more complex hash" do
16
- hash = {
17
- :foo => "bar",
18
- :baz => {
19
- "quux" => {
20
- :zing => {
21
- :ying => 42
22
- }
23
- },
24
- :blargh => {
25
- :spaz => "sox",
26
- :razz => "matazz"
27
- }
28
- }
29
- }
30
- array = Patron::Util.build_query_pairs_from_hash(hash)
31
- expect(array.size).to be == 4
32
- expect(array).to include("foo=bar")
33
- expect(array).to include("baz[quux][zing][ying]=42")
34
- expect(array).to include("baz[blargh][spaz]=sox")
35
- expect(array).to include("baz[blargh][razz]=matazz")
36
- end
37
- end
38
-
39
- describe :build_query_string_from_hash do
40
- it "correctly serializes a simple hash" do
41
- hash = {:foo => "bar", "baz" => 42}
42
- array = Patron::Util.build_query_string_from_hash(hash).split('&')
43
- expect(array.size).to be == 2
44
- expect(array).to include("foo=bar")
45
- expect(array).to include("baz=42")
46
- end
47
- it "correctly serializes a more complex hash" do
48
- hash = {
49
- :foo => "bar",
50
- :baz => {
51
- "quux" => {
52
- :zing => {
53
- :ying => 42
54
- }
55
- },
56
- :blargh => {
57
- :spaz => "sox",
58
- :razz => "matazz"
59
- }
60
- }
61
- }
62
- array = Patron::Util.build_query_string_from_hash(hash).split('&')
63
- expect(array.size).to be == 4
64
- expect(array).to include("foo=bar")
65
- expect(array).to include("baz[quux][zing][ying]=42")
66
- expect(array).to include("baz[blargh][spaz]=sox")
67
- expect(array).to include("baz[blargh][razz]=matazz")
68
- end
69
- end
70
-
71
- end