httparty 0.13.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop.yml +92 -0
  6. data/.rubocop_todo.yml +124 -0
  7. data/CONTRIBUTING.md +23 -0
  8. data/Changelog.md +557 -0
  9. data/Gemfile +15 -3
  10. data/Guardfile +4 -3
  11. data/README.md +24 -25
  12. data/Rakefile +5 -7
  13. data/bin/httparty +20 -14
  14. data/docs/README.md +191 -0
  15. data/examples/README.md +89 -0
  16. data/examples/aaws.rb +10 -6
  17. data/examples/basic.rb +6 -10
  18. data/examples/body_stream.rb +14 -0
  19. data/examples/crack.rb +2 -2
  20. data/examples/custom_parsers.rb +6 -5
  21. data/examples/delicious.rb +8 -8
  22. data/examples/google.rb +2 -2
  23. data/examples/headers_and_user_agents.rb +7 -3
  24. data/examples/idn.rb +10 -0
  25. data/examples/logging.rb +36 -0
  26. data/examples/microsoft_graph.rb +52 -0
  27. data/examples/multipart.rb +22 -0
  28. data/examples/nokogiri_html_parser.rb +0 -3
  29. data/examples/peer_cert.rb +9 -0
  30. data/examples/rescue_json.rb +17 -0
  31. data/examples/rubyurl.rb +3 -3
  32. data/examples/stackexchange.rb +24 -0
  33. data/examples/stream_download.rb +26 -0
  34. data/examples/tripit_sign_in.rb +20 -9
  35. data/examples/twitter.rb +7 -7
  36. data/examples/whoismyrep.rb +1 -1
  37. data/httparty.gemspec +13 -9
  38. data/lib/httparty/connection_adapter.rb +105 -25
  39. data/lib/httparty/cookie_hash.rb +10 -9
  40. data/lib/httparty/decompressor.rb +102 -0
  41. data/lib/httparty/exceptions.rb +8 -2
  42. data/lib/httparty/hash_conversions.rb +39 -19
  43. data/lib/httparty/headers_processor.rb +32 -0
  44. data/lib/httparty/logger/apache_formatter.rb +47 -0
  45. data/lib/httparty/logger/curl_formatter.rb +93 -0
  46. data/lib/httparty/logger/logger.rb +22 -10
  47. data/lib/httparty/logger/logstash_formatter.rb +61 -0
  48. data/lib/httparty/module_inheritable_attributes.rb +6 -4
  49. data/lib/httparty/net_digest_auth.rb +76 -25
  50. data/lib/httparty/parser.rb +28 -15
  51. data/lib/httparty/request/body.rb +105 -0
  52. data/lib/httparty/request/multipart_boundary.rb +13 -0
  53. data/lib/httparty/request.rb +218 -130
  54. data/lib/httparty/response/headers.rb +23 -19
  55. data/lib/httparty/response.rb +99 -15
  56. data/lib/httparty/response_fragment.rb +21 -0
  57. data/lib/httparty/text_encoder.rb +72 -0
  58. data/lib/httparty/utils.rb +13 -0
  59. data/lib/httparty/version.rb +3 -1
  60. data/lib/httparty.rb +191 -83
  61. data/website/css/common.css +1 -1
  62. data/website/index.html +3 -3
  63. metadata +50 -120
  64. data/.travis.yml +0 -7
  65. data/History +0 -303
  66. data/features/basic_authentication.feature +0 -20
  67. data/features/command_line.feature +0 -7
  68. data/features/deals_with_http_error_codes.feature +0 -26
  69. data/features/digest_authentication.feature +0 -20
  70. data/features/handles_compressed_responses.feature +0 -27
  71. data/features/handles_multiple_formats.feature +0 -57
  72. data/features/steps/env.rb +0 -22
  73. data/features/steps/httparty_response_steps.rb +0 -52
  74. data/features/steps/httparty_steps.rb +0 -35
  75. data/features/steps/mongrel_helper.rb +0 -94
  76. data/features/steps/remote_service_steps.rb +0 -74
  77. data/features/supports_redirection.feature +0 -22
  78. data/features/supports_timeout_option.feature +0 -13
  79. data/lib/httparty/core_extensions.rb +0 -32
  80. data/lib/httparty/logger/apache_logger.rb +0 -22
  81. data/lib/httparty/logger/curl_logger.rb +0 -48
  82. data/spec/fixtures/delicious.xml +0 -23
  83. data/spec/fixtures/empty.xml +0 -0
  84. data/spec/fixtures/google.html +0 -3
  85. data/spec/fixtures/ssl/generate.sh +0 -29
  86. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -16
  87. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  88. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  89. data/spec/fixtures/ssl/generated/ca.key +0 -15
  90. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  91. data/spec/fixtures/ssl/generated/server.crt +0 -13
  92. data/spec/fixtures/ssl/generated/server.key +0 -15
  93. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  94. data/spec/fixtures/twitter.csv +0 -2
  95. data/spec/fixtures/twitter.json +0 -1
  96. data/spec/fixtures/twitter.xml +0 -403
  97. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  98. data/spec/httparty/connection_adapter_spec.rb +0 -298
  99. data/spec/httparty/cookie_hash_spec.rb +0 -83
  100. data/spec/httparty/exception_spec.rb +0 -23
  101. data/spec/httparty/logger/apache_logger_spec.rb +0 -26
  102. data/spec/httparty/logger/curl_logger_spec.rb +0 -18
  103. data/spec/httparty/logger/logger_spec.rb +0 -22
  104. data/spec/httparty/net_digest_auth_spec.rb +0 -152
  105. data/spec/httparty/parser_spec.rb +0 -165
  106. data/spec/httparty/request_spec.rb +0 -631
  107. data/spec/httparty/response_spec.rb +0 -221
  108. data/spec/httparty/ssl_spec.rb +0 -74
  109. data/spec/httparty_spec.rb +0 -764
  110. data/spec/spec.opts +0 -2
  111. data/spec/spec_helper.rb +0 -37
  112. data/spec/support/ssl_test_helper.rb +0 -47
  113. data/spec/support/ssl_test_server.rb +0 -80
  114. data/spec/support/stub_response.rb +0 -43
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  # Default connection adapter that returns a new Net::HTTP each time
3
5
  #
