dkastner-httparty 0.9.0

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 (80) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +8 -0
  3. data/Gemfile +15 -0
  4. data/Guardfile +16 -0
  5. data/History +293 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +79 -0
  8. data/Rakefile +15 -0
  9. data/bin/httparty +114 -0
  10. data/cucumber.yml +1 -0
  11. data/examples/aaws.rb +32 -0
  12. data/examples/basic.rb +32 -0
  13. data/examples/crack.rb +19 -0
  14. data/examples/custom_parsers.rb +67 -0
  15. data/examples/delicious.rb +37 -0
  16. data/examples/google.rb +16 -0
  17. data/examples/headers_and_user_agents.rb +6 -0
  18. data/examples/nokogiri_html_parser.rb +22 -0
  19. data/examples/rubyurl.rb +14 -0
  20. data/examples/tripit_sign_in.rb +33 -0
  21. data/examples/twitter.rb +31 -0
  22. data/examples/whoismyrep.rb +10 -0
  23. data/features/basic_authentication.feature +20 -0
  24. data/features/command_line.feature +7 -0
  25. data/features/deals_with_http_error_codes.feature +26 -0
  26. data/features/digest_authentication.feature +20 -0
  27. data/features/handles_compressed_responses.feature +19 -0
  28. data/features/handles_multiple_formats.feature +34 -0
  29. data/features/steps/env.rb +22 -0
  30. data/features/steps/httparty_response_steps.rb +26 -0
  31. data/features/steps/httparty_steps.rb +27 -0
  32. data/features/steps/mongrel_helper.rb +94 -0
  33. data/features/steps/remote_service_steps.rb +69 -0
  34. data/features/supports_redirection.feature +22 -0
  35. data/features/supports_timeout_option.feature +13 -0
  36. data/httparty.gemspec +24 -0
  37. data/lib/httparty.rb +503 -0
  38. data/lib/httparty/connection_adapter.rb +116 -0
  39. data/lib/httparty/cookie_hash.rb +22 -0
  40. data/lib/httparty/core_extensions.rb +32 -0
  41. data/lib/httparty/exceptions.rb +26 -0
  42. data/lib/httparty/hash_conversions.rb +51 -0
  43. data/lib/httparty/module_inheritable_attributes.rb +44 -0
  44. data/lib/httparty/net_digest_auth.rb +84 -0
  45. data/lib/httparty/parser.rb +145 -0
  46. data/lib/httparty/request.rb +243 -0
  47. data/lib/httparty/response.rb +62 -0
  48. data/lib/httparty/response/headers.rb +31 -0
  49. data/lib/httparty/version.rb +3 -0
  50. data/spec/fixtures/delicious.xml +23 -0
  51. data/spec/fixtures/empty.xml +0 -0
  52. data/spec/fixtures/google.html +3 -0
  53. data/spec/fixtures/ssl/generate.sh +29 -0
  54. data/spec/fixtures/ssl/generated/1fe462c2.0 +16 -0
  55. data/spec/fixtures/ssl/generated/bogushost.crt +13 -0
  56. data/spec/fixtures/ssl/generated/ca.crt +16 -0
  57. data/spec/fixtures/ssl/generated/ca.key +15 -0
  58. data/spec/fixtures/ssl/generated/selfsigned.crt +14 -0
  59. data/spec/fixtures/ssl/generated/server.crt +13 -0
  60. data/spec/fixtures/ssl/generated/server.key +15 -0
  61. data/spec/fixtures/ssl/openssl-exts.cnf +9 -0
  62. data/spec/fixtures/twitter.json +1 -0
  63. data/spec/fixtures/twitter.xml +403 -0
  64. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  65. data/spec/httparty/connection_adapter_spec.rb +206 -0
  66. data/spec/httparty/cookie_hash_spec.rb +70 -0
  67. data/spec/httparty/net_digest_auth_spec.rb +115 -0
  68. data/spec/httparty/parser_spec.rb +171 -0
  69. data/spec/httparty/request_spec.rb +507 -0
  70. data/spec/httparty/response_spec.rb +214 -0
  71. data/spec/httparty/ssl_spec.rb +62 -0
  72. data/spec/httparty_spec.rb +703 -0
  73. data/spec/spec.opts +2 -0
  74. data/spec/spec_helper.rb +30 -0
  75. data/spec/support/ssl_test_helper.rb +47 -0
  76. data/spec/support/ssl_test_server.rb +80 -0
  77. data/spec/support/stub_response.rb +43 -0
  78. data/website/css/common.css +47 -0
  79. data/website/index.html +73 -0
  80. metadata +190 -0
