├── spec ├── fixtures │ ├── empty.xml │ ├── ssl │ │ ├── generated │ │ │ ├── 1fe462c2.0 │ │ │ ├── bogushost.crt │ │ │ ├── server.crt │ │ │ ├── selfsigned.crt │ │ │ ├── ca.key │ │ │ ├── server.key │ │ │ └── ca.crt │ │ ├── openssl-exts.cnf │ │ └── generate.sh │ ├── undefined_method_add_node_for_nil.xml │ ├── twitter.csv │ ├── google.html │ ├── delicious.xml │ └── twitter.json ├── httparty │ ├── exception_spec.rb │ ├── logger │ │ ├── logger_spec.rb │ │ ├── apache_formatter_spec.rb │ │ └── curl_formatter_spec.rb │ ├── hash_conversions_spec.rb │ ├── ssl_spec.rb │ ├── cookie_hash_spec.rb │ ├── parser_spec.rb │ ├── net_digest_auth_spec.rb │ ├── response_spec.rb │ └── connection_adapter_spec.rb ├── spec_helper.rb └── support │ ├── ssl_test_helper.rb │ ├── stub_response.rb │ └── ssl_test_server.rb ├── .simplecov ├── cucumber.yml ├── lib └── httparty │ ├── version.rb │ ├── cookie_hash.rb │ ├── response │ └── headers.rb │ ├── logger │ ├── logger.rb │ ├── apache_formatter.rb │ └── curl_formatter.rb │ ├── exceptions.rb │ ├── module_inheritable_attributes.rb │ ├── hash_conversions.rb │ ├── response.rb │ ├── net_digest_auth.rb │ ├── parser.rb │ ├── connection_adapter.rb │ └── request.rb ├── .gitignore ├── .travis.yml ├── Rakefile ├── examples ├── headers_and_user_agents.rb ├── whoismyrep.rb ├── rubyurl.rb ├── nokogiri_html_parser.rb ├── google.rb ├── crack.rb ├── stream_download.rb ├── stackexchange.rb ├── rescue_json.rb ├── basic.rb ├── twitter.rb ├── aaws.rb ├── logging.rb ├── tripit_sign_in.rb ├── delicious.rb ├── custom_parsers.rb └── README.md ├── Gemfile ├── Guardfile ├── features ├── steps │ ├── env.rb │ ├── httparty_steps.rb │ ├── httparty_response_steps.rb │ ├── remote_service_steps.rb │ └── mongrel_helper.rb ├── supports_timeout_option.feature ├── supports_read_timeout_option.feature ├── supports_redirection.feature ├── basic_authentication.feature ├── handles_compressed_responses.feature ├── deals_with_http_error_codes.feature ├── digest_authentication.feature ├── handles_multiple_formats.feature └── command_line.feature ├── script └── release ├── MIT-LICENSE ├── CONTRIBUTING.md ├── httparty.gemspec ├── docs └── README.md ├── website ├── css │ └── common.css └── index.html ├── .rubocop.yml ├── README.md ├── .rubocop_todo.yml └── bin └── httparty /spec/fixtures/empty.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start "test_frameworks" -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/1fe462c2.0: -------------------------------------------------------------------------------- 1 | ca.crt -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: features --format progress 2 | -------------------------------------------------------------------------------- /lib/httparty/version.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | VERSION = "0.14.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .DS_Store 3 | .yardoc/ 4 | doc/ 5 | tmp/ 6 | log/ 7 | pkg/ 8 | *.swp 9 | /.bundle 10 | .rvmrc 11 | coverage 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.8 5 | - 2.2.4 6 | - 2.3.0 7 | bundler_args: --without development 8 | before_install: gem install bundler 9 | -------------------------------------------------------------------------------- /spec/fixtures/undefined_method_add_node_for_nil.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rspec/core/rake_task' 3 | RSpec::Core::RakeTask.new(:spec) 4 | rescue LoadError 5 | end 6 | 7 | require 'cucumber/rake/task' 8 | Cucumber::Rake::Task.new(:features) 9 | 10 | task default: [:spec, :features] 11 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/openssl-exts.cnf: -------------------------------------------------------------------------------- 1 | [ca] 2 | basicConstraints=critical,CA:true 3 | subjectKeyIdentifier=hash 4 | authorityKeyIdentifier=keyid:always,issuer:always 5 | 6 | [cert] 7 | basicConstraints=critical,CA:false 8 | subjectKeyIdentifier=hash 9 | authorityKeyIdentifier=keyid,issuer 10 | -------------------------------------------------------------------------------- /spec/fixtures/twitter.csv: -------------------------------------------------------------------------------- 1 | "name","url","id","description","protected","screen_name","followers_count","profile_image_url","location" 2 | "Magic 8 Bot",,"17656026","ask me a question","false","magic8bot","90","http://s3.amazonaws.com/twitter_production/profile_images/65565851/8ball_large_normal.jpg", -------------------------------------------------------------------------------- /examples/headers_and_user_agents.rb: -------------------------------------------------------------------------------- 1 | # To send custom user agents to identify your application to a web service (or mask as a specific browser for testing), send "User-Agent" as a hash to headers as shown below. 2 | 3 | require 'httparty' 4 | 5 | APPLICATION_NAME = "Httparty" 6 | response = HTTParty.get('http://example.com', headers: {"User-Agent" => APPLICATION_NAME}) 7 | -------------------------------------------------------------------------------- /examples/whoismyrep.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Rep 6 | include HTTParty 7 | end 8 | 9 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php?zip=46544') 10 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', query: { zip: 46544 }) 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'rake' 5 | gem 'fakeweb', '~> 1.3' 6 | gem 'mongrel', '1.2.0.pre2' 7 | 8 | group :development do 9 | gem 'guard' 10 | gem 'guard-rspec' 11 | gem 'guard-bundler' 12 | end 13 | 14 | group :test do 15 | gem 'rspec', '~> 3.4' 16 | gem 'simplecov', require: false 17 | gem 'aruba' 18 | gem 'cucumber', '~> 2.3' 19 | end 20 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | rspec_options = { 2 | version: 1, 3 | all_after_pass: false, 4 | all_on_start: false 5 | } 6 | 7 | guard 'rspec', rspec_options do 8 | watch(%r{^spec/.+_spec\.rb$}) 9 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 10 | watch('spec/spec_helper.rb') { "spec" } 11 | end 12 | 13 | guard 'bundler' do 14 | watch('Gemfile') 15 | watch(/^.+\.gemspec/) 16 | end 17 | -------------------------------------------------------------------------------- /examples/rubyurl.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Rubyurl 6 | include HTTParty 7 | base_uri 'rubyurl.com' 8 | 9 | def self.shorten(website_url) 10 | post('/api/links.json', query: { link: { website_url: website_url } }) 11 | end 12 | end 13 | 14 | pp Rubyurl.shorten('http://istwitterdown.com/') 15 | -------------------------------------------------------------------------------- /examples/nokogiri_html_parser.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'nokogiri' 3 | 4 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require File.join(dir, 'httparty') 6 | require 'pp' 7 | 8 | class HtmlParserIncluded < HTTParty::Parser 9 | def html 10 | Nokogiri::HTML(body) 11 | end 12 | end 13 | 14 | class Page 15 | include HTTParty 16 | parser HtmlParserIncluded 17 | end 18 | 19 | pp Page.get('http://www.google.com') 20 | -------------------------------------------------------------------------------- /examples/google.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Google 6 | include HTTParty 7 | format :html 8 | end 9 | 10 | # google.com redirects to www.google.com so this is live test for redirection 11 | pp Google.get('http://google.com') 12 | 13 | puts '', '*' * 70, '' 14 | 15 | # check that ssl is requesting right 16 | pp Google.get('https://www.google.com') 17 | -------------------------------------------------------------------------------- /examples/crack.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'crack' 3 | 4 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require File.join(dir, 'httparty') 6 | require 'pp' 7 | 8 | class Rep 9 | include HTTParty 10 | 11 | parser( 12 | proc do |body, format| 13 | Crack::XML.parse(body) 14 | end 15 | ) 16 | end 17 | 18 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php?zip=46544') 19 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', query: { zip: 46544 }) 20 | -------------------------------------------------------------------------------- /features/steps/env.rb: -------------------------------------------------------------------------------- 1 | require 'mongrel' 2 | require './lib/httparty' 3 | require 'rspec/expectations' 4 | require 'aruba/cucumber' 5 | 6 | def run_server(port) 7 | @host_and_port = "0.0.0.0:#{port}" 8 | @server = Mongrel::HttpServer.new("0.0.0.0", port) 9 | @server.run 10 | @request_options = {} 11 | end 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 | 25 | After do 26 | @server.stop if @server 27 | end 28 | -------------------------------------------------------------------------------- /examples/stream_download.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | # download file linux-4.6.4.tar.xz without using the memory 6 | response = nil 7 | filename = "linux-4.6.4.tar.xz" 8 | url = "https://cdn.kernel.org/pub/linux/kernel/v4.x/#{filename}" 9 | 10 | File.open(filename, "w") do |file| 11 | response = HTTParty.get(url, stream_body: true) do |fragment| 12 | print "." 13 | file.write(fragment) 14 | end 15 | end 16 | puts 17 | 18 | pp "Success: #{response.success?}" 19 | pp File.stat(filename).inspect 20 | File.unlink(filename) 21 | -------------------------------------------------------------------------------- /lib/httparty/cookie_hash.rb: -------------------------------------------------------------------------------- 1 | class HTTParty::CookieHash < Hash #:nodoc: 2 | CLIENT_COOKIES = %w(path expires domain path secure httponly) 3 | 4 | def add_cookies(value) 5 | case value 6 | when Hash 7 | merge!(value) 8 | when String 9 | value.split('; ').each do |cookie| 10 | array = cookie.split('=', 2) 11 | self[array[0].to_sym] = array[1] 12 | end 13 | else 14 | raise "add_cookies only takes a Hash or a String" 15 | end 16 | end 17 | 18 | def to_cookie_string 19 | reject { |k, v| CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join("; ") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/stackexchange.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class StackExchange 6 | include HTTParty 7 | base_uri 'api.stackexchange.com' 8 | 9 | def initialize(service, page) 10 | @options = { query: { site: service, page: page } } 11 | end 12 | 13 | def questions 14 | self.class.get("/2.2/questions", @options) 15 | end 16 | 17 | def users 18 | self.class.get("/2.2/users", @options) 19 | end 20 | end 21 | 22 | stack_exchange = StackExchange.new("stackoverflow", 1) 23 | pp stack_exchange.questions 24 | pp stack_exchange.users 25 | -------------------------------------------------------------------------------- /features/supports_timeout_option.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports the 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 timeout as elapsed 5 | 6 | Scenario: A long running response 7 | Given a remote service that returns '

Some HTML

' 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 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 14 | -------------------------------------------------------------------------------- /features/supports_read_timeout_option.feature: -------------------------------------------------------------------------------- 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 '

Some HTML

