httparty 0.15.0 → 0.21.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.
Files changed (96) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop_todo.yml +1 -1
  6. data/{History.md → Changelog.md} +137 -0
  7. data/Gemfile +8 -1
  8. data/Guardfile +3 -2
  9. data/README.md +6 -6
  10. data/docs/README.md +120 -38
  11. data/examples/README.md +28 -11
  12. data/examples/aaws.rb +6 -2
  13. data/examples/body_stream.rb +14 -0
  14. data/examples/custom_parsers.rb +4 -0
  15. data/examples/headers_and_user_agents.rb +7 -3
  16. data/examples/idn.rb +10 -0
  17. data/examples/logging.rb +3 -3
  18. data/examples/microsoft_graph.rb +52 -0
  19. data/examples/multipart.rb +22 -0
  20. data/examples/peer_cert.rb +9 -0
  21. data/examples/stream_download.rb +8 -2
  22. data/httparty.gemspec +7 -4
  23. data/lib/httparty/connection_adapter.rb +59 -16
  24. data/lib/httparty/cookie_hash.rb +10 -8
  25. data/lib/httparty/decompressor.rb +102 -0
  26. data/lib/httparty/exceptions.rb +4 -1
  27. data/lib/httparty/hash_conversions.rb +28 -12
  28. data/lib/httparty/headers_processor.rb +32 -0
  29. data/lib/httparty/logger/apache_formatter.rb +31 -6
  30. data/lib/httparty/logger/curl_formatter.rb +9 -7
  31. data/lib/httparty/logger/logger.rb +5 -1
  32. data/lib/httparty/logger/logstash_formatter.rb +61 -0
  33. data/lib/httparty/module_inheritable_attributes.rb +6 -4
  34. data/lib/httparty/net_digest_auth.rb +15 -15
  35. data/lib/httparty/parser.rb +25 -16
  36. data/lib/httparty/request/body.rb +105 -0
  37. data/lib/httparty/request/multipart_boundary.rb +13 -0
  38. data/lib/httparty/request.rb +96 -105
  39. data/lib/httparty/response/headers.rb +6 -2
  40. data/lib/httparty/response.rb +59 -8
  41. data/lib/httparty/response_fragment.rb +21 -0
  42. data/lib/httparty/text_encoder.rb +72 -0
  43. data/lib/httparty/utils.rb +13 -0
  44. data/lib/httparty/version.rb +3 -1
  45. data/lib/httparty.rb +70 -25
  46. data/website/css/common.css +1 -1
  47. metadata +38 -106
  48. data/.simplecov +0 -1
  49. data/.travis.yml +0 -8
  50. data/features/basic_authentication.feature +0 -20
  51. data/features/command_line.feature +0 -95
  52. data/features/deals_with_http_error_codes.feature +0 -26
  53. data/features/digest_authentication.feature +0 -30
  54. data/features/handles_compressed_responses.feature +0 -27
  55. data/features/handles_multiple_formats.feature +0 -57
  56. data/features/steps/env.rb +0 -27
  57. data/features/steps/httparty_response_steps.rb +0 -56
  58. data/features/steps/httparty_steps.rb +0 -43
  59. data/features/steps/mongrel_helper.rb +0 -127
  60. data/features/steps/remote_service_steps.rb +0 -92
  61. data/features/supports_read_timeout_option.feature +0 -13
  62. data/features/supports_redirection.feature +0 -22
  63. data/features/supports_timeout_option.feature +0 -13
  64. data/spec/fixtures/delicious.xml +0 -23
  65. data/spec/fixtures/empty.xml +0 -0
  66. data/spec/fixtures/google.html +0 -3
  67. data/spec/fixtures/ssl/generate.sh +0 -29
  68. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -1
  69. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  70. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  71. data/spec/fixtures/ssl/generated/ca.key +0 -15
  72. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  73. data/spec/fixtures/ssl/generated/server.crt +0 -13
  74. data/spec/fixtures/ssl/generated/server.key +0 -15
  75. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  76. data/spec/fixtures/twitter.csv +0 -2
  77. data/spec/fixtures/twitter.json +0 -1
  78. data/spec/fixtures/twitter.xml +0 -403
  79. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  80. data/spec/httparty/connection_adapter_spec.rb +0 -495
  81. data/spec/httparty/cookie_hash_spec.rb +0 -100
  82. data/spec/httparty/exception_spec.rb +0 -45
  83. data/spec/httparty/hash_conversions_spec.rb +0 -49
  84. data/spec/httparty/logger/apache_formatter_spec.rb +0 -41
  85. data/spec/httparty/logger/curl_formatter_spec.rb +0 -119
  86. data/spec/httparty/logger/logger_spec.rb +0 -38
  87. data/spec/httparty/net_digest_auth_spec.rb +0 -268
  88. data/spec/httparty/parser_spec.rb +0 -178
  89. data/spec/httparty/request_spec.rb +0 -1244
  90. data/spec/httparty/response_spec.rb +0 -347
  91. data/spec/httparty/ssl_spec.rb +0 -74
  92. data/spec/httparty_spec.rb +0 -877
  93. data/spec/spec_helper.rb +0 -59
  94. data/spec/support/ssl_test_helper.rb +0 -47
  95. data/spec/support/ssl_test_server.rb +0 -80
  96. data/spec/support/stub_response.rb +0 -49
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'httparty/logger/apache_formatter'
2
4
  require 'httparty/logger/curl_formatter'
