httparty 0.10.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of httparty might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +92 -0
  4. data/.rubocop_todo.yml +124 -0
  5. data/.simplecov +1 -0
  6. data/.travis.yml +5 -4
  7. data/CONTRIBUTING.md +23 -0
  8. data/Gemfile +9 -5
  9. data/Guardfile +3 -3
  10. data/History +109 -8
  11. data/README.md +21 -21
  12. data/Rakefile +5 -10
  13. data/bin/httparty +21 -14
  14. data/docs/README.md +100 -0
  15. data/examples/README.md +67 -0
  16. data/examples/aaws.rb +9 -9
  17. data/examples/basic.rb +6 -10
  18. data/examples/crack.rb +3 -3
  19. data/examples/custom_parsers.rb +1 -4
  20. data/examples/delicious.rb +12 -12
  21. data/examples/google.rb +2 -2
  22. data/examples/headers_and_user_agents.rb +2 -2
  23. data/examples/logging.rb +36 -0
  24. data/examples/nokogiri_html_parser.rb +0 -3
  25. data/examples/rescue_json.rb +17 -0
  26. data/examples/rubyurl.rb +3 -3
  27. data/examples/stackexchange.rb +24 -0
  28. data/examples/tripit_sign_in.rb +20 -9
  29. data/examples/twitter.rb +11 -11
  30. data/examples/whoismyrep.rb +2 -2
  31. data/features/command_line.feature +90 -2
  32. data/features/digest_authentication.feature +10 -0
  33. data/features/handles_compressed_responses.feature +8 -0
  34. data/features/handles_multiple_formats.feature +23 -0
  35. data/features/steps/env.rb +16 -11
  36. data/features/steps/httparty_response_steps.rb +40 -10
  37. data/features/steps/httparty_steps.rb +19 -3
  38. data/features/steps/mongrel_helper.rb +35 -2
  39. data/features/steps/remote_service_steps.rb +31 -8
  40. data/features/supports_read_timeout_option.feature +13 -0
  41. data/httparty.gemspec +9 -6
  42. data/lib/httparty/connection_adapter.rb +76 -11
  43. data/lib/httparty/cookie_hash.rb +3 -4
  44. data/lib/httparty/exceptions.rb +10 -4
  45. data/lib/httparty/hash_conversions.rb +19 -17
  46. data/lib/httparty/logger/apache_formatter.rb +22 -0
  47. data/lib/httparty/logger/curl_formatter.rb +91 -0
  48. data/lib/httparty/logger/logger.rb +26 -0
  49. data/lib/httparty/module_inheritable_attributes.rb +1 -1
  50. data/lib/httparty/net_digest_auth.rb +69 -18
  51. data/lib/httparty/parser.rb +15 -11
  52. data/lib/httparty/request.rb +186 -47
  53. data/lib/httparty/response/headers.rb +2 -2
  54. data/lib/httparty/response.rb +44 -9
  55. data/lib/httparty/version.rb +1 -1
  56. data/lib/httparty.rb +187 -65
  57. data/script/release +42 -0
  58. data/spec/fixtures/twitter.csv +2 -0
  59. data/spec/httparty/connection_adapter_spec.rb +334 -62
  60. data/spec/httparty/cookie_hash_spec.rb +53 -23
  61. data/spec/httparty/exception_spec.rb +45 -0
  62. data/spec/httparty/hash_conversions_spec.rb +49 -0
  63. data/spec/httparty/logger/apache_formatter_spec.rb +41 -0
  64. data/spec/httparty/logger/curl_formatter_spec.rb +119 -0
  65. data/spec/httparty/logger/logger_spec.rb +38 -0
  66. data/spec/httparty/net_digest_auth_spec.rb +148 -23
  67. data/spec/httparty/parser_spec.rb +48 -41
  68. data/spec/httparty/request_spec.rb +845 -151
  69. data/spec/httparty/response_spec.rb +147 -70
  70. data/spec/httparty/ssl_spec.rb +33 -21
  71. data/spec/httparty_spec.rb +337 -186
  72. data/spec/spec_helper.rb +38 -9
  73. data/spec/support/ssl_test_helper.rb +10 -10
  74. data/spec/support/ssl_test_server.rb +21 -21
  75. data/spec/support/stub_response.rb +20 -14
  76. data/website/index.html +3 -3
  77. metadata +46 -37
  78. data/lib/httparty/core_extensions.rb +0 -32
  79. data/spec/spec.opts +0 -2
