httpx 0.7.0 → 0.10.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/LICENSE.txt +48 -0
- data/README.md +9 -5
- data/doc/release_notes/0_0_1.md +7 -0
- data/doc/release_notes/0_0_2.md +9 -0
- data/doc/release_notes/0_0_3.md +9 -0
- data/doc/release_notes/0_0_4.md +7 -0
- data/doc/release_notes/0_0_5.md +5 -0
- data/doc/release_notes/0_10_0.md +66 -0
- data/doc/release_notes/0_1_0.md +9 -0
- data/doc/release_notes/0_2_0.md +5 -0
- data/doc/release_notes/0_2_1.md +16 -0
- data/doc/release_notes/0_3_0.md +12 -0
- data/doc/release_notes/0_3_1.md +6 -0
- data/doc/release_notes/0_4_0.md +51 -0
- data/doc/release_notes/0_4_1.md +3 -0
- data/doc/release_notes/0_5_0.md +15 -0
- data/doc/release_notes/0_5_1.md +14 -0
- data/doc/release_notes/0_6_0.md +5 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_6_2.md +6 -0
- data/doc/release_notes/0_6_3.md +13 -0
- data/doc/release_notes/0_6_4.md +21 -0
- data/doc/release_notes/0_6_5.md +22 -0
- data/doc/release_notes/0_6_6.md +19 -0
- data/doc/release_notes/0_6_7.md +5 -0
- data/doc/release_notes/0_7_0.md +46 -0
- data/doc/release_notes/0_8_0.md +27 -0
- data/doc/release_notes/0_8_1.md +8 -0
- data/doc/release_notes/0_8_2.md +7 -0
- data/doc/release_notes/0_9_0.md +38 -0
- data/lib/httpx.rb +2 -0
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/altsvc.rb +18 -2
- data/lib/httpx/chainable.rb +9 -8
- data/lib/httpx/connection.rb +177 -72
- data/lib/httpx/connection/http1.rb +44 -13
- data/lib/httpx/connection/http2.rb +77 -34
- data/lib/httpx/domain_name.rb +440 -0
- data/lib/httpx/errors.rb +1 -0
- data/lib/httpx/extensions.rb +23 -3
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +11 -4
- data/lib/httpx/io/tcp.rb +16 -5
- data/lib/httpx/io/udp.rb +4 -1
- data/lib/httpx/loggable.rb +6 -6
- data/lib/httpx/options.rb +22 -15
- data/lib/httpx/parser/http1.rb +14 -17
- data/lib/httpx/plugins/compression.rb +49 -64
- data/lib/httpx/plugins/compression/brotli.rb +10 -14
- data/lib/httpx/plugins/compression/deflate.rb +7 -6
- data/lib/httpx/plugins/compression/gzip.rb +45 -17
- data/lib/httpx/plugins/cookies.rb +21 -60
- data/lib/httpx/plugins/cookies/cookie.rb +173 -0
- data/lib/httpx/plugins/cookies/jar.rb +74 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
- data/lib/httpx/plugins/digest_authentication.rb +2 -0
- data/lib/httpx/plugins/expect.rb +12 -1
- data/lib/httpx/plugins/follow_redirects.rb +20 -2
- data/lib/httpx/plugins/h2c.rb +1 -1
- data/lib/httpx/plugins/multipart.rb +0 -8
- data/lib/httpx/plugins/persistent.rb +6 -1
- data/lib/httpx/plugins/proxy.rb +16 -12
- data/lib/httpx/plugins/proxy/http.rb +7 -2
- data/lib/httpx/plugins/proxy/socks4.rb +4 -2
- data/lib/httpx/plugins/proxy/socks5.rb +5 -1
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/plugins/rate_limiter.rb +51 -0
- data/lib/httpx/plugins/retries.rb +13 -6
- data/lib/httpx/plugins/stream.rb +109 -13
- data/lib/httpx/pool.rb +13 -15
- data/lib/httpx/registry.rb +2 -1
- data/lib/httpx/request.rb +14 -19
- data/lib/httpx/resolver.rb +7 -8
- data/lib/httpx/resolver/https.rb +22 -5
- data/lib/httpx/resolver/native.rb +27 -33
- data/lib/httpx/resolver/options.rb +2 -2
- data/lib/httpx/resolver/resolver_mixin.rb +1 -1
- data/lib/httpx/response.rb +22 -17
- data/lib/httpx/selector.rb +96 -97
- data/lib/httpx/session.rb +32 -24
- data/lib/httpx/timeout.rb +7 -1
- data/lib/httpx/transcoder/chunker.rb +0 -2
- data/lib/httpx/transcoder/form.rb +0 -6
- data/lib/httpx/transcoder/json.rb +0 -4
- data/lib/httpx/utils.rb +45 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +24 -0
- data/sig/callbacks.rbs +14 -0
- data/sig/chainable.rbs +37 -0
- data/sig/connection.rbs +2 -0
- data/sig/connection/http2.rbs +4 -0
- data/sig/domain_name.rbs +17 -0
- data/sig/errors.rbs +3 -0
- data/sig/headers.rbs +42 -0
- data/sig/httpx.rbs +14 -0
- data/sig/loggable.rbs +11 -0
- data/sig/missing.rbs +12 -0
- data/sig/options.rbs +118 -0
- data/sig/parser/http1.rbs +50 -0
- data/sig/plugins/authentication.rbs +11 -0
- data/sig/plugins/basic_authentication.rbs +13 -0
- data/sig/plugins/compression.rbs +55 -0
- data/sig/plugins/compression/brotli.rbs +21 -0
- data/sig/plugins/compression/deflate.rbs +17 -0
- data/sig/plugins/compression/gzip.rbs +29 -0
- data/sig/plugins/cookies.rbs +26 -0
- data/sig/plugins/cookies/cookie.rbs +50 -0
- data/sig/plugins/cookies/jar.rbs +27 -0
- data/sig/plugins/digest_authentication.rbs +33 -0
- data/sig/plugins/expect.rbs +19 -0
- data/sig/plugins/follow_redirects.rbs +37 -0
- data/sig/plugins/h2c.rbs +26 -0
- data/sig/plugins/multipart.rbs +19 -0
- data/sig/plugins/persistent.rbs +17 -0
- data/sig/plugins/proxy.rbs +47 -0
- data/sig/plugins/proxy/http.rbs +14 -0
- data/sig/plugins/proxy/socks4.rbs +33 -0
- data/sig/plugins/proxy/socks5.rbs +36 -0
- data/sig/plugins/proxy/ssh.rbs +18 -0
- data/sig/plugins/push_promise.rbs +22 -0
- data/sig/plugins/rate_limiter.rbs +11 -0
- data/sig/plugins/retries.rbs +48 -0
- data/sig/plugins/stream.rbs +39 -0
- data/sig/pool.rbs +2 -0
- data/sig/registry.rbs +9 -0
- data/sig/request.rbs +61 -0
- data/sig/response.rbs +87 -0
- data/sig/session.rbs +49 -0
- data/sig/test.rbs +9 -0
- data/sig/timeout.rbs +29 -0
- data/sig/transcoder.rbs +16 -0
- data/sig/transcoder/body.rbs +18 -0
- data/sig/transcoder/chunker.rbs +32 -0
- data/sig/transcoder/form.rbs +16 -0
- data/sig/transcoder/json.rbs +14 -0
- metadata +120 -21
|
@@ -23,6 +23,19 @@ module HTTPX
|
|
|
23
23
|
@requests = []
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def interests
|
|
27
|
+
# this means we're processing incoming response already
|
|
28
|
+
return :r if @request
|
|
29
|
+
|
|
30
|
+
return if @requests.empty?
|
|
31
|
+
|
|
32
|
+
request = @requests.first
|
|
33
|
+
|
|
34
|
+
return :w if request.interests == :w || !@buffer.empty?
|
|
35
|
+
|
|
36
|
+
:r
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
def reset
|
|
27
40
|
@max_requests = @options.max_requests || MAX_REQUESTS
|
|
28
41
|
@parser.reset!
|
|
@@ -30,7 +43,7 @@ module HTTPX
|
|
|
30
43
|
|
|
31
44
|
def close
|
|
32
45
|
reset
|
|
33
|
-
emit(:close)
|
|
46
|
+
emit(:close, true)
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
def exhausted?
|
|
@@ -53,16 +66,19 @@ module HTTPX
|
|
|
53
66
|
return
|
|
54
67
|
end
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
@requests << request
|
|
58
|
-
@pipelining = true if @requests.size > 1
|
|
59
|
-
end
|
|
69
|
+
return if @requests.include?(request)
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
@requests << request
|
|
72
|
+
@pipelining = true if @requests.size > 1
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
def consume
|
|
65
|
-
@requests.
|
|
76
|
+
requests_limit = [@max_concurrent_requests, @max_requests, @requests.size].min
|
|
77
|
+
@requests.each_with_index do |request, idx|
|
|
78
|
+
break if idx >= requests_limit
|
|
79
|
+
next if request.state == :done
|
|
80
|
+
|
|
81
|
+
request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
|
|
66
82
|
handle(request)
|
|
67
83
|
end
|
|
68
84
|
end
|
|
@@ -121,7 +137,7 @@ module HTTPX
|
|
|
121
137
|
|
|
122
138
|
def dispatch
|
|
123
139
|
if @request.expects?
|
|
124
|
-
reset
|
|
140
|
+
@parser.reset!
|
|
125
141
|
return handle(@request)
|
|
126
142
|
end
|
|
127
143
|
|
|
@@ -136,10 +152,10 @@ module HTTPX
|
|
|
136
152
|
throw(:called)
|
|
137
153
|
end
|
|
138
154
|
|
|
139
|
-
reset
|
|
155
|
+
@parser.reset!
|
|
140
156
|
@max_requests -= 1
|
|
141
|
-
send(@pending.shift) unless @pending.empty?
|
|
142
157
|
manage_connection(response)
|
|
158
|
+
send(@pending.shift) unless @pending.empty?
|
|
143
159
|
end
|
|
144
160
|
|
|
145
161
|
def handle_error(ex)
|
|
@@ -149,15 +165,23 @@ module HTTPX
|
|
|
149
165
|
@requests.each do |request|
|
|
150
166
|
emit(:error, request, ex)
|
|
151
167
|
end
|
|
168
|
+
@pending.each do |request|
|
|
169
|
+
emit(:error, request, ex)
|
|
170
|
+
end
|
|
152
171
|
end
|
|
153
172
|
end
|
|
154
173
|
|
|
174
|
+
def ping
|
|
175
|
+
emit(:reset)
|
|
176
|
+
emit(:exhausted)
|
|
177
|
+
end
|
|
178
|
+
|
|
155
179
|
private
|
|
156
180
|
|
|
157
181
|
def manage_connection(response)
|
|
158
182
|
connection = response.headers["connection"]
|
|
159
183
|
case connection
|
|
160
|
-
when /keep
|
|
184
|
+
when /keep-alive/i
|
|
161
185
|
keep_alive = response.headers["keep-alive"]
|
|
162
186
|
return unless keep_alive
|
|
163
187
|
|
|
@@ -188,7 +212,14 @@ module HTTPX
|
|
|
188
212
|
def disable_pipelining
|
|
189
213
|
return if @requests.empty?
|
|
190
214
|
|
|
191
|
-
@requests.each
|
|
215
|
+
@requests.each do |r|
|
|
216
|
+
r.transition(:idle)
|
|
217
|
+
|
|
218
|
+
# when we disable pipelining, we still want to try keep-alive.
|
|
219
|
+
# only when keep-alive with one request fails, do we fallback to
|
|
220
|
+
# connection: close.
|
|
221
|
+
r.headers["connection"] = "close" if @max_concurrent_requests == 1
|
|
222
|
+
end
|
|
192
223
|
# server doesn't handle pipelining, and probably
|
|
193
224
|
# doesn't support keep-alive. Fallback to send only
|
|
194
225
|
# 1 keep alive request.
|
|
@@ -198,7 +229,7 @@ module HTTPX
|
|
|
198
229
|
|
|
199
230
|
def set_request_headers(request)
|
|
200
231
|
request.headers["host"] ||= request.authority
|
|
201
|
-
request.headers["connection"] ||= "keep-alive"
|
|
232
|
+
request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
|
|
202
233
|
if !request.headers.key?("content-length") &&
|
|
203
234
|
request.body.bytesize == Float::INFINITY
|
|
204
235
|
request.chunk!
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
3
4
|
require "io/wait"
|
|
4
5
|
require "http/2/next"
|
|
5
6
|
|
|
@@ -25,13 +26,38 @@ module HTTPX
|
|
|
25
26
|
@pending = []
|
|
26
27
|
@streams = {}
|
|
27
28
|
@drains = {}
|
|
29
|
+
@pings = []
|
|
28
30
|
@buffer = buffer
|
|
29
31
|
@handshake_completed = false
|
|
30
32
|
init_connection
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
+
def interests
|
|
36
|
+
# waiting for WINDOW_UPDATE frames
|
|
37
|
+
return :r if @buffer.full?
|
|
38
|
+
|
|
39
|
+
return :w if @connection.state == :closed
|
|
40
|
+
|
|
41
|
+
unless (@connection.state == :connected && @handshake_completed)
|
|
42
|
+
return @buffer.empty? ? :r : :rw
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return :w unless @pending.empty?
|
|
46
|
+
|
|
47
|
+
return :w if @streams.each_key.any? { |r| r.interests == :w }
|
|
48
|
+
|
|
49
|
+
return :r if @buffer.empty?
|
|
50
|
+
|
|
51
|
+
:rw
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reset
|
|
55
|
+
init_connection
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def close(*args)
|
|
59
|
+
@connection.goaway(*args) unless @connection.state == :closed
|
|
60
|
+
emit(:close)
|
|
35
61
|
end
|
|
36
62
|
|
|
37
63
|
def empty?
|
|
@@ -39,6 +65,8 @@ module HTTPX
|
|
|
39
65
|
end
|
|
40
66
|
|
|
41
67
|
def exhausted?
|
|
68
|
+
return false if @max_requests.zero? && @connection.active_stream_count.zero?
|
|
69
|
+
|
|
42
70
|
@connection.active_stream_count >= @max_requests
|
|
43
71
|
end
|
|
44
72
|
|
|
@@ -68,6 +96,8 @@ module HTTPX
|
|
|
68
96
|
|
|
69
97
|
def consume
|
|
70
98
|
@streams.each do |request, stream|
|
|
99
|
+
next if request.state == :done
|
|
100
|
+
|
|
71
101
|
handle(request, stream)
|
|
72
102
|
end
|
|
73
103
|
end
|
|
@@ -81,6 +111,13 @@ module HTTPX
|
|
|
81
111
|
end
|
|
82
112
|
end
|
|
83
113
|
|
|
114
|
+
def ping
|
|
115
|
+
ping = SecureRandom.gen_random(8)
|
|
116
|
+
@connection.ping(ping)
|
|
117
|
+
ensure
|
|
118
|
+
@pings << ping
|
|
119
|
+
end
|
|
120
|
+
|
|
84
121
|
private
|
|
85
122
|
|
|
86
123
|
def send_pending
|
|
@@ -115,6 +152,7 @@ module HTTPX
|
|
|
115
152
|
@connection.on(:promise, &method(:on_promise))
|
|
116
153
|
@connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
|
|
117
154
|
@connection.on(:settings_ack, &method(:on_settings))
|
|
155
|
+
@connection.on(:ack, &method(:on_pong))
|
|
118
156
|
@connection.on(:goaway, &method(:on_close))
|
|
119
157
|
#
|
|
120
158
|
# Some servers initiate HTTP/2 negotiation right away, some don't.
|
|
@@ -128,7 +166,7 @@ module HTTPX
|
|
|
128
166
|
def handle_stream(stream, request)
|
|
129
167
|
stream.on(:close, &method(:on_stream_close).curry[stream, request])
|
|
130
168
|
stream.on(:half_close) do
|
|
131
|
-
log(level: 2
|
|
169
|
+
log(level: 2) { "#{stream.id}: waiting for response..." }
|
|
132
170
|
end
|
|
133
171
|
stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
|
|
134
172
|
stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
|
|
@@ -143,8 +181,8 @@ module HTTPX
|
|
|
143
181
|
headers[":path"] = headline_uri(request)
|
|
144
182
|
headers[":authority"] = request.authority
|
|
145
183
|
headers = headers.merge(request.headers)
|
|
146
|
-
log(level: 1,
|
|
147
|
-
headers.map { |k, v| "-> HEADER: #{k}: #{v}" }.join("\n")
|
|
184
|
+
log(level: 1, color: :yellow) do
|
|
185
|
+
headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
|
|
148
186
|
end
|
|
149
187
|
stream.headers(headers, end_stream: request.empty?)
|
|
150
188
|
end
|
|
@@ -155,8 +193,8 @@ module HTTPX
|
|
|
155
193
|
chunk = @drains.delete(request) || request.drain_body
|
|
156
194
|
while chunk
|
|
157
195
|
next_chunk = request.drain_body
|
|
158
|
-
log(level: 1,
|
|
159
|
-
log(level: 2,
|
|
196
|
+
log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
|
|
197
|
+
log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
|
|
160
198
|
stream.data(chunk, end_stream: !next_chunk)
|
|
161
199
|
if next_chunk && @buffer.full?
|
|
162
200
|
@drains[request] = next_chunk
|
|
@@ -171,25 +209,25 @@ module HTTPX
|
|
|
171
209
|
######
|
|
172
210
|
|
|
173
211
|
def on_stream_headers(stream, request, h)
|
|
174
|
-
log(
|
|
175
|
-
h.map { |k, v| "<- HEADER: #{k}: #{v}" }.join("\n")
|
|
212
|
+
log(color: :yellow) do
|
|
213
|
+
h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
|
|
176
214
|
end
|
|
177
215
|
_, status = h.shift
|
|
178
216
|
headers = request.options.headers_class.new(h)
|
|
179
217
|
response = request.options.response_class.new(request, status, "2.0", headers)
|
|
180
218
|
request.response = response
|
|
181
219
|
@streams[request] = stream
|
|
220
|
+
|
|
221
|
+
handle(request, stream) if request.expects?
|
|
182
222
|
end
|
|
183
223
|
|
|
184
224
|
def on_stream_data(stream, request, data)
|
|
185
|
-
log(level: 1,
|
|
186
|
-
log(level: 2,
|
|
225
|
+
log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
|
|
226
|
+
log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
|
|
187
227
|
request.response << data
|
|
188
228
|
end
|
|
189
229
|
|
|
190
230
|
def on_stream_close(stream, request, error)
|
|
191
|
-
return handle(request, stream) if request.expects?
|
|
192
|
-
|
|
193
231
|
if error && error != :no_error
|
|
194
232
|
ex = Error.new(stream.id, error)
|
|
195
233
|
ex.set_backtrace(caller)
|
|
@@ -204,14 +242,13 @@ module HTTPX
|
|
|
204
242
|
emit(:response, request, response)
|
|
205
243
|
end
|
|
206
244
|
end
|
|
207
|
-
log(level: 2
|
|
245
|
+
log(level: 2) { "#{stream.id}: closing stream" }
|
|
208
246
|
|
|
209
247
|
@streams.delete(request)
|
|
210
248
|
send(@pending.shift) unless @pending.empty?
|
|
211
249
|
return unless @streams.empty? && exhausted?
|
|
212
250
|
|
|
213
251
|
close
|
|
214
|
-
emit(:close)
|
|
215
252
|
emit(:exhausted) unless @pending.empty?
|
|
216
253
|
end
|
|
217
254
|
|
|
@@ -222,7 +259,11 @@ module HTTPX
|
|
|
222
259
|
def on_settings(*)
|
|
223
260
|
@handshake_completed = true
|
|
224
261
|
|
|
225
|
-
|
|
262
|
+
if @max_requests.zero?
|
|
263
|
+
@max_requests = @connection.remote_settings[:settings_max_concurrent_streams]
|
|
264
|
+
|
|
265
|
+
@connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
|
|
266
|
+
end
|
|
226
267
|
|
|
227
268
|
@max_concurrent_requests = [@max_concurrent_requests, @max_requests].min
|
|
228
269
|
send_pending
|
|
@@ -242,32 +283,26 @@ module HTTPX
|
|
|
242
283
|
end
|
|
243
284
|
|
|
244
285
|
def on_frame_sent(frame)
|
|
245
|
-
log(level: 2
|
|
246
|
-
log(level: 2,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
else
|
|
251
|
-
frame.inspect
|
|
252
|
-
end
|
|
286
|
+
log(level: 2) { "#{frame[:stream]}: frame was sent!" }
|
|
287
|
+
log(level: 2, color: :blue) do
|
|
288
|
+
payload = frame
|
|
289
|
+
payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
|
|
290
|
+
"#{frame[:stream]}: #{payload}"
|
|
253
291
|
end
|
|
254
292
|
end
|
|
255
293
|
|
|
256
294
|
def on_frame_received(frame)
|
|
257
|
-
log(level: 2
|
|
258
|
-
log(level: 2,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
else
|
|
263
|
-
frame.inspect
|
|
264
|
-
end
|
|
295
|
+
log(level: 2) { "#{frame[:stream]}: frame was received!" }
|
|
296
|
+
log(level: 2, color: :magenta) do
|
|
297
|
+
payload = frame
|
|
298
|
+
payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
|
|
299
|
+
"#{frame[:stream]}: #{payload}"
|
|
265
300
|
end
|
|
266
301
|
end
|
|
267
302
|
|
|
268
303
|
def on_altsvc(origin, frame)
|
|
269
|
-
log(level: 2
|
|
270
|
-
log(level: 2
|
|
304
|
+
log(level: 2) { "#{frame[:stream]}: altsvc frame was received" }
|
|
305
|
+
log(level: 2) { "#{frame[:stream]}: #{frame.inspect}" }
|
|
271
306
|
alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
|
|
272
307
|
params = { "ma" => frame[:max_age] }
|
|
273
308
|
emit(:altsvc, origin, alt_origin, origin, params)
|
|
@@ -281,6 +316,14 @@ module HTTPX
|
|
|
281
316
|
emit(:origin, origin)
|
|
282
317
|
end
|
|
283
318
|
|
|
319
|
+
def on_pong(ping)
|
|
320
|
+
if !@pings.delete(ping)
|
|
321
|
+
close(:protocol_error, "ping payload did not match")
|
|
322
|
+
else
|
|
323
|
+
emit(:pong)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
284
327
|
def respond_to_missing?(meth, *args)
|
|
285
328
|
@connection.respond_to?(meth, *args) || super
|
|
286
329
|
end
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# domain_name.rb - Domain Name manipulation library for Ruby
|
|
5
|
+
#
|
|
6
|
+
# Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
|
|
7
|
+
#
|
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
|
9
|
+
# modification, are permitted provided that the following conditions
|
|
10
|
+
# are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright
|
|
12
|
+
# notice, this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
18
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
21
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
23
|
+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
24
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
25
|
+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
26
|
+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
27
|
+
# SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
require "ipaddr"
|
|
30
|
+
|
|
31
|
+
module HTTPX
|
|
32
|
+
# Represents a domain name ready for extracting its registered domain
|
|
33
|
+
# and TLD.
|
|
34
|
+
class DomainName
|
|
35
|
+
include Comparable
|
|
36
|
+
|
|
37
|
+
# The full host name normalized, ASCII-ized and downcased using the
|
|
38
|
+
# Unicode NFC rules and the Punycode algorithm. If initialized with
|
|
39
|
+
# an IP address, the string representation of the IP address
|
|
40
|
+
# suitable for opening a connection to.
|
|
41
|
+
attr_reader :hostname
|
|
42
|
+
|
|
43
|
+
# The Unicode representation of the #hostname property.
|
|
44
|
+
#
|
|
45
|
+
# :attr_reader: hostname_idn
|
|
46
|
+
|
|
47
|
+
# The least "universally original" domain part of this domain name.
|
|
48
|
+
# For example, "example.co.uk" for "www.sub.example.co.uk". This
|
|
49
|
+
# may be nil if the hostname does not have one, like when it is an
|
|
50
|
+
# IP address, an effective TLD or higher itself, or of a
|
|
51
|
+
# non-canonical domain.
|
|
52
|
+
attr_reader :domain
|
|
53
|
+
|
|
54
|
+
DOT = "." # :nodoc:
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
def new(domain)
|
|
58
|
+
return domain if domain.is_a?(self)
|
|
59
|
+
|
|
60
|
+
super(domain)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Normalizes a _domain_ using the Punycode algorithm as necessary.
|
|
64
|
+
# The result will be a downcased, ASCII-only string.
|
|
65
|
+
def normalize(domain)
|
|
66
|
+
domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
|
|
67
|
+
Punycode.encode_hostname(domain).downcase
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Parses _hostname_ into a DomainName object. An IP address is also
|
|
72
|
+
# accepted. An IPv6 address may be enclosed in square brackets.
|
|
73
|
+
def initialize(hostname)
|
|
74
|
+
hostname = String(hostname)
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
@ipaddr = IPAddr.new(hostname)
|
|
80
|
+
@hostname = @ipaddr.to_s
|
|
81
|
+
return
|
|
82
|
+
rescue IPAddr::Error
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@hostname = DomainName.normalize(hostname)
|
|
87
|
+
tld = if (last_dot = @hostname.rindex(DOT))
|
|
88
|
+
@hostname[(last_dot + 1)..-1]
|
|
89
|
+
else
|
|
90
|
+
@hostname
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# unknown/local TLD
|
|
94
|
+
@domain = if last_dot
|
|
95
|
+
# fallback - accept cookies down to second level
|
|
96
|
+
# cf. http://www.dkim-reputation.org/regdom-libs/
|
|
97
|
+
if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
|
|
98
|
+
@hostname[(penultimate_dot + 1)..-1]
|
|
99
|
+
else
|
|
100
|
+
@hostname
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
# no domain part - must be a local hostname
|
|
104
|
+
tld
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Checks if the server represented by this domain is qualified to
|
|
109
|
+
# send and receive cookies with a domain attribute value of
|
|
110
|
+
# _domain_. A true value given as the second argument represents
|
|
111
|
+
# cookies without a domain attribute value, in which case only
|
|
112
|
+
# hostname equality is checked.
|
|
113
|
+
def cookie_domain?(domain, host_only = false)
|
|
114
|
+
# RFC 6265 #5.3
|
|
115
|
+
# When the user agent "receives a cookie":
|
|
116
|
+
return self == @domain if host_only
|
|
117
|
+
|
|
118
|
+
domain = DomainName.new(domain)
|
|
119
|
+
|
|
120
|
+
# RFC 6265 #5.1.3
|
|
121
|
+
# Do not perform subdomain matching against IP addresses.
|
|
122
|
+
@hostname == domain.hostname if @ipaddr
|
|
123
|
+
|
|
124
|
+
# RFC 6265 #4.1.1
|
|
125
|
+
# Domain-value must be a subdomain.
|
|
126
|
+
@domain && self <= domain && domain <= @domain ? true : false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# def ==(other)
|
|
130
|
+
# other = DomainName.new(other)
|
|
131
|
+
# other.hostname == @hostname
|
|
132
|
+
# end
|
|
133
|
+
|
|
134
|
+
def <=>(other)
|
|
135
|
+
other = DomainName.new(other)
|
|
136
|
+
othername = other.hostname
|
|
137
|
+
if othername == @hostname
|
|
138
|
+
0
|
|
139
|
+
elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
|
|
140
|
+
# The other is higher
|
|
141
|
+
-1
|
|
142
|
+
elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT
|
|
143
|
+
# The other is lower
|
|
144
|
+
1
|
|
145
|
+
else
|
|
146
|
+
1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# :nocov:
|
|
151
|
+
# rubocop:disable all
|
|
152
|
+
# -*- coding: utf-8 -*-
|
|
153
|
+
#--
|
|
154
|
+
# punycode.rb - PunyCode encoder for the Domain Name library
|
|
155
|
+
#
|
|
156
|
+
# Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
|
|
157
|
+
#
|
|
158
|
+
# Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
|
|
159
|
+
# Library.
|
|
160
|
+
#
|
|
161
|
+
# Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
|
|
162
|
+
#
|
|
163
|
+
# Redistribution and use in source and binary forms, with or
|
|
164
|
+
# without modification, are permitted provided that the following
|
|
165
|
+
# conditions are met:
|
|
166
|
+
#
|
|
167
|
+
# 1) Redistributions of source code must retain the above copyright
|
|
168
|
+
# notice, this list of conditions and the following disclaimer.
|
|
169
|
+
#
|
|
170
|
+
# 2) Redistributions in binary form must reproduce the above copyright
|
|
171
|
+
# notice, this list of conditions and the following disclaimer in
|
|
172
|
+
# the documentation and/or other materials provided with the
|
|
173
|
+
# distribution.
|
|
174
|
+
#
|
|
175
|
+
# 3) Neither the name of the VeriSign Inc. nor the names of its
|
|
176
|
+
# contributors may be used to endorse or promote products derived
|
|
177
|
+
# from this software without specific prior written permission.
|
|
178
|
+
#
|
|
179
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
180
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
181
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
182
|
+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
183
|
+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
184
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
185
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
186
|
+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
187
|
+
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
188
|
+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
189
|
+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
190
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
191
|
+
#
|
|
192
|
+
# This software is licensed under the BSD open source license. For more
|
|
193
|
+
# information visit www.opensource.org.
|
|
194
|
+
#
|
|
195
|
+
# Authors:
|
|
196
|
+
# John Colosi (VeriSign)
|
|
197
|
+
# Srikanth Veeramachaneni (VeriSign)
|
|
198
|
+
# Nagesh Chigurupati (Verisign)
|
|
199
|
+
# Praveen Srinivasan(Verisign)
|
|
200
|
+
#++
|
|
201
|
+
module Punycode
|
|
202
|
+
BASE = 36
|
|
203
|
+
TMIN = 1
|
|
204
|
+
TMAX = 26
|
|
205
|
+
SKEW = 38
|
|
206
|
+
DAMP = 700
|
|
207
|
+
INITIAL_BIAS = 72
|
|
208
|
+
INITIAL_N = 0x80
|
|
209
|
+
DELIMITER = "-"
|
|
210
|
+
|
|
211
|
+
MAXINT = (1 << 32) - 1
|
|
212
|
+
|
|
213
|
+
LOBASE = BASE - TMIN
|
|
214
|
+
CUTOFF = LOBASE * TMAX / 2
|
|
215
|
+
|
|
216
|
+
RE_NONBASIC = /[^\x00-\x7f]/.freeze
|
|
217
|
+
|
|
218
|
+
# Returns the numeric value of a basic code point (for use in
|
|
219
|
+
# representing integers) in the range 0 to base-1, or nil if cp
|
|
220
|
+
# is does not represent a value.
|
|
221
|
+
DECODE_DIGIT = {}.tap do |map|
|
|
222
|
+
# ASCII A..Z map to 0..25
|
|
223
|
+
# ASCII a..z map to 0..25
|
|
224
|
+
(0..25).each { |i| map[65 + i] = map[97 + i] = i }
|
|
225
|
+
# ASCII 0..9 map to 26..35
|
|
226
|
+
(26..35).each { |i| map[22 + i] = i }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Returns the basic code point whose value (when used for
|
|
230
|
+
# representing integers) is d, which must be in the range 0 to
|
|
231
|
+
# BASE-1. The lowercase form is used unless flag is true, in
|
|
232
|
+
# which case the uppercase form is used. The behavior is
|
|
233
|
+
# undefined if flag is nonzero and digit d has no uppercase
|
|
234
|
+
# form.
|
|
235
|
+
ENCODE_DIGIT = proc { |d, flag|
|
|
236
|
+
(d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
|
|
237
|
+
# 0..25 map to ASCII a..z or A..Z
|
|
238
|
+
# 26..35 map to ASCII 0..9
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
DOT = "."
|
|
242
|
+
PREFIX = "xn--"
|
|
243
|
+
|
|
244
|
+
# Most errors we raise are basically kind of ArgumentError.
|
|
245
|
+
class ArgumentError < ::ArgumentError; end
|
|
246
|
+
class BufferOverflowError < ArgumentError; end
|
|
247
|
+
|
|
248
|
+
class << self
|
|
249
|
+
# Encode a +string+ in Punycode
|
|
250
|
+
def encode(string)
|
|
251
|
+
input = string.unpack("U*")
|
|
252
|
+
output = +""
|
|
253
|
+
|
|
254
|
+
# Initialize the state
|
|
255
|
+
n = INITIAL_N
|
|
256
|
+
delta = 0
|
|
257
|
+
bias = INITIAL_BIAS
|
|
258
|
+
|
|
259
|
+
# Handle the basic code points
|
|
260
|
+
input.each { |cp| output << cp.chr if cp < 0x80 }
|
|
261
|
+
|
|
262
|
+
h = b = output.length
|
|
263
|
+
|
|
264
|
+
# h is the number of code points that have been handled, b is the
|
|
265
|
+
# number of basic code points, and out is the number of characters
|
|
266
|
+
# that have been output.
|
|
267
|
+
|
|
268
|
+
output << DELIMITER if b > 0
|
|
269
|
+
|
|
270
|
+
# Main encoding loop
|
|
271
|
+
|
|
272
|
+
while h < input.length
|
|
273
|
+
# All non-basic code points < n have been handled already. Find
|
|
274
|
+
# the next larger one
|
|
275
|
+
|
|
276
|
+
m = MAXINT
|
|
277
|
+
input.each do |cp|
|
|
278
|
+
m = cp if (n...m) === cp
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Increase delta enough to advance the decoder's <n,i> state to
|
|
282
|
+
# <m,0>, but guard against overflow
|
|
283
|
+
|
|
284
|
+
delta += (m - n) * (h + 1)
|
|
285
|
+
raise BufferOverflowError if delta > MAXINT
|
|
286
|
+
|
|
287
|
+
n = m
|
|
288
|
+
|
|
289
|
+
input.each do |cp|
|
|
290
|
+
# AMC-ACE-Z can use this simplified version instead
|
|
291
|
+
if cp < n
|
|
292
|
+
delta += 1
|
|
293
|
+
raise BufferOverflowError if delta > MAXINT
|
|
294
|
+
elsif cp == n
|
|
295
|
+
# Represent delta as a generalized variable-length integer
|
|
296
|
+
q = delta
|
|
297
|
+
k = BASE
|
|
298
|
+
loop do
|
|
299
|
+
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
|
|
300
|
+
break if q < t
|
|
301
|
+
|
|
302
|
+
q, r = (q - t).divmod(BASE - t)
|
|
303
|
+
output << ENCODE_DIGIT[t + r, false]
|
|
304
|
+
k += BASE
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
output << ENCODE_DIGIT[q, false]
|
|
308
|
+
|
|
309
|
+
# Adapt the bias
|
|
310
|
+
delta = h == b ? delta / DAMP : delta >> 1
|
|
311
|
+
delta += delta / (h + 1)
|
|
312
|
+
bias = 0
|
|
313
|
+
while delta > CUTOFF
|
|
314
|
+
delta /= LOBASE
|
|
315
|
+
bias += BASE
|
|
316
|
+
end
|
|
317
|
+
bias += (LOBASE + 1) * delta / (delta + SKEW)
|
|
318
|
+
|
|
319
|
+
delta = 0
|
|
320
|
+
h += 1
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
delta += 1
|
|
325
|
+
n += 1
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
output
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Encode a hostname using IDN/Punycode algorithms
|
|
332
|
+
def encode_hostname(hostname)
|
|
333
|
+
hostname.match(RE_NONBASIC) || (return hostname)
|
|
334
|
+
|
|
335
|
+
hostname.split(DOT).map do |name|
|
|
336
|
+
if name.match(RE_NONBASIC)
|
|
337
|
+
PREFIX + encode(name)
|
|
338
|
+
else
|
|
339
|
+
name
|
|
340
|
+
end
|
|
341
|
+
end.join(DOT)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Decode a +string+ encoded in Punycode
|
|
345
|
+
def decode(string)
|
|
346
|
+
# Initialize the state
|
|
347
|
+
n = INITIAL_N
|
|
348
|
+
i = 0
|
|
349
|
+
bias = INITIAL_BIAS
|
|
350
|
+
|
|
351
|
+
if j = string.rindex(DELIMITER)
|
|
352
|
+
b = string[0...j]
|
|
353
|
+
|
|
354
|
+
b.match(RE_NONBASIC) &&
|
|
355
|
+
raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
|
|
356
|
+
|
|
357
|
+
# Handle the basic code points
|
|
358
|
+
|
|
359
|
+
output = b.unpack("U*")
|
|
360
|
+
u = string[(j + 1)..-1]
|
|
361
|
+
else
|
|
362
|
+
output = []
|
|
363
|
+
u = string
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Main decoding loop: Start just after the last delimiter if any
|
|
367
|
+
# basic code points were copied; start at the beginning
|
|
368
|
+
# otherwise.
|
|
369
|
+
|
|
370
|
+
input = u.unpack("C*")
|
|
371
|
+
input_length = input.length
|
|
372
|
+
h = 0
|
|
373
|
+
out = output.length
|
|
374
|
+
|
|
375
|
+
while h < input_length
|
|
376
|
+
# Decode a generalized variable-length integer into delta,
|
|
377
|
+
# which gets added to i. The overflow checking is easier
|
|
378
|
+
# if we increase i as we go, then subtract off its starting
|
|
379
|
+
# value at the end to obtain delta.
|
|
380
|
+
|
|
381
|
+
oldi = i
|
|
382
|
+
w = 1
|
|
383
|
+
k = BASE
|
|
384
|
+
|
|
385
|
+
loop do
|
|
386
|
+
(digit = DECODE_DIGIT[input[h]]) ||
|
|
387
|
+
raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
|
|
388
|
+
h += 1
|
|
389
|
+
i += digit * w
|
|
390
|
+
raise BufferOverflowError if i > MAXINT
|
|
391
|
+
|
|
392
|
+
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
|
|
393
|
+
break if digit < t
|
|
394
|
+
|
|
395
|
+
w *= BASE - t
|
|
396
|
+
raise BufferOverflowError if w > MAXINT
|
|
397
|
+
|
|
398
|
+
k += BASE
|
|
399
|
+
(h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Adapt the bias
|
|
403
|
+
delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
|
|
404
|
+
delta += delta / (out + 1)
|
|
405
|
+
bias = 0
|
|
406
|
+
while delta > CUTOFF
|
|
407
|
+
delta /= LOBASE
|
|
408
|
+
bias += BASE
|
|
409
|
+
end
|
|
410
|
+
bias += (LOBASE + 1) * delta / (delta + SKEW)
|
|
411
|
+
|
|
412
|
+
# i was supposed to wrap around from out+1 to 0, incrementing
|
|
413
|
+
# n each time, so we'll fix that now:
|
|
414
|
+
|
|
415
|
+
q, i = i.divmod(out + 1)
|
|
416
|
+
n += q
|
|
417
|
+
raise BufferOverflowError if n > MAXINT
|
|
418
|
+
|
|
419
|
+
# Insert n at position i of the output:
|
|
420
|
+
|
|
421
|
+
output[i, 0] = n
|
|
422
|
+
|
|
423
|
+
out += 1
|
|
424
|
+
i += 1
|
|
425
|
+
end
|
|
426
|
+
output.pack("U*")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Decode a hostname using IDN/Punycode algorithms
|
|
430
|
+
def decode_hostname(hostname)
|
|
431
|
+
hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
|
|
432
|
+
Regexp.last_match(1) << decode(Regexp.last_match(2))
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
# rubocop:enable all
|
|
437
|
+
# :nocov:
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|