4
6
  # == Custom Connection Factories
5
7
  #
6
8
  # If you like to implement your own connection adapter, subclassing
7
- # HTTPParty::ConnectionAdapter will make it easier. Just override
9
+ # HTTParty::ConnectionAdapter will make it easier. Just override
8
10
  # the #connection method. The uri and options attributes will have
9
11
  # all the info you need to construct your http connection. Whatever
10
12
  # you return from your connection method needs to adhere to the
@@ -38,47 +40,109 @@ module HTTParty
38
40
  # in the #options attribute. It is up to you to interpret them within your
39
41
  # connection adapter. Take a look at the implementation of
40
42
  # HTTParty::ConnectionAdapter#connection for examples of how they are used.
41
- # Something are probably interesting are as follows:
43
+ # The keys used in options are
42
44
  # * :+timeout+: timeout in seconds
45
+ # * :+open_timeout+: http connection open_timeout in seconds, overrides timeout if set
46
+ # * :+read_timeout+: http connection read_timeout in seconds, overrides timeout if set
47
+ # * :+write_timeout+: http connection write_timeout in seconds, overrides timeout if set (Ruby >= 2.6.0 required)
43
48
  # * :+debug_output+: see HTTParty::ClassMethods.debug_output.
44
- # * :+pem+: contains pem data. see HTTParty::ClassMethods.pem.
49
+ # * :+cert_store+: contains certificate data. see method 'attach_ssl_certificates'
50
+ # * :+pem+: contains pem client certificate data. see method 'attach_ssl_certificates'
51
+ # * :+p12+: contains PKCS12 client client certificate data. see method 'attach_ssl_certificates'
52
+ # * :+verify+: verify the server’s certificate against the ca certificate.
53
+ # * :+verify_peer+: set to false to turn off server verification but still send client certificate
45
54
  # * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file.
46
55
  # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
