bartzon-httparty 0.6.1

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