@@ -18,3 +18,13 @@ Feature: Digest Authentication
18
18
  | username | password |
19
19
  | jcash | maninblack |
20
20
  Then the return value should match 'Digest Authenticated Page'
21
+
22
+ Scenario: Passing proper credentials to a page requiring Digest Authentication using md5-sess algorithm
23
+ Given a remote service that returns 'Digest Authenticated Page Using MD5-sess'
24
+ And that service is accessed at the path '/digest_auth.html'
25
+ And that service is protected by MD5-sess Digest Authentication
26
+ And that service requires the username 'jcash' with the password 'maninblack'
27
+ When I call HTTParty#get with '/digest_auth.html' and a digest_auth hash:
28
+ | username | password |
29
+ | jcash | maninblack |
30
+ Then the return value should match 'Digest Authenticated Page Using MD5-sess'
@@ -17,3 +17,11 @@ Feature: Handles Compressed Responses
17
17
  And that service is accessed at the path '/gzip_service.html'
18
18
  When I call HTTParty#get with '/gzip_service.html'
19
19
  Then the return value should match '<h1>Some HTML</h1>'
20
+
21
+ Scenario: Supports HEAD request with gzip encoding
22
+ Given a remote gzip service
23
+ And that service is accessed at the path '/gzip_head.gz.js'
24
+ When I call HTTParty#head with '/gzip_head.gz.js'
25
+ Then it should return a response with a 200 response code
26
+ Then it should return a response with a gzip content-encoding
27
+ Then it should return a response with a blank body
@@ -14,6 +14,21 @@ Feature: Handles Multiple Formats
14
14
  Then it should return a String
15
15
  And the return value should match '<h1>Some HTML</h1>'
16
16
 
17
+ Scenario: A CSV service
18
+ Given a remote service that returns:
19
+ """
20
+ "Last Name","Name"
21
+ "jennings","waylon"
22
+ "cash","johnny"
23
+ """
24
+ And that service is accessed at the path '/service.csv'
25
+ And the response from the service has a Content-Type of 'application/csv'
26
+ When I call HTTParty#get with '/service.csv'
27
+ Then it should return an Array equaling:
28
+ | Last Name | Name |
29
+ | jennings | waylon |
30
+ | cash | johnny |
31
+
17
32
  Scenario: A JSON service
18
33
  Given a remote service that returns '{ "jennings": "waylon", "cash": "johnny" }'
19
34
  And that service is accessed at the path '/service.json'
@@ -32,3 +47,11 @@ Feature: Handles Multiple Formats
32
47
  Then it should return a Hash equaling:
33
48
  | key | value |
34
49
  | singer | waylon jennings |
50
+
51
+ Scenario: A Javascript remote file
52
+ Given a remote service that returns '$(function() { alert("hi"); });'
53
+ And that service is accessed at the path '/service.js'
54
+ And the response from the service has a Content-Type of 'application/javascript'
55
+ When I call HTTParty#get with '/service.js'
56
+ Then it should return a String
57
+ And the return value should match '$(function() { alert("hi"); });'
@@ -1,22 +1,27 @@
1
1
  require 'mongrel'
2
2
  require './lib/httparty'
3
- require 'spec/expectations'
3
+ require 'rspec/expectations'
4
+ require 'aruba/cucumber'
4
5
 
5
- Before do
6
- def new_port
7
- server = TCPServer.new('0.0.0.0', nil)
8
- port = server.addr[1]
9
- ensure
10
- server.close
11
- end
12
-
13
- port = ENV["HTTPARTY_PORT"] || new_port
6
+ def run_server(port)
14
7
  @host_and_port = "0.0.0.0:#{port}"
