httparty 0.13.7 → 0.24.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.
Files changed (108) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +24 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop_todo.yml +1 -1
  7. data/Changelog.md +624 -0
  8. data/Gemfile +11 -3
  9. data/Guardfile +3 -2
  10. data/README.md +18 -17
  11. data/bin/httparty +3 -1
  12. data/docs/README.md +223 -0
  13. data/examples/README.md +35 -12
  14. data/examples/aaws.rb +7 -3
  15. data/examples/body_stream.rb +14 -0
  16. data/examples/crack.rb +1 -1
  17. data/examples/custom_parsers.rb +5 -1
  18. data/examples/delicious.rb +4 -4
  19. data/examples/headers_and_user_agents.rb +7 -3
  20. data/examples/idn.rb +10 -0
  21. data/examples/logging.rb +4 -4
  22. data/examples/microsoft_graph.rb +52 -0
  23. data/examples/multipart.rb +35 -0
  24. data/examples/party_foul_mode.rb +90 -0
  25. data/examples/peer_cert.rb +9 -0
  26. data/examples/stackexchange.rb +1 -1
  27. data/examples/stream_download.rb +26 -0
  28. data/examples/tripit_sign_in.rb +17 -6
  29. data/examples/twitter.rb +2 -2
  30. data/examples/whoismyrep.rb +1 -1
  31. data/httparty.gemspec +9 -5
  32. data/lib/httparty/connection_adapter.rb +71 -24
  33. data/lib/httparty/cookie_hash.rb +10 -8
  34. data/lib/httparty/decompressor.rb +102 -0
  35. data/lib/httparty/exceptions.rb +42 -5
  36. data/lib/httparty/hash_conversions.rb +30 -8
  37. data/lib/httparty/headers_processor.rb +32 -0
  38. data/lib/httparty/logger/apache_formatter.rb +31 -6
  39. data/lib/httparty/logger/curl_formatter.rb +68 -23
  40. data/lib/httparty/logger/logger.rb +5 -1
  41. data/lib/httparty/logger/logstash_formatter.rb +62 -0
  42. data/lib/httparty/module_inheritable_attributes.rb +9 -9
  43. data/lib/httparty/net_digest_auth.rb +23 -21
  44. data/lib/httparty/parser.rb +28 -14
  45. data/lib/httparty/request/body.rb +125 -0
  46. data/lib/httparty/request/multipart_boundary.rb +13 -0
  47. data/lib/httparty/request/streaming_multipart_body.rb +190 -0
  48. data/lib/httparty/request.rb +224 -122
  49. data/lib/httparty/response/headers.rb +23 -19
  50. data/lib/httparty/response.rb +92 -13
  51. data/lib/httparty/response_fragment.rb +21 -0
  52. data/lib/httparty/text_encoder.rb +72 -0
  53. data/lib/httparty/utils.rb +13 -0
  54. data/lib/httparty/version.rb +3 -1
  55. data/lib/httparty.rb +118 -42
  56. data/script/release +4 -4
  57. data/website/css/common.css +1 -1
  58. metadata +50 -112
  59. data/.simplecov +0 -1
  60. data/.travis.yml +0 -7
  61. data/History +0 -390
  62. data/features/basic_authentication.feature +0 -20
  63. data/features/command_line.feature +0 -95
  64. data/features/deals_with_http_error_codes.feature +0 -26
  65. data/features/digest_authentication.feature +0 -30
  66. data/features/handles_compressed_responses.feature +0 -27
  67. data/features/handles_multiple_formats.feature +0 -57
  68. data/features/steps/env.rb +0 -27
  69. data/features/steps/httparty_response_steps.rb +0 -52
  70. data/features/steps/httparty_steps.rb +0 -43
  71. data/features/steps/mongrel_helper.rb +0 -127
  72. data/features/steps/remote_service_steps.rb +0 -90
  73. data/features/supports_read_timeout_option.feature +0 -13
  74. data/features/supports_redirection.feature +0 -22
  75. data/features/supports_timeout_option.feature +0 -13
  76. data/spec/fixtures/delicious.xml +0 -23
  77. data/spec/fixtures/empty.xml +0 -0
  78. data/spec/fixtures/google.html +0 -3
  79. data/spec/fixtures/ssl/generate.sh +0 -29
  80. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -16
  81. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  82. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  83. data/spec/fixtures/ssl/generated/ca.key +0 -15
  84. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  85. data/spec/fixtures/ssl/generated/server.crt +0 -13
  86. data/spec/fixtures/ssl/generated/server.key +0 -15
  87. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  88. data/spec/fixtures/twitter.csv +0 -2
  89. data/spec/fixtures/twitter.json +0 -1
  90. data/spec/fixtures/twitter.xml +0 -403
  91. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  92. data/spec/httparty/connection_adapter_spec.rb +0 -468
  93. data/spec/httparty/cookie_hash_spec.rb +0 -83
  94. data/spec/httparty/exception_spec.rb +0 -38
  95. data/spec/httparty/hash_conversions_spec.rb +0 -41
  96. data/spec/httparty/logger/apache_formatter_spec.rb +0 -41
  97. data/spec/httparty/logger/curl_formatter_spec.rb +0 -18
  98. data/spec/httparty/logger/logger_spec.rb +0 -38
  99. data/spec/httparty/net_digest_auth_spec.rb +0 -230
  100. data/spec/httparty/parser_spec.rb +0 -173
  101. data/spec/httparty/request_spec.rb +0 -1073
  102. data/spec/httparty/response_spec.rb +0 -241
  103. data/spec/httparty/ssl_spec.rb +0 -74
  104. data/spec/httparty_spec.rb +0 -850
  105. data/spec/spec_helper.rb +0 -59
  106. data/spec/support/ssl_test_helper.rb +0 -47
  107. data/spec/support/ssl_test_server.rb +0 -80
  108. data/spec/support/stub_response.rb +0 -49
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multipart_boundary'
4
+ require_relative 'streaming_multipart_body'
5
+
6
+ module HTTParty
7
+ class Request
8
+ class Body
9
+ NEWLINE = "\r\n"
10
+ private_constant :NEWLINE
11
+
12
+ def initialize(params, query_string_normalizer: nil, force_multipart: false)
13
+ @params = params
14
+ @query_string_normalizer = query_string_normalizer
15
+ @force_multipart = force_multipart
16
+ end
17
+
18
+ def call
19
+ if params.respond_to?(:to_hash)
20
+ multipart? ? generate_multipart : normalize_query(params)
21
+ else
22
+ params
23
+ end
24
+ end
25
+
26
+ def boundary
27
+ @boundary ||= MultipartBoundary.generate
28
+ end
29
+
30
+ def multipart?
31
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
32
+ end
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
+
50
+ private
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
+
59
+ def generate_multipart
60
+ normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
61
+
62
+ multipart = normalized_params.inject(''.b) do |memo, (key, value)|
63
+ memo << "--#{boundary}#{NEWLINE}".b
64
+ memo << %(Content-Disposition: form-data; name="#{key}").b
65
+ # value.path is used to support ActionDispatch::Http::UploadedFile
66
+ # https://github.com/jnunemaker/httparty/pull/585
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
73
+ end
74
+
75
+ multipart << "--#{boundary}--#{NEWLINE}".b
76
+ end
77
+
78
+ def has_file?(value)
79
+ if value.respond_to?(:to_hash)
80
+ value.to_hash.any? { |_, v| has_file?(v) }
81
+ elsif value.respond_to?(:to_ary)
82
+ value.to_ary.any? { |v| has_file?(v) }
83
+ else
84
+ file?(value)
85
+ end
86
+ end
87
+
88
+ def file?(object)
89
+ object.respond_to?(:path) && object.respond_to?(:read)
90
+ end
91
+
92
+ def normalize_query(query)
93
+ if query_string_normalizer
94
+ query_string_normalizer.call(query)
95
+ else
96
+ HashConversions.to_params(query)
97
+ end
98
+ end
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
+
111
+ def content_type(object)
112
+ return object.content_type if object.respond_to?(:content_type)
113
+ require 'mini_mime'
114
+ mime = MiniMime.lookup_by_filename(object.path)
115
+ mime ? mime.content_type : 'application/octet-stream'
116
+ end
117
+
118
+ def file_name(object)
119
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
120
+ end
121
+
122
+ attr_reader :params, :query_string_normalizer, :force_multipart
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module HTTParty
6
+ class Request
7
+ class MultipartBoundary
8
+ def self.generate
9
+ "------------------------#{SecureRandom.urlsafe_base64(12)}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,190 @@
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
+ else
139
+ header << NEWLINE.b
140
+ end
141
+ header << NEWLINE.b
142
+ header
143
+ end
144
+
145
+ def calculate_size
146
+ total = 0
147
+ @parts.each do |key, value, is_file|
148
+ total += build_part_header(key, value, is_file).bytesize
149
+ total += content_size(value, is_file)
150
+ total += NEWLINE.bytesize
151
+ end
152
+ total += "--#{@boundary}--#{NEWLINE}".bytesize
153
+ total
154
+ end
155
+
156
+ def content_size(value, is_file)
157
+ if is_file
158
+ if value.respond_to?(:size)
159
+ value.size
160
+ elsif value.respond_to?(:stat)
161
+ value.stat.size
162
+ else
163
+ value.read.bytesize.tap { value.rewind }
164
+ end
165
+ else
166
+ value.to_s.b.bytesize
167
+ end
168
+ end
169
+
170
+ def content_type(object)
171
+ return object.content_type if object.respond_to?(:content_type)
172
+ require 'mini_mime'
173
+ mime = MiniMime.lookup_by_filename(object.path)
174
+ mime ? mime.content_type : 'application/octet-stream'
175
+ end
176
+
177
+ def file_name(object)
178
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
179
+ end
180
+
181
+ def replacement_table
182
+ @replacement_table ||= {
183
+ '"' => '%22',
184
+ "\r" => '%0D',
185
+ "\n" => '%0A'
186
+ }.freeze
187
+ end
188
+ end
189
+ end
190
+ end