httparty-responsibly 0.17.0.r1

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +18 -0
  3. data/.gitignore +13 -0
  4. data/.rubocop.yml +92 -0
  5. data/.rubocop_todo.yml +124 -0
  6. data/.simplecov +1 -0
  7. data/.travis.yml +11 -0
  8. data/CONTRIBUTING.md +23 -0
  9. data/Changelog.md +502 -0
  10. data/Gemfile +23 -0
  11. data/Guardfile +16 -0
  12. data/MIT-LICENSE +20 -0
  13. data/README.md +78 -0
  14. data/Rakefile +10 -0
  15. data/bin/httparty +123 -0
  16. data/cucumber.yml +1 -0
  17. data/docs/README.md +106 -0
  18. data/examples/README.md +86 -0
  19. data/examples/aaws.rb +32 -0
  20. data/examples/basic.rb +28 -0
  21. data/examples/body_stream.rb +14 -0
  22. data/examples/crack.rb +19 -0
  23. data/examples/custom_parsers.rb +68 -0
  24. data/examples/delicious.rb +37 -0
  25. data/examples/google.rb +16 -0
  26. data/examples/headers_and_user_agents.rb +10 -0
  27. data/examples/logging.rb +36 -0
  28. data/examples/microsoft_graph.rb +52 -0
  29. data/examples/multipart.rb +22 -0
  30. data/examples/nokogiri_html_parser.rb +19 -0
  31. data/examples/peer_cert.rb +9 -0
  32. data/examples/rescue_json.rb +17 -0
  33. data/examples/rubyurl.rb +14 -0
  34. data/examples/stackexchange.rb +24 -0
  35. data/examples/stream_download.rb +26 -0
  36. data/examples/tripit_sign_in.rb +44 -0
  37. data/examples/twitter.rb +31 -0
  38. data/examples/whoismyrep.rb +10 -0
  39. data/httparty-responsibly.gemspec +27 -0
  40. data/lib/httparty.rb +684 -0
  41. data/lib/httparty/connection_adapter.rb +244 -0
  42. data/lib/httparty/cookie_hash.rb +21 -0
  43. data/lib/httparty/exceptions.rb +33 -0
  44. data/lib/httparty/hash_conversions.rb +69 -0
  45. data/lib/httparty/logger/apache_formatter.rb +45 -0
  46. data/lib/httparty/logger/curl_formatter.rb +91 -0
  47. data/lib/httparty/logger/logger.rb +28 -0
  48. data/lib/httparty/logger/logstash_formatter.rb +59 -0
  49. data/lib/httparty/module_inheritable_attributes.rb +56 -0
  50. data/lib/httparty/net_digest_auth.rb +136 -0
  51. data/lib/httparty/parser.rb +150 -0
  52. data/lib/httparty/request.rb +386 -0
  53. data/lib/httparty/request/body.rb +84 -0
  54. data/lib/httparty/request/multipart_boundary.rb +11 -0
  55. data/lib/httparty/response.rb +140 -0
  56. data/lib/httparty/response/headers.rb +33 -0
  57. data/lib/httparty/response_fragment.rb +19 -0
  58. data/lib/httparty/text_encoder.rb +70 -0
  59. data/lib/httparty/utils.rb +11 -0
  60. data/lib/httparty/version.rb +3 -0
  61. data/script/release +42 -0
  62. data/website/css/common.css +47 -0
  63. data/website/index.html +73 -0
  64. metadata +138 -0
