httpx-patched 1.6.2.1
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 +7 -0
- data/LICENSE.txt +191 -0
- data/README.md +162 -0
- 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_10_1.md +37 -0
- data/doc/release_notes/0_10_2.md +5 -0
- data/doc/release_notes/0_11_0.md +74 -0
- 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/doc/release_notes/0_13_1.md +5 -0
- data/doc/release_notes/0_13_2.md +9 -0
- data/doc/release_notes/0_14_0.md +79 -0
- data/doc/release_notes/0_14_1.md +7 -0
- data/doc/release_notes/0_14_2.md +6 -0
- data/doc/release_notes/0_14_3.md +5 -0
- data/doc/release_notes/0_14_4.md +5 -0
- data/doc/release_notes/0_14_5.md +11 -0
- data/doc/release_notes/0_15_0.md +53 -0
- data/doc/release_notes/0_15_1.md +8 -0
- data/doc/release_notes/0_15_2.md +9 -0
- data/doc/release_notes/0_15_3.md +5 -0
- data/doc/release_notes/0_15_4.md +5 -0
- data/doc/release_notes/0_16_0.md +93 -0
- data/doc/release_notes/0_16_1.md +5 -0
- data/doc/release_notes/0_17_0.md +49 -0
- data/doc/release_notes/0_18_0.md +69 -0
- data/doc/release_notes/0_18_1.md +12 -0
- data/doc/release_notes/0_18_2.md +10 -0
- data/doc/release_notes/0_18_3.md +7 -0
- data/doc/release_notes/0_18_4.md +14 -0
- data/doc/release_notes/0_18_5.md +10 -0
- data/doc/release_notes/0_18_6.md +5 -0
- data/doc/release_notes/0_18_7.md +5 -0
- data/doc/release_notes/0_19_0.md +39 -0
- data/doc/release_notes/0_19_1.md +5 -0
- data/doc/release_notes/0_19_2.md +7 -0
- data/doc/release_notes/0_19_3.md +6 -0
- data/doc/release_notes/0_19_4.md +14 -0
- data/doc/release_notes/0_19_5.md +13 -0
- data/doc/release_notes/0_19_6.md +5 -0
- data/doc/release_notes/0_19_7.md +5 -0
- data/doc/release_notes/0_19_8.md +5 -0
- data/doc/release_notes/0_1_0.md +9 -0
- data/doc/release_notes/0_20_0.md +36 -0
- data/doc/release_notes/0_20_1.md +5 -0
- data/doc/release_notes/0_20_2.md +7 -0
- data/doc/release_notes/0_20_3.md +6 -0
- data/doc/release_notes/0_20_4.md +17 -0
- data/doc/release_notes/0_20_5.md +3 -0
- data/doc/release_notes/0_21_0.md +96 -0
- data/doc/release_notes/0_21_1.md +12 -0
- data/doc/release_notes/0_22_0.md +13 -0
- data/doc/release_notes/0_22_1.md +11 -0
- data/doc/release_notes/0_22_2.md +5 -0
- data/doc/release_notes/0_22_3.md +55 -0
- data/doc/release_notes/0_22_4.md +6 -0
- data/doc/release_notes/0_22_5.md +6 -0
- data/doc/release_notes/0_23_0.md +42 -0
- data/doc/release_notes/0_23_1.md +5 -0
- data/doc/release_notes/0_23_2.md +5 -0
- data/doc/release_notes/0_23_3.md +6 -0
- data/doc/release_notes/0_23_4.md +5 -0
- data/doc/release_notes/0_24_0.md +48 -0
- data/doc/release_notes/0_24_1.md +12 -0
- data/doc/release_notes/0_24_2.md +12 -0
- data/doc/release_notes/0_24_3.md +12 -0
- data/doc/release_notes/0_24_4.md +18 -0
- data/doc/release_notes/0_24_5.md +6 -0
- data/doc/release_notes/0_24_6.md +5 -0
- data/doc/release_notes/0_24_7.md +10 -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/doc/release_notes/1_0_0.md +60 -0
- data/doc/release_notes/1_0_1.md +5 -0
- data/doc/release_notes/1_0_2.md +7 -0
- data/doc/release_notes/1_1_0.md +32 -0
- data/doc/release_notes/1_1_1.md +17 -0
- data/doc/release_notes/1_1_2.md +12 -0
- data/doc/release_notes/1_1_3.md +18 -0
- data/doc/release_notes/1_1_4.md +6 -0
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/doc/release_notes/1_2_1.md +6 -0
- data/doc/release_notes/1_2_2.md +10 -0
- data/doc/release_notes/1_2_3.md +16 -0
- data/doc/release_notes/1_2_4.md +8 -0
- data/doc/release_notes/1_2_5.md +7 -0
- data/doc/release_notes/1_2_6.md +13 -0
- data/doc/release_notes/1_3_0.md +18 -0
- data/doc/release_notes/1_3_1.md +17 -0
- data/doc/release_notes/1_3_2.md +6 -0
- data/doc/release_notes/1_3_3.md +5 -0
- data/doc/release_notes/1_3_4.md +6 -0
- data/doc/release_notes/1_4_0.md +43 -0
- data/doc/release_notes/1_4_1.md +19 -0
- data/doc/release_notes/1_4_2.md +20 -0
- data/doc/release_notes/1_4_3.md +11 -0
- data/doc/release_notes/1_4_4.md +14 -0
- data/doc/release_notes/1_5_0.md +126 -0
- data/doc/release_notes/1_5_1.md +6 -0
- data/doc/release_notes/1_6_0.md +50 -0
- data/doc/release_notes/1_6_1.md +17 -0
- data/doc/release_notes/1_6_2.md +11 -0
- data/lib/httpx/adapters/datadog.rb +359 -0
- data/lib/httpx/adapters/faraday.rb +303 -0
- data/lib/httpx/adapters/sentry.rb +121 -0
- data/lib/httpx/adapters/webmock.rb +175 -0
- data/lib/httpx/altsvc.rb +163 -0
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/buffer.rb +61 -0
- data/lib/httpx/callbacks.rb +35 -0
- data/lib/httpx/chainable.rb +106 -0
- data/lib/httpx/connection/http1.rb +399 -0
- data/lib/httpx/connection/http2.rb +468 -0
- data/lib/httpx/connection.rb +954 -0
- data/lib/httpx/domain_name.rb +145 -0
- data/lib/httpx/errors.rb +111 -0
- data/lib/httpx/extensions.rb +59 -0
- data/lib/httpx/headers.rb +176 -0
- data/lib/httpx/io/ssl.rb +163 -0
- data/lib/httpx/io/tcp.rb +239 -0
- data/lib/httpx/io/udp.rb +62 -0
- data/lib/httpx/io/unix.rb +71 -0
- data/lib/httpx/io.rb +11 -0
- data/lib/httpx/loggable.rb +56 -0
- data/lib/httpx/options.rb +463 -0
- data/lib/httpx/parser/http1.rb +186 -0
- data/lib/httpx/plugins/auth/basic.rb +20 -0
- data/lib/httpx/plugins/auth/digest.rb +102 -0
- data/lib/httpx/plugins/auth/ntlm.rb +35 -0
- data/lib/httpx/plugins/auth/socks5.rb +22 -0
- data/lib/httpx/plugins/auth.rb +25 -0
- data/lib/httpx/plugins/aws_sdk_authentication.rb +111 -0
- data/lib/httpx/plugins/aws_sigv4.rb +239 -0
- data/lib/httpx/plugins/basic_auth.rb +29 -0
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/callbacks.rb +127 -0
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
- data/lib/httpx/plugins/circuit_breaker.rb +147 -0
- data/lib/httpx/plugins/content_digest.rb +204 -0
- data/lib/httpx/plugins/cookies/cookie.rb +174 -0
- data/lib/httpx/plugins/cookies/jar.rb +95 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +143 -0
- data/lib/httpx/plugins/cookies.rb +107 -0
- data/lib/httpx/plugins/digest_auth.rb +67 -0
- data/lib/httpx/plugins/expect.rb +120 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
- data/lib/httpx/plugins/follow_redirects.rb +233 -0
- data/lib/httpx/plugins/grpc/call.rb +63 -0
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +90 -0
- data/lib/httpx/plugins/grpc/message.rb +55 -0
- data/lib/httpx/plugins/grpc.rb +282 -0
- data/lib/httpx/plugins/h2c.rb +127 -0
- data/lib/httpx/plugins/internal_telemetry.rb +107 -0
- data/lib/httpx/plugins/ntlm_auth.rb +62 -0
- data/lib/httpx/plugins/oauth.rb +183 -0
- data/lib/httpx/plugins/persistent.rb +82 -0
- data/lib/httpx/plugins/proxy/http.rb +184 -0
- data/lib/httpx/plugins/proxy/socks4.rb +135 -0
- data/lib/httpx/plugins/proxy/socks5.rb +194 -0
- data/lib/httpx/plugins/proxy/ssh.rb +94 -0
- data/lib/httpx/plugins/proxy.rb +349 -0
- data/lib/httpx/plugins/push_promise.rb +81 -0
- data/lib/httpx/plugins/query.rb +35 -0
- data/lib/httpx/plugins/rate_limiter.rb +55 -0
- data/lib/httpx/plugins/response_cache/file_store.rb +140 -0
- data/lib/httpx/plugins/response_cache/store.rb +33 -0
- data/lib/httpx/plugins/response_cache.rb +333 -0
- data/lib/httpx/plugins/retries.rb +230 -0
- data/lib/httpx/plugins/ssrf_filter.rb +145 -0
- data/lib/httpx/plugins/stream.rb +183 -0
- data/lib/httpx/plugins/stream_bidi.rb +315 -0
- data/lib/httpx/plugins/upgrade/h2.rb +64 -0
- data/lib/httpx/plugins/upgrade.rb +86 -0
- data/lib/httpx/plugins/webdav.rb +86 -0
- data/lib/httpx/plugins/xml.rb +76 -0
- data/lib/httpx/pmatch_extensions.rb +33 -0
- data/lib/httpx/pool.rb +190 -0
- data/lib/httpx/punycode.rb +22 -0
- data/lib/httpx/request/body.rb +158 -0
- data/lib/httpx/request.rb +328 -0
- data/lib/httpx/resolver/entry.rb +30 -0
- data/lib/httpx/resolver/https.rb +256 -0
- data/lib/httpx/resolver/multi.rb +102 -0
- data/lib/httpx/resolver/native.rb +547 -0
- data/lib/httpx/resolver/resolver.rb +173 -0
- data/lib/httpx/resolver/system.rb +255 -0
- data/lib/httpx/resolver.rb +189 -0
- data/lib/httpx/response/body.rb +242 -0
- data/lib/httpx/response/buffer.rb +115 -0
- data/lib/httpx/response.rb +304 -0
- data/lib/httpx/selector.rb +282 -0
- data/lib/httpx/session.rb +612 -0
- data/lib/httpx/session_extensions.rb +30 -0
- data/lib/httpx/timers.rb +133 -0
- data/lib/httpx/transcoder/body.rb +43 -0
- data/lib/httpx/transcoder/chunker.rb +115 -0
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +68 -0
- data/lib/httpx/transcoder/gzip.rb +71 -0
- data/lib/httpx/transcoder/json.rb +71 -0
- data/lib/httpx/transcoder/multipart/decoder.rb +141 -0
- data/lib/httpx/transcoder/multipart/encoder.rb +120 -0
- data/lib/httpx/transcoder/multipart/mime_type_detector.rb +78 -0
- data/lib/httpx/transcoder/multipart/part.rb +35 -0
- data/lib/httpx/transcoder/multipart.rb +31 -0
- data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
- data/lib/httpx/transcoder/utils/deflater.rb +75 -0
- data/lib/httpx/transcoder.rb +91 -0
- data/lib/httpx/utils.rb +75 -0
- data/lib/httpx/version.rb +5 -0
- data/lib/httpx.rb +66 -0
- data/sig/altsvc.rbs +33 -0
- data/sig/buffer.rbs +27 -0
- data/sig/callbacks.rbs +15 -0
- data/sig/chainable.rbs +55 -0
- data/sig/connection/http1.rbs +85 -0
- data/sig/connection/http2.rbs +116 -0
- data/sig/connection.rbs +169 -0
- data/sig/domain_name.rbs +17 -0
- data/sig/errors.rbs +69 -0
- data/sig/headers.rbs +49 -0
- data/sig/httpx.rbs +27 -0
- data/sig/io/ssl.rbs +27 -0
- data/sig/io/tcp.rbs +72 -0
- data/sig/io/udp.rbs +25 -0
- data/sig/io/unix.rbs +26 -0
- data/sig/io.rbs +3 -0
- data/sig/loggable.rbs +17 -0
- data/sig/options.rbs +202 -0
- data/sig/parser/http1.rbs +59 -0
- data/sig/plugins/auth/basic.rbs +17 -0
- data/sig/plugins/auth/digest.rbs +25 -0
- data/sig/plugins/auth/ntlm.rbs +20 -0
- data/sig/plugins/auth/socks5.rbs +18 -0
- data/sig/plugins/auth.rbs +13 -0
- data/sig/plugins/aws_sdk_authentication.rbs +43 -0
- data/sig/plugins/aws_sigv4.rbs +78 -0
- data/sig/plugins/basic_auth.rbs +15 -0
- data/sig/plugins/brotli.rbs +22 -0
- data/sig/plugins/callbacks.rbs +38 -0
- data/sig/plugins/circuit_breaker.rbs +71 -0
- data/sig/plugins/compression.rbs +57 -0
- data/sig/plugins/content_digest.rbs +51 -0
- data/sig/plugins/cookies/cookie.rbs +55 -0
- data/sig/plugins/cookies/jar.rbs +26 -0
- data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
- data/sig/plugins/cookies.rbs +28 -0
- data/sig/plugins/digest_auth.rbs +21 -0
- data/sig/plugins/expect.rbs +15 -0
- data/sig/plugins/fiber_concurrency.rbs +51 -0
- data/sig/plugins/follow_redirects.rbs +47 -0
- data/sig/plugins/grpc/call.rbs +23 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +65 -0
- data/sig/plugins/h2c.rbs +27 -0
- data/sig/plugins/ntlm_auth.rbs +21 -0
- data/sig/plugins/oauth.rbs +68 -0
- data/sig/plugins/persistent.rbs +14 -0
- data/sig/plugins/proxy/http.rbs +30 -0
- data/sig/plugins/proxy/socks4.rbs +37 -0
- data/sig/plugins/proxy/socks5.rbs +49 -0
- data/sig/plugins/proxy/ssh.rbs +18 -0
- data/sig/plugins/proxy.rbs +70 -0
- data/sig/plugins/push_promise.rbs +23 -0
- data/sig/plugins/query.rbs +18 -0
- data/sig/plugins/rate_limiter.rbs +13 -0
- data/sig/plugins/response_cache/file_store.rbs +19 -0
- data/sig/plugins/response_cache/store.rbs +13 -0
- data/sig/plugins/response_cache.rbs +86 -0
- data/sig/plugins/retries.rbs +66 -0
- data/sig/plugins/ssrf_filter.rbs +26 -0
- data/sig/plugins/stream.rbs +54 -0
- data/sig/plugins/stream_bidi.rbs +68 -0
- data/sig/plugins/upgrade/h2.rbs +9 -0
- data/sig/plugins/upgrade.rbs +29 -0
- data/sig/plugins/webdav.rbs +23 -0
- data/sig/plugins/xml.rbs +37 -0
- data/sig/pool.rbs +51 -0
- data/sig/punycode.rbs +5 -0
- data/sig/request/body.rbs +34 -0
- data/sig/request.rbs +88 -0
- data/sig/resolver/entry.rbs +13 -0
- data/sig/resolver/https.rbs +45 -0
- data/sig/resolver/multi.rbs +32 -0
- data/sig/resolver/native.rbs +74 -0
- data/sig/resolver/resolver.rbs +64 -0
- data/sig/resolver/system.rbs +34 -0
- data/sig/resolver.rbs +48 -0
- data/sig/response/body.rbs +52 -0
- data/sig/response/buffer.rbs +23 -0
- data/sig/response.rbs +103 -0
- data/sig/selector.rbs +68 -0
- data/sig/session.rbs +104 -0
- data/sig/timers.rbs +54 -0
- data/sig/transcoder/body.rbs +24 -0
- data/sig/transcoder/chunker.rbs +49 -0
- data/sig/transcoder/deflate.rbs +12 -0
- data/sig/transcoder/form.rbs +34 -0
- data/sig/transcoder/gzip.rbs +27 -0
- data/sig/transcoder/json.rbs +28 -0
- data/sig/transcoder/multipart.rbs +103 -0
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +28 -0
- data/sig/transcoder.rbs +43 -0
- data/sig/utils.rbs +19 -0
- metadata +518 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for bidirectional HTTP/2 streams.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/StreamBidi
|
|
9
|
+
#
|
|
10
|
+
# It is required that the request body allows chunk to be buffered, (i.e., responds to +#<<(chunk)+).
|
|
11
|
+
module StreamBidi
|
|
12
|
+
# Extension of the Connection::HTTP2 class, which adds functionality to
|
|
13
|
+
# deal with a request that can't be drained and must be interleaved with
|
|
14
|
+
# the response streams.
|
|
15
|
+
#
|
|
16
|
+
# The streams keeps send DATA frames while there's data; when they're ain't,
|
|
17
|
+
# the stream is kept open; it must be explicitly closed by the end user.
|
|
18
|
+
#
|
|
19
|
+
class HTTP2Bidi < Connection::HTTP2
|
|
20
|
+
def initialize(*)
|
|
21
|
+
super
|
|
22
|
+
@lock = Thread::Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
%i[close empty? exhausted? send <<].each do |lock_meth|
|
|
26
|
+
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
27
|
+
# lock.aware version of +#{lock_meth}+
|
|
28
|
+
def #{lock_meth}(*) # def close(*)
|
|
29
|
+
return super if @lock.owned?
|
|
30
|
+
|
|
31
|
+
# small race condition between
|
|
32
|
+
# checking for ownership and
|
|
33
|
+
# acquiring lock.
|
|
34
|
+
# TODO: fix this at the parser.
|
|
35
|
+
@lock.synchronize { super }
|
|
36
|
+
end
|
|
37
|
+
METH
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
%i[join_headers join_trailers join_body].each do |lock_meth|
|
|
43
|
+
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
44
|
+
# lock.aware version of +#{lock_meth}+
|
|
45
|
+
private def #{lock_meth}(*) # private def join_headers(*)
|
|
46
|
+
return super if @lock.owned?
|
|
47
|
+
|
|
48
|
+
# small race condition between
|
|
49
|
+
# checking for ownership and
|
|
50
|
+
# acquiring lock.
|
|
51
|
+
# TODO: fix this at the parser.
|
|
52
|
+
@lock.synchronize { super }
|
|
53
|
+
end
|
|
54
|
+
METH
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_stream(stream, request)
|
|
58
|
+
request.on(:body) do
|
|
59
|
+
next unless request.headers_sent
|
|
60
|
+
|
|
61
|
+
handle(request, stream)
|
|
62
|
+
|
|
63
|
+
emit(:flush_buffer)
|
|
64
|
+
end
|
|
65
|
+
super
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# when there ain't more chunks, it makes the buffer as full.
|
|
69
|
+
def send_chunk(request, stream, chunk, next_chunk)
|
|
70
|
+
super
|
|
71
|
+
|
|
72
|
+
return if next_chunk
|
|
73
|
+
|
|
74
|
+
request.transition(:waiting_for_chunk)
|
|
75
|
+
throw(:buffer_full)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# sets end-stream flag when the request is closed.
|
|
79
|
+
def end_stream?(request, next_chunk)
|
|
80
|
+
request.closed? && next_chunk.nil?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# BidiBuffer is a Buffer which can be receive data from threads othr
|
|
85
|
+
# than the thread of the corresponding Connection/Session.
|
|
86
|
+
#
|
|
87
|
+
# It synchronizes access to a secondary internal +@oob_buffer+, which periodically
|
|
88
|
+
# is reconciled to the main internal +@buffer+.
|
|
89
|
+
class BidiBuffer < Buffer
|
|
90
|
+
def initialize(*)
|
|
91
|
+
super
|
|
92
|
+
@parent_thread = Thread.current
|
|
93
|
+
@oob_mutex = Thread::Mutex.new
|
|
94
|
+
@oob_buffer = "".b
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# buffers the +chunk+ to be sent
|
|
98
|
+
def <<(chunk)
|
|
99
|
+
return super if Thread.current == @parent_thread
|
|
100
|
+
|
|
101
|
+
@oob_mutex.synchronize { @oob_buffer << chunk }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# reconciles the main and secondary buffer (which receives data from other threads).
|
|
105
|
+
def rebuffer
|
|
106
|
+
raise Error, "can only rebuffer while waiting on a response" unless Thread.current == @parent_thread
|
|
107
|
+
|
|
108
|
+
@oob_mutex.synchronize do
|
|
109
|
+
@buffer << @oob_buffer
|
|
110
|
+
@oob_buffer.clear
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Proxy to wake up the session main loop when one
|
|
116
|
+
# of the connections has buffered data to write. It abides by the HTTPX::_Selectable API,
|
|
117
|
+
# which allows it to be registered in the selector alongside actual HTTP-based
|
|
118
|
+
# HTTPX::Connection objects.
|
|
119
|
+
class Signal
|
|
120
|
+
def initialize
|
|
121
|
+
@closed = false
|
|
122
|
+
@pipe_read, @pipe_write = IO.pipe
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def state
|
|
126
|
+
@closed ? :closed : :open
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# noop
|
|
130
|
+
def log(**, &_); end
|
|
131
|
+
|
|
132
|
+
def to_io
|
|
133
|
+
@pipe_read.to_io
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def wakeup
|
|
137
|
+
return if @closed
|
|
138
|
+
|
|
139
|
+
@pipe_write.write("\0")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def call
|
|
143
|
+
return if @closed
|
|
144
|
+
|
|
145
|
+
@pipe_read.readpartial(1)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def interests
|
|
149
|
+
return if @closed
|
|
150
|
+
|
|
151
|
+
:r
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def timeout; end
|
|
155
|
+
|
|
156
|
+
def terminate
|
|
157
|
+
@pipe_write.close
|
|
158
|
+
@pipe_read.close
|
|
159
|
+
@closed = true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# noop (the owner connection will take of it)
|
|
163
|
+
def handle_socket_timeout(interval); end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class << self
|
|
167
|
+
def load_dependencies(klass)
|
|
168
|
+
klass.plugin(:stream)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def extra_options(options)
|
|
172
|
+
options.merge(fallback_protocol: "h2")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
module InstanceMethods
|
|
177
|
+
def initialize(*)
|
|
178
|
+
@signal = Signal.new
|
|
179
|
+
super
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def close(selector = Selector.new)
|
|
183
|
+
@signal.terminate
|
|
184
|
+
selector.deregister(@signal)
|
|
185
|
+
super(selector)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def select_connection(connection, selector)
|
|
189
|
+
super
|
|
190
|
+
selector.register(@signal)
|
|
191
|
+
connection.signal = @signal
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def deselect_connection(connection, *)
|
|
195
|
+
super
|
|
196
|
+
connection.signal = nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Adds synchronization to request operations which may buffer payloads from different
|
|
201
|
+
# threads.
|
|
202
|
+
module RequestMethods
|
|
203
|
+
attr_accessor :headers_sent
|
|
204
|
+
|
|
205
|
+
def initialize(*)
|
|
206
|
+
super
|
|
207
|
+
@headers_sent = false
|
|
208
|
+
@closed = false
|
|
209
|
+
@mutex = Thread::Mutex.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def closed?
|
|
213
|
+
@closed
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def can_buffer?
|
|
217
|
+
super && @state != :waiting_for_chunk
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# overrides state management transitions to introduce an intermediate
|
|
221
|
+
# +:waiting_for_chunk+ state, which the request transitions to once payload
|
|
222
|
+
# is buffered.
|
|
223
|
+
def transition(nextstate)
|
|
224
|
+
headers_sent = @headers_sent
|
|
225
|
+
|
|
226
|
+
case nextstate
|
|
227
|
+
when :waiting_for_chunk
|
|
228
|
+
return unless @state == :body
|
|
229
|
+
when :body
|
|
230
|
+
case @state
|
|
231
|
+
when :headers
|
|
232
|
+
headers_sent = true
|
|
233
|
+
when :waiting_for_chunk
|
|
234
|
+
# HACK: to allow super to pass through
|
|
235
|
+
@state = :headers
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
super.tap do
|
|
240
|
+
# delay setting this up until after the first transition to :body
|
|
241
|
+
@headers_sent = headers_sent
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def <<(chunk)
|
|
246
|
+
@mutex.synchronize do
|
|
247
|
+
if @drainer
|
|
248
|
+
@body.clear if @body.respond_to?(:clear)
|
|
249
|
+
@drainer = nil
|
|
250
|
+
end
|
|
251
|
+
@body << chunk
|
|
252
|
+
|
|
253
|
+
transition(:body)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def close
|
|
258
|
+
@mutex.synchronize do
|
|
259
|
+
return if @closed
|
|
260
|
+
|
|
261
|
+
@closed = true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# last chunk to send which ends the stream
|
|
265
|
+
self << ""
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
module RequestBodyMethods
|
|
270
|
+
def initialize(*, **)
|
|
271
|
+
super
|
|
272
|
+
@headers.delete("content-length")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def empty?
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# overrides the declaration of +@write_buffer+, which is now a thread-safe buffer
|
|
281
|
+
# responding to the same API.
|
|
282
|
+
module ConnectionMethods
|
|
283
|
+
attr_writer :signal
|
|
284
|
+
|
|
285
|
+
def initialize(*)
|
|
286
|
+
super
|
|
287
|
+
@write_buffer = BidiBuffer.new(@options.buffer_size)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# rebuffers the +@write_buffer+ before calculating interests.
|
|
291
|
+
def interests
|
|
292
|
+
@write_buffer.rebuffer
|
|
293
|
+
|
|
294
|
+
super
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def parser_type(protocol)
|
|
300
|
+
return HTTP2Bidi if protocol == "h2"
|
|
301
|
+
|
|
302
|
+
super
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def set_parser_callbacks(parser)
|
|
306
|
+
super
|
|
307
|
+
parser.on(:flush_buffer) do
|
|
308
|
+
@signal.wakeup if @signal
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
register_plugin :stream_bidi, StreamBidi
|
|
314
|
+
end
|
|
315
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
|
|
7
|
+
# via an Upgrade: h2 response declaration
|
|
8
|
+
#
|
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
|
|
10
|
+
#
|
|
11
|
+
module H2
|
|
12
|
+
class << self
|
|
13
|
+
def extra_options(options)
|
|
14
|
+
options.merge(upgrade_handlers: options.upgrade_handlers.merge("h2" => self))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(connection, _request, _response)
|
|
18
|
+
connection.upgrade_to_h2
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ConnectionMethods
|
|
23
|
+
using URIExtensions
|
|
24
|
+
|
|
25
|
+
def interests
|
|
26
|
+
return super unless connecting? && @parser
|
|
27
|
+
|
|
28
|
+
connect
|
|
29
|
+
|
|
30
|
+
return @io.interests if connecting?
|
|
31
|
+
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def upgrade_to_h2
|
|
36
|
+
prev_parser = @parser
|
|
37
|
+
|
|
38
|
+
if prev_parser
|
|
39
|
+
prev_parser.reset
|
|
40
|
+
@inflight -= prev_parser.requests.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@parser = @options.http2_class.new(@write_buffer, @options)
|
|
44
|
+
set_parser_callbacks(@parser)
|
|
45
|
+
@upgrade_protocol = "h2"
|
|
46
|
+
|
|
47
|
+
# what's happening here:
|
|
48
|
+
# a deviation from the state machine is done to perform the actions when a
|
|
49
|
+
# connection is closed, without transitioning, so the connection is kept in the pool.
|
|
50
|
+
# the state is reset to initial, so that the socket reconnect works out of the box,
|
|
51
|
+
# while the parser is already here.
|
|
52
|
+
purge_after_closed
|
|
53
|
+
transition(:idle)
|
|
54
|
+
|
|
55
|
+
prev_parser.requests.each do |req|
|
|
56
|
+
req.transition(:idle)
|
|
57
|
+
send(req)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
register_plugin(:"upgrade/h2", H2)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
|
|
7
|
+
# Upgrade header.
|
|
8
|
+
#
|
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Upgrade
|
|
10
|
+
#
|
|
11
|
+
module Upgrade
|
|
12
|
+
class << self
|
|
13
|
+
def configure(klass)
|
|
14
|
+
klass.plugin(:"upgrade/h2")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extra_options(options)
|
|
18
|
+
options.merge(upgrade_handlers: {})
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module OptionsMethods
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def option_upgrade_handlers(value)
|
|
26
|
+
raise TypeError, ":upgrade_handlers must be a Hash" unless value.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module InstanceMethods
|
|
33
|
+
def fetch_response(request, selector, options)
|
|
34
|
+
response = super
|
|
35
|
+
|
|
36
|
+
if response
|
|
37
|
+
return response unless response.is_a?(Response)
|
|
38
|
+
|
|
39
|
+
return response unless response.headers.key?("upgrade")
|
|
40
|
+
|
|
41
|
+
upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
|
|
42
|
+
|
|
43
|
+
return response unless upgrade_protocol && options.upgrade_handlers.key?(upgrade_protocol)
|
|
44
|
+
|
|
45
|
+
protocol_handler = options.upgrade_handlers[upgrade_protocol]
|
|
46
|
+
|
|
47
|
+
return response unless protocol_handler
|
|
48
|
+
|
|
49
|
+
log { "upgrading to #{upgrade_protocol}..." }
|
|
50
|
+
connection = find_connection(request.uri, selector, options)
|
|
51
|
+
|
|
52
|
+
# do not upgrade already upgraded connections
|
|
53
|
+
return if connection.upgrade_protocol == upgrade_protocol
|
|
54
|
+
|
|
55
|
+
protocol_handler.call(connection, request, response)
|
|
56
|
+
|
|
57
|
+
# keep in the loop if the server is switching, unless
|
|
58
|
+
# the connection has been hijacked, in which case you want
|
|
59
|
+
# to terminante immediately
|
|
60
|
+
return if response.status == 101 && !connection.hijacked
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
response
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module ConnectionMethods
|
|
68
|
+
attr_reader :upgrade_protocol, :hijacked
|
|
69
|
+
|
|
70
|
+
def initialize(*)
|
|
71
|
+
super
|
|
72
|
+
|
|
73
|
+
@upgrade_protocol = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def hijack_io
|
|
77
|
+
@hijacked = true
|
|
78
|
+
|
|
79
|
+
# connection is taken away from selector and not given back to the pool.
|
|
80
|
+
@current_session.deselect_connection(self, @current_selector, true)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
register_plugin(:upgrade, Upgrade)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin implements convenience methods for performing WEBDAV requests.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/WebDav
|
|
9
|
+
#
|
|
10
|
+
module WebDav
|
|
11
|
+
def self.configure(klass)
|
|
12
|
+
klass.plugin(:xml)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module InstanceMethods
|
|
16
|
+
def copy(src, dest)
|
|
17
|
+
request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def move(src, dest)
|
|
21
|
+
request("MOVE", src, headers: { "destination" => @options.origin.merge(dest) })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lock(path, timeout: nil, &blk)
|
|
25
|
+
headers = {}
|
|
26
|
+
headers["timeout"] = if timeout && timeout.positive?
|
|
27
|
+
"Second-#{timeout}"
|
|
28
|
+
else
|
|
29
|
+
"Infinite, Second-4100000000"
|
|
30
|
+
end
|
|
31
|
+
xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" \
|
|
32
|
+
"<D:lockinfo xmlns:D=\"DAV:\">" \
|
|
33
|
+
"<D:lockscope><D:exclusive/></D:lockscope>" \
|
|
34
|
+
"<D:locktype><D:write/></D:locktype>" \
|
|
35
|
+
"<D:owner>null</D:owner>" \
|
|
36
|
+
"</D:lockinfo>"
|
|
37
|
+
response = request("LOCK", path, headers: headers, xml: xml)
|
|
38
|
+
|
|
39
|
+
return response unless response.is_a?(Response)
|
|
40
|
+
|
|
41
|
+
return response unless blk && response.status == 200
|
|
42
|
+
|
|
43
|
+
lock_token = response.headers["lock-token"]
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
blk.call(response)
|
|
47
|
+
ensure
|
|
48
|
+
unlock(path, lock_token)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
response
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unlock(path, lock_token)
|
|
55
|
+
request("UNLOCK", path, headers: { "lock-token" => lock_token })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mkcol(dir)
|
|
59
|
+
request("MKCOL", dir)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def propfind(path, xml = nil)
|
|
63
|
+
body = case xml
|
|
64
|
+
when :acl
|
|
65
|
+
'<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:prop><D:owner/>' \
|
|
66
|
+
"<D:supported-privilege-set/><D:current-user-privilege-set/><D:acl/></D:prop></D:propfind>"
|
|
67
|
+
when nil
|
|
68
|
+
'<?xml version="1.0" encoding="utf-8"?><DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
|
|
69
|
+
else
|
|
70
|
+
xml
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
request("PROPFIND", path, headers: { "depth" => "1" }, xml: body)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def proppatch(path, xml)
|
|
77
|
+
body = "<?xml version=\"1.0\"?>" \
|
|
78
|
+
"<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"http://ns.example.com/standards/z39.50/\">#{xml}</D:propertyupdate>"
|
|
79
|
+
request("PROPPATCH", path, xml: body)
|
|
80
|
+
end
|
|
81
|
+
# %i[ orderpatch acl report search]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
register_plugin(:webdav, WebDav)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin supports request XML encoding/response decoding using the nokogiri gem.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/XML
|
|
9
|
+
#
|
|
10
|
+
module XML
|
|
11
|
+
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
|
|
12
|
+
module Transcoder
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
class Encoder
|
|
16
|
+
def initialize(xml)
|
|
17
|
+
@raw = xml
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def content_type
|
|
21
|
+
charset = @raw.respond_to?(:encoding) && @raw.encoding ? @raw.encoding.to_s.downcase : "utf-8"
|
|
22
|
+
"application/xml; charset=#{charset}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def bytesize
|
|
26
|
+
@raw.to_s.bytesize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
@raw.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def encode(xml)
|
|
35
|
+
Encoder.new(xml)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def decode(response)
|
|
39
|
+
content_type = response.content_type.mime_type
|
|
40
|
+
|
|
41
|
+
raise HTTPX::Error, "invalid form mime type (#{content_type})" unless MIME_TYPES.match?(content_type)
|
|
42
|
+
|
|
43
|
+
Nokogiri::XML.method(:parse)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
def load_dependencies(*)
|
|
49
|
+
require "nokogiri"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module ResponseMethods
|
|
54
|
+
# decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
|
|
55
|
+
# "application/xml" (requires the "nokogiri" gem).
|
|
56
|
+
def xml
|
|
57
|
+
decode(Transcoder)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module RequestBodyClassMethods
|
|
62
|
+
# ..., xml: Nokogiri::XML::Node #=> xml encoder
|
|
63
|
+
def initialize_body(params)
|
|
64
|
+
if (xml = params.delete(:xml))
|
|
65
|
+
# @type var xml: Nokogiri::XML::Node | String
|
|
66
|
+
return Transcoder.encode(xml)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
register_plugin(:xml, XML)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module ResponsePatternMatchExtensions
|
|
5
|
+
def deconstruct
|
|
6
|
+
[@status, @headers, @body]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def deconstruct_keys(_keys)
|
|
10
|
+
{ status: @status, headers: @headers, body: @body }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ErrorResponsePatternMatchExtensions
|
|
15
|
+
def deconstruct
|
|
16
|
+
[@error]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def deconstruct_keys(_keys)
|
|
20
|
+
{ error: @error }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module HeadersPatternMatchExtensions
|
|
25
|
+
def deconstruct
|
|
26
|
+
to_a
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Headers.include HeadersPatternMatchExtensions
|
|
31
|
+
Response.include ResponsePatternMatchExtensions
|
|
32
|
+
ErrorResponse.include ErrorResponsePatternMatchExtensions
|
|
33
|
+
end
|