15
8
  @server = Mongrel::HttpServer.new("0.0.0.0", port)
16
9
  @server.run
17
10
  @request_options = {}
18
11
  end
19
12
 
13
+ def new_port
14
+ server = TCPServer.new('0.0.0.0', nil)
15
+ port = server.addr[1]
16
+ ensure
17
+ server.close
18
+ end
19
+
20
+ Before('~@command_line') do
21
+ port = ENV["HTTPARTY_PORT"] || new_port
22
+ run_server(port)
23
+ end
24
+
20
25
  After do
21
- @server.stop
26
+ @server.stop if @server
22
27
  end
@@ -1,26 +1,56 @@
1
- Then /it should return an? (\w+)$/ do |class_string|
2
- @response_from_httparty.should be_an_instance_of(class_string.class)
1
+ # Not needed anymore in ruby 2.0, but needed to resolve constants
2
+ # in nested namespaces. This is taken from rails :)
3
+ def constantize(camel_cased_word)
4
+ names = camel_cased_word.split('::')
5
+ names.shift if names.empty? || names.first.empty?
6
+
7
+ constant = Object
8
+ names.each do |name|
9
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
10
+ end
11
+ constant
12
+ end
13
+
14
+ Then /it should return an? ([\w\:]+)$/ do |class_string|
15
+ expect(@response_from_httparty.parsed_response).to be_a(Object.const_get(class_string))
3
16
  end
4
17
 
5
18
  Then /the return value should match '(.*)'/ do |expected_text|
6
- @response_from_httparty.should eql(expected_text)
19
+ expect(@response_from_httparty.parsed_response).to eq(expected_text)
7
20
  end
8
21
 
9
22
  Then /it should return a Hash equaling:/ do |hash_table|
10
- @response_from_httparty.should be_an_instance_of(Hash)
11
- @response_from_httparty.keys.length.should eql(hash_table.rows.length)
23
+ expect(@response_from_httparty.parsed_response).to be_a(Hash)
24
+ expect(@response_from_httparty.keys.length).to eq(hash_table.rows.length)
12
25
  hash_table.hashes.each do |pair|
13
26
  key, value = pair["key"], pair["value"]
14
- @response_from_httparty.keys.should include(key)
15
- @response_from_httparty[key].should eql(value)
27
+ expect(@response_from_httparty.keys).to include(key)
28
+ expect(@response_from_httparty[key]).to eq(value)
16
29
  end
17
30
  end
18
31
 
32
+ Then /it should return an Array equaling:/ do |array|
33
+ expect(@response_from_httparty.parsed_response).to be_a(Array)
34
+ expect(@response_from_httparty.parsed_response).to eq(array.raw)
35
+ end
36
+
19
37
  Then /it should return a response with a (\d+) response code/ do |code|
20
- @response_from_httparty.code.should eql(code.to_i)
38
+ expect(@response_from_httparty.code).to eq(code.to_i)
39
+ end
40
+
41
+ Then /it should return a response with a (.*) content\-encoding$/ do |content_type|
42
+ expect(@response_from_httparty.headers['content-encoding']).to eq('gzip')
43
+ end
44
+
45
+ Then /it should return a response with a blank body$/ do
46
+ expect(@response_from_httparty.body).to be_nil
21
47
  end
22
48
 
23
49
  Then /it should raise (?:an|a) ([\w:]+) exception/ do |exception|
24
- @exception_from_httparty.should_not be_nil
25
- @exception_from_httparty.class.name.should eql(exception)
50
+ expect(@exception_from_httparty).to_not be_nil
51
+ expect(@exception_from_httparty).to be_a constantize(exception)
52
+ end
53
+
54
+ Then /it should not raise (?:an|a) ([\w:]+) exception/ do |exception|
55
+ expect(@exception_from_httparty).to be_nil
26
56
  end
@@ -2,6 +2,14 @@ When /^I set my HTTParty timeout option to (\d+)$/ do |timeout|
2
2
  @request_options[:timeout] = timeout.to_i
3
3
  end
4
4
 