@@ -0,0 +1,28 @@
1
+ require 'httparty/logger/apache_formatter'
2
+ require 'httparty/logger/curl_formatter'
3
+ require 'httparty/logger/logstash_formatter'
4
+
5
+ module HTTParty
6
+ module Logger
7
+ def self.formatters
8
+ @formatters ||= {
9
+ :curl => Logger::CurlFormatter,
10
+ :apache => Logger::ApacheFormatter,
11
+ :logstash => Logger::LogstashFormatter,
12
+ }
13
+ end
14
+
15
+ def self.add_formatter(name, formatter)
16
+ raise HTTParty::Error.new("Log Formatter with name #{name} already exists") if formatters.include?(name)
17
+ formatters.merge!(name.to_sym => formatter)
18
+ end
19
+
20
+ def self.build(logger, level, formatter)
21
+ level ||= :info
22
+ formatter ||= :apache
23
+
24
+ logger_klass = formatters[formatter] || Logger::ApacheFormatter
25
+ logger_klass.new(logger, level)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ module HTTParty
2
+ module Logger
3
+ class LogstashFormatter #:nodoc:
4
+ TAG_NAME = HTTParty.name
5
+
6
+ attr_accessor :level, :logger
7
+
8
+ def initialize(logger, level)
9
+ @logger = logger
10
+ @level = level.to_sym
11
+ end
12
+
13
+ def format(request, response)
14
+ @request = request
15
+ @response = response
16
+
17
+ logger.public_send level, logstash_message
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :request, :response
23
+
24
+ def logstash_message
25
+ {
26
+ '@timestamp' => current_time,
27
+ '@version' => 1,
28
+ 'content_length' => content_length || '-',
29
+ 'http_method' => http_method,
30
+ 'message' => message,
31
+ 'path' => path,
32
+ 'response_code' => response.code,
33
+ 'severity' => level,
34
+ 'tags' => [TAG_NAME],
35
+ }.to_json
36
+ end
37
+
38
+ def message
39
+ "[#{TAG_NAME}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
40
+ end
41
+
42
+ def current_time
43
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
44
+ end
45
+
46
+ def http_method
47
+ @http_method ||= request.http_method.name.split("::").last.upcase
48
+ end
49
+
50
+ def path
51
+ @path ||= request.path.to_s
52
+ end
53
+
54
+ def content_length
55
+ @content_length ||= response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
1
+ module HTTParty
2
+ module ModuleInheritableAttributes #:nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # borrowed from Rails 3.2 ActiveSupport
8
+ def self.hash_deep_dup(hash)
9
+ duplicate = hash.dup
10
+
11
+ duplicate.each_pair do |key, value|
12
+ if value.is_a?(Hash)
13
+ duplicate[key] = hash_deep_dup(value)
14
+ elsif value.is_a?(Proc)
15
+ duplicate[key] = value.dup
16
+ else
17
+ duplicate[key] = value
18
+ end
19
+ end
20
+
21
+ duplicate
22
+ end
23
+
24
+ module ClassMethods #:nodoc:
25
+ def mattr_inheritable(*args)
26
+ @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
27
+ @mattr_inheritable_attrs += args
28
+
29
+ args.each do |arg|
30
+ module_eval %(class << self; attr_accessor :#{arg} end)
31
+ end
32
+
33
+ @mattr_inheritable_attrs
34
+ end
35
+
36
+ def inherited(subclass)
37
+ super
38
+ @mattr_inheritable_attrs.each do |inheritable_attribute|
39
+ ivar = "@#{inheritable_attribute}"
40
+ subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
41
+
42
+ if instance_variable_get(ivar).respond_to?(:merge)
43
+ method = <<-EOM
44
+ def self.#{inheritable_attribute}
45
+ duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar})
46
+ #{ivar} = superclass.#{inheritable_attribute}.merge(duplicate)
47
+ end
48
+ EOM
49
+
50
+ subclass.class_eval method
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,136 @@
1
+ require 'digest/md5'
2
+ require 'net/http'
3
+
4
+ module Net
5
+ module HTTPHeader
6
+ def digest_auth(username, password, response)
7
+ authenticator = DigestAuthenticator.new(
8
+ username,
9
+ password,
10
+ @method,
11
+ @path,
12
+ response
13
+ )
14
+
15
+ authenticator.authorization_header.each do |v|
16
+ add_field('Authorization', v)
17
+ end
18
+
19
+ authenticator.cookie_header.each do |v|
20
+ add_field('Cookie', v)
21
+ end
22
+ end
23
+
24
+ class DigestAuthenticator
25
+ def initialize(username, password, method, path, response_header)
26
+ @username = username
27
+ @password = password
28
+ @method = method
29
+ @path = path
30
+ @response = parse(response_header)
31
+ @cookies = parse_cookies(response_header)
32
+ end
33
+
34
+ def authorization_header
35
+ @cnonce = md5(random)
36
+ header = [
37
+ %(Digest username="#{@username}"),
38
+ %(realm="#{@response['realm']}"),
39
+ %(nonce="#{@response['nonce']}"),
40
+ %(uri="#{@path}"),
41
+ %(response="#{request_digest}")
42
+ ]
43
+
44
+ header << %(algorithm="#{@response['algorithm']}") if algorithm_present?
45
+
46
+ if qop_present?
47
+ fields = [
48
+ %(cnonce="#{@cnonce}"),
49
+ %(qop="#{@response['qop']}"),
50
+ "nc=00000001"
51
+ ]
52
+ fields.each { |field| header << field }
53
+ end
54
+
55
+ header << %(opaque="#{@response['opaque']}") if opaque_present?
56
+ header
57
+ end
58
+
59
+ def cookie_header
60
+ @cookies
61
+ end
62
+
63
+ private
64
+
65
+ def parse(response_header)
66
+ header = response_header['www-authenticate']
67
+
68
+ header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
69
+
70
+ header =~ /Digest (.*)/
71
+ params = {}
72
+ if $1
73
+ non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
74
+ non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 }
75
+ end
76
+ params
77
+ end
78
+
79
+ def parse_cookies(response_header)
80
+ return [] unless response_header['Set-Cookie']
81
+
82
+ cookies = response_header['Set-Cookie'].split('; ')
83
+
84
+ cookies.reduce([]) do |ret, cookie|
85
+ ret << cookie
86
+ ret
87
+ end
88
+
89
+ cookies
90
+ end
91
+
92
+ def opaque_present?
93
+ @response.key?('opaque') && !@response['opaque'].empty?
94
+ end
95
+
96
+ def qop_present?
97
+ @response.key?('qop') && !@response['qop'].empty?
98
+ end
99
+
100
+ def random
101
+ format "%x", (Time.now.to_i + rand(65535))
102
+ end
103
+
104
+ def request_digest
105
+ a = [md5(a1), @response['nonce'], md5(a2)]
106
+ a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
107
+ md5(a.join(":"))
108
+ end
109
+
110
+ def md5(str)
111
+ Digest::MD5.hexdigest(str)
112
+ end
113
+
114
+ def algorithm_present?
115
+ @response.key?('algorithm') && !@response['algorithm'].empty?
116
+ end
117
+
118
+ def use_md5_sess?
119
+ algorithm_present? && @response['algorithm'] == 'MD5-sess'
120
+ end
121
+
122
+ def a1
123
+ a1_user_realm_pwd = [@username, @response['realm'], @password].join(':')
124
+ if use_md5_sess?
125
+ [ md5(a1_user_realm_pwd), @response['nonce'], @cnonce ].join(':')
126
+ else
127
+ a1_user_realm_pwd
128
+ end
129
+ end
130
+
131
+ def a2
132
+ [@method, @path].join(":")
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,150 @@
1
+ module HTTParty
2
+ # The default parser used by HTTParty, supports xml, json, html, csv and
3
+ # plain text.
4
+ #
5
+ # == Custom Parsers
6
+ #
7
+ # If you'd like to do your own custom parsing, subclassing HTTParty::Parser
8
+ # will make that process much easier. There are a few different ways you can
9
+ # utilize HTTParty::Parser as a superclass.
10
+ #
11
+ # @example Intercept the parsing for all formats
12
+ # class SimpleParser < HTTParty::Parser
13
+ # def parse
14
+ # perform_parsing
15
+ # end
16
+ # end
17
+ #
18
+ # @example Add the atom format and parsing method to the default parser
19
+ # class AtomParsingIncluded < HTTParty::Parser
20
+ # SupportedFormats.merge!(
21
+ # {"application/atom+xml" => :atom}
22
+ # )
23
+ #
24
+ # def atom
25
+ # perform_atom_parsing
26
+ # end
27
+ # end
28
+ #
29
+ # @example Only support the atom format
30
+ # class ParseOnlyAtom < HTTParty::Parser
31
+ # SupportedFormats = {"application/atom+xml" => :atom}
32
+ #
33
+ # def atom
34
+ # perform_atom_parsing
35
+ # end
36
+ # end
37
+ #
38
+ # @abstract Read the Custom Parsers section for more information.
39
+ class Parser
40
+ SupportedFormats = {
41
+ 'text/xml' => :xml,
42
+ 'application/xml' => :xml,
43
+ 'application/json' => :json,
44
+ 'application/vnd.api+json' => :json,
45
+ 'application/hal+json' => :json,
46
+ 'text/json' => :json,
47
+ 'application/javascript' => :plain,
48
+ 'text/javascript' => :plain,
49
+ 'text/html' => :html,
50
+ 'text/plain' => :plain,
51
+ 'text/csv' => :csv,
52
+ 'application/csv' => :csv,
53
+ 'text/comma-separated-values' => :csv
54
+ }
55
+
56
+ # The response body of the request
57
+ # @return [String]
58
+ attr_reader :body
59
+
60
+ # The intended parsing format for the request
61
+ # @return [Symbol] e.g. :json
62
+ attr_reader :format
63
+
64
+ # Instantiate the parser and call {#parse}.
65
+ # @param [String] body the response body
66
+ # @param [Symbol] format the response format
67
+ # @return parsed response
68
+ def self.call(body, format)
69
+ new(body, format).parse
70
+ end
71
+
72
+ # @return [Hash] the SupportedFormats hash
73
+ def self.formats
74
+ const_get(:SupportedFormats)
75
+ end
76
+
77
+ # @param [String] mimetype response MIME type
78
+ # @return [Symbol]
79
+ # @return [nil] mime type not supported
80
+ def self.format_from_mimetype(mimetype)
81
+ formats[formats.keys.detect {|k| mimetype.include?(k)}]
82
+ end
83
+
84
+ # @return [Array<Symbol>] list of supported formats
85
+ def self.supported_formats
86
+ formats.values.uniq
87
+ end
88
+
89
+ # @param [Symbol] format e.g. :json, :xml
90
+ # @return [Boolean]
91
+ def self.supports_format?(format)
92
+ supported_formats.include?(format)
93
+ end
94
+
95
+ def initialize(body, format)
96
+ @body = body
97
+ @format = format
98
+ end
99
+
100
+ # @return [Object] the parsed body
101
+ # @return [nil] when the response body is nil, an empty string, spaces only or "null"
102
+ def parse
103
+ return nil if body.nil?
104
+ return nil if body == "null"
105
+ return nil if body.valid_encoding? && body.strip.empty?
106
+ if body.valid_encoding? && body.encoding == Encoding::UTF_8
107
+ @body = body.gsub(/\A#{UTF8_BOM}/, '')
108
+ end
109
+ if supports_format?
110
+ parse_supported_format
111
+ else
112
+ body
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ def xml
119
+ MultiXml.parse(body)
120
+ end
121
+
122
+ UTF8_BOM = "\xEF\xBB\xBF".freeze
123
+
124
+ def json
125
+ JSON.parse(body, :quirks_mode => true, :allow_nan => true)
126
+ end
127
+
128
+ def csv
129
+ CSV.parse(body)
130
+ end
131
+
132
+ def html
133
+ body
134
+ end
135
+
136
+ def plain
137
+ body
138
+ end
139
+
140
+ def supports_format?
141
+ self.class.supports_format?(format)
142
+ end
143
+
144
+ def parse_supported_format
145
+ send(format)
146
+ rescue NoMethodError => e
147
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,386 @@
1
+ require 'erb'
2
+
3
+ module HTTParty
4
+ class Request #:nodoc:
5
+ SupportedHTTPMethods = [
6
+ Net::HTTP::Get,
7
+ Net::HTTP::Post,
8
+ Net::HTTP::Patch,
9
+ Net::HTTP::Put,
10
+ Net::HTTP::Delete,
11
+ Net::HTTP::Head,
12
+ Net::HTTP::Options,
13
+ Net::HTTP::Move,
14
+ Net::HTTP::Copy,
15
+ Net::HTTP::Mkcol,
16
+ Net::HTTP::Lock,
17
+ Net::HTTP::Unlock,
18
+ ]
19
+
20
+ SupportedURISchemes = ['http', 'https', 'webcal', nil]
21
+
22
+ NON_RAILS_QUERY_STRING_NORMALIZER = proc do |query|
23
+ Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
24
+ if value.nil?
25
+ key.to_s
26
+ elsif value.respond_to?(:to_ary)
27
+ value.to_ary.map {|v| "#{key}=#{ERB::Util.url_encode(v.to_s)}"}
28
+ else
29
+ HashConversions.to_params(key => value)
30
+ end
31
+ end.flatten.join('&')
32
+ end
33
+
34
+ JSON_API_QUERY_STRING_NORMALIZER = proc do |query|
35
+ Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
36
+ if value.nil?
37
+ key.to_s
38
+ elsif value.respond_to?(:to_ary)
39
+ values = value.to_ary.map{|v| ERB::Util.url_encode(v.to_s)}
40
+ "#{key}=#{values.join(',')}"
41
+ else
42
+ HashConversions.to_params(key => value)
43
+ end
44
+ end.flatten.join('&')
45
+ end
46
+
47
+ attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
48
+ attr_reader :path
49
+
50
+ def initialize(http_method, path, o = {})
51
+ @changed_hosts = false
52
+ @credentials_sent = false
53
+
54
+ self.http_method = http_method
55
+ self.options = {
56
+ limit: o.delete(:no_follow) ? 1 : 5,
57
+ assume_utf16_is_big_endian: true,
58
+ default_params: {},
59
+ follow_redirects: true,
60
+ parser: Parser,
61
+ uri_adapter: URI,
62
+ connection_adapter: ConnectionAdapter
63
+ }.merge(o)
64
+ self.path = path
65
+ set_basic_auth_from_uri
66
+ end
67
+
68
+ def path=(uri)
69
+ uri_adapter = options[:uri_adapter]
70
+
71
+ @path = if uri.is_a?(uri_adapter)
72
+ uri
73
+ elsif String.try_convert(uri)
74
+ uri_adapter.parse uri
75
+ else
76
+ raise ArgumentError,
77
+ "bad argument (expected #{uri_adapter} object or URI string)"
78
+ end
79
+ end
80
+
81
+ def request_uri(uri)
82
+ if uri.respond_to? :request_uri
83
+ uri.request_uri
84
+ else
85
+ uri.path
86
+ end
87
+ end
88
+
89
+ def uri
90
+ if redirect && path.relative? && path.path[0] != "/"
91
+ last_uri_host = @last_uri.path.gsub(/[^\/]+$/, "")
92
+
93
+ path.path = "/#{path.path}" if last_uri_host[-1] != "/"
94
+ path.path = last_uri_host + path.path
95
+ end
96
+
97
+ if path.relative? && path.host
98
+ new_uri = options[:uri_adapter].parse("#{@last_uri.scheme}:#{path}")
99
+ elsif path.relative?
100
+ new_uri = options[:uri_adapter].parse("#{base_uri}#{path}")
101
+ else
102
+ new_uri = path.clone
103
+ end
104
+
105
+ # avoid double query string on redirects [#12]
106
+ unless redirect
107
+ new_uri.query = query_string(new_uri)
108
+ end
109
+
110
+ unless SupportedURISchemes.include? new_uri.scheme
111
+ raise UnsupportedURIScheme, "'#{new_uri}' Must be HTTP, HTTPS or Generic"
112
+ end
113
+
114
+ @last_uri = new_uri
115
+ end
116
+
117
+ def base_uri
118
+ if redirect
119
+ base_uri = "#{@last_uri.scheme}://#{@last_uri.host}"
120
+ base_uri += ":#{@last_uri.port}" if @last_uri.port != 80
121
+ base_uri
122
+ else
123
+ options[:base_uri] && HTTParty.normalize_base_uri(options[:base_uri])
124
+ end
125
+ end
126
+
127
+ def format
128
+ options[:format] || (format_from_mimetype(last_response['content-type']) if last_response)
129
+ end
130
+
131
+ def parser
132
+ options[:parser]
133
+ end
134
+
135
+ def connection_adapter
136
+ options[:connection_adapter]
137
+ end
138
+
139
+ def perform(&block)
140
+ validate
141
+ setup_raw_request
142
+ chunked_body = nil
143
+ current_http = http
144
+
145
+ self.last_response = current_http.request(@raw_request) do |http_response|
146
+ if block
147
+ chunks = []
148
+
149
+ http_response.read_body do |fragment|
150
+ encoded_fragment = encode_text(fragment, http_response['content-type'])
151
+ chunks << encoded_fragment if !options[:stream_body]
152
+ block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
153
+ end
154
+
155
+ chunked_body = chunks.join
156
+ end
157
+ end
158
+
159
+ handle_host_redirection if response_redirects?
160
+ result = handle_unauthorized
161
+ result ||= handle_response(chunked_body, &block)
162
+ result
163
+ end
164
+
165
+ def handle_unauthorized(&block)
166
+ return unless digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
167
+ return if @credentials_sent
168
+ @credentials_sent = true
169
+ perform(&block)
170
+ end
171
+
172
+ def raw_body
173
+ @raw_request.body
174
+ end
175
+
176
+ private
177
+
178
+ def http
179
+ connection_adapter.call(uri, options)
180
+ end
181
+
182
+ def credentials
183
+ (options[:basic_auth] || options[:digest_auth]).to_hash
184
+ end
185
+
186
+ def username
187
+ credentials[:username]
188
+ end
189
+
190
+ def password
191
+ credentials[:password]
192
+ end
193
+
194
+ def normalize_query(query)
195
+ if query_string_normalizer
196
+ query_string_normalizer.call(query)
197
+ else
198
+ HashConversions.to_params(query)
199
+ end
200
+ end
201
+
202
+ def query_string_normalizer
203
+ options[:query_string_normalizer]
204
+ end
205
+
206
+ def setup_raw_request
207
+ @raw_request = http_method.new(request_uri(uri))
208
+ @raw_request.body_stream = options[:body_stream] if options[:body_stream]
209
+
210
+ if options[:headers].respond_to?(:to_hash)
211
+ headers_hash = options[:headers].to_hash
212
+
213
+ @raw_request.initialize_http_header(headers_hash)
214
+ # If the caller specified a header of 'Accept-Encoding', assume they want to
215
+ # deal with encoding of content. Disable the internal logic in Net:HTTP
216
+ # that handles encoding, if the platform supports it.
217
+ if @raw_request.respond_to?(:decode_content) && (headers_hash.key?('Accept-Encoding') || headers_hash.key?('accept-encoding'))
218
+ # Using the '[]=' sets decode_content to false
219
+ @raw_request['accept-encoding'] = @raw_request['accept-encoding']
220
+ end
221
+ end
222
+
223
+ if options[:body]
224
+ body = Body.new(
225
+ options[:body],
226
+ query_string_normalizer: query_string_normalizer,
227
+ force_multipart: options[:multipart]
228
+ )
229
+
230
+ if body.multipart?
231
+ content_type = "multipart/form-data; boundary=#{body.boundary}"
232
+ @raw_request['Content-Type'] = content_type
233
+ end
234
+ @raw_request.body = body.call
235
+ end
236
+
237
+ if options[:basic_auth] && send_authorization_header?
238
+ @raw_request.basic_auth(username, password)
239
+ @credentials_sent = true
240
+ end
241
+ setup_digest_auth if digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
242
+ end
243
+
244
+ def digest_auth?
245
+ !!options[:digest_auth]
246
+ end
247
+
248
+ def response_unauthorized?
249
+ !!last_response && last_response.code == '401'
250
+ end
251
+
252
+ def response_has_digest_auth_challenge?
253
+ !last_response['www-authenticate'].nil? && last_response['www-authenticate'].length > 0
254
+ end
255
+
256
+ def setup_digest_auth
257
+ @raw_request.digest_auth(username, password, last_response)
258
+ end
259
+
260
+ def query_string(uri)
261
+ query_string_parts = []
262
+ query_string_parts << uri.query unless uri.query.nil?
263
+
264
+ if options[:query].respond_to?(:to_hash)
265
+ query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash))
266
+ else
267
+ query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
268
+ query_string_parts << options[:query] unless options[:query].nil?
269
+ end
270
+
271
+ query_string_parts.reject!(&:empty?) unless query_string_parts == [""]
272
+ query_string_parts.size > 0 ? query_string_parts.join('&') : nil
273
+ end
274
+
275
+ def assume_utf16_is_big_endian
276
+ options[:assume_utf16_is_big_endian]
277
+ end
278
+
279
+ def handle_response(body, &block)
280
+ if response_redirects?
281
+ options[:limit] -= 1
282
+ if options[:logger]
283
+ logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
284
+ logger.format(self, last_response)
285
+ end
286
+ self.path = last_response['location']
287
+ self.redirect = true
288
+ if last_response.class == Net::HTTPSeeOther
289
+ unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
290
+ self.http_method = Net::HTTP::Get
291
+ end
292
+ elsif last_response.code != '307' && last_response.code != '308'
293
+ unless options[:maintain_method_across_redirects]
294
+ self.http_method = Net::HTTP::Get
295
+ end
296
+ end
297
+ capture_cookies(last_response)
298
+ perform(&block)
299
+ else
300
+ body ||= last_response.body
301
+ body = body.nil? ? body : encode_text(body, last_response['content-type'])
302
+ Response.new(self, last_response, lambda { parse_response(body) }, body: body)
303
+ end
304
+ end
305
+
306
+ def handle_host_redirection
307
+ check_duplicate_location_header
308
+ redirect_path = options[:uri_adapter].parse last_response['location']
309
+ return if redirect_path.relative? || path.host == redirect_path.host
310
+ @changed_hosts = true
311
+ end
312
+
313
+ def check_duplicate_location_header
314
+ location = last_response.get_fields('location')
315
+ if location.is_a?(Array) && location.count > 1
316
+ raise DuplicateLocationHeader.new(last_response)
317
+ end
318
+ end
319
+
320
+ def send_authorization_header?
321
+ !@changed_hosts
322
+ end
323
+
324
+ def response_redirects?
325
+ case last_response
326
+ when Net::HTTPNotModified # 304
327
+ false
328
+ when Net::HTTPRedirection
329
+ options[:follow_redirects] && last_response.key?('location')
330
+ end
331
+ end
332
+
333
+ def parse_response(body)
334
+ parser.call(body, format)
335
+ end
336
+
337
+ def capture_cookies(response)
338
+ return unless response['Set-Cookie']
339
+ cookies_hash = HTTParty::CookieHash.new
340
+ cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie']
341
+ response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) }
342
+
343
+ options[:headers] ||= {}
344
+ options[:headers]['Cookie'] = cookies_hash.to_cookie_string
345
+ end
346
+
347
+ # Uses the HTTP Content-Type header to determine the format of the
348
+ # response It compares the MIME type returned to the types stored in the
349
+ # SupportedFormats hash
350
+ def format_from_mimetype(mimetype)
351
+ if mimetype && parser.respond_to?(:format_from_mimetype)
352
+ parser.format_from_mimetype(mimetype)
353
+ end
354
+ end
355
+
356
+ def validate
357
+ raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
358
+ raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
359
+ raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].respond_to?(:to_hash)
360
+ raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
361
+ raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].respond_to?(:to_hash)
362
+ raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].respond_to?(:to_hash)
363
+ raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].respond_to?(:to_hash)
364
+ end
365
+
366
+ def post?
367
+ Net::HTTP::Post == http_method
368
+ end
369
+
370
+ def set_basic_auth_from_uri
371
+ if path.userinfo
372
+ username, password = path.userinfo.split(':')
373
+ options[:basic_auth] = {username: username, password: password}
374
+ @credentials_sent = true
375
+ end
376
+ end
377
+
378
+ def encode_text(text, content_type)
379
+ TextEncoder.new(
380
+ text,
381
+ content_type: content_type,
382
+ assume_utf16_is_big_endian: assume_utf16_is_big_endian
383
+ ).call
384
+ end
385
+ end
386
+ end