httparty2 0.7.10

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 (71) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +6 -0
  3. data/History +253 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +54 -0
  6. data/Rakefile +13 -0
  7. data/bin/httparty +108 -0
  8. data/cucumber.yml +1 -0
  9. data/examples/aaws.rb +32 -0
  10. data/examples/basic.rb +32 -0
  11. data/examples/custom_parsers.rb +67 -0
  12. data/examples/delicious.rb +37 -0
  13. data/examples/google.rb +16 -0
  14. data/examples/rubyurl.rb +14 -0
  15. data/examples/tripit_sign_in.rb +33 -0
  16. data/examples/twitter.rb +31 -0
  17. data/examples/whoismyrep.rb +10 -0
  18. data/features/basic_authentication.feature +20 -0
  19. data/features/command_line.feature +7 -0
  20. data/features/deals_with_http_error_codes.feature +26 -0
  21. data/features/digest_authentication.feature +20 -0
  22. data/features/handles_compressed_responses.feature +19 -0
  23. data/features/handles_multiple_formats.feature +34 -0
  24. data/features/steps/env.rb +22 -0
  25. data/features/steps/httparty_response_steps.rb +26 -0
  26. data/features/steps/httparty_steps.rb +27 -0
  27. data/features/steps/mongrel_helper.rb +94 -0
  28. data/features/steps/remote_service_steps.rb +69 -0
  29. data/features/supports_redirection.feature +22 -0
  30. data/features/supports_timeout_option.feature +13 -0
  31. data/httparty.gemspec +31 -0
  32. data/lib/httparty.rb +455 -0
  33. data/lib/httparty/cookie_hash.rb +22 -0
  34. data/lib/httparty/core_extensions.rb +9 -0
  35. data/lib/httparty/exceptions.rb +26 -0
  36. data/lib/httparty/module_inheritable_attributes.rb +34 -0
  37. data/lib/httparty/net_digest_auth.rb +71 -0
  38. data/lib/httparty/parser.rb +140 -0
  39. data/lib/httparty/request.rb +252 -0
  40. data/lib/httparty/response.rb +85 -0
  41. data/lib/httparty/version.rb +3 -0
  42. data/spec/fixtures/delicious.xml +23 -0
  43. data/spec/fixtures/empty.xml +0 -0
  44. data/spec/fixtures/google.html +3 -0
  45. data/spec/fixtures/ssl/generate.sh +29 -0
  46. data/spec/fixtures/ssl/generated/1fe462c2.0 +16 -0
  47. data/spec/fixtures/ssl/generated/bogushost.crt +13 -0
  48. data/spec/fixtures/ssl/generated/ca.crt +16 -0
  49. data/spec/fixtures/ssl/generated/ca.key +15 -0
  50. data/spec/fixtures/ssl/generated/selfsigned.crt +14 -0
  51. data/spec/fixtures/ssl/generated/server.crt +13 -0
  52. data/spec/fixtures/ssl/generated/server.key +15 -0
  53. data/spec/fixtures/ssl/openssl-exts.cnf +9 -0
  54. data/spec/fixtures/twitter.json +1 -0
  55. data/spec/fixtures/twitter.xml +403 -0
  56. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  57. data/spec/httparty/cookie_hash_spec.rb +71 -0
  58. data/spec/httparty/net_digest_auth_spec.rb +93 -0
  59. data/spec/httparty/parser_spec.rb +155 -0
  60. data/spec/httparty/request_spec.rb +496 -0
  61. data/spec/httparty/response_spec.rb +193 -0
  62. data/spec/httparty/ssl_spec.rb +54 -0
  63. data/spec/httparty_spec.rb +621 -0
  64. data/spec/spec.opts +3 -0
  65. data/spec/spec_helper.rb +23 -0
  66. data/spec/support/ssl_test_helper.rb +25 -0
  67. data/spec/support/ssl_test_server.rb +69 -0
  68. data/spec/support/stub_response.rb +30 -0
  69. data/website/css/common.css +47 -0
  70. data/website/index.html +73 -0
  71. metadata +206 -0
