bartzon-httparty 0.6.1

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 (70) hide show
  1. data/.gitignore +8 -0
  2. data/History +216 -0
  3. data/MIT-LICENSE +20 -0
  4. data/Manifest +47 -0
  5. data/README.rdoc +54 -0
  6. data/Rakefile +89 -0
  7. data/VERSION +1 -0
  8. data/bartzon-httparty.gemspec +147 -0
  9. data/bin/httparty +108 -0
  10. data/cucumber.yml +1 -0
  11. data/examples/aaws.rb +32 -0
  12. data/examples/basic.rb +11 -0
  13. data/examples/custom_parsers.rb +67 -0
  14. data/examples/delicious.rb +37 -0
  15. data/examples/google.rb +16 -0
  16. data/examples/rubyurl.rb +14 -0
  17. data/examples/twitter.rb +31 -0
  18. data/examples/whoismyrep.rb +10 -0
  19. data/features/basic_authentication.feature +20 -0
  20. data/features/command_line.feature +7 -0
  21. data/features/deals_with_http_error_codes.feature +26 -0
  22. data/features/digest_authentication.feature +20 -0
  23. data/features/handles_compressed_responses.feature +19 -0
  24. data/features/handles_multiple_formats.feature +34 -0
  25. data/features/steps/env.rb +23 -0
  26. data/features/steps/httparty_response_steps.rb +26 -0
  27. data/features/steps/httparty_steps.rb +27 -0
  28. data/features/steps/mongrel_helper.rb +94 -0
  29. data/features/steps/remote_service_steps.rb +69 -0
  30. data/features/supports_redirection.feature +22 -0
  31. data/features/supports_timeout_option.feature +13 -0
  32. data/httparty.gemspec +146 -0
  33. data/lib/httparty.rb +383 -0
  34. data/lib/httparty/cookie_hash.rb +22 -0
  35. data/lib/httparty/core_extensions.rb +31 -0
  36. data/lib/httparty/exceptions.rb +26 -0
  37. data/lib/httparty/module_inheritable_attributes.rb +34 -0
  38. data/lib/httparty/net_digest_auth.rb +35 -0
  39. data/lib/httparty/parser.rb +141 -0
  40. data/lib/httparty/request.rb +277 -0
  41. data/lib/httparty/response.rb +79 -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 +15 -0
  47. data/spec/fixtures/ssl/generated/bogushost.crt +13 -0
  48. data/spec/fixtures/ssl/generated/ca.crt +15 -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/parser_spec.rb +155 -0
  59. data/spec/httparty/request_spec.rb +488 -0
  60. data/spec/httparty/response_spec.rb +188 -0
  61. data/spec/httparty/ssl_spec.rb +55 -0
  62. data/spec/httparty_spec.rb +570 -0
  63. data/spec/spec.opts +3 -0
  64. data/spec/spec_helper.rb +20 -0
  65. data/spec/support/ssl_test_helper.rb +25 -0
  66. data/spec/support/ssl_test_server.rb +69 -0
  67. data/spec/support/stub_response.rb +30 -0
  68. data/website/css/common.css +47 -0
  69. data/website/index.html +73 -0
  70. metadata +244 -0