5
+ When /^I set my HTTParty open_timeout option to (\d+)$/ do |timeout|
6
+ @request_options[:open_timeout] = timeout.to_i
7
+ end
8
+
9
+ When /^I set my HTTParty read_timeout option to (\d+)$/ do |timeout|
10
+ @request_options[:read_timeout] = timeout.to_i
11
+ end
12
+
5
13
  When /I call HTTParty#get with '(.*)'$/ do |url|
6
14
  begin
7
15
  @response_from_httparty = HTTParty.get("http://#{@host_and_port}#{url}", @request_options)
@@ -10,11 +18,19 @@ When /I call HTTParty#get with '(.*)'$/ do |url|
10
18
  end
11
19
  end
12
20
 
21
+ When /^I call HTTParty#head with '(.*)'$/ do |url|
22
+ begin
23
+ @response_from_httparty = HTTParty.head("http://#{@host_and_port}#{url}", @request_options)
24
+ rescue HTTParty::RedirectionTooDeep, Timeout::Error => e
25
+ @exception_from_httparty = e
26
+ end
27
+ end
28
+
13
29
  When /I call HTTParty#get with '(.*)' and a basic_auth hash:/ do |url, auth_table|
14
30
  h = auth_table.hashes.first
15
31
  @response_from_httparty = HTTParty.get(
16
32
  "http://#{@host_and_port}#{url}",
17
- :basic_auth => { :username => h["username"], :password => h["password"] }
33
+ basic_auth: { username: h["username"], password: h["password"] }
18
34
  )
19
35
  end
20
36
 
@@ -22,6 +38,6 @@ When /I call HTTParty#get with '(.*)' and a digest_auth hash:/ do |url, auth_tab
22
38
  h = auth_table.hashes.first
23
39
  @response_from_httparty = HTTParty.get(
24
40
  "http://#{@host_and_port}#{url}",
25
- :digest_auth => { :username => h["username"], :password => h["password"] }
41
+ digest_auth: { username: h["username"], password: h["password"] }
26
42
  )
27
- end
43
+ end
@@ -10,14 +10,14 @@ class BasicMongrelHandler < Mongrel::HttpHandler
10
10
  end
11
11
 
12
12
  def process(request, response)
13
- instance_eval &preprocessor if preprocessor
13
+ instance_eval(&preprocessor) if preprocessor
14
14
  reply_with(response, response_code, response_body)
15
15
  end
16
16
 
17
17
  def reply_with(response, code, response_body)
18
18
  response.start(code) do |head, body|
19
19
  head["Content-Type"] = content_type
20
- custom_headers.each { |k,v| head[k] = v }
20
+ custom_headers.each { |k, v| head[k] = v }
21
21
  body.write(response_body)
22
22
  end
23
23
  end
@@ -88,6 +88,39 @@ module DigestAuthentication
88
88
  end
89
89
  end
90
90
 
91
+ module DigestAuthenticationUsingMD5Sess
92
+ NONCE = 'nonce'
93
+ REALM = 'testrealm@host.com'
94
+ QOP = 'auth,auth-int'
95
+ def self.extended(base)
96
+ base.custom_headers["WWW-Authenticate"] = %(Digest realm="#{REALM}",qop="#{QOP}",algorithm="MD5-sess",nonce="#{NONCE}",opaque="opaque"')
97
+ end
98
+
99
+ def process(request, response)
100
+ if authorized?(request)
101
+ super
102
+ else
103
+ reply_with(response, 401, "Incorrect. You have 20 seconds to comply.")
104
+ end
105
+ end
106
+
107
+ def md5(str)
108
+ Digest::MD5.hexdigest(str)
109
+ end
110
+
111
+ def authorized?(request)
112
+ auth = request.params["HTTP_AUTHORIZATION"]
113
+ params = {}
114
+ auth.to_s.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 }
115
+ a1a = [@username,REALM,@password].join(':')
116
+ a1 = [md5(a1a),NONCE,params['cnonce'] ].join(':')
117
+ a2 = [ request.params["REQUEST_METHOD"], request.params["REQUEST_URI"] ] .join(':')
118
+ expected_response = md5( [md5(a1), NONCE, params['nc'], params['cnonce'], QOP, md5(a2)].join(':') )
119
+ expected_response == params['response']
120
+ end
121
+ end
122
+
123
+
91
124
  def new_mongrel_redirector(target_url, relative_path = false)
