httpx 0.11.0 → 0.13.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/README.md +2 -2
- data/doc/release_notes/0_11_1.md +5 -0
- data/doc/release_notes/0_11_2.md +5 -0
- data/doc/release_notes/0_11_3.md +5 -0
- data/doc/release_notes/0_12_0.md +55 -0
- data/doc/release_notes/0_13_0.md +58 -0
- data/lib/httpx.rb +2 -1
- data/lib/httpx/adapters/faraday.rb +4 -6
- data/lib/httpx/altsvc.rb +1 -0
- data/lib/httpx/chainable.rb +2 -2
- data/lib/httpx/connection.rb +80 -28
- data/lib/httpx/connection/http1.rb +19 -6
- data/lib/httpx/connection/http2.rb +32 -25
- data/lib/httpx/io.rb +16 -3
- data/lib/httpx/io/ssl.rb +35 -24
- data/lib/httpx/io/tcp.rb +48 -28
- data/lib/httpx/io/tls.rb +218 -0
- data/lib/httpx/io/tls/box.rb +365 -0
- data/lib/httpx/io/tls/context.rb +199 -0
- data/lib/httpx/io/tls/ffi.rb +390 -0
- data/lib/httpx/io/udp.rb +3 -2
- data/lib/httpx/io/unix.rb +27 -12
- data/lib/httpx/options.rb +11 -23
- data/lib/httpx/parser/http1.rb +4 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
- data/lib/httpx/plugins/aws_sigv4.rb +218 -0
- data/lib/httpx/plugins/compression.rb +21 -9
- data/lib/httpx/plugins/compression/brotli.rb +8 -6
- data/lib/httpx/plugins/compression/deflate.rb +4 -7
- data/lib/httpx/plugins/compression/gzip.rb +2 -2
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
- data/lib/httpx/plugins/digest_authentication.rb +1 -1
- data/lib/httpx/plugins/follow_redirects.rb +1 -1
- data/lib/httpx/plugins/h2c.rb +43 -58
- data/lib/httpx/plugins/internal_telemetry.rb +93 -0
- data/lib/httpx/plugins/multipart.rb +2 -0
- data/lib/httpx/plugins/multipart/encoder.rb +4 -9
- data/lib/httpx/plugins/proxy.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +1 -1
- data/lib/httpx/plugins/proxy/socks4.rb +8 -0
- data/lib/httpx/plugins/proxy/socks5.rb +8 -0
- data/lib/httpx/plugins/push_promise.rb +3 -2
- data/lib/httpx/plugins/retries.rb +2 -2
- data/lib/httpx/plugins/stream.rb +6 -6
- data/lib/httpx/plugins/upgrade.rb +83 -0
- data/lib/httpx/plugins/upgrade/h2.rb +54 -0
- data/lib/httpx/pool.rb +14 -6
- data/lib/httpx/registry.rb +1 -7
- data/lib/httpx/request.rb +11 -1
- data/lib/httpx/resolver/https.rb +3 -11
- data/lib/httpx/response.rb +14 -7
- data/lib/httpx/selector.rb +5 -0
- data/lib/httpx/session.rb +25 -2
- data/lib/httpx/transcoder/body.rb +3 -5
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +2 -1
- data/sig/connection/http1.rbs +3 -2
- data/sig/connection/http2.rbs +5 -3
- data/sig/options.rbs +7 -20
- data/sig/plugins/aws_sdk_authentication.rbs +17 -0
- data/sig/plugins/aws_sigv4.rbs +64 -0
- data/sig/plugins/compression.rbs +5 -3
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +1 -1
- data/sig/plugins/cookies.rbs +0 -1
- data/sig/plugins/digest_authentication.rbs +0 -1
- data/sig/plugins/expect.rbs +0 -2
- data/sig/plugins/follow_redirects.rbs +0 -2
- data/sig/plugins/h2c.rbs +5 -10
- data/sig/plugins/persistent.rbs +0 -1
- data/sig/plugins/proxy.rbs +0 -1
- data/sig/plugins/push_promise.rbs +1 -1
- data/sig/plugins/retries.rbs +0 -4
- data/sig/plugins/upgrade.rbs +23 -0
- data/sig/response.rbs +3 -1
- metadata +48 -26
@@ -42,11 +42,15 @@ module HTTPX
|
|
42
42
|
return @buffer.empty? ? :r : :rw
|
43
43
|
end
|
44
44
|
|
45
|
-
return :w
|
45
|
+
return :w if !@pending.empty? && can_buffer_more_requests?
|
46
46
|
|
47
47
|
return :w if @streams.each_key.any? { |r| r.interests == :w }
|
48
48
|
|
49
|
-
|
49
|
+
if @buffer.empty?
|
50
|
+
return if @streams.empty? && @pings.empty?
|
51
|
+
|
52
|
+
return :r
|
53
|
+
end
|
50
54
|
|
51
55
|
:rw
|
52
56
|
end
|
@@ -70,10 +74,14 @@ module HTTPX
|
|
70
74
|
@connection << data
|
71
75
|
end
|
72
76
|
|
77
|
+
def can_buffer_more_requests?
|
78
|
+
@handshake_completed &&
|
79
|
+
@streams.size < @max_concurrent_requests &&
|
80
|
+
@streams.size < @max_requests
|
81
|
+
end
|
82
|
+
|
73
83
|
def send(request)
|
74
|
-
|
75
|
-
@streams.size >= @max_concurrent_requests ||
|
76
|
-
@streams.size >= @max_requests
|
84
|
+
unless can_buffer_more_requests?
|
77
85
|
@pending << request
|
78
86
|
return
|
79
87
|
end
|
@@ -126,8 +134,6 @@ module HTTPX
|
|
126
134
|
request.path
|
127
135
|
end
|
128
136
|
|
129
|
-
def set_request_headers(request); end
|
130
|
-
|
131
137
|
def handle(request, stream)
|
132
138
|
catch(:buffer_full) do
|
133
139
|
request.transition(:headers)
|
@@ -172,18 +178,19 @@ module HTTPX
|
|
172
178
|
stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
|
173
179
|
end
|
174
180
|
|
181
|
+
def set_protocol_headers(request)
|
182
|
+
request.headers[":scheme"] = request.scheme
|
183
|
+
request.headers[":method"] = request.verb.to_s.upcase
|
184
|
+
request.headers[":path"] = headline_uri(request)
|
185
|
+
request.headers[":authority"] = request.authority
|
186
|
+
end
|
187
|
+
|
175
188
|
def join_headers(stream, request)
|
176
|
-
|
177
|
-
headers = {}
|
178
|
-
headers[":scheme"] = request.scheme
|
179
|
-
headers[":method"] = request.verb.to_s.upcase
|
180
|
-
headers[":path"] = headline_uri(request)
|
181
|
-
headers[":authority"] = request.authority
|
182
|
-
headers = headers.merge(request.headers)
|
189
|
+
set_protocol_headers(request)
|
183
190
|
log(level: 1, color: :yellow) do
|
184
|
-
headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
|
191
|
+
request.headers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
|
185
192
|
end
|
186
|
-
stream.headers(headers, end_stream: request.empty?)
|
193
|
+
stream.headers(request.headers.each, end_stream: request.empty?)
|
187
194
|
end
|
188
195
|
|
189
196
|
def join_body(stream, request)
|
@@ -227,10 +234,15 @@ module HTTPX
|
|
227
234
|
end
|
228
235
|
|
229
236
|
def on_stream_close(stream, request, error)
|
237
|
+
log(level: 2) { "#{stream.id}: closing stream" }
|
238
|
+
@drains.delete(request)
|
239
|
+
@streams.delete(request)
|
240
|
+
|
230
241
|
if error && error != :no_error
|
231
242
|
ex = Error.new(stream.id, error)
|
232
243
|
ex.set_backtrace(caller)
|
233
|
-
|
244
|
+
response = ErrorResponse.new(request, ex, request.options)
|
245
|
+
emit(:response, request, response)
|
234
246
|
else
|
235
247
|
response = request.response
|
236
248
|
if response.status == 421
|
@@ -241,9 +253,6 @@ module HTTPX
|
|
241
253
|
emit(:response, request, response)
|
242
254
|
end
|
243
255
|
end
|
244
|
-
log(level: 2) { "#{stream.id}: closing stream" }
|
245
|
-
|
246
|
-
@streams.delete(request)
|
247
256
|
send(@pending.shift) unless @pending.empty?
|
248
257
|
return unless @streams.empty? && exhausted?
|
249
258
|
|
@@ -328,11 +337,9 @@ module HTTPX
|
|
328
337
|
end
|
329
338
|
|
330
339
|
def method_missing(meth, *args, &blk)
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
super
|
335
|
-
end
|
340
|
+
return super unless @connection.respond_to?(meth)
|
341
|
+
|
342
|
+
@connection.__send__(meth, *args, &blk)
|
336
343
|
end
|
337
344
|
end
|
338
345
|
Connection.register "h2", Connection::HTTP2
|
data/lib/httpx/io.rb
CHANGED
@@ -2,16 +2,29 @@
|
|
2
2
|
|
3
3
|
require "socket"
|
4
4
|
require "httpx/io/tcp"
|
5
|
-
require "httpx/io/ssl"
|
6
5
|
require "httpx/io/unix"
|
7
6
|
require "httpx/io/udp"
|
8
7
|
|
9
8
|
module HTTPX
|
10
9
|
module IO
|
11
10
|
extend Registry
|
12
|
-
register "tcp", TCP
|
13
|
-
register "ssl", SSL
|
14
11
|
register "udp", UDP
|
15
12
|
register "unix", HTTPX::UNIX
|
13
|
+
register "tcp", TCP
|
14
|
+
|
15
|
+
if RUBY_ENGINE == "jruby"
|
16
|
+
begin
|
17
|
+
require "httpx/io/tls"
|
18
|
+
register "ssl", TLS
|
19
|
+
rescue LoadError
|
20
|
+
# :nocov:
|
21
|
+
require "httpx/io/ssl"
|
22
|
+
register "ssl", SSL
|
23
|
+
# :nocov:
|
24
|
+
end
|
25
|
+
else
|
26
|
+
require "httpx/io/ssl"
|
27
|
+
register "ssl", SSL
|
28
|
+
end
|
16
29
|
end
|
17
30
|
end
|
data/lib/httpx/io/ssl.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require "openssl"
|
4
4
|
|
5
5
|
module HTTPX
|
6
|
+
TLSError = OpenSSL::SSL::SSLError
|
7
|
+
|
6
8
|
class SSL < TCP
|
7
9
|
TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
|
8
10
|
{ alpn_protocols: %w[h2 http/1.1] }
|
@@ -11,19 +13,14 @@ module HTTPX
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def initialize(_, _, options)
|
16
|
+
super
|
14
17
|
@ctx = OpenSSL::SSL::SSLContext.new
|
15
18
|
ctx_options = TLS_OPTIONS.merge(options.ssl)
|
16
|
-
@
|
19
|
+
@sni_hostname = ctx_options.delete(:hostname) || @hostname
|
17
20
|
@ctx.set_params(ctx_options) unless ctx_options.empty?
|
18
|
-
super
|
19
|
-
@tls_hostname ||= @hostname
|
20
21
|
@state = :negotiated if @keep_open
|
21
22
|
end
|
22
23
|
|
23
|
-
def interests
|
24
|
-
@interests || super
|
25
|
-
end
|
26
|
-
|
27
24
|
def protocol
|
28
25
|
@io.alpn_protocol || super
|
29
26
|
rescue StandardError
|
@@ -50,28 +47,30 @@ module HTTPX
|
|
50
47
|
|
51
48
|
def connect
|
52
49
|
super
|
53
|
-
if @keep_open
|
54
|
-
@state = :negotiated
|
55
|
-
return
|
56
|
-
end
|
57
50
|
return if @state == :negotiated ||
|
58
51
|
@state != :connected
|
59
52
|
|
60
53
|
unless @io.is_a?(OpenSSL::SSL::SSLSocket)
|
61
54
|
@io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
|
62
|
-
@io.hostname = @
|
55
|
+
@io.hostname = @sni_hostname
|
63
56
|
@io.sync_close = true
|
64
57
|
end
|
65
|
-
|
66
|
-
@io.post_connection_check(@tls_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
|
67
|
-
transition(:negotiated)
|
68
|
-
rescue ::IO::WaitReadable
|
69
|
-
@interests = :r
|
70
|
-
rescue ::IO::WaitWritable
|
71
|
-
@interests = :w
|
58
|
+
try_ssl_connect
|
72
59
|
end
|
73
60
|
|
74
61
|
if RUBY_VERSION < "2.3"
|
62
|
+
# :nocov:
|
63
|
+
def try_ssl_connect
|
64
|
+
@io.connect_nonblock
|
65
|
+
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
|
66
|
+
transition(:negotiated)
|
67
|
+
@interests = :w
|
68
|
+
rescue ::IO::WaitReadable
|
69
|
+
@interests = :r
|
70
|
+
rescue ::IO::WaitWritable
|
71
|
+
@interests = :w
|
72
|
+
end
|
73
|
+
|
75
74
|
def read(_, buffer)
|
76
75
|
super
|
77
76
|
rescue ::IO::WaitWritable
|
@@ -84,7 +83,23 @@ module HTTPX
|
|
84
83
|
rescue ::IO::WaitReadable
|
85
84
|
0
|
86
85
|
end
|
86
|
+
# :nocov:
|
87
87
|
else
|
88
|
+
def try_ssl_connect
|
89
|
+
case @io.connect_nonblock(exception: false)
|
90
|
+
when :wait_readable
|
91
|
+
@interests = :r
|
92
|
+
return
|
93
|
+
when :wait_writable
|
94
|
+
@interests = :w
|
95
|
+
return
|
96
|
+
end
|
97
|
+
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
|
98
|
+
transition(:negotiated)
|
99
|
+
@interests = :w
|
100
|
+
end
|
101
|
+
|
102
|
+
# :nocov:
|
88
103
|
if OpenSSL::VERSION < "2.0.6"
|
89
104
|
def read(size, buffer)
|
90
105
|
@io.read_nonblock(size, buffer)
|
@@ -97,11 +112,7 @@ module HTTPX
|
|
97
112
|
nil
|
98
113
|
end
|
99
114
|
end
|
100
|
-
|
101
|
-
|
102
|
-
def inspect
|
103
|
-
id = @io.closed? ? "closed" : @io.to_io.fileno
|
104
|
-
"#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
|
115
|
+
# :nocov:
|
105
116
|
end
|
106
117
|
|
107
118
|
private
|
data/lib/httpx/io/tcp.rb
CHANGED
@@ -7,17 +7,19 @@ module HTTPX
|
|
7
7
|
class TCP
|
8
8
|
include Loggable
|
9
9
|
|
10
|
-
|
10
|
+
using URIExtensions
|
11
|
+
|
12
|
+
attr_reader :ip, :port, :addresses, :state, :interests
|
11
13
|
|
12
14
|
alias_method :host, :ip
|
13
15
|
|
14
16
|
def initialize(origin, addresses, options)
|
15
17
|
@state = :idle
|
16
18
|
@hostname = origin.host
|
17
|
-
@addresses = addresses
|
18
19
|
@options = Options.new(options)
|
19
20
|
@fallback_protocol = @options.fallback_protocol
|
20
21
|
@port = origin.port
|
22
|
+
@interests = :w
|
21
23
|
if @options.io
|
22
24
|
@io = case @options.io
|
23
25
|
when Hash
|
@@ -25,24 +27,20 @@ module HTTPX
|
|
25
27
|
else
|
26
28
|
@options.io
|
27
29
|
end
|
30
|
+
raise Error, "Given IO objects do not match the request authority" unless @io
|
31
|
+
|
28
32
|
_, _, _, @ip = @io.addr
|
29
33
|
@addresses ||= [@ip]
|
30
34
|
@ip_index = @addresses.size - 1
|
31
|
-
|
32
|
-
|
33
|
-
@state = :connected
|
34
|
-
end
|
35
|
+
@keep_open = true
|
36
|
+
@state = :connected
|
35
37
|
else
|
36
|
-
@
|
37
|
-
@ip = @addresses[@ip_index]
|
38
|
+
@addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
|
38
39
|
end
|
40
|
+
@ip_index = @addresses.size - 1
|
39
41
|
@io ||= build_socket
|
40
42
|
end
|
41
43
|
|
42
|
-
def interests
|
43
|
-
:w
|
44
|
-
end
|
45
|
-
|
46
44
|
def to_io
|
47
45
|
@io.to_io
|
48
46
|
end
|
@@ -54,32 +52,40 @@ module HTTPX
|
|
54
52
|
def connect
|
55
53
|
return unless closed?
|
56
54
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@io = build_socket
|
61
|
-
end
|
62
|
-
@io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
|
63
|
-
rescue Errno::EISCONN
|
55
|
+
if @io.closed?
|
56
|
+
transition(:idle)
|
57
|
+
@io = build_socket
|
64
58
|
end
|
65
|
-
|
59
|
+
try_connect
|
66
60
|
rescue Errno::EHOSTUNREACH => e
|
67
61
|
raise e if @ip_index <= 0
|
68
62
|
|
69
63
|
@ip_index -= 1
|
70
64
|
retry
|
71
65
|
rescue Errno::ETIMEDOUT => e
|
72
|
-
raise ConnectTimeoutError, e.message if @ip_index <= 0
|
66
|
+
raise ConnectTimeoutError.new(@options.timeout.connect_timeout, e.message) if @ip_index <= 0
|
73
67
|
|
74
68
|
@ip_index -= 1
|
75
69
|
retry
|
76
|
-
rescue Errno::EINPROGRESS,
|
77
|
-
Errno::EALREADY,
|
78
|
-
::IO::WaitReadable
|
79
70
|
end
|
80
71
|
|
81
72
|
if RUBY_VERSION < "2.3"
|
82
73
|
# :nocov:
|
74
|
+
def try_connect
|
75
|
+
@io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
|
76
|
+
rescue ::IO::WaitWritable, Errno::EALREADY
|
77
|
+
@interests = :w
|
78
|
+
rescue ::IO::WaitReadable
|
79
|
+
@interests = :r
|
80
|
+
rescue Errno::EISCONN
|
81
|
+
transition(:connected)
|
82
|
+
@interests = :w
|
83
|
+
else
|
84
|
+
transition(:connected)
|
85
|
+
@interests = :w
|
86
|
+
end
|
87
|
+
private :try_connect
|
88
|
+
|
83
89
|
def read(size, buffer)
|
84
90
|
@io.read_nonblock(size, buffer)
|
85
91
|
log { "READ: #{buffer.bytesize} bytes..." }
|
@@ -103,6 +109,20 @@ module HTTPX
|
|
103
109
|
end
|
104
110
|
# :nocov:
|
105
111
|
else
|
112
|
+
def try_connect
|
113
|
+
case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
|
114
|
+
when :wait_readable
|
115
|
+
@interests = :r
|
116
|
+
return
|
117
|
+
when :wait_writable
|
118
|
+
@interests = :w
|
119
|
+
return
|
120
|
+
end
|
121
|
+
transition(:connected)
|
122
|
+
@interests = :w
|
123
|
+
end
|
124
|
+
private :try_connect
|
125
|
+
|
106
126
|
def read(size, buffer)
|
107
127
|
ret = @io.read_nonblock(size, buffer, exception: false)
|
108
128
|
if ret == :wait_readable
|
@@ -147,14 +167,14 @@ module HTTPX
|
|
147
167
|
|
148
168
|
# :nocov:
|
149
169
|
def inspect
|
150
|
-
|
151
|
-
"#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
|
170
|
+
"#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"
|
152
171
|
end
|
153
172
|
# :nocov:
|
154
173
|
|
155
174
|
private
|
156
175
|
|
157
176
|
def build_socket
|
177
|
+
@ip = @addresses[@ip_index]
|
158
178
|
Socket.new(@ip.family, :STREAM, 0)
|
159
179
|
end
|
160
180
|
|
@@ -177,9 +197,9 @@ module HTTPX
|
|
177
197
|
def log_transition_state(nextstate)
|
178
198
|
case nextstate
|
179
199
|
when :connected
|
180
|
-
"Connected to #{
|
200
|
+
"Connected to #{host} (##{@io.fileno})"
|
181
201
|
else
|
182
|
-
"#{
|
202
|
+
"#{host} #{@state} -> #{nextstate}"
|
183
203
|
end
|
184
204
|
end
|
185
205
|
end
|
data/lib/httpx/io/tls.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
class TLS < TCP
|
7
|
+
Error = Class.new(StandardError)
|
8
|
+
|
9
|
+
def initialize(_, _, options)
|
10
|
+
super
|
11
|
+
@encrypted = Buffer.new(Connection::BUFFER_SIZE)
|
12
|
+
@decrypted = "".b
|
13
|
+
tls_options = convert_tls_options(options.ssl)
|
14
|
+
@sni_hostname = tls_options[:hostname]
|
15
|
+
@ctx = TLS::Box.new(false, self, tls_options)
|
16
|
+
@state = :negotiated if @keep_open
|
17
|
+
end
|
18
|
+
|
19
|
+
def interests
|
20
|
+
@interests || super
|
21
|
+
end
|
22
|
+
|
23
|
+
def protocol
|
24
|
+
@protocol || super
|
25
|
+
end
|
26
|
+
|
27
|
+
def connected?
|
28
|
+
@state == :negotiated
|
29
|
+
end
|
30
|
+
|
31
|
+
def connect
|
32
|
+
super
|
33
|
+
if @keep_open
|
34
|
+
@state = :negotiated
|
35
|
+
return
|
36
|
+
end
|
37
|
+
return if @state == :negotiated ||
|
38
|
+
@state != :connected
|
39
|
+
|
40
|
+
super
|
41
|
+
@ctx.start
|
42
|
+
@interests = :r
|
43
|
+
read(@options.window_size, @decrypted)
|
44
|
+
end
|
45
|
+
|
46
|
+
# :nocov:
|
47
|
+
def inspect
|
48
|
+
id = @io.closed? ? "closed" : @io
|
49
|
+
"#<TLS(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
|
50
|
+
end
|
51
|
+
# :nocov:
|
52
|
+
|
53
|
+
alias_method :transport_close, :close
|
54
|
+
def close
|
55
|
+
transport_close
|
56
|
+
@ctx.cleanup
|
57
|
+
end
|
58
|
+
|
59
|
+
def read(*, buffer)
|
60
|
+
ret = super
|
61
|
+
return ret if !ret || ret.zero?
|
62
|
+
|
63
|
+
@ctx.decrypt(buffer.to_s.dup)
|
64
|
+
buffer.replace(@decrypted)
|
65
|
+
@decrypted.clear
|
66
|
+
buffer.bytesize
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :unencrypted_write, :write
|
70
|
+
def write(buffer)
|
71
|
+
@ctx.encrypt(buffer.to_s.dup)
|
72
|
+
buffer.clear
|
73
|
+
do_write
|
74
|
+
end
|
75
|
+
|
76
|
+
# TLS callback.
|
77
|
+
#
|
78
|
+
# buffers the encrypted +data+
|
79
|
+
def transmit_cb(data)
|
80
|
+
log { "TLS encrypted: #{data.bytesize} bytes" }
|
81
|
+
log(level: 2) { data.inspect }
|
82
|
+
@encrypted << data
|
83
|
+
do_write
|
84
|
+
end
|
85
|
+
|
86
|
+
# TLS callback.
|
87
|
+
#
|
88
|
+
# buffers the decrypted +data+
|
89
|
+
def dispatch_cb(data)
|
90
|
+
log { "TLS decrypted: #{data.bytesize} bytes" }
|
91
|
+
log(level: 2) { data.inspect }
|
92
|
+
|
93
|
+
@decrypted << data
|
94
|
+
end
|
95
|
+
|
96
|
+
# TLS callback.
|
97
|
+
#
|
98
|
+
# signals TLS invalid status / shutdown.
|
99
|
+
def close_cb(msg = nil)
|
100
|
+
log { "TLS Error: #{msg}, closing" }
|
101
|
+
raise Error, "certificate verify failed (#{msg})"
|
102
|
+
end
|
103
|
+
|
104
|
+
# TLS callback.
|
105
|
+
#
|
106
|
+
# alpn protocol negotiation (+protocol+).
|
107
|
+
#
|
108
|
+
def alpn_protocol_cb(protocol)
|
109
|
+
@protocol = protocol
|
110
|
+
log { "TLS ALPN protocol negotiated: #{@protocol}" }
|
111
|
+
end
|
112
|
+
|
113
|
+
# TLS callback.
|
114
|
+
#
|
115
|
+
# handshake finished.
|
116
|
+
#
|
117
|
+
def handshake_cb
|
118
|
+
log { "TLS handshake completed" }
|
119
|
+
transition(:negotiated)
|
120
|
+
end
|
121
|
+
|
122
|
+
# TLS callback.
|
123
|
+
#
|
124
|
+
# passed the peer +cert+ to be verified.
|
125
|
+
#
|
126
|
+
def verify_cb(cert)
|
127
|
+
raise Error, "Peer verification enabled, but no certificate received." if cert.nil?
|
128
|
+
|
129
|
+
log { "TLS verifying #{cert}" }
|
130
|
+
@peer_cert = OpenSSL::X509::Certificate.new(cert)
|
131
|
+
|
132
|
+
# by default one doesn't verify client certificates in the server
|
133
|
+
verify_hostname(@sni_hostname)
|
134
|
+
end
|
135
|
+
|
136
|
+
# copied from:
|
137
|
+
# https://github.com/ruby/ruby/blob/8cbf2dae5aadfa5d6241b0df2bf44d55db46704f/ext/openssl/lib/openssl/ssl.rb#L395-L409
|
138
|
+
#
|
139
|
+
def verify_hostname(host)
|
140
|
+
return false unless @ctx.verify_peer && @peer_cert
|
141
|
+
|
142
|
+
OpenSSL::SSL.verify_certificate_identity(@peer_cert, host)
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def do_write
|
148
|
+
nwritten = 0
|
149
|
+
until @encrypted.empty?
|
150
|
+
siz = unencrypted_write(@encrypted)
|
151
|
+
break unless !siz || siz.zero?
|
152
|
+
|
153
|
+
nwritten += siz
|
154
|
+
end
|
155
|
+
nwritten
|
156
|
+
end
|
157
|
+
|
158
|
+
def convert_tls_options(ssl_options)
|
159
|
+
options = {}
|
160
|
+
options[:verify_peer] = !ssl_options.key?(:verify_mode) || ssl_options[:verify_mode] != OpenSSL::SSL::VERIFY_NONE
|
161
|
+
options[:version] = ssl_options[:ssl_version] if ssl_options.key?(:ssl_version)
|
162
|
+
|
163
|
+
if ssl_options.key?(:key)
|
164
|
+
private_key = ssl_options[:key]
|
165
|
+
private_key = private_key.to_pem if private_key.respond_to?(:to_pem)
|
166
|
+
options[:private_key] = private_key
|
167
|
+
end
|
168
|
+
|
169
|
+
if ssl_options.key?(:ca_path) || ssl_options.key?(:ca_file)
|
170
|
+
ca_path = ssl_options[:ca_path] || ssl_options[:ca_file].path
|
171
|
+
options[:cert_chain] = ca_path
|
172
|
+
end
|
173
|
+
|
174
|
+
options[:ciphers] = ssl_options[:ciphers] if ssl_options.key?(:ciphers)
|
175
|
+
options[:protocols] = ssl_options.fetch(:alpn_protocols, %w[h2 http/1.1])
|
176
|
+
options[:hostname] = ssl_options.fetch(:hostname, @hostname)
|
177
|
+
options
|
178
|
+
end
|
179
|
+
|
180
|
+
def transition(nextstate)
|
181
|
+
case nextstate
|
182
|
+
when :negotiated
|
183
|
+
return unless @state == :connected
|
184
|
+
when :closed
|
185
|
+
return unless @state == :negotiated ||
|
186
|
+
@state == :connected
|
187
|
+
end
|
188
|
+
do_transition(nextstate)
|
189
|
+
end
|
190
|
+
|
191
|
+
def log_transition_state(nextstate)
|
192
|
+
return super unless nextstate == :negotiated
|
193
|
+
|
194
|
+
server_cert = @peer_cert
|
195
|
+
|
196
|
+
"#{super}\n\n" \
|
197
|
+
"SSL connection using #{@ctx.ssl_version} / #{Array(@ctx.cipher).first}\n" \
|
198
|
+
"ALPN, server accepted to use #{protocol}\n" +
|
199
|
+
(if server_cert
|
200
|
+
"Server certificate:\n" \
|
201
|
+
" subject: #{server_cert.subject}\n" \
|
202
|
+
" start date: #{server_cert.not_before}\n" \
|
203
|
+
" expire date: #{server_cert.not_after}\n" \
|
204
|
+
" issuer: #{server_cert.issuer}\n" \
|
205
|
+
" SSL certificate verify ok."
|
206
|
+
else
|
207
|
+
"SSL certificate verify failed."
|
208
|
+
end
|
209
|
+
)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
TLSError = TLS::Error
|
214
|
+
end
|
215
|
+
|
216
|
+
require "httpx/io/tls/ffi"
|
217
|
+
require "httpx/io/tls/context"
|
218
|
+
require "httpx/io/tls/box"
|