httpx 0.16.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- 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/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +5 -3
- data/lib/httpx/adapters/webmock.rb +9 -3
- data/lib/httpx/altsvc.rb +2 -2
- data/lib/httpx/chainable.rb +4 -4
- data/lib/httpx/connection/http1.rb +23 -14
- data/lib/httpx/connection/http2.rb +35 -17
- data/lib/httpx/connection.rb +74 -76
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/extensions.rb +50 -4
- data/lib/httpx/headers.rb +1 -1
- data/lib/httpx/io/ssl.rb +5 -1
- data/lib/httpx/io/tls.rb +7 -7
- data/lib/httpx/loggable.rb +5 -5
- data/lib/httpx/options.rb +35 -13
- data/lib/httpx/parser/http1.rb +10 -6
- data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
- data/lib/httpx/plugins/aws_sigv4.rb +9 -11
- data/lib/httpx/plugins/compression.rb +5 -3
- data/lib/httpx/plugins/cookies/jar.rb +1 -1
- data/lib/httpx/plugins/digest_authentication.rb +4 -4
- data/lib/httpx/plugins/expect.rb +7 -3
- data/lib/httpx/plugins/grpc/message.rb +2 -2
- data/lib/httpx/plugins/grpc.rb +3 -3
- data/lib/httpx/plugins/h2c.rb +7 -3
- data/lib/httpx/plugins/internal_telemetry.rb +8 -8
- data/lib/httpx/plugins/multipart/decoder.rb +187 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/multipart.rb +16 -2
- data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
- data/lib/httpx/plugins/proxy/ssh.rb +11 -4
- data/lib/httpx/plugins/proxy.rb +6 -4
- data/lib/httpx/plugins/response_cache/store.rb +55 -0
- data/lib/httpx/plugins/response_cache.rb +88 -0
- data/lib/httpx/plugins/retries.rb +36 -14
- data/lib/httpx/plugins/stream.rb +3 -4
- data/lib/httpx/pool.rb +39 -13
- data/lib/httpx/registry.rb +1 -1
- data/lib/httpx/request.rb +12 -13
- data/lib/httpx/resolver/https.rb +5 -7
- data/lib/httpx/resolver/native.rb +4 -2
- data/lib/httpx/resolver/resolver_mixin.rb +2 -1
- data/lib/httpx/resolver/system.rb +2 -0
- data/lib/httpx/resolver.rb +2 -2
- data/lib/httpx/response.rb +60 -44
- data/lib/httpx/selector.rb +16 -19
- data/lib/httpx/session.rb +22 -15
- data/lib/httpx/session2.rb +1 -1
- data/lib/httpx/timers.rb +84 -0
- data/lib/httpx/transcoder/body.rb +2 -1
- data/lib/httpx/transcoder/form.rb +20 -0
- data/lib/httpx/transcoder/json.rb +12 -0
- data/lib/httpx/transcoder.rb +62 -1
- data/lib/httpx/utils.rb +10 -2
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +1 -0
- data/sig/buffer.rbs +2 -2
- data/sig/chainable.rbs +7 -1
- data/sig/connection/http1.rbs +15 -4
- data/sig/connection/http2.rbs +19 -5
- data/sig/connection.rbs +15 -9
- data/sig/headers.rbs +19 -18
- data/sig/options.rbs +13 -5
- data/sig/parser/http1.rbs +3 -3
- data/sig/plugins/aws_sdk_authentication.rbs +22 -4
- data/sig/plugins/aws_sigv4.rbs +12 -3
- data/sig/plugins/basic_authentication.rbs +1 -1
- data/sig/plugins/multipart.rbs +64 -8
- data/sig/plugins/proxy.rbs +6 -6
- data/sig/plugins/response_cache.rbs +35 -0
- data/sig/plugins/retries.rbs +3 -0
- data/sig/pool.rbs +6 -0
- data/sig/request.rbs +11 -8
- data/sig/resolver/native.rbs +2 -1
- data/sig/resolver/resolver_mixin.rbs +1 -1
- data/sig/resolver/system.rbs +3 -1
- data/sig/response.rbs +11 -4
- data/sig/selector.rbs +8 -6
- data/sig/session.rbs +8 -14
- data/sig/timers.rbs +32 -0
- data/sig/transcoder/form.rbs +1 -0
- data/sig/transcoder/json.rbs +1 -0
- data/sig/transcoder.rbs +5 -4
- data/sig/utils.rbs +4 -0
- metadata +18 -17
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
|
-
require "aws-sdk-s3"
|
5
|
-
|
6
3
|
module HTTPX
|
7
4
|
module Plugins
|
8
5
|
#
|
@@ -75,16 +72,16 @@ module HTTPX
|
|
75
72
|
|
76
73
|
# canonical request
|
77
74
|
creq = "#{request.verb.to_s.upcase}" \
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
75
|
+
"\n#{request.canonical_path}" \
|
76
|
+
"\n#{request.canonical_query}" \
|
77
|
+
"\n#{canonical_headers}" \
|
78
|
+
"\n#{signed_headers}" \
|
79
|
+
"\n#{content_hashed}"
|
83
80
|
|
84
81
|
credential_scope = "#{date}" \
|
85
|
-
|
86
|
-
|
87
|
-
|
82
|
+
"/#{@region}" \
|
83
|
+
"/#{@service}" \
|
84
|
+
"/#{lower_provider_prefix}_request"
|
88
85
|
|
89
86
|
algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
|
90
87
|
# string to sign
|
@@ -142,6 +139,7 @@ module HTTPX
|
|
142
139
|
|
143
140
|
class << self
|
144
141
|
def load_dependencies(*)
|
142
|
+
require "set"
|
145
143
|
require "digest/sha2"
|
146
144
|
require "openssl"
|
147
145
|
end
|
@@ -72,6 +72,8 @@ module HTTPX
|
|
72
72
|
end
|
73
73
|
|
74
74
|
module ResponseBodyMethods
|
75
|
+
using ArrayExtensions
|
76
|
+
|
75
77
|
attr_reader :encodings
|
76
78
|
|
77
79
|
def initialize(*)
|
@@ -90,7 +92,7 @@ module HTTPX
|
|
90
92
|
Float::INFINITY
|
91
93
|
end
|
92
94
|
|
93
|
-
@_inflaters = @headers.get("content-encoding").
|
95
|
+
@_inflaters = @headers.get("content-encoding").filter_map do |encoding|
|
94
96
|
next if encoding == "identity"
|
95
97
|
|
96
98
|
inflater = @options.encodings.registry(encoding).inflater(compressed_length)
|
@@ -100,7 +102,7 @@ module HTTPX
|
|
100
102
|
|
101
103
|
@encodings << encoding
|
102
104
|
inflater
|
103
|
-
end
|
105
|
+
end
|
104
106
|
|
105
107
|
# this can happen if the only declared encoding is "identity"
|
106
108
|
remove_instance_variable(:@_inflaters) if @_inflaters.empty?
|
@@ -134,7 +136,7 @@ module HTTPX
|
|
134
136
|
end
|
135
137
|
|
136
138
|
def each(&blk)
|
137
|
-
return enum_for(__method__) unless
|
139
|
+
return enum_for(__method__) unless blk
|
138
140
|
|
139
141
|
return deflate(&blk) if @buffer.size.zero?
|
140
142
|
|
@@ -40,12 +40,12 @@ module HTTPX
|
|
40
40
|
|
41
41
|
alias_method :digest_auth, :digest_authentication
|
42
42
|
|
43
|
-
def send_requests(*requests
|
43
|
+
def send_requests(*requests)
|
44
44
|
requests.flat_map do |request|
|
45
45
|
digest = request.options.digest
|
46
46
|
|
47
47
|
if digest
|
48
|
-
probe_response = wrap { super(request
|
48
|
+
probe_response = wrap { super(request).first }
|
49
49
|
|
50
50
|
if digest && !probe_response.is_a?(ErrorResponse) &&
|
51
51
|
probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
|
@@ -56,12 +56,12 @@ module HTTPX
|
|
56
56
|
token = digest.generate_header(request, probe_response)
|
57
57
|
request.headers["authorization"] = "Digest #{token}"
|
58
58
|
|
59
|
-
super(request
|
59
|
+
super(request)
|
60
60
|
else
|
61
61
|
probe_response
|
62
62
|
end
|
63
63
|
else
|
64
|
-
super(request
|
64
|
+
super(request)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -69,9 +69,14 @@ module HTTPX
|
|
69
69
|
end
|
70
70
|
|
71
71
|
module ConnectionMethods
|
72
|
-
def
|
72
|
+
def send_request_to_parser(request)
|
73
|
+
super
|
74
|
+
|
75
|
+
return unless request.headers["expect"] == "100-continue"
|
76
|
+
|
73
77
|
request.once(:expect) do
|
74
|
-
@timers.after(
|
78
|
+
@timers.after(request.options.expect_timeout) do
|
79
|
+
# expect timeout expired
|
75
80
|
if request.state == :expect && !request.expects?
|
76
81
|
Expect.no_expect_store << request.origin
|
77
82
|
request.headers.delete("expect")
|
@@ -79,7 +84,6 @@ module HTTPX
|
|
79
84
|
end
|
80
85
|
end
|
81
86
|
end
|
82
|
-
super
|
83
87
|
end
|
84
88
|
end
|
85
89
|
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
17
17
|
|
18
18
|
# lazy decodes a grpc stream response
|
19
19
|
def stream(response, &block)
|
20
|
-
return enum_for(__method__, response) unless
|
20
|
+
return enum_for(__method__, response) unless block
|
21
21
|
|
22
22
|
response.each do |frame|
|
23
23
|
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
@@ -57,7 +57,7 @@ module HTTPX
|
|
57
57
|
|
58
58
|
yield data
|
59
59
|
|
60
|
-
message = message.byteslice(5 + size..-1)
|
60
|
+
message = message.byteslice((5 + size)..-1)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
data/lib/httpx/plugins/grpc.rb
CHANGED
@@ -143,9 +143,9 @@ module HTTPX
|
|
143
143
|
|
144
144
|
session_class = Class.new(self.class) do
|
145
145
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
146
|
-
def #{rpc_name}(input, **opts)
|
147
|
-
rpc_execute("#{rpc_name}", input, **opts)
|
148
|
-
end
|
146
|
+
def #{rpc_name}(input, **opts) # def grpc_action(input, **opts)
|
147
|
+
rpc_execute("#{rpc_name}", input, **opts) # rpc_execute("grpc_action", input, **opts)
|
148
|
+
end # end
|
149
149
|
OUT
|
150
150
|
end
|
151
151
|
|
data/lib/httpx/plugins/h2c.rb
CHANGED
@@ -24,15 +24,19 @@ module HTTPX
|
|
24
24
|
def call(connection, request, response)
|
25
25
|
connection.upgrade_to_h2c(request, response)
|
26
26
|
end
|
27
|
+
|
28
|
+
def extra_options(options)
|
29
|
+
options.merge(max_concurrent_requests: 1)
|
30
|
+
end
|
27
31
|
end
|
28
32
|
|
29
33
|
module InstanceMethods
|
30
|
-
def send_requests(*requests
|
34
|
+
def send_requests(*requests)
|
31
35
|
upgrade_request, *remainder = requests
|
32
36
|
|
33
37
|
return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
|
34
38
|
|
35
|
-
connection = pool.find_connection(upgrade_request.uri,
|
39
|
+
connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
|
36
40
|
|
37
41
|
return super if connection && connection.upgrade_protocol == :h2c
|
38
42
|
|
@@ -42,7 +46,7 @@ module HTTPX
|
|
42
46
|
upgrade_request.headers["upgrade"] = "h2c"
|
43
47
|
upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
|
44
48
|
|
45
|
-
super(upgrade_request, *remainder
|
49
|
+
super(upgrade_request, *remainder)
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|
@@ -44,6 +44,11 @@ module HTTPX
|
|
44
44
|
meter_elapsed_time("Session: initialized!!!")
|
45
45
|
end
|
46
46
|
|
47
|
+
def close(*)
|
48
|
+
super
|
49
|
+
meter_elapsed_time("Session -> close")
|
50
|
+
end
|
51
|
+
|
47
52
|
private
|
48
53
|
|
49
54
|
def build_requests(*)
|
@@ -55,11 +60,6 @@ module HTTPX
|
|
55
60
|
meter_elapsed_time("Session -> response") if response
|
56
61
|
response
|
57
62
|
end
|
58
|
-
|
59
|
-
def close(*)
|
60
|
-
super
|
61
|
-
meter_elapsed_time("Session -> close")
|
62
|
-
end
|
63
63
|
end
|
64
64
|
|
65
65
|
module RequestMethods
|
@@ -69,9 +69,9 @@ module HTTPX
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def transition(nextstate)
|
72
|
-
|
72
|
+
prev_state = @state
|
73
73
|
super
|
74
|
-
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{
|
74
|
+
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{prev_state}] -> #{@state}") if prev_state != @state
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -84,7 +84,7 @@ module HTTPX
|
|
84
84
|
def transition(nextstate)
|
85
85
|
state = @state
|
86
86
|
super
|
87
|
-
meter_elapsed_time("Connection[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
87
|
+
meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "delegate"
|
5
|
+
|
6
|
+
module HTTPX::Plugins
|
7
|
+
module Multipart
|
8
|
+
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
9
|
+
|
10
|
+
CRLF = "\r\n"
|
11
|
+
|
12
|
+
class FilePart < SimpleDelegator
|
13
|
+
attr_reader :original_filename, :content_type
|
14
|
+
|
15
|
+
def initialize(filename, content_type)
|
16
|
+
@original_filename = filename
|
17
|
+
@content_type = content_type
|
18
|
+
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
19
|
+
super(@file)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
24
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
25
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i.freeze
|
26
|
+
BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i.freeze
|
27
|
+
BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i.freeze
|
28
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
29
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
30
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
31
|
+
# Updated definitions from RFC 2231
|
32
|
+
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}.freeze
|
33
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/.freeze
|
34
|
+
SECTION = /\*[0-9]+/.freeze
|
35
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/.freeze
|
36
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/.freeze
|
37
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/.freeze
|
38
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/.freeze
|
39
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/.freeze
|
40
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/.freeze
|
41
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/.freeze
|
42
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/.freeze
|
43
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/.freeze
|
44
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/.freeze
|
45
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i.freeze
|
46
|
+
|
47
|
+
class Decoder
|
48
|
+
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
49
|
+
WINDOW_SIZE = 2 << 14
|
50
|
+
|
51
|
+
def initialize(response)
|
52
|
+
@boundary = begin
|
53
|
+
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
54
|
+
raise Error, "no boundary declared in content-type header" unless m
|
55
|
+
|
56
|
+
m.strip
|
57
|
+
end
|
58
|
+
@buffer = "".b
|
59
|
+
@parts = {}
|
60
|
+
@intermediate_boundary = "--#{@boundary}"
|
61
|
+
@state = :idle
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(response, _)
|
65
|
+
response.body.each do |chunk|
|
66
|
+
@buffer << chunk
|
67
|
+
|
68
|
+
parse
|
69
|
+
end
|
70
|
+
|
71
|
+
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
72
|
+
|
73
|
+
@parts
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse
|
79
|
+
case @state
|
80
|
+
when :idle
|
81
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
82
|
+
|
83
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
84
|
+
|
85
|
+
@state = :part_header
|
86
|
+
when :part_header
|
87
|
+
idx = @buffer.index("#{CRLF}#{CRLF}")
|
88
|
+
|
89
|
+
# raise Error, "couldn't parse part headers" unless idx
|
90
|
+
return unless idx
|
91
|
+
|
92
|
+
head = @buffer.byteslice(0..idx + 4 - 1)
|
93
|
+
|
94
|
+
@buffer = @buffer.byteslice(head.bytesize..-1)
|
95
|
+
|
96
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
97
|
+
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
98
|
+
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
99
|
+
name.gsub!(/\\(.)/, "\\1")
|
100
|
+
name
|
101
|
+
else
|
102
|
+
name = head[MULTIPART_CONTENT_ID, 1]
|
103
|
+
end
|
104
|
+
|
105
|
+
filename = get_filename(head)
|
106
|
+
|
107
|
+
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
108
|
+
|
109
|
+
@current = name
|
110
|
+
|
111
|
+
@parts[name] = if filename
|
112
|
+
FilePart.new(filename, content_type)
|
113
|
+
else
|
114
|
+
"".b
|
115
|
+
end
|
116
|
+
|
117
|
+
@state = :part_body
|
118
|
+
when :part_body
|
119
|
+
part = @parts[@current]
|
120
|
+
|
121
|
+
body_separator = if part.is_a?(FilePart)
|
122
|
+
"#{CRLF}#{CRLF}"
|
123
|
+
else
|
124
|
+
CRLF
|
125
|
+
end
|
126
|
+
idx = @buffer.index(body_separator)
|
127
|
+
|
128
|
+
if idx
|
129
|
+
payload = @buffer.byteslice(0..idx - 1)
|
130
|
+
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
131
|
+
part << payload
|
132
|
+
part.rewind if part.respond_to?(:rewind)
|
133
|
+
@state = :parse_boundary
|
134
|
+
else
|
135
|
+
part << @buffer
|
136
|
+
@buffer.clear
|
137
|
+
end
|
138
|
+
when :parse_boundary
|
139
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
140
|
+
|
141
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
142
|
+
|
143
|
+
if @buffer == "--"
|
144
|
+
@buffer.clear
|
145
|
+
@state = :done
|
146
|
+
return
|
147
|
+
elsif @buffer.start_with?(CRLF)
|
148
|
+
@buffer = @buffer.byteslice(2..-1)
|
149
|
+
@state = :part_header
|
150
|
+
else
|
151
|
+
return
|
152
|
+
end
|
153
|
+
when :done
|
154
|
+
raise Error, "parsing should have been over by now"
|
155
|
+
end until @buffer.empty?
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_filename(head)
|
159
|
+
filename = nil
|
160
|
+
case head
|
161
|
+
when RFC2183
|
162
|
+
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
163
|
+
|
164
|
+
if (filename = params["filename"])
|
165
|
+
filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
|
166
|
+
elsif (filename = params["filename*"])
|
167
|
+
encoding, _, filename = filename.split("'", 3)
|
168
|
+
end
|
169
|
+
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
170
|
+
filename = Regexp.last_match(1)
|
171
|
+
end
|
172
|
+
|
173
|
+
return unless filename
|
174
|
+
|
175
|
+
filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
176
|
+
|
177
|
+
filename.scrub!
|
178
|
+
|
179
|
+
filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
|
180
|
+
|
181
|
+
filename.force_encoding ::Encoding.find(encoding) if encoding
|
182
|
+
|
183
|
+
filename
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
17
17
|
|
18
18
|
elsif defined?(MimeMagic)
|
19
19
|
|
20
|
-
def call(file,
|
20
|
+
def call(file, _)
|
21
21
|
mime = MimeMagic.by_magic(file)
|
22
22
|
mime.type if mime
|
23
23
|
end
|
@@ -25,7 +25,7 @@ module HTTPX
|
|
25
25
|
elsif system("which file", out: File::NULL)
|
26
26
|
require "open3"
|
27
27
|
|
28
|
-
def call(file,
|
28
|
+
def call(file, _)
|
29
29
|
return if file.eof? # file command returns "application/x-empty" for empty files
|
30
30
|
|
31
31
|
Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
@@ -56,7 +56,7 @@ module HTTPX
|
|
56
56
|
|
57
57
|
else
|
58
58
|
|
59
|
-
def call(
|
59
|
+
def call(_, _); end
|
60
60
|
|
61
61
|
end
|
62
62
|
end
|
@@ -8,7 +8,7 @@ module HTTPX
|
|
8
8
|
def call(value)
|
9
9
|
# take out specialized objects of the way
|
10
10
|
if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
|
11
|
-
return
|
11
|
+
return value, value.content_type, value.filename
|
12
12
|
end
|
13
13
|
|
14
14
|
content_type = filename = nil
|
@@ -19,7 +19,7 @@ module HTTPX
|
|
19
19
|
value = value[:body]
|
20
20
|
end
|
21
21
|
|
22
|
-
value = value.open(
|
22
|
+
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
23
23
|
|
24
24
|
if value.is_a?(File)
|
25
25
|
filename ||= File.basename(value.path)
|
@@ -29,13 +29,14 @@ module HTTPX
|
|
29
29
|
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
30
30
|
require "http/form_data"
|
31
31
|
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
32
|
-
|
33
|
-
|
32
|
+
"https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
33
|
+
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
34
34
|
end
|
35
35
|
rescue LoadError
|
36
36
|
end
|
37
37
|
# :nocov:
|
38
38
|
require "httpx/plugins/multipart/encoder"
|
39
|
+
require "httpx/plugins/multipart/decoder"
|
39
40
|
require "httpx/plugins/multipart/part"
|
40
41
|
require "httpx/plugins/multipart/mime_type_detector"
|
41
42
|
end
|
@@ -56,6 +57,19 @@ module HTTPX
|
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
60
|
+
def decode(response)
|
61
|
+
content_type = response.content_type.mime_type
|
62
|
+
|
63
|
+
case content_type
|
64
|
+
when "application/x-www-form-urlencoded"
|
65
|
+
Transcoder::Form.decode(response)
|
66
|
+
when "multipart/form-data"
|
67
|
+
Decoder.new(response)
|
68
|
+
else
|
69
|
+
raise Error, "invalid form mime type (#{content_type})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
59
73
|
def multipart?(data)
|
60
74
|
data.any? do |_, v|
|
61
75
|
MULTIPART_VALUE_COND.call(v) ||
|
@@ -34,13 +34,13 @@ module HTTPX
|
|
34
34
|
|
35
35
|
alias_method :ntlm_auth, :ntlm_authentication
|
36
36
|
|
37
|
-
def send_requests(*requests
|
37
|
+
def send_requests(*requests)
|
38
38
|
requests.flat_map do |request|
|
39
39
|
ntlm = request.options.ntlm
|
40
40
|
|
41
41
|
if ntlm
|
42
42
|
request.headers["authorization"] = "NTLM #{NTLM.negotiate(domain: ntlm.domain).to_base64}"
|
43
|
-
probe_response = wrap { super(request
|
43
|
+
probe_response = wrap { super(request).first }
|
44
44
|
|
45
45
|
if !probe_response.is_a?(ErrorResponse) && probe_response.status == 401 &&
|
46
46
|
probe_response.headers.key?("www-authenticate") &&
|
@@ -52,12 +52,12 @@ module HTTPX
|
|
52
52
|
request.transition(:idle)
|
53
53
|
|
54
54
|
request.headers["authorization"] = "NTLM #{ntlm_challenge}"
|
55
|
-
super(request
|
55
|
+
super(request)
|
56
56
|
else
|
57
57
|
probe_response
|
58
58
|
end
|
59
59
|
else
|
60
|
-
super(request
|
60
|
+
super(request)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
@@ -19,10 +19,14 @@ module HTTPX
|
|
19
19
|
end
|
20
20
|
|
21
21
|
module InstanceMethods
|
22
|
-
|
22
|
+
def request(*args, **options)
|
23
|
+
raise ArgumentError, "must perform at least one request" if args.empty?
|
24
|
+
|
25
|
+
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
26
|
+
|
27
|
+
request = requests.first or return super
|
23
28
|
|
24
|
-
|
25
|
-
request_options = @options.merge(options)
|
29
|
+
request_options = request.options
|
26
30
|
|
27
31
|
return super unless request_options.proxy
|
28
32
|
|
@@ -37,18 +41,21 @@ module HTTPX
|
|
37
41
|
if request_options.debug
|
38
42
|
ssh_options[:verbose] = request_options.debug_level == 2 ? :debug : :info
|
39
43
|
end
|
44
|
+
|
40
45
|
request_uri = URI(requests.first.uri)
|
41
46
|
@_gateway = Net::SSH::Gateway.new(ssh_uri.host, ssh_username, ssh_options)
|
42
47
|
begin
|
43
48
|
@_gateway.open(request_uri.host, request_uri.port) do |local_port|
|
44
49
|
io = build_gateway_socket(local_port, request_uri, request_options)
|
45
|
-
super(*
|
50
|
+
super(*args, **options.merge(io: io))
|
46
51
|
end
|
47
52
|
ensure
|
48
53
|
@_gateway.shutdown!
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
57
|
+
private
|
58
|
+
|
52
59
|
def build_gateway_socket(port, request_uri, options)
|
53
60
|
case request_uri.scheme
|
54
61
|
when "https"
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -5,7 +5,8 @@ require "ipaddr"
|
|
5
5
|
require "forwardable"
|
6
6
|
|
7
7
|
module HTTPX
|
8
|
-
HTTPProxyError
|
8
|
+
class HTTPProxyError < Error; end
|
9
|
+
|
9
10
|
module Plugins
|
10
11
|
#
|
11
12
|
# This plugin adds support for proxies. It ships with support for:
|
@@ -136,10 +137,11 @@ module HTTPX
|
|
136
137
|
def __proxy_error?(response)
|
137
138
|
error = response.error
|
138
139
|
case error
|
139
|
-
when
|
140
|
+
when NativeResolveError
|
140
141
|
# failed resolving proxy domain
|
141
|
-
|
142
|
-
|
142
|
+
error.connection.origin.to_s == @_proxy_uris.first
|
143
|
+
when ResolveError
|
144
|
+
error.message.end_with?(@_proxy_uris.first)
|
143
145
|
when *PROXY_ERRORS
|
144
146
|
# timeout errors connecting to proxy
|
145
147
|
true
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module HTTPX::Plugins
|
6
|
+
module ResponseCache
|
7
|
+
class Store
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegator :@store, :clear
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@store = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def lookup(uri)
|
17
|
+
@store[uri]
|
18
|
+
end
|
19
|
+
|
20
|
+
def cached?(uri)
|
21
|
+
@store.key?(uri)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cache(uri, response)
|
25
|
+
@store[uri] = response
|
26
|
+
end
|
27
|
+
|
28
|
+
def prepare(request)
|
29
|
+
cached_response = @store[request.uri]
|
30
|
+
|
31
|
+
return unless cached_response
|
32
|
+
|
33
|
+
original_request = cached_response.instance_variable_get(:@request)
|
34
|
+
|
35
|
+
if (vary = cached_response.headers["vary"])
|
36
|
+
if vary == "*"
|
37
|
+
return unless request.headers.same_headers?(original_request.headers)
|
38
|
+
else
|
39
|
+
return unless vary.split(/ *, */).all? do |cache_field|
|
40
|
+
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
46
|
+
request.headers.add("if-modified-since", last_modified)
|
47
|
+
end
|
48
|
+
|
49
|
+
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
|
50
|
+
request.headers.add("if-none-match", etag)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|