httparty2 0.7.10

Sign up to get free protection for your applications and to get access to all the features.
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