47
- # * :+connection_adapter_options+: contains the hash your passed to HTTParty.connection_adapter when you configured your connection adapter
48
- class ConnectionAdapter
56
+ # * :+ssl_version+: SSL versions to allow. see method 'attach_ssl_certificates'
57
+ # * :+ciphers+: The list of SSL ciphers to support
58
+ # * :+connection_adapter_options+: contains the hash you passed to HTTParty.connection_adapter when you configured your connection adapter
59
+ # * :+local_host+: The local address to bind to
60
+ # * :+local_port+: The local port to bind to
61
+ # * :+http_proxyaddr+: HTTP Proxy address
62
+ # * :+http_proxyport+: HTTP Proxy port
63
+ # * :+http_proxyuser+: HTTP Proxy user
64
+ # * :+http_proxypass+: HTTP Proxy password
65
+ #
66
+ # === Inherited methods
67
+ # * :+clean_host+: Method used to sanitize host names
49
68
 
69
+ class ConnectionAdapter
50
70
  # Private: Regex used to strip brackets from IPv6 URIs.
51
71
  StripIpv6BracketsRegex = /\A\[(.*)\]\z/
52
72
 
73
+ OPTION_DEFAULTS = {
74
+ verify: true,
75
+ verify_peer: true
76
+ }
77
+
53
78
  # Public
54
79
  def self.call(uri, options)
55
80
  new(uri, options).connection
56
81
  end
57
82
 
83
+ def self.default_cert_store
84
+ @default_cert_store ||= OpenSSL::X509::Store.new.tap do |cert_store|
85
+ cert_store.set_default_paths
86
+ end
87
+ end
88
+
58
89
  attr_reader :uri, :options
59
90
 
60
- def initialize(uri, options={})
61
- raise ArgumentError, "uri must be a URI, not a #{uri.class}" unless uri.kind_of? URI
91
+ def initialize(uri, options = {})
92
+ uri_adapter = options[:uri_adapter] || URI
93
+ raise ArgumentError, "uri must be a #{uri_adapter}, not a #{uri.class}" unless uri.is_a? uri_adapter
62
94
 
63
95
  @uri = uri
64
- @options = options
96
+ @options = OPTION_DEFAULTS.merge(options)
65
97
  end
66
98
 
67
99
  def connection
68
100
  host = clean_host(uri.host)
69
- if options[:http_proxyaddr]
70
- http = Net::HTTP.new(host, uri.port, options[:http_proxyaddr], options[:http_proxyport], options[:http_proxyuser], options[:http_proxypass])
101
+ port = uri.port || (uri.scheme == 'https' ? 443 : 80)
102
+ if options.key?(:http_proxyaddr)
103
+ http = Net::HTTP.new(
104
+ host,
105
+ port,
106
+ options[:http_proxyaddr],
107
+ options[:http_proxyport],
108
+ options[:http_proxyuser],
109
+ options[:http_proxypass]
110
+ )
71
111
  else
72
- http = Net::HTTP.new(host, uri.port)
112
+ http = Net::HTTP.new(host, port)
73
113
  end
74
114
 
75
115
  http.use_ssl = ssl_implied?(uri)
76
116
 
77
117
  attach_ssl_certificates(http, options)
78
118
 
79
- if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
119
+ if add_timeout?(options[:timeout])
80
120
  http.open_timeout = options[:timeout]
81
121
  http.read_timeout = options[:timeout]
122
+
123
+ from_ruby_version('2.6.0', option: :write_timeout, warn: false) do
124
+ http.write_timeout = options[:timeout]
125
+ end
126
+ end
127
+
128
+ if add_timeout?(options[:read_timeout])
129
+ http.read_timeout = options[:read_timeout]
130
+ end
131
+
132
+ if add_timeout?(options[:open_timeout])
133
+ http.open_timeout = options[:open_timeout]
134
+ end
135
+
136
+ if add_timeout?(options[:write_timeout])
137
+ from_ruby_version('2.6.0', option: :write_timeout) do
138
+ http.write_timeout = options[:write_timeout]
139
+ end
140
+ end
141
+
142
+ if add_max_retries?(options[:max_retries])
143
+ from_ruby_version('2.5.0', option: :max_retries) do
144
+ http.max_retries = options[:max_retries]
145
+ end
82
146
  end
83
147
 
84
148
  if options[:debug_output]
@@ -93,26 +157,38 @@ module HTTParty
93
157
  #
94
158
  # @see https://bugs.ruby-lang.org/issues/6617
95
159
  if options[:local_host]
96
- if RUBY_VERSION >= "2.0.0"
160
+ from_ruby_version('2.0.0', option: :local_host) do
97
161
  http.local_host = options[:local_host]
98
- else
99
- Kernel.warn("Warning: option :local_host requires Ruby version 2.0 or later")
100
162
  end
101
163
  end
102
164
 
103
165
  if options[:local_port]
104
- if RUBY_VERSION >= "2.0.0"
166
+ from_ruby_version('2.0.0', option: :local_port) do
105
167
  http.local_port = options[:local_port]
106
- else
107
- Kernel.warn("Warning: option :local_port requires Ruby version 2.0 or later")
108
168
  end
109
169
  end
110
170
 
111
- return http
171
+ http
112
172
  end
113
173
 
114
174
  private
115
175
 
176
+ def from_ruby_version(ruby_version, option: nil, warn: true)
177
+ if RUBY_VERSION >= ruby_version
178
+ yield
179
+ elsif warn
180
+ Kernel.warn("Warning: option #{ option } requires Ruby version #{ ruby_version } or later")
181
+ end
182
+ end
183
+
184
+ def add_timeout?(timeout)
185
+ timeout && (timeout.is_a?(Integer) || timeout.is_a?(Float))
186
+ end
187
+
188
+ def add_max_retries?(max_retries)
189
+ max_retries && max_retries.is_a?(Integer) && max_retries >= 0
190
+ end
191
+
116
192
  def clean_host(host)
117
193
  strip_ipv6_brackets(host)
118
194
  end
@@ -122,7 +198,11 @@ module HTTParty
122
198
  end
123
199
 
124
200
  def ssl_implied?(uri)
125
- uri.port == 443 || uri.instance_of?(URI::HTTPS)
201
+ uri.port == 443 || uri.scheme == 'https'
202
+ end
203
+
204
+ def verify_ssl_certificate?
205
+ !(options[:verify] == false || options[:verify_peer] == false)
126
206
  end
127
207
 
128
208
  def attach_ssl_certificates(http, options)
@@ -133,18 +213,18 @@ module HTTParty
133
213
  http.cert_store = options[:cert_store]
134
214
  else
135
215
  # Use the default cert store by default, i.e. system ca certs
136
- http.cert_store = OpenSSL::X509::Store.new
137
- http.cert_store.set_default_paths
216
+ http.cert_store = self.class.default_cert_store
138
217
  end
139
218
  else
140
219
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
141
220
  end
142
221
 
143
222
  # Client certificate authentication
223
+ # Note: options[:pem] must contain the content of a PEM file having the private key appended
144
224
  if options[:pem]
145
225
  http.cert = OpenSSL::X509::Certificate.new(options[:pem])
146
- http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
147
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
226
+ http.key = OpenSSL::PKey.read(options[:pem], options[:pem_password])
227
+ http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
148
228
  end
149
229
 
150
230
  # PKCS12 client certificate authentication
@@ -152,7 +232,7 @@ module HTTParty
152
232
  p12 = OpenSSL::PKCS12.new(options[:p12], options[:p12_password])
153
233
  http.cert = p12.certificate
154
234
  http.key = p12.key
155
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
235
+ http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
156
236
  end
157
237
 
158
238
  # SSL certificate authority file and/or directory
@@ -1,15 +1,16 @@
1
- class HTTParty::CookieHash < Hash #:nodoc:
1
+ # frozen_string_literal: true
2
2
 
3
- CLIENT_COOKIES = %w{path expires domain path secure HTTPOnly}
3
+ class HTTParty::CookieHash < Hash #:nodoc:
4
+ CLIENT_COOKIES = %w(path expires domain path secure httponly samesite)
4
5
 
5
- def add_cookies(value)
6
- case value
6
+ def add_cookies(data)
7
+ case data
7
8
  when Hash
8
- merge!(value)
9
+ merge!(data)
9
10
  when String
10
- value.split('; ').each do |cookie|
11
- array = cookie.split('=',2)
12
- self[array[0].to_sym] = array[1]
11
+ data.split('; ').each do |cookie|
12
+ key, value = cookie.split('=', 2)
13
+ self[key.to_sym] = value if key
13
14
  end
14
15
  else