5
+ require 'httparty/logger/logstash_formatter'
3
6
 
4
7
  module HTTParty
5
8
  module Logger
6
9
  def self.formatters
7
10
  @formatters ||= {
8
11
  :curl => Logger::CurlFormatter,
9
- :apache => Logger::ApacheFormatter
12
+ :apache => Logger::ApacheFormatter,
13
+ :logstash => Logger::LogstashFormatter,
10
14
  }
11
15
  end
12
16
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ module Logger
5
+ class LogstashFormatter #: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, logstash_message
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :request, :response
25
+
26
+ def logstash_message
27
+ {
28
+ '@timestamp' => current_time,
29
+ '@version' => 1,
30
+ 'content_length' => content_length || '-',
31
+ 'http_method' => http_method,
32
+ 'message' => message,
33
+ 'path' => path,
34
+ 'response_code' => response.code,
35
+ 'severity' => level,
36
+ 'tags' => [TAG_NAME],
37
+ }.to_json
38
+ end
39
+
40
+ def message
41
+ "[#{TAG_NAME}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
42
+ end
43
+
44
+ def current_time
45
+ Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
46
+ end
47
+
48
+ def http_method
49
+ @http_method ||= request.http_method.name.split('::').last.upcase
50
+ end
51
+
52
+ def path
53
+ @path ||= request.path.to_s
54
+ end
55
+
56
+ def content_length
57
+ @content_length ||= response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
58
+ end
59
+ end
60
+ end
61
+ end
@@ -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)
@@ -9,12 +11,12 @@ module HTTParty
9
11
  duplicate = hash.dup
10
12
 
11
13
  duplicate.each_pair do |key, value|
12
- duplicate[key] = if value.is_a?(Hash)
13
- hash_deep_dup(value)
14
+ if value.is_a?(Hash)
15
+ duplicate[key] = hash_deep_dup(value)
14
16
  elsif value.is_a?(Proc)
15
17
  duplicate[key] = value.dup
16
18
  else
17
- value
19
+ duplicate[key] = value
18
20
  end
19
21
  end
20
22
 
@@ -36,7 +38,7 @@ 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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
2
4
  require 'net/http'
3
5
 
@@ -14,11 +16,11 @@ module Net
14
16
 
15
17
  authenticator.authorization_header.each do |v|
16
18
  add_field('Authorization', v)
17
- end
19
+ end
18
20
 
19
21
  authenticator.cookie_header.each do |v|