@@ -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,35 @@
1
+ require 'digest/md5'
2
+ require 'net/http'
3
+
4
+ module Net
5
+ module HTTPHeader
6
+ def digest_auth(user, password, response)
7
+ response['www-authenticate'] =~ /^(\w+) (.*)/
8
+
9
+ params = {}
10
+ $2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
11
+ params.merge!("cnonce" => Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535))))
12
+
13
+ a_1 = Digest::MD5.hexdigest("#{user}:#{params['realm']}:#{password}")
14
+ a_2 = Digest::MD5.hexdigest("#{@method}:#{@path}")
15
+
16
+ request_digest = Digest::MD5.hexdigest(
17
+ [a_1, params['nonce'], "0", params['cnonce'], params['qop'], a_2].join(":")
18
+ )
19
+
20
+ header = [
21
+ %Q(Digest username="#{user}"),
22
+ %Q(realm="#{params['realm']}"),
23
+ %Q(qop="#{params['qop']}"),
24
+ %Q(uri="#{@path}"),
25
+ %Q(nonce="#{params['nonce']}"),
26
+ %Q(nc="0"),
27
+ %Q(cnonce="#{params['cnonce']}"),
28
+ %Q(opaque="#{params['opaque']}"),
29
+ %Q(response="#{request_digest}")
30
+ ]
31
+
32
+ @header['Authorization'] = header
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,141 @@
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
+ private_class_method :new
97
+
98
+ # @return [Object] the parsed body
99
+ # @return [nil] when the response body is nil or an empty string
100
+ def parse
101
+ return nil if body.nil? || body.empty?
102
+ if supports_format?
103
+ parse_supported_format
104
+ else
105
+ body
106
+ end
107
+ end
108
+
109
+ protected
110
+
111
+ def xml
112
+ Crack::XML.parse(body)
113
+ end
114
+
115
+ def json
116
+ Crack::JSON.parse(body)
117
+ end
118
+
119
+ def yaml
120
+ YAML.load(body)
121
+ end
122
+
123
+ def html
124
+ body
125
+ end
126
+
127
+ def plain
128
+ body
129
+ end
130
+
131
+ def supports_format?
132
+ self.class.supports_format?(format)
133
+ end
134
+
135
+ def parse_supported_format
136
+ send(format)
137
+ rescue NoMethodError
138
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,277 @@
1
+ module HTTParty
2
+ class Request #:nodoc:
3
+ SupportedHTTPMethods = [
4
+ Net::HTTP::Get,
5
+ Net::HTTP::Post,
6
+ Net::HTTP::Post::Multipart,
7
+ Net::HTTP::Put,
8
+ Net::HTTP::Delete,
9
+ Net::HTTP::Head,
10
+ Net::HTTP::Options
11
+ ]
12
+
13
+ SupportedURISchemes = [URI::HTTP, URI::HTTPS]
14
+
15
+ attr_accessor :http_method, :path, :options, :last_response, :redirect
16
+
17
+ def initialize(http_method, path, o={})
18
+ self.http_method = http_method
19
+ self.path = path
20
+ self.options = {
21
+ :limit => o.delete(:no_follow) ? 1 : 5,
22
+ :default_params => {},
23
+ :parser => Parser
24
+ }.merge(o)
25
+ end
26
+
27
+ def path=(uri)
28
+ @path = URI.parse(uri)
29
+ end
30
+
31
+ def uri
32
+ new_uri = path.relative? ? URI.parse("#{options[:base_uri]}#{path}") : path
33
+
34
+ # avoid double query string on redirects [#12]
35
+ unless redirect
36
+ new_uri.query = query_string(new_uri)
37
+ end
38
+
39
+ unless SupportedURISchemes.include? new_uri.class
40
+ raise UnsupportedURIScheme, "'#{new_uri}' Must be HTTP or HTTPS"
41
+ end
42
+
43
+ new_uri
44
+ end
45
+
46
+ def format
47
+ options[:format] || (format_from_mimetype(last_response['content-type']) if last_response)
48
+ end
49
+
50
+ def parser
51
+ options[:parser]
52
+ end
53
+
54
+ def perform
55
+ validate
56
+ setup_raw_request
57
+ get_response
58
+ handle_response
59
+ end
60
+
61
+ private
62
+
63
+ def attach_ssl_certificates(http)
64
+ if http.use_ssl?
65
+ # Client certificate authentication
66
+ if options[:pem]
67
+ http.cert = OpenSSL::X509::Certificate.new(options[:pem])
68
+ http.key = OpenSSL::PKey::RSA.new(options[:pem])
69
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
70
+ end
71
+
72
+ # SSL certificate authority file and/or directory
73
+ if options[:ssl_ca_file]
74
+ http.ca_file = options[:ssl_ca_file]
75
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
76
+ end
77
+ if options[:ssl_ca_path]
78
+ http.ca_path = options[:ssl_ca_path]
79
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
80
+ end
81
+ end
82
+ end
83
+
84
+ def http
85
+ http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport])
86
+ http.use_ssl = ssl_implied?
87
+
88
+ if options[:timeout] && options[:timeout].is_a?(Integer)
89
+ http.open_timeout = options[:timeout]
90
+ http.read_timeout = options[:timeout]
91
+ end
92
+
93
+ attach_ssl_certificates(http)
94
+
95
+ if options[:debug_output]
96
+ http.set_debug_output(options[:debug_output])
97
+ end
98
+
99
+ http
100
+ end
101
+
102
+ def ssl_implied?
103
+ uri.port == 443 || uri.instance_of?(URI::HTTPS)
104
+ end
105
+
106
+ def body
107
+ options[:body].is_a?(Hash) ? options[:body].to_params : options[:body]
108
+ end
109
+
110
+ def credentials
111
+ options[:basic_auth] || options[:digest_auth]
112
+ end
113
+
114
+ def username
115
+ credentials[:username]
116
+ end
117
+
118
+ def password
119
+ credentials[:password]
120
+ end
121
+
122
+ def setup_raw_request
123
+ if multipart?
124
+ @file_handles = []
125
+ io_objects = {}
126
+
127
+ options[:multipart].each do |field_name, info|
128
+ fp = File.open(info[:path])
129
+ @file_handles << fp
130
+
131
+ io_objects[field_name] = UploadIO.new(fp, info[:type], info[:path])
132
+ end
133
+
134
+ @raw_request = http_method.new(uri.request_uri, io_objects)
135
+
136
+ # We have to duplicate and merge the headers set by the
137
+ # multipart object to make sure that Net::HTTP
138
+ # doesn't override them down the line when it calls
139
+ # initialize_http_header.
140
+ #
141
+ # Otherwise important headers like Content-Length,
142
+ # Accept, and Content-Type will be deleted.
143
+ original_headers = {}
144
+ @raw_request.each do |key, value|
145
+ original_headers[key] = value
146
+ end
147
+
148
+ options[:headers] ||= {}
149
+ original_headers.merge!(options[:headers])
150
+ options[:headers] = original_headers
151
+ else
152
+ @raw_request = http_method.new(uri.request_uri)
153
+ end
154
+
155
+ if post? && options[:query]
156
+ @raw_request.set_form_data(options[:query])
157
+ end
158
+
159
+ @raw_request.body = body if body
160
+ @raw_request.initialize_http_header(options[:headers])
161
+ @raw_request.basic_auth(username, password) if options[:basic_auth]
162
+ setup_digest_auth if options[:digest_auth]
163
+ end
164
+
165
+ def setup_digest_auth
166
+ res = http.head(uri.request_uri, options[:headers])
167
+ if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
168
+ @raw_request.digest_auth(username, password, res)
169
+ end
170
+ end
171
+
172
+ def perform_actual_request
173
+ http.request(@raw_request)
174
+ end
175
+
176
+ def get_response
177
+ self.last_response = perform_actual_request
178
+
179
+ if @file_handles
180
+ @file_handles.each do |fp|
181
+ fp.close
182
+ end
183
+ end
184
+ end
185
+
186
+ def query_string(uri)
187
+ query_string_parts = []
188
+ query_string_parts << uri.query unless uri.query.nil?
189
+
190
+ if options[:query].is_a?(Hash)
191
+ query_string_parts << options[:default_params].merge(options[:query]).to_params
192
+ else
193
+ query_string_parts << options[:default_params].to_params unless options[:default_params].empty?
194
+ query_string_parts << options[:query] unless options[:query].nil?
195
+ end
196
+
197
+ query_string_parts.size > 0 ? query_string_parts.join('&') : nil
198
+ end
199
+
200
+ # Raises exception Net::XXX (http error code) if an http error occured
201
+ def handle_response
202
+ handle_deflation
203
+ case last_response
204
+ when Net::HTTPMultipleChoice, # 300
205
+ Net::HTTPMovedPermanently, # 301
206
+ Net::HTTPFound, # 302
207
+ Net::HTTPSeeOther, # 303
208
+ Net::HTTPUseProxy, # 305
209
+ Net::HTTPTemporaryRedirect
210
+ if last_response.key?('location')
211
+ options[:limit] -= 1
212
+ self.path = last_response['location']
213
+ self.redirect = true
214
+ self.http_method = Net::HTTP::Get unless options[:maintain_method_across_redirects]
215
+ capture_cookies(last_response)
216
+ perform
217
+ else
218
+ last_response
219
+ end
220
+ else
221
+ Response.new(last_response, parse_response(last_response.body))
222
+ end
223
+ end
224
+
225
+ # Inspired by Ruby 1.9
226
+ def handle_deflation
227
+ case last_response["content-encoding"]
228
+ when "gzip"
229
+ body_io = StringIO.new(last_response.body)
230
+ last_response.body.replace Zlib::GzipReader.new(body_io).read
231
+ when "deflate"
232
+ last_response.body.replace Zlib::Inflate.inflate(last_response.body)
233
+ end
234
+ end
235
+
236
+ def parse_response(body)
237
+ parser.call(body, format)
238
+ end
239
+
240
+ def capture_cookies(response)
241
+ return unless response['Set-Cookie']
242
+ cookies_hash = HTTParty::CookieHash.new()
243
+ cookies_hash.add_cookies(options[:headers]['Cookie']) if options[:headers] && options[:headers]['Cookie']
244
+ cookies_hash.add_cookies(response['Set-Cookie'])
245
+ options[:headers] ||= {}
246
+ options[:headers]['Cookie'] = cookies_hash.to_cookie_string
247
+ end
248
+
249
+ # Uses the HTTP Content-Type header to determine the format of the
250
+ # response It compares the MIME type returned to the types stored in the
251
+ # SupportedFormats hash
252
+ def format_from_mimetype(mimetype)
253
+ if mimetype && parser.respond_to?(:format_from_mimetype)
254
+ parser.format_from_mimetype(mimetype)
255
+ end
256
+ end
257
+
258
+ def validate
259
+ raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
260
+ raise ArgumentError, 'only get, post, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
261
+ raise ArgumentError, 'multipart must include at least one file' if multipart? && (options[:multipart].nil? || options[:multipart].empty?)
262
+ raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
263
+ raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
264
+ raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
265
+ raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].is_a?(Hash)
266
+ raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].is_a?(Hash)
267
+ end
268
+
269
+ def post?
270
+ [Net::HTTP::Post, Net::HTTP::Post::Multipart].include?(http_method)
271
+ end
272
+
273
+ def multipart?
274
+ Net::HTTP::Post::Multipart == http_method
275
+ end
276
+ end
277
+ end