@@ -0,0 +1,22 @@
1
+ class HTTParty::CookieHash < Hash #:nodoc:
2
+
3
+ CLIENT_COOKIES = %w{path expires domain path secure HTTPOnly}
4
+
5
+ def add_cookies(value)
6
+ case value
7
+ when Hash
8
+ merge!(value)
9
+ when String
10
+ value.split('; ').each do |cookie|
11
+ array = cookie.split('=')
12
+ self[array[0].to_sym] = array[1]
13
+ end
14
+ else
15
+ raise "add_cookies only takes a Hash or a String"
16
+ end
17
+ end
18
+
19
+ def to_cookie_string
20
+ delete_if { |k, v| CLIENT_COOKIES.include?(k.to_s) }.collect { |k, v| "#{k}=#{v}" }.join("; ")
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module HTTParty
2
+ if defined?(::BasicObject)
3
+ BasicObject = ::BasicObject #:nodoc:
4
+ else
5
+ class BasicObject #:nodoc:
6
+ instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module HTTParty
2
+ # Exception raised when you attempt to set a non-existant format
3
+ class UnsupportedFormat < StandardError; end
4
+
5
+ # Exception raised when using a URI scheme other than HTTP or HTTPS
6
+ class UnsupportedURIScheme < StandardError; end
7
+
8
+ # @abstract Exceptions which inherit from ResponseError contain the Net::HTTP
9
+ # response object accessible via the {#response} method.
10
+ class ResponseError < StandardError
11
+ # Returns the response of the last request
12
+ # @return [Net::HTTPResponse] A subclass of Net::HTTPResponse, e.g.
13
+ # Net::HTTPOK
14
+ attr_reader :response
15
+
16
+ # Instantiate an instance of ResponseError with a Net::HTTPResponse object
17
+ # @param [Net::HTTPResponse]
18
+ def initialize(response)
19
+ @response = response
20
+ end
21
+ end
22
+
23
+ # Exception that is raised when request has redirected too many times.
24
+ # Calling {#response} returns the Net:HTTP response object.
25
+ class RedirectionTooDeep < ResponseError; end
26
+ end
@@ -0,0 +1,34 @@
1
+ module HTTParty
2
+ module ModuleInheritableAttributes #:nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods #:nodoc:
8
+ def mattr_inheritable(*args)
9
+ @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
10
+ @mattr_inheritable_attrs += args
11
+ args.each do |arg|
12
+ module_eval %(class << self; attr_accessor :#{arg} end)
13
+ end
14
+ @mattr_inheritable_attrs
15
+ end
16
+
17
+ def inherited(subclass)
18
+ super
19
+ @mattr_inheritable_attrs.each do |inheritable_attribute|
20
+ ivar = "@#{inheritable_attribute}"
21
+ subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
22
+ if instance_variable_get(ivar).respond_to?(:merge)
23
+ method = <<-EOM
24
+ def self.#{inheritable_attribute}
25
+ #{ivar} = superclass.#{inheritable_attribute}.merge #{ivar}
26
+ end
27
+ EOM
28
+ subclass.class_eval method
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,71 @@
1
+ require 'digest/md5'
2
+ require 'net/http'
3
+
4
+ module Net
5
+ module HTTPHeader
6
+ def digest_auth(username, password, response)
7
+ @header['Authorization'] = DigestAuthenticator.new(username, password,
8
+ @method, @path, response).authorization_header
9
+ end
10
+
11
+
12
+ class DigestAuthenticator
13
+ def initialize(username, password, method, path, response_header)
14
+ @username = username
15
+ @password = password
16
+ @method = method
17
+ @path = path
18
+ @response = parse(response_header)
19
+ end
20
+
21
+ def authorization_header
22
+ @cnonce = md5(random)
23
+ header = [%Q(Digest username="#{@username}"),
24
+ %Q(realm="#{@response['realm']}"),
25
+ %Q(nonce="#{@response['nonce']}"),
26
+ %Q(uri="#{@path}"),
27
+ %Q(response="#{request_digest}")]
28
+ [%Q(cnonce="#{@cnonce}"),
29
+ %Q(opaque="#{@response['opaque']}"),
30
+ %Q(qop="#{@response['qop']}"),
31
+ %Q(nc="0")].each { |field| header << field } if qop_present?
32
+ header
33
+ end
34
+
35
+ private
36
+
37
+ def parse(response_header)
38
+ response_header['www-authenticate'] =~ /^(\w+) (.*)/
39
+ params = {}
40
+ $2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
41
+ params
42
+ end
43
+
44
+ def qop_present?
45
+ @response.has_key?('qop') and not @response['qop'].empty?
46
+ end
47
+
48
+ def random
49
+ "%x" % (Time.now.to_i + rand(65535))
50
+ end
51
+
52
+ def request_digest
53
+ a = [md5(a1), @response['nonce'], md5(a2)]
54
+ a.insert(2, "0", @cnonce, @response['qop']) if qop_present?
55
+ md5(a.join(":"))
56
+ end
57
+
58
+ def md5(str)
59
+ Digest::MD5.hexdigest(str)
60
+ end
61
+
62
+ def a1
63
+ [@username, @response['realm'], @password].join(":")
64
+ end
65
+
66
+ def a2
67
+ [@method, @path].join(":")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,140 @@
1
+ module HTTParty
2
+ # The default parser used by HTTParty, supports xml, json, html, yaml, 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
+ 'text/json' => :json,
45
+ 'application/javascript' => :json,
46
+ 'text/javascript' => :json,
47
+ 'text/html' => :html,
48
+ 'application/x-yaml' => :yaml,
49
+ 'text/yaml' => :yaml,
50
+ 'text/plain' => :plain
51
+ }
52
+
53
+ # The response body of the request
54
+ # @return [String]
55
+ attr_reader :body
56
+
57
+ # The intended parsing format for the request
58
+ # @return [Symbol] e.g. :json
59
+ attr_reader :format
60
+
61
+ # Instantiate the parser and call {#parse}.
62
+ # @param [String] body the response body
63
+ # @param [Symbol] format the response format
64
+ # @return parsed response
65
+ def self.call(body, format)
66
+ new(body, format).parse
67
+ end
68
+
69
+ # @return [Hash] the SupportedFormats hash
70
+ def self.formats
71
+ const_get(:SupportedFormats)
72
+ end
73
+
74
+ # @param [String] mimetype response MIME type
75
+ # @return [Symbol]
76
+ # @return [nil] mime type not supported
77
+ def self.format_from_mimetype(mimetype)
78
+ formats[formats.keys.detect {|k| mimetype.include?(k)}]
79
+ end
80
+
81
+ # @return [Array<Symbol>] list of supported formats
82
+ def self.supported_formats
83
+ formats.values.uniq
84
+ end
85
+
86
+ # @param [Symbol] format e.g. :json, :xml
87
+ # @return [Boolean]
88
+ def self.supports_format?(format)
89
+ supported_formats.include?(format)
90
+ end
91
+
92
+ def initialize(body, format)
93
+ @body = body
94
+ @format = format
95
+ end
96
+
97
+ # @return [Object] the parsed body
98
+ # @return [nil] when the response body is nil or an empty string
99
+ def parse
100
+ return nil if body.nil? || body.empty?
101
+ if supports_format?
102
+ parse_supported_format
103
+ else
104
+ body
105
+ end
106
+ end
107
+
108
+ protected
109
+
110
+ def xml
111
+ MultiXml.parse(body)
112
+ end
113
+
114
+ def json
115
+ MultiJson.decode(body)
116
+ end
117
+
118
+ def yaml
119
+ YAML.load(body)
120
+ end
121
+
122
+ def html
123
+ body
124
+ end
125
+
126
+ def plain
127
+ body
128
+ end
129
+
130
+ def supports_format?
131
+ self.class.supports_format?(format)
132
+ end
133
+
134
+ def parse_supported_format
135
+ send(format)
136
+ rescue NoMethodError
137
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,252 @@
1
+ module HTTParty
2
+ class Request #:nodoc:
3
+ SupportedHTTPMethods = [
4
+ Net::HTTP::Get,
5
+ Net::HTTP::Post,
6
+ Net::HTTP::Put,
7
+ Net::HTTP::Delete,
8
+ Net::HTTP::Head,
9
+ Net::HTTP::Options
10
+ ]
11
+
12
+ SupportedURISchemes = [URI::HTTP, URI::HTTPS]
13
+
14
+ NON_RAILS_QUERY_STRING_NORMALIZER = Proc.new do |query|
15
+ Array(query).map do |key, value|
16
+ if value.nil?
17
+ key.to_s
18
+ elsif value.is_a?(Array)
19
+ value.map {|v| "#{key}=#{URI.encode(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}"}
20
+ else
21
+ {key => value}.to_param
22
+ end
23
+ end.flatten.sort.join('&')
24
+ end
25
+
26
+ attr_accessor :http_method, :path, :options, :last_response, :redirect
27
+
28
+ def initialize(http_method, path, o={})
29
+ self.http_method = http_method
30
+ self.path = path
31
+ self.options = {
32
+ :limit => o.delete(:no_follow) ? 1 : 5,
33
+ :default_params => {},
34
+ :follow_redirects => true,
35
+ :parser => Parser
36
+ }.merge(o)
37
+ end
38
+
39
+ def path=(uri)
40
+ @path = URI.parse(uri)
41
+ end
42
+
43
+ def uri
44
+ new_uri = path.relative? ? URI.parse("#{options[:base_uri]}#{path}") : path
45
+
46
+ # avoid double query string on redirects [#12]
47
+ unless redirect
48
+ new_uri.query = query_string(new_uri)
49
+ end
50
+
51
+ unless SupportedURISchemes.include? new_uri.class
52
+ raise UnsupportedURIScheme, "'#{new_uri}' Must be HTTP or HTTPS"
53
+ end
54
+
55
+ new_uri
56
+ end
57
+
58
+ def format
59
+ options[:format] || (format_from_mimetype(last_response['content-type']) if last_response)
60
+ end
61
+
62
+ def parser
63
+ options[:parser]
64
+ end
65
+
66
+ def perform
67
+ validate
68
+ setup_raw_request
69
+ self.last_response = http.request(@raw_request)
70
+ handle_deflation
71
+ handle_response
72
+ end
73
+
74
+ private
75
+
76
+ def attach_ssl_certificates(http)
77
+ if http.use_ssl?
78
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
79
+
80
+ # Client certificate authentication
81
+ if options[:pem]
82
+ http.cert = OpenSSL::X509::Certificate.new(options[:pem])
83
+ http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
84
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
85
+ end
86
+
87
+ # SSL certificate authority file and/or directory
88
+ if options[:ssl_ca_file]
89
+ http.ca_file = options[:ssl_ca_file]
90
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
91
+ end
92
+
93
+ if options[:ssl_ca_path]
94
+ http.ca_path = options[:ssl_ca_path]
95
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
96
+ end
97
+ end
98
+ end
99
+
100
+ def http
101
+ http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport])
102
+ http.use_ssl = ssl_implied?
103
+
104
+ if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
105
+ http.open_timeout = options[:timeout]
106
+ http.read_timeout = options[:timeout]
107
+ end
108
+
109
+ attach_ssl_certificates(http)
110
+
111
+ if options[:debug_output]
112
+ http.set_debug_output(options[:debug_output])
113
+ end
114
+
115
+ http
116
+ end
117
+
118
+ def ssl_implied?
119
+ uri.port == 443 || uri.instance_of?(URI::HTTPS)
120
+ end
121
+
122
+ def body
123
+ options[:body].is_a?(Hash) ? normalize_query(options[:body]) : options[:body]
124
+ end
125
+
126
+ def credentials
127
+ options[:basic_auth] || options[:digest_auth]
128
+ end
129
+
130
+ def username
131
+ credentials[:username]
132
+ end
133
+
134
+ def password
135
+ credentials[:password]
136
+ end
137
+
138
+ def normalize_query(query)
139
+ if query_string_normalizer
140
+ query_string_normalizer.call(query)
141
+ else
142
+ query.to_param
143
+ end
144
+ end
145
+
146
+ def query_string_normalizer
147
+ options[:query_string_normalizer]
148
+ end
149
+
150
+ def setup_raw_request
151
+ @raw_request = http_method.new(uri.request_uri)
152
+ @raw_request.body = body if body
153
+ @raw_request.initialize_http_header(options[:headers])
154
+ @raw_request.basic_auth(username, password) if options[:basic_auth]
155
+ setup_digest_auth if options[:digest_auth]
156
+ end
157
+
158
+ def setup_digest_auth
159
+ res = http.head(uri.request_uri, options[:headers])
160
+ if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
161
+ @raw_request.digest_auth(username, password, res)
162
+ end
163
+ end
164
+
165
+ def query_string(uri)
166
+ query_string_parts = []
167
+ query_string_parts << uri.query unless uri.query.nil?
168
+
169
+ if options[:query].is_a?(Hash)
170
+ query_string_parts << normalize_query(options[:default_params].merge(options[:query]))
171
+ else
172
+ query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
173
+ query_string_parts << options[:query] unless options[:query].nil?
174
+ end
175
+
176
+ query_string_parts.size > 0 ? query_string_parts.join('&') : nil
177
+ end
178
+
179
+ # Raises exception Net::XXX (http error code) if an http error occured
180
+ def handle_response
181
+ if response_redirects?
182
+ options[:limit] -= 1
183
+ self.path = last_response['location']
184
+ self.redirect = true
185
+ self.http_method = Net::HTTP::Get unless options[:maintain_method_across_redirects]
186
+ capture_cookies(last_response)
187
+ perform
188
+ else
189
+ Response.new(self, last_response, parse_response(last_response.body))
190
+ end
191
+ end
192
+
193
+ # Inspired by Ruby 1.9
194
+ def handle_deflation
195
+ case last_response["content-encoding"]
196
+ when "gzip"
197
+ body_io = StringIO.new(last_response.body)
198
+ last_response.body.replace Zlib::GzipReader.new(body_io).read
199
+ when "deflate"
200
+ last_response.body.replace Zlib::Inflate.inflate(last_response.body)
201
+ end
202
+ end
203
+
204
+ def response_redirects?
205
+ case last_response
206
+ when Net::HTTPMultipleChoice, # 300
207
+ Net::HTTPMovedPermanently, # 301
208
+ Net::HTTPFound, # 302
209
+ Net::HTTPSeeOther, # 303
210
+ Net::HTTPUseProxy, # 305
211
+ Net::HTTPTemporaryRedirect
212
+ options[:follow_redirects] && last_response.key?('location')
213
+ end
214
+ end
215
+
216
+ def parse_response(body)
217
+ parser.call(body, format)
218
+ end
219
+
220
+ def capture_cookies(response)
221
+ return unless response['Set-Cookie']
222
+ cookies_hash = HTTParty::CookieHash.new()
223
+ cookies_hash.add_cookies(options[:headers]['Cookie']) if options[:headers] && options[:headers]['Cookie']
224
+ cookies_hash.add_cookies(response['Set-Cookie'])
225
+ options[:headers] ||= {}
226
+ options[:headers]['Cookie'] = cookies_hash.to_cookie_string
227
+ end
228
+
229
+ # Uses the HTTP Content-Type header to determine the format of the
230
+ # response It compares the MIME type returned to the types stored in the
231
+ # SupportedFormats hash
232
+ def format_from_mimetype(mimetype)
233
+ if mimetype && parser.respond_to?(:format_from_mimetype)
234
+ parser.format_from_mimetype(mimetype)
235
+ end
236
+ end
237
+
238
+ def validate
239
+ raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
240
+ raise ArgumentError, 'only get, post, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
241
+ raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
242
+ raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
243
+ raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
244
+ raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].is_a?(Hash)
245
+ raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].is_a?(Hash)
246
+ end
247
+
248
+ def post?
249
+ Net::HTTP::Post == http_method
250
+ end
251
+ end
252
+ end