92
125
  target_url = "http://#{@host_and_port}#{target_url}" unless relative_path
93
126
  Mongrel::RedirectHandler.new(target_url)
@@ -1,6 +1,11 @@
1
1
  Given /a remote service that returns '(.*)'/ do |response_body|
2
2
  @handler = BasicMongrelHandler.new
3
- Given "the response from the service has a body of '#{response_body}'"
3
+ step "the response from the service has a body of '#{response_body}'"
4
+ end
5
+
6
+ Given /^a remote service that returns:$/ do |response_body|
7
+ @handler = BasicMongrelHandler.new
8
+ @handler.response_body = response_body
4
9
  end
5
10
 
6
11
  Given /a remote service that returns a (\d+) status code/ do |code|
@@ -12,15 +17,22 @@ Given /that service is accessed at the path '(.*)'/ do |path|
12
17
  @server.register(path, @handler)
13
18
  end
14
19
 
15
- Given /^that service takes (\d+) seconds to generate a response$/ do |time|
16
- @server_response_time = time.to_i
17
- @handler.preprocessor = Proc.new { sleep time.to_i }
20
+ Given /^that service takes (\d+) (.*) to generate a response$/ do |time, unit|
21
+ time = time.to_i
22
+ time *= 60 if unit =~ /minute/
23
+ @server_response_time = time
24
+ @handler.preprocessor = proc { sleep time }
18
25
  end
19
26
 
20
27
  Given /^a remote deflate service$/ do
21
28
  @handler = DeflateHandler.new
22
29
  end
23
30
 
31
+ Given /^a remote deflate service on port '(\d+)'/ do |port|
32
+ run_server(port)
33
+ @handler = DeflateHandler.new
34
+ end
35
+
24
36
  Given /^a remote gzip service$/ do
25
37
  @handler = GzipHandler.new
26
38
  end
@@ -45,16 +57,27 @@ Given /that service is protected by Digest Authentication/ do
45
57
  @handler.extend DigestAuthentication
46
58
  end
47
59
 
60
+ Given /that service is protected by MD5-sess Digest Authentication/ do
61
+ @handler.extend DigestAuthenticationUsingMD5Sess
62
+ end
63
+
48
64
  Given /that service requires the username '(.*)' with the password '(.*)'/ do |username, password|
49
65
  @handler.username = username
50
66
  @handler.password = password
51
67
  end
52
68
 
69
+ # customize aruba cucumber step
70
+ Then /^the output should contain '(.*)'$/ do |expected|
71
+ expect(all_commands.map(&:output).join("\n")).to match_output_string(expected)
72
+ end
73
+
53
74
  Given /a restricted page at '(.*)'/ do |url|
54
- Given "a remote service that returns 'A response I will never see'"
55
- And "that service is accessed at the path '#{url}'"
56
- And "that service is protected by Basic Authentication"
57
- And "that service requires the username 'something' with the password 'secret'"
75
+ steps "
76
+ Given a remote service that returns 'A response I will never see'
77
+ And that service is accessed at the path '#{url}'
78
+ And that service is protected by Basic Authentication
79
+ And that service requires the username 'something' with the password 'secret'
80
+ "
58
81
  end
59
82
 
60
83
  # This joins the server thread, and halts cucumber, so you can actually hit the
@@ -0,0 +1,13 @@
1
+ Feature: Supports the read timeout option
2
+ In order to handle inappropriately slow response times
3
+ As a developer
4
+ I want my request to raise an exception after my specified read_timeout as elapsed
5
+
6
+ Scenario: A long running response
7
+ Given a remote service that returns '<h1>Some HTML</h1>'
8
+ And that service is accessed at the path '/long_running_service.html'
9
+ And that service takes 2 seconds to generate a response
10
+ When I set my HTTParty read_timeout option to 1
11
+ And I call HTTParty#get with '/long_running_service.html'
12
+ Then it should raise a Timeout::Error exception
13
+ And I wait for the server to recover
data/httparty.gemspec CHANGED
@@ -1,24 +1,27 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
3
3
  require "httparty/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "httparty"
