httparty 0.18.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +24 -0
- data/.gitignore +2 -1
- data/Changelog.md +402 -308
- data/Gemfile +3 -0
- data/Guardfile +3 -2
- data/README.md +16 -18
- data/docs/README.md +89 -2
- data/examples/README.md +3 -0
- data/examples/aaws.rb +6 -2
- data/examples/idn.rb +10 -0
- data/examples/party_foul_mode.rb +90 -0
- data/httparty.gemspec +4 -2
- data/lib/httparty/connection_adapter.rb +8 -25
- data/lib/httparty/cookie_hash.rb +3 -1
- data/lib/httparty/decompressor.rb +102 -0
- data/lib/httparty/exceptions.rb +37 -4
- data/lib/httparty/hash_conversions.rb +4 -2
- data/lib/httparty/headers_processor.rb +2 -0
- data/lib/httparty/logger/apache_formatter.rb +4 -2
- data/lib/httparty/logger/curl_formatter.rb +5 -3
- data/lib/httparty/logger/logger.rb +2 -0
- data/lib/httparty/logger/logstash_formatter.rb +5 -2
- data/lib/httparty/module_inheritable_attributes.rb +6 -6
- data/lib/httparty/net_digest_auth.rb +9 -10
- data/lib/httparty/parser.rb +12 -5
- data/lib/httparty/request/body.rb +53 -12
- data/lib/httparty/request/multipart_boundary.rb +2 -0
- data/lib/httparty/request/streaming_multipart_body.rb +188 -0
- data/lib/httparty/request.rb +130 -43
- data/lib/httparty/response/headers.rb +2 -0
- data/lib/httparty/response.rb +7 -5
- data/lib/httparty/response_fragment.rb +2 -0
- data/lib/httparty/text_encoder.rb +7 -5
- data/lib/httparty/utils.rb +2 -0
- data/lib/httparty/version.rb +3 -1
- data/lib/httparty.rb +45 -14
- data/script/release +4 -4
- metadata +33 -14
- data/.simplecov +0 -1
- data/.travis.yml +0 -11
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTTParty
|
|
2
4
|
module Logger
|
|
3
5
|
class CurlFormatter #:nodoc:
|
|
4
6
|
TAG_NAME = HTTParty.name
|
|
5
|
-
OUT = '>'
|
|
6
|
-
IN = '<'
|
|
7
|
+
OUT = '>'
|
|
8
|
+
IN = '<'
|
|
7
9
|
|
|
8
10
|
attr_accessor :level, :logger
|
|
9
11
|
|
|
@@ -44,7 +46,7 @@ module HTTParty
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def log_url
|
|
47
|
-
http_method = request.http_method.name.split(
|
|
49
|
+
http_method = request.http_method.name.split('::').last.upcase
|
|
48
50
|
uri = if request.options[:base_uri]
|
|
49
51
|
request.options[:base_uri] + request.path.path
|
|
50
52
|
else
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTTParty
|
|
2
4
|
module Logger
|
|
3
5
|
class LogstashFormatter #:nodoc:
|
|
@@ -22,6 +24,7 @@ module HTTParty
|
|
|
22
24
|
attr_reader :request, :response
|
|
23
25
|
|
|
24
26
|
def logstash_message
|
|
27
|
+
require 'json'
|
|
25
28
|
{
|
|
26
29
|
'@timestamp' => current_time,
|
|
27
30
|
'@version' => 1,
|
|
@@ -40,11 +43,11 @@ module HTTParty
|
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
def current_time
|
|
43
|
-
Time.now.strftime(
|
|
46
|
+
Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def http_method
|
|
47
|
-
@http_method ||= request.http_method.name.split(
|
|
50
|
+
@http_method ||= request.http_method.name.split('::').last.upcase
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def path
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTTParty
|
|
2
4
|
module ModuleInheritableAttributes #:nodoc:
|
|
3
5
|
def self.included(base)
|
|
@@ -27,7 +29,7 @@ module HTTParty
|
|
|
27
29
|
@mattr_inheritable_attrs += args
|
|
28
30
|
|
|
29
31
|
args.each do |arg|
|
|
30
|
-
|
|
32
|
+
singleton_class.attr_accessor(arg)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
@mattr_inheritable_attrs
|
|
@@ -36,18 +38,16 @@ module HTTParty
|
|
|
36
38
|
def inherited(subclass)
|
|
37
39
|
super
|
|
38
40
|
@mattr_inheritable_attrs.each do |inheritable_attribute|
|
|
39
|
-
ivar = "@#{inheritable_attribute}"
|
|
41
|
+
ivar = :"@#{inheritable_attribute}"
|
|
40
42
|
subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
|
|
41
43
|
|
|
42
44
|
if instance_variable_get(ivar).respond_to?(:merge)
|
|
43
|
-
|
|
45
|
+
subclass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
44
46
|
def self.#{inheritable_attribute}
|
|
45
47
|
duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar})
|
|
46
48
|
#{ivar} = superclass.#{inheritable_attribute}.merge(duplicate)
|
|
47
49
|
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
subclass.class_eval method
|
|
50
|
+
RUBY
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'digest/md5'
|
|
2
4
|
require 'net/http'
|
|
3
5
|
|
|
@@ -44,12 +46,9 @@ module Net
|
|
|
44
46
|
header << %(algorithm="#{@response['algorithm']}") if algorithm_present?
|
|
45
47
|
|
|
46
48
|
if qop_present?
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"nc=00000001"
|
|
51
|
-
]
|
|
52
|
-
fields.each { |field| header << field }
|
|
49
|
+
header << %(cnonce="#{@cnonce}")
|
|
50
|
+
header << %(qop="#{@response['qop']}")
|
|
51
|
+
header << 'nc=00000001'
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
header << %(opaque="#{@response['opaque']}") if opaque_present?
|
|
@@ -98,13 +97,13 @@ module Net
|
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
def random
|
|
101
|
-
format
|
|
100
|
+
format '%x', (Time.now.to_i + rand(65535))
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
def request_digest
|
|
105
104
|
a = [md5(a1), @response['nonce'], md5(a2)]
|
|
106
|
-
a.insert(2,
|
|
107
|
-
md5(a.join(
|
|
105
|
+
a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
|
|
106
|
+
md5(a.join(':'))
|
|
108
107
|
end
|
|
109
108
|
|
|
110
109
|
def md5(str)
|
|
@@ -129,7 +128,7 @@ module Net
|
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
def a2
|
|
132
|
-
[@method, @path].join(
|
|
131
|
+
[@method, @path].join(':')
|
|
133
132
|
end
|
|
134
133
|
end
|
|
135
134
|
end
|
data/lib/httparty/parser.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTTParty
|
|
2
4
|
# The default parser used by HTTParty, supports xml, json, html, csv and
|
|
3
5
|
# plain text.
|
|
@@ -101,7 +103,7 @@ module HTTParty
|
|
|
101
103
|
# @return [nil] when the response body is nil, an empty string, spaces only or "null"
|
|
102
104
|
def parse
|
|
103
105
|
return nil if body.nil?
|
|
104
|
-
return nil if body ==
|
|
106
|
+
return nil if body == 'null'
|
|
105
107
|
return nil if body.valid_encoding? && body.strip.empty?
|
|
106
108
|
if body.valid_encoding? && body.encoding == Encoding::UTF_8
|
|
107
109
|
@body = body.gsub(/\A#{UTF8_BOM}/, '')
|
|
@@ -116,16 +118,19 @@ module HTTParty
|
|
|
116
118
|
protected
|
|
117
119
|
|
|
118
120
|
def xml
|
|
121
|
+
require 'multi_xml'
|
|
119
122
|
MultiXml.parse(body)
|
|
120
123
|
end
|
|
121
124
|
|
|
122
|
-
UTF8_BOM = "\xEF\xBB\xBF"
|
|
125
|
+
UTF8_BOM = "\xEF\xBB\xBF"
|
|
123
126
|
|
|
124
127
|
def json
|
|
128
|
+
require 'json'
|
|
125
129
|
JSON.parse(body, :quirks_mode => true, :allow_nan => true)
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
def csv
|
|
133
|
+
require 'csv'
|
|
129
134
|
CSV.parse(body)
|
|
130
135
|
end
|
|
131
136
|
|
|
@@ -142,9 +147,11 @@ module HTTParty
|
|
|
142
147
|
end
|
|
143
148
|
|
|
144
149
|
def parse_supported_format
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
if respond_to?(format, true)
|
|
151
|
+
send(format)
|
|
152
|
+
else
|
|
153
|
+
raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
|
|
154
|
+
end
|
|
148
155
|
end
|
|
149
156
|
end
|
|
150
157
|
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'multipart_boundary'
|
|
4
|
+
require_relative 'streaming_multipart_body'
|
|
2
5
|
|
|
3
6
|
module HTTParty
|
|
4
7
|
class Request
|
|
5
8
|
class Body
|
|
9
|
+
NEWLINE = "\r\n"
|
|
10
|
+
private_constant :NEWLINE
|
|
11
|
+
|
|
6
12
|
def initialize(params, query_string_normalizer: nil, force_multipart: false)
|
|
7
13
|
@params = params
|
|
8
14
|
@query_string_normalizer = query_string_normalizer
|
|
@@ -25,25 +31,48 @@ module HTTParty
|
|
|
25
31
|
params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
def streaming?
|
|
35
|
+
multipart? && has_file?(params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_stream
|
|
39
|
+
return nil unless streaming?
|
|
40
|
+
StreamingMultipartBody.new(prepared_parts, boundary)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prepared_parts
|
|
44
|
+
normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
|
|
45
|
+
normalized_params.map do |key, value|
|
|
46
|
+
[key, value, file?(value)]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
private
|
|
29
51
|
|
|
52
|
+
# https://html.spec.whatwg.org/#multipart-form-data
|
|
53
|
+
MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
|
|
54
|
+
'"' => '%22',
|
|
55
|
+
"\r" => '%0D',
|
|
56
|
+
"\n" => '%0A'
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
30
59
|
def generate_multipart
|
|
31
60
|
normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
|
|
32
61
|
|
|
33
|
-
multipart = normalized_params.inject('') do |memo, (key, value)|
|
|
34
|
-
memo
|
|
35
|
-
memo
|
|
62
|
+
multipart = normalized_params.inject(''.b) do |memo, (key, value)|
|
|
63
|
+
memo << "--#{boundary}#{NEWLINE}".b
|
|
64
|
+
memo << %(Content-Disposition: form-data; name="#{key}").b
|
|
36
65
|
# value.path is used to support ActionDispatch::Http::UploadedFile
|
|
37
66
|
# https://github.com/jnunemaker/httparty/pull/585
|
|
38
|
-
memo
|
|
39
|
-
memo
|
|
40
|
-
memo
|
|
41
|
-
memo
|
|
42
|
-
memo
|
|
43
|
-
memo
|
|
67
|
+
memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}").b if file?(value)
|
|
68
|
+
memo << NEWLINE.b
|
|
69
|
+
memo << "Content-Type: #{content_type(value)}#{NEWLINE}".b if file?(value)
|
|
70
|
+
memo << NEWLINE.b
|
|
71
|
+
memo << content_body(value)
|
|
72
|
+
memo << NEWLINE.b
|
|
44
73
|
end
|
|
45
74
|
|
|
46
|
-
multipart
|
|
75
|
+
multipart << "--#{boundary}--#{NEWLINE}".b
|
|
47
76
|
end
|
|
48
77
|
|
|
49
78
|
def has_file?(value)
|
|
@@ -68,10 +97,22 @@ module HTTParty
|
|
|
68
97
|
end
|
|
69
98
|
end
|
|
70
99
|
|
|
100
|
+
def content_body(object)
|
|
101
|
+
if file?(object)
|
|
102
|
+
object = (file = object).read
|
|
103
|
+
object.force_encoding(Encoding::BINARY) if object.respond_to?(:force_encoding)
|
|
104
|
+
file.rewind if file.respond_to?(:rewind)
|
|
105
|
+
object.to_s
|
|
106
|
+
else
|
|
107
|
+
object.to_s.b
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
71
111
|
def content_type(object)
|
|
72
112
|
return object.content_type if object.respond_to?(:content_type)
|
|
73
|
-
|
|
74
|
-
mime
|
|
113
|
+
require 'mini_mime'
|
|
114
|
+
mime = MiniMime.lookup_by_filename(object.path)
|
|
115
|
+
mime ? mime.content_type : 'application/octet-stream'
|
|
75
116
|
end
|
|
76
117
|
|
|
77
118
|
def file_name(object)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTParty
|
|
4
|
+
class Request
|
|
5
|
+
class StreamingMultipartBody
|
|
6
|
+
NEWLINE = "\r\n"
|
|
7
|
+
CHUNK_SIZE = 64 * 1024 # 64 KB chunks
|
|
8
|
+
|
|
9
|
+
def initialize(parts, boundary)
|
|
10
|
+
@parts = parts
|
|
11
|
+
@boundary = boundary
|
|
12
|
+
@part_index = 0
|
|
13
|
+
@state = :header
|
|
14
|
+
@current_file = nil
|
|
15
|
+
@header_buffer = nil
|
|
16
|
+
@header_offset = 0
|
|
17
|
+
@footer_sent = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def size
|
|
21
|
+
@size ||= calculate_size
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read(length = nil, outbuf = nil)
|
|
25
|
+
outbuf = outbuf ? outbuf.replace(''.b) : ''.b
|
|
26
|
+
|
|
27
|
+
return read_all(outbuf) if length.nil?
|
|
28
|
+
|
|
29
|
+
while outbuf.bytesize < length
|
|
30
|
+
chunk = read_chunk(length - outbuf.bytesize)
|
|
31
|
+
break if chunk.nil?
|
|
32
|
+
outbuf << chunk
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
outbuf.empty? ? nil : outbuf
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rewind
|
|
39
|
+
@part_index = 0
|
|
40
|
+
@state = :header
|
|
41
|
+
@current_file = nil
|
|
42
|
+
@header_buffer = nil
|
|
43
|
+
@header_offset = 0
|
|
44
|
+
@footer_sent = false
|
|
45
|
+
@parts.each do |_key, value, _is_file|
|
|
46
|
+
value.rewind if value.respond_to?(:rewind)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def read_all(outbuf)
|
|
53
|
+
while (chunk = read_chunk(CHUNK_SIZE))
|
|
54
|
+
outbuf << chunk
|
|
55
|
+
end
|
|
56
|
+
outbuf.empty? ? nil : outbuf
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def read_chunk(max_length)
|
|
60
|
+
loop do
|
|
61
|
+
return nil if @part_index >= @parts.size && @footer_sent
|
|
62
|
+
|
|
63
|
+
if @part_index >= @parts.size
|
|
64
|
+
@footer_sent = true
|
|
65
|
+
return "--#{@boundary}--#{NEWLINE}".b
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
key, value, is_file = @parts[@part_index]
|
|
69
|
+
|
|
70
|
+
case @state
|
|
71
|
+
when :header
|
|
72
|
+
chunk = read_header_chunk(key, value, is_file, max_length)
|
|
73
|
+
return chunk if chunk
|
|
74
|
+
|
|
75
|
+
when :body
|
|
76
|
+
chunk = read_body_chunk(value, is_file, max_length)
|
|
77
|
+
return chunk if chunk
|
|
78
|
+
|
|
79
|
+
when :newline
|
|
80
|
+
@state = :header
|
|
81
|
+
@part_index += 1
|
|
82
|
+
return NEWLINE.b
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_header_chunk(key, value, is_file, max_length)
|
|
88
|
+
if @header_buffer.nil?
|
|
89
|
+
@header_buffer = build_part_header(key, value, is_file)
|
|
90
|
+
@header_offset = 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
remaining = @header_buffer.bytesize - @header_offset
|
|
94
|
+
if remaining > 0
|
|
95
|
+
chunk_size = [remaining, max_length].min
|
|
96
|
+
chunk = @header_buffer.byteslice(@header_offset, chunk_size)
|
|
97
|
+
@header_offset += chunk_size
|
|
98
|
+
return chunk
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
@header_buffer = nil
|
|
102
|
+
@header_offset = 0
|
|
103
|
+
@state = :body
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def read_body_chunk(value, is_file, max_length)
|
|
108
|
+
if is_file
|
|
109
|
+
chunk = read_file_chunk(value, max_length)
|
|
110
|
+
if chunk
|
|
111
|
+
return chunk
|
|
112
|
+
else
|
|
113
|
+
@current_file = nil
|
|
114
|
+
@state = :newline
|
|
115
|
+
return nil
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
@state = :newline
|
|
119
|
+
return value.to_s.b
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def read_file_chunk(file, max_length)
|
|
124
|
+
chunk_size = [max_length, CHUNK_SIZE].min
|
|
125
|
+
chunk = file.read(chunk_size)
|
|
126
|
+
return nil if chunk.nil?
|
|
127
|
+
chunk.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
|
|
128
|
+
chunk
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_part_header(key, value, is_file)
|
|
132
|
+
header = "--#{@boundary}#{NEWLINE}".b
|
|
133
|
+
header << %(Content-Disposition: form-data; name="#{key}").b
|
|
134
|
+
if is_file
|
|
135
|
+
header << %(; filename="#{file_name(value).gsub(/["\r\n]/, replacement_table)}").b
|
|
136
|
+
header << NEWLINE.b
|
|
137
|
+
header << "Content-Type: #{content_type(value)}#{NEWLINE}".b
|
|
138
|
+
end
|
|
139
|
+
header << NEWLINE.b
|
|
140
|
+
header
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def calculate_size
|
|
144
|
+
total = 0
|
|
145
|
+
@parts.each do |key, value, is_file|
|
|
146
|
+
total += build_part_header(key, value, is_file).bytesize
|
|
147
|
+
total += content_size(value, is_file)
|
|
148
|
+
total += NEWLINE.bytesize
|
|
149
|
+
end
|
|
150
|
+
total += "--#{@boundary}--#{NEWLINE}".bytesize
|
|
151
|
+
total
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def content_size(value, is_file)
|
|
155
|
+
if is_file
|
|
156
|
+
if value.respond_to?(:size)
|
|
157
|
+
value.size
|
|
158
|
+
elsif value.respond_to?(:stat)
|
|
159
|
+
value.stat.size
|
|
160
|
+
else
|
|
161
|
+
value.read.bytesize.tap { value.rewind }
|
|
162
|
+
end
|
|
163
|
+
else
|
|
164
|
+
value.to_s.b.bytesize
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def content_type(object)
|
|
169
|
+
return object.content_type if object.respond_to?(:content_type)
|
|
170
|
+
require 'mini_mime'
|
|
171
|
+
mime = MiniMime.lookup_by_filename(object.path)
|
|
172
|
+
mime ? mime.content_type : 'application/octet-stream'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def file_name(object)
|
|
176
|
+
object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def replacement_table
|
|
180
|
+
@replacement_table ||= {
|
|
181
|
+
'"' => '%22',
|
|
182
|
+
"\r" => '%0D',
|
|
183
|
+
"\n" => '%0A'
|
|
184
|
+
}.freeze
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|