' 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 14 | -------------------------------------------------------------------------------- /examples/rescue_json.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | 4 | # Take note of the "; 1" at the end of the following line. It's required only if 5 | # running this in IRB, because IRB will try to inspect the variable named 6 | # "request", triggering the exception. 7 | request = HTTParty.get 'https://rubygems.org/api/v1/versions/doesnotexist.json' ; 1 8 | 9 | # Check an exception due to parsing the response 10 | # because HTTParty evaluate the response lazily 11 | begin 12 | request.inspect 13 | # This would also suffice by forcing the request to be parsed: 14 | # request.parsed_response 15 | rescue => e 16 | puts "Rescued #{e.inspect}" 17 | end 18 | -------------------------------------------------------------------------------- /lib/httparty/response/headers.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | class Response #:nodoc: 3 | class Headers 4 | include ::Net::HTTPHeader 5 | 6 | def initialize(header = {}) 7 | @header = header 8 | end 9 | 10 | def ==(other) 11 | @header == other 12 | end 13 | 14 | def inspect 15 | @header.inspect 16 | end 17 | 18 | def method_missing(name, *args, &block) 19 | if @header.respond_to?(name) 20 | @header.send(name, *args, &block) 21 | else 22 | super 23 | end 24 | end 25 | 26 | def respond_to?(method, include_all = false) 27 | super || @header.respond_to?(method, include_all) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/httparty/logger/logger.rb: -------------------------------------------------------------------------------- 1 | require 'httparty/logger/apache_formatter' 2 | require 'httparty/logger/curl_formatter' 3 | 4 | module HTTParty 5 | module Logger 6 | def self.formatters 7 | @formatters ||= { 8 | :curl => Logger::CurlFormatter, 9 | :apache => Logger::ApacheFormatter 10 | } 11 | end 12 | 13 | def self.add_formatter(name, formatter) 14 | raise HTTParty::Error.new("Log Formatter with name #{name} already exists") if formatters.include?(name) 15 | formatters.merge!(name.to_sym => formatter) 16 | end 17 | 18 | def self.build(logger, level, formatter) 19 | level ||= :info 20 | formatter ||= :apache 21 | 22 | logger_klass = formatters[formatter] || Logger::ApacheFormatter 23 | logger_klass.new(logger, level) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/bogushost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICBTCCAW6gAwIBAgIBATANBgkqhkiG9w0BAQUFADAuMSwwKgYDVQQDEyNJTlNF 3 | Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0xMDEwMjAxMzQ2MjNa 4 | GA80NzQ4MDkxNTEzNDYyM1owDzENMAsGA1UEAxMEYm9nbzCBnzANBgkqhkiG9w0B 5 | AQEFAAOBjQAwgYkCgYEAr6b0ZBrRrVvPmPbQv36Jnj5jv00ZkhimXrmbv9Z1AdIZ 6 | WSsBpMd8TP7exE5OR5/DaxKmiZqVskgRyRkLm52/Dkt7Ncrzr5I3unHnMqsAv/28 7 | 5fGlYoRxnkCGMse/6NOFgCemRFw/bglxPNAGrFYKStameBRbCm0dCgtlvcwzdf8C 8 | AwEAAaNQME4wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUddLPFtGmb0aFWbTl2kAo 9 | xD+fd6kwHwYDVR0jBBgwFoAUy0Lz6RgmtpywlBOXdPABQArp358wDQYJKoZIhvcN 10 | AQEFBQADgYEAosqpPVsFu6cOIhGFT85Y1wwRUaihO0vWO7ghBU5ScuRU3tuvyJDZ 11 | Z/HoAMXV6XZjVZzRosjtPjFbyWkZYjUqJJRMyEaRiGArWe6urKLzwnD6R9O3eNa5 12 | 7bgFhzZ5WBldJmtq4A3oNqBuvgZkYM6NVKvS4UoakkTliHB21/mDOSY= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICCjCCAXOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAuMSwwKgYDVQQDEyNJTlNF 3 | Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0xMDEwMjAxMzQ2MjNa 4 | GA80NzQ4MDkxNTEzNDYyM1owFDESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqG 5 | SIb3DQEBAQUAA4GNADCBiQKBgQCvpvRkGtGtW8+Y9tC/fomePmO/TRmSGKZeuZu/ 6 | 1nUB0hlZKwGkx3xM/t7ETk5Hn8NrEqaJmpWySBHJGQubnb8OS3s1yvOvkje6cecy 7 | qwC//bzl8aVihHGeQIYyx7/o04WAJ6ZEXD9uCXE80AasVgpK1qZ4FFsKbR0KC2W9 8 | zDN1/wIDAQABo1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBR10s8W0aZvRoVZ 9 | tOXaQCjEP593qTAfBgNVHSMEGDAWgBTLQvPpGCa2nLCUE5d08AFACunfnzANBgkq 10 | hkiG9w0BAQUFAAOBgQCR4Oor0YAvK0tNFrOLtqmC6D0F5IYCyu7komk7JGn9L4nn 11 | 7VyVxd4MXdc1r1v+WP5JtnA9ZjMmEmH9gl4gwR/Cu+TMkArsq0Z8mREOLNL8pwpx 12 | Zxgk0CwacYR9RQcpuJ9nSDzVoO5ecYkb5C9q7gwgqbmCzr7oz/rwTqRwiUZCVQ== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /lib/httparty/logger/apache_formatter.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | module Logger 3 | class ApacheFormatter #:nodoc: 4 | TAG_NAME = HTTParty.name 5 | 6 | attr_accessor :level, :logger, :current_time 7 | 8 | def initialize(logger, level) 9 | @logger = logger 10 | @level = level.to_sym 11 | end 12 | 13 | def format(request, response) 14 | current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S %z") 15 | http_method = request.http_method.name.split("::").last.upcase 16 | path = request.path.to_s 17 | content_length = response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length'] 18 | @logger.send @level, "[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} " 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICHTCCAYagAwIBAgIJALT/G+ylQljIMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV 3 | BAMTCWxvY2FsaG9zdDAgFw0xMDEwMjAxMzQ2MjNaGA80NzQ4MDkxNTEzNDYyM1ow 4 | FDESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 5 | gQCvpvRkGtGtW8+Y9tC/fomePmO/TRmSGKZeuZu/1nUB0hlZKwGkx3xM/t7ETk5H 6 | n8NrEqaJmpWySBHJGQubnb8OS3s1yvOvkje6cecyqwC//bzl8aVihHGeQIYyx7/o 7 | 04WAJ6ZEXD9uCXE80AasVgpK1qZ4FFsKbR0KC2W9zDN1/wIDAQABo3UwczAdBgNV 8 | HQ4EFgQUddLPFtGmb0aFWbTl2kAoxD+fd6kwRAYDVR0jBD0wO4AUddLPFtGmb0aF 9 | WbTl2kAoxD+fd6mhGKQWMBQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJALT/G+ylQljI 10 | MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAlOCBO54S88mD3VYviER6 11 | V+lkd7iWmdas2wUUDeMKA9CxnirWi7ne2U7wQH/5FJ1j3ImSfjb4h/98xiVJE84e 12 | Ld7mb61g/M4g4b62kt0HK8/cGUxfuz5zwIfi28qJq3ow6AFEq1fywbJvUAnnamwU 13 | cZF/qoVfJhus2mXjYc4hFWg= 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | # You can also use post, put, delete, head, options in the same fashion 6 | response = HTTParty.get('https://api.stackexchange.com/2.2/questions?site=stackoverflow') 7 | puts response.body, response.code, response.message, response.headers.inspect 8 | 9 | # An example post to a minimal rails app in the development environment 10 | # Note that "skip_before_filter :verify_authenticity_token" must be set in the 11 | # "pears" controller for this example 12 | 13 | class Partay 14 | include HTTParty 15 | base_uri 'http://localhost:3000' 16 | end 17 | 18 | options = { 19 | body: { 20 | pear: { # your resource 21 | foo: '123', # your columns/data 22 | bar: 'second', 23 | baz: 'last thing' 24 | } 25 | } 26 | } 27 | 28 | pp Partay.post('/pears.xml', options) 29 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDeWQFx3fnaqIgjOqfK+8mPn1zHV2fMzKOe5orZxEfjbSsF+6Qs 3 | TYxs5Wv2dir/sJ2OpekTy4UAX5RNIgroxZn9uJ2SvbF39638J7alUhXEf6u3eknP 4 | fBmELcdjP4dTgIPaIj3ADOxvqYxXuZ3V+OVh+KvhIEYY9ORypQlSnb+1AwIDAQAB 5 | AoGBAL147VFCDlM1gGU865V+wIFCFQbNxedwjxGuda4io/v6oEoF6R3Tq5F0Y27v 6 | va6Lq4fOe/LhYGI0EKU2GEPJd3F2wA21r+81InPKAkqYI5CDQtKDDNLviur8ZVKF 7 | i3UzutjeYoCqmWeHaKPD6w5DtqeBieem7LTWRyXlFtHZV/nBAkEA8nsMOSd1+JTm 8 | ZT4HDsEFQrN8mIFUUioFSHPut2CwzvTEW+hTkLQiog3bua4n7uQOFImR63X9qMsh 9 | IjZRJQNmowJBAOq+mQdnRWYKl0SYb++Eb3uW6L4h1zsW375+caKo9omtpeqDW/y0 10 | BWyY0q4DPkm3yU26Yr+b2JijISrml9/8PiECQQDHuXyG8y7jktn3GFE94NURbL+6 11 | 6gPnLX9ufzdoSjc4MDowrbtvHEDOlHWgioeP6L6EQhA0DtrhlnbzNCRARX3bAkEA 12 | jQOsF+dwqAjKr/lGnMKY2cxgyf64NZXbGKsKhmUrnK9E0SjR9G8MJx1yyffGzi/q 13 | bJf/xAzRw3eTcBsPtwznIQJAHq5MOK7oaUuO+6cbsZYpOYOOkKIvDLiOtdSr7LTI 14 | DziH/fpzB0VhCmFhhEQwHhlB4t3m66A9TelHmhrCDsIaLA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQCvpvRkGtGtW8+Y9tC/fomePmO/TRmSGKZeuZu/1nUB0hlZKwGk 3 | x3xM/t7ETk5Hn8NrEqaJmpWySBHJGQubnb8OS3s1yvOvkje6cecyqwC//bzl8aVi 4 | hHGeQIYyx7/o04WAJ6ZEXD9uCXE80AasVgpK1qZ4FFsKbR0KC2W9zDN1/wIDAQAB 5 | AoGALIdgkTgTS6VovVhklwcXEBy04LxE7Tp+gqj/COTvCKUgc/BpHELOCh7ajl1j 6 | jti7i5tQyLV9mZKXn6lPvgWBd0w+p6VhM4NFA97CoodEJm2ckFC9zUABCh9dOpbm 7 | 8KzF7hdpYWgJJchwwZ60tbcP7K1DkiNX6Kk9qKQEWvitMBECQQDpOSzzLldcEU9l 8 | ze/nG2+rf6ecaPnKeafY8R2qVils8I7ZJAW3+0bNT5gQs7rT7aWo8vMvrXq++lWb 9 | JkNV6hK9AkEAwM5wsmg7REmAaDwgUBq5mNt963/uG2ihAODFS70lYT23UYl5Y3rD 10 | s3qU4ntG4DvWIQgPdwdstzDh9fMBVXa1awJBAID1WoOE5k1ETRDP1I2HwDGmPnng 11 | Ge75YfQ1LuAXEITqZzJuFrNqv/Waw0zI9M9moqlO3WVJmYusRFWrzKPe8EkCQEwC 12 | FlN+275z63csHOD3aCtmfCGW8VtEyBP8iErvagkHt3khZQVepD/hF0ihqLNFY4jq 13 | EI6wEp+1WZ8ICYKTpbkCQQDhl5QLdy5Xo3k3agCnB9nktSzs2iqFvsGvfOAW4628 14 | iThKTNua6bBvbdiG0Vh2Sv0XBYVJoHB3WnTVgFyPJaaF 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICbTCCAdagAwIBAgIJAIAeO9TXtJ45MA0GCSqGSIb3DQEBBQUAMC4xLDAqBgNV 3 | BAMTI0lOU0VDVVJFIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MCAXDTEwMTAy 4 | MDEzNDYyM1oYDzQ3NDgwOTE1MTM0NjIzWjAuMSwwKgYDVQQDEyNJTlNFQ1VSRSBU 5 | ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw 6 | gYkCgYEA3lkBcd352qiIIzqnyvvJj59cx1dnzMyjnuaK2cRH420rBfukLE2MbOVr 7 | 9nYq/7CdjqXpE8uFAF+UTSIK6MWZ/bidkr2xd/et/Ce2pVIVxH+rt3pJz3wZhC3H 8 | Yz+HU4CD2iI9wAzsb6mMV7md1fjlYfir4SBGGPTkcqUJUp2/tQMCAwEAAaOBkDCB 9 | jTAdBgNVHQ4EFgQUy0Lz6RgmtpywlBOXdPABQArp358wXgYDVR0jBFcwVYAUy0Lz 10 | 6RgmtpywlBOXdPABQArp35+hMqQwMC4xLDAqBgNVBAMTI0lOU0VDVVJFIFRlc3Qg 11 | Q2VydGlmaWNhdGUgQXV0aG9yaXR5ggkAgB471Ne0njkwDAYDVR0TBAUwAwEB/zAN 12 | BgkqhkiG9w0BAQUFAAOBgQCmi3JQm+EIWjkRlyz9sijkYS+Ps4opmd/weeaXwa4E 13 | gVBWJGyiduB+kBnfv61+/tDjlrbjBDH5dP8suczHQL8gox4zGgjw64KH4o1ujZYR 14 | cEPbhnUpwbXu7yItlajBZfpFefjF5P0Ao2iEzQldDy0D6nQ19h5QANvQxqweTPQp 15 | pw== 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /features/supports_redirection.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports Redirection 2 | 3 | As a developer 4 | I want to work with services that may redirect me 5 | And I want it to follow a reasonable number of redirects 6 | Because sometimes web services do that 7 | 8 | Scenario: A service that redirects once 9 | Given a remote service that returns 'Service Response' 10 | And that service is accessed at the path '/landing_service.html' 11 | And the url '/redirector.html' redirects to '/landing_service.html' 12 | When I call HTTParty#get with '/redirector.html' 13 | Then the return value should match 'Service Response' 14 | 15 | # TODO: Look in to why this actually fails... 16 | Scenario: A service that redirects to a relative URL 17 | 18 | Scenario: A service that redirects infinitely 19 | Given the url '/first.html' redirects to '/second.html' 20 | And the url '/second.html' redirects to '/first.html' 21 | When I call HTTParty#get with '/first.html' 22 | Then it should raise an HTTParty::RedirectionTooDeep exception 23 | -------------------------------------------------------------------------------- /features/basic_authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Authentication 2 | 3 | As a developer 4 | I want to be able to use a service that requires Basic Authentication 5 | Because that is not an uncommon requirement 6 | 7 | Scenario: Passing no credentials to a page requiring Basic Authentication 8 | Given a restricted page at '/basic_auth.html' 9 | When I call HTTParty#get with '/basic_auth.html' 10 | Then it should return a response with a 401 response code 11 | 12 | Scenario: Passing proper credentials to a page requiring Basic Authentication 13 | Given a remote service that returns 'Authenticated Page' 14 | And that service is accessed at the path '/basic_auth.html' 15 | And that service is protected by Basic Authentication 16 | And that service requires the username 'jcash' with the password 'maninblack' 17 | When I call HTTParty#get with '/basic_auth.html' and a basic_auth hash: 18 | | username | password | 19 | | jcash | maninblack | 20 | Then the return value should match 'Authenticated Page' 21 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: release 3 | #/ 4 | #/ Tag the version in the repo and push the gem. 5 | #/ 6 | 7 | set -e 8 | cd $(dirname "$0")/.. 9 | 10 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 11 | grep '^#/' <"$0"| cut -c4- 12 | exit 0 13 | } 14 | 15 | gem_name=httparty 16 | 17 | # Build a new gem archive. 18 | rm -rf $gem_name-*.gem 19 | gem build -q $gem_name.gemspec 20 | 21 | # Make sure we're on the master branch. 22 | (git branch | grep -q '* master') || { 23 | echo "Only release from the master branch." 24 | exit 1 25 | } 26 | 27 | # Figure out what version we're releasing. 28 | tag=v`ls $gem_name-*.gem | sed "s/^$gem_name-\(.*\)\.gem$/\1/"` 29 | 30 | echo "Releasing $tag" 31 | 32 | # Make sure we haven't released this version before. 33 | git fetch -t origin 34 | 35 | (git tag -l | grep -q "$tag") && { 36 | echo "Whoops, there's already a '${tag}' tag." 37 | exit 1 38 | } 39 | 40 | # Tag it and bag it. 41 | gem push $gem_name-*.gem && git tag "$tag" && 42 | git push origin master && git push origin "$tag" 43 | -------------------------------------------------------------------------------- /examples/twitter.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | config = YAML.load(File.read(File.join(ENV['HOME'], '.twitter'))) 5 | 6 | class Twitter 7 | include HTTParty 8 | base_uri 'twitter.com' 9 | 10 | def initialize(u, p) 11 | @auth = {username: u, password: p} 12 | end 13 | 14 | # which can be :friends, :user or :public 15 | # options[:query] can be things like since, since_id, count, etc. 16 | def timeline(which = :friends, options = {}) 17 | options.merge!({ basic_auth: @auth }) 18 | self.class.get("/statuses/#{which}_timeline.json", options) 19 | end 20 | 21 | def post(text) 22 | options = { query: { status: text }, basic_auth: @auth } 23 | self.class.post('/statuses/update.json', options) 24 | end 25 | end 26 | 27 | twitter = Twitter.new(config['email'], config['password']) 28 | pp twitter.timeline 29 | # pp twitter.timeline(:friends, query: {since_id: 868482746}) 30 | # pp twitter.timeline(:friends, query: 'since_id=868482746') 31 | # pp twitter.post('this is a test of 0.2.0') 32 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 John Nunemaker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * Contributions will not be accepted without tests. 4 | * Please post unconfirmed bugs to the mailing list first: https://groups.google.com/forum/#!forum/httparty-gem 5 | * Don't change the version. The maintainers will handle that when they release. 6 | * Always provide as much information and reproducibility as possible when filing an issue or submitting a pull request. 7 | 8 | ## Workflow 9 | 10 | * Fork the project. 11 | * Run `bundle` 12 | * Run `bundle exec rake` 13 | * Make your feature addition or bug fix. 14 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 15 | * Run `bundle exec rake` (No, REALLY :)) 16 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull) 17 | * Send me a pull request. Bonus points for topic branches. 18 | 19 | ## Help and Docs 20 | 21 | * https://groups.google.com/forum/#!forum/httparty-gem 22 | * http://stackoverflow.com/questions/tagged/httparty 23 | * http://rdoc.info/projects/jnunemaker/httparty 24 | -------------------------------------------------------------------------------- /examples/aaws.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_support' 3 | 4 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require File.join(dir, 'httparty') 6 | require 'pp' 7 | config = YAML.load(File.read(File.join(ENV['HOME'], '.aaws'))) 8 | 9 | module AAWS 10 | class Book 11 | include HTTParty 12 | base_uri 'http://ecs.amazonaws.com' 13 | default_params Service: 'AWSECommerceService', Operation: 'ItemSearch', SearchIndex: 'Books' 14 | 15 | def initialize(key) 16 | self.class.default_params AWSAccessKeyId: key 17 | end 18 | 19 | def search(options = {}) 20 | raise ArgumentError, 'You must search for something' if options[:query].blank? 21 | 22 | # amazon uses nasty camelized query params 23 | options[:query] = options[:query].inject({}) { |h, q| h[q[0].to_s.camelize] = q[1]; h } 24 | 25 | # make a request and return the items (NOTE: this doesn't handle errors at this point) 26 | self.class.get('/onca/xml', options)['ItemSearchResponse']['Items'] 27 | end 28 | end 29 | end 30 | 31 | aaws = AAWS::Book.new(config[:access_key]) 32 | pp aaws.search(query: { title: 'Ruby On Rails' }) 33 | -------------------------------------------------------------------------------- /httparty.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH.push File.expand_path("../lib", __FILE__) 3 | require "httparty/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "httparty" 7 | s.version = HTTParty::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.licenses = ['MIT'] 10 | s.authors = ["John Nunemaker", "Sandro Turriate"] 11 | s.email = ["nunemaker@gmail.com"] 12 | s.homepage = "http://jnunemaker.github.com/httparty" 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.' 15 | 16 | s.required_ruby_version = '>= 2.0.0' 17 | 18 | s.add_dependency 'multi_xml', ">= 0.5.2" 19 | 20 | # If this line is removed, all hard partying will cease. 21 | s.post_install_message = "When you HTTParty, you must party hard!" 22 | 23 | s.files = `git ls-files`.split("\n") 24 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 25 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 26 | s.require_paths = ["lib"] 27 | end 28 | -------------------------------------------------------------------------------- /examples/logging.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'logger' 4 | require 'pp' 5 | 6 | my_logger = Logger.new "httparty.log" 7 | 8 | my_logger.info "Logging can be used on the main HTTParty class. It logs redirects too." 9 | HTTParty.get "http://google.com", logger: my_logger 10 | 11 | my_logger.info '*' * 70 12 | 13 | my_logger.info "It can be used also on a custom class." 14 | 15 | class Google 16 | include HTTParty 17 | logger ::Logger.new "httparty.log" 18 | end 19 | 20 | Google.get "http://google.com" 21 | 22 | my_logger.info '*' * 70 23 | 24 | my_logger.info "The default formatter is :apache. The :curl formatter can also be used." 25 | my_logger.info "You can tell wich method to call on the logger too. It is info by default." 26 | HTTParty.get "http://google.com", logger: my_logger, log_level: :debug, log_format: :curl 27 | 28 | my_logger.info '*' * 70 29 | 30 | my_logger.info "These configs are also available on custom classes." 31 | class Google 32 | include HTTParty 33 | logger ::Logger.new("httparty.log"), :debug, :curl 34 | end 35 | 36 | Google.get "http://google.com" 37 | -------------------------------------------------------------------------------- /features/handles_compressed_responses.feature: -------------------------------------------------------------------------------- 1 | Feature: Handles Compressed Responses 2 | 3 | In order to save bandwidth 4 | As a developer 5 | I want to uncompress compressed responses 6 | 7 | Scenario: Supports deflate encoding 8 | Given a remote deflate service 9 | And the response from the service has a body of '

Some HTML

' 10 | And that service is accessed at the path '/deflate_service.html' 11 | When I call HTTParty#get with '/deflate_service.html' 12 | Then the return value should match '

Some HTML

' 13 | 14 | Scenario: Supports gzip encoding 15 | Given a remote gzip service 16 | And the response from the service has a body of '

Some HTML

' 17 | And that service is accessed at the path '/gzip_service.html' 18 | When I call HTTParty#get with '/gzip_service.html' 19 | Then the return value should match '

Some HTML