7
7
  s.version = HTTParty::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
+ s.licenses = ['MIT']
9
10
  s.authors = ["John Nunemaker", "Sandro Turriate"]
10
11
  s.email = ["nunemaker@gmail.com"]
11
12
  s.homepage = "http://jnunemaker.github.com/httparty"
12
- s.summary = %q{Makes http fun! Also, makes consuming restful web services dead easy.}
13
- s.description = %q{Makes http fun! Also, makes consuming restful web services dead easy.}
13
+ s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
+ s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
15
 
15
- s.add_dependency 'multi_json', "~> 1.0"
16
- s.add_dependency 'multi_xml'
16
+ s.required_ruby_version = '>= 1.9.3'
17
17
 
18
+ s.add_dependency 'multi_xml', ">= 0.5.2"
19
+
20
+ # If this line is removed, all hard partying will cease.
18
21
  s.post_install_message = "When you HTTParty, you must party hard!"
19
22
 
20
23
  s.files = `git ls-files`.split("\n")
21
24
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
23
26
  s.require_paths = ["lib"]
24
27
  end
@@ -38,18 +38,26 @@ module HTTParty
38
38
  # in the #options attribute. It is up to you to interpret them within your
39
39
  # connection adapter. Take a look at the implementation of
40
40
  # HTTParty::ConnectionAdapter#connection for examples of how they are used.
41
- # Something are probably interesting are as follows:
41
+ # Some things that are probably interesting are as follows:
42
42
  # * :+timeout+: timeout in seconds
43
+ # * :+open_timeout+: http connection open_timeout in seconds, overrides timeout if set
44
+ # * :+read_timeout+: http connection read_timeout in seconds, overrides timeout if set
43
45
  # * :+debug_output+: see HTTParty::ClassMethods.debug_output.
44
46
  # * :+pem+: contains pem data. see HTTParty::ClassMethods.pem.
47
+ # * :+verify+: verify the server’s certificate against the ca certificate.
48
+ # * :+verify_peer+: set to false to turn off server verification but still send client certificate
45
49
  # * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file.
46
50
  # * :+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
51
+ # * :+connection_adapter_options+: contains the hash you passed to HTTParty.connection_adapter when you configured your connection adapter
48
52
  class ConnectionAdapter
49
-
50
53
  # Private: Regex used to strip brackets from IPv6 URIs.
51
54
  StripIpv6BracketsRegex = /\A\[(.*)\]\z/
52
55
 
56
+ OPTION_DEFAULTS = {
57
+ verify: true,
58
+ verify_peer: true
59
+ }
60
+
53
61
  # Public
54
62
  def self.call(uri, options)
55
63
  new(uri, options).connection
@@ -57,16 +65,22 @@ module HTTParty
57
65
 
58
66
  attr_reader :uri, :options
59
67
 
60
- def initialize(uri, options={})
61
- raise ArgumentError, "uri must be a URI, not a #{uri.class}" unless uri.kind_of? URI
68
+ def initialize(uri, options = {})
69
+ uri_adapter = options[:uri_adapter] || URI
70
+ raise ArgumentError, "uri must be a #{uri_adapter}, not a #{uri.class}" unless uri.is_a? uri_adapter
62
71
 
63
72
  @uri = uri
64
- @options = options
73
+ @options = OPTION_DEFAULTS.merge(options)
65
74
  end
66
75
 
67
76
  def connection
68
77
  host = clean_host(uri.host)
69
- http = Net::HTTP.new(host, uri.port, options[:http_proxyaddr], options[:http_proxyport], options[:http_proxyuser], options[:http_proxypass])
78
+ port = uri.port || (uri.scheme == 'https' ? 443 : 80)
79
+ if options.key?(:http_proxyaddr)
80
+ http = Net::HTTP.new(host, port, options[:http_proxyaddr], options[:http_proxyport], options[:http_proxyuser], options[:http_proxypass])
81
+ else
82
+ http = Net::HTTP.new(host, port)
83
+ end
70
84
 
