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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module HTTPX
|
|
8
|
+
module Plugins
|
|
9
|
+
module Authentication
|
|
10
|
+
class Digest
|
|
11
|
+
def initialize(user, password, hashed: false, **)
|
|
12
|
+
@user = user
|
|
13
|
+
@password = password
|
|
14
|
+
@nonce = 0
|
|
15
|
+
@hashed = hashed
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def can_authenticate?(authenticate)
|
|
19
|
+
authenticate && /Digest .*/.match?(authenticate)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def authenticate(request, authenticate)
|
|
23
|
+
"Digest #{generate_header(request.verb, request.path, authenticate)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def generate_header(meth, uri, authenticate)
|
|
29
|
+
# discard first token, it's Digest
|
|
30
|
+
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
|
31
|
+
|
|
32
|
+
params = auth_info.split(/ *, */)
|
|
33
|
+
.to_h { |val| val.split("=", 2) }
|
|
34
|
+
.transform_values { |v| v.delete("\"") }
|
|
35
|
+
nonce = params["nonce"]
|
|
36
|
+
nc = next_nonce
|
|
37
|
+
|
|
38
|
+
# verify qop
|
|
39
|
+
qop = params["qop"]
|
|
40
|
+
|
|
41
|
+
if params["algorithm"] =~ /(.*?)(-sess)?$/
|
|
42
|
+
alg = Regexp.last_match(1)
|
|
43
|
+
algorithm = ::Digest.const_get(alg)
|
|
44
|
+
raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
|
|
45
|
+
|
|
46
|
+
sess = Regexp.last_match(2)
|
|
47
|
+
else
|
|
48
|
+
algorithm = ::Digest::MD5
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if qop || sess
|
|
52
|
+
cnonce = make_cnonce
|
|
53
|
+
nc = format("%<nonce>08x", nonce: nc)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
a1 = if sess
|
|
57
|
+
[
|
|
58
|
+
(@hashed ? @password : algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}")),
|
|
59
|
+
nonce,
|
|
60
|
+
cnonce,
|
|
61
|
+
].join ":"
|
|
62
|
+
else
|
|
63
|
+
@hashed ? @password : "#{@user}:#{params["realm"]}:#{@password}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
ha1 = algorithm.hexdigest(a1)
|
|
67
|
+
ha2 = algorithm.hexdigest("#{meth}:#{uri}")
|
|
68
|
+
request_digest = [ha1, nonce]
|
|
69
|
+
request_digest.push(nc, cnonce, qop) if qop
|
|
70
|
+
request_digest << ha2
|
|
71
|
+
request_digest = request_digest.join(":")
|
|
72
|
+
|
|
73
|
+
header = [
|
|
74
|
+
%(username="#{@user}"),
|
|
75
|
+
%(nonce="#{nonce}"),
|
|
76
|
+
%(uri="#{uri}"),
|
|
77
|
+
%(response="#{algorithm.hexdigest(request_digest)}"),
|
|
78
|
+
]
|
|
79
|
+
header << %(realm="#{params["realm"]}") if params.key?("realm")
|
|
80
|
+
header << %(algorithm=#{params["algorithm"]}) if params.key?("algorithm")
|
|
81
|
+
header << %(cnonce="#{cnonce}") if cnonce
|
|
82
|
+
header << %(nc=#{nc})
|
|
83
|
+
header << %(qop=#{qop}) if qop
|
|
84
|
+
header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
|
|
85
|
+
header.join ", "
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def make_cnonce
|
|
89
|
+
::Digest::MD5.hexdigest [
|
|
90
|
+
Time.now.to_i,
|
|
91
|
+
Process.pid,
|
|
92
|
+
SecureRandom.random_number(2**32),
|
|
93
|
+
].join ":"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def next_nonce
|
|
97
|
+
@nonce += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpx/base64"
|
|
4
|
+
require "ntlm"
|
|
5
|
+
|
|
6
|
+
module HTTPX
|
|
7
|
+
module Plugins
|
|
8
|
+
module Authentication
|
|
9
|
+
class Ntlm
|
|
10
|
+
def initialize(user, password, domain: nil)
|
|
11
|
+
@user = user
|
|
12
|
+
@password = password
|
|
13
|
+
@domain = domain
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def can_authenticate?(authenticate)
|
|
17
|
+
authenticate && /NTLM .*/.match?(authenticate)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negotiate
|
|
21
|
+
"NTLM #{NTLM.negotiate(domain: @domain).to_base64}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def authenticate(_req, www)
|
|
25
|
+
challenge = www[/NTLM (.*)/, 1]
|
|
26
|
+
|
|
27
|
+
challenge = Base64.decode64(challenge)
|
|
28
|
+
ntlm_challenge = NTLM.authenticate(challenge, @user, @domain, @password).to_base64
|
|
29
|
+
|
|
30
|
+
"NTLM #{ntlm_challenge}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
module Authentication
|
|
6
|
+
class Socks5
|
|
7
|
+
def initialize(user, password, **)
|
|
8
|
+
@user = user
|
|
9
|
+
@password = password
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def can_authenticate?(*)
|
|
13
|
+
@user && @password
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def authenticate(*)
|
|
17
|
+
[0x01, @user.bytesize, @user, @password.bytesize, @password].pack("CCA*CA*")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds a shim +authorization+ method to the session, which will fill
|
|
7
|
+
# the HTTP Authorization header, and another, +bearer_auth+, which fill the "Bearer " prefix
|
|
8
|
+
# in its value.
|
|
9
|
+
#
|
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/Auth#auth
|
|
11
|
+
#
|
|
12
|
+
module Auth
|
|
13
|
+
module InstanceMethods
|
|
14
|
+
def authorization(token)
|
|
15
|
+
with(headers: { "authorization" => token })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def bearer_auth(token)
|
|
19
|
+
authorization("Bearer #{token}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
register_plugin :auth, Auth
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin applies AWS Sigv4 to requests, using the AWS SDK credentials and configuration.
|
|
7
|
+
#
|
|
8
|
+
# It requires the "aws-sdk-core" gem.
|
|
9
|
+
#
|
|
10
|
+
module AwsSdkAuthentication
|
|
11
|
+
# Mock configuration, to be used only when resolving credentials
|
|
12
|
+
class Configuration
|
|
13
|
+
attr_reader :profile
|
|
14
|
+
|
|
15
|
+
def initialize(profile)
|
|
16
|
+
@profile = profile
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def respond_to_missing?(*)
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def method_missing(*); end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# encapsulates access to an AWS SDK credentials store.
|
|
28
|
+
#
|
|
29
|
+
class Credentials
|
|
30
|
+
def initialize(aws_credentials)
|
|
31
|
+
@aws_credentials = aws_credentials
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def username
|
|
35
|
+
@aws_credentials.access_key_id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def password
|
|
39
|
+
@aws_credentials.secret_access_key
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def security_token
|
|
43
|
+
@aws_credentials.session_token
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
def load_dependencies(_klass)
|
|
49
|
+
require "aws-sdk-core"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configure(klass)
|
|
53
|
+
klass.plugin(:aws_sigv4)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extra_options(options)
|
|
57
|
+
options.merge(max_concurrent_requests: 1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def credentials(profile)
|
|
61
|
+
mock_configuration = Configuration.new(profile)
|
|
62
|
+
Credentials.new(Aws::CredentialProviderChain.new(mock_configuration).resolve)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def region(profile)
|
|
66
|
+
# https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L62
|
|
67
|
+
keys = %w[AWS_REGION AMAZON_REGION AWS_DEFAULT_REGION]
|
|
68
|
+
env_region = ENV.values_at(*keys).compact.first
|
|
69
|
+
env_region = nil if env_region == ""
|
|
70
|
+
cfg_region = Aws.shared_config.region(profile: profile)
|
|
71
|
+
env_region || cfg_region
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# adds support for the following options:
|
|
76
|
+
#
|
|
77
|
+
# :aws_profile :: AWS account profile to retrieve credentials from.
|
|
78
|
+
module OptionsMethods
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def option_aws_profile(value)
|
|
82
|
+
String(value)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module InstanceMethods
|
|
87
|
+
#
|
|
88
|
+
# aws_authentication
|
|
89
|
+
# aws_authentication(credentials: Aws::Credentials.new('akid', 'secret'))
|
|
90
|
+
# aws_authentication()
|
|
91
|
+
#
|
|
92
|
+
def aws_sdk_authentication(
|
|
93
|
+
credentials: AwsSdkAuthentication.credentials(@options.aws_profile),
|
|
94
|
+
region: AwsSdkAuthentication.region(@options.aws_profile),
|
|
95
|
+
**options
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
aws_sigv4_authentication(
|
|
99
|
+
credentials: credentials,
|
|
100
|
+
region: region,
|
|
101
|
+
provider_prefix: "aws",
|
|
102
|
+
header_provider_field: "amz",
|
|
103
|
+
**options
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
alias_method :aws_auth, :aws_sdk_authentication
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
register_plugin :aws_sdk_authentication, AwsSdkAuthentication
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds AWS Sigv4 authentication.
|
|
7
|
+
#
|
|
8
|
+
# https://docs.aws.amazon.com/IAM/latest/UserGuide/signing-elements.html
|
|
9
|
+
#
|
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/AWS-SigV4
|
|
11
|
+
#
|
|
12
|
+
module AWSSigV4
|
|
13
|
+
Credentials = Struct.new(:username, :password, :security_token)
|
|
14
|
+
|
|
15
|
+
# Signs requests using the AWS sigv4 signing.
|
|
16
|
+
class Signer
|
|
17
|
+
def initialize(
|
|
18
|
+
service:,
|
|
19
|
+
region:,
|
|
20
|
+
credentials: nil,
|
|
21
|
+
username: nil,
|
|
22
|
+
password: nil,
|
|
23
|
+
security_token: nil,
|
|
24
|
+
provider_prefix: "aws",
|
|
25
|
+
header_provider_field: "amz",
|
|
26
|
+
unsigned_headers: [],
|
|
27
|
+
apply_checksum_header: true,
|
|
28
|
+
algorithm: "SHA256"
|
|
29
|
+
)
|
|
30
|
+
@credentials = credentials || Credentials.new(username, password, security_token)
|
|
31
|
+
@service = service
|
|
32
|
+
@region = region
|
|
33
|
+
|
|
34
|
+
@unsigned_headers = Set.new(unsigned_headers.map(&:downcase))
|
|
35
|
+
@unsigned_headers << "authorization"
|
|
36
|
+
@unsigned_headers << "x-amzn-trace-id"
|
|
37
|
+
@unsigned_headers << "expect"
|
|
38
|
+
|
|
39
|
+
@apply_checksum_header = apply_checksum_header
|
|
40
|
+
@provider_prefix = provider_prefix
|
|
41
|
+
@header_provider_field = header_provider_field
|
|
42
|
+
|
|
43
|
+
@algorithm = algorithm
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sign!(request)
|
|
47
|
+
lower_provider_prefix = "#{@provider_prefix}4"
|
|
48
|
+
upper_provider_prefix = lower_provider_prefix.upcase
|
|
49
|
+
|
|
50
|
+
downcased_algorithm = @algorithm.downcase
|
|
51
|
+
|
|
52
|
+
datetime = (request.headers["x-#{@header_provider_field}-date"] ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ"))
|
|
53
|
+
date = datetime[0, 8]
|
|
54
|
+
|
|
55
|
+
content_hashed = request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] || hexdigest(request.body)
|
|
56
|
+
|
|
57
|
+
request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] ||= content_hashed if @apply_checksum_header
|
|
58
|
+
request.headers["x-#{@header_provider_field}-security-token"] ||= @credentials.security_token if @credentials.security_token
|
|
59
|
+
|
|
60
|
+
signature_headers = request.headers.each.reject do |k, _|
|
|
61
|
+
@unsigned_headers.include?(k)
|
|
62
|
+
end
|
|
63
|
+
# aws sigv4 needs to declare the host, regardless of protocol version
|
|
64
|
+
signature_headers << ["host", request.authority] unless request.headers.key?("host")
|
|
65
|
+
signature_headers.sort_by!(&:first)
|
|
66
|
+
|
|
67
|
+
signed_headers = signature_headers.map(&:first).join(";")
|
|
68
|
+
|
|
69
|
+
canonical_headers = signature_headers.map do |k, v|
|
|
70
|
+
# eliminate whitespace between value fields, unless it's a quoted value
|
|
71
|
+
"#{k}:#{v.start_with?("\"") && v.end_with?("\"") ? v : v.gsub(/\s+/, " ").strip}\n"
|
|
72
|
+
end.join
|
|
73
|
+
|
|
74
|
+
# canonical request
|
|
75
|
+
creq = "#{request.verb}" \
|
|
76
|
+
"\n#{request.canonical_path}" \
|
|
77
|
+
"\n#{request.canonical_query}" \
|
|
78
|
+
"\n#{canonical_headers}" \
|
|
79
|
+
"\n#{signed_headers}" \
|
|
80
|
+
"\n#{content_hashed}"
|
|
81
|
+
|
|
82
|
+
credential_scope = "#{date}" \
|
|
83
|
+
"/#{@region}" \
|
|
84
|
+
"/#{@service}" \
|
|
85
|
+
"/#{lower_provider_prefix}_request"
|
|
86
|
+
|
|
87
|
+
algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
|
|
88
|
+
# string to sign
|
|
89
|
+
sts = "#{algo_line}" \
|
|
90
|
+
"\n#{datetime}" \
|
|
91
|
+
"\n#{credential_scope}" \
|
|
92
|
+
"\n#{OpenSSL::Digest.new(@algorithm).hexdigest(creq)}"
|
|
93
|
+
|
|
94
|
+
# signature
|
|
95
|
+
k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
|
|
96
|
+
k_region = hmac(k_date, @region)
|
|
97
|
+
k_service = hmac(k_region, @service)
|
|
98
|
+
k_credentials = hmac(k_service, "#{lower_provider_prefix}_request")
|
|
99
|
+
sig = hexhmac(k_credentials, sts)
|
|
100
|
+
|
|
101
|
+
credential = "#{@credentials.username}/#{credential_scope}"
|
|
102
|
+
# apply signature
|
|
103
|
+
request.headers["authorization"] =
|
|
104
|
+
"#{algo_line} " \
|
|
105
|
+
"Credential=#{credential}, " \
|
|
106
|
+
"SignedHeaders=#{signed_headers}, " \
|
|
107
|
+
"Signature=#{sig}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def hexdigest(value)
|
|
113
|
+
digest = OpenSSL::Digest.new(@algorithm)
|
|
114
|
+
|
|
115
|
+
if value.respond_to?(:read)
|
|
116
|
+
if value.respond_to?(:to_path)
|
|
117
|
+
# files, pathnames
|
|
118
|
+
digest.file(value.to_path).hexdigest
|
|
119
|
+
else
|
|
120
|
+
# gzipped request bodies
|
|
121
|
+
raise Error, "request body must be rewindable" unless value.respond_to?(:rewind)
|
|
122
|
+
|
|
123
|
+
buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
124
|
+
begin
|
|
125
|
+
IO.copy_stream(value, buffer)
|
|
126
|
+
buffer.flush
|
|
127
|
+
|
|
128
|
+
digest.file(buffer.to_path).hexdigest
|
|
129
|
+
ensure
|
|
130
|
+
value.rewind
|
|
131
|
+
buffer.close
|
|
132
|
+
buffer.unlink
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
# error on endless generators
|
|
137
|
+
raise Error, "hexdigest for endless enumerators is not supported" if value.unbounded_body?
|
|
138
|
+
|
|
139
|
+
mb_buffer = value.each.with_object("".b) do |chunk, b|
|
|
140
|
+
b << chunk
|
|
141
|
+
break if b.bytesize >= 1024 * 1024
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
digest.hexdigest(mb_buffer)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def hmac(key, value)
|
|
149
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new(@algorithm), key, value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def hexhmac(key, value)
|
|
153
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(@algorithm), key, value)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class << self
|
|
158
|
+
def load_dependencies(*)
|
|
159
|
+
require "set"
|
|
160
|
+
require "digest/sha2"
|
|
161
|
+
require "cgi/escape"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def configure(klass)
|
|
165
|
+
klass.plugin(:expect)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# adds support for the following options:
|
|
170
|
+
#
|
|
171
|
+
# :sigv4_signer :: instance of HTTPX::Plugins::AWSSigV4 used to sign requests.
|
|
172
|
+
module OptionsMethods
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def option_sigv4_signer(value)
|
|
176
|
+
value.is_a?(Signer) ? value : Signer.new(value)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
module InstanceMethods
|
|
181
|
+
def aws_sigv4_authentication(**options)
|
|
182
|
+
with(sigv4_signer: Signer.new(**options))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_request(*)
|
|
186
|
+
request = super
|
|
187
|
+
|
|
188
|
+
return request if request.headers.key?("authorization")
|
|
189
|
+
|
|
190
|
+
signer = request.options.sigv4_signer
|
|
191
|
+
|
|
192
|
+
return request unless signer
|
|
193
|
+
|
|
194
|
+
signer.sign!(request)
|
|
195
|
+
|
|
196
|
+
request
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
module RequestMethods
|
|
201
|
+
def canonical_path
|
|
202
|
+
path = uri.path.dup
|
|
203
|
+
path << "/" if path.empty?
|
|
204
|
+
path.gsub(%r{[^/]+}) { |part| CGI.escape(part.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~") }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def canonical_query
|
|
208
|
+
params = query.split("&")
|
|
209
|
+
# params = params.map { |p| p.match(/=/) ? p : p + '=' }
|
|
210
|
+
# From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
|
|
211
|
+
# Sort the parameter names by character code point in ascending order.
|
|
212
|
+
# Parameters with duplicate names should be sorted by value.
|
|
213
|
+
#
|
|
214
|
+
# Default sort <=> in JRuby will swap members
|
|
215
|
+
# occasionally when <=> is 0 (considered still sorted), but this
|
|
216
|
+
# causes our normalized query string to not match the sent querystring.
|
|
217
|
+
# When names match, we then sort by their values. When values also
|
|
218
|
+
# match then we sort by their original order
|
|
219
|
+
params.each.with_index.sort do |a, b|
|
|
220
|
+
a, a_offset = a
|
|
221
|
+
b, b_offset = b
|
|
222
|
+
a_name, a_value = a.split("=", 2)
|
|
223
|
+
b_name, b_value = b.split("=", 2)
|
|
224
|
+
if a_name == b_name
|
|
225
|
+
if a_value == b_value
|
|
226
|
+
a_offset <=> b_offset
|
|
227
|
+
else
|
|
228
|
+
a_value <=> b_value
|
|
229
|
+
end
|
|
230
|
+
else
|
|
231
|
+
a_name <=> b_name
|
|
232
|
+
end
|
|
233
|
+
end.map(&:first).join("&")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
register_plugin :aws_sigv4, AWSSigV4
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds helper methods to implement HTTP Basic Auth (https://datatracker.ietf.org/doc/html/rfc7617)
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Auth#basic-auth
|
|
9
|
+
#
|
|
10
|
+
module BasicAuth
|
|
11
|
+
class << self
|
|
12
|
+
def load_dependencies(_klass)
|
|
13
|
+
require_relative "auth/basic"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure(klass)
|
|
17
|
+
klass.plugin(:auth)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module InstanceMethods
|
|
22
|
+
def basic_auth(user, password)
|
|
23
|
+
authorization(Authentication::Basic.new(user, password).authenticate)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
register_plugin :basic_auth, BasicAuth
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
module Brotli
|
|
6
|
+
class Deflater < Transcoder::Deflater
|
|
7
|
+
def deflate(chunk)
|
|
8
|
+
return unless chunk
|
|
9
|
+
|
|
10
|
+
::Brotli.deflate(chunk)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module RequestBodyClassMethods
|
|
15
|
+
def initialize_deflater_body(body, encoding)
|
|
16
|
+
return Brotli.encode(body) if encoding == "br"
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ResponseBodyClassMethods
|
|
23
|
+
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
|
24
|
+
return Brotli.decode(response, **kwargs) if encoding == "br"
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def load_dependencies(*)
|
|
33
|
+
require "brotli"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.extra_options(options)
|
|
37
|
+
options.merge(supported_compression_formats: %w[br] + options.supported_compression_formats)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def encode(body)
|
|
41
|
+
Deflater.new(body)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def decode(_response, **)
|
|
45
|
+
::Brotli.method(:inflate)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
register_plugin :brotli, Brotli
|
|
49
|
+
end
|
|
50
|
+
end
|