' 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 28 | -------------------------------------------------------------------------------- /features/deals_with_http_error_codes.feature: -------------------------------------------------------------------------------- 1 | Feature: Deals with HTTP error codes 2 | 3 | As a developer 4 | I want to be informed of non-successful responses 5 | Because sometimes thing explode 6 | And I should probably know what happened 7 | 8 | Scenario: A response of '404 - Not Found' 9 | Given a remote service that returns a 404 status code 10 | And that service is accessed at the path '/404_service.html' 11 | When I call HTTParty#get with '/404_service.html' 12 | Then it should return a response with a 404 response code 13 | 14 | Scenario: A response of '500 - Internal Server Error' 15 | Given a remote service that returns a 500 status code 16 | And that service is accessed at the path '/500_service.html' 17 | When I call HTTParty#get with '/500_service.html' 18 | Then it should return a response with a 500 response code 19 | 20 | Scenario: A non-successful response where I need the body 21 | Given a remote service that returns a 400 status code 22 | And the response from the service has a body of 'Bad response' 23 | And that service is accessed at the path '/400_service.html' 24 | When I call HTTParty#get with '/400_service.html' 25 | Then it should return a response with a 400 response code 26 | And the return value should match 'Bad response' 27 | -------------------------------------------------------------------------------- /examples/tripit_sign_in.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | 4 | class TripIt 5 | include HTTParty 6 | base_uri 'https://www.tripit.com' 7 | debug_output 8 | 9 | def initialize(email, password) 10 | @email = email 11 | get_response = self.class.get('/account/login') 12 | get_response_cookie = parse_cookie(get_response.headers['Set-Cookie']) 13 | 14 | post_response = self.class.post( 15 | '/account/login', 16 | body: { 17 | login_email_address: email, 18 | login_password: password 19 | }, 20 | headers: {'Cookie' => get_response_cookie.to_cookie_string } 21 | ) 22 | 23 | @cookie = parse_cookie(post_response.headers['Set-Cookie']) 24 | end 25 | 26 | def account_settings 27 | self.class.get('/account/edit', headers: { 'Cookie' => @cookie.to_cookie_string }) 28 | end 29 | 30 | def logged_in? 31 | account_settings.include? "You're logged in as #{@email}" 32 | end 33 | 34 | private 35 | 36 | def parse_cookie(resp) 37 | cookie_hash = CookieHash.new 38 | resp.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) } 39 | cookie_hash 40 | end 41 | end 42 | 43 | tripit = TripIt.new('email', 'password') 44 | puts "Logged in: #{tripit.logged_in?}" 45 | -------------------------------------------------------------------------------- /examples/delicious.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | config = YAML.load(File.read(File.join(ENV['HOME'], '.delicious'))) 5 | 6 | class Delicious 7 | include HTTParty 8 | base_uri 'https://api.del.icio.us/v1' 9 | 10 | def initialize(u, p) 11 | @auth = { username: u, password: p } 12 | end 13 | 14 | # query params that filter the posts are: 15 | # tag (optional). Filter by this tag. 16 | # dt (optional). Filter by this date (CCYY-MM-DDThh:mm:ssZ). 17 | # url (optional). Filter by this url. 18 | # ie: posts(query: {tag: 'ruby'}) 19 | def posts(options = {}) 20 | options.merge!({ basic_auth: @auth }) 21 | self.class.get('/posts/get', options) 22 | end 23 | 24 | # query params that filter the posts are: 25 | # tag (optional). Filter by this tag. 26 | # count (optional). Number of items to retrieve (Default:15, Maximum:100). 27 | def recent(options = {}) 28 | options.merge!({ basic_auth: @auth }) 29 | self.class.get('/posts/recent', options) 30 | end 31 | end 32 | 33 | delicious = Delicious.new(config['username'], config['password']) 34 | pp delicious.posts(query: { tag: 'ruby' }) 35 | pp delicious.recent 36 | 37 | delicious.recent['posts']['post'].each { |post| puts post['href'] } 38 | -------------------------------------------------------------------------------- /lib/httparty/exceptions.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 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 7 | 8 | # Exception raised when using a URI scheme other than HTTP or HTTPS 9 | class UnsupportedURIScheme < Error; end 10 | 11 | # @abstract Exceptions which inherit from ResponseError contain the Net::HTTP 12 | # response object accessible via the {#response} method. 13 | class ResponseError < Error 14 | # Returns the response of the last request 15 | # @return [Net::HTTPResponse] A subclass of Net::HTTPResponse, e.g. 16 | # Net::HTTPOK 17 | attr_reader :response 18 | 19 | # Instantiate an instance of ResponseError with a Net::HTTPResponse object 20 | # @param [Net::HTTPResponse] 21 | def initialize(response) 22 | @response = response 23 | end 24 | end 25 | 26 | # Exception that is raised when request has redirected too many times. 27 | # Calling {#response} returns the Net:HTTP response object. 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 32 | end 33 | -------------------------------------------------------------------------------- /examples/custom_parsers.rb: -------------------------------------------------------------------------------- 1 | class ParseAtom 2 | include HTTParty 3 | 4 | # Support Atom along with the default parsers: xml, json, etc. 5 | class Parser::Atom < HTTParty::Parser 6 | SupportedFormats.merge!({"application/atom+xml" => :atom}) 7 | 8 | protected 9 | 10 | # perform atom parsing on body 11 | def atom 12 | body.to_atom 13 | end 14 | end 15 | 16 | parser Parser::Atom 17 | end 18 | 19 | class OnlyParseAtom 20 | include HTTParty 21 | 22 | # Only support Atom 23 | class Parser::OnlyAtom < HTTParty::Parser 24 | SupportedFormats = { "application/atom+xml" => :atom } 25 | 26 | protected 27 | 28 | # perform atom parsing on body 29 | def atom 30 | body.to_atom 31 | end 32 | end 33 | 34 | parser Parser::OnlyAtom 35 | end 36 | 37 | class SkipParsing 38 | include HTTParty 39 | 40 | # Parse the response body however you like 41 | class Parser::Simple < HTTParty::Parser 42 | def parse 43 | body 44 | end 45 | end 46 | 47 | parser Parser::Simple 48 | end 49 | 50 | class AdHocParsing 51 | include HTTParty 52 | parser( 53 | proc do |body, format| 54 | case format 55 | when :json 56 | body.to_json 57 | when :xml 58 | body.to_xml 59 | else 60 | body 61 | end 62 | end 63 | ) 64 | end 65 | -------------------------------------------------------------------------------- /spec/httparty/exception_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Error do 4 | subject { described_class } 5 | 6 | describe '#ancestors' do 7 | subject { super().ancestors } 8 | it { is_expected.to include(StandardError) } 9 | end 10 | 11 | describe HTTParty::UnsupportedFormat do 12 | describe '#ancestors' do 13 | subject { super().ancestors } 14 | it { is_expected.to include(HTTParty::Error) } 15 | end 16 | end 17 | 18 | describe HTTParty::UnsupportedURIScheme do 19 | describe '#ancestors' do 20 | subject { super().ancestors } 21 | it { is_expected.to include(HTTParty::Error) } 22 | end 23 | end 24 | 25 | describe HTTParty::ResponseError do 26 | describe '#ancestors' do 27 | subject { super().ancestors } 28 | it { is_expected.to include(HTTParty::Error) } 29 | end 30 | end 31 | 32 | describe HTTParty::RedirectionTooDeep do 33 | describe '#ancestors' do 34 | subject { super().ancestors } 35 | it { is_expected.to include(HTTParty::ResponseError) } 36 | end 37 | end 38 | 39 | describe HTTParty::DuplicateLocationHeader do 40 | describe '#ancestors' do 41 | subject { super().ancestors } 42 | it { is_expected.to include(HTTParty::ResponseError) } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d "generated" ] ; then 5 | echo >&2 "error: 'generated' directory already exists. Delete it first." 6 | exit 1 7 | fi 8 | 9 | mkdir generated 10 | 11 | # Generate the CA private key and certificate 12 | openssl req -batch -subj '/CN=INSECURE Test Certificate Authority' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/ca.key -nodes -out generated/ca.crt 13 | 14 | # Create symlinks for ssl_ca_path 15 | c_rehash generated 16 | 17 | # Generate the server private key and self-signed certificate 18 | openssl req -batch -subj '/CN=localhost' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/server.key -nodes -out generated/selfsigned.crt 19 | 20 | # Generate certificate signing request with bogus hostname 21 | openssl req -batch -subj '/CN=bogo' -new -days 999999 -key generated/server.key -nodes -out generated/bogushost.csr 22 | 23 | # Sign the certificate requests 24 | openssl x509 -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/selfsigned.crt -out generated/server.crt -clrext -extfile openssl-exts.cnf -extensions cert -days 999999 25 | openssl x509 -req -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/bogushost.csr -out generated/bogushost.crt -clrext -extfile openssl-exts.cnf -extensions cert -days 999999 26 | 27 | # Remove certificate signing requests 28 | rm -f generated/*.csr 29 | 30 | -------------------------------------------------------------------------------- /spec/httparty/logger/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Logger do 4 | describe ".build" do 5 | subject { HTTParty::Logger } 6 | 7 | it "defaults level to :info" do 8 | logger_double = double 9 | expect(subject.build(logger_double, nil, nil).level).to eq(:info) 10 | end 11 | 12 | it "defaults format to :apache" do 13 | logger_double = double 14 | expect(subject.build(logger_double, nil, nil)).to be_an_instance_of(HTTParty::Logger::ApacheFormatter) 15 | end 16 | 17 | it "builds :curl style logger" do 18 | logger_double = double 19 | expect(subject.build(logger_double, nil, :curl)).to be_an_instance_of(HTTParty::Logger::CurlFormatter) 20 | end 21 | 22 | it "builds :custom style logger" do 23 | CustomFormatter = Class.new(HTTParty::Logger::CurlFormatter) 24 | HTTParty::Logger.add_formatter(:custom, CustomFormatter) 25 | 26 | logger_double = double 27 | expect(subject.build(logger_double, nil, :custom)). 28 | to be_an_instance_of(CustomFormatter) 29 | end 30 | it "raises error when formatter exists" do 31 | CustomFormatter2= Class.new(HTTParty::Logger::CurlFormatter) 32 | HTTParty::Logger.add_formatter(:custom2, CustomFormatter2) 33 | 34 | expect{ HTTParty::Logger.add_formatter(:custom2, CustomFormatter2) }. 35 | to raise_error HTTParty::Error 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | require "httparty" 5 | require "fakeweb" 6 | 7 | def file_fixture(filename) 8 | open(File.join(File.dirname(__FILE__), 'fixtures', "#{filename}")).read 9 | end 10 | 11 | Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each {|f| require f} 12 | 13 | RSpec.configure do |config| 14 | config.include HTTParty::StubResponse 15 | config.include HTTParty::SSLTestHelper 16 | 17 | config.before(:suite) do 18 | FakeWeb.allow_net_connect = false 19 | end 20 | 21 | config.after(:suite) do 22 | FakeWeb.allow_net_connect = true 23 | end 24 | 25 | config.expect_with :rspec do |expectations| 26 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 27 | end 28 | 29 | config.mock_with :rspec do |mocks| 30 | mocks.verify_partial_doubles = false 31 | end 32 | 33 | config.filter_run :focus 34 | config.run_all_when_everything_filtered = true 35 | 36 | config.disable_monkey_patching! 37 | 38 | config.warnings = true 39 | 40 | if config.files_to_run.one? 41 | config.default_formatter = 'doc' 42 | end 43 | 44 | config.profile_examples = 10 45 | 46 | config.order = :random 47 | 48 | Kernel.srand config.seed 49 | end 50 | 51 | RSpec::Matchers.define :use_ssl do 52 | match(&:use_ssl?) 53 | end 54 | 55 | RSpec::Matchers.define :use_cert_store do |cert_store| 56 | match do |connection| 57 | connection.cert_store == cert_store 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /features/steps/httparty_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I set my HTTParty timeout option to (\d+)$/ do |timeout| 2 | @request_options[:timeout] = timeout.to_i 3 | end 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 | 13 | When /I call HTTParty#get with '(.*)'$/ do |url| 14 | begin 15 | @response_from_httparty = HTTParty.get("http://#{@host_and_port}#{url}", @request_options) 16 | rescue HTTParty::RedirectionTooDeep, Timeout::Error => e 17 | @exception_from_httparty = e 18 | end 19 | end 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 | 29 | When /I call HTTParty#get with '(.*)' and a basic_auth hash:/ do |url, auth_table| 30 | h = auth_table.hashes.first 31 | @response_from_httparty = HTTParty.get( 32 | "http://#{@host_and_port}#{url}", 33 | basic_auth: { username: h["username"], password: h["password"] } 34 | ) 35 | end 36 | 37 | When /I call HTTParty#get with '(.*)' and a digest_auth hash:/ do |url, auth_table| 38 | h = auth_table.hashes.first 39 | @response_from_httparty = HTTParty.get( 40 | "http://#{@host_and_port}#{url}", 41 | digest_auth: { username: h["username"], password: h["password"] } 42 | ) 43 | end 44 | -------------------------------------------------------------------------------- /spec/httparty/hash_conversions_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe HTTParty::HashConversions do 2 | describe ".to_params" do 3 | it "creates a params string from a hash" do 4 | hash = { 5 | name: "bob", 6 | address: { 7 | street: '111 ruby ave.', 8 | city: 'ruby central', 9 | phones: ['111-111-1111', '222-222-2222'] 10 | } 11 | } 12 | expect(HTTParty::HashConversions.to_params(hash)).to eq("name=bob&address[street]=111%20ruby%20ave.&address[city]=ruby%20central&address[phones][]=111-111-1111&address[phones][]=222-222-2222") 13 | end 14 | end 15 | 16 | describe ".normalize_param" do 17 | context "value is an array" do 18 | it "creates a params string" do 19 | expect( 20 | HTTParty::HashConversions.normalize_param(:people, ["Bob Jones", "Mike Smith"]) 21 | ).to eq("people[]=Bob%20Jones&people[]=Mike%20Smith&") 22 | end 23 | end 24 | 25 | context "value is an empty array" do 26 | it "creates a params string" do 27 | expect( 28 | HTTParty::HashConversions.normalize_param(:people, []) 29 | ).to eq("people[]=&") 30 | end 31 | end 32 | 33 | context "value is hash" do 34 | it "creates a params string" do 35 | expect( 36 | HTTParty::HashConversions.normalize_param(:person, { name: "Bob Jones" }) 37 | ).to eq("person[name]=Bob%20Jones&") 38 | end 39 | end 40 | 41 | context "value is a string" do 42 | it "creates a params string" do 43 | expect( 44 | HTTParty::HashConversions.normalize_param(:name, "Bob Jones") 45 | ).to eq("name=Bob%20Jones&") 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/support/ssl_test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module HTTParty 4 | module SSLTestHelper 5 | def ssl_verify_test(mode, ca_basename, server_cert_filename, options = {}) 6 | options = { 7 | format: :json, 8 | timeout: 30 9 | }.merge(options) 10 | 11 | if mode 12 | ca_path = File.expand_path("../../fixtures/ssl/generated/#{ca_basename}", __FILE__) 13 | raise ArgumentError.new("#{ca_path} does not exist") unless File.exist?(ca_path) 14 | options[mode] = ca_path 15 | end 16 | 17 | begin 18 | test_server = SSLTestServer.new( 19 | rsa_key: File.read(File.expand_path("../../fixtures/ssl/generated/server.key", __FILE__)), 20 | cert: File.read(File.expand_path("../../fixtures/ssl/generated/#{server_cert_filename}", __FILE__))) 21 | 22 | test_server.start 23 | 24 | if mode 25 | ca_path = File.expand_path("../../fixtures/ssl/generated/#{ca_basename}", __FILE__) 26 | raise ArgumentError.new("#{ca_path} does not exist") unless File.exist?(ca_path) 27 | return HTTParty.get("https://localhost:#{test_server.port}/", options) 28 | else 29 | return HTTParty.get("https://localhost:#{test_server.port}/", options) 30 | end 31 | ensure 32 | test_server.stop if test_server 33 | end 34 | 35 | test_server = SSLTestServer.new({ 36 | rsa_key: path.join('server.key').read, 37 | cert: path.join(server_cert_filename).read 38 | }) 39 | 40 | test_server.start 41 | 42 | HTTParty.get("https://localhost:#{test_server.port}/", options) 43 | ensure 44 | test_server.stop if test_server 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /features/digest_authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Digest Authentication 2 | 3 | As a developer 4 | I want to be able to use a service that requires Digest Authentication 5 | Because that is not an uncommon requirement 6 | 7 | Scenario: Passing no credentials to a page requiring Digest Authentication 8 | Given a restricted page at '/digest_auth.html' 9 | When I call HTTParty#get with '/digest_auth.html' 10 | Then it should return a response with a 401 response code 11 | 12 | Scenario: Passing proper credentials to a page requiring Digest Authentication 13 | Given a remote service that returns 'Digest Authenticated Page' 14 | And that service is accessed at the path '/digest_auth.html' 15 | And that service is protected by Digest Authentication 16 | And that service requires the username 'jcash' with the password 'maninblack' 17 | When I call HTTParty#get with '/digest_auth.html' and a digest_auth hash: 18 | | username | password | 19 | | jcash | maninblack | 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' 31 | -------------------------------------------------------------------------------- /spec/httparty/logger/apache_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Logger::ApacheFormatter do 4 | let(:subject) { described_class.new(logger_double, :info) } 5 | let(:logger_double) { double('Logger') } 6 | let(:request_double) { double('Request', http_method: Net::HTTP::Get, path: "http://my.domain.com/my_path") } 7 | let(:request_time) { Time.new.strftime("%Y-%m-%d %H:%M:%S %z") } 8 | 9 | before do 10 | subject.current_time = request_time 11 | expect(logger_double).to receive(:info).with(log_message) 12 | end 13 | 14 | describe "#format" do 15 | let(:log_message) { "[HTTParty] [#{request_time}] 302 \"GET http://my.domain.com/my_path\" - " } 16 | 17 | it "formats a response in a style that resembles apache's access log" do 18 | response_double = double( 19 | code: 302, 20 | :[] => nil 21 | ) 22 | 23 | subject.format(request_double, response_double) 24 | end 25 | 26 | context 'when there is a parsed response' do 27 | let(:log_message) { "[HTTParty] [#{request_time}] 200 \"GET http://my.domain.com/my_path\" 512 "} 28 | 29 | it "can handle the Content-Length header" do 30 | # Simulate a parsed response that is an array, where accessing a string key will raise an error. See Issue #299. 31 | response_double = double( 32 | code: 200, 33 | headers: { 'Content-Length' => 512 } 34 | ) 35 | allow(response_double).to receive(:[]).with('Content-Length').and_raise(TypeError.new('no implicit conversion of String into Integer')) 36 | 37 | subject.format(request_double, response_double) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/httparty/module_inheritable_attributes.rb: -------------------------------------------------------------------------------- 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(hash) 9 | duplicate = hash.dup 10 | 11 | duplicate.each_pair do |key, value| 12 | duplicate[key] = if value.is_a?(Hash) 13 | hash_deep_dup(value) 14 | elsif value.is_a?(Proc) 15 | duplicate[key] = value.dup 16 | else 17 | value 18 | end 19 | end 20 | 21 | duplicate 22 | end 23 | 24 | module ClassMethods #:nodoc: 25 | def mattr_inheritable(*args) 26 | @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs] 27 | @mattr_inheritable_attrs += args 28 | 29 | args.each do |arg| 30 | module_eval %(class << self; attr_accessor :#{arg} end) 31 | end 32 | 33 | @mattr_inheritable_attrs 34 | end 35 | 36 | def inherited(subclass) 37 | super 38 | @mattr_inheritable_attrs.each do |inheritable_attribute| 39 | ivar = "@#{inheritable_attribute}" 40 | subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone) 41 | 42 | if instance_variable_get(ivar).respond_to?(:merge) 43 | method = <<-EOM 44 | def self.#{inheritable_attribute} 45 | duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar}) 46 | #{ivar} = superclass.#{inheritable_attribute}.merge(duplicate) 47 | end 48 | EOM 49 | 50 | subclass.class_eval method 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/httparty/hash_conversions.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module HTTParty 4 | module HashConversions 5 | # @return This hash as a query string 6 | # 7 | # @example 8 | # { name: "Bob", 9 | # address: { 10 | # street: '111 Ruby Ave.', 11 | # city: 'Ruby Central', 12 | # phones: ['111-111-1111', '222-222-2222'] 13 | # } 14 | # }.to_params 15 | # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave." 16 | def self.to_params(hash) 17 | hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop 18 | end 19 | 20 | # @param key The key for the param. 21 | # @param value The value for the param. 22 | # 23 | # @return 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.respond_to?(:to_ary) 31 | param << if value.empty? 32 | "#{key}[]=&" 33 | else 34 | value.to_ary.map { |element| normalize_param("#{key}[]", element) }.join 35 | end 36 | elsif value.respond_to?(:to_hash) 37 | stack << [key, value.to_hash] 38 | else 39 | param << "#{key}=#{ERB::Util.url_encode(value.to_s)}&" 40 | end 41 | 42 | stack.each do |parent, hash| 43 | hash.each do |k, v| 44 | if v.respond_to?(:to_hash) 45 | stack << ["#{parent}[#{k}]", v.to_hash] 46 | else 47 | param << normalize_param("#{parent}[#{k}]", v) 48 | end 49 | end 50 | end 51 | 52 | param 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/stub_response.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | module StubResponse 3 | def stub_http_response_with(filename) 4 | format = filename.split('.').last.intern 5 | data = file_fixture(filename) 6 | 7 | response = Net::HTTPOK.new("1.1", 200, "Content for you") 8 | allow(response).to receive(:body).and_return(data) 9 | 10 | http_request = HTTParty::Request.new(Net::HTTP::Get, 'http://localhost', format: format) 11 | allow(http_request).to receive_message_chain(:http, :request).and_return(response) 12 | 13 | expect(HTTParty::Request).to receive(:new).and_return(http_request) 14 | end 15 | 16 | def stub_chunked_http_response_with(chunks, options = {format: "html"}) 17 | response = Net::HTTPResponse.new("1.1", 200, nil) 18 | allow(response).to receive(:chunked_data).and_return(chunks) 19 | def response.read_body(&block) 20 | @body || chunked_data.each(&block) 21 | end 22 | 23 | http_request = HTTParty::Request.new(Net::HTTP::Get, 'http://localhost', options) 24 | allow(http_request).to receive_message_chain(:http, :request).and_yield(response).and_return(response) 25 | 26 | expect(HTTParty::Request).to receive(:new).and_return(http_request) 27 | end 28 | 29 | def stub_response(body, code = '200') 30 | code = code.to_s 31 | @request.options[:base_uri] ||= 'http://localhost' 32 | unless defined?(@http) && @http 33 | @http = Net::HTTP.new('localhost', 80) 34 | allow(@request).to receive(:http).and_return(@http) 35 | end 36 | 37 | # CODE_TO_OBJ currently missing 308 38 | if code == '308' 39 | response = Net::HTTPRedirection.new("1.1", code, body) 40 | else 41 | response = Net::HTTPResponse::CODE_TO_OBJ[code].new("1.1", code, body) 42 | end 43 | allow(response).to receive(:body).and_return(body) 44 | 45 | allow(@http).to receive(:request).and_return(response) 46 | response 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /features/steps/httparty_response_steps.rb: -------------------------------------------------------------------------------- 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)) 16 | end 17 | 18 | Then /the return value should match '(.*)'/ do |expected_text| 19 | expect(@response_from_httparty.parsed_response).to eq(expected_text) 20 | end 21 | 22 | Then /it should return a Hash equaling:/ do |hash_table| 23 | expect(@response_from_httparty.parsed_response).to be_a(Hash) 24 | expect(@response_from_httparty.keys.length).to eq(hash_table.rows.length) 25 | hash_table.hashes.each do |pair| 26 | key, value = pair["key"], pair["value"] 27 | expect(@response_from_httparty.keys).to include(key) 28 | expect(@response_from_httparty[key]).to eq(value) 29 | end 30 | end 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 | 37 | Then /it should return a response with a (\d+) response code/ do |code| 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 47 | end 48 | 49 | Then /it should raise (?:an|a) ([\w:]+) exception/ do |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 56 | end 57 | -------------------------------------------------------------------------------- /spec/support/ssl_test_server.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'socket' 3 | require 'thread' 4 | 5 | # NOTE: This code is garbage. It probably has deadlocks, it might leak 6 | # threads, and otherwise cause problems in a real system. It's really only 7 | # intended for testing HTTParty. 8 | class SSLTestServer 9 | attr_accessor :ctx # SSLContext object 10 | attr_reader :port 11 | 12 | def initialize(options = {}) 13 | @ctx = OpenSSL::SSL::SSLContext.new 14 | @ctx.cert = OpenSSL::X509::Certificate.new(options[:cert]) 15 | @ctx.key = OpenSSL::PKey::RSA.new(options[:rsa_key]) 16 | @ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # Don't verify client certificate 17 | @port = options[:port] || 0 18 | @thread = nil 19 | @stopping_mutex = Mutex.new 20 | @stopping = false 21 | end 22 | 23 | def start 24 | @raw_server = TCPServer.new(@port) 25 | 26 | if @port == 0 27 | @port = Socket.getnameinfo(@raw_server.getsockname, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)[1].to_i 28 | end 29 | 30 | @ssl_server = OpenSSL::SSL::SSLServer.new(@raw_server, @ctx) 31 | 32 | @stopping_mutex.synchronize { 33 | return if @stopping 34 | @thread = Thread.new { thread_main } 35 | } 36 | 37 | nil 38 | end 39 | 40 | def stop 41 | @stopping_mutex.synchronize { 42 | return if @stopping 43 | @stopping = true 44 | } 45 | @thread.join 46 | end 47 | 48 | private 49 | 50 | def thread_main 51 | until @stopping_mutex.synchronize { @stopping } 52 | (rr, _, _) = select([@ssl_server.to_io], nil, nil, 0.1) 53 | 54 | next unless rr && rr.include?(@ssl_server.to_io) 55 | 56 | socket = @ssl_server.accept 57 | 58 | Thread.new { 59 | header = [] 60 | 61 | until (line = socket.readline).rstrip.empty? 62 | header << line 63 | end 64 | 65 | response = < 0 59 | 60 | log OUT, 'Headers: ' 61 | log_hash request.options[:headers] 62 | end 63 | 64 | def log_query 65 | return unless request.options[:query] 66 | 67 | log OUT, 'Query: ' 68 | log_hash request.options[:query] 69 | end 70 | 71 | def log_response_headers 72 | headers = response.respond_to?(:headers) ? response.headers : response 73 | response.each_header do |response_header| 74 | log IN, "#{response_header.capitalize}: #{headers[response_header]}" 75 | end 76 | end 77 | 78 | def log_hash(hash) 79 | hash.each { |k, v| log(OUT, "#{k}: #{v}") } 80 | end 81 | 82 | def log(direction, line = '') 83 | messages << "[#{TAG_NAME}] [#{time}] #{direction} #{line}" 84 | end 85 | 86 | def time 87 | @time ||= Time.now.strftime("%Y-%m-%d %H:%M:%S %z") 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /features/handles_multiple_formats.feature: -------------------------------------------------------------------------------- 1 | Feature: Handles Multiple Formats 2 | 3 | As a developer 4 | I want to be able to consume remote services of many different formats 5 | And I want those formats to be automatically detected and handled 6 | Because web services take many forms 7 | And I don't want to have to do any extra work 8 | 9 | Scenario: An HTML service 10 | Given a remote service that returns '

Some HTML

' 11 | And that service is accessed at the path '/html_service.html' 12 | And the response from the service has a Content-Type of 'text/html' 13 | When I call HTTParty#get with '/html_service.html' 14 | Then it should return a String 15 | And the return value should match '

Some HTML

' 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 | 32 | Scenario: A JSON service 33 | Given a remote service that returns '{ "jennings": "waylon", "cash": "johnny" }' 34 | And that service is accessed at the path '/service.json' 35 | And the response from the service has a Content-Type of 'application/json' 36 | When I call HTTParty#get with '/service.json' 37 | Then it should return a Hash equaling: 38 | | key | value | 39 | | jennings | waylon | 40 | | cash | johnny | 41 | 42 | Scenario: An XML Service 43 | Given a remote service that returns 'waylon jennings' 44 | And that service is accessed at the path '/service.xml' 45 | And the response from the service has a Content-Type of 'text/xml' 46 | When I call HTTParty#get with '/service.xml' 47 | Then it should return a Hash equaling: 48 | | key | value | 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"); });' 58 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTTParty by John Nunemaker 6 | 7 | 8 | 9 | 10 |
11 | 21 | 22 |
23 |

Install

24 |
$ sudo gem install httparty
25 | 26 |

Some Quick Examples

27 | 28 |

The following is a simple example of wrapping Twitter's API for posting updates.

29 | 30 |
class Twitter
31 |   include HTTParty
32 |   base_uri 'twitter.com'
33 |   basic_auth 'username', 'password'
34 | end
35 | 
36 | Twitter.post('/statuses/update.json', query: {status: "It's an HTTParty and everyone is invited!"})
37 | 38 |

That is really it! The object returned is a ruby hash that is decoded from Twitter's json response. JSON parsing is used because of the .json extension in the path of the request. You can also explicitly set a format (see the examples).

39 | 40 |

That works and all but what if you don't want to embed your username and password in the class? Below is an example to fix that:

41 | 42 |
class Twitter
43 |   include HTTParty
44 |   base_uri 'twitter.com'
45 | 
46 |   def initialize(u, p)
47 |     @auth = {username: u, password: p}
48 |   end
49 | 
50 |   def post(text)
51 |     options = { query: {status: text}, basic_auth: @auth }
52 |     self.class.post('/statuses/update.json', options)
53 |   end
54 | end
55 | 
56 | Twitter.new('username', 'password').post("It's an HTTParty and everyone is invited!")
57 | 58 |

More Examples: There are several examples in the gem itself.

59 | 60 |

Support

61 |

Conversations welcome in the google group and bugs/features over at Github.

62 | 63 | 64 |
65 | 66 | 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/httparty/response.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | class Response < BasicObject 3 | def self.underscore(string) 4 | string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase 5 | end 6 | 7 | attr_reader :request, :response, :body, :headers 8 | 9 | def initialize(request, response, parsed_block, options = {}) 10 | @request = request 11 | @response = response 12 | @body = options[:body] || response.body 13 | @parsed_block = parsed_block 14 | @headers = Headers.new(response.to_hash) 15 | 16 | if request.options[:logger] 17 | logger = ::HTTParty::Logger.build(request.options[:logger], request.options[:log_level], request.options[:log_format]) 18 | logger.format(request, self) 19 | end 20 | 21 | throw_exception 22 | end 23 | 24 | def parsed_response 25 | @parsed_response ||= @parsed_block.call 26 | end 27 | 28 | def class 29 | Response 30 | end 31 | 32 | def is_a?(klass) 33 | self.class == klass || self.class < klass 34 | end 35 | 36 | alias_method :kind_of?, :is_a? 37 | 38 | def code 39 | response.code.to_i 40 | end 41 | 42 | def tap 43 | yield self 44 | self 45 | end 46 | 47 | def inspect 48 | inspect_id = ::Kernel::format "%x", (object_id * 2) 49 | %(#<#{self.class}:0x#{inspect_id} parsed_response=#{parsed_response.inspect}, @response=#{response.inspect}, @headers=#{headers.inspect}>) 50 | end 51 | 52 | RESPOND_TO_METHODS = [:request, :response, :parsed_response, :body, :headers] 53 | 54 | CODES_TO_OBJ = ::Net::HTTPResponse::CODE_CLASS_TO_OBJ.merge ::Net::HTTPResponse::CODE_TO_OBJ 55 | 56 | CODES_TO_OBJ.each do |response_code, klass| 57 | name = klass.name.sub("Net::HTTP", '') 58 | name = "#{underscore(name)}?".to_sym 59 | 60 | RESPOND_TO_METHODS << name 61 | 62 | define_method(name) do 63 | klass === response 64 | end 65 | end 66 | 67 | # Support old multiple_choice? method from pre 2.0.0 era. 68 | if ::RUBY_VERSION >= "2.0.0" && ::RUBY_PLATFORM != "java" 69 | alias_method :multiple_choice?, :multiple_choices? 70 | end 71 | 72 | def respond_to?(name, include_all = false) 73 | return true if RESPOND_TO_METHODS.include?(name) 74 | parsed_response.respond_to?(name, include_all) || response.respond_to?(name, include_all) 75 | end 76 | 77 | protected 78 | 79 | def method_missing(name, *args, &block) 80 | if parsed_response.respond_to?(name) 81 | parsed_response.send(name, *args, &block) 82 | elsif response.respond_to?(name) 83 | response.send(name, *args, &block) 84 | else 85 | super 86 | end 87 | end 88 | 89 | def throw_exception 90 | if @request.options[:raise_on] && @request.options[:raise_on].include?(code) 91 | ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}" 92 | end 93 | end 94 | end 95 | end 96 | 97 | require 'httparty/response/headers' 98 | -------------------------------------------------------------------------------- /features/steps/remote_service_steps.rb: -------------------------------------------------------------------------------- 1 | Given /a remote service that returns '(.*)'/ do |response_body| 2 | @handler = BasicMongrelHandler.new 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 9 | end 10 | 11 | Given /a remote service that returns a (\d+) status code/ do |code| 12 | @handler = BasicMongrelHandler.new 13 | @handler.response_code = code 14 | end 15 | 16 | Given /that service is accessed at the path '(.*)'/ do |path| 17 | @server.register(path, @handler) 18 | end 19 | 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 } 25 | end 26 | 27 | Given /^a remote deflate service$/ do 28 | @handler = DeflateHandler.new 29 | end 30 | 31 | Given /^a remote deflate service on port '(\d+)'/ do |port| 32 | run_server(port) 33 | @handler = DeflateHandler.new 34 | end 35 | 36 | Given /^a remote gzip service$/ do 37 | @handler = GzipHandler.new 38 | end 39 | 40 | Given /the response from the service has a Content-Type of '(.*)'/ do |content_type| 41 | @handler.content_type = content_type 42 | end 43 | 44 | Given /the response from the service has a body of '(.*)'/ do |response_body| 45 | @handler.response_body = response_body 46 | end 47 | 48 | Given /the url '(.*)' redirects to '(.*)'/ do |redirection_url, target_url| 49 | @server.register redirection_url, new_mongrel_redirector(target_url) 50 | end 51 | 52 | Given /that service is protected by Basic Authentication/ do 53 | @handler.extend BasicAuthentication 54 | end 55 | 56 | Given /that service is protected by Digest Authentication/ do 57 | @handler.extend DigestAuthentication 58 | end 59 | 60 | Given /that service is protected by MD5-sess Digest Authentication/ do 61 | @handler.extend DigestAuthenticationUsingMD5Sess 62 | end 63 | 64 | Given /that service requires the username '(.*)' with the password '(.*)'/ do |username, password| 65 | @handler.username = username 66 | @handler.password = password 67 | end 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 | 74 | Given /a restricted page at '(.*)'/ do |url| 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 | " 81 | end 82 | 83 | # This joins the server thread, and halts cucumber, so you can actually hit the 84 | # server with a browser. Runs until you kill it with Ctrl-c 85 | Given /I want to hit this in a browser/ do 86 | @server.acceptor.join 87 | end 88 | 89 | Then /I wait for the server to recover/ do 90 | timeout = @request_options[:timeout] || 0 91 | sleep @server_response_time - timeout 92 | end 93 | -------------------------------------------------------------------------------- /spec/httparty/ssl_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Request do 4 | context "SSL certificate verification" do 5 | before do 6 | FakeWeb.allow_net_connect = true 7 | end 8 | 9 | after do 10 | FakeWeb.allow_net_connect = false 11 | end 12 | 13 | it "should fail when no trusted CA list is specified, by default" do 14 | expect do 15 | ssl_verify_test(nil, nil, "selfsigned.crt") 16 | end.to raise_error OpenSSL::SSL::SSLError 17 | end 18 | 19 | it "should work when no trusted CA list is specified, when the verify option is set to false" do 20 | expect(ssl_verify_test(nil, nil, "selfsigned.crt", verify: false).parsed_response).to eq({'success' => true}) 21 | end 22 | 23 | it "should fail when no trusted CA list is specified, with a bogus hostname, by default" do 24 | expect do 25 | ssl_verify_test(nil, nil, "bogushost.crt") 26 | end.to raise_error OpenSSL::SSL::SSLError 27 | end 28 | 29 | it "should work when no trusted CA list is specified, even with a bogus hostname, when the verify option is set to true" do 30 | expect(ssl_verify_test(nil, nil, "bogushost.crt", verify: false).parsed_response).to eq({'success' => true}) 31 | end 32 | 33 | it "should work when using ssl_ca_file with a self-signed CA" do 34 | expect(ssl_verify_test(:ssl_ca_file, "selfsigned.crt", "selfsigned.crt").parsed_response).to eq({'success' => true}) 35 | end 36 | 37 | it "should work when using ssl_ca_file with a certificate authority" do 38 | expect(ssl_verify_test(:ssl_ca_file, "ca.crt", "server.crt").parsed_response).to eq({'success' => true}) 39 | end 40 | 41 | it "should work when using ssl_ca_path with a certificate authority" do 42 | http = Net::HTTP.new('www.google.com', 443) 43 | response = double(Net::HTTPResponse, :[] => '', body: '', to_hash: {}) 44 | allow(http).to receive(:request).and_return(response) 45 | expect(Net::HTTP).to receive(:new).with('www.google.com', 443).and_return(http) 46 | expect(http).to receive(:ca_path=).with('/foo/bar') 47 | HTTParty.get('https://www.google.com', ssl_ca_path: '/foo/bar') 48 | end 49 | 50 | it "should fail when using ssl_ca_file and the server uses an unrecognized certificate authority" do 51 | expect do 52 | ssl_verify_test(:ssl_ca_file, "ca.crt", "selfsigned.crt") 53 | end.to raise_error(OpenSSL::SSL::SSLError) 54 | end 55 | 56 | it "should fail when using ssl_ca_path and the server uses an unrecognized certificate authority" do 57 | expect do 58 | ssl_verify_test(:ssl_ca_path, ".", "selfsigned.crt") 59 | end.to raise_error(OpenSSL::SSL::SSLError) 60 | end 61 | 62 | it "should fail when using ssl_ca_file and the server uses a bogus hostname" do 63 | expect do 64 | ssl_verify_test(:ssl_ca_file, "ca.crt", "bogushost.crt") 65 | end.to raise_error(OpenSSL::SSL::SSLError) 66 | end 67 | 68 | it "should fail when using ssl_ca_path and the server uses a bogus hostname" do 69 | expect do 70 | ssl_verify_test(:ssl_ca_path, ".", "bogushost.crt") 71 | end.to raise_error(OpenSSL::SSL::SSLError) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by `rubocop --auto-gen-config` 2 | # on 2015-04-24 07:22:28 +0200 using RuboCop version 0.30.0. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the offenses are removed from the code base. 5 | # Note that changes in the inspected code, or installation of new 6 | # versions of RuboCop, may require this file to be generated again. 7 | 8 | # Offense count: 33 9 | Lint/AmbiguousRegexpLiteral: 10 | Enabled: false 11 | 12 | # Offense count: 1 13 | # Configuration parameters: AlignWith, SupportedStyles. 14 | Lint/EndAlignment: 15 | Enabled: false 16 | 17 | # Offense count: 1 18 | Lint/HandleExceptions: 19 | Enabled: false 20 | 21 | # Offense count: 5 22 | Lint/UselessAssignment: 23 | Enabled: false 24 | 25 | # Offense count: 23 26 | Metrics/AbcSize: 27 | Max: 86 28 | 29 | # Offense count: 1 30 | # Configuration parameters: CountComments. 31 | Metrics/ClassLength: 32 | Max: 285 33 | 34 | # Offense count: 8 35 | Metrics/CyclomaticComplexity: 36 | Max: 17 37 | 38 | # Offense count: 332 39 | # Configuration parameters: AllowURI, URISchemes. 40 | Metrics/LineLength: 41 | Max: 266 42 | 43 | # Offense count: 17 44 | # Configuration parameters: CountComments. 45 | Metrics/MethodLength: 46 | Max: 39 47 | 48 | # Offense count: 8 49 | Metrics/PerceivedComplexity: 50 | Max: 20 51 | 52 | # Offense count: 1 53 | Style/AccessorMethodName: 54 | Enabled: false 55 | 56 | # Offense count: 1 57 | Style/AsciiComments: 58 | Enabled: false 59 | 60 | # Offense count: 14 61 | # Cop supports --auto-correct. 62 | # Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. 63 | Style/BlockDelimiters: 64 | Enabled: false 65 | 66 | # Offense count: 2 67 | Style/CaseEquality: 68 | Enabled: false 69 | 70 | # Offense count: 3 71 | # Configuration parameters: IndentWhenRelativeTo, SupportedStyles, IndentOneStep. 72 | Style/CaseIndentation: 73 | Enabled: false 74 | 75 | # Offense count: 4 76 | # Configuration parameters: EnforcedStyle, SupportedStyles. 77 | Style/ClassAndModuleChildren: 78 | Enabled: false 79 | 80 | # Offense count: 7 81 | Style/ConstantName: 82 | Enabled: false 83 | 84 | # Offense count: 2 85 | Style/EachWithObject: 86 | Enabled: false 87 | 88 | # Offense count: 2 89 | # Cop supports --auto-correct. 90 | Style/ElseAlignment: 91 | Enabled: false 92 | 93 | # Offense count: 3 94 | # Cop supports --auto-correct. 95 | # Configuration parameters: EnforcedStyle, SupportedStyles. 96 | Style/FirstParameterIndentation: 97 | Enabled: false 98 | 99 | # Offense count: 2 100 | # Cop supports --auto-correct. 101 | # Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues. 102 | Style/HashSyntax: 103 | Enabled: false 104 | 105 | # Offense count: 7 106 | # Cop supports --auto-correct. 107 | # Configuration parameters: MaxLineLength. 108 | Style/IfUnlessModifier: 109 | Enabled: false 110 | 111 | # Offense count: 11 112 | # Cop supports --auto-correct. 113 | Style/Lambda: 114 | Enabled: false 115 | 116 | # Offense count: 1 117 | # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. 118 | Style/Next: 119 | Enabled: false 120 | 121 | # Offense count: 2 122 | # Configuration parameters: EnforcedStyle, SupportedStyles. 123 | Style/RaiseArgs: 124 | Enabled: false 125 | -------------------------------------------------------------------------------- /bin/httparty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "optparse" 4 | require "pp" 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "/../lib")) 7 | require "httparty" 8 | 9 | opts = { 10 | action: :get, 11 | headers: {}, 12 | verbose: false 13 | } 14 | 15 | OptionParser.new do |o| 16 | o.banner = "USAGE: #{$PROGRAM_NAME} [options] [url]" 17 | 18 | o.on("-f", 19 | "--format [FORMAT]", 20 | "Output format to use instead of pretty-print ruby: " \ 21 | "plain, csv, json or xml") do |f| 22 | opts[:output_format] = f.downcase.to_sym 23 | end 24 | 25 | o.on("-a", 26 | "--action [ACTION]", 27 | "HTTP action: get (default), post, put, delete, head, or options") do |a| 28 | opts[:action] = a.downcase.to_sym 29 | end 30 | 31 | o.on("-d", 32 | "--data [BODY]", 33 | "Data to put in request body (prefix with '@' for file)") do |d| 34 | if d =~ /^@/ 35 | opts[:body] = open(d[1..-1]).read 36 | else 37 | opts[:body] = d 38 | end 39 | end 40 | 41 | o.on("-H", "--header [NAME:VALUE]", "Additional HTTP headers in NAME:VALUE form") do |h| 42 | abort "Invalid header specification, should be Name:Value" unless h =~ /.+:.+/ 43 | name, value = h.split(':') 44 | opts[:headers][name.strip] = value.strip 45 | end 46 | 47 | o.on("-v", "--verbose", "If set, print verbose output") do |v| 48 | opts[:verbose] = true 49 | end 50 | 51 | o.on("-u", "--user [CREDS]", "Use basic authentication. Value should be user:password") do |u| 52 | abort "Invalid credentials format. Must be user:password" unless u =~ /.*:.+/ 53 | user, password = u.split(':') 54 | opts[:basic_auth] = { username: user, password: password } 55 | end 56 | 57 | o.on("-r", "--response-code", "Command fails if response code >= 400") do 58 | opts[:response_code] = true 59 | end 60 | 61 | o.on("-h", "--help", "Show help documentation") do |h| 62 | puts o 63 | exit 64 | end 65 | 66 | o.on("--version", "Show HTTParty version") do |ver| 67 | puts "Version: #{HTTParty::VERSION}" 68 | exit 69 | end 70 | end.parse! 71 | 72 | if ARGV.empty? 73 | STDERR.puts "You need to provide a URL" 74 | STDERR.puts "USAGE: #{$PROGRAM_NAME} [options] [url]" 75 | end 76 | 77 | def dump_headers(response) 78 | resp_type = Net::HTTPResponse::CODE_TO_OBJ[response.code.to_s] 79 | puts "#{response.code} #{resp_type.to_s.sub(/^Net::HTTP/, '')}" 80 | response.headers.each do |n, v| 81 | puts "#{n}: #{v}" 82 | end 83 | puts 84 | end 85 | 86 | if opts[:verbose] 87 | puts "#{opts[:action].to_s.upcase} #{ARGV.first}" 88 | opts[:headers].each do |n, v| 89 | puts "#{n}: #{v}" 90 | end 91 | puts 92 | end 93 | 94 | response = HTTParty.send(opts[:action], ARGV.first, opts) 95 | if opts[:output_format].nil? 96 | dump_headers(response) if opts[:verbose] 97 | pp response 98 | else 99 | print_format = opts[:output_format] 100 | dump_headers(response) if opts[:verbose] 101 | 102 | case opts[:output_format] 103 | when :json 104 | begin 105 | require 'json' 106 | puts JSON.pretty_generate(response) 107 | rescue LoadError 108 | puts YAML.dump(response) 109 | end 110 | when :xml 111 | require 'rexml/document' 112 | REXML::Document.new(response.body).write(STDOUT, 2) 113 | puts 114 | when :csv 115 | require 'csv' 116 | puts CSV.parse(response.body).map(&:to_s) 117 | else 118 | puts response 119 | end 120 | end 121 | exit false if opts[:response_code] && response.code >= 400 122 | -------------------------------------------------------------------------------- /spec/httparty/cookie_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper')) 2 | 3 | RSpec.describe HTTParty::CookieHash do 4 | before(:each) do 5 | @cookie_hash = HTTParty::CookieHash.new 6 | end 7 | 8 | describe "#add_cookies" do 9 | describe "with a hash" do 10 | it "should add new key/value pairs to the hash" do 11 | @cookie_hash.add_cookies(foo: "bar") 12 | @cookie_hash.add_cookies(rofl: "copter") 13 | expect(@cookie_hash.length).to eql(2) 14 | end 15 | 16 | it "should overwrite any existing key" do 17 | @cookie_hash.add_cookies(foo: "bar") 18 | @cookie_hash.add_cookies(foo: "copter") 19 | expect(@cookie_hash.length).to eql(1) 20 | expect(@cookie_hash[:foo]).to eql("copter") 21 | end 22 | end 23 | 24 | describe "with a string" do 25 | it "should add new key/value pairs to the hash" do 26 | @cookie_hash.add_cookies("first=one; second=two; third") 27 | expect(@cookie_hash[:first]).to eq('one') 28 | expect(@cookie_hash[:second]).to eq('two') 29 | expect(@cookie_hash[:third]).to eq(nil) 30 | end 31 | 32 | it "should overwrite any existing key" do 33 | @cookie_hash[:foo] = 'bar' 34 | @cookie_hash.add_cookies("foo=tar") 35 | expect(@cookie_hash.length).to eql(1) 36 | expect(@cookie_hash[:foo]).to eql("tar") 37 | end 38 | 39 | it "should handle '=' within cookie value" do 40 | @cookie_hash.add_cookies("first=one=1; second=two=2==") 41 | expect(@cookie_hash.keys).to include(:first, :second) 42 | expect(@cookie_hash[:first]).to eq('one=1') 43 | expect(@cookie_hash[:second]).to eq('two=2==') 44 | end 45 | end 46 | 47 | describe 'with other class' do 48 | it "should error" do 49 | expect { 50 | @cookie_hash.add_cookies([]) 51 | }.to raise_error(RuntimeError) 52 | end 53 | end 54 | end 55 | 56 | # The regexen are required because Hashes aren't ordered, so a test against 57 | # a hardcoded string was randomly failing. 58 | describe "#to_cookie_string" do 59 | before(:each) do 60 | @cookie_hash.add_cookies(foo: "bar") 61 | @cookie_hash.add_cookies(rofl: "copter") 62 | @s = @cookie_hash.to_cookie_string 63 | end 64 | 65 | it "should format the key/value pairs, delimited by semi-colons" do 66 | expect(@s).to match(/foo=bar/) 67 | expect(@s).to match(/rofl=copter/) 68 | expect(@s).to match(/^\w+=\w+; \w+=\w+$/) 69 | end 70 | 71 | it "should not include client side only cookies" do 72 | @cookie_hash.add_cookies(path: "/") 73 | @s = @cookie_hash.to_cookie_string 74 | expect(@s).not_to match(/path=\//) 75 | end 76 | 77 | it "should not include client side only cookies even when attributes use camal case" do 78 | @cookie_hash.add_cookies(Path: "/") 79 | @s = @cookie_hash.to_cookie_string 80 | expect(@s).not_to match(/Path=\//) 81 | end 82 | 83 | it "should not mutate the hash" do 84 | original_hash = { 85 | "session" => "91e25e8b-6e32-418d-c72f-2d18adf041cd", 86 | "Max-Age" => "15552000", 87 | "cart" => "91e25e8b-6e32-418d-c72f-2d18adf041cd", 88 | "httponly" => nil, 89 | "Path" => "/", 90 | "secure" => nil, 91 | } 92 | 93 | cookie_hash = HTTParty::CookieHash[original_hash] 94 | 95 | cookie_hash.to_cookie_string 96 | 97 | expect(cookie_hash).to eq(original_hash) 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /features/steps/mongrel_helper.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | class BasicMongrelHandler < Mongrel::HttpHandler 3 | attr_accessor :content_type, :custom_headers, :response_body, :response_code, :preprocessor, :username, :password 4 | 5 | def initialize 6 | @content_type = "text/html" 7 | @response_body = "" 8 | @response_code = 200 9 | @custom_headers = {} 10 | end 11 | 12 | def process(request, response) 13 | instance_eval(&preprocessor) if preprocessor 14 | reply_with(response, response_code, response_body) 15 | end 16 | 17 | def reply_with(response, code, response_body) 18 | response.start(code) do |head, body| 19 | head["Content-Type"] = content_type 20 | custom_headers.each { |k, v| head[k] = v } 21 | body.write(response_body) 22 | end 23 | end 24 | end 25 | 26 | class DeflateHandler < BasicMongrelHandler 27 | def process(request, response) 28 | response.start do |head, body| 29 | head['Content-Encoding'] = 'deflate' 30 | body.write Zlib::Deflate.deflate(response_body) 31 | end 32 | end 33 | end 34 | 35 | class GzipHandler < BasicMongrelHandler 36 | def process(request, response) 37 | response.start do |head, body| 38 | head['Content-Encoding'] = 'gzip' 39 | body.write gzip(response_body) 40 | end 41 | end 42 | 43 | protected 44 | 45 | def gzip(string) 46 | sio = StringIO.new('', 'r+') 47 | gz = Zlib::GzipWriter.new sio 48 | gz.write string 49 | gz.finish 50 | sio.rewind 51 | sio.read 52 | end 53 | end 54 | 55 | module BasicAuthentication 56 | def self.extended(base) 57 | base.custom_headers["WWW-Authenticate"] = 'Basic Realm="Super Secret Page"' 58 | end 59 | 60 | def process(request, response) 61 | if authorized?(request) 62 | super 63 | else 64 | reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") 65 | end 66 | end 67 | 68 | def authorized?(request) 69 | request.params["HTTP_AUTHORIZATION"] == "Basic " + Base64.encode64("#{@username}:#{@password}").strip 70 | end 71 | end 72 | 73 | module DigestAuthentication 74 | def self.extended(base) 75 | base.custom_headers["WWW-Authenticate"] = 'Digest realm="testrealm@host.com",qop="auth,auth-int",nonce="nonce",opaque="opaque"' 76 | end 77 | 78 | def process(request, response) 79 | if authorized?(request) 80 | super 81 | else 82 | reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") 83 | end 84 | end 85 | 86 | def authorized?(request) 87 | request.params["HTTP_AUTHORIZATION"] =~ /Digest.*uri=/ 88 | end 89 | end 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 | 124 | def new_mongrel_redirector(target_url, relative_path = false) 125 | target_url = "http://#{@host_and_port}#{target_url}" unless relative_path 126 | Mongrel::RedirectHandler.new(target_url) 127 | end 128 | -------------------------------------------------------------------------------- /lib/httparty/net_digest_auth.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'net/http' 3 | 4 | module Net 5 | module HTTPHeader 6 | def digest_auth(username, password, response) 7 | authenticator = DigestAuthenticator.new( 8 | username, 9 | password, 10 | @method, 11 | @path, 12 | response 13 | ) 14 | 15 | @header['Authorization'] = authenticator.authorization_header 16 | @header['cookie'] = append_cookies(authenticator) if response['Set-Cookie'] 17 | end 18 | 19 | def append_cookies(authenticator) 20 | cookies = @header['cookie'] ? @header['cookie'] : [] 21 | cookies.concat(authenticator.cookie_header) 22 | end 23 | 24 | class DigestAuthenticator 25 | def initialize(username, password, method, path, response_header) 26 | @username = username 27 | @password = password 28 | @method = method 29 | @path = path 30 | @response = parse(response_header) 31 | @cookies = parse_cookies(response_header) 32 | end 33 | 34 | def authorization_header 35 | @cnonce = md5(random) 36 | header = [ 37 | %(Digest username="#{@username}"), 38 | %(realm="#{@response['realm']}"), 39 | %(nonce="#{@response['nonce']}"), 40 | %(uri="#{@path}"), 41 | %(response="#{request_digest}") 42 | ] 43 | 44 | header << %(algorithm="#{@response['algorithm']}") if algorithm_present? 45 | 46 | if qop_present? 47 | fields = [ 48 | %(cnonce="#{@cnonce}"), 49 | %(qop="#{@response['qop']}"), 50 | "nc=00000001" 51 | ] 52 | fields.each { |field| header << field } 53 | end 54 | 55 | header << %(opaque="#{@response['opaque']}") if opaque_present? 56 | header 57 | end 58 | 59 | def cookie_header 60 | @cookies 61 | end 62 | 63 | private 64 | 65 | def parse(response_header) 66 | header = response_header['www-authenticate'] 67 | .gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"') 68 | 69 | header =~ /Digest (.*)/ 70 | params = {} 71 | if $1 72 | non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 } 73 | non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } 74 | end 75 | params 76 | end 77 | 78 | def parse_cookies(response_header) 79 | return [] unless response_header['Set-Cookie'] 80 | 81 | cookies = response_header['Set-Cookie'].split('; ') 82 | 83 | cookies.reduce([]) do |ret, cookie| 84 | ret << cookie 85 | ret 86 | end 87 | 88 | cookies 89 | end 90 | 91 | def opaque_present? 92 | @response.key?('opaque') && !@response['opaque'].empty? 93 | end 94 | 95 | def qop_present? 96 | @response.key?('qop') && !@response['qop'].empty? 97 | end 98 | 99 | def random 100 | format "%x", (Time.now.to_i + rand(65535)) 101 | end 102 | 103 | def request_digest 104 | a = [md5(a1), @response['nonce'], md5(a2)] 105 | a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present? 106 | md5(a.join(":")) 107 | end 108 | 109 | def md5(str) 110 | Digest::MD5.hexdigest(str) 111 | end 112 | 113 | def algorithm_present? 114 | @response.key?('algorithm') && !@response['algorithm'].empty? 115 | end 116 | 117 | def use_md5_sess? 118 | algorithm_present? && @response['algorithm'] == 'MD5-sess' 119 | end 120 | 121 | def a1 122 | a1_user_realm_pwd = [@username, @response['realm'], @password].join(':') 123 | if use_md5_sess? 124 | [ md5(a1_user_realm_pwd), @response['nonce'], @cnonce ].join(':') 125 | else 126 | a1_user_realm_pwd 127 | end 128 | end 129 | 130 | def a2 131 | [@method, @path].join(":") 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/httparty/logger/curl_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Logger::CurlFormatter do 4 | describe "#format" do 5 | let(:logger) { double('Logger') } 6 | let(:response_object) { Net::HTTPOK.new('1.1', 200, 'OK') } 7 | let(:parsed_response) { lambda { {"foo" => "bar"} } } 8 | 9 | let(:response) do 10 | HTTParty::Response.new(request, response_object, parsed_response) 11 | end 12 | 13 | let(:request) do 14 | HTTParty::Request.new(Net::HTTP::Get, 'http://foo.bar.com/') 15 | end 16 | 17 | subject { described_class.new(logger, :info) } 18 | 19 | before do 20 | allow(logger).to receive(:info) 21 | allow(request).to receive(:raw_body).and_return('content') 22 | allow(response_object).to receive_messages(body: "{foo:'bar'}") 23 | response_object['header-key'] = 'header-value' 24 | 25 | subject.format request, response 26 | end 27 | 28 | context 'when request is logged' do 29 | context "and request's option 'base_uri' is not present" do 30 | it 'logs url' do 31 | expect(logger).to have_received(:info).with(/\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/foo.bar.com/) 32 | end 33 | end 34 | 35 | context "and request's option 'base_uri' is present" do 36 | let(:request) do 37 | HTTParty::Request.new(Net::HTTP::Get, '/path', base_uri: 'http://foo.bar.com') 38 | end 39 | 40 | it 'logs url' do 41 | expect(logger).to have_received(:info).with(/\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/foo.bar.com\/path/) 42 | end 43 | end 44 | 45 | context 'and headers are not present' do 46 | it 'not log Headers' do 47 | expect(logger).not_to have_received(:info).with(/Headers/) 48 | end 49 | end 50 | 51 | context 'and headers are present' do 52 | let(:request) do 53 | HTTParty::Request.new(Net::HTTP::Get, '/path', base_uri: 'http://foo.bar.com', headers: { key: 'value' }) 54 | end 55 | 56 | it 'logs Headers' do 57 | expect(logger).to have_received(:info).with(/Headers/) 58 | end 59 | 60 | it 'logs headers keys' do 61 | expect(logger).to have_received(:info).with(/key: value/) 62 | end 63 | end 64 | 65 | context 'and query is not present' do 66 | it 'not logs Query' do 67 | expect(logger).not_to have_received(:info).with(/Query/) 68 | end 69 | end 70 | 71 | context 'and query is present' do 72 | let(:request) do 73 | HTTParty::Request.new(Net::HTTP::Get, '/path', query: { key: 'value' }) 74 | end 75 | 76 | it 'logs Query' do 77 | expect(logger).to have_received(:info).with(/Query/) 78 | end 79 | 80 | it 'logs query params' do 81 | expect(logger).to have_received(:info).with(/key: value/) 82 | end 83 | end 84 | 85 | context 'when request raw_body is present' do 86 | it 'not logs request body' do 87 | expect(logger).to have_received(:info).with(/content/) 88 | end 89 | end 90 | end 91 | 92 | context 'when response is logged' do 93 | it 'logs http version and response code' do 94 | expect(logger).to have_received(:info).with(/HTTP\/1.1 200/) 95 | end 96 | 97 | it 'logs headers' do 98 | expect(logger).to have_received(:info).with(/Header-key: header-value/) 99 | end 100 | 101 | it 'logs body' do 102 | expect(logger).to have_received(:info).with(/{foo:'bar'}/) 103 | end 104 | end 105 | 106 | it "formats a response in a style that resembles a -v curl" do 107 | logger_double = double 108 | expect(logger_double).to receive(:info).with( 109 | /\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/localhost/) 110 | 111 | subject = described_class.new(logger_double, :info) 112 | 113 | stub_http_response_with("google.html") 114 | 115 | response = HTTParty::Request.new.perform 116 | subject.format(response.request, response) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/httparty/parser.rb: -------------------------------------------------------------------------------- 1 | module HTTParty 2 | # The default parser used by HTTParty, supports xml, json, html, csv 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' => :plain, 46 | 'text/javascript' => :plain, 47 | 'text/html' => :html, 48 | 'text/plain' => :plain, 49 | 'text/csv' => :csv, 50 | 'application/csv' => :csv, 51 | 'text/comma-separated-values' => :csv 52 | } 53 | 54 | # The response body of the request 55 | # @return [String] 56 | attr_reader :body 57 | 58 | # The intended parsing format for the request 59 | # @return [Symbol] e.g. :json 60 | attr_reader :format 61 | 62 | # Instantiate the parser and call {#parse}. 63 | # @param [String] body the response body 64 | # @param [Symbol] format the response format 65 | # @return parsed response 66 | def self.call(body, format) 67 | new(body, format).parse 68 | end 69 | 70 | # @return [Hash] the SupportedFormats hash 71 | def self.formats 72 | const_get(:SupportedFormats) 73 | end 74 | 75 | # @param [String] mimetype response MIME type 76 | # @return [Symbol] 77 | # @return [nil] mime type not supported 78 | def self.format_from_mimetype(mimetype) 79 | formats[formats.keys.detect {|k| mimetype.include?(k)}] 80 | end 81 | 82 | # @return [Array] list of supported formats 83 | def self.supported_formats 84 | formats.values.uniq 85 | end 86 | 87 | # @param [Symbol] format e.g. :json, :xml 88 | # @return [Boolean] 89 | def self.supports_format?(format) 90 | supported_formats.include?(format) 91 | end 92 | 93 | def initialize(body, format) 94 | @body = body 95 | @format = format 96 | end 97 | 98 | # @return [Object] the parsed body 99 | # @return [nil] when the response body is nil, an empty string, spaces only or "null" 100 | def parse 101 | return nil if body.nil? 102 | return nil if body == "null" 103 | return nil if body.valid_encoding? && body.strip.empty? 104 | if supports_format? 105 | parse_supported_format 106 | else 107 | body 108 | end 109 | end 110 | 111 | protected 112 | 113 | def xml 114 | MultiXml.parse(body) 115 | end 116 | 117 | def json 118 | JSON.parse(body, :quirks_mode => true, :allow_nan => true) 119 | end 120 | 121 | def csv 122 | CSV.parse(body) 123 | end 124 | 125 | def html 126 | body 127 | end 128 | 129 | def plain 130 | body 131 | end 132 | 133 | def supports_format? 134 | self.class.supports_format?(format) 135 | end 136 | 137 | def parse_supported_format 138 | send(format) 139 | rescue NoMethodError => e 140 | raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /features/command_line.feature: -------------------------------------------------------------------------------- 1 | @command_line 2 | Feature: Command Line 3 | 4 | As a developer 5 | I want to be able to harness the power of HTTParty from the command line 6 | Because that would make quick testing and debugging easy 7 | 8 | Scenario: Show help information 9 | When I run `httparty --help` 10 | Then the output should contain "-f, --format [FORMAT]" 11 | 12 | Scenario: Show current version 13 | When I run `httparty --version` 14 | Then the output should contain "Version:" 15 | And the output should not contain "You need to provide a URL" 16 | 17 | Scenario: Make a get request 18 | Given a remote deflate service on port '4001' 19 | And the response from the service has a body of 'GET request' 20 | And that service is accessed at the path '/fun' 21 | When I run `httparty http://0.0.0.0:4001/fun` 22 | Then the output should contain "GET request" 23 | 24 | Scenario: Make a post request 25 | Given a remote deflate service on port '4002' 26 | And the response from the service has a body of 'POST request' 27 | And that service is accessed at the path '/fun' 28 | When I run `httparty http://0.0.0.0:4002/fun --action post --data "a=1&b=2"` 29 | Then the output should contain "POST request" 30 | 31 | Scenario: Make a put request 32 | Given a remote deflate service on port '4003' 33 | And the response from the service has a body of 'PUT request' 34 | And that service is accessed at the path '/fun' 35 | When I run `httparty http://0.0.0.0:4003/fun --action put --data "a=1&b=2"` 36 | Then the output should contain "PUT request" 37 | 38 | Scenario: Make a delete request 39 | Given a remote deflate service on port '4004' 40 | And the response from the service has a body of 'DELETE request' 41 | And that service is accessed at the path '/fun' 42 | When I run `httparty http://0.0.0.0:4004/fun --action delete` 43 | Then the output should contain "DELETE request" 44 | 45 | Scenario: Set a verbose mode 46 | Given a remote deflate service on port '4005' 47 | And the response from the service has a body of 'Some request' 48 | And that service is accessed at the path '/fun' 49 | When I run `httparty http://0.0.0.0:4005/fun --verbose` 50 | Then the output should contain "content-length" 51 | 52 | Scenario: Use service with basic authentication 53 | Given a remote deflate service on port '4006' 54 | And the response from the service has a body of 'Successfull authentication' 55 | And that service is accessed at the path '/fun' 56 | And that service is protected by Basic Authentication 57 | And that service requires the username 'user' with the password 'pass' 58 | When I run `httparty http://0.0.0.0:4006/fun --user 'user:pass'` 59 | Then the output should contain "Successfull authentication" 60 | 61 | Scenario: Get response in plain format 62 | Given a remote deflate service on port '4007' 63 | And the response from the service has a body of 'Some request' 64 | And that service is accessed at the path '/fun' 65 | When I run `httparty http://0.0.0.0:4007/fun --format plain` 66 | Then the output should contain "Some request" 67 | 68 | Scenario: Get response in json format 69 | Given a remote deflate service on port '4008' 70 | Given a remote service that returns '{ "jennings": "waylon", "cash": "johnny" }' 71 | And that service is accessed at the path '/service.json' 72 | And the response from the service has a Content-Type of 'application/json' 73 | When I run `httparty http://0.0.0.0:4008/service.json --format json` 74 | Then the output should contain '"jennings": "waylon"' 75 | 76 | Scenario: Get response in xml format 77 | Given a remote deflate service on port '4009' 78 | Given a remote service that returns 'waylon jennings' 79 | And that service is accessed at the path '/service.xml' 80 | And the response from the service has a Content-Type of 'text/xml' 81 | When I run `httparty http://0.0.0.0:4009/service.xml --format xml` 82 | Then the output should contain "" 83 | 84 | Scenario: Get response in csv format 85 | Given a remote deflate service on port '4010' 86 | Given a remote service that returns: 87 | """ 88 | "Last Name","Name" 89 | "jennings","waylon" 90 | "cash","johnny" 91 | """ 92 | And that service is accessed at the path '/service.csv' 93 | And the response from the service has a Content-Type of 'application/csv' 94 | When I run `httparty http://0.0.0.0:4010/service.csv --format csv` 95 | Then the output should contain '["Last Name", "Name"]' 96 | -------------------------------------------------------------------------------- /spec/fixtures/google.html: -------------------------------------------------------------------------------- 1 | Google

Google

 
  Advanced Search
  Preferences
  Language Tools


Advertising Programs - Business Solutions - About Google

©2008 - Privacy

-------------------------------------------------------------------------------- /spec/httparty/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Parser do 4 | describe ".SupportedFormats" do 5 | it "returns a hash" do 6 | expect(HTTParty::Parser::SupportedFormats).to be_instance_of(Hash) 7 | end 8 | end 9 | 10 | describe ".call" do 11 | it "generates an HTTParty::Parser instance with the given body and format" do 12 | expect(HTTParty::Parser).to receive(:new).with('body', :plain).and_return(double(parse: nil)) 13 | HTTParty::Parser.call('body', :plain) 14 | end 15 | 16 | it "calls #parse on the parser" do 17 | parser = double('Parser') 18 | expect(parser).to receive(:parse) 19 | allow(HTTParty::Parser).to receive_messages(new: parser) 20 | parser = HTTParty::Parser.call('body', :plain) 21 | end 22 | end 23 | 24 | describe ".formats" do 25 | it "returns the SupportedFormats constant" do 26 | expect(HTTParty::Parser.formats).to eq(HTTParty::Parser::SupportedFormats) 27 | end 28 | 29 | it "returns the SupportedFormats constant for subclasses" do 30 | class MyParser < HTTParty::Parser 31 | SupportedFormats = {"application/atom+xml" => :atom} 32 | end 33 | expect(MyParser.formats).to eq({"application/atom+xml" => :atom}) 34 | end 35 | end 36 | 37 | describe ".format_from_mimetype" do 38 | it "returns a symbol representing the format mimetype" do 39 | expect(HTTParty::Parser.format_from_mimetype("text/plain")).to eq(:plain) 40 | end 41 | 42 | it "returns nil when the mimetype is not supported" do 43 | expect(HTTParty::Parser.format_from_mimetype("application/atom+xml")).to be_nil 44 | end 45 | end 46 | 47 | describe ".supported_formats" do 48 | it "returns a unique set of supported formats represented by symbols" do 49 | expect(HTTParty::Parser.supported_formats).to eq(HTTParty::Parser::SupportedFormats.values.uniq) 50 | end 51 | end 52 | 53 | describe ".supports_format?" do 54 | it "returns true for a supported format" do 55 | allow(HTTParty::Parser).to receive_messages(supported_formats: [:json]) 56 | expect(HTTParty::Parser.supports_format?(:json)).to be_truthy 57 | end 58 | 59 | it "returns false for an unsupported format" do 60 | allow(HTTParty::Parser).to receive_messages(supported_formats: []) 61 | expect(HTTParty::Parser.supports_format?(:json)).to be_falsey 62 | end 63 | end 64 | 65 | describe "#parse" do 66 | before do 67 | @parser = HTTParty::Parser.new('body', :json) 68 | end 69 | 70 | it "attempts to parse supported formats" do 71 | allow(@parser).to receive_messages(supports_format?: true) 72 | expect(@parser).to receive(:parse_supported_format) 73 | @parser.parse 74 | end 75 | 76 | it "returns the unparsed body when the format is unsupported" do 77 | allow(@parser).to receive_messages(supports_format?: false) 78 | expect(@parser.parse).to eq(@parser.body) 79 | end 80 | 81 | it "returns nil for an empty body" do 82 | allow(@parser).to receive_messages(body: '') 83 | expect(@parser.parse).to be_nil 84 | end 85 | 86 | it "returns nil for a nil body" do 87 | allow(@parser).to receive_messages(body: nil) 88 | expect(@parser.parse).to be_nil 89 | end 90 | 91 | it "returns nil for a 'null' body" do 92 | allow(@parser).to receive_messages(body: "null") 93 | expect(@parser.parse).to be_nil 94 | end 95 | 96 | it "returns nil for a body with spaces only" do 97 | allow(@parser).to receive_messages(body: " ") 98 | expect(@parser.parse).to be_nil 99 | end 100 | 101 | it "does not raise exceptions for bodies with invalid encodings" do 102 | allow(@parser).to receive_messages(body: "\x80") 103 | allow(@parser).to receive_messages(supports_format?: false) 104 | expect(@parser.parse).to_not be_nil 105 | end 106 | end 107 | 108 | describe "#supports_format?" do 109 | it "utilizes the class method to determine if the format is supported" do 110 | expect(HTTParty::Parser).to receive(:supports_format?).with(:json) 111 | parser = HTTParty::Parser.new('body', :json) 112 | parser.send(:supports_format?) 113 | end 114 | end 115 | 116 | describe "#parse_supported_format" do 117 | it "calls the parser for the given format" do 118 | parser = HTTParty::Parser.new('body', :json) 119 | expect(parser).to receive(:json) 120 | parser.send(:parse_supported_format) 121 | end 122 | 123 | context "when a parsing method does not exist for the given format" do 124 | it "raises an exception" do 125 | parser = HTTParty::Parser.new('body', :atom) 126 | expect do 127 | parser.send(:parse_supported_format) 128 | end.to raise_error(NotImplementedError, "HTTParty::Parser has not implemented a parsing method for the :atom format.") 129 | end 130 | 131 | it "raises a useful exception message for subclasses" do 132 | atom_parser = Class.new(HTTParty::Parser) do 133 | def self.name 134 | 'AtomParser' 135 | end 136 | end 137 | parser = atom_parser.new 'body', :atom 138 | expect do 139 | parser.send(:parse_supported_format) 140 | end.to raise_error(NotImplementedError, "AtomParser has not implemented a parsing method for the :atom format.") 141 | end 142 | end 143 | end 144 | 145 | context "parsers" do 146 | subject do 147 | HTTParty::Parser.new('body', nil) 148 | end 149 | 150 | it "parses xml with MultiXml" do 151 | expect(MultiXml).to receive(:parse).with('body') 152 | subject.send(:xml) 153 | end 154 | 155 | it "parses json with JSON" do 156 | expect(JSON).to receive(:parse).with('body', :quirks_mode => true, :allow_nan => true) 157 | subject.send(:json) 158 | end 159 | 160 | it "parses html by simply returning the body" do 161 | expect(subject.send(:html)).to eq('body') 162 | end 163 | 164 | it "parses plain text by simply returning the body" do 165 | expect(subject.send(:plain)).to eq('body') 166 | end 167 | 168 | it "parses csv with CSV" do 169 | expect(CSV).to receive(:parse).with('body') 170 | subject.send(:csv) 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/fixtures/delicious.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/httparty/connection_adapter.rb: -------------------------------------------------------------------------------- 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 | # Some things that are probably interesting are as follows: 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 45 | # * :+debug_output+: see HTTParty::ClassMethods.debug_output. 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 49 | # * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file. 50 | # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path. 51 | # * :+connection_adapter_options+: contains the hash you passed to HTTParty.connection_adapter when you configured your connection adapter 52 | class ConnectionAdapter 53 | # Private: Regex used to strip brackets from IPv6 URIs. 54 | StripIpv6BracketsRegex = /\A\[(.*)\]\z/ 55 | 56 | OPTION_DEFAULTS = { 57 | verify: true, 58 | verify_peer: true 59 | } 60 | 61 | # Public 62 | def self.call(uri, options) 63 | new(uri, options).connection 64 | end 65 | 66 | attr_reader :uri, :options 67 | 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 71 | 72 | @uri = uri 73 | @options = OPTION_DEFAULTS.merge(options) 74 | end 75 | 76 | def connection 77 | host = clean_host(uri.host) 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 84 | 85 | http.use_ssl = ssl_implied?(uri) 86 | 87 | attach_ssl_certificates(http, options) 88 | 89 | if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float)) 90 | http.open_timeout = options[:timeout] 91 | http.read_timeout = options[:timeout] 92 | end 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 | 102 | if options[:debug_output] 103 | http.set_debug_output(options[:debug_output]) 104 | end 105 | 106 | if options[:ciphers] 107 | http.ciphers = options[:ciphers] 108 | end 109 | 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 130 | end 131 | 132 | private 133 | 134 | def clean_host(host) 135 | strip_ipv6_brackets(host) 136 | end 137 | 138 | def strip_ipv6_brackets(host) 139 | StripIpv6BracketsRegex =~ host ? $1 : host 140 | end 141 | 142 | def ssl_implied?(uri) 143 | uri.port == 443 || uri.scheme == 'https' 144 | end 145 | 146 | def verify_ssl_certificate? 147 | !(options[:verify] == false || options[:verify_peer] == false) 148 | end 149 | 150 | def attach_ssl_certificates(http, options) 151 | if http.use_ssl? 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 164 | 165 | # Client certificate authentication 166 | # Note: options[:pem] must contain the content of a PEM file having the private key appended 167 | if options[:pem] 168 | http.cert = OpenSSL::X509::Certificate.new(options[:pem]) 169 | http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password]) 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 179 | end 180 | 181 | # SSL certificate authority file and/or directory 182 | if options[:ssl_ca_file] 183 | http.ca_file = options[:ssl_ca_file] 184 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 185 | end 186 | 187 | if options[:ssl_ca_path] 188 | http.ca_path = options[:ssl_ca_path] 189 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 190 | end 191 | 192 | # This is only Ruby 1.9+ 193 | if options[:ssl_version] && http.respond_to?(:ssl_version=) 194 | http.ssl_version = options[:ssl_version] 195 | end 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/httparty/net_digest_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe Net::HTTPHeader::DigestAuthenticator do 4 | def setup_digest(response) 5 | digest = Net::HTTPHeader::DigestAuthenticator.new("Mufasa", 6 | "Circle Of Life", "GET", "/dir/index.html", response) 7 | allow(digest).to receive(:random).and_return("deadbeef") 8 | allow(Digest::MD5).to receive(:hexdigest) { |str| "md5(#{str})" } 9 | digest 10 | end 11 | 12 | def authorization_header 13 | @digest.authorization_header.join(", ") 14 | end 15 | 16 | def cookie_header 17 | @digest.cookie_header 18 | end 19 | 20 | context "with a cookie value in the response header" do 21 | before do 22 | @digest = setup_digest({ 23 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"', 24 | 'Set-Cookie' => 'custom-cookie=1234567' 25 | }) 26 | end 27 | 28 | it "should set cookie header" do 29 | expect(cookie_header).to include('custom-cookie=1234567') 30 | end 31 | end 32 | 33 | context "without a cookie value in the response header" do 34 | before do 35 | @digest = setup_digest({ 36 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"' 37 | }) 38 | end 39 | 40 | it "should set empty cookie header array" do 41 | expect(cookie_header).to eql [] 42 | end 43 | end 44 | 45 | context "with an opaque value in the response header" do 46 | before do 47 | @digest = setup_digest({ 48 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", opaque="solid"' 49 | }) 50 | end 51 | 52 | it "should set opaque" do 53 | expect(authorization_header).to include('opaque="solid"') 54 | end 55 | end 56 | 57 | context "without an opaque valid in the response header" do 58 | before do 59 | @digest = setup_digest({ 60 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"' 61 | }) 62 | end 63 | 64 | it "should not set opaque" do 65 | expect(authorization_header).not_to include("opaque=") 66 | end 67 | end 68 | 69 | context "with specified quality of protection (qop)" do 70 | before do 71 | @digest = setup_digest({ 72 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth"' 73 | }) 74 | end 75 | 76 | it "should set prefix" do 77 | expect(authorization_header).to match(/^Digest /) 78 | end 79 | 80 | it "should set username" do 81 | expect(authorization_header).to include('username="Mufasa"') 82 | end 83 | 84 | it "should set digest-uri" do 85 | expect(authorization_header).to include('uri="/dir/index.html"') 86 | end 87 | 88 | it "should set qop" do 89 | expect(authorization_header).to include('qop="auth"') 90 | end 91 | 92 | it "should set cnonce" do 93 | expect(authorization_header).to include('cnonce="md5(deadbeef)"') 94 | end 95 | 96 | it "should set nonce-count" do 97 | expect(authorization_header).to include("nc=00000001") 98 | end 99 | 100 | it "should set response" do 101 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 102 | expect(authorization_header).to include(%(response="#{request_digest}")) 103 | end 104 | end 105 | 106 | context "when quality of protection (qop) is unquoted" do 107 | before do 108 | @digest = setup_digest({ 109 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop=auth' 110 | }) 111 | end 112 | 113 | it "should still set qop" do 114 | expect(authorization_header).to include('qop="auth"') 115 | end 116 | end 117 | 118 | context "with unspecified quality of protection (qop)" do 119 | before do 120 | @digest = setup_digest({ 121 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE"' 122 | }) 123 | end 124 | 125 | it "should set prefix" do 126 | expect(authorization_header).to match(/^Digest /) 127 | end 128 | 129 | it "should set username" do 130 | expect(authorization_header).to include('username="Mufasa"') 131 | end 132 | 133 | it "should set digest-uri" do 134 | expect(authorization_header).to include('uri="/dir/index.html"') 135 | end 136 | 137 | it "should not set qop" do 138 | expect(authorization_header).not_to include("qop=") 139 | end 140 | 141 | it "should not set cnonce" do 142 | expect(authorization_header).not_to include("cnonce=") 143 | end 144 | 145 | it "should not set nonce-count" do 146 | expect(authorization_header).not_to include("nc=") 147 | end 148 | 149 | it "should set response" do 150 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:md5(GET:/dir/index.html))" 151 | expect(authorization_header).to include(%(response="#{request_digest}")) 152 | end 153 | end 154 | 155 | context "with http basic auth response when net digest auth expected" do 156 | it "should not fail" do 157 | @digest = setup_digest({ 158 | 'www-authenticate' => 'WWW-Authenticate: Basic realm="testrealm.com""' 159 | }) 160 | 161 | expect(authorization_header).to include("Digest") 162 | end 163 | end 164 | 165 | context "with multiple authenticate headers" do 166 | before do 167 | @digest = setup_digest({ 168 | 'www-authenticate' => 'NTLM, Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth"' 169 | }) 170 | end 171 | 172 | it "should set prefix" do 173 | expect(authorization_header).to match(/^Digest /) 174 | end 175 | 176 | it "should set username" do 177 | expect(authorization_header).to include('username="Mufasa"') 178 | end 179 | 180 | it "should set digest-uri" do 181 | expect(authorization_header).to include('uri="/dir/index.html"') 182 | end 183 | 184 | it "should set qop" do 185 | expect(authorization_header).to include('qop="auth"') 186 | end 187 | 188 | it "should set cnonce" do 189 | expect(authorization_header).to include('cnonce="md5(deadbeef)"') 190 | end 191 | 192 | it "should set nonce-count" do 193 | expect(authorization_header).to include("nc=00000001") 194 | end 195 | 196 | it "should set response" do 197 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 198 | expect(authorization_header).to include(%(response="#{request_digest}")) 199 | end 200 | end 201 | 202 | context "with algorithm specified" do 203 | before do 204 | @digest = setup_digest({ 205 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5' 206 | }) 207 | end 208 | 209 | it "should recognise algorithm was specified" do 210 | expect( @digest.send :algorithm_present? ).to be(true) 211 | end 212 | 213 | it "should set the algorithm header" do 214 | expect(authorization_header).to include('algorithm="MD5"') 215 | end 216 | end 217 | 218 | context "with md5-sess algorithm specified" do 219 | before do 220 | @digest = setup_digest({ 221 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5-sess' 222 | }) 223 | end 224 | 225 | it "should recognise algorithm was specified" do 226 | expect( @digest.send :algorithm_present? ).to be(true) 227 | end 228 | 229 | it "should set the algorithm header" do 230 | expect(authorization_header).to include('algorithm="MD5-sess"') 231 | end 232 | 233 | it "should set response using md5-sess algorithm" do 234 | request_digest = "md5(md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:md5(deadbeef)):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 235 | expect(authorization_header).to include(%(response="#{request_digest}")) 236 | end 237 | 238 | end 239 | 240 | end 241 | -------------------------------------------------------------------------------- /spec/fixtures/twitter.json: -------------------------------------------------------------------------------- 1 | [{"user":{"followers_count":1,"description":null,"url":null,"profile_image_url":"http:\/\/static.twitter.com\/images\/default_profile_normal.png","protected":false,"location":"Opera Plaza, California","screen_name":"Pyk","name":"Pyk","id":"7694602"},"text":"@lapilofu If you need a faster transfer, target disk mode should work too :)","truncated":false,"favorited":false,"in_reply_to_user_id":6128642,"created_at":"Sat Dec 06 21:29:14 +0000 2008","source":"twitterrific","in_reply_to_status_id":1042497164,"id":"1042500587"},{"user":{"followers_count":278,"description":"しがないプログラマ\/とりあえず宮子は俺の嫁\u2026いや、娘だ!\/Delphi&Pythonプログラマ修行中\/bot製作にハマりつつある。三つほど製作&構想中\/[eof]","url":"http:\/\/logiq.orz.hm\/","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/59588711\/l_ワ_l↑_normal.png","protected":false,"location":"ひだまり荘202号室","screen_name":"feiz","name":"azkn3","id":"14310520"},"text":"@y_benjo ちょー遅レスですがただのはだいろすぎる・・・ ( ll ワ ll )","truncated":false,"favorited":false,"in_reply_to_user_id":8428752,"created_at":"Sat Dec 06 21:29:14 +0000 2008","source":"twit","in_reply_to_status_id":1042479758,"id":"1042500586"},{"user":{"followers_count":1233,"description":""to understand one life you must swallow the world." I run refine+focus: a marketing agency working w\/ brands, media and VCs. http:\/\/tinyurl.com\/63mrn","url":"http:\/\/www.quiverandquill.com","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/53684650\/539059005_2a3b462d20_normal.jpg","protected":false,"location":"Cambridge, MA ","screen_name":"quiverandquill","name":"zach Braiker","id":"6845872"},"text":"@18percentgrey I didn't see Damon on Palin. i'll look on youtube. thx .Z","truncated":false,"favorited":false,"in_reply_to_user_id":10529932,"created_at":"Sat Dec 06 21:29:12 +0000 2008","source":"web","in_reply_to_status_id":1042499331,"id":"1042500584"},{"user":{"followers_count":780,"description":"Mein Blog ist unter http:\/\/blog.helmschrott.de zu finden. Unter http:\/\/blogalm.de kannst Du Deinen Blog eintragen!","url":"http:\/\/helmschrott.de\/blog","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/60997686\/avatar-250_normal.jpg","protected":false,"location":"Münchner Straße","screen_name":"helmi","name":"Frank Helmschrott","id":"867641"},"text":"@gigold auch mist oder?ich glaub ich fangs jetzt dann einfach mal an - wird schon vernünftige update-prozesse geben.","truncated":false,"favorited":false,"in_reply_to_user_id":959931,"created_at":"Sat Dec 06 21:29:11 +0000 2008","source":"twhirl","in_reply_to_status_id":1042500095,"id":"1042500583"},{"user":{"followers_count":63,"description":"Liberation from Misconceptions","url":"http:\/\/sexorcism.blogspot.com","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/63897302\/having-sex-on-bed_normal.jpg","protected":false,"location":"USA","screen_name":"Sexorcism","name":"Sexorcism","id":"16929435"},"text":"@thursdays_child someecards might.","truncated":false,"favorited":false,"in_reply_to_user_id":14484963,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"twittergadget","in_reply_to_status_id":1042499777,"id":"1042500581"},{"user":{"followers_count":106,"description":"Researcher. Maître de Conférences - Sémiologue - Spécialiste des médias audiovisuels. Analyste des médias, de la télévision et de la presse people (gossip). ","url":"http:\/\/semioblog.over-blog.org\/","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/57988482\/Thomas_et_Vic_pour_promo_2_058_normal.JPG","protected":false,"location":"France","screen_name":"semioblog","name":"Virginie Spies","id":"10078802"},"text":"@richardvonstern on reparle de tout cela bientôt, si vous voulez vraiment m'aider","truncated":false,"favorited":false,"in_reply_to_user_id":15835317,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"twitterrific","in_reply_to_status_id":1042357537,"id":"1042500580"},{"user":{"followers_count":26,"description":"","url":"","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/63461084\/November2ndpics_125_normal.jpg","protected":false,"location":"Louisville, Ky","screen_name":"scrapaunt","name":"scrapaunt","id":"16660671"},"text":"@NKOTB4LIFE Hope your neck feels better after your nap.","truncated":false,"favorited":false,"in_reply_to_user_id":16041403,"created_at":"Sat Dec 06 21:29:10 +0000 2008","source":"web","in_reply_to_status_id":1042450159,"id":"1042500579"},{"user":{"followers_count":245,"description":"Maui HI Real Estate Salesperson specializing in off the grid lifestyle","url":"http:\/\/www.eastmaui.com","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/59558480\/face2_normal.jpg","protected":false,"location":"north shore maui hawaii","screen_name":"mauihunter","name":"Georgina M. Hunter ","id":"16161708"},"text":"@BeeRealty http:\/\/twitpic.com\/qpog - It's a good safe place to lay - no dogs up there.","truncated":false,"favorited":false,"in_reply_to_user_id":15781063,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"twitpic","in_reply_to_status_id":1042497815,"id":"1042500578"},{"user":{"followers_count":95,"description":"","url":"","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/66581657\/nose-pick_normal.jpg","protected":false,"location":"zoetermeer","screen_name":"GsKlukkluk","name":"Klukkluk","id":"14218588"},"text":"twit \/off zalige nacht!","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:14 +0000 2008","source":"web","in_reply_to_status_id":null,"id":"1042500577"},{"user":{"followers_count":33,"description":"Living in denial that I live in a podunk town, I spend my time in search of good music in LA. Pure city-dweller and puma. That's about it.","url":"","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/56024131\/Photo_145_normal.jpg","protected":false,"location":"Santa Barbara, CA","screen_name":"pumainthemvmt","name":"Valerie","id":"15266837"},"text":"I love my parents with all my heart, but sometimes they make me want to scratch my eyes out.","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:10 +0000 2008","source":"sms","in_reply_to_status_id":null,"id":"1042500576"},{"user":{"followers_count":99,"description":"大学生ですよ。Ad[es]er。趣味で辭書とか編輯してゐます。JavaScriptでゲーム書きたいけど時間ねえ。","url":"http:\/\/greengablez.net\/diary\/","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/60269370\/zonu_1_normal.gif","protected":false,"location":"Sapporo, Hokkaido, Japan","screen_name":"tadsan","name":"船越次男","id":"11637282"},"text":"リトル・プリンセスとだけ書かれたら小公女を連想するだろ、常識的に考へて。","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:11 +0000 2008","source":"tiitan","in_reply_to_status_id":null,"id":"1042500575"},{"user":{"followers_count":68,"description":"I love all things beer. What goes better with beer than Porn, nothig I tell ya nothing.","url":"","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/66479069\/Picture_9_normal.jpg","protected":false,"location":"Durant","screen_name":"Jeffporn","name":"Jeffporn","id":"14065262"},"text":"At Lefthand having milk stout on cask - Photo: http:\/\/bkite.com\/02PeH","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:10 +0000 2008","source":"brightkite","in_reply_to_status_id":null,"id":"1042500574"},{"user":{"followers_count":7,"description":"","url":"http:\/\/www.PeteKinser.com","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/65572489\/PeteKinser-close_normal.jpg","protected":false,"location":"Denver, CO","screen_name":"pkinser","name":"pkinser","id":"15570525"},"text":"Snooze is where it's at for brunch if you're ever in Denver. Yum.","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:11 +0000 2008","source":"fring","in_reply_to_status_id":null,"id":"1042500572"},{"user":{"followers_count":75,"description":"I am a gamer and this is my gaming account, check out my other Twitter account for non-gaming tweets.","url":"http:\/\/twitter.com\/Nailhead","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/56881055\/nailhead_184x184_normal.jpg","protected":false,"location":"Huntsville, AL","screen_name":"Nailhead_Gamer","name":"Eric Fullerton","id":"15487663"},"text":"Completed the epic quest line for the Death Knight. Now what? Outlands? I wish to skip Outlands please, thanks.","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"twitterfox","in_reply_to_status_id":null,"id":"1042500571"},{"user":{"followers_count":111,"description":"","url":"http:\/\/www.ns-tech.com\/blog\/geldred.nsf","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/63865052\/brak2_normal.JPG","protected":false,"location":"Cleveland OH","screen_name":"geldred","name":"geldred","id":"14093394"},"text":"I'm at Target Store - Avon OH (35830 Detroit Rd, Avon, OH 44011, USA) - http:\/\/bkite.com\/02PeI","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"brightkite","in_reply_to_status_id":null,"id":"1042500570"},{"user":{"followers_count":16,"description":"Soundtrack Composer\/Musician\/Producer","url":"http:\/\/www.reverbnation\/musicbystratos","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/63311865\/logo-stratos_normal.png","protected":false,"location":"Grove City, Ohio 43123","screen_name":"Stratos","name":"Bryan K Borgman","id":"756062"},"text":"is reminded how beautiful the world can be when it's blanketed by clean white snow.","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"web","in_reply_to_status_id":null,"id":"1042500569"},{"user":{"followers_count":7,"description":null,"url":null,"profile_image_url":"http:\/\/static.twitter.com\/images\/default_profile_normal.png","protected":false,"location":null,"screen_name":"garrettromine","name":"garrettromine","id":"16120885"},"text":"Go Julio","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:10 +0000 2008","source":"sms","in_reply_to_status_id":null,"id":"1042500568"},{"user":{"followers_count":111,"description":"WHAT IS HAPPNING IN THE WORLD??? SEE DIFFERENT NEWS STORIES FROM MANY SOURCES.","url":"http:\/\/henrynews.wetpaint.com","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/65118112\/2008-election-map-nytimes_normal.png","protected":false,"location":"","screen_name":"HenryNews","name":"HenryNews","id":"17398510"},"text":"Svindal completes double at Beaver Creek: Read full story for latest details. http:\/\/tinyurl.com\/6qugub","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"twitterfeed","in_reply_to_status_id":null,"id":"1042500567"},{"user":{"followers_count":34,"description":"I am a man of many bio's, scott bio's!","url":"http:\/\/flickr.com\/photos\/giantcandy","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/25680382\/Icon_for_Twitter_normal.jpg","protected":false,"location":"Loves Park, IL, USA","screen_name":"Pychobj2001","name":"William Boehm Jr","id":"809103"},"text":"I have a 3rd break light and the license plate lights are out...just replacing 1 plate light...abide by law just enough","truncated":false,"favorited":false,"in_reply_to_user_id":null,"created_at":"Sat Dec 06 21:29:10 +0000 2008","source":"twidroid","in_reply_to_status_id":null,"id":"1042500566"},{"user":{"followers_count":61,"description":"Wife. Designer. Green Enthusiast. New Homeowner. Pet Owner. Internet Addict.","url":"http:\/\/confessionsofadesignjunkie.blogspot.com\/","profile_image_url":"http:\/\/s3.amazonaws.com\/twitter_production\/profile_images\/66044439\/n27310459_33432814_6743-1_normal.jpg","protected":false,"location":"Indiana","screen_name":"smquaseb","name":"Stacy","id":"15530992"},"text":"@Indygardener We still had a few people shoveling in our neighborhood - I didn't think it was enough to shovel, but keeps the kids busy.","truncated":false,"favorited":false,"in_reply_to_user_id":12811482,"created_at":"Sat Dec 06 21:29:13 +0000 2008","source":"web","in_reply_to_status_id":1042488558,"id":"1042500565"}] -------------------------------------------------------------------------------- /spec/httparty/response_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::Response do 4 | before do 5 | @last_modified = Date.new(2010, 1, 15).to_s 6 | @content_length = '1024' 7 | @request_object = HTTParty::Request.new Net::HTTP::Get, '/' 8 | @response_object = Net::HTTPOK.new('1.1', 200, 'OK') 9 | allow(@response_object).to receive_messages(body: "{foo:'bar'}") 10 | @response_object['last-modified'] = @last_modified 11 | @response_object['content-length'] = @content_length 12 | @parsed_response = lambda { {"foo" => "bar"} } 13 | @response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 14 | end 15 | 16 | describe ".underscore" do 17 | it "works with one capitalized word" do 18 | expect(HTTParty::Response.underscore("Accepted")).to eq("accepted") 19 | end 20 | 21 | it "works with titlecase" do 22 | expect(HTTParty::Response.underscore("BadGateway")).to eq("bad_gateway") 23 | end 24 | 25 | it "works with all caps" do 26 | expect(HTTParty::Response.underscore("OK")).to eq("ok") 27 | end 28 | end 29 | 30 | describe "initialization" do 31 | it "should set the Net::HTTP Response" do 32 | expect(@response.response).to eq(@response_object) 33 | end 34 | 35 | it "should set body" do 36 | expect(@response.body).to eq(@response_object.body) 37 | end 38 | 39 | it "should set code" do 40 | expect(@response.code).to eq(@response_object.code) 41 | end 42 | 43 | it "should set code as a Fixnum" do 44 | expect(@response.code).to be_an_instance_of(Fixnum) 45 | end 46 | 47 | context 'when raise_on is supplied' do 48 | let(:request) { HTTParty::Request.new(Net::HTTP::Get, '/', raise_on: [404]) } 49 | 50 | context "and response's status code is in range" do 51 | let(:body) { 'Not Found' } 52 | let(:response) { Net::HTTPNotFound.new('1.1', 404, body) } 53 | 54 | before do 55 | allow(response).to receive(:body).and_return(body) 56 | end 57 | 58 | subject { described_class.new(request, response, @parsed_response) } 59 | 60 | it 'throws exception' do 61 | expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}") 62 | end 63 | end 64 | 65 | context "and response's status code is not in range" do 66 | subject { described_class.new(request, @response_object, @parsed_response) } 67 | 68 | it 'does not throw exception' do 69 | expect{ subject }.not_to raise_error(HTTParty::ResponseError) 70 | end 71 | end 72 | end 73 | end 74 | 75 | it "returns response headers" do 76 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 77 | expect(response.headers).to eq({'last-modified' => [@last_modified], 'content-length' => [@content_length]}) 78 | end 79 | 80 | it "should send missing methods to delegate" do 81 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 82 | expect(response['foo']).to eq('bar') 83 | end 84 | 85 | it "response to request" do 86 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 87 | expect(response.respond_to?(:request)).to be_truthy 88 | end 89 | 90 | it "responds to response" do 91 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 92 | expect(response.respond_to?(:response)).to be_truthy 93 | end 94 | 95 | it "responds to body" do 96 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 97 | expect(response.respond_to?(:body)).to be_truthy 98 | end 99 | 100 | it "responds to headers" do 101 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 102 | expect(response.respond_to?(:headers)).to be_truthy 103 | end 104 | 105 | it "responds to parsed_response" do 106 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 107 | expect(response.respond_to?(:parsed_response)).to be_truthy 108 | end 109 | 110 | it "responds to predicates" do 111 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 112 | expect(response.respond_to?(:success?)).to be_truthy 113 | end 114 | 115 | it "responds to anything parsed_response responds to" do 116 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 117 | expect(response.respond_to?(:[])).to be_truthy 118 | end 119 | 120 | it "should be able to iterate if it is array" do 121 | response = HTTParty::Response.new(@request_object, @response_object, lambda { [{'foo' => 'bar'}, {'foo' => 'baz'}] }) 122 | expect(response.size).to eq(2) 123 | expect { 124 | response.each { |item| } 125 | }.to_not raise_error 126 | end 127 | 128 | it "allows headers to be accessed by mixed-case names in hash notation" do 129 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 130 | expect(response.headers['Content-LENGTH']).to eq(@content_length) 131 | end 132 | 133 | it "returns a comma-delimited value when multiple values exist" do 134 | @response_object.add_field 'set-cookie', 'csrf_id=12345; path=/' 135 | @response_object.add_field 'set-cookie', '_github_ses=A123CdE; path=/' 136 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 137 | expect(response.headers['set-cookie']).to eq("csrf_id=12345; path=/, _github_ses=A123CdE; path=/") 138 | end 139 | 140 | # Backwards-compatibility - previously, #headers returned a Hash 141 | it "responds to hash methods" do 142 | response = HTTParty::Response.new(@request_object, @response_object, @parsed_response) 143 | hash_methods = {}.methods - response.headers.methods 144 | hash_methods.each do |method_name| 145 | expect(response.headers.respond_to?(method_name)).to be_truthy 146 | end 147 | end 148 | 149 | describe "#is_a?" do 150 | subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) } 151 | 152 | it { is_expected.to respond_to(:is_a?).with(1).arguments } 153 | it { expect(subject.is_a?(HTTParty::Response)).to be_truthy } 154 | it { expect(subject.is_a?(BasicObject)).to be_truthy } 155 | it { expect(subject.is_a?(Object)).to be_falsey } 156 | end 157 | 158 | describe "#kind_of?" do 159 | subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) } 160 | 161 | it { is_expected.to respond_to(:kind_of?).with(1).arguments } 162 | it { expect(subject.kind_of?(HTTParty::Response)).to be_truthy } 163 | it { expect(subject.kind_of?(BasicObject)).to be_truthy } 164 | it { expect(subject.kind_of?(Object)).to be_falsey } 165 | end 166 | 167 | describe "semantic methods for response codes" do 168 | def response_mock(klass) 169 | response = klass.new('', '', '') 170 | allow(response).to receive(:body) 171 | response 172 | end 173 | 174 | context "major codes" do 175 | it "is information" do 176 | net_response = response_mock(Net::HTTPInformation) 177 | response = HTTParty::Response.new(@request_object, net_response, '') 178 | expect(response.information?).to be_truthy 179 | end 180 | 181 | it "is success" do 182 | net_response = response_mock(Net::HTTPSuccess) 183 | response = HTTParty::Response.new(@request_object, net_response, '') 184 | expect(response.success?).to be_truthy 185 | end 186 | 187 | it "is redirection" do 188 | net_response = response_mock(Net::HTTPRedirection) 189 | response = HTTParty::Response.new(@request_object, net_response, '') 190 | expect(response.redirection?).to be_truthy 191 | end 192 | 193 | it "is client error" do 194 | net_response = response_mock(Net::HTTPClientError) 195 | response = HTTParty::Response.new(@request_object, net_response, '') 196 | expect(response.client_error?).to be_truthy 197 | end 198 | 199 | it "is server error" do 200 | net_response = response_mock(Net::HTTPServerError) 201 | response = HTTParty::Response.new(@request_object, net_response, '') 202 | expect(response.server_error?).to be_truthy 203 | end 204 | end 205 | 206 | context "for specific codes" do 207 | SPECIFIC_CODES = { 208 | accepted?: Net::HTTPAccepted, 209 | bad_gateway?: Net::HTTPBadGateway, 210 | bad_request?: Net::HTTPBadRequest, 211 | conflict?: Net::HTTPConflict, 212 | continue?: Net::HTTPContinue, 213 | created?: Net::HTTPCreated, 214 | expectation_failed?: Net::HTTPExpectationFailed, 215 | forbidden?: Net::HTTPForbidden, 216 | found?: Net::HTTPFound, 217 | gateway_time_out?: Net::HTTPGatewayTimeOut, 218 | gone?: Net::HTTPGone, 219 | internal_server_error?: Net::HTTPInternalServerError, 220 | length_required?: Net::HTTPLengthRequired, 221 | method_not_allowed?: Net::HTTPMethodNotAllowed, 222 | moved_permanently?: Net::HTTPMovedPermanently, 223 | multiple_choice?: Net::HTTPMultipleChoice, 224 | no_content?: Net::HTTPNoContent, 225 | non_authoritative_information?: Net::HTTPNonAuthoritativeInformation, 226 | not_acceptable?: Net::HTTPNotAcceptable, 227 | not_found?: Net::HTTPNotFound, 228 | not_implemented?: Net::HTTPNotImplemented, 229 | not_modified?: Net::HTTPNotModified, 230 | ok?: Net::HTTPOK, 231 | partial_content?: Net::HTTPPartialContent, 232 | payment_required?: Net::HTTPPaymentRequired, 233 | precondition_failed?: Net::HTTPPreconditionFailed, 234 | proxy_authentication_required?: Net::HTTPProxyAuthenticationRequired, 235 | request_entity_too_large?: Net::HTTPRequestEntityTooLarge, 236 | request_time_out?: Net::HTTPRequestTimeOut, 237 | request_uri_too_long?: Net::HTTPRequestURITooLong, 238 | requested_range_not_satisfiable?: Net::HTTPRequestedRangeNotSatisfiable, 239 | reset_content?: Net::HTTPResetContent, 240 | see_other?: Net::HTTPSeeOther, 241 | service_unavailable?: Net::HTTPServiceUnavailable, 242 | switch_protocol?: Net::HTTPSwitchProtocol, 243 | temporary_redirect?: Net::HTTPTemporaryRedirect, 244 | unauthorized?: Net::HTTPUnauthorized, 245 | unsupported_media_type?: Net::HTTPUnsupportedMediaType, 246 | use_proxy?: Net::HTTPUseProxy, 247 | version_not_supported?: Net::HTTPVersionNotSupported 248 | } 249 | 250 | # Ruby 2.0, new name for this response. 251 | if RUBY_VERSION >= "2.0.0" && ::RUBY_PLATFORM != "java" 252 | SPECIFIC_CODES[:multiple_choices?] = Net::HTTPMultipleChoices 253 | end 254 | 255 | SPECIFIC_CODES.each do |method, klass| 256 | it "responds to #{method}" do 257 | net_response = response_mock(klass) 258 | response = HTTParty::Response.new(@request_object, net_response, '') 259 | expect(response.__send__(method)).to be_truthy 260 | end 261 | end 262 | end 263 | end 264 | 265 | describe "headers" do 266 | it "can initialize without headers" do 267 | headers = HTTParty::Response::Headers.new 268 | expect(headers).to eq({}) 269 | end 270 | end 271 | 272 | describe "#tap" do 273 | it "is possible to tap into a response" do 274 | result = @response.tap(&:code) 275 | 276 | expect(result).to eq @response 277 | end 278 | end 279 | 280 | describe "#inspect" do 281 | it "works" do 282 | inspect = @response.inspect 283 | expect(inspect).to include("HTTParty::Response:0x") 284 | expect(inspect).to include("parsed_response={\"foo\"=>\"bar\"}") 285 | expect(inspect).to include("@response=#") 286 | expect(inspect).to include("@headers={") 287 | expect(inspect).to include("last-modified") 288 | expect(inspect).to include("content-length") 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /lib/httparty/request.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module HTTParty 4 | class Request #:nodoc: 5 | SupportedHTTPMethods = [ 6 | Net::HTTP::Get, 7 | Net::HTTP::Post, 8 | Net::HTTP::Patch, 9 | Net::HTTP::Put, 10 | Net::HTTP::Delete, 11 | Net::HTTP::Head, 12 | Net::HTTP::Options, 13 | Net::HTTP::Move, 14 | Net::HTTP::Copy, 15 | Net::HTTP::Mkcol, 16 | ] 17 | 18 | SupportedURISchemes = ['http', 'https', 'webcal', nil] 19 | 20 | NON_RAILS_QUERY_STRING_NORMALIZER = proc do |query| 21 | Array(query).sort_by { |a| a[0].to_s }.map do |key, value| 22 | if value.nil? 23 | key.to_s 24 | elsif value.respond_to?(:to_ary) 25 | value.to_ary.map {|v| "#{key}=#{ERB::Util.url_encode(v.to_s)}"} 26 | else 27 | HashConversions.to_params(key => value) 28 | end 29 | end.flatten.join('&') 30 | end 31 | 32 | attr_accessor :http_method, :options, :last_response, :redirect, :last_uri 33 | attr_reader :path 34 | 35 | def initialize(http_method, path, o = {}) 36 | self.http_method = http_method 37 | self.options = { 38 | limit: o.delete(:no_follow) ? 1 : 5, 39 | assume_utf16_is_big_endian: true, 40 | default_params: {}, 41 | follow_redirects: true, 42 | parser: Parser, 43 | uri_adapter: URI, 44 | connection_adapter: ConnectionAdapter 45 | }.merge(o) 46 | self.path = path 47 | set_basic_auth_from_uri 48 | end 49 | 50 | def path=(uri) 51 | uri_adapter = options[:uri_adapter] 52 | 53 | @path = if uri.is_a?(uri_adapter) 54 | uri 55 | elsif String.try_convert(uri) 56 | uri_adapter.parse uri 57 | else 58 | raise ArgumentError, 59 | "bad argument (expected #{uri_adapter} object or URI string)" 60 | end 61 | end 62 | 63 | def request_uri(uri) 64 | if uri.respond_to? :request_uri 65 | uri.request_uri 66 | else 67 | uri.path 68 | end 69 | end 70 | 71 | def uri 72 | if redirect && path.relative? && path.path[0] != "/" 73 | last_uri_host = @last_uri.path.gsub(/[^\/]+$/, "") 74 | 75 | path.path = "/#{path.path}" if last_uri_host[-1] != "/" 76 | path.path = last_uri_host + path.path 77 | end 78 | 79 | if path.relative? && path.host 80 | new_uri = options[:uri_adapter].parse("#{@last_uri.scheme}:#{path}") 81 | elsif path.relative? 82 | new_uri = options[:uri_adapter].parse("#{base_uri}#{path}") 83 | else 84 | new_uri = path.clone 85 | end 86 | 87 | # avoid double query string on redirects [#12] 88 | unless redirect 89 | new_uri.query = query_string(new_uri) 90 | end 91 | 92 | unless SupportedURISchemes.include? new_uri.scheme 93 | raise UnsupportedURIScheme, "'#{new_uri}' Must be HTTP, HTTPS or Generic" 94 | end 95 | 96 | @last_uri = new_uri 97 | end 98 | 99 | def base_uri 100 | if redirect 101 | base_uri = "#{@last_uri.scheme}://#{@last_uri.host}" 102 | base_uri += ":#{@last_uri.port}" if @last_uri.port != 80 103 | base_uri 104 | else 105 | options[:base_uri] && HTTParty.normalize_base_uri(options[:base_uri]) 106 | end 107 | end 108 | 109 | def format 110 | options[:format] || (format_from_mimetype(last_response['content-type']) if last_response) 111 | end 112 | 113 | def parser 114 | options[:parser] 115 | end 116 | 117 | def connection_adapter 118 | options[:connection_adapter] 119 | end 120 | 121 | def perform(&block) 122 | validate 123 | setup_raw_request 124 | chunked_body = nil 125 | 126 | self.last_response = http.request(@raw_request) do |http_response| 127 | if block 128 | chunks = [] 129 | 130 | http_response.read_body do |fragment| 131 | chunks << fragment unless options[:stream_body] 132 | block.call(fragment) 133 | end 134 | 135 | chunked_body = chunks.join 136 | end 137 | end 138 | 139 | handle_deflation unless http_method == Net::HTTP::Head 140 | handle_host_redirection if response_redirects? 141 | handle_response(chunked_body, &block) 142 | end 143 | 144 | def raw_body 145 | @raw_request.body 146 | end 147 | 148 | private 149 | 150 | def http 151 | connection_adapter.call(uri, options) 152 | end 153 | 154 | def body 155 | options[:body].respond_to?(:to_hash) ? normalize_query(options[:body]) : options[:body] 156 | end 157 | 158 | def credentials 159 | (options[:basic_auth] || options[:digest_auth]).to_hash 160 | end 161 | 162 | def username 163 | credentials[:username] 164 | end 165 | 166 | def password 167 | credentials[:password] 168 | end 169 | 170 | def normalize_query(query) 171 | if query_string_normalizer 172 | query_string_normalizer.call(query) 173 | else 174 | HashConversions.to_params(query) 175 | end 176 | end 177 | 178 | def query_string_normalizer 179 | options[:query_string_normalizer] 180 | end 181 | 182 | def setup_raw_request 183 | @raw_request = http_method.new(request_uri(uri)) 184 | @raw_request.body = body if body 185 | @raw_request.body_stream = options[:body_stream] if options[:body_stream] 186 | @raw_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash) 187 | @raw_request.basic_auth(username, password) if options[:basic_auth] && send_authorization_header? 188 | setup_digest_auth if options[:digest_auth] 189 | end 190 | 191 | def setup_digest_auth 192 | auth_request = http_method.new(uri.request_uri) 193 | auth_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash) 194 | res = http.request(auth_request) 195 | 196 | if !res['www-authenticate'].nil? && res['www-authenticate'].length > 0 197 | @raw_request.digest_auth(username, password, res) 198 | end 199 | end 200 | 201 | def query_string(uri) 202 | query_string_parts = [] 203 | query_string_parts << uri.query unless uri.query.nil? 204 | 205 | if options[:query].respond_to?(:to_hash) 206 | query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash)) 207 | else 208 | query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty? 209 | query_string_parts << options[:query] unless options[:query].nil? 210 | end 211 | 212 | query_string_parts.reject!(&:empty?) unless query_string_parts == [""] 213 | query_string_parts.size > 0 ? query_string_parts.join('&') : nil 214 | end 215 | 216 | def get_charset 217 | content_type = last_response["content-type"] 218 | if content_type.nil? 219 | return nil 220 | end 221 | 222 | if content_type =~ /;\s*charset\s*=\s*([^=,;"\s]+)/i 223 | return $1 224 | end 225 | 226 | if content_type =~ /;\s*charset\s*=\s*"((\\.|[^\\"])+)"/i 227 | return $1.gsub(/\\(.)/, '\1') 228 | end 229 | 230 | nil 231 | end 232 | 233 | def encode_with_ruby_encoding(body, charset) 234 | encoding = Encoding.find(charset) 235 | body.force_encoding(encoding) 236 | rescue 237 | body 238 | end 239 | 240 | def assume_utf16_is_big_endian 241 | options[:assume_utf16_is_big_endian] 242 | end 243 | 244 | def encode_utf_16(body) 245 | if body.bytesize >= 2 246 | if body.getbyte(0) == 0xFF && body.getbyte(1) == 0xFE 247 | return body.force_encoding("UTF-16LE") 248 | elsif body.getbyte(0) == 0xFE && body.getbyte(1) == 0xFF 249 | return body.force_encoding("UTF-16BE") 250 | end 251 | end 252 | 253 | if assume_utf16_is_big_endian 254 | body.force_encoding("UTF-16BE") 255 | else 256 | body.force_encoding("UTF-16LE") 257 | end 258 | end 259 | 260 | def _encode_body(body) 261 | charset = get_charset 262 | 263 | if charset.nil? 264 | return body 265 | end 266 | 267 | if "utf-16".casecmp(charset) == 0 268 | encode_utf_16(body) 269 | else 270 | encode_with_ruby_encoding(body, charset) 271 | end 272 | end 273 | 274 | def encode_body(body) 275 | if "".respond_to?(:encoding) 276 | _encode_body(body) 277 | else 278 | body 279 | end 280 | end 281 | 282 | def handle_response(body, &block) 283 | if response_redirects? 284 | options[:limit] -= 1 285 | if options[:logger] 286 | logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format]) 287 | logger.format(self, last_response) 288 | end 289 | self.path = last_response['location'] 290 | self.redirect = true 291 | if last_response.class == Net::HTTPSeeOther 292 | unless options[:maintain_method_across_redirects] && options[:resend_on_redirect] 293 | self.http_method = Net::HTTP::Get 294 | end 295 | elsif last_response.code != '307' && last_response.code != '308' 296 | unless options[:maintain_method_across_redirects] 297 | self.http_method = Net::HTTP::Get 298 | end 299 | end 300 | capture_cookies(last_response) 301 | perform(&block) 302 | else 303 | body ||= last_response.body 304 | body = encode_body(body) 305 | Response.new(self, last_response, lambda { parse_response(body) }, body: body) 306 | end 307 | end 308 | 309 | # Inspired by Ruby 1.9 310 | def handle_deflation 311 | return if response_redirects? 312 | return if last_response.body.nil? 313 | 314 | case last_response["content-encoding"] 315 | when "gzip", "x-gzip" 316 | body_io = StringIO.new(last_response.body) 317 | last_response.body.replace Zlib::GzipReader.new(body_io).read 318 | last_response.delete('content-encoding') 319 | when "deflate" 320 | last_response.body.replace Zlib::Inflate.inflate(last_response.body) 321 | last_response.delete('content-encoding') 322 | end 323 | end 324 | 325 | def handle_host_redirection 326 | check_duplicate_location_header 327 | redirect_path = options[:uri_adapter].parse last_response['location'] 328 | return if redirect_path.relative? || path.host == redirect_path.host 329 | @changed_hosts = true 330 | end 331 | 332 | def check_duplicate_location_header 333 | location = last_response.get_fields('location') 334 | if location.is_a?(Array) && location.count > 1 335 | raise DuplicateLocationHeader.new(last_response) 336 | end 337 | end 338 | 339 | def send_authorization_header? 340 | !defined?(@changed_hosts) 341 | end 342 | 343 | def response_redirects? 344 | case last_response 345 | when Net::HTTPNotModified # 304 346 | false 347 | when Net::HTTPRedirection 348 | options[:follow_redirects] && last_response.key?('location') 349 | end 350 | end 351 | 352 | def parse_response(body) 353 | parser.call(body, format) 354 | end 355 | 356 | def capture_cookies(response) 357 | return unless response['Set-Cookie'] 358 | cookies_hash = HTTParty::CookieHash.new 359 | cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie'] 360 | response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) } 361 | options[:headers] ||= {} 362 | options[:headers]['Cookie'] = cookies_hash.to_cookie_string 363 | end 364 | 365 | # Uses the HTTP Content-Type header to determine the format of the 366 | # response It compares the MIME type returned to the types stored in the 367 | # SupportedFormats hash 368 | def format_from_mimetype(mimetype) 369 | if mimetype && parser.respond_to?(:format_from_mimetype) 370 | parser.format_from_mimetype(mimetype) 371 | end 372 | end 373 | 374 | def validate 375 | raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0 376 | raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method) 377 | raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].respond_to?(:to_hash) 378 | raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth] 379 | raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].respond_to?(:to_hash) 380 | raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].respond_to?(:to_hash) 381 | raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].respond_to?(:to_hash) 382 | end 383 | 384 | def post? 385 | Net::HTTP::Post == http_method 386 | end 387 | 388 | def set_basic_auth_from_uri 389 | if path.userinfo 390 | username, password = path.userinfo.split(':') 391 | options[:basic_auth] = {username: username, password: password} 392 | end 393 | end 394 | end 395 | end 396 | -------------------------------------------------------------------------------- /spec/httparty/connection_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | RSpec.describe HTTParty::ConnectionAdapter do 4 | describe "initialization" do 5 | let(:uri) { URI 'http://www.google.com' } 6 | it "takes a URI as input" do 7 | HTTParty::ConnectionAdapter.new(uri) 8 | end 9 | 10 | it "raises an ArgumentError if the uri is nil" do 11 | expect { HTTParty::ConnectionAdapter.new(nil) }.to raise_error ArgumentError 12 | end 13 | 14 | it "raises an ArgumentError if the uri is a String" do 15 | expect { HTTParty::ConnectionAdapter.new('http://www.google.com') }.to raise_error ArgumentError 16 | end 17 | 18 | it "sets the uri" do 19 | adapter = HTTParty::ConnectionAdapter.new(uri) 20 | expect(adapter.uri).to be uri 21 | end 22 | 23 | it "also accepts an optional options hash" do 24 | HTTParty::ConnectionAdapter.new(uri, {}) 25 | end 26 | 27 | it "sets the options" do 28 | options = {foo: :bar} 29 | adapter = HTTParty::ConnectionAdapter.new(uri, options) 30 | expect(adapter.options.keys).to include(:verify, :verify_peer, :foo) 31 | end 32 | end 33 | 34 | describe ".call" do 35 | it "generates an HTTParty::ConnectionAdapter instance with the given uri and options" do 36 | expect(HTTParty::ConnectionAdapter).to receive(:new).with(@uri, @options).and_return(double(connection: nil)) 37 | HTTParty::ConnectionAdapter.call(@uri, @options) 38 | end 39 | 40 | it "calls #connection on the connection adapter" do 41 | adapter = double('Adapter') 42 | connection = double('Connection') 43 | expect(adapter).to receive(:connection).and_return(connection) 44 | allow(HTTParty::ConnectionAdapter).to receive_messages(new: adapter) 45 | expect(HTTParty::ConnectionAdapter.call(@uri, @options)).to be connection 46 | end 47 | end 48 | 49 | describe '#connection' do 50 | let(:uri) { URI 'http://www.google.com' } 51 | let(:options) { Hash.new } 52 | let(:adapter) { HTTParty::ConnectionAdapter.new(uri, options) } 53 | 54 | describe "the resulting connection" do 55 | subject { adapter.connection } 56 | it { is_expected.to be_an_instance_of Net::HTTP } 57 | 58 | context "using port 80" do 59 | let(:uri) { URI 'http://foobar.com' } 60 | it { is_expected.not_to use_ssl } 61 | end 62 | 63 | context "when dealing with ssl" do 64 | let(:uri) { URI 'https://foobar.com' } 65 | 66 | context "uses the system cert_store, by default" do 67 | let!(:system_cert_store) do 68 | system_cert_store = double('default_cert_store') 69 | expect(system_cert_store).to receive(:set_default_paths) 70 | expect(OpenSSL::X509::Store).to receive(:new).and_return(system_cert_store) 71 | system_cert_store 72 | end 73 | it { is_expected.to use_cert_store(system_cert_store) } 74 | end 75 | 76 | context "should use the specified cert store, when one is given" do 77 | let(:custom_cert_store) { double('custom_cert_store') } 78 | let(:options) { {cert_store: custom_cert_store} } 79 | it { is_expected.to use_cert_store(custom_cert_store) } 80 | end 81 | 82 | context "using port 443 for ssl" do 83 | let(:uri) { URI 'https://api.foo.com/v1:443' } 84 | it { is_expected.to use_ssl } 85 | end 86 | 87 | context "https scheme with default port" do 88 | it { is_expected.to use_ssl } 89 | end 90 | 91 | context "https scheme with non-standard port" do 92 | let(:uri) { URI 'https://foobar.com:123456' } 93 | it { is_expected.to use_ssl } 94 | end 95 | 96 | context "when ssl version is set" do 97 | let(:options) { {ssl_version: :TLSv1} } 98 | 99 | it "sets ssl version" do 100 | expect(subject.ssl_version).to eq(:TLSv1) 101 | end 102 | end if RUBY_VERSION > '1.9' 103 | end 104 | 105 | context "when dealing with IPv6" do 106 | let(:uri) { URI 'http://[fd00::1]' } 107 | 108 | it "strips brackets from the address" do 109 | expect(subject.address).to eq('fd00::1') 110 | end 111 | end 112 | 113 | context "specifying ciphers" do 114 | let(:options) { {ciphers: 'RC4-SHA' } } 115 | 116 | it "should set the ciphers on the connection" do 117 | expect(subject.ciphers).to eq('RC4-SHA') 118 | end 119 | end if RUBY_VERSION > '1.9' 120 | 121 | context "when timeout is not set" do 122 | it "doesn't set the timeout" do 123 | http = double( 124 | "http", 125 | :null_object => true, 126 | :use_ssl= => false, 127 | :use_ssl? => false 128 | ) 129 | expect(http).not_to receive(:open_timeout=) 130 | expect(http).not_to receive(:read_timeout=) 131 | allow(Net::HTTP).to receive_messages(new: http) 132 | 133 | adapter.connection 134 | end 135 | end 136 | 137 | context "when setting timeout" do 138 | context "to 5 seconds" do 139 | let(:options) { {timeout: 5} } 140 | 141 | describe '#open_timeout' do 142 | subject { super().open_timeout } 143 | it { is_expected.to eq(5) } 144 | end 145 | 146 | describe '#read_timeout' do 147 | subject { super().read_timeout } 148 | it { is_expected.to eq(5) } 149 | end 150 | end 151 | 152 | context "and timeout is a string" do 153 | let(:options) { {timeout: "five seconds"} } 154 | 155 | it "doesn't set the timeout" do 156 | http = double( 157 | "http", 158 | :null_object => true, 159 | :use_ssl= => false, 160 | :use_ssl? => false 161 | ) 162 | expect(http).not_to receive(:open_timeout=) 163 | expect(http).not_to receive(:read_timeout=) 164 | allow(Net::HTTP).to receive_messages(new: http) 165 | 166 | adapter.connection 167 | end 168 | end 169 | end 170 | 171 | context "when timeout is not set and read_timeout is set to 6 seconds" do 172 | let(:options) { {read_timeout: 6} } 173 | 174 | describe '#read_timeout' do 175 | subject { super().read_timeout } 176 | it { is_expected.to eq(6) } 177 | end 178 | 179 | it "should not set the open_timeout" do 180 | http = double( 181 | "http", 182 | :null_object => true, 183 | :use_ssl= => false, 184 | :use_ssl? => false, 185 | :read_timeout= => 0 186 | ) 187 | expect(http).not_to receive(:open_timeout=) 188 | allow(Net::HTTP).to receive_messages(new: http) 189 | adapter.connection 190 | end 191 | end 192 | 193 | context "when timeout is set and read_timeout is set to 6 seconds" do 194 | let(:options) { {timeout: 5, read_timeout: 6} } 195 | 196 | describe '#open_timeout' do 197 | subject { super().open_timeout } 198 | it { is_expected.to eq(5) } 199 | end 200 | 201 | describe '#read_timeout' do 202 | subject { super().read_timeout } 203 | it { is_expected.to eq(6) } 204 | end 205 | 206 | it "should override the timeout option" do 207 | http = double( 208 | "http", 209 | :null_object => true, 210 | :use_ssl= => false, 211 | :use_ssl? => false, 212 | :read_timeout= => 0, 213 | :open_timeout= => 0 214 | ) 215 | expect(http).to receive(:open_timeout=) 216 | expect(http).to receive(:read_timeout=).twice 217 | allow(Net::HTTP).to receive_messages(new: http) 218 | adapter.connection 219 | end 220 | end 221 | 222 | context "when timeout is not set and open_timeout is set to 7 seconds" do 223 | let(:options) { {open_timeout: 7} } 224 | 225 | describe '#open_timeout' do 226 | subject { super().open_timeout } 227 | it { is_expected.to eq(7) } 228 | end 229 | 230 | it "should not set the read_timeout" do 231 | http = double( 232 | "http", 233 | :null_object => true, 234 | :use_ssl= => false, 235 | :use_ssl? => false, 236 | :open_timeout= => 0 237 | ) 238 | expect(http).not_to receive(:read_timeout=) 239 | allow(Net::HTTP).to receive_messages(new: http) 240 | adapter.connection 241 | end 242 | end 243 | 244 | context "when timeout is set and open_timeout is set to 7 seconds" do 245 | let(:options) { {timeout: 5, open_timeout: 7} } 246 | 247 | describe '#open_timeout' do 248 | subject { super().open_timeout } 249 | it { is_expected.to eq(7) } 250 | end 251 | 252 | describe '#read_timeout' do 253 | subject { super().read_timeout } 254 | it { is_expected.to eq(5) } 255 | end 256 | 257 | it "should override the timeout option" do 258 | http = double( 259 | "http", 260 | :null_object => true, 261 | :use_ssl= => false, 262 | :use_ssl? => false, 263 | :read_timeout= => 0, 264 | :open_timeout= => 0 265 | ) 266 | expect(http).to receive(:open_timeout=).twice 267 | expect(http).to receive(:read_timeout=) 268 | allow(Net::HTTP).to receive_messages(new: http) 269 | adapter.connection 270 | end 271 | end 272 | 273 | context "when debug_output" do 274 | let(:http) { Net::HTTP.new(uri) } 275 | before do 276 | allow(Net::HTTP).to receive_messages(new: http) 277 | end 278 | 279 | context "is set to $stderr" do 280 | let(:options) { {debug_output: $stderr} } 281 | it "has debug output set" do 282 | expect(http).to receive(:set_debug_output).with($stderr) 283 | adapter.connection 284 | end 285 | end 286 | 287 | context "is not provided" do 288 | it "does not set_debug_output" do 289 | expect(http).not_to receive(:set_debug_output) 290 | adapter.connection 291 | end 292 | end 293 | end 294 | 295 | context 'when providing proxy address and port' do 296 | let(:options) { {http_proxyaddr: '1.2.3.4', http_proxyport: 8080} } 297 | 298 | it { is_expected.to be_a_proxy } 299 | 300 | describe '#proxy_address' do 301 | subject { super().proxy_address } 302 | it { is_expected.to eq('1.2.3.4') } 303 | end 304 | 305 | describe '#proxy_port' do 306 | subject { super().proxy_port } 307 | it { is_expected.to eq(8080) } 308 | end 309 | 310 | context 'as well as proxy user and password' do 311 | let(:options) do 312 | {http_proxyaddr: '1.2.3.4', http_proxyport: 8080, 313 | http_proxyuser: 'user', http_proxypass: 'pass'} 314 | end 315 | 316 | describe '#proxy_user' do 317 | subject { super().proxy_user } 318 | it { is_expected.to eq('user') } 319 | end 320 | 321 | describe '#proxy_pass' do 322 | subject { super().proxy_pass } 323 | it { is_expected.to eq('pass') } 324 | end 325 | end 326 | end 327 | 328 | context 'when providing nil as proxy address' do 329 | let(:uri) { URI 'http://noproxytest.com' } 330 | let(:options) { {http_proxyaddr: nil} } 331 | 332 | it { is_expected.not_to be_a_proxy } 333 | 334 | it "does pass nil proxy parameters to the connection, this forces to not use a proxy" do 335 | http = Net::HTTP.new("noproxytest.com") 336 | expect(Net::HTTP).to receive(:new).once.with("noproxytest.com", 80, nil, nil, nil, nil).and_return(http) 337 | adapter.connection 338 | end 339 | end 340 | 341 | context 'when not providing a proxy address' do 342 | let(:uri) { URI 'http://proxytest.com' } 343 | 344 | it "does not pass any proxy parameters to the connection" do 345 | http = Net::HTTP.new("proxytest.com") 346 | expect(Net::HTTP).to receive(:new).once.with("proxytest.com", 80).and_return(http) 347 | adapter.connection 348 | end 349 | end 350 | 351 | context 'when providing a local bind address and port' do 352 | let(:options) { {local_host: "127.0.0.1", local_port: 12345 } } 353 | 354 | describe '#local_host' do 355 | subject { super().local_host } 356 | it { is_expected.to eq('127.0.0.1') } 357 | end 358 | 359 | describe '#local_port' do 360 | subject { super().local_port } 361 | it { is_expected.to eq(12345) } 362 | end 363 | end if RUBY_VERSION >= '2.0' 364 | 365 | context "when providing PEM certificates" do 366 | let(:pem) { :pem_contents } 367 | let(:options) { {pem: pem, pem_password: "password"} } 368 | 369 | context "when scheme is https" do 370 | let(:uri) { URI 'https://google.com' } 371 | let(:cert) { double("OpenSSL::X509::Certificate") } 372 | let(:key) { double("OpenSSL::PKey::RSA") } 373 | 374 | before do 375 | expect(OpenSSL::X509::Certificate).to receive(:new).with(pem).and_return(cert) 376 | expect(OpenSSL::PKey::RSA).to receive(:new).with(pem, "password").and_return(key) 377 | end 378 | 379 | it "uses the provided PEM certificate" do 380 | expect(subject.cert).to eq(cert) 381 | expect(subject.key).to eq(key) 382 | end 383 | 384 | it "will verify the certificate" do 385 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) 386 | end 387 | 388 | context "when options include verify=false" do 389 | let(:options) { {pem: pem, pem_password: "password", verify: false} } 390 | 391 | it "should not verify the certificate" do 392 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) 393 | end 394 | end 395 | context "when options include verify_peer=false" do 396 | let(:options) { {pem: pem, pem_password: "password", verify_peer: false} } 397 | 398 | it "should not verify the certificate" do 399 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) 400 | end 401 | end 402 | end 403 | 404 | context "when scheme is not https" do 405 | let(:uri) { URI 'http://google.com' } 406 | let(:http) { Net::HTTP.new(uri) } 407 | 408 | before do 409 | allow(Net::HTTP).to receive_messages(new: http) 410 | expect(OpenSSL::X509::Certificate).not_to receive(:new).with(pem) 411 | expect(OpenSSL::PKey::RSA).not_to receive(:new).with(pem, "password") 412 | expect(http).not_to receive(:cert=) 413 | expect(http).not_to receive(:key=) 414 | end 415 | 416 | it "has no PEM certificate " do 417 | expect(subject.cert).to be_nil 418 | expect(subject.key).to be_nil 419 | end 420 | end 421 | end 422 | 423 | context "when providing PKCS12 certificates" do 424 | let(:p12) { :p12_contents } 425 | let(:options) { {p12: p12, p12_password: "password"} } 426 | 427 | context "when scheme is https" do 428 | let(:uri) { URI 'https://google.com' } 429 | let(:pkcs12) { double("OpenSSL::PKCS12", certificate: cert, key: key) } 430 | let(:cert) { double("OpenSSL::X509::Certificate") } 431 | let(:key) { double("OpenSSL::PKey::RSA") } 432 | 433 | before do 434 | expect(OpenSSL::PKCS12).to receive(:new).with(p12, "password").and_return(pkcs12) 435 | end 436 | 437 | it "uses the provided P12 certificate " do 438 | expect(subject.cert).to eq(cert) 439 | expect(subject.key).to eq(key) 440 | end 441 | 442 | it "will verify the certificate" do 443 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) 444 | end 445 | 446 | context "when options include verify=false" do 447 | let(:options) { {p12: p12, p12_password: "password", verify: false} } 448 | 449 | it "should not verify the certificate" do 450 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) 451 | end 452 | end 453 | context "when options include verify_peer=false" do 454 | let(:options) { {p12: p12, p12_password: "password", verify_peer: false} } 455 | 456 | it "should not verify the certificate" do 457 | expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) 458 | end 459 | end 460 | end 461 | 462 | context "when scheme is not https" do 463 | let(:uri) { URI 'http://google.com' } 464 | let(:http) { Net::HTTP.new(uri) } 465 | 466 | before do 467 | allow(Net::HTTP).to receive_messages(new: http) 468 | expect(OpenSSL::PKCS12).not_to receive(:new).with(p12, "password") 469 | expect(http).not_to receive(:cert=) 470 | expect(http).not_to receive(:key=) 471 | end 472 | 473 | it "has no PKCS12 certificate " do 474 | expect(subject.cert).to be_nil 475 | expect(subject.key).to be_nil 476 | end 477 | end 478 | end 479 | 480 | context "when uri port is not defined" do 481 | context "falls back to 80 port on http" do 482 | let(:uri) { URI 'http://foobar.com' } 483 | before { allow(uri).to receive(:port).and_return(nil) } 484 | it { expect(subject.port).to be 80 } 485 | end 486 | 487 | context "falls back to 443 port on https" do 488 | let(:uri) { URI 'https://foobar.com' } 489 | before { allow(uri).to receive(:port).and_return(nil) } 490 | it { expect(subject.port).to be 443 } 491 | end 492 | end 493 | end 494 | end 495 | end 496 | --------------------------------------------------------------------------------