71
85
  http.use_ssl = ssl_implied?(uri)
72
86
 
@@ -77,6 +91,14 @@ module HTTParty
77
91
  http.read_timeout = options[:timeout]
78
92
  end
79
93
 
94
+ if options[:read_timeout] && (options[:read_timeout].is_a?(Integer) || options[:read_timeout].is_a?(Float))
95
+ http.read_timeout = options[:read_timeout]
96
+ end
97
+
98
+ if options[:open_timeout] && (options[:open_timeout].is_a?(Integer) || options[:open_timeout].is_a?(Float))
99
+ http.open_timeout = options[:open_timeout]
100
+ end
101
+
80
102
  if options[:debug_output]
81
103
  http.set_debug_output(options[:debug_output])
82
104
  end
@@ -85,7 +107,26 @@ module HTTParty
85
107
  http.ciphers = options[:ciphers]
86
108
  end
87
109
 
88
- return http
110
+ # Bind to a specific local address or port
111
+ #
112
+ # @see https://bugs.ruby-lang.org/issues/6617
113
+ if options[:local_host]
114
+ if RUBY_VERSION >= "2.0.0"
115
+ http.local_host = options[:local_host]
116
+ else
117
+ Kernel.warn("Warning: option :local_host requires Ruby version 2.0 or later")
118
+ end
119
+ end
120
+
121
+ if options[:local_port]
122
+ if RUBY_VERSION >= "2.0.0"
123
+ http.local_port = options[:local_port]
124
+ else
125
+ Kernel.warn("Warning: option :local_port requires Ruby version 2.0 or later")
126
+ end
127
+ end
128
+
129
+ http
89
130
  end
90
131
 
91
132
  private
@@ -99,18 +140,42 @@ module HTTParty
99
140
  end
100
141
 
101
142
  def ssl_implied?(uri)
102
- uri.port == 443 || uri.instance_of?(URI::HTTPS)
143
+ uri.port == 443 || uri.scheme == 'https'
144
+ end
145
+
146
+ def verify_ssl_certificate?
147
+ !(options[:verify] == false || options[:verify_peer] == false)
103
148
  end
104
149
 
105
150
  def attach_ssl_certificates(http, options)
106
151
  if http.use_ssl?
107
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
152
+ if options.fetch(:verify, true)
153
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
154
+ if options[:cert_store]
155
+ http.cert_store = options[:cert_store]
156
+ else
157
+ # Use the default cert store by default, i.e. system ca certs
158
+ http.cert_store = OpenSSL::X509::Store.new
159
+ http.cert_store.set_default_paths
160
+ end
161
+ else
162
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
163
+ end
108
164
 
109
165
  # Client certificate authentication
166
+ # Note: options[:pem] must contain the content of a PEM file having the private key appended
110
167
  if options[:pem]
111
168
  http.cert = OpenSSL::X509::Certificate.new(options[:pem])
112
169
  http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
113
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
170
+ http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
171
+ end
172
+
173
+ # PKCS12 client certificate authentication
174
+ if options[:p12]
175
+ p12 = OpenSSL::PKCS12.new(options[:p12], options[:p12_password])
176
+ http.cert = p12.certificate
177
+ http.key = p12.key
178
+ http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
114
179
  end
115
180
 
116
181
  # SSL certificate authority file and/or directory
@@ -1,6 +1,5 @@
1
1
  class HTTParty::CookieHash < Hash #:nodoc:
2
-
3
- CLIENT_COOKIES = %w{path expires domain path secure HTTPOnly}
2
+ CLIENT_COOKIES = %w(path expires domain path secure httponly)
4
3
 
5
4
  def add_cookies(value)
6
5
  case value
@@ -8,7 +7,7 @@ class HTTParty::CookieHash < Hash #:nodoc:
8
7
  merge!(value)
9
8
  when String
10
9
  value.split('; ').each do |cookie|