@@ -0,0 +1,116 @@
1
+ module HTTParty
2
+ # Default connection adapter that returns a new Net::HTTP each time
3
+ #
4
+ # == Custom Connection Factories
5
+ #
6
+ # If you like to implement your own connection adapter, subclassing
7
+ # HTTPParty::ConnectionAdapter will make it easier. Just override
8
+ # the #connection method. The uri and options attributes will have
9
+ # all the info you need to construct your http connection. Whatever
10
+ # you return from your connection method needs to adhere to the
11
+ # Net::HTTP interface as this is what HTTParty expects.
12
+ #
13
+ # @example log the uri and options
14
+ # class LoggingConnectionAdapter < HTTParty::ConnectionAdapter
15
+ # def connection
16
+ # puts uri
17
+ # puts options
18
+ # Net::HTTP.new(uri)
19
+ # end
20
+ # end
21
+ #
22
+ # @example count number of http calls
23
+ # class CountingConnectionAdapter < HTTParty::ConnectionAdapter
24
+ # @@count = 0
25
+ #
26
+ # self.count
27
+ # @@count
28
+ # end
29
+ #
30
+ # def connection
31
+ # self.count += 1
32
+ # super
33
+ # end
34
+ # end
35
+ #
36
+ # === Configuration
37
+ # There is lots of configuration data available for your connection adapter
38
+ # in the #options attribute. It is up to you to interpret them within your
39
+ # connection adapter. Take a look at the implementation of
40
+ # HTTParty::ConnectionAdapter#connection for examples of how they are used.
41
+ # Something are probably interesting are as follows:
42
+ # * :+timeout+: timeout in seconds
43
+ # * :+debug_output+: see HTTParty::ClassMethods.debug_output.
44
+ # * :+pem+: contains pem data. see HTTParty::ClassMethods.pem.
45
+ # * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file.
46
+ # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
47
+ # * :+connection_adapter_options+: contains the hash your passed to HTTParty.connection_adapter when you configured your connection adapter
48
+ class ConnectionAdapter
49
+
50
+ def self.call(uri, options)
51
+ new(uri, options).connection
52
+ end
53
+
54
+ attr_reader :uri, :options
55
+
56
+ def initialize(uri, options={})
57
+ raise ArgumentError, "uri must be a URI, not a #{uri.class}" unless uri.kind_of? URI
58
+
59
+ @uri = uri
60
+ @options = options
61
+ end
62
+
63
+ def connection
64
+ http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport], options[:http_proxyuser], options[:http_proxypass])
65
+
66
+ http.use_ssl = ssl_implied?(uri)
67
+
68
+ attach_ssl_certificates(http, options)
69
+
70
+ if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
71
+ http.open_timeout = options[:timeout]
72
+ http.read_timeout = options[:timeout]
73
+ end
74
+
75
+ if options[:debug_output]
76
+ http.set_debug_output(options[:debug_output])
77
+ end
78
+
79
+ return http
80
+ end
81
+
82
+ private
83
+ def ssl_implied?(uri)
84
+ uri.port == 443 || uri.instance_of?(URI::HTTPS)
85
+ end
86
+
87
+ def attach_ssl_certificates(http, options)
88
+ if http.use_ssl?
89
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
90
+
91
+ # Client certificate authentication
92
+ if options[:pem]
93
+ http.cert = OpenSSL::X509::Certificate.new(options[:pem])
94
+ http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
95
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
96
+ end
97
+
98
+ # SSL certificate authority file and/or directory
99
+ if options[:ssl_ca_file]
100
+ http.ca_file = options[:ssl_ca_file]
101
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
102
+ end
103
+
104
+ if options[:ssl_ca_path]
105
+ http.ca_path = options[:ssl_ca_path]
106
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
107
+ end
108
+
109
+ # This is only Ruby 1.9+
110
+ if options[:ssl_version] && http.respond_to?(:ssl_version=)
111
+ http.ssl_version = options[:ssl_version]
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -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,32 @@
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
+
10
+ unless defined?(Net::HTTP::Patch)
11
+ class Net::HTTP
12
+ def patch(path, data, initheader = nil, dest = nil, &block) #:nodoc:
13
+ res = nil
14
+ request(Patch.new(path, initheader), data) {|r|
15
+ r.read_body dest, &block
16
+ res = r
17
+ }
18
+ unless @newimpl
19
+ res.value
20
+ return res, res.body
21
+ end
22
+ res
23
+ end
24
+
25
+ class Patch < Net::HTTPRequest
26
+ METHOD = 'PATCH'
27
+ REQUEST_HAS_BODY = true
28
+ RESPONSE_HAS_BODY = true
29
+ end
30
+ end
31
+ end
32
+ 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,51 @@
1
+ module HTTParty
2
+ module HashConversions
3
+ # @return <String> This hash as a query string
4
+ #
5
+ # @example
6
+ # { :name => "Bob",
7
+ # :address => {
8
+ # :street => '111 Ruby Ave.',
9
+ # :city => 'Ruby Central',
10
+ # :phones => ['111-111-1111', '222-222-2222']
11
+ # }
12
+ # }.to_params
13
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
14
+ def self.to_params(hash)
15
+ params = hash.map { |k,v| normalize_param(k,v) }.join
16
+ params.chop! # trailing &
17
+ params
18
+ end
19
+
20
+ # @param key<Object> The key for the param.
21
+ # @param value<Object> The value for the param.
22
+ #
23
+ # @return <String> This key value pair as a param
24
+ #
25
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
26
+ def self.normalize_param(key, value)
27
+ param = ''
28
+ stack = []
29
+
30
+ if value.is_a?(Array)
31
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
32
+ elsif value.is_a?(Hash)
33
+ stack << [key,value]
34
+ else
35
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
36
+ end
37
+
38
+ stack.each do |parent, hash|
39
+ hash.each do |key, value|
40
+ if value.is_a?(Hash)
41
+ stack << ["#{parent}[#{key}]", value]
42
+ else
43
+ param << normalize_param("#{parent}[#{key}]", value)
44
+ end
45
+ end
46
+ end
47
+
48
+ param
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
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(h)
9
+ duplicate = h.dup
10
+ duplicate.each_pair do |k,v|
11
+ tv = duplicate[k]
12
+ duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? hash_deep_dup(tv) : v
13
+ end
14
+ duplicate
15
+ end
16
+
17
+ module ClassMethods #:nodoc:
18
+ def mattr_inheritable(*args)
19
+ @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
20
+ @mattr_inheritable_attrs += args
21
+ args.each do |arg|
22
+ module_eval %(class << self; attr_accessor :#{arg} end)
23
+ end
24
+ @mattr_inheritable_attrs
25
+ end
26
+
27
+ def inherited(subclass)
28
+ super
29
+ @mattr_inheritable_attrs.each do |inheritable_attribute|
30
+ ivar = "@#{inheritable_attribute}"
31
+ subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
32
+ if instance_variable_get(ivar).respond_to?(:merge)
33
+ method = <<-EOM
34
+ def self.#{inheritable_attribute}
35
+ #{ivar} = superclass.#{inheritable_attribute}.merge ModuleInheritableAttributes.hash_deep_dup(#{ivar})
36
+ end
37
+ EOM
38
+ subclass.class_eval method
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,84 @@
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 = [
24
+ %Q(Digest username="#{@username}"),
25
+ %Q(realm="#{@response['realm']}"),
26
+ %Q(nonce="#{@response['nonce']}"),
27
+ %Q(uri="#{@path}"),
28
+ %Q(response="#{request_digest}"),
29
+ ]
30
+
31
+ if qop_present?
32
+ fields = [
33
+ %Q(cnonce="#{@cnonce}"),
34
+ %Q(qop="#{@response['qop']}"),
35
+ %Q(nc="00000001")
36
+ ]
37
+ fields.each { |field| header << field }
38
+ end
39
+
40
+ header << %Q(opaque="#{@response['opaque']}") if opaque_present?
41
+ header
42
+ end
43
+
44
+ private
45
+
46
+ def parse(response_header)
47
+ response_header['www-authenticate'] =~ /^(\w+) (.*)/
48
+ params = {}
49
+ $2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
50
+ params
51
+ end
52
+
53
+ def opaque_present?
54
+ @response.has_key?('opaque') and not @response['opaque'].empty?
55
+ end
56
+
57
+ def qop_present?
58
+ @response.has_key?('qop') and not @response['qop'].empty?
59
+ end
60
+
61
+ def random
62
+ "%x" % (Time.now.to_i + rand(65535))
63
+ end
64
+
65
+ def request_digest
66
+ a = [md5(a1), @response['nonce'], md5(a2)]
67
+ a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
68
+ md5(a.join(":"))
69
+ end
70
+
71
+ def md5(str)
72
+ Digest::MD5.hexdigest(str)
73
+ end
74
+
75
+ def a1
76
+ [@username, @response['realm'], @password].join(":")
77
+ end
78
+
79
+ def a2
80
+ [@method, @path].join(":")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,145 @@
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, an empty string, spaces only or "null"
99
+ def parse
100
+ return nil if body.nil? || body.strip.empty? || body == "null"
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
+ # https://github.com/sferik/rails/commit/5e62670131dfa1718eaf21ff8dd3371395a5f1cc
116
+ if MultiJson.respond_to?(:adapter)
117
+ MultiJson.load(body)
118
+ else
119
+ MultiJson.decode(body)
120
+ end
121
+ end
122
+
123
+ def yaml
124
+ YAML.load(body)
125
+ end
126
+
127
+ def html
128
+ body
129
+ end
130
+
131
+ def plain
132
+ body
133
+ end
134
+
135
+ def supports_format?
136
+ self.class.supports_format?(format)
137
+ end
138
+
139
+ def parse_supported_format
140
+ send(format)
141
+ rescue NoMethodError => e
142
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
143
+ end
144
+ end
145
+ end