httpx 0.11.1 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/doc/release_notes/0_11_1.md +5 -1
- 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/doc/release_notes/0_13_1.md +5 -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 +50 -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/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 +20 -8
- 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 +24 -2
@@ -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,22 @@ 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
|
+
rescue Errno::EALREADY
|
124
|
+
@interests = :w
|
125
|
+
end
|
126
|
+
private :try_connect
|
127
|
+
|
106
128
|
def read(size, buffer)
|
107
129
|
ret = @io.read_nonblock(size, buffer, exception: false)
|
108
130
|
if ret == :wait_readable
|
@@ -147,14 +169,14 @@ module HTTPX
|
|
147
169
|
|
148
170
|
# :nocov:
|
149
171
|
def inspect
|
150
|
-
|
151
|
-
"#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
|
172
|
+
"#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"
|
152
173
|
end
|
153
174
|
# :nocov:
|
154
175
|
|
155
176
|
private
|
156
177
|
|
157
178
|
def build_socket
|
179
|
+
@ip = @addresses[@ip_index]
|
158
180
|
Socket.new(@ip.family, :STREAM, 0)
|
159
181
|
end
|
160
182
|
|
@@ -177,9 +199,9 @@ module HTTPX
|
|
177
199
|
def log_transition_state(nextstate)
|
178
200
|
case nextstate
|
179
201
|
when :connected
|
180
|
-
"Connected to #{
|
202
|
+
"Connected to #{host} (##{@io.fileno})"
|
181
203
|
else
|
182
|
-
"#{
|
204
|
+
"#{host} #{@state} -> #{nextstate}"
|
183
205
|
end
|
184
206
|
end
|
185
207
|
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"
|