11
- array = cookie.split('=')
10
+ array = cookie.split('=', 2)
12
11
  self[array[0].to_sym] = array[1]
13
12
  end
14
13
  else
@@ -17,6 +16,6 @@ class HTTParty::CookieHash < Hash #:nodoc:
17
16
  end
18
17
 
19
18
  def to_cookie_string
20
- delete_if { |k, v| CLIENT_COOKIES.include?(k.to_s) }.collect { |k, v| "#{k}=#{v}" }.join("; ")
19
+ reject { |k, v| CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join("; ")
21
20
  end
22
21
  end
@@ -1,13 +1,16 @@
1
1
  module HTTParty
2
- # Exception raised when you attempt to set a non-existant format
3
- class UnsupportedFormat < StandardError; end
2
+ # @abstact Exceptions raised by HTTParty inherit from Error
3
+ class Error < StandardError; end
4
+
5
+ # Exception raised when you attempt to set a non-existent format
6
+ class UnsupportedFormat < Error; end
4
7
 
5
8
  # Exception raised when using a URI scheme other than HTTP or HTTPS
6
- class UnsupportedURIScheme < StandardError; end
9
+ class UnsupportedURIScheme < Error; end
7
10
 
8
11
  # @abstract Exceptions which inherit from ResponseError contain the Net::HTTP
9
12
  # response object accessible via the {#response} method.
10
- class ResponseError < StandardError
13
+ class ResponseError < Error
11
14
  # Returns the response of the last request
12
15
  # @return [Net::HTTPResponse] A subclass of Net::HTTPResponse, e.g.
13
16
  # Net::HTTPOK
@@ -23,4 +26,7 @@ module HTTParty
23
26
  # Exception that is raised when request has redirected too many times.
24
27
  # Calling {#response} returns the Net:HTTP response object.
25
28
  class RedirectionTooDeep < ResponseError; end
29
+
30
+ # Exception that is raised when request redirects and location header is present more than once
31
+ class DuplicateLocationHeader < ResponseError; end
26
32
  end
@@ -3,18 +3,16 @@ module HTTParty
3
3
  # @return <String> This hash as a query string
4
4
  #
5
5
  # @example
6
- # { :name => "Bob",
7
- # :address => {
8
- # :street => '111 Ruby Ave.',
9
- # :city => 'Ruby Central',
10
- # :phones => ['111-111-1111', '222-222-2222']
6
+ # { name: "Bob",
7
+ # address: {
8
+ # street: '111 Ruby Ave.',
9
+ # city: 'Ruby Central',
10
+ # phones: ['111-111-1111', '222-222-2222']
11
11
  # }
12
12
  # }.to_params
13
13
  # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
14
14
  def self.to_params(hash)
15
- params = hash.map { |k,v| normalize_param(k,v) }.join
16
- params.chop! # trailing &
17
- params
15
+ hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop
18
16
  end
19
17
 
20
18
  # @param key<Object> The key for the param.
@@ -27,20 +25,24 @@ module HTTParty
27
25
  param = ''
28
26
  stack = []
29
27
 
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]
28
+ if value.respond_to?(:to_ary)
29
+ param << if value.empty?
30
+ "#{key}[]=&"
31
+ else
32
+ value.to_ary.map { |element| normalize_param("#{key}[]", element) }.join
33
+ end
34
+ elsif value.respond_to?(:to_hash)
35
+ stack << [key, value.to_hash]
34
36
  else
35
- param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
37
+ param << "#{key}=#{ERB::Util.url_encode(value.to_s)}&"
36
38
  end
37
39
 
38
40
  stack.each do |parent, hash|
39
- hash.each do |key, value|
40
- if value.is_a?(Hash)
41
- stack << ["#{parent}[#{key}]", value]
41
+ hash.each do |k, v|
42
+ if v.respond_to?(:to_hash)
43
+ stack << ["#{parent}[#{k}]", v.to_hash]
42
44
  else
43
- param << normalize_param("#{parent}[#{key}]", value)
45
+ param << normalize_param("#{parent}[#{k}]", v)
44
46
  end
45
47
  end
46
48
  end