15
16
  raise "add_cookies only takes a Hash or a String"
@@ -17,6 +18,6 @@ class HTTParty::CookieHash < Hash #:nodoc:
17
18
  end
18
19
 
19
20
  def to_cookie_string
20
- delete_if { |k, v| CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join("; ")
21
+ select { |k, v| !CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join('; ')
21
22
  end
22
23
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ # Decompresses the response body based on the Content-Encoding header.
5
+ #
6
+ # Net::HTTP automatically decompresses Content-Encoding values "gzip" and "deflate".
7
+ # This class will handle "br" (Brotli) and "compress" (LZW) if the requisite
8
+ # gems are installed. Otherwise, it returns nil if the body data cannot be
9
+ # decompressed.
10
+ #
11
+ # @abstract Read the HTTP Compression section for more information.
12
+ class Decompressor
13
+
14
+ # "gzip" and "deflate" are handled by Net::HTTP
15
+ # hence they do not need to be handled by HTTParty
16
+ SupportedEncodings = {
17
+ 'none' => :none,
18
+ 'identity' => :none,
19
+ 'br' => :brotli,
20
+ 'compress' => :lzw,
21
+ 'zstd' => :zstd
22
+ }.freeze
23
+
24
+ # The response body of the request
25
+ # @return [String]
26
+ attr_reader :body
27
+
28
+ # The Content-Encoding algorithm used to encode the body
29
+ # @return [Symbol] e.g. :gzip
30
+ attr_reader :encoding
31
+
32
+ # @param [String] body - the response body of the request
33
+ # @param [Symbol] encoding - the Content-Encoding algorithm used to encode the body
34
+ def initialize(body, encoding)
35
+ @body = body
36
+ @encoding = encoding
37
+ end
38
+
39
+ # Perform decompression on the response body
40
+ # @return [String] the decompressed body
41
+ # @return [nil] when the response body is nil or cannot decompressed
42
+ def decompress
43
+ return nil if body.nil?
44
+ return body if encoding.nil? || encoding.strip.empty?
45
+
46
+ if supports_encoding?
47
+ decompress_supported_encoding
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def supports_encoding?
56
+ SupportedEncodings.keys.include?(encoding)
57
+ end
58
+
59
+ def decompress_supported_encoding
60
+ method = SupportedEncodings[encoding]
61
+ if respond_to?(method, true)
62
+ send(method)
63
+ else
64
+ raise NotImplementedError, "#{self.class.name} has not implemented a decompression method for #{encoding.inspect} encoding."
65
+ end
66
+ end
67
+
68
+ def none
69
+ body
70
+ end
71
+
72
+ def brotli
73
+ return nil unless defined?(::Brotli)
74
+ begin
75
+ ::Brotli.inflate(body)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+ end
80
+
81
+ def lzw
82
+ begin
83
+ if defined?(::LZWS::String)
84
+ ::LZWS::String.decompress(body)
85
+ elsif defined?(::LZW::Simple)
86
+ ::LZW::Simple.new.decompress(body)
87
+ end
88
+ rescue StandardError
89
+ nil
90
+ end
91
+ end
92
+
93
+ def zstd
94
+ return nil unless defined?(::Zstd)
95
+ begin
96
+ ::Zstd.decompress(body)
97
+ rescue StandardError
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
- # @abstact Exceptions raised by HTTParty inherit from Error
4
+ # @abstract Exceptions raised by HTTParty inherit from Error
3
5
  class Error < StandardError; end
4
6
 
5
- # Exception raised when you attempt to set a non-existant format
7
+ # Exception raised when you attempt to set a non-existent format
6
8
  class UnsupportedFormat < Error; end
7
9
 
8
10
  # Exception raised when using a URI scheme other than HTTP or HTTPS
@@ -20,10 +22,14 @@ module HTTParty
20
22
  # @param [Net::HTTPResponse]
21
23
  def initialize(response)
22
24
  @response = response
25
+ super(response)
23
26
  end
24
27
  end
25
28
 
26
29
  # Exception that is raised when request has redirected too many times.
27
30
  # Calling {#response} returns the Net:HTTP response object.
28
31
  class RedirectionTooDeep < ResponseError; end
32
+
33
+ # Exception that is raised when request redirects and location header is present more than once
34
+ class DuplicateLocationHeader < ResponseError; end
29
35
  end
@@ -1,20 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
1
5
  module HTTParty
2
6
  module HashConversions
3
7
  # @return <String> This hash as a query string
4
8
  #
5
9
  # @example
6
- # { :name => "Bob",
7
- # :address => {
8
- # :street => '111 Ruby Ave.',
9
- # :city => 'Ruby Central',
10
- # :phones => ['111-111-1111', '222-222-2222']
10
+ # { name: "Bob",
11
+ # address: {
12
+ # street: '111 Ruby Ave.',
13
+ # city: 'Ruby Central',
14
+ # phones: ['111-111-1111', '222-222-2222']
11
15
  # }
12
16
  # }.to_params
13
17
  # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
14
18
  def self.to_params(hash)
15
- params = hash.map { |k,v| normalize_param(k,v) }.join
16
- params.chop! # trailing &
17
- params
19
+ hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop
18
20
  end
19
21
 
20
22
  # @param key<Object> The key for the param.
@@ -24,28 +26,46 @@ module HTTParty
24
26
  #
25
27
  # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
26
28
  def self.normalize_param(key, value)
27
- param = ''
29
+ normalized_keys = normalize_keys(key, value)
30
+
31
+ normalized_keys.flatten.each_slice(2).inject(''.dup) do |string, (k, v)|
32
+ string << "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v.to_s)}&"
33
+ end
34
+ end
35
+
36
+ def self.normalize_keys(key, value)
28
37
  stack = []
38
+ normalized_keys = []
29
39
 
30
- if value.is_a?(Array)
31
- param << value.map { |element| normalize_param("#{key}[]", element) }.join
32
- elsif value.is_a?(Hash)
33
- stack << [key,value]
40
+ if value.respond_to?(:to_ary)
41
+ if value.empty?
42
+ normalized_keys << ["#{key}[]", '']
43
+ else
44
+ normalized_keys = value.to_ary.flat_map do |element|
45
+ normalize_keys("#{key}[]", element)
46
+ end
47
+ end
48
+ elsif value.respond_to?(:to_hash)
49
+ stack << [key, value.to_hash]
34
50
  else
35
- param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
51
+ normalized_keys << [key.to_s, value]
36
52
  end
37
53
 
38
54
  stack.each do |parent, hash|
39
- hash.each do |k, v|
40
- if v.is_a?(Hash)
41
- stack << ["#{parent}[#{k}]", v]
55
+ hash.each do |child_key, child_value|
56
+ if child_value.respond_to?(:to_hash)
57
+ stack << ["#{parent}[#{child_key}]", child_value.to_hash]
58
+ elsif child_value.respond_to?(:to_ary)
59
+ child_value.to_ary.each do |v|
60
+ normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten
61
+ end
42
62
  else
43
- param << normalize_param("#{parent}[#{k}]", v)
63
+ normalized_keys << normalize_keys("#{parent}[#{child_key}]", child_value).flatten
44
64
  end
45
65
  end
46
66
  end
47
67
 
48
- param
68
+ normalized_keys
49
69
  end
50
70
  end
51
71
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ class HeadersProcessor
5
+ attr_reader :headers, :options
6
+
7
+ def initialize(headers, options)
8
+ @headers = headers
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ return unless options[:headers]
14
+
15
+ options[:headers] = headers.merge(options[:headers]) if headers.any?
16
+ options[:headers] = Utils.stringify_keys(process_dynamic_headers)
17
+ end
18
+
19
+ private
20
+
21
+ def process_dynamic_headers
22
+ options[:headers].each_with_object({}) do |header, processed_headers|
23
+ key, value = header
24
+ processed_headers[key] = if value.respond_to?(:call)
25
+ value.arity == 0 ? value.call : value.call(options)
26
+ else
27
+ value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ module Logger
5
+ class ApacheFormatter #:nodoc:
6
+ TAG_NAME = HTTParty.name
7
+
8
+ attr_accessor :level, :logger
9
+
10
+ def initialize(logger, level)
11
+ @logger = logger
12
+ @level = level.to_sym
13
+ end
14
+
15
+ def format(request, response)
16
+ @request = request
17
+ @response = response
18
+
19
+ logger.public_send level, message
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :request, :response
25
+
26
+ def message
27
+ "[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
28
+ end
29
+
30
+ def current_time
31
+ Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
32
+ end
33
+
34
+ def http_method
35
+ request.http_method.name.split('::').last.upcase
36
+ end
37
+
38
+ def path
39
+ request.path.to_s
40
+ end
41
+
42
+ def content_length
43
+ response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ module Logger
5
+ class CurlFormatter #:nodoc:
6
+ TAG_NAME = HTTParty.name
7
+ OUT = '>'
8
+ IN = '<'
9
+
10
+ attr_accessor :level, :logger
11
+
12
+ def initialize(logger, level)
13
+ @logger = logger
14
+ @level = level.to_sym
15
+ @messages = []
16
+ end
17
+
18
+ def format(request, response)
19
+ @request = request
20
+ @response = response
21
+
22
+ log_request
23
+ log_response
24
+
25
+ logger.public_send level, messages.join("\n")
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :request, :response
31
+ attr_accessor :messages
32
+
33
+ def log_request
34
+ log_url
35
+ log_headers
36
+ log_query
37
+ log OUT, request.raw_body if request.raw_body
38
+ log OUT
39
+ end
40
+
41
+ def log_response
42
+ log IN, "HTTP/#{response.http_version} #{response.code}"
43
+ log_response_headers
44
+ log IN, "\n#{response.body}"
45
+ log IN
46
+ end
47
+
48
+ def log_url
49
+ http_method = request.http_method.name.split('::').last.upcase
50
+ uri = if request.options[:base_uri]
51
+ request.options[:base_uri] + request.path.path
52
+ else
53
+ request.path.to_s
54
+ end
55
+
56
+ log OUT, "#{http_method} #{uri}"
57
+ end
58
+
59
+ def log_headers
60
+ return unless request.options[:headers] && request.options[:headers].size > 0
61
+
62
+ log OUT, 'Headers: '
63
+ log_hash request.options[:headers]
64
+ end
65
+
66
+ def log_query
67
+ return unless request.options[:query]
68
+
69
+ log OUT, 'Query: '
70
+ log_hash request.options[:query]
71
+ end
72
+
73
+ def log_response_headers
74
+ headers = response.respond_to?(:headers) ? response.headers : response
75
+ response.each_header do |response_header|
76
+ log IN, "#{response_header.capitalize}: #{headers[response_header]}"
77
+ end
78
+ end
79
+
80
+ def log_hash(hash)
81
+ hash.each { |k, v| log(OUT, "#{k}: #{v}") }
82
+ end
83
+
84
+ def log(direction, line = '')
85
+ messages << "[#{TAG_NAME}] [#{current_time}] #{direction} #{line}"
86
+ end
87
+
88
+ def current_time
89
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,18 +1,30 @@
1
- require 'httparty/logger/apache_logger'
2
- require 'httparty/logger/curl_logger'
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty/logger/apache_formatter'
4
+ require 'httparty/logger/curl_formatter'
5
+ require 'httparty/logger/logstash_formatter'
3
6
 
4
7
  module HTTParty
5
8
  module Logger
9
+ def self.formatters
10
+ @formatters ||= {
11
+ :curl => Logger::CurlFormatter,
12
+ :apache => Logger::ApacheFormatter,
13
+ :logstash => Logger::LogstashFormatter,
14
+ }
15
+ end
16
+
17
+ def self.add_formatter(name, formatter)
18
+ raise HTTParty::Error.new("Log Formatter with name #{name} already exists") if formatters.include?(name)
19
+ formatters.merge!(name.to_sym => formatter)
20
+ end
21
+
6
22
  def self.build(logger, level, formatter)
7
- level ||= :info
8
- format ||= :apache
23
+ level ||= :info
24
+ formatter ||= :apache
9
25
 
10
- case formatter
11
- when :curl
12
- Logger::CurlLogger.new(logger, level)
13
- else
14
- Logger::ApacheLogger.new(logger, level)
15
- end
26
+ logger_klass = formatters[formatter] || Logger::ApacheFormatter
27
+ logger_klass.new(logger, level)
16
28
  end
17
29
  end
18
30
  end