20
22
  add_field('Cookie', v)
21
- end
23
+ end
22
24
  end
23
25
 
24
26
  class DigestAuthenticator
@@ -44,12 +46,9 @@ module Net
44
46
  header << %(algorithm="#{@response['algorithm']}") if algorithm_present?
45
47
 
46
48
  if qop_present?
47
- fields = [
48
- %(cnonce="#{@cnonce}"),
49
- %(qop="#{@response['qop']}"),
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?
@@ -64,7 +63,8 @@ module Net
64
63
 
65
64
  def parse(response_header)
66
65
  header = response_header['www-authenticate']
67
- .gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
66
+
67
+ header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
68
68
 
69
69
  header =~ /Digest (.*)/
70
70
  params = {}
@@ -97,13 +97,13 @@ module Net
97
97
  end
98
98
 
99
99
  def random
100
- format "%x", (Time.now.to_i + rand(65535))
100
+ format '%x', (Time.now.to_i + rand(65535))
101
101
  end
102
102
 
103
103
  def request_digest
104
104
  a = [md5(a1), @response['nonce'], md5(a2)]
105
- a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
106
- md5(a.join(":"))
105
+ a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
106
+ md5(a.join(':'))
107
107
  end
108
108
 
109
109
  def md5(str)
@@ -113,11 +113,11 @@ module Net
113
113
  def algorithm_present?
114
114
  @response.key?('algorithm') && !@response['algorithm'].empty?
115
115
  end
116
-
116
+
117
117
  def use_md5_sess?
118
118
  algorithm_present? && @response['algorithm'] == 'MD5-sess'
119
119
  end
120
-
120
+
121
121
  def a1
122
122
  a1_user_realm_pwd = [@username, @response['realm'], @password].join(':')
123
123
  if use_md5_sess?
@@ -128,7 +128,7 @@ module Net
128
128
  end
129
129
 
130
130
  def a2
131
- [@method, @path].join(":")
131
+ [@method, @path].join(':')
132
132
  end
133
133
  end
134
134
  end
@@ -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.
@@ -38,16 +40,18 @@ module HTTParty
38
40
  # @abstract Read the Custom Parsers section for more information.
39
41
  class Parser
40
42
  SupportedFormats = {
41
- 'text/xml' => :xml,
42
- 'application/xml' => :xml,
43
- 'application/json' => :json,
44
- 'text/json' => :json,
45
- 'application/javascript' => :plain,
46
- 'text/javascript' => :plain,
47
- 'text/html' => :html,
48
- 'text/plain' => :plain,
49
- 'text/csv' => :csv,
50
- 'application/csv' => :csv,
43
+ 'text/xml' => :xml,
44
+ 'application/xml' => :xml,
45
+ 'application/json' => :json,
46
+ 'application/vnd.api+json' => :json,
47
+ 'application/hal+json' => :json,
48
+ 'text/json' => :json,
49
+ 'application/javascript' => :plain,
50
+ 'text/javascript' => :plain,
51
+ 'text/html' => :html,
52
+ 'text/plain' => :plain,
53
+ 'text/csv' => :csv,
54
+ 'application/csv' => :csv,
51
55
  'text/comma-separated-values' => :csv
52
56
  }
53
57
 
@@ -99,8 +103,11 @@ module HTTParty
99
103
  # @return [nil] when the response body is nil, an empty string, spaces only or "null"
100
104
  def parse
101
105
  return nil if body.nil?
102
- return nil if body == "null"
106
+ return nil if body == 'null'
103
107
  return nil if body.valid_encoding? && body.strip.empty?
108
+ if body.valid_encoding? && body.encoding == Encoding::UTF_8
109
+ @body = body.gsub(/\A#{UTF8_BOM}/, '')
110
+ end
104
111
  if supports_format?
105
112
  parse_supported_format
106
113
  else
@@ -114,10 +121,10 @@ module HTTParty
114
121
  MultiXml.parse(body)
115
122
  end
116
123
 
117
- UTF8_BOM = "\xEF\xBB\xBF".freeze
124
+ UTF8_BOM = "\xEF\xBB\xBF"
118
125
 
119
126
  def json
120
- JSON.parse(body.gsub(/\A#{UTF8_BOM}/, ''), :quirks_mode => true, :allow_nan => true)
127
+ JSON.parse(body, :quirks_mode => true, :allow_nan => true)
121
128
  end
122
129
 
123
130
  def csv
@@ -137,9 +144,11 @@ module HTTParty
137
144
  end
138
145
 
139
146
  def parse_supported_format
140
- send(format)
141
- rescue NoMethodError => e
142
- raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
147
+ if respond_to?(format, true)
148
+ send(format)
149
+ else
150
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
151
+ end
143
152
  end
144
153
  end
145
154
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multipart_boundary'
4
+
5
+ module HTTParty
6
+ class Request
7
+ class Body
8
+ NEWLINE = "\r\n"
9
+ private_constant :NEWLINE
10
+
11
+ def initialize(params, query_string_normalizer: nil, force_multipart: false)
12
+ @params = params
13
+ @query_string_normalizer = query_string_normalizer
14
+ @force_multipart = force_multipart
15
+ end
16
+
17
+ def call
18
+ if params.respond_to?(:to_hash)
19
+ multipart? ? generate_multipart : normalize_query(params)
20
+ else
21
+ params
22
+ end
23
+ end
24
+
25
+ def boundary
26
+ @boundary ||= MultipartBoundary.generate
27
+ end
28
+
29
+ def multipart?
30
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
31
+ end
32
+
33
+ private
34
+
35
+ # https://html.spec.whatwg.org/#multipart-form-data
36
+ MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
37
+ '"' => '%22',
38
+ "\r" => '%0D',
39
+ "\n" => '%0A'
40
+ }.freeze
41
+
42
+ def generate_multipart
43
+ normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
44
+
45
+ multipart = normalized_params.inject(''.dup) do |memo, (key, value)|
46
+ memo << "--#{boundary}#{NEWLINE}"
47
+ memo << %(Content-Disposition: form-data; name="#{key}")
48
+ # value.path is used to support ActionDispatch::Http::UploadedFile
49
+ # https://github.com/jnunemaker/httparty/pull/585
50
+ memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}") if file?(value)
51
+ memo << NEWLINE
52
+ memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value)
53
+ memo << NEWLINE
54
+ memo << content_body(value)
55
+ memo << NEWLINE
56
+ end
57
+
58
+ multipart << "--#{boundary}--#{NEWLINE}"
59
+ end
60
+
61
+ def has_file?(value)
62
+ if value.respond_to?(:to_hash)
63
+ value.to_hash.any? { |_, v| has_file?(v) }
64
+ elsif value.respond_to?(:to_ary)
65
+ value.to_ary.any? { |v| has_file?(v) }
66
+ else
67
+ file?(value)
68
+ end
69
+ end
70
+
71
+ def file?(object)
72
+ object.respond_to?(:path) && object.respond_to?(:read)
73
+ end
74
+
75
+ def normalize_query(query)
76
+ if query_string_normalizer
77
+ query_string_normalizer.call(query)
78
+ else
79
+ HashConversions.to_params(query)
80
+ end
81
+ end
82
+
83
+ def content_body(object)
84
+ if file?(object)
85
+ object = (file = object).read
86
+ file.rewind if file.respond_to?(:rewind)
87
+ end
88
+
89
+ object.to_s
90
+ end
91
+
92
+ def content_type(object)
93
+ return object.content_type if object.respond_to?(:content_type)
94
+ mime = MiniMime.lookup_by_filename(object.path)
95
+ mime ? mime.content_type : 'application/octet-stream'
96
+ end
97
+
98
+ def file_name(object)
99
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
100
+ end
101
+
102
+ attr_reader :params, :query_string_normalizer, :force_multipart
103
+ end
104
+ end
105
+ 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