http-4.4.1/0000755000004100000410000000000013640204124012523 5ustar www-datawww-datahttp-4.4.1/.travis.yml0000644000004100000410000000115713640204124014640 0ustar www-datawww-datalanguage: ruby sudo: false cache: bundler before_install: - gem update --system - gem --version - gem install bundler --no-document - bundle --version install: bundle install --without development doc script: bundle exec rake env: JRUBY_OPTS="$JRUBY_OPTS --debug" rvm: # Include JRuby first because it takes the longest - jruby-9.2.5.0 - 2.3 - 2.4 - 2.5 - 2.6 - 2.7 matrix: fast_finish: true include: # Only run RuboCop and Yardstick metrics on the latest Ruby - rvm: 2.7 env: SUITE="rubocop" - rvm: 2.7 env: SUITE="yardstick" branches: only: - master - 4-x-stable http-4.4.1/.rspec0000644000004100000410000000012013640204124013631 0ustar www-datawww-data--backtrace --color --format=documentation --order random --require spec_helper http-4.4.1/README.md0000644000004100000410000001525313640204124014010 0ustar www-datawww-data# ![http.rb](https://raw.github.com/httprb/http.rb/master/logo.png) [![Gem Version](https://badge.fury.io/rb/http.svg)](https://rubygems.org/gems/http) [![Build Status](https://secure.travis-ci.org/httprb/http.svg?branch=4-x-stable)](https://travis-ci.org/httprb/http) [![Code Climate](https://codeclimate.com/github/httprb/http.svg?branch=4-x-stable)](https://codeclimate.com/github/httprb/http) [![Coverage Status](https://coveralls.io/repos/httprb/http/badge.svg?branch=4-x-stable)](https://coveralls.io/r/httprb/http) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/httprb/http/blob/4-x-stable/LICENSE.txt) [Documentation] _NOTE: This is the 4.x **stable** branch. For the 3.x **stable** branch, please see:_ https://github.com/httprb/http/tree/3-x-stable ## About HTTP (The Gem! a.k.a. http.rb) is an easy-to-use client library for making requests from Ruby. It uses a simple method chaining system for building requests, similar to Python's [Requests]. Under the hood, http.rb uses [http_parser.rb], a fast HTTP parsing native extension based on the Node.js parser and a Java port thereof. This library isn't just yet another wrapper around Net::HTTP. It implements the HTTP protocol natively and outsources the parsing to native extensions. [requests]: http://docs.python-requests.org/en/latest/ [http_parser.rb]: https://github.com/tmm1/http_parser.rb ## Another Ruby HTTP library? Why should I care? There are a lot of HTTP libraries to choose from in the Ruby ecosystem. So why would you choose this one? Top three reasons: 1. **Clean API**: http.rb offers an easy-to-use API that should be a breath of fresh air after using something like Net::HTTP. 2. **Maturity**: http.rb is one of the most mature Ruby HTTP clients, supporting features like persistent connections and fine-grained timeouts. 3. **Performance**: using native parsers and a clean, lightweight implementation, http.rb achieves the best performance of any Ruby HTTP library which implements the HTTP protocol in Ruby instead of C: | HTTP client | Time | Implementation | |--------------------------|--------|-----------------------| | curb (persistent) | 2.519 | libcurl wrapper | | em-http-request | 2.731 | EM + http_parser.rb | | Typhoeus | 2.851 | libcurl wrapper | | StreamlyFFI (persistent) | 2.853 | libcurl wrapper | | http.rb (persistent) | 2.970 | Ruby + http_parser.rb | | http.rb | 3.588 | Ruby + http_parser.rb | | HTTParty | 3.931 | Net::HTTP wrapper | | Net::HTTP | 3.959 | Pure Ruby | | Net::HTTP (persistent) | 4.043 | Pure Ruby | | open-uri | 4.479 | Net::HTTP wrapper | | Excon (persistent) | 4.618 | Pure Ruby | | Excon | 4.701 | Pure Ruby | | RestClient | 26.838 | Net::HTTP wrapper | Benchmarks performed using excon's benchmarking tool DISCLAIMER: Most benchmarks you find in READMEs are crap, including this one. These are out-of-date. If you care about performance, benchmark for yourself for your own use cases! ## Help and Discussion If you need help or just want to talk about the http.rb, visit the http.rb Google Group: https://groups.google.com/forum/#!forum/httprb You can join by email by sending a message to: [httprb+subscribe@googlegroups.com](mailto:httprb+subscribe@googlegroups.com) If you believe you've found a bug, please report it at: https://github.com/httprb/http/issues ## Installation Add this line to your application's Gemfile: ```ruby gem "http" ``` And then execute: ```bash $ bundle ``` Or install it yourself as: ```bash $ gem install http ``` Inside of your Ruby program do: ```ruby require "http" ``` ...to pull it in as a dependency. ## Documentation [Please see the http.rb wiki][documentation] for more detailed documentation and usage notes. The following API documentation is also available: * [YARD API documentation](http://www.rubydoc.info/gems/http/frames) * [Chainable module (all chainable methods)](http://www.rubydoc.info/gems/http/HTTP/Chainable) [documentation]: https://github.com/httprb/http/wiki ### Basic Usage Here's some simple examples to get you started: ```ruby >> HTTP.get("https://github.com").to_s => "\n\n\n\n\n > HTTP.get("https://github.com") => #"GitHub.com", "Date"=>"Tue, 10 May...> ``` We can also obtain an `HTTP::Response::Body` object for this response: ```ruby >> HTTP.get("https://github.com").body => # ``` The response body can be streamed with `HTTP::Response::Body#readpartial`. In practice, you'll want to bind the HTTP::Response::Body to a local variable and call `#readpartial` on it repeatedly until it returns `nil`: ```ruby >> body = HTTP.get("https://github.com").body => # >> body.readpartial => "\n\n\n\n\n > body.readpartial => "\" href=\"/apple-touch-icon-72x72.png\">\n > body.readpartial => nil ``` ## Supported Ruby Versions This library aims to support and is [tested against][travis] the following Ruby versions: * Ruby 2.3.x * Ruby 2.4.x * Ruby 2.5.x * Ruby 2.6.x * JRuby 9.2.x.x If something doesn't work on one of these versions, it's a bug. This library may inadvertently work (or seem to work) on other Ruby versions, however support will only be provided for the versions listed above. If you would like this library to support another Ruby version or implementation, you may volunteer to be a maintainer. Being a maintainer entails making sure all tests run and pass on that implementation. When something breaks on your implementation, you will be responsible for providing patches in a timely fashion. If critical issues for a particular implementation exist at the time of a major release, support for that Ruby version may be dropped. [travis]: http://travis-ci.org/httprb/http ## Contributing to http.rb * Fork http.rb on GitHub * Make your changes * Ensure all tests pass (`bundle exec rake`) * Send a pull request * If we like them we'll merge them * If we've accepted a patch, feel free to ask for commit access! ## Copyright Copyright (c) 2011-2019 Tony Arcieri, Alexey V. Zapparov, Erik Michaels-Ober, Zachary Anker. See LICENSE.txt for further details. http-4.4.1/spec/0000755000004100000410000000000013640204124013455 5ustar www-datawww-datahttp-4.4.1/spec/lib/0000755000004100000410000000000013640204124014223 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/0000755000004100000410000000000013640204124015202 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/options_spec.rb0000644000004100000410000000044313640204124020235 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options do subject { described_class.new(:response => :body) } it "has reader methods for attributes" do expect(subject.response).to eq(:body) end it "coerces to a Hash" do expect(subject.to_hash).to be_a(Hash) end end http-4.4.1/spec/lib/http/headers_spec.rb0000644000004100000410000003166713640204124020171 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Headers do subject(:headers) { described_class.new } it "is Enumerable" do expect(headers).to be_an Enumerable end describe "#set" do it "sets header value" do headers.set "Accept", "application/json" expect(headers["Accept"]).to eq "application/json" end it "normalizes header name" do headers.set :content_type, "application/json" expect(headers["Content-Type"]).to eq "application/json" end it "overwrites previous value" do headers.set :set_cookie, "hoo=ray" headers.set :set_cookie, "woo=hoo" expect(headers["Set-Cookie"]).to eq "woo=hoo" end it "allows set multiple values" do headers.set :set_cookie, "hoo=ray" headers.set :set_cookie, %w[hoo=ray woo=hoo] expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo] end it "fails with empty header name" do expect { headers.set "", "foo bar" }. to raise_error HTTP::HeaderError end it "fails with invalid header name" do expect { headers.set "foo bar", "baz" }. to raise_error HTTP::HeaderError end end describe "#[]=" do it "sets header value" do headers["Accept"] = "application/json" expect(headers["Accept"]).to eq "application/json" end it "normalizes header name" do headers[:content_type] = "application/json" expect(headers["Content-Type"]).to eq "application/json" end it "overwrites previous value" do headers[:set_cookie] = "hoo=ray" headers[:set_cookie] = "woo=hoo" expect(headers["Set-Cookie"]).to eq "woo=hoo" end it "allows set multiple values" do headers[:set_cookie] = "hoo=ray" headers[:set_cookie] = %w[hoo=ray woo=hoo] expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo] end end describe "#delete" do before { headers.set "Content-Type", "application/json" } it "removes given header" do headers.delete "Content-Type" expect(headers["Content-Type"]).to be_nil end it "normalizes header name" do headers.delete :content_type expect(headers["Content-Type"]).to be_nil end it "fails with empty header name" do expect { headers.delete "" }. to raise_error HTTP::HeaderError end it "fails with invalid header name" do expect { headers.delete "foo bar" }. to raise_error HTTP::HeaderError end end describe "#add" do it "sets header value" do headers.add "Accept", "application/json" expect(headers["Accept"]).to eq "application/json" end it "normalizes header name" do headers.add :content_type, "application/json" expect(headers["Content-Type"]).to eq "application/json" end it "appends new value if header exists" do headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo] end it "allows append multiple values" do headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, %w[woo=hoo yup=pie] expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo yup=pie] end it "fails with empty header name" do expect { headers.add("", "foobar") }. to raise_error HTTP::HeaderError end it "fails with invalid header name" do expect { headers.add "foo bar", "baz" }. to raise_error HTTP::HeaderError end end describe "#get" do before { headers.set("Content-Type", "application/json") } it "returns array of associated values" do expect(headers.get("Content-Type")).to eq %w[application/json] end it "normalizes header name" do expect(headers.get(:content_type)).to eq %w[application/json] end context "when header does not exists" do it "returns empty array" do expect(headers.get(:accept)).to eq [] end end it "fails with empty header name" do expect { headers.get("") }. to raise_error HTTP::HeaderError end it "fails with invalid header name" do expect { headers.get("foo bar") }. to raise_error HTTP::HeaderError end end describe "#[]" do context "when header does not exists" do it "returns nil" do expect(headers[:accept]).to be_nil end end context "when header has a single value" do before { headers.set "Content-Type", "application/json" } it "normalizes header name" do expect(headers[:content_type]).to_not be_nil end it "returns it returns a single value" do expect(headers[:content_type]).to eq "application/json" end end context "when header has a multiple values" do before do headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" end it "normalizes header name" do expect(headers[:set_cookie]).to_not be_nil end it "returns array of associated values" do expect(headers[:set_cookie]).to eq %w[hoo=ray woo=hoo] end end end describe "#include?" do before do headers.add :content_type, "application/json" headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" end it "tells whenever given headers is set or not" do expect(headers.include?("Content-Type")).to be true expect(headers.include?("Set-Cookie")).to be true expect(headers.include?("Accept")).to be false end it "normalizes given header name" do expect(headers.include?(:content_type)).to be true expect(headers.include?(:set_cookie)).to be true expect(headers.include?(:accept)).to be false end end describe "#to_h" do before do headers.add :content_type, "application/json" headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" end it "returns a Hash" do expect(headers.to_h).to be_a ::Hash end it "returns Hash with normalized keys" do expect(headers.to_h.keys).to match_array %w[Content-Type Set-Cookie] end context "for a header with single value" do it "provides a value as is" do expect(headers.to_h["Content-Type"]).to eq "application/json" end end context "for a header with multiple values" do it "provides an array of values" do expect(headers.to_h["Set-Cookie"]).to eq %w[hoo=ray woo=hoo] end end end describe "#to_a" do before do headers.add :content_type, "application/json" headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" end it "returns an Array" do expect(headers.to_a).to be_a Array end it "returns Array of key/value pairs with normalized keys" do expect(headers.to_a).to eq [ %w[Content-Type application/json], %w[Set-Cookie hoo=ray], %w[Set-Cookie woo=hoo] ] end end describe "#inspect" do before { headers.set :set_cookie, %w[hoo=ray woo=hoo] } subject { headers.inspect } it { is_expected.to eq '#["hoo=ray", "woo=hoo"]}>' } end describe "#keys" do before do headers.add :content_type, "application/json" headers.add :set_cookie, "hoo=ray" headers.add :set_cookie, "woo=hoo" end it "returns uniq keys only" do expect(headers.keys.size).to eq 2 end it "normalizes keys" do expect(headers.keys).to include("Content-Type", "Set-Cookie") end end describe "#each" do before do headers.add :set_cookie, "hoo=ray" headers.add :content_type, "application/json" headers.add :set_cookie, "woo=hoo" end it "yields each key/value pair separatedly" do expect { |b| headers.each(&b) }.to yield_control.exactly(3).times end it "yields headers in the same order they were added" do expect { |b| headers.each(&b) }.to yield_successive_args( %w[Set-Cookie hoo=ray], %w[Content-Type application/json], %w[Set-Cookie woo=hoo] ) end it "returns self instance if block given" do expect(headers.each { |*| }).to be headers end it "returns Enumerator if no block given" do expect(headers.each).to be_a Enumerator end end describe ".empty?" do subject { headers.empty? } context "initially" do it { is_expected.to be true } end context "when header exists" do before { headers.add :accept, "text/plain" } it { is_expected.to be false } end context "when last header was removed" do before do headers.add :accept, "text/plain" headers.delete :accept end it { is_expected.to be true } end end describe "#hash" do let(:left) { described_class.new } let(:right) { described_class.new } it "equals if two headers equals" do left.add :accept, "text/plain" right.add :accept, "text/plain" expect(left.hash).to eq right.hash end end describe "#==" do let(:left) { described_class.new } let(:right) { described_class.new } it "compares header keys and values" do left.add :accept, "text/plain" right.add :accept, "text/plain" expect(left).to eq right end it "allows comparison with Array of key/value pairs" do left.add :accept, "text/plain" expect(left).to eq [%w[Accept text/plain]] end it "sensitive to headers order" do left.add :accept, "text/plain" left.add :cookie, "woo=hoo" right.add :cookie, "woo=hoo" right.add :accept, "text/plain" expect(left).to_not eq right end it "sensitive to header values order" do left.add :cookie, "hoo=ray" left.add :cookie, "woo=hoo" right.add :cookie, "woo=hoo" right.add :cookie, "hoo=ray" expect(left).to_not eq right end end describe "#dup" do before { headers.set :content_type, "application/json" } subject(:dupped) { headers.dup } it { is_expected.to be_a described_class } it { is_expected.not_to be headers } it "has headers copied" do expect(dupped[:content_type]).to eq "application/json" end context "modifying a copy" do before { dupped.set :content_type, "text/plain" } it "modifies dupped copy" do expect(dupped[:content_type]).to eq "text/plain" end it "does not affects original headers" do expect(headers[:content_type]).to eq "application/json" end end end describe "#merge!" do before do headers.set :host, "example.com" headers.set :accept, "application/json" headers.merge! :accept => "plain/text", :cookie => %w[hoo=ray woo=hoo] end it "leaves headers not presented in other as is" do expect(headers[:host]).to eq "example.com" end it "overwrites existing values" do expect(headers[:accept]).to eq "plain/text" end it "appends other headers, not presented in base" do expect(headers[:cookie]).to eq %w[hoo=ray woo=hoo] end end describe "#merge" do before do headers.set :host, "example.com" headers.set :accept, "application/json" end subject(:merged) do headers.merge :accept => "plain/text", :cookie => %w[hoo=ray woo=hoo] end it { is_expected.to be_a described_class } it { is_expected.not_to be headers } it "does not affects original headers" do expect(merged.to_h).to_not eq headers.to_h end it "leaves headers not presented in other as is" do expect(merged[:host]).to eq "example.com" end it "overwrites existing values" do expect(merged[:accept]).to eq "plain/text" end it "appends other headers, not presented in base" do expect(merged[:cookie]).to eq %w[hoo=ray woo=hoo] end end describe ".coerce" do let(:dummyClass) { Class.new { def respond_to?(*); end } } it "accepts any object that respond to #to_hash" do hashie = double :to_hash => {"accept" => "json"} expect(described_class.coerce(hashie)["accept"]).to eq "json" end it "accepts any object that respond to #to_h" do hashie = double :to_h => {"accept" => "json"} expect(described_class.coerce(hashie)["accept"]).to eq "json" end it "accepts any object that respond to #to_a" do hashie = double :to_a => [%w[accept json]] expect(described_class.coerce(hashie)["accept"]).to eq "json" end it "fails if given object cannot be coerced" do expect { described_class.coerce dummyClass.new }.to raise_error HTTP::Error end context "with duplicate header keys (mixed case)" do let(:headers) { {"Set-Cookie" => "hoo=ray", "set-cookie" => "woo=hoo"} } it "adds all headers" do expect(described_class.coerce(headers).to_a). to match_array( [ %w[Set-Cookie hoo=ray], %w[Set-Cookie woo=hoo] ] ) end end it "is aliased as .[]" do expect(described_class.method(:coerce)).to eq described_class.method(:[]) end end end http-4.4.1/spec/lib/http/redirector_spec.rb0000644000004100000410000003312513640204124020707 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Redirector do def simple_response(status, body = "", headers = {}) HTTP::Response.new( :status => status, :version => "1.1", :headers => headers, :body => body ) end def redirect_response(status, location) simple_response status, "", "Location" => location end describe "#strict" do subject { redirector.strict } context "by default" do let(:redirector) { described_class.new } it { is_expected.to be true } end end describe "#max_hops" do subject { redirector.max_hops } context "by default" do let(:redirector) { described_class.new } it { is_expected.to eq 5 } end end describe "#perform" do let(:options) { {} } let(:redirector) { described_class.new options } it "fails with TooManyRedirectsError if max hops reached" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = proc { |prev_req| redirect_response(301, "#{prev_req.uri}/1") } expect { redirector.perform(req, res.call(req), &res) }. to raise_error HTTP::Redirector::TooManyRedirectsError end it "fails with EndlessRedirectError if endless loop detected" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response(301, req.uri) expect { redirector.perform(req, res) { res } }. to raise_error HTTP::Redirector::EndlessRedirectError end it "fails with StateError if there were no Location header" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = simple_response(301) expect { |b| redirector.perform(req, res, &b) }. to raise_error HTTP::StateError end it "returns first non-redirect response" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" hops = [ redirect_response(301, "http://example.com/1"), redirect_response(301, "http://example.com/2"), redirect_response(301, "http://example.com/3"), simple_response(200, "foo"), redirect_response(301, "http://example.com/4"), simple_response(200, "bar") ] res = redirector.perform(req, hops.shift) { hops.shift } expect(res.to_s).to eq "foo" end it "concatenates multiple Location headers" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" headers = HTTP::Headers.new %w[http://example.com /123].each { |loc| headers.add("Location", loc) } res = redirector.perform(req, simple_response(301, "", headers)) do |redirect| simple_response(200, redirect.uri.to_s) end expect(res.to_s).to eq "http://example.com/123" end context "following 300 redirect" do context "with strict mode" do let(:options) { {:strict => true} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "raises StateError if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end end context "with non-strict mode" do let(:options) { {:strict => false} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "it follows with GET if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 300, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end end end context "following 301 redirect" do context "with strict mode" do let(:options) { {:strict => true} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "raises StateError if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end end context "with non-strict mode" do let(:options) { {:strict => false} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "it follows with GET if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 301, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end end end context "following 302 redirect" do context "with strict mode" do let(:options) { {:strict => true} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "raises StateError if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end it "raises StateError if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" expect { redirector.perform(req, res) { simple_response 200 } }. to raise_error HTTP::StateError end end context "with non-strict mode" do let(:options) { {:strict => false} } it "it follows with original verb if it's safe" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "it follows with GET if original request was PUT" do req = HTTP::Request.new :verb => :put, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was POST" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "it follows with GET if original request was DELETE" do req = HTTP::Request.new :verb => :delete, :uri => "http://example.com" res = redirect_response 302, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end end end context "following 303 redirect" do it "follows with HEAD if original request was HEAD" do req = HTTP::Request.new :verb => :head, :uri => "http://example.com" res = redirect_response 303, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :head simple_response 200 end end it "follows with GET if original request was GET" do req = HTTP::Request.new :verb => :get, :uri => "http://example.com" res = redirect_response 303, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end it "follows with GET if original request was neither GET nor HEAD" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 303, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :get simple_response 200 end end end context "following 307 redirect" do it "follows with original request's verb" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 307, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :post simple_response 200 end end end context "following 308 redirect" do it "follows with original request's verb" do req = HTTP::Request.new :verb => :post, :uri => "http://example.com" res = redirect_response 308, "http://example.com/1" redirector.perform(req, res) do |prev_req, _| expect(prev_req.verb).to be :post simple_response 200 end end end end end http-4.4.1/spec/lib/http/request_spec.rb0000644000004100000410000001532613640204124020240 0ustar www-datawww-data# frozen_string_literal: true # coding: utf-8 RSpec.describe HTTP::Request do let(:proxy) { {} } let(:headers) { {:accept => "text/html"} } let(:request_uri) { "http://example.com/foo?bar=baz" } subject :request do HTTP::Request.new( :verb => :get, :uri => request_uri, :headers => headers, :proxy => proxy ) end it "includes HTTP::Headers::Mixin" do expect(described_class).to include HTTP::Headers::Mixin end it "requires URI to have scheme part" do expect { HTTP::Request.new(:verb => :get, :uri => "example.com/") }.to \ raise_error(HTTP::Request::UnsupportedSchemeError) end it "provides a #scheme accessor" do expect(request.scheme).to eq(:http) end it "provides a #verb accessor" do expect(subject.verb).to eq(:get) end it "sets given headers" do expect(subject["Accept"]).to eq("text/html") end describe "Host header" do subject { request["Host"] } context "was not given" do it { is_expected.to eq "example.com" } context "and request URI has non-standard port" do let(:request_uri) { "http://example.com:3000/" } it { is_expected.to eq "example.com:3000" } end end context "was explicitly given" do before { headers[:host] = "github.com" } it { is_expected.to eq "github.com" } end end describe "User-Agent header" do subject { request["User-Agent"] } context "was not given" do it { is_expected.to eq HTTP::Request::USER_AGENT } end context "was explicitly given" do before { headers[:user_agent] = "MrCrawly/123" } it { is_expected.to eq "MrCrawly/123" } end end describe "#redirect" do let(:headers) { {:accept => "text/html"} } let(:proxy) { {:proxy_username => "douglas", :proxy_password => "adams"} } let(:body) { "The Ultimate Question" } let :request do HTTP::Request.new( :verb => :post, :uri => "http://example.com/", :headers => headers, :proxy => proxy, :body => body ) end subject(:redirected) { request.redirect "http://blog.example.com/" } its(:uri) { is_expected.to eq HTTP::URI.parse "http://blog.example.com/" } its(:verb) { is_expected.to eq request.verb } its(:body) { is_expected.to eq request.body } its(:proxy) { is_expected.to eq request.proxy } it "presets new Host header" do expect(redirected["Host"]).to eq "blog.example.com" end context "with URL with non-standard port given" do subject(:redirected) { request.redirect "http://example.com:8080" } its(:uri) { is_expected.to eq HTTP::URI.parse "http://example.com:8080" } its(:verb) { is_expected.to eq request.verb } its(:body) { is_expected.to eq request.body } its(:proxy) { is_expected.to eq request.proxy } it "presets new Host header" do expect(redirected["Host"]).to eq "example.com:8080" end end context "with schema-less absolute URL given" do subject(:redirected) { request.redirect "//another.example.com/blog" } its(:uri) { is_expected.to eq HTTP::URI.parse "http://another.example.com/blog" } its(:verb) { is_expected.to eq request.verb } its(:body) { is_expected.to eq request.body } its(:proxy) { is_expected.to eq request.proxy } it "presets new Host header" do expect(redirected["Host"]).to eq "another.example.com" end end context "with relative URL given" do subject(:redirected) { request.redirect "/blog" } its(:uri) { is_expected.to eq HTTP::URI.parse "http://example.com/blog" } its(:verb) { is_expected.to eq request.verb } its(:body) { is_expected.to eq request.body } its(:proxy) { is_expected.to eq request.proxy } it "keeps Host header" do expect(redirected["Host"]).to eq "example.com" end context "with original URI having non-standard port" do let :request do HTTP::Request.new( :verb => :post, :uri => "http://example.com:8080/", :headers => headers, :proxy => proxy, :body => body ) end its(:uri) { is_expected.to eq HTTP::URI.parse "http://example.com:8080/blog" } end end context "with relative URL that misses leading slash given" do subject(:redirected) { request.redirect "blog" } its(:uri) { is_expected.to eq HTTP::URI.parse "http://example.com/blog" } its(:verb) { is_expected.to eq request.verb } its(:body) { is_expected.to eq request.body } its(:proxy) { is_expected.to eq request.proxy } it "keeps Host header" do expect(redirected["Host"]).to eq "example.com" end context "with original URI having non-standard port" do let :request do HTTP::Request.new( :verb => :post, :uri => "http://example.com:8080/", :headers => headers, :proxy => proxy, :body => body ) end its(:uri) { is_expected.to eq HTTP::URI.parse "http://example.com:8080/blog" } end end context "with new verb given" do subject { request.redirect "http://blog.example.com/", :get } its(:verb) { is_expected.to be :get } end end describe "#headline" do subject(:headline) { request.headline } it { is_expected.to eq "GET /foo?bar=baz HTTP/1.1" } context "when URI contains encoded query" do let(:encoded_query) { "t=1970-01-01T01%3A00%3A00%2B01%3A00" } let(:request_uri) { "http://example.com/foo/?#{encoded_query}" } it "does not unencodes query part" do expect(headline).to eq "GET /foo/?#{encoded_query} HTTP/1.1" end end context "when URI contains non-ASCII path" do let(:request_uri) { "http://example.com/キョ" } it "encodes non-ASCII path part" do expect(headline).to eq "GET /%E3%82%AD%E3%83%A7 HTTP/1.1" end end context "when URI contains fragment" do let(:request_uri) { "http://example.com/foo#bar" } it "omits fragment part" do expect(headline).to eq "GET /foo HTTP/1.1" end end context "with proxy" do let(:proxy) { {:user => "user", :pass => "pass"} } it { is_expected.to eq "GET http://example.com/foo?bar=baz HTTP/1.1" } context "and HTTPS uri" do let(:request_uri) { "https://example.com/foo?bar=baz" } it { is_expected.to eq "GET /foo?bar=baz HTTP/1.1" } end end end describe "#inspect" do subject { request.inspect } it { is_expected.to eq "#" } end end http-4.4.1/spec/lib/http/response/0000755000004100000410000000000013640204124017040 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/response/body_spec.rb0000644000004100000410000000470313640204124021340 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Response::Body do let(:connection) { double(:sequence_id => 0) } let(:chunks) { [String.new("Hello, "), String.new("World!")] } before do allow(connection).to receive(:readpartial) { chunks.shift } allow(connection).to receive(:body_completed?) { chunks.empty? } end subject(:body) { described_class.new(connection, :encoding => Encoding::UTF_8) } it "streams bodies from responses" do expect(subject.to_s).to eq("Hello, World!") end context "when body empty" do let(:chunks) { [String.new("")] } it "returns responds to empty? with true" do expect(subject).to be_empty end end describe "#readpartial" do context "with size given" do it "passes value to underlying connection" do expect(connection).to receive(:readpartial).with(42) body.readpartial 42 end end context "without size given" do it "does not blows up" do expect { body.readpartial }.to_not raise_error end it "calls underlying connection readpartial without specific size" do expect(connection).to receive(:readpartial).with no_args body.readpartial end end it "returns content in specified encoding" do body = described_class.new(connection) expect(connection).to receive(:readpartial). and_return(String.new("content").force_encoding(Encoding::UTF_8)) expect(body.readpartial.encoding).to eq Encoding::BINARY body = described_class.new(connection, :encoding => Encoding::UTF_8) expect(connection).to receive(:readpartial). and_return(String.new("content").force_encoding(Encoding::BINARY)) expect(body.readpartial.encoding).to eq Encoding::UTF_8 end end context "when body is gzipped" do let(:chunks) do body = Zlib::Deflate.deflate("Hi, HTTP here ☺") len = body.length [String.new(body[0, len / 2]), String.new(body[(len / 2)..-1])] end subject(:body) do inflater = HTTP::Response::Inflater.new(connection) described_class.new(inflater, :encoding => Encoding::UTF_8) end it "decodes body" do expect(subject.to_s).to eq("Hi, HTTP here ☺") end describe "#readpartial" do it "streams decoded body" do [ "Hi, HTTP ", "here ☺", nil ].each do |part| expect(subject.readpartial).to eq(part) end end end end end http-4.4.1/spec/lib/http/response/status_spec.rb0000644000004100000410000001520113640204124021721 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Response::Status do describe ".new" do it "fails if given value does not respond to #to_i" do expect { described_class.new double }.to raise_error TypeError end it "accepts any object that responds to #to_i" do expect { described_class.new double :to_i => 200 }.to_not raise_error end end describe "#code" do subject { described_class.new("200.0").code } it { is_expected.to eq 200 } it { is_expected.to be_a Integer } end describe "#reason" do subject { described_class.new(code).reason } context "with unknown code" do let(:code) { 1024 } it { is_expected.to be_nil } end described_class::REASONS.each do |code, reason| class_eval <<-RUBY context 'with well-known code: #{code}' do let(:code) { #{code} } it { is_expected.to eq #{reason.inspect} } it { is_expected.to be_frozen } end RUBY end end context "with 1xx codes" do subject { (100...200).map { |code| described_class.new code } } it "is #informational?" do expect(subject).to all(satisfy(&:informational?)) end it "is not #success?" do expect(subject).to all(satisfy { |status| !status.success? }) end it "is not #redirect?" do expect(subject).to all(satisfy { |status| !status.redirect? }) end it "is not #client_error?" do expect(subject).to all(satisfy { |status| !status.client_error? }) end it "is not #server_error?" do expect(subject).to all(satisfy { |status| !status.server_error? }) end end context "with 2xx codes" do subject { (200...300).map { |code| described_class.new code } } it "is not #informational?" do expect(subject).to all(satisfy { |status| !status.informational? }) end it "is #success?" do expect(subject).to all(satisfy(&:success?)) end it "is not #redirect?" do expect(subject).to all(satisfy { |status| !status.redirect? }) end it "is not #client_error?" do expect(subject).to all(satisfy { |status| !status.client_error? }) end it "is not #server_error?" do expect(subject).to all(satisfy { |status| !status.server_error? }) end end context "with 3xx codes" do subject { (300...400).map { |code| described_class.new code } } it "is not #informational?" do expect(subject).to all(satisfy { |status| !status.informational? }) end it "is not #success?" do expect(subject).to all(satisfy { |status| !status.success? }) end it "is #redirect?" do expect(subject).to all(satisfy(&:redirect?)) end it "is not #client_error?" do expect(subject).to all(satisfy { |status| !status.client_error? }) end it "is not #server_error?" do expect(subject).to all(satisfy { |status| !status.server_error? }) end end context "with 4xx codes" do subject { (400...500).map { |code| described_class.new code } } it "is not #informational?" do expect(subject).to all(satisfy { |status| !status.informational? }) end it "is not #success?" do expect(subject).to all(satisfy { |status| !status.success? }) end it "is not #redirect?" do expect(subject).to all(satisfy { |status| !status.redirect? }) end it "is #client_error?" do expect(subject).to all(satisfy(&:client_error?)) end it "is not #server_error?" do expect(subject).to all(satisfy { |status| !status.server_error? }) end end context "with 5xx codes" do subject { (500...600).map { |code| described_class.new code } } it "is not #informational?" do expect(subject).to all(satisfy { |status| !status.informational? }) end it "is not #success?" do expect(subject).to all(satisfy { |status| !status.success? }) end it "is not #redirect?" do expect(subject).to all(satisfy { |status| !status.redirect? }) end it "is not #client_error?" do expect(subject).to all(satisfy { |status| !status.client_error? }) end it "is #server_error?" do expect(subject).to all(satisfy(&:server_error?)) end end describe "#to_sym" do subject { described_class.new(code).to_sym } context "with unknown code" do let(:code) { 1024 } it { is_expected.to be_nil } end described_class::SYMBOLS.each do |code, symbol| class_eval <<-RUBY context 'with well-known code: #{code}' do let(:code) { #{code} } it { is_expected.to be #{symbol.inspect} } end RUBY end end describe "#inspect" do it "returns quoted code and reason phrase" do status = described_class.new 200 expect(status.inspect).to eq "#" end end # testing edge cases only describe "::SYMBOLS" do subject { described_class::SYMBOLS } # "OK" its([200]) { is_expected.to be :ok } # "Bad Request" its([400]) { is_expected.to be :bad_request } end described_class::SYMBOLS.each do |code, symbol| class_eval <<-RUBY describe '##{symbol}?' do subject { status.#{symbol}? } context 'when code is #{code}' do let(:status) { described_class.new #{code} } it { is_expected.to be true } end context 'when code is higher than #{code}' do let(:status) { described_class.new #{code + 1} } it { is_expected.to be false } end context 'when code is lower than #{code}' do let(:status) { described_class.new #{code - 1} } it { is_expected.to be false } end end RUBY end describe ".coerce" do context "with String" do it "coerces reasons" do expect(described_class.coerce("Bad request")).to eq described_class.new 400 end it "fails when reason is unknown" do expect { described_class.coerce "foobar" }.to raise_error HTTP::Error end end context "with Symbol" do it "coerces symbolized reasons" do expect(described_class.coerce(:bad_request)).to eq described_class.new 400 end it "fails when symbolized reason is unknown" do expect { described_class.coerce(:foobar) }.to raise_error HTTP::Error end end context "with Numeric" do it "coerces as Fixnum code" do expect(described_class.coerce(200.1)).to eq described_class.new 200 end end it "fails if coercion failed" do expect { described_class.coerce(true) }.to raise_error HTTP::Error end it "is aliased as `.[]`" do expect(described_class.method(:coerce)).to eq described_class.method :[] end end end http-4.4.1/spec/lib/http/response/parser_spec.rb0000644000004100000410000000213313640204124021672 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Response::Parser do subject(:parser) { described_class.new } let(:raw_response) do "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMy-Header: val\r\nEmpty-Header: \r\n\r\n{}" end let(:expected_headers) do { "Content-Length" => "2", "Content-Type" => "application/json", "My-Header" => "val", "Empty-Header" => "" } end let(:expected_body) { "{}" } before do parts.each { |part| subject.add(part) } end context "whole response in one part" do let(:parts) { [raw_response] } it "parses headers" do expect(subject.headers.to_h).to eq(expected_headers) end it "parses body" do expect(subject.read(expected_body.size)).to eq(expected_body) end end context "response in many parts" do let(:parts) { raw_response.split(//) } it "parses headers" do expect(subject.headers.to_h).to eq(expected_headers) end it "parses body" do expect(subject.read(expected_body.size)).to eq(expected_body) end end end http-4.4.1/spec/lib/http/headers/0000755000004100000410000000000013640204124016615 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/headers/mixin_spec.rb0000644000004100000410000000136513640204124021305 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Headers::Mixin do let :dummy_class do Class.new do include HTTP::Headers::Mixin def initialize(headers) @headers = headers end end end let(:headers) { HTTP::Headers.new } let(:dummy) { dummy_class.new headers } describe "#headers" do it "returns @headers instance variable" do expect(dummy.headers).to be headers end end describe "#[]" do it "proxies to headers#[]" do expect(headers).to receive(:[]).with(:accept) dummy[:accept] end end describe "#[]=" do it "proxies to headers#[]" do expect(headers).to receive(:[]=).with(:accept, "text/plain") dummy[:accept] = "text/plain" end end end http-4.4.1/spec/lib/http/options/0000755000004100000410000000000013640204124016675 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/options/headers_spec.rb0000644000004100000410000000105513640204124021650 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "headers" do let(:opts) { HTTP::Options.new } it "defaults to be empty" do expect(opts.headers).to be_empty end it "may be specified with with_headers" do opts2 = opts.with_headers("accept" => "json") expect(opts.headers).to be_empty expect(opts2.headers).to eq([%w[Accept json]]) end it "accepts any object that respond to :to_hash" do x = Struct.new(:to_hash).new("accept" => "json") expect(opts.with_headers(x).headers["accept"]).to eq("json") end end http-4.4.1/spec/lib/http/options/json_spec.rb0000644000004100000410000000052513640204124021207 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "json" do let(:opts) { HTTP::Options.new } it "defaults to nil" do expect(opts.json).to be nil end it "may be specified with with_json data" do opts2 = opts.with_json(:foo => 42) expect(opts.json).to be nil expect(opts2.json).to eq(:foo => 42) end end http-4.4.1/spec/lib/http/options/body_spec.rb0000644000004100000410000000050613640204124021172 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "body" do let(:opts) { HTTP::Options.new } it "defaults to nil" do expect(opts.body).to be nil end it "may be specified with with_body" do opts2 = opts.with_body("foo") expect(opts.body).to be nil expect(opts2.body).to eq("foo") end end http-4.4.1/spec/lib/http/options/features_spec.rb0000644000004100000410000000212113640204124022046 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "features" do let(:opts) { HTTP::Options.new } it "defaults to be empty" do expect(opts.features).to be_empty end it "accepts plain symbols in array" do opts2 = opts.with_features([:auto_inflate]) expect(opts.features).to be_empty expect(opts2.features.keys).to eq([:auto_inflate]) expect(opts2.features[:auto_inflate]). to be_instance_of(HTTP::Features::AutoInflate) end it "accepts feature name with its options in array" do opts2 = opts.with_features([{:auto_deflate => {:method => :deflate}}]) expect(opts.features).to be_empty expect(opts2.features.keys).to eq([:auto_deflate]) expect(opts2.features[:auto_deflate]). to be_instance_of(HTTP::Features::AutoDeflate) expect(opts2.features[:auto_deflate].method).to eq("deflate") end it "raises error for not supported features" do expect { opts.with_features([:wrong_feature]) }. to raise_error(HTTP::Error) { |error| expect(error.message).to eq("Unsupported feature: wrong_feature") } end end http-4.4.1/spec/lib/http/options/form_spec.rb0000644000004100000410000000052513640204124021201 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "form" do let(:opts) { HTTP::Options.new } it "defaults to nil" do expect(opts.form).to be nil end it "may be specified with with_form_data" do opts2 = opts.with_form(:foo => 42) expect(opts.form).to be nil expect(opts2.form).to eq(:foo => 42) end end http-4.4.1/spec/lib/http/options/proxy_spec.rb0000644000004100000410000000141013640204124021411 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "proxy" do let(:opts) { HTTP::Options.new } it "defaults to {}" do expect(opts.proxy).to eq({}) end it "may be specified with with_proxy" do opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080) expect(opts.proxy).to eq({}) expect(opts2.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080) end it "accepts proxy address, port, username, and password" do opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password") expect(opts2.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password") end end http-4.4.1/spec/lib/http/options/merge_spec.rb0000644000004100000410000000447213640204124021342 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "merge" do let(:opts) { HTTP::Options.new } it "supports a Hash" do old_response = opts.response expect(opts.merge(:response => :body).response).to eq(:body) expect(opts.response).to eq(old_response) end it "supports another Options" do merged = opts.merge(HTTP::Options.new(:response => :body)) expect(merged.response).to eq(:body) end it "merges as excepted in complex cases" do # FIXME: yuck :( foo = HTTP::Options.new( :response => :body, :params => {:baz => "bar"}, :form => {:foo => "foo"}, :body => "body-foo", :json => {:foo => "foo"}, :headers => {:accept => "json", :foo => "foo"}, :proxy => {}, :features => {} ) bar = HTTP::Options.new( :response => :parsed_body, :persistent => "https://www.googe.com", :params => {:plop => "plip"}, :form => {:bar => "bar"}, :body => "body-bar", :json => {:bar => "bar"}, :keep_alive_timeout => 10, :headers => {:accept => "xml", :bar => "bar"}, :timeout_options => {:foo => :bar}, :ssl => {:foo => "bar"}, :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080} ) expect(foo.merge(bar).to_hash).to eq( :response => :parsed_body, :timeout_class => described_class.default_timeout_class, :timeout_options => {:foo => :bar}, :params => {:plop => "plip"}, :form => {:bar => "bar"}, :body => "body-bar", :json => {:bar => "bar"}, :persistent => "https://www.googe.com", :keep_alive_timeout => 10, :ssl => {:foo => "bar"}, :headers => {"Foo" => "foo", "Accept" => "xml", "Bar" => "bar"}, :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}, :follow => nil, :socket_class => described_class.default_socket_class, :nodelay => false, :ssl_socket_class => described_class.default_ssl_socket_class, :ssl_context => nil, :cookies => {}, :encoding => nil, :features => {} ) end end http-4.4.1/spec/lib/http/options/new_spec.rb0000644000004100000410000000160613640204124021030 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Options, "new" do it "supports a Options instance" do opts = HTTP::Options.new expect(HTTP::Options.new(opts)).to eq(opts) end context "with a Hash" do it "coerces :response correctly" do opts = HTTP::Options.new(:response => :object) expect(opts.response).to eq(:object) end it "coerces :headers correctly" do opts = HTTP::Options.new(:headers => {:accept => "json"}) expect(opts.headers).to eq([%w[Accept json]]) end it "coerces :proxy correctly" do opts = HTTP::Options.new(:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}) expect(opts.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080) end it "coerces :form correctly" do opts = HTTP::Options.new(:form => {:foo => 42}) expect(opts.form).to eq(:foo => 42) end end end http-4.4.1/spec/lib/http/connection_spec.rb0000644000004100000410000000350113640204124020677 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Connection do let(:req) do HTTP::Request.new( :verb => :get, :uri => "http://example.com/", :headers => {} ) end let(:socket) { double(:connect => nil) } let(:timeout_class) { double(:new => socket) } let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) } let(:connection) { HTTP::Connection.new(req, opts) } describe "#read_headers!" do before do connection.instance_variable_set(:@pending_response, true) expect(socket).to receive(:readpartial) do <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") | HTTP/1.1 200 OK | Content-Type: text | RESPONSE end end it "reads data in parts" do connection.read_headers! expect(connection.headers).to eq("Content-Type" => "text") end end describe "#readpartial" do before do connection.instance_variable_set(:@pending_response, true) expect(socket).to receive(:readpartial) do <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") | HTTP/1.1 200 OK | Content-Type: text | RESPONSE end expect(socket).to receive(:readpartial) { "1" } expect(socket).to receive(:readpartial) { "23" } expect(socket).to receive(:readpartial) { "456" } expect(socket).to receive(:readpartial) { "78" } expect(socket).to receive(:readpartial) { "9" } expect(socket).to receive(:readpartial) { "0" } expect(socket).to receive(:readpartial) { :eof } expect(socket).to receive(:closed?) { true } end it "reads data in parts" do connection.read_headers! buffer = String.new while (s = connection.readpartial(3)) buffer << s end expect(buffer).to eq "1234567890" end end end http-4.4.1/spec/lib/http/request/0000755000004100000410000000000013640204124016672 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/request/body_spec.rb0000644000004100000410000001121013640204124021161 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Request::Body do let(:body) { "" } subject { HTTP::Request::Body.new(body) } describe "#initialize" do context "when body is nil" do let(:body) { nil } it "does not raise an error" do expect { subject }.not_to raise_error end end context "when body is a string" do let(:body) { "string body" } it "does not raise an error" do expect { subject }.not_to raise_error end end context "when body is an IO" do let(:body) { FakeIO.new("IO body") } it "does not raise an error" do expect { subject }.not_to raise_error end end context "when body is an Enumerable" do let(:body) { %w[bees cows] } it "does not raise an error" do expect { subject }.not_to raise_error end end context "when body is of unrecognized type" do let(:body) { 123 } it "raises an error" do expect { subject }.to raise_error(HTTP::RequestError) end end end describe "#source" do it "returns the original object" do expect(subject.source).to eq "" end end describe "#size" do context "when body is nil" do let(:body) { nil } it "returns zero" do expect(subject.size).to eq 0 end end context "when body is a string" do let(:body) { "Привет, мир!" } it "returns string bytesize" do expect(subject.size).to eq 21 end end context "when body is an IO with size" do let(:body) { FakeIO.new("content") } it "returns IO size" do expect(subject.size).to eq 7 end end context "when body is an IO without size" do let(:body) { IO.pipe[0] } it "raises a RequestError" do expect { subject.size }.to raise_error(HTTP::RequestError) end end context "when body is an Enumerable" do let(:body) { %w[bees cows] } it "raises a RequestError" do expect { subject.size }.to raise_error(HTTP::RequestError) end end end describe "#each" do let(:chunks) do chunks = [] subject.each { |chunk| chunks << chunk.dup } chunks end context "when body is nil" do let(:body) { nil } it "yields nothing" do expect(chunks).to eq [] end end context "when body is a string" do let(:body) { "content" } it "yields the string" do expect(chunks).to eq %w[content] end end context "when body is a non-Enumerable IO" do let(:body) { FakeIO.new("a" * 16 * 1024 + "b" * 10 * 1024) } it "yields chunks of content" do expect(chunks.inject("", :+)).to eq "a" * 16 * 1024 + "b" * 10 * 1024 end end context "when body is a pipe" do let(:ios) { IO.pipe } let(:body) { ios[0] } around do |example| writer = Thread.new(ios[1]) do |io| io << "abcdef" io.close end begin example.run ensure writer.join end end it "yields chunks of content" do expect(chunks.inject("", :+)).to eq("abcdef") end end context "when body is an Enumerable IO" do let(:data) { "a" * 16 * 1024 + "b" * 10 * 1024 } let(:body) { StringIO.new data } it "yields chunks of content" do expect(chunks.inject("", :+)).to eq data end it "allows to enumerate multiple times" do results = [] 2.times do result = "" subject.each { |chunk| result += chunk } results << result end aggregate_failures do expect(results.count).to eq 2 expect(results).to all eq data end end end context "when body is an Enumerable" do let(:body) { %w[bees cows] } it "yields elements" do expect(chunks).to eq %w[bees cows] end end end describe "#==" do context "when sources are equivalent" do let(:body1) { HTTP::Request::Body.new("content") } let(:body2) { HTTP::Request::Body.new("content") } it "returns true" do expect(body1).to eq body2 end end context "when sources are not equivalent" do let(:body1) { HTTP::Request::Body.new("content") } let(:body2) { HTTP::Request::Body.new(nil) } it "returns false" do expect(body1).not_to eq body2 end end context "when objects are not of the same class" do let(:body1) { HTTP::Request::Body.new("content") } let(:body2) { "content" } it "returns false" do expect(body1).not_to eq body2 end end end end http-4.4.1/spec/lib/http/request/writer_spec.rb0000644000004100000410000000533113640204124021547 0ustar www-datawww-data# frozen_string_literal: true # coding: utf-8 RSpec.describe HTTP::Request::Writer do let(:io) { StringIO.new } let(:body) { HTTP::Request::Body.new("") } let(:headers) { HTTP::Headers.new } let(:headerstart) { "GET /test HTTP/1.1" } subject(:writer) { described_class.new(io, body, headers, headerstart) } describe "#stream" do context "when multiple headers are set" do let(:headers) { HTTP::Headers.coerce "Host" => "example.org" } it "separates headers with carriage return and line feed" do writer.stream expect(io.string).to eq [ "#{headerstart}\r\n", "Host: example.org\r\nContent-Length: 0\r\n\r\n" ].join end end context "when body is nonempty" do let(:body) { HTTP::Request::Body.new("content") } it "writes it to the socket and sets Content-Length" do writer.stream expect(io.string).to eq [ "#{headerstart}\r\n", "Content-Length: 7\r\n\r\n", "content" ].join end end context "when body is empty" do let(:body) { HTTP::Request::Body.new(nil) } it "doesn't write anything to the socket and sets Content-Length" do writer.stream expect(io.string).to eq [ "#{headerstart}\r\n", "Content-Length: 0\r\n\r\n" ].join end end context "when Content-Length header is set" do let(:headers) { HTTP::Headers.coerce "Content-Length" => "12" } let(:body) { HTTP::Request::Body.new("content") } it "keeps the given value" do writer.stream expect(io.string).to eq [ "#{headerstart}\r\n", "Content-Length: 12\r\n\r\n", "content" ].join end end context "when Transfer-Encoding is chunked" do let(:headers) { HTTP::Headers.coerce "Transfer-Encoding" => "chunked" } let(:body) { HTTP::Request::Body.new(%w[request body]) } it "writes encoded content and omits Content-Length" do writer.stream expect(io.string).to eq [ "#{headerstart}\r\n", "Transfer-Encoding: chunked\r\n\r\n", "7\r\nrequest\r\n4\r\nbody\r\n0\r\n\r\n" ].join end end context "when server won't accept any more data" do before do expect(io).to receive(:write).and_raise(Errno::EPIPE) end it "aborts silently" do writer.stream end end context "when writing to socket raises an exception" do before do expect(io).to receive(:write).and_raise(Errno::ECONNRESET) end it "raises a ConnectionError" do expect { writer.stream }.to raise_error(HTTP::ConnectionError) end end end end http-4.4.1/spec/lib/http/uri_spec.rb0000644000004100000410000000162113640204124017340 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::URI do let(:example_http_uri_string) { "http://example.com" } let(:example_https_uri_string) { "https://example.com" } subject(:http_uri) { described_class.parse(example_http_uri_string) } subject(:https_uri) { described_class.parse(example_https_uri_string) } it "knows URI schemes" do expect(http_uri.scheme).to eq "http" expect(https_uri.scheme).to eq "https" end it "sets default ports for HTTP URIs" do expect(http_uri.port).to eq 80 end it "sets default ports for HTTPS URIs" do expect(https_uri.port).to eq 443 end describe "#dup" do it "doesn't share internal value between duplicates" do duplicated_uri = http_uri.dup duplicated_uri.host = "example.org" expect(duplicated_uri.to_s).to eq("http://example.org") expect(http_uri.to_s).to eq("http://example.com") end end end http-4.4.1/spec/lib/http/client_spec.rb0000644000004100000410000003155113640204124020024 0ustar www-datawww-data# frozen_string_literal: true # coding: utf-8 require "support/http_handling_shared" require "support/dummy_server" require "support/ssl_helper" RSpec.describe HTTP::Client do run_server(:dummy) { DummyServer.new } StubbedClient = Class.new(HTTP::Client) do def perform(request, options) stubs.fetch(request.uri) { super(request, options) } end def stubs @stubs ||= {} end def stub(stubs) @stubs = stubs.each_with_object({}) do |(k, v), o| o[HTTP::URI.parse k] = v end self end end def redirect_response(location, status = 302) HTTP::Response.new( :status => status, :version => "1.1", :headers => {"Location" => location}, :body => "" ) end def simple_response(body, status = 200) HTTP::Response.new( :status => status, :version => "1.1", :body => body ) end describe "following redirects" do it "returns response of new location" do client = StubbedClient.new(:follow => true).stub( "http://example.com/" => redirect_response("http://example.com/blog"), "http://example.com/blog" => simple_response("OK") ) expect(client.get("http://example.com/").to_s).to eq "OK" end it "prepends previous request uri scheme and host if needed" do client = StubbedClient.new(:follow => true).stub( "http://example.com/" => redirect_response("/index"), "http://example.com/index" => redirect_response("/index.html"), "http://example.com/index.html" => simple_response("OK") ) expect(client.get("http://example.com/").to_s).to eq "OK" end it "fails upon endless redirects" do client = StubbedClient.new(:follow => true).stub( "http://example.com/" => redirect_response("/") ) expect { client.get("http://example.com/") }. to raise_error(HTTP::Redirector::EndlessRedirectError) end it "fails if max amount of hops reached" do client = StubbedClient.new(:follow => {:max_hops => 5}).stub( "http://example.com/" => redirect_response("/1"), "http://example.com/1" => redirect_response("/2"), "http://example.com/2" => redirect_response("/3"), "http://example.com/3" => redirect_response("/4"), "http://example.com/4" => redirect_response("/5"), "http://example.com/5" => redirect_response("/6"), "http://example.com/6" => simple_response("OK") ) expect { client.get("http://example.com/") }. to raise_error(HTTP::Redirector::TooManyRedirectsError) end context "with non-ASCII URLs" do it "theoretically works like a charm" do client = StubbedClient.new(:follow => true).stub( "http://example.com/" => redirect_response("/könig"), "http://example.com/könig" => simple_response("OK") ) expect { client.get "http://example.com/könig" }.not_to raise_error end it "works like a charm in real world" do expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)). to include("url" => "https://httpbin.org/anything/könig") end end end describe "parsing params" do let(:client) { HTTP::Client.new } before { allow(client).to receive :perform } it "accepts params within the provided URL" do expect(HTTP::Request).to receive(:new) do |opts| expect(CGI.parse(opts[:uri].query)).to eq("foo" => %w[bar]) end client.get("http://example.com/?foo=bar") end it "combines GET params from the URI with the passed in params" do expect(HTTP::Request).to receive(:new) do |opts| expect(CGI.parse(opts[:uri].query)).to eq("foo" => %w[bar], "baz" => %w[quux]) end client.get("http://example.com/?foo=bar", :params => {:baz => "quux"}) end it "merges duplicate values" do expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:uri].query).to match(/^(a=1&a=2|a=2&a=1)$/) end client.get("http://example.com/?a=1", :params => {:a => 2}) end it "does not modifies query part if no params were given" do expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:uri].query).to eq "deadbeef" end client.get("http://example.com/?deadbeef") end it "does not corrupts index-less arrays" do expect(HTTP::Request).to receive(:new) do |opts| expect(CGI.parse(opts[:uri].query)).to eq "a[]" => %w[b c], "d" => %w[e] end client.get("http://example.com/?a[]=b&a[]=c", :params => {:d => "e"}) end it "properly encodes colons" do expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:uri].query).to eq "t=1970-01-01T00%3A00%3A00Z" end client.get("http://example.com/", :params => {:t => "1970-01-01T00:00:00Z"}) end it 'does not convert newlines into \r\n before encoding string values' do expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:uri].query).to eq "foo=bar%0Abaz" end client.get("http://example.com/", :params => {:foo => "bar\nbaz"}) end end describe "passing multipart form data" do it "creates url encoded form data object" do client = HTTP::Client.new allow(client).to receive(:perform) expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:body]).to be_a(HTTP::FormData::Urlencoded) expect(opts[:body].to_s).to eq "foo=bar" end client.get("http://example.com/", :form => {:foo => "bar"}) end it "creates multipart form data object" do client = HTTP::Client.new allow(client).to receive(:perform) expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:body]).to be_a(HTTP::FormData::Multipart) expect(opts[:body].to_s).to include("content") end client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")}) end context "when passing an HTTP::FormData object directly" do it "creates url encoded form data object" do client = HTTP::Client.new form_data = HTTP::FormData::Multipart.new(:foo => "bar") allow(client).to receive(:perform) expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:body]).to be form_data expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m) end client.get("http://example.com/", :form => form_data) end end end describe "passing json" do it "encodes given object" do client = HTTP::Client.new allow(client).to receive(:perform) expect(HTTP::Request).to receive(:new) do |opts| expect(opts[:body]).to eq '{"foo":"bar"}' end client.get("http://example.com/", :json => {:foo => :bar}) end end describe "#request" do context "with non-ASCII URLs" do it "theoretically works like a charm" do client = described_class.new expect { client.get "#{dummy.endpoint}/könig" }.not_to raise_error end it "works like a charm in real world" do url = "https://httpbin.org/anything/ö無" expect(HTTP.follow.get(url).parse(:json)).to include("url" => url) end end context "with explicitly given `Host` header" do let(:headers) { {"Host" => "another.example.com"} } let(:client) { described_class.new :headers => headers } it "keeps `Host` header as is" do expect(client).to receive(:perform) do |req, _| expect(req["Host"]).to eq "another.example.com" end client.request(:get, "http://example.com/") end end context "when :auto_deflate was specified" do let(:headers) { {"Content-Length" => "12"} } let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}}, :body => "foo" } it "deletes Content-Length header" do expect(client).to receive(:perform) do |req, _| expect(req["Content-Length"]).to eq nil end client.request(:get, "http://example.com/") end it "sets Content-Encoding header" do expect(client).to receive(:perform) do |req, _| expect(req["Content-Encoding"]).to eq "gzip" end client.request(:get, "http://example.com/") end context "and there is no body" do let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} } it "doesn't set Content-Encoding header" do expect(client).to receive(:perform) do |req, _| expect(req.headers).not_to include "Content-Encoding" end client.request(:get, "http://example.com/") end end end end include_context "HTTP handling" do let(:extra_options) { {} } let(:options) { {} } let(:server) { dummy } let(:client) { described_class.new(options.merge(extra_options)) } end describe "working with SSL" do run_server(:dummy_ssl) { DummyServer.new(:ssl => true) } let(:extra_options) { {} } let(:client) do described_class.new options.merge(:ssl_context => SSLHelper.client_context).merge(extra_options) end include_context "HTTP handling" do let(:server) { dummy_ssl } end it "just works" do response = client.get(dummy_ssl.endpoint) expect(response.body.to_s).to eq("") end it "fails with OpenSSL::SSL::SSLError if host mismatch" do expect { client.get(dummy_ssl.endpoint.gsub("127.0.0.1", "localhost")) }. to raise_error(OpenSSL::SSL::SSLError, /does not match/) end context "with SSL options instead of a context" do let(:client) do described_class.new options.merge :ssl => SSLHelper.client_params end it "just works" do response = client.get(dummy_ssl.endpoint) expect(response.body.to_s).to eq("") end end end describe "#perform" do let(:client) { described_class.new } it "calls finish_response once body was fully flushed" do expect_any_instance_of(HTTP::Connection).to receive(:finish_response).and_call_original client.get(dummy.endpoint).to_s end context "with HEAD request" do it "does not iterates through body" do expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial) client.head(dummy.endpoint) end it "finishes response after headers were received" do expect_any_instance_of(HTTP::Connection).to receive(:finish_response).and_call_original client.head(dummy.endpoint) end end context "when server fully flushes response in one chunk" do before do socket_spy = double chunks = [ <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") | HTTP/1.1 200 OK | Content-Type: text/html | Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-11-22) | Date: Mon, 24 Mar 2014 00:32:22 GMT | Content-Length: 15 | Connection: Keep-Alive | | RESPONSE ] allow(socket_spy).to receive(:close) { nil } allow(socket_spy).to receive(:closed?) { true } allow(socket_spy).to receive(:readpartial) { chunks.shift || :eof } allow(socket_spy).to receive(:write) { chunks[0].length } allow(TCPSocket).to receive(:open) { socket_spy } end it "properly reads body" do body = client.get(dummy.endpoint).to_s expect(body).to eq "" end end context "when uses chunked transfer encoding" do let(:chunks) do [ <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") << body | HTTP/1.1 200 OK | Content-Type: application/json | Transfer-Encoding: chunked | Connection: close | RESPONSE ] end let(:body) do <<-BODY.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") | 9 | {"state": | 5 | "ok"} | 0 | BODY end before do socket_spy = double allow(socket_spy).to receive(:close) { nil } allow(socket_spy).to receive(:closed?) { true } allow(socket_spy).to receive(:readpartial) { chunks.shift || :eof } allow(socket_spy).to receive(:write) { chunks[0].length } allow(TCPSocket).to receive(:open) { socket_spy } end it "properly reads body" do body = client.get(dummy.endpoint).to_s expect(body).to eq '{"state":"ok"}' end context "with broken body (too early closed connection)" do let(:body) do <<-BODY.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") | 9 | {"state": BODY end it "raises HTTP::ConnectionError" do expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError) end end end end end http-4.4.1/spec/lib/http/response_spec.rb0000644000004100000410000001135713640204124020406 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Response do let(:body) { "Hello world!" } let(:uri) { "http://example.com/" } let(:headers) { {} } subject(:response) do HTTP::Response.new( :status => 200, :version => "1.1", :headers => headers, :body => body, :uri => uri ) end it "includes HTTP::Headers::Mixin" do expect(described_class).to include HTTP::Headers::Mixin end describe "to_a" do let(:body) { "Hello world" } let(:content_type) { "text/plain" } let(:headers) { {"Content-Type" => content_type} } it "returns a Rack-like array" do expect(subject.to_a).to eq([200, headers, body]) end end describe "#content_length" do subject { response.content_length } context "without Content-Length header" do it { is_expected.to be_nil } end context "with Content-Length: 5" do let(:headers) { {"Content-Length" => "5"} } it { is_expected.to eq 5 } end context "with invalid Content-Length" do let(:headers) { {"Content-Length" => "foo"} } it { is_expected.to be_nil } end end describe "mime_type" do subject { response.mime_type } context "without Content-Type header" do let(:headers) { {} } it { is_expected.to be_nil } end context "with Content-Type: text/html" do let(:headers) { {"Content-Type" => "text/html"} } it { is_expected.to eq "text/html" } end context "with Content-Type: text/html; charset=utf-8" do let(:headers) { {"Content-Type" => "text/html; charset=utf-8"} } it { is_expected.to eq "text/html" } end end describe "charset" do subject { response.charset } context "without Content-Type header" do let(:headers) { {} } it { is_expected.to be_nil } end context "with Content-Type: text/html" do let(:headers) { {"Content-Type" => "text/html"} } it { is_expected.to be_nil } end context "with Content-Type: text/html; charset=utf-8" do let(:headers) { {"Content-Type" => "text/html; charset=utf-8"} } it { is_expected.to eq "utf-8" } end end describe "#parse" do let(:headers) { {"Content-Type" => content_type} } let(:body) { '{"foo":"bar"}' } context "with known content type" do let(:content_type) { "application/json" } it "returns parsed body" do expect(response.parse).to eq "foo" => "bar" end end context "with unknown content type" do let(:content_type) { "application/deadbeef" } it "raises HTTP::Error" do expect { response.parse }.to raise_error HTTP::Error end end context "with explicitly given mime type" do let(:content_type) { "application/deadbeef" } it "ignores mime_type of response" do expect(response.parse("application/json")).to eq "foo" => "bar" end it "supports MIME type aliases" do expect(response.parse(:json)).to eq "foo" => "bar" end end end describe "#flush" do let(:body) { double :to_s => "" } it "returns response self-reference" do expect(response.flush).to be response end it "flushes body" do expect(body).to receive :to_s response.flush end end describe "#inspect" do subject { response.inspect } let(:headers) { {:content_type => "text/plain"} } let(:body) { double :to_s => "foobar" } it { is_expected.to eq '#"text/plain"}>' } end describe "#cookies" do let(:cookies) { ["a=1", "b=2; domain=example.com", "c=3; domain=bad.org"] } let(:headers) { {"Set-Cookie" => cookies} } subject(:jar) { response.cookies } it { is_expected.to be_an HTTP::CookieJar } it "contains cookies without domain restriction" do expect(jar.count { |c| "a" == c.name }).to eq 1 end it "contains cookies limited to domain of request uri" do expect(jar.count { |c| "b" == c.name }).to eq 1 end it "does not contains cookies limited to non-requeted uri" do expect(jar.count { |c| "c" == c.name }).to eq 0 end end describe "#connection" do let(:connection) { double } subject(:response) do HTTP::Response.new( :version => "1.1", :status => 200, :connection => connection ) end it "returns the connection object used to instantiate the response" do expect(response.connection).to eq connection end end describe "#chunked?" do subject { response } context "when encoding is set to chunked" do let(:headers) { {"Transfer-Encoding" => "chunked"} } it { is_expected.to be_chunked } end it { is_expected.not_to be_chunked } end end http-4.4.1/spec/lib/http/content_type_spec.rb0000644000004100000410000000325113640204124021255 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::ContentType do describe ".parse" do context "with text/plain" do subject { described_class.parse "text/plain" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to be_nil } end context "with tEXT/plaIN" do subject { described_class.parse "tEXT/plaIN" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to be_nil } end context "with text/plain; charset=utf-8" do subject { described_class.parse "text/plain; charset=utf-8" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to eq "utf-8" } end context 'with text/plain; charset="utf-8"' do subject { described_class.parse 'text/plain; charset="utf-8"' } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to eq "utf-8" } end context "with text/plain; charSET=utf-8" do subject { described_class.parse "text/plain; charSET=utf-8" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to eq "utf-8" } end context "with text/plain; foo=bar; charset=utf-8" do subject { described_class.parse "text/plain; foo=bar; charset=utf-8" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to eq "utf-8" } end context "with text/plain;charset=utf-8;foo=bar" do subject { described_class.parse "text/plain;charset=utf-8;foo=bar" } its(:mime_type) { is_expected.to eq "text/plain" } its(:charset) { is_expected.to eq "utf-8" } end end end http-4.4.1/spec/lib/http/features/0000755000004100000410000000000013640204124017020 5ustar www-datawww-datahttp-4.4.1/spec/lib/http/features/instrumentation_spec.rb0000644000004100000410000000254313640204124023626 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Features::Instrumentation do subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) } let(:instrumenter) { TestInstrumenter.new } describe "logging the request" do let(:request) do HTTP::Request.new( :verb => :post, :uri => "https://example.com/", :headers => {:accept => "application/json"}, :body => '{"hello": "world!"}' ) end it "should log the request" do feature.wrap_request(request) expect(instrumenter.output[:start]).to eq(:request => request) end end describe "logging the response" do let(:response) do HTTP::Response.new( :version => "1.1", :uri => "https://example.com", :status => 200, :headers => {:content_type => "application/json"}, :body => '{"success": true}' ) end it "should log the response" do feature.wrap_response(response) expect(instrumenter.output[:finish]).to eq(:response => response) end end class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter attr_reader :output def initialize @output = {} end def start(_name, payload) output[:start] = payload end def finish(_name, payload) output[:finish] = payload end end end http-4.4.1/spec/lib/http/features/auto_inflate_spec.rb0000644000004100000410000000461013640204124023032 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Features::AutoInflate do subject(:feature) { HTTP::Features::AutoInflate.new } let(:connection) { double } let(:headers) { {} } let(:response) do HTTP::Response.new( :version => "1.1", :status => 200, :headers => headers, :connection => connection ) end describe "#wrap_response" do subject(:result) { feature.wrap_response(response) } context "when there is no Content-Encoding header" do it "returns original request" do expect(result).to be response end end context "for identity Content-Encoding header" do let(:headers) { {:content_encoding => "identity"} } it "returns original request" do expect(result).to be response end end context "for unknown Content-Encoding header" do let(:headers) { {:content_encoding => "not-supported"} } it "returns original request" do expect(result).to be response end end context "for deflate Content-Encoding header" do let(:headers) { {:content_encoding => "deflate"} } it "returns a HTTP::Response wrapping the inflated response body" do expect(result.body).to be_instance_of HTTP::Response::Body end end context "for gzip Content-Encoding header" do let(:headers) { {:content_encoding => "gzip"} } it "returns a HTTP::Response wrapping the inflated response body" do expect(result.body).to be_instance_of HTTP::Response::Body end end context "for x-gzip Content-Encoding header" do let(:headers) { {:content_encoding => "x-gzip"} } it "returns a HTTP::Response wrapping the inflated response body" do expect(result.body).to be_instance_of HTTP::Response::Body end end # TODO(ixti): We should refactor API to either make uri non-optional, # or add reference to request into response object (better). context "when response has uri" do let(:response) do HTTP::Response.new( :version => "1.1", :status => 200, :headers => {:content_encoding => "gzip"}, :connection => connection, :uri => "https://example.com" ) end it "preserves uri in wrapped response" do expect(result.uri).to eq HTTP::URI.parse("https://example.com") end end end end http-4.4.1/spec/lib/http/features/logging_spec.rb0000644000004100000410000000277313640204124022016 0ustar www-datawww-data# frozen_string_literal: true require "logger" RSpec.describe HTTP::Features::Logging do subject(:feature) do logger = Logger.new(logdev) logger.formatter = ->(severity, _, _, message) do format("** %s **\n%s\n", severity, message) end described_class.new(:logger => logger) end let(:logdev) { StringIO.new } describe "logging the request" do let(:request) do HTTP::Request.new( :verb => :post, :uri => "https://example.com/", :headers => {:accept => "application/json"}, :body => '{"hello": "world!"}' ) end it "should log the request" do feature.wrap_request(request) expect(logdev.string).to eq <<~OUTPUT ** INFO ** > POST https://example.com/ ** DEBUG ** Accept: application/json Host: example.com User-Agent: http.rb/#{HTTP::VERSION} {"hello": "world!"} OUTPUT end end describe "logging the response" do let(:response) do HTTP::Response.new( :version => "1.1", :uri => "https://example.com", :status => 200, :headers => {:content_type => "application/json"}, :body => '{"success": true}' ) end it "should log the response" do feature.wrap_response(response) expect(logdev.string).to eq <<~OUTPUT ** INFO ** < 200 OK ** DEBUG ** Content-Type: application/json {"success": true} OUTPUT end end end http-4.4.1/spec/lib/http/features/auto_deflate_spec.rb0000644000004100000410000000445513640204124023023 0ustar www-datawww-data# frozen_string_literal: true RSpec.describe HTTP::Features::AutoDeflate do subject { HTTP::Features::AutoDeflate.new } it "raises error for wrong type" do expect { HTTP::Features::AutoDeflate.new(:method => :wrong) }. to raise_error(HTTP::Error) { |error| expect(error.message).to eq("Only gzip and deflate methods are supported") } end it "accepts gzip method" do expect(HTTP::Features::AutoDeflate.new(:method => :gzip).method).to eq "gzip" end it "accepts deflate method" do expect(HTTP::Features::AutoDeflate.new(:method => :deflate).method).to eq "deflate" end it "accepts string as method" do expect(HTTP::Features::AutoDeflate.new(:method => "gzip").method).to eq "gzip" end it "uses gzip by default" do expect(subject.method).to eq("gzip") end describe "#deflated_body" do let(:body) { %w[bees cows] } let(:deflated_body) { subject.deflated_body(body) } context "when method is gzip" do subject { HTTP::Features::AutoDeflate.new(:method => :gzip) } it "returns object which yields gzipped content of the given body" do io = StringIO.new io.set_encoding(Encoding::BINARY) gzip = Zlib::GzipWriter.new(io) gzip.write("beescows") gzip.close gzipped = io.string expect(deflated_body.each.to_a.join).to eq gzipped end it "caches compressed content when size is called" do io = StringIO.new io.set_encoding(Encoding::BINARY) gzip = Zlib::GzipWriter.new(io) gzip.write("beescows") gzip.close gzipped = io.string expect(deflated_body.size).to eq gzipped.bytesize expect(deflated_body.each.to_a.join).to eq gzipped end end context "when method is deflate" do subject { HTTP::Features::AutoDeflate.new(:method => :deflate) } it "returns object which yields deflated content of the given body" do deflated = Zlib::Deflate.deflate("beescows") expect(deflated_body.each.to_a.join).to eq deflated end it "caches compressed content when size is called" do deflated = Zlib::Deflate.deflate("beescows") expect(deflated_body.size).to eq deflated.bytesize expect(deflated_body.each.to_a.join).to eq deflated end end end end http-4.4.1/spec/lib/http_spec.rb0000644000004100000410000003416513640204124016552 0ustar www-datawww-data# frozen_string_literal: true # encoding: utf-8 require "json" require "support/dummy_server" require "support/proxy_server" RSpec.describe HTTP do run_server(:dummy) { DummyServer.new } run_server(:dummy_ssl) { DummyServer.new(:ssl => true) } let(:ssl_client) do HTTP::Client.new :ssl_context => SSLHelper.client_context end context "getting resources" do it "is easy" do response = HTTP.get dummy.endpoint expect(response.to_s).to match(//) end context "with URI instance" do it "is easy" do response = HTTP.get HTTP::URI.parse dummy.endpoint expect(response.to_s).to match(//) end end context "with query string parameters" do it "is easy" do response = HTTP.get "#{dummy.endpoint}/params", :params => {:foo => "bar"} expect(response.to_s).to match(/Params!/) end end context "with query string parameters in the URI and opts hash" do it "includes both" do response = HTTP.get "#{dummy.endpoint}/multiple-params?foo=bar", :params => {:baz => "quux"} expect(response.to_s).to match(/More Params!/) end end context "with two leading slashes in path" do it "is allowed" do expect { HTTP.get "#{dummy.endpoint}//" }.not_to raise_error end end context "with headers" do it "is easy" do response = HTTP.accept("application/json").get dummy.endpoint expect(response.to_s.include?("json")).to be true end end context "with a large request body" do let(:request_body) { "“" * 1_000_000 } # use multi-byte character [:null, 6, {:read => 2, :write => 2, :connect => 2}].each do |timeout| context "with `.timeout(#{timeout.inspect})`" do let(:client) { HTTP.timeout(timeout) } it "writes the whole body" do response = client.post "#{dummy.endpoint}/echo-body", :body => request_body expect(response.body.to_s).to eq(request_body.b) expect(response.headers["Content-Length"].to_i).to eq request_body.bytesize end end end end end describe ".via" do context "anonymous proxy" do run_server(:proxy) { ProxyServer.new } it "proxies the request" do response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint expect(response.headers["X-Proxied"]).to eq "true" end it "responds with the endpoint's body" do response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint expect(response.to_s).to match(//) end it "raises an argument error if no port given" do expect { HTTP.via(proxy.addr) }.to raise_error HTTP::RequestError end it "ignores credentials" do response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint expect(response.to_s).to match(//) end context "ssl" do it "responds with the endpoint's body" do response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint expect(response.to_s).to match(//) end it "ignores credentials" do response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint expect(response.to_s).to match(//) end end end context "proxy with authentication" do run_server(:proxy) { AuthProxyServer.new } it "proxies the request" do response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint expect(response.headers["X-Proxied"]).to eq "true" end it "responds with the endpoint's body" do response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint expect(response.to_s).to match(//) end it "responds with 407 when wrong credentials given" do response = HTTP.via(proxy.addr, proxy.port, "user", "pass").get dummy.endpoint expect(response.status).to eq(407) end it "responds with 407 if no credentials given" do response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint expect(response.status).to eq(407) end context "ssl" do it "responds with the endpoint's body" do response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint expect(response.to_s).to match(//) end it "responds with 407 when wrong credentials given" do response = ssl_client.via(proxy.addr, proxy.port, "user", "pass").get dummy_ssl.endpoint expect(response.status).to eq(407) end it "responds with 407 if no credentials given" do response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint expect(response.status).to eq(407) end end end end context "posting forms to resources" do it "is easy" do response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"} expect(response.to_s).to eq("passed :)") end end context "loading binary data" do it "is encoded as bytes" do response = HTTP.get "#{dummy.endpoint}/bytes" expect(response.to_s.encoding).to eq(Encoding::BINARY) end end context "loading endpoint with charset" do it "uses charset from headers" do response = HTTP.get "#{dummy.endpoint}/iso-8859-1" expect(response.to_s.encoding).to eq(Encoding::ISO8859_1) expect(response.to_s.encode(Encoding::UTF_8)).to eq("testæ") end context "with encoding option" do it "respects option" do response = HTTP.get "#{dummy.endpoint}/iso-8859-1", :encoding => Encoding::BINARY expect(response.to_s.encoding).to eq(Encoding::BINARY) end end end context "passing a string encoding type" do it "finds encoding" do response = HTTP.get dummy.endpoint, :encoding => "ascii" expect(response.to_s.encoding).to eq(Encoding::ASCII) end end context "loading text with no charset" do it "is binary encoded" do response = HTTP.get dummy.endpoint expect(response.to_s.encoding).to eq(Encoding::BINARY) end end context "posting with an explicit body" do it "is easy" do response = HTTP.post "#{dummy.endpoint}/body", :body => "testing-body" expect(response.to_s).to eq("passed :)") end end context "with redirects" do it "is easy for 301" do response = HTTP.follow.get("#{dummy.endpoint}/redirect-301") expect(response.to_s).to match(//) end it "is easy for 302" do response = HTTP.follow.get("#{dummy.endpoint}/redirect-302") expect(response.to_s).to match(//) end end context "head requests" do it "is easy" do response = HTTP.head dummy.endpoint expect(response.status).to eq(200) expect(response["content-type"]).to match(/html/) end end describe ".auth" do it "sets Authorization header to the given value" do client = HTTP.auth "abc" expect(client.default_options.headers[:authorization]).to eq "abc" end it "accepts any #to_s object" do client = HTTP.auth double :to_s => "abc" expect(client.default_options.headers[:authorization]).to eq "abc" end end describe ".basic_auth" do it "fails when options is not a Hash" do expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error(NoMethodError) end it "fails when :pass is not given" do expect { HTTP.basic_auth :user => "[USER]" }.to raise_error(KeyError) end it "fails when :user is not given" do expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error(KeyError) end it "sets Authorization header with proper BasicAuth value" do client = HTTP.basic_auth :user => "foo", :pass => "bar" expect(client.default_options.headers[:authorization]). to match(%r{^Basic [A-Za-z0-9+/]+=*$}) end end describe ".persistent" do let(:host) { "https://api.github.com" } context "with host only given" do subject { HTTP.persistent host } it { is_expected.to be_an HTTP::Client } it { is_expected.to be_persistent } end context "with host and block given" do it "returns last evaluation of last expression" do expect(HTTP.persistent(host) { :http }).to be :http end it "auto-closes connection" do HTTP.persistent host do |client| expect(client).to receive(:close).and_call_original client.get("/repos/httprb/http.rb") end end end context "with timeout specified" do subject(:client) { HTTP.persistent host, :timeout => 100 } it "sets keep_alive_timeout" do options = client.default_options expect(options.keep_alive_timeout).to eq(100) end end end describe ".timeout" do context "specifying a null timeout" do subject(:client) { HTTP.timeout :null } it "sets timeout_class to Null" do expect(client.default_options.timeout_class). to be HTTP::Timeout::Null end end context "specifying per operation timeouts" do subject(:client) { HTTP.timeout :read => 123 } it "sets timeout_class to PerOperation" do expect(client.default_options.timeout_class). to be HTTP::Timeout::PerOperation end it "sets given timeout options" do expect(client.default_options.timeout_options). to eq :read_timeout => 123 end end context "specifying a global timeout" do subject(:client) { HTTP.timeout 123 } it "sets timeout_class to Global" do expect(client.default_options.timeout_class). to be HTTP::Timeout::Global end it "sets given timeout option" do expect(client.default_options.timeout_options). to eq :global_timeout => 123 end end end describe ".cookies" do let(:endpoint) { "#{dummy.endpoint}/cookies" } it "passes correct `Cookie` header" do expect(HTTP.cookies(:abc => :def).get(endpoint).to_s). to eq "abc: def" end it "properly works with cookie jars from response" do res = HTTP.get(endpoint).flush expect(HTTP.cookies(res.cookies).get(endpoint).to_s). to eq "foo: bar" end it "properly merges cookies" do res = HTTP.get(endpoint).flush client = HTTP.cookies(:foo => 123, :bar => 321).cookies(res.cookies) expect(client.get(endpoint).to_s).to eq "foo: bar\nbar: 321" end it "properly merges Cookie headers and cookies" do client = HTTP.headers("Cookie" => "foo=bar").cookies(:baz => :moo) expect(client.get(endpoint).to_s).to eq "foo: bar\nbaz: moo" end end describe ".nodelay" do before do HTTP.default_options = {:socket_class => socket_spy_class} end after do HTTP.default_options = {} end let(:socket_spy_class) do Class.new(TCPSocket) do def self.setsockopt_calls @setsockopt_calls ||= [] end def setsockopt(*args) self.class.setsockopt_calls << args super end end end it "sets TCP_NODELAY on the underlying socket" do HTTP.get(dummy.endpoint) expect(socket_spy_class.setsockopt_calls).to eq([]) HTTP.nodelay.get(dummy.endpoint) expect(socket_spy_class.setsockopt_calls).to eq([[Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1]]) end end describe ".use" do it "turns on given feature" do client = HTTP.use :auto_deflate expect(client.default_options.features.keys).to eq [:auto_deflate] end context "with :auto_deflate" do it "sends gzipped body" do client = HTTP.use :auto_deflate body = "Hello!" response = client.post("#{dummy.endpoint}/echo-body", :body => body) encoded = response.to_s expect(Zlib::GzipReader.new(StringIO.new(encoded)).read).to eq body end it "sends deflated body" do client = HTTP.use :auto_deflate => {:method => "deflate"} body = "Hello!" response = client.post("#{dummy.endpoint}/echo-body", :body => body) encoded = response.to_s expect(Zlib::Inflate.inflate(encoded)).to eq body end end context "with :auto_inflate" do it "returns raw body when Content-Encoding type is missing" do client = HTTP.use :auto_inflate body = "Hello!" response = client.post("#{dummy.endpoint}/encoded-body", :body => body) expect(response.to_s).to eq("#{body}-raw") end it "returns decoded body" do client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip") body = "Hello!" response = client.post("#{dummy.endpoint}/encoded-body", :body => body) expect(response.to_s).to eq("#{body}-gzipped") end it "returns deflated body" do client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate") body = "Hello!" response = client.post("#{dummy.endpoint}/encoded-body", :body => body) expect(response.to_s).to eq("#{body}-deflated") end end context "with :normalize_uri" do it "normalizes URI" do response = HTTP.get "#{dummy.endpoint}/hello world" expect(response.to_s).to eq("hello world") end it "uses the custom URI Normalizer method" do client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc}) response = client.get("#{dummy.endpoint}/hello world") expect(response.status).to eq(400) end it "uses the default URI normalizer" do client = HTTP.use :normalize_uri expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original response = client.get("#{dummy.endpoint}/hello world") expect(response.to_s).to eq("hello world") end end end it "unifies socket errors into HTTP::ConnectionError" do expect { HTTP.get "http://thishostshouldnotexists.com" }. to raise_error HTTP::ConnectionError expect { HTTP.get "http://127.0.0.1:111" }. to raise_error HTTP::ConnectionError end end http-4.4.1/spec/spec_helper.rb0000644000004100000410000000642313640204124016300 0ustar www-datawww-data# frozen_string_literal: true require "simplecov" require "coveralls" SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] ) SimpleCov.start do add_filter "/spec/" minimum_coverage 80 end require "http" require "rspec/its" require "support/capture_warning" require "support/fakeio" # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| config.expect_with :rspec do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: # be_bigger_than(2).and_smaller_than(4).description # # => "be bigger than 2 and smaller than 4" # ...rather than: # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to # `true` in RSpec 4. mocks.verify_partial_doubles = true end # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with # `:focus` metadata. When nothing is tagged with `:focus`, all examples # get run. config.filter_run :focus config.filter_run_excluding :flaky if defined?(JRUBY_VERSION) && ENV["CI"] config.run_all_when_everything_filtered = true # Limits the available syntax to the non-monkey patched syntax that is recommended. # For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = 0 == ENV["GUARD_RSPEC"].to_i # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = "doc" end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed end http-4.4.1/spec/regression_specs.rb0000644000004100000410000000130213640204124017353 0ustar www-datawww-data# frozen_string_literal: true require "spec_helper" RSpec.describe "Regression testing" do describe "#248" do it "does not fail with github" do github_uri = "http://github.com/" expect { HTTP.get(github_uri).to_s }.not_to raise_error end it "does not fail with googleapis" do google_uri = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" expect { HTTP.get(google_uri).to_s }.not_to raise_error end end describe "#422" do it "reads body when 200 OK response contains Upgrade header" do res = HTTP.get("https://httpbin.org/response-headers?Upgrade=h2,h2c") expect(res.parse(:json)).to include("Upgrade" => "h2,h2c") end end end http-4.4.1/spec/support/0000755000004100000410000000000013640204124015171 5ustar www-datawww-datahttp-4.4.1/spec/support/proxy_server.rb0000644000004100000410000000152713640204124020272 0ustar www-datawww-data# frozen_string_literal: true require "webrick/httpproxy" require "support/black_hole" require "support/servers/config" require "support/servers/runner" class ProxyServer < WEBrick::HTTPProxyServer include ServerConfig CONFIG = { :BindAddress => "127.0.0.1", :Port => 0, :AccessLog => BlackHole, :Logger => BlackHole, :RequestCallback => proc { |_, res| res["X-PROXIED"] = true } }.freeze def initialize super CONFIG end end class AuthProxyServer < WEBrick::HTTPProxyServer include ServerConfig AUTHENTICATOR = proc do |req, res| WEBrick::HTTPAuth.proxy_basic_auth(req, res, "proxy") do |user, pass| user == "username" && pass == "password" end end CONFIG = ProxyServer::CONFIG.merge :ProxyAuthProc => AUTHENTICATOR def initialize super CONFIG end end http-4.4.1/spec/support/http_handling_shared.rb0000644000004100000410000001247513640204124021700 0ustar www-datawww-data# frozen_string_literal: true RSpec.shared_context "HTTP handling" do context "without timeouts" do let(:options) { {:timeout_class => HTTP::Timeout::Null, :timeout_options => {}} } it "works" do expect(client.get(server.endpoint).body.to_s).to eq("") end end context "with a per operation timeout" do let(:response) { client.get(server.endpoint).body.to_s } let(:options) do { :timeout_class => HTTP::Timeout::PerOperation, :timeout_options => { :connect_timeout => conn_timeout, :read_timeout => read_timeout, :write_timeout => write_timeout } } end let(:conn_timeout) { 1 } let(:read_timeout) { 1 } let(:write_timeout) { 1 } it "works" do expect(response).to eq("") end context "connection" do context "of 1" do let(:conn_timeout) { 1 } it "does not time out" do expect { response }.to_not raise_error end end end context "read" do context "of 0" do let(:read_timeout) { 0 } it "times out", :flaky do expect { response }.to raise_error(HTTP::TimeoutError, /Read/i) end end context "of 2.5" do let(:read_timeout) { 2.5 } it "does not time out", :flaky do expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error end end end end context "with a global timeout" do let(:options) do { :timeout_class => HTTP::Timeout::Global, :timeout_options => { :global_timeout => global_timeout } } end let(:global_timeout) { 1 } let(:response) { client.get(server.endpoint).body.to_s } it "errors if connecting takes too long" do expect(TCPSocket).to receive(:open) do sleep 1.25 end expect { response }.to raise_error(HTTP::TimeoutError, /execution/) end it "errors if reading takes too long" do expect { client.get("#{server.endpoint}/sleep").body.to_s }. to raise_error(HTTP::TimeoutError, /Timed out/) end context "it resets state when reusing connections" do let(:extra_options) { {:persistent => server.endpoint} } let(:global_timeout) { 2.5 } it "does not timeout", :flaky do client.get("#{server.endpoint}/sleep").body.to_s client.get("#{server.endpoint}/sleep").body.to_s end end end describe "connection reuse" do let(:sockets_used) do [ client.get("#{server.endpoint}/socket/1").body.to_s, client.get("#{server.endpoint}/socket/2").body.to_s ] end context "when enabled" do let(:options) { {:persistent => server.endpoint} } context "without a host" do it "infers host from persistent config" do expect(client.get("/").body.to_s).to eq("") end end it "re-uses the socket" do expect(sockets_used).to_not include("") expect(sockets_used.uniq.length).to eq(1) end context "on a mixed state" do it "re-opens the connection", :flaky do first_socket_id = client.get("#{server.endpoint}/socket/1").body.to_s client.instance_variable_set(:@state, :dirty) second_socket_id = client.get("#{server.endpoint}/socket/2").body.to_s expect(first_socket_id).to_not eq(second_socket_id) end end context "when trying to read a stale body" do it "errors" do client.get("#{server.endpoint}/not-found") expect { client.get(server.endpoint) }.to raise_error(HTTP::StateError, /Tried to send a request/) end end context "when reading a cached body" do it "succeeds" do first_res = client.get(server.endpoint) first_res.body.to_s second_res = client.get(server.endpoint) expect(first_res.body.to_s).to eq("") expect(second_res.body.to_s).to eq("") end end context "with a socket issue" do it "transparently reopens", :flaky do first_socket_id = client.get("#{server.endpoint}/socket").body.to_s expect(first_socket_id).to_not eq("") # Kill off the sockets we used # rubocop:disable Style/RescueModifier DummyServer::Servlet.sockets.each do |socket| socket.close rescue nil end DummyServer::Servlet.sockets.clear # rubocop:enable Style/RescueModifier # Should error because we tried to use a bad socket expect { client.get("#{server.endpoint}/socket").body.to_s }.to raise_error HTTP::ConnectionError # Should succeed since we create a new socket second_socket_id = client.get("#{server.endpoint}/socket").body.to_s expect(second_socket_id).to_not eq(first_socket_id) end end context "with a change in host" do it "errors" do expect { client.get("https://invalid.com/socket") }.to raise_error(/Persistence is enabled/i) end end end context "when disabled" do let(:options) { {} } it "opens new sockets", :flaky do expect(sockets_used).to_not include("") expect(sockets_used.uniq.length).to eq(2) end end end end http-4.4.1/spec/support/black_hole.rb0000644000004100000410000000032113640204124017575 0ustar www-datawww-data# frozen_string_literal: true module BlackHole class << self def method_missing(*) # rubocop: disable Style/MethodMissing self end def respond_to_missing?(*) true end end end http-4.4.1/spec/support/ssl_helper.rb0000644000004100000410000000424413640204124017662 0ustar www-datawww-data# frozen_string_literal: true require "pathname" require "certificate_authority" module SSLHelper CERTS_PATH = Pathname.new File.expand_path("../../../tmp/certs", __FILE__) class RootCertificate < ::CertificateAuthority::Certificate EXTENSIONS = {"keyUsage" => {"usage" => %w[critical keyCertSign]}}.freeze def initialize super() subject.common_name = "honestachmed.com" serial_number.number = 1 key_material.generate_key self.signing_entity = true sign!("extensions" => EXTENSIONS) end def file return @file if defined? @file CERTS_PATH.mkpath cert_file = CERTS_PATH.join("ca.crt") cert_file.open("w") { |io| io << to_pem } @file = cert_file.to_s end end class ChildCertificate < ::CertificateAuthority::Certificate def initialize(parent) super() subject.common_name = "127.0.0.1" serial_number.number = 1 key_material.generate_key self.parent = parent sign! end def cert OpenSSL::X509::Certificate.new to_pem end def key OpenSSL::PKey::RSA.new key_material.private_key.to_pem end end class << self def server_context context = OpenSSL::SSL::SSLContext.new context.verify_mode = OpenSSL::SSL::VERIFY_PEER context.key = server_cert.key context.cert = server_cert.cert context.ca_file = ca.file context end def client_context context = OpenSSL::SSL::SSLContext.new context.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] context.verify_mode = OpenSSL::SSL::VERIFY_PEER context.key = client_cert.key context.cert = client_cert.cert context.ca_file = ca.file context end def client_params { :key => client_cert.key, :cert => client_cert.cert, :ca_file => ca.file } end %w[server client].each do |side| class_eval <<-RUBY, __FILE__, __LINE__ def #{side}_cert @#{side}_cert ||= ChildCertificate.new ca end RUBY end def ca @ca ||= RootCertificate.new end end end http-4.4.1/spec/support/capture_warning.rb0000644000004100000410000000023613640204124020707 0ustar www-datawww-data# frozen_string_literal: true def capture_warning old_stderr = $stderr $stderr = StringIO.new yield $stderr.string ensure $stderr = old_stderr end http-4.4.1/spec/support/fakeio.rb0000644000004100000410000000036313640204124016756 0ustar www-datawww-data# frozen_string_literal: true require "stringio" class FakeIO def initialize(content) @io = StringIO.new(content) end def string @io.string end def read(*args) @io.read(*args) end def size @io.size end end http-4.4.1/spec/support/dummy_server.rb0000644000004100000410000000157413640204124020246 0ustar www-datawww-data# frozen_string_literal: true require "webrick" require "webrick/ssl" require "support/black_hole" require "support/dummy_server/servlet" require "support/servers/config" require "support/servers/runner" require "support/ssl_helper" class DummyServer < WEBrick::HTTPServer include ServerConfig CONFIG = { :BindAddress => "127.0.0.1", :Port => 0, :AccessLog => BlackHole, :Logger => BlackHole }.freeze SSL_CONFIG = CONFIG.merge( :SSLEnable => true, :SSLStartImmediately => true ).freeze def initialize(options = {}) # rubocop:disable Style/OptionHash super(options[:ssl] ? SSL_CONFIG : CONFIG) mount("/", Servlet) end def endpoint "#{scheme}://#{addr}:#{port}" end def scheme config[:SSLEnable] ? "https" : "http" end def ssl_context @ssl_context ||= SSLHelper.server_context end end http-4.4.1/spec/support/dummy_server/0000755000004100000410000000000013640204124017712 5ustar www-datawww-datahttp-4.4.1/spec/support/dummy_server/servlet.rb0000644000004100000410000001057113640204124021727 0ustar www-datawww-data# frozen_string_literal: true # encoding: UTF-8 class DummyServer < WEBrick::HTTPServer # rubocop:disable Metrics/ClassLength class Servlet < WEBrick::HTTPServlet::AbstractServlet def self.sockets @sockets ||= [] end def not_found(_req, res) res.status = 404 res.body = "Not Found" end def self.handlers @handlers ||= {} end %w[get post head].each do |method| class_eval <<-RUBY, __FILE__, __LINE__ def self.#{method}(path, &block) handlers["#{method}:\#{path}"] = block end def do_#{method.upcase}(req, res) handler = self.class.handlers["#{method}:\#{req.path}"] return instance_exec(req, res, &handler) if handler not_found end RUBY end get "/" do |req, res| res.status = 200 case req["Accept"] when "application/json" res["Content-Type"] = "application/json" res.body = '{"json": true}' else res["Content-Type"] = "text/html" res.body = "" end end get "/sleep" do |_, res| sleep 2 res.status = 200 res.body = "hello" end post "/sleep" do |_, res| sleep 2 res.status = 200 res.body = "hello" end ["", "/1", "/2"].each do |path| get "/socket#{path}" do |req, res| self.class.sockets << req.instance_variable_get(:@socket) res.status = 200 res.body = req.instance_variable_get(:@socket).object_id.to_s end end get "/params" do |req, res| next not_found unless "foo=bar" == req.query_string res.status = 200 res.body = "Params!" end get "/multiple-params" do |req, res| params = CGI.parse req.query_string next not_found unless {"foo" => ["bar"], "baz" => ["quux"]} == params res.status = 200 res.body = "More Params!" end get "/proxy" do |_req, res| res.status = 200 res.body = "Proxy!" end get "/not-found" do |_req, res| res.status = 404 res.body = "not found" end get "/redirect-301" do |_req, res| res.status = 301 res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/" end get "/redirect-302" do |_req, res| res.status = 302 res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/" end post "/form" do |req, res| if "testing-form" == req.query["example"] res.status = 200 res.body = "passed :)" else res.status = 400 res.body = "invalid! >:E" end end post "/body" do |req, res| if "testing-body" == req.body res.status = 200 res.body = "passed :)" else res.status = 400 res.body = "invalid! >:E" end end head "/" do |_req, res| res.status = 200 res["Content-Type"] = "text/html" end get "/bytes" do |_req, res| bytes = [80, 75, 3, 4, 20, 0, 0, 0, 8, 0, 123, 104, 169, 70, 99, 243, 243] res["Content-Type"] = "application/octet-stream" res.body = bytes.pack("c*") end get "/iso-8859-1" do |_req, res| res["Content-Type"] = "text/plain; charset=ISO-8859-1" res.body = "testæ".encode(Encoding::ISO8859_1) end get "/cookies" do |req, res| res["Set-Cookie"] = "foo=bar" res.body = req.cookies.map { |c| [c.name, c.value].join ": " }.join("\n") end post "/echo-body" do |req, res| res.status = 200 res.body = req.body end get "/hello world" do |_req, res| res.status = 200 res.body = "hello world" end post "/encoded-body" do |req, res| res.status = 200 res.body = case req["Accept-Encoding"] when "gzip" then res["Content-Encoding"] = "gzip" StringIO.open do |out| Zlib::GzipWriter.wrap(out) do |gz| gz.write "#{req.body}-gzipped" gz.finish out.tap(&:rewind).read end end when "deflate" then res["Content-Encoding"] = "deflate" Zlib::Deflate.deflate("#{req.body}-deflated") else "#{req.body}-raw" end end end end http-4.4.1/spec/support/servers/0000755000004100000410000000000013640204124016662 5ustar www-datawww-datahttp-4.4.1/spec/support/servers/runner.rb0000644000004100000410000000042213640204124020516 0ustar www-datawww-data# frozen_string_literal: true module ServerRunner def run_server(name) let! name do server = yield Thread.new { server.start } server end after do send(name).shutdown end end end RSpec.configure { |c| c.extend ServerRunner } http-4.4.1/spec/support/servers/config.rb0000644000004100000410000000020513640204124020451 0ustar www-datawww-data# frozen_string_literal: true module ServerConfig def addr config[:BindAddress] end def port config[:Port] end end http-4.4.1/.rubocop.yml0000644000004100000410000000360513640204124015001 0ustar www-datawww-dataAllCops: TargetRubyVersion: 2.3 DisplayCopNames: true ## Layout ###################################################################### Layout/DotPosition: EnforcedStyle: trailing Layout/SpaceAroundOperators: AllowForAlignment: true Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space ## Metrics ##################################################################### Metrics/AbcSize: Enabled: false Metrics/BlockLength: Exclude: - "spec/**/*" - "**/*.gemspec" Metrics/BlockNesting: Max: 2 Metrics/ClassLength: CountComments: false Max: 125 # TODO: Lower to 6 Metrics/CyclomaticComplexity: Max: 8 Metrics/PerceivedComplexity: Max: 8 # TODO: Lower to 80 Metrics/LineLength: AllowURI: true Max: 143 # TODO: Lower to 15 Metrics/MethodLength: CountComments: false Max: 25 Metrics/ModuleLength: CountComments: false Max: 120 Metrics/ParameterLists: Max: 5 CountKeywordArgs: true ## Performance ################################################################# # XXX: requires ruby 2.4+ Performance/RegexpMatch: Enabled: false ## Style ####################################################################### Style/CollectionMethods: PreferredMethods: collect: 'map' reduce: 'inject' find: 'detect' find_all: 'select' Style/Documentation: Enabled: false Style/DoubleNegation: Enabled: false Style/EachWithObject: Enabled: false Style/Encoding: Enabled: false Style/EmptyCaseCondition: Enabled: false # XXX: Lots of times it suggests making code terrible to read. Style/GuardClause: Enabled: false Style/HashSyntax: EnforcedStyle: hash_rockets Style/Lambda: Enabled: false Style/OptionHash: Enabled: true # XXX: requires ruby 2.3+ Style/SafeNavigation: Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes Style/TrivialAccessors: Enabled: false Style/YodaCondition: Enabled: false http-4.4.1/.gitignore0000644000004100000410000000026413640204124014515 0ustar www-datawww-data*.gem .bundle .config .rvmrc .ruby-version .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc lib/bundler/man measurement pkg rdoc spec/reports test/tmp test/version_tmp tmp http-4.4.1/http.gemspec0000644000004100000410000000314613640204124015053 0ustar www-datawww-data# frozen_string_literal: true lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "http/version" Gem::Specification.new do |gem| gem.authors = ["Tony Arcieri", "Erik Michaels-Ober", "Alexey V. Zapparov", "Zachary Anker"] gem.email = ["bascule@gmail.com"] gem.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ") An easy-to-use client library for making requests from Ruby. It uses a simple method chaining system for building requests, similar to Python's Requests. DESCRIPTION gem.summary = "HTTP should be easy" gem.homepage = "https://github.com/httprb/http" gem.licenses = ["MIT"] gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") gem.name = "http" gem.require_paths = ["lib"] gem.version = HTTP::VERSION gem.required_ruby_version = ">= 2.3" gem.add_runtime_dependency "addressable", "~> 2.3" gem.add_runtime_dependency "http-cookie", "~> 1.0" gem.add_runtime_dependency "http-form_data", "~> 2.2" gem.add_runtime_dependency "http-parser", "~> 1.2.0" gem.add_development_dependency "bundler", "~> 2.0" gem.metadata = { "source_code_uri" => "https://github.com/httprb/http", "wiki_uri" => "https://github.com/httprb/http/wiki", "bug_tracker_uri" => "https://github.com/httprb/http/issues", "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGES.md" } end http-4.4.1/Rakefile0000644000004100000410000000347113640204124014175 0ustar www-datawww-data# frozen_string_literal: true require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new require "rubocop/rake_task" RuboCop::RakeTask.new require "yardstick/rake/measurement" Yardstick::Rake::Measurement.new do |measurement| measurement.output = "measurement/report.txt" end require "yardstick/rake/verify" Yardstick::Rake::Verify.new do |verify| verify.require_exact_threshold = false verify.threshold = 55 end task :generate_status_codes do require "http" require "nokogiri" url = "http://www.iana.org/assignments/http-status-codes/http-status-codes.xml" xml = Nokogiri::XML HTTP.get url arr = xml.xpath("//xmlns:record").reduce([]) do |a, e| code = e.xpath("xmlns:value").text.to_s desc = e.xpath("xmlns:description").text.to_s next a if %w[Unassigned (Unused)].include?(desc) a << "#{code} => #{desc.inspect}" end File.open("./lib/http/response/status/reasons.rb", "w") do |io| io.puts <<-TPL.gsub(/^[ ]{6}/, "") # AUTO-GENERATED FILE, DO NOT CHANGE IT MANUALLY require "delegate" module HTTP class Response class Status < ::Delegator # Code to Reason map # # @example Usage # # REASONS[400] # => "Bad Request" # REASONS[414] # => "Request-URI Too Long" # # @return [Hash String>] REASONS = { #{arr.join ",\n "} }.each { |_, v| v.freeze }.freeze end end end TPL end end if ENV["CI"].nil? task :default => %i[spec rubocop verify_measurements] else case ENV["SUITE"] when "rubocop" then task :default => :rubocop when "yardstick" then task :default => :verify_measurements else task :default => :spec end end http-4.4.1/lib/0000755000004100000410000000000013640204124013271 5ustar www-datawww-datahttp-4.4.1/lib/http.rb0000644000004100000410000000104613640204124014576 0ustar www-datawww-data# frozen_string_literal: true require "http/errors" require "http/timeout/null" require "http/timeout/per_operation" require "http/timeout/global" require "http/chainable" require "http/client" require "http/connection" require "http/options" require "http/feature" require "http/request" require "http/request/writer" require "http/response" require "http/response/body" require "http/response/parser" # HTTP should be easy module HTTP extend Chainable class << self # HTTP[:accept => 'text/html'].get(...) alias [] headers end end http-4.4.1/lib/http/0000755000004100000410000000000013640204124014250 5ustar www-datawww-datahttp-4.4.1/lib/http/timeout/0000755000004100000410000000000013640204124015736 5ustar www-datawww-datahttp-4.4.1/lib/http/timeout/null.rb0000644000004100000410000000363013640204124017237 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "io/wait" module HTTP module Timeout class Null extend Forwardable def_delegators :@socket, :close, :closed? attr_reader :options, :socket def initialize(options = {}) # rubocop:disable Style/OptionHash @options = options end # Connects to a socket def connect(socket_class, host, port, nodelay = false) @socket = socket_class.open(host, port) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay end # Starts a SSL connection on a socket def connect_ssl @socket.connect end # Configures the SSL connection and starts the connection def start_tls(host, ssl_socket_class, ssl_context) @socket = ssl_socket_class.new(socket, ssl_context) @socket.hostname = host if @socket.respond_to? :hostname= @socket.sync_close = true if @socket.respond_to? :sync_close= connect_ssl return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER @socket.post_connection_check(host) end # Read from the socket def readpartial(size, buffer = nil) @socket.readpartial(size, buffer) rescue EOFError :eof end # Write to the socket def write(data) @socket.write(data) end alias << write private # Retry reading def rescue_readable(timeout = read_timeout) yield rescue IO::WaitReadable retry if @socket.to_io.wait_readable(timeout) raise TimeoutError, "Read timed out after #{timeout} seconds" end # Retry writing def rescue_writable(timeout = write_timeout) yield rescue IO::WaitWritable retry if @socket.to_io.wait_writable(timeout) raise TimeoutError, "Write timed out after #{timeout} seconds" end end end end http-4.4.1/lib/http/timeout/per_operation.rb0000644000004100000410000000645013640204124021136 0ustar www-datawww-data# frozen_string_literal: true require "timeout" require "http/timeout/null" module HTTP module Timeout class PerOperation < Null CONNECT_TIMEOUT = 0.25 WRITE_TIMEOUT = 0.25 READ_TIMEOUT = 0.25 def initialize(*args) super @read_timeout = options.fetch(:read_timeout, READ_TIMEOUT) @write_timeout = options.fetch(:write_timeout, WRITE_TIMEOUT) @connect_timeout = options.fetch(:connect_timeout, CONNECT_TIMEOUT) end def connect(socket_class, host, port, nodelay = false) ::Timeout.timeout(@connect_timeout, TimeoutError) do @socket = socket_class.open(host, port) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay end end def connect_ssl rescue_readable(@connect_timeout) do rescue_writable(@connect_timeout) do @socket.connect_nonblock end end end # NIO with exceptions if RUBY_VERSION < "2.1.0" # Read data from the socket def readpartial(size, buffer = nil) rescue_readable do @socket.read_nonblock(size, buffer) end rescue EOFError :eof end # Write data to the socket def write(data) rescue_writable do @socket.write_nonblock(data) end rescue EOFError :eof end # NIO without exceptions else # Read data from the socket def readpartial(size, buffer = nil) timeout = false loop do result = @socket.read_nonblock(size, buffer, :exception => false) return :eof if result.nil? return result if result != :wait_readable raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout # marking the socket for timeout. Why is this not being raised immediately? # it seems there is some race-condition on the network level between calling # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting # for reads, and when waiting for x seconds, it returns nil suddenly without completing # the x seconds. In a normal case this would be a timeout on wait/read, but it can # also mean that the socket has been closed by the server. Therefore we "mark" the # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no # timeout. Else, the first timeout was a proper timeout. # This hack has to be done because io/wait#wait_readable doesn't provide a value for when # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks. timeout = true unless @socket.to_io.wait_readable(@read_timeout) end end # Write data to the socket def write(data) timeout = false loop do result = @socket.write_nonblock(data, :exception => false) return result unless result == :wait_writable raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout timeout = true unless @socket.to_io.wait_writable(@write_timeout) end end end end end end http-4.4.1/lib/http/timeout/global.rb0000644000004100000410000000621613640204124017530 0ustar www-datawww-data# frozen_string_literal: true require "timeout" require "io/wait" require "http/timeout/null" module HTTP module Timeout class Global < Null def initialize(*args) super @timeout = @time_left = options.fetch(:global_timeout) end # To future me: Don't remove this again, past you was smarter. def reset_counter @time_left = @timeout end def connect(socket_class, host, port, nodelay = false) reset_timer ::Timeout.timeout(@time_left, TimeoutError) do @socket = socket_class.open(host, port) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay end log_time end def connect_ssl reset_timer begin @socket.connect_nonblock rescue IO::WaitReadable IO.select([@socket], nil, nil, @time_left) log_time retry rescue IO::WaitWritable IO.select(nil, [@socket], nil, @time_left) log_time retry end end # Read from the socket def readpartial(size, buffer = nil) perform_io { read_nonblock(size, buffer) } end # Write to the socket def write(data) perform_io { write_nonblock(data) } end alias << write private if RUBY_VERSION < "2.1.0" def read_nonblock(size, buffer = nil) @socket.read_nonblock(size, buffer) end def write_nonblock(data) @socket.write_nonblock(data) end else def read_nonblock(size, buffer = nil) @socket.read_nonblock(size, buffer, :exception => false) end def write_nonblock(data) @socket.write_nonblock(data, :exception => false) end end # Perform the given I/O operation with the given argument def perform_io reset_timer loop do begin result = yield case result when :wait_readable then wait_readable_or_timeout when :wait_writable then wait_writable_or_timeout when NilClass then return :eof else return result end rescue IO::WaitReadable wait_readable_or_timeout rescue IO::WaitWritable wait_writable_or_timeout end end rescue EOFError :eof end # Wait for a socket to become readable def wait_readable_or_timeout @socket.to_io.wait_readable(@time_left) log_time end # Wait for a socket to become writable def wait_writable_or_timeout @socket.to_io.wait_writable(@time_left) log_time end # Due to the run/retry nature of nonblocking I/O, it's easier to keep track of time # via method calls instead of a block to monitor. def reset_timer @started = Time.now end def log_time @time_left -= (Time.now - @started) if @time_left <= 0 raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" end reset_timer end end end end http-4.4.1/lib/http/version.rb0000644000004100000410000000010313640204124016254 0ustar www-datawww-data# frozen_string_literal: true module HTTP VERSION = "4.4.1" end http-4.4.1/lib/http/connection.rb0000644000004100000410000001341213640204124016735 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "http/headers" require "http/response/parser" module HTTP # A connection to the HTTP server class Connection extend Forwardable # Allowed values for CONNECTION header KEEP_ALIVE = "Keep-Alive" CLOSE = "close" # Attempt to read this much data BUFFER_SIZE = 16_384 # HTTP/1.0 HTTP_1_0 = "1.0" # HTTP/1.1 HTTP_1_1 = "1.1" # Returned after HTTP CONNECT (via proxy) attr_reader :proxy_response_headers # @param [HTTP::Request] req # @param [HTTP::Options] options # @raise [HTTP::ConnectionError] when failed to connect def initialize(req, options) @persistent = options.persistent? @keep_alive_timeout = options.keep_alive_timeout.to_f @pending_request = false @pending_response = false @failed_proxy_connect = false @buffer = "".b @parser = Response::Parser.new @socket = options.timeout_class.new(options.timeout_options) @socket.connect(options.socket_class, req.socket_host, req.socket_port, options.nodelay) send_proxy_connect_request(req) start_tls(req, options) reset_timer rescue IOError, SocketError, SystemCallError => ex raise ConnectionError, "failed to connect: #{ex}", ex.backtrace end # @see (HTTP::Response::Parser#status_code) def_delegator :@parser, :status_code # @see (HTTP::Response::Parser#http_version) def_delegator :@parser, :http_version # @see (HTTP::Response::Parser#headers) def_delegator :@parser, :headers # @return [Boolean] whenever proxy connect failed def failed_proxy_connect? @failed_proxy_connect end # Send a request to the server # # @param [Request] req Request to send to the server # @return [nil] def send_request(req) raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body." if @pending_response raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body." if @pending_request @pending_request = true req.stream @socket @pending_response = true @pending_request = false end # Read a chunk of the body # # @return [String] data chunk # @return [nil] when no more data left def readpartial(size = BUFFER_SIZE) return unless @pending_response chunk = @parser.read(size) return chunk if chunk finished = (read_more(size) == :eof) || @parser.finished? chunk = @parser.read(size) finish_response if finished chunk || "".b end # Reads data from socket up until headers are loaded # @return [void] def read_headers! until @parser.headers? result = read_more(BUFFER_SIZE) raise ConnectionError, "couldn't read response headers" if result == :eof end set_keep_alive end # Callback for when we've reached the end of a response # @return [void] def finish_response close unless keep_alive? @parser.reset @socket.reset_counter if @socket.respond_to?(:reset_counter) reset_timer @pending_response = false end # Close the connection # @return [void] def close @socket.close unless @socket.closed? @pending_response = false @pending_request = false end # Whether we're keeping the conn alive # @return [Boolean] def keep_alive? !!@keep_alive && !@socket.closed? end # Whether our connection has expired # @return [Boolean] def expired? !@conn_expires_at || @conn_expires_at < Time.now end private # Sets up SSL context and starts TLS if needed. # @param (see #initialize) # @return [void] def start_tls(req, options) return unless req.uri.https? && !failed_proxy_connect? ssl_context = options.ssl_context unless ssl_context ssl_context = OpenSSL::SSL::SSLContext.new ssl_context.set_params(options.ssl || {}) end @socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context) end # Open tunnel through proxy def send_proxy_connect_request(req) return unless req.uri.https? && req.using_proxy? @pending_request = true req.connect_using_proxy @socket @pending_request = false @pending_response = true read_headers! @proxy_response_headers = @parser.headers if @parser.status_code != 200 @failed_proxy_connect = true return end @parser.reset @pending_response = false end # Resets expiration of persistent connection. # @return [void] def reset_timer @conn_expires_at = Time.now + @keep_alive_timeout if @persistent end # Store whether the connection should be kept alive. # Once we reset the parser, we lose all of this state. # @return [void] def set_keep_alive return @keep_alive = false unless @persistent @keep_alive = case @parser.http_version when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive @parser.headers[Headers::CONNECTION] == KEEP_ALIVE when HTTP_1_1 # HTTP/1.1 is opt-out @parser.headers[Headers::CONNECTION] != CLOSE else # Anything else we assume doesn't supportit false end end # Feeds some more data into parser # @return [void] def read_more(size) return if @parser.finished? value = @socket.readpartial(size, @buffer) if value == :eof @parser << "" :eof elsif value @parser << value end rescue IOError, SocketError, SystemCallError => ex raise ConnectionError, "error reading from socket: #{ex}", ex.backtrace end end end http-4.4.1/lib/http/uri.rb0000644000004100000410000001104113640204124015371 0ustar www-datawww-data# frozen_string_literal: true require "addressable/uri" module HTTP class URI extend Forwardable def_delegators :@uri, :scheme, :normalized_scheme, :scheme= def_delegators :@uri, :user, :normalized_user, :user= def_delegators :@uri, :password, :normalized_password, :password= def_delegators :@uri, :host, :normalized_host, :host= def_delegators :@uri, :authority, :normalized_authority, :authority= def_delegators :@uri, :origin, :origin= def_delegators :@uri, :normalized_port, :port= def_delegators :@uri, :path, :normalized_path, :path= def_delegators :@uri, :query, :normalized_query, :query= def_delegators :@uri, :query_values, :query_values= def_delegators :@uri, :request_uri, :request_uri= def_delegators :@uri, :fragment, :normalized_fragment, :fragment= def_delegators :@uri, :omit, :join, :normalize # @private HTTP_SCHEME = "http" # @private HTTPS_SCHEME = "https" # @private NORMALIZER = lambda do |uri| uri = HTTP::URI.parse uri HTTP::URI.new( :scheme => uri.normalized_scheme, :authority => uri.normalized_authority, :path => uri.normalized_path, :query => uri.query, :fragment => uri.normalized_fragment ) end # Parse the given URI string, returning an HTTP::URI object # # @param [HTTP::URI, String, #to_str] uri to parse # # @return [HTTP::URI] new URI instance def self.parse(uri) return uri if uri.is_a?(self) new(Addressable::URI.parse(uri)) end # Encodes key/value pairs as application/x-www-form-urlencoded # # @param [#to_hash, #to_ary] form_values to encode # @param [TrueClass, FalseClass] sort should key/value pairs be sorted first? # # @return [String] encoded value def self.form_encode(form_values, sort = false) Addressable::URI.form_encode(form_values, sort) end # Creates an HTTP::URI instance from the given options # # @param [Hash, Addressable::URI] options_or_uri # # @option options_or_uri [String, #to_str] :scheme URI scheme # @option options_or_uri [String, #to_str] :user for basic authentication # @option options_or_uri [String, #to_str] :password for basic authentication # @option options_or_uri [String, #to_str] :host name component # @option options_or_uri [String, #to_str] :port network port to connect to # @option options_or_uri [String, #to_str] :path component to request # @option options_or_uri [String, #to_str] :query component distinct from path # @option options_or_uri [String, #to_str] :fragment component at the end of the URI # # @return [HTTP::URI] new URI instance def initialize(options_or_uri = {}) case options_or_uri when Hash @uri = Addressable::URI.new(options_or_uri) when Addressable::URI @uri = options_or_uri else raise TypeError, "expected Hash for options, got #{options_or_uri.class}" end end # Are these URI objects equal? Normalizes both URIs prior to comparison # # @param [Object] other URI to compare this one with # # @return [TrueClass, FalseClass] are the URIs equivalent (after normalization)? def ==(other) other.is_a?(URI) && normalize.to_s == other.normalize.to_s end # Are these URI objects equal? Does NOT normalizes both URIs prior to comparison # # @param [Object] other URI to compare this one with # # @return [TrueClass, FalseClass] are the URIs equivalent? def eql?(other) other.is_a?(URI) && to_s == other.to_s end # Hash value based off the normalized form of a URI # # @return [Integer] A hash of the URI def hash @hash ||= to_s.hash * -1 end # Port number, either as specified or the default if unspecified # # @return [Integer] port number def port @uri.port || @uri.default_port end # @return [True] if URI is HTTP # @return [False] otherwise def http? HTTP_SCHEME == scheme end # @return [True] if URI is HTTPS # @return [False] otherwise def https? HTTPS_SCHEME == scheme end # @return [Object] duplicated URI def dup self.class.new @uri.dup end # Convert an HTTP::URI to a String # # @return [String] URI serialized as a String def to_s @uri.to_s end alias to_str to_s # @return [String] human-readable representation of URI def inspect format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s) end end end http-4.4.1/lib/http/feature.rb0000644000004100000410000000070713640204124016234 0ustar www-datawww-data# frozen_string_literal: true module HTTP class Feature def initialize(opts = {}) # rubocop:disable Style/OptionHash @opts = opts end def wrap_request(request) request end def wrap_response(response) response end end end require "http/features/auto_inflate" require "http/features/auto_deflate" require "http/features/logging" require "http/features/instrumentation" require "http/features/normalize_uri" http-4.4.1/lib/http/chainable.rb0000644000004100000410000001647113640204124016514 0ustar www-datawww-data# frozen_string_literal: true require "base64" require "http/headers" module HTTP module Chainable # Request a get sans response body # @param uri # @option options [Hash] def head(uri, options = {}) # rubocop:disable Style/OptionHash request :head, uri, options end # Get a resource # @param uri # @option options [Hash] def get(uri, options = {}) # rubocop:disable Style/OptionHash request :get, uri, options end # Post to a resource # @param uri # @option options [Hash] def post(uri, options = {}) # rubocop:disable Style/OptionHash request :post, uri, options end # Put to a resource # @param uri # @option options [Hash] def put(uri, options = {}) # rubocop:disable Style/OptionHash request :put, uri, options end # Delete a resource # @param uri # @option options [Hash] def delete(uri, options = {}) # rubocop:disable Style/OptionHash request :delete, uri, options end # Echo the request back to the client # @param uri # @option options [Hash] def trace(uri, options = {}) # rubocop:disable Style/OptionHash request :trace, uri, options end # Return the methods supported on the given URI # @param uri # @option options [Hash] def options(uri, options = {}) # rubocop:disable Style/OptionHash request :options, uri, options end # Convert to a transparent TCP/IP tunnel # @param uri # @option options [Hash] def connect(uri, options = {}) # rubocop:disable Style/OptionHash request :connect, uri, options end # Apply partial modifications to a resource # @param uri # @option options [Hash] def patch(uri, options = {}) # rubocop:disable Style/OptionHash request :patch, uri, options end # Make an HTTP request with the given verb # @param (see Client#request) def request(*args) branch(default_options).request(*args) end # Prepare an HTTP request with the given verb # @param (see Client#build_request) def build_request(*args) branch(default_options).build_request(*args) end # @overload timeout(options = {}) # Adds per operation timeouts to the request # @param [Hash] options # @option options [Float] :read Read timeout # @option options [Float] :write Write timeout # @option options [Float] :connect Connect timeout # @overload timeout(global_timeout) # Adds a global timeout to the full request # @param [Numeric] global_timeout def timeout(options) klass, options = case options when Numeric then [HTTP::Timeout::Global, {:global => options}] when Hash then [HTTP::Timeout::PerOperation, options] when :null then [HTTP::Timeout::Null, {}] else raise ArgumentError, "Use `.timeout(global_timeout_in_seconds)` or `.timeout(connect: x, write: y, read: z)`." end %i[global read write connect].each do |k| next unless options.key? k options["#{k}_timeout".to_sym] = options.delete k end branch default_options.merge( :timeout_class => klass, :timeout_options => options ) end # @overload persistent(host, timeout: 5) # Flags as persistent # @param [String] host # @option [Integer] timeout Keep alive timeout # @raise [Request::Error] if Host is invalid # @return [HTTP::Client] Persistent client # @overload persistent(host, timeout: 5, &block) # Executes given block with persistent client and automatically closes # connection at the end of execution. # # @example # # def keys(users) # HTTP.persistent("https://github.com") do |http| # users.map { |u| http.get("/#{u}.keys").to_s } # end # end # # # same as # # def keys(users) # http = HTTP.persistent "https://github.com" # users.map { |u| http.get("/#{u}.keys").to_s } # ensure # http.close if http # end # # # @yieldparam [HTTP::Client] client Persistent client # @return [Object] result of last expression in the block def persistent(host, timeout: 5) options = {:keep_alive_timeout => timeout} p_client = branch default_options.merge(options).with_persistent host return p_client unless block_given? yield p_client ensure p_client.close if p_client end # Make a request through an HTTP proxy # @param [Array] proxy # @raise [Request::Error] if HTTP proxy is invalid def via(*proxy) proxy_hash = {} proxy_hash[:proxy_address] = proxy[0] if proxy[0].is_a?(String) proxy_hash[:proxy_port] = proxy[1] if proxy[1].is_a?(Integer) proxy_hash[:proxy_username] = proxy[2] if proxy[2].is_a?(String) proxy_hash[:proxy_password] = proxy[3] if proxy[3].is_a?(String) proxy_hash[:proxy_headers] = proxy[2] if proxy[2].is_a?(Hash) proxy_hash[:proxy_headers] = proxy[4] if proxy[4].is_a?(Hash) raise(RequestError, "invalid HTTP proxy: #{proxy_hash}") unless (2..5).cover?(proxy_hash.keys.size) branch default_options.with_proxy(proxy_hash) end alias through via # Make client follow redirects. # @param opts # @return [HTTP::Client] # @see Redirector#initialize def follow(options = {}) # rubocop:disable Style/OptionHash branch default_options.with_follow options end # Make a request with the given headers # @param headers def headers(headers) branch default_options.with_headers(headers) end # Make a request with the given cookies def cookies(cookies) branch default_options.with_cookies(cookies) end # Force a specific encoding for response body def encoding(encoding) branch default_options.with_encoding(encoding) end # Accept the given MIME type(s) # @param type def accept(type) headers Headers::ACCEPT => MimeType.normalize(type) end # Make a request with the given Authorization header # @param [#to_s] value Authorization header value def auth(value) headers Headers::AUTHORIZATION => value.to_s end # Make a request with the given Basic authorization header # @see http://tools.ietf.org/html/rfc2617 # @param [#fetch] opts # @option opts [#to_s] :user # @option opts [#to_s] :pass def basic_auth(opts) user = opts.fetch :user pass = opts.fetch :pass auth("Basic " + Base64.strict_encode64("#{user}:#{pass}")) end # Get options for HTTP # @return [HTTP::Options] def default_options @default_options ||= HTTP::Options.new end # Set options for HTTP # @param opts # @return [HTTP::Options] def default_options=(opts) @default_options = HTTP::Options.new(opts) end # Set TCP_NODELAY on the socket def nodelay branch default_options.with_nodelay(true) end # Turn on given features. Available features are: # * auto_inflate # * auto_deflate # @param features def use(*features) branch default_options.with_features(features) end private # :nodoc: def branch(options) HTTP::Client.new(options) end end end http-4.4.1/lib/http/response/0000755000004100000410000000000013640204124016106 5ustar www-datawww-datahttp-4.4.1/lib/http/response/inflater.rb0000644000004100000410000000110013640204124020227 0ustar www-datawww-data# frozen_string_literal: true require "zlib" module HTTP class Response class Inflater attr_reader :connection def initialize(connection) @connection = connection end def readpartial(*args) chunk = @connection.readpartial(*args) if chunk chunk = zstream.inflate(chunk) elsif !zstream.closed? zstream.finish zstream.close end chunk end private def zstream @zstream ||= Zlib::Inflate.new(32 + Zlib::MAX_WBITS) end end end end http-4.4.1/lib/http/response/body.rb0000644000004100000410000000424013640204124017370 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "http/client" module HTTP class Response # A streamable response body, also easily converted into a string class Body extend Forwardable include Enumerable def_delegator :to_s, :empty? # The connection object used to make the corresponding request. # # @return [HTTP::Connection] attr_reader :connection def initialize(stream, encoding: Encoding::BINARY) @stream = stream @connection = stream.is_a?(Inflater) ? stream.connection : stream @streaming = nil @contents = nil @encoding = find_encoding(encoding) end # (see HTTP::Client#readpartial) def readpartial(*args) stream! chunk = @stream.readpartial(*args) chunk.force_encoding(@encoding) if chunk end # Iterate over the body, allowing it to be enumerable def each while (chunk = readpartial) yield chunk end end # @return [String] eagerly consume the entire body as a string def to_s return @contents if @contents raise StateError, "body is being streamed" unless @streaming.nil? begin @streaming = false @contents = String.new("").force_encoding(@encoding) while (chunk = @stream.readpartial) @contents << chunk.force_encoding(@encoding) chunk.clear # deallocate string end rescue @contents = nil raise end @contents end alias to_str to_s # Assert that the body is actively being streamed def stream! raise StateError, "body has already been consumed" if @streaming == false @streaming = true end # Easier to interpret string inspect def inspect "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>" end private # Retrieve encoding by name. If encoding cannot be found, default to binary. def find_encoding(encoding) Encoding.find encoding rescue ArgumentError Encoding::BINARY end end end end http-4.4.1/lib/http/response/status.rb0000644000004100000410000000767313640204124017773 0ustar www-datawww-data# frozen_string_literal: true require "delegate" require "http/response/status/reasons" module HTTP class Response class Status < ::Delegator class << self # Coerces given value to Status. # # @example # # Status.coerce(:bad_request) # => Status.new(400) # Status.coerce("400") # => Status.new(400) # Status.coerce(true) # => raises HTTP::Error # # @raise [Error] if coercion is impossible # @param [Symbol, #to_i] object # @return [Status] def coerce(object) code = case when object.is_a?(String) then SYMBOL_CODES[symbolize object] when object.is_a?(Symbol) then SYMBOL_CODES[object] when object.is_a?(Numeric) then object.to_i end return new code if code raise Error, "Can't coerce #{object.class}(#{object}) to #{self}" end alias [] coerce private # Symbolizes given string # # @example # # symbolize "Bad Request" # => :bad_request # symbolize "Request-URI Too Long" # => :request_uri_too_long # symbolize "I'm a Teapot" # => :im_a_teapot # # @param [#to_s] str # @return [Symbol] def symbolize(str) str.to_s.downcase.tr("-", " ").gsub(/[^a-z ]/, "").gsub(/\s+/, "_").to_sym end end # Code to Symbol map # # @example Usage # # SYMBOLS[400] # => :bad_request # SYMBOLS[414] # => :request_uri_too_long # SYMBOLS[418] # => :im_a_teapot # # @return [Hash Symbol>] SYMBOLS = Hash[REASONS.map { |k, v| [k, symbolize(v)] }].freeze # Reversed {SYMBOLS} map. # # @example Usage # # SYMBOL_CODES[:bad_request] # => 400 # SYMBOL_CODES[:request_uri_too_long] # => 414 # SYMBOL_CODES[:im_a_teapot] # => 418 # # @return [Hash Fixnum>] SYMBOL_CODES = Hash[SYMBOLS.map { |k, v| [v, k] }].freeze # @return [Fixnum] status code attr_reader :code # @see REASONS # @return [String, nil] status message def reason REASONS[code] end # @return [String] string representation of HTTP status def to_s "#{code} #{reason}".strip end # Check if status code is informational (1XX) # @return [Boolean] def informational? 100 <= code && code < 200 end # Check if status code is successful (2XX) # @return [Boolean] def success? 200 <= code && code < 300 end # Check if status code is redirection (3XX) # @return [Boolean] def redirect? 300 <= code && code < 400 end # Check if status code is client error (4XX) # @return [Boolean] def client_error? 400 <= code && code < 500 end # Check if status code is server error (5XX) # @return [Boolean] def server_error? 500 <= code && code < 600 end # Symbolized {#reason} # # @return [nil] unless code is well-known (see REASONS) # @return [Symbol] def to_sym SYMBOLS[code] end # Printable version of HTTP Status, surrounded by quote marks, # with special characters escaped. # # (see String#inspect) def inspect "#<#{self.class} #{self}>" end SYMBOLS.each do |code, symbol| class_eval <<-RUBY, __FILE__, __LINE__ def #{symbol}? # def bad_request? #{code} == code # 400 == code end # end RUBY end def __setobj__(obj) raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i @code = obj.to_i end def __getobj__ @code end end end end http-4.4.1/lib/http/response/status/0000755000004100000410000000000013640204124017431 5ustar www-datawww-datahttp-4.4.1/lib/http/response/status/reasons.rb0000644000004100000410000000465413640204124021441 0ustar www-datawww-data# frozen_string_literal: true # AUTO-GENERATED FILE, DO NOT CHANGE IT MANUALLY require "delegate" module HTTP class Response class Status < ::Delegator # Code to Reason map # # @example Usage # # REASONS[400] # => "Bad Request" # REASONS[414] # => "Request-URI Too Long" # # @return [Hash String>] REASONS = { 100 => "Continue", 101 => "Switching Protocols", 102 => "Processing", 200 => "OK", 201 => "Created", 202 => "Accepted", 203 => "Non-Authoritative Information", 204 => "No Content", 205 => "Reset Content", 206 => "Partial Content", 207 => "Multi-Status", 208 => "Already Reported", 226 => "IM Used", 300 => "Multiple Choices", 301 => "Moved Permanently", 302 => "Found", 303 => "See Other", 304 => "Not Modified", 305 => "Use Proxy", 307 => "Temporary Redirect", 308 => "Permanent Redirect", 400 => "Bad Request", 401 => "Unauthorized", 402 => "Payment Required", 403 => "Forbidden", 404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable", 407 => "Proxy Authentication Required", 408 => "Request Timeout", 409 => "Conflict", 410 => "Gone", 411 => "Length Required", 412 => "Precondition Failed", 413 => "Payload Too Large", 414 => "URI Too Long", 415 => "Unsupported Media Type", 416 => "Range Not Satisfiable", 417 => "Expectation Failed", 421 => "Misdirected Request", 422 => "Unprocessable Entity", 423 => "Locked", 424 => "Failed Dependency", 426 => "Upgrade Required", 428 => "Precondition Required", 429 => "Too Many Requests", 431 => "Request Header Fields Too Large", 451 => "Unavailable For Legal Reasons", 500 => "Internal Server Error", 501 => "Not Implemented", 502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout", 505 => "HTTP Version Not Supported", 506 => "Variant Also Negotiates", 507 => "Insufficient Storage", 508 => "Loop Detected", 510 => "Not Extended", 511 => "Network Authentication Required" }.each { |_, v| v.freeze }.freeze end end end http-4.4.1/lib/http/response/parser.rb0000644000004100000410000000523113640204124017730 0ustar www-datawww-data# frozen_string_literal: true require "http-parser" module HTTP class Response # @api private # # NOTE(ixti): This class is a subject of future refactoring, thus don't # expect this class API to be stable until this message disappears and # class is not marked as private anymore. class Parser attr_reader :headers def initialize @state = HttpParser::Parser.new_instance { |i| i.type = :response } @parser = HttpParser::Parser.new(self) reset end # @return [self] def add(data) # XXX(ixti): API doc of HttpParser::Parser is misleading, it says that # it returns boolean true if data was parsed successfully, but instead # it's response tells if there was an error; So when it's `true` that # means parse failed, and `false` means parse was successful. # case of success. return self unless @parser.parse(@state, data) raise IOError, "Could not parse data" end alias << add def headers? @finished[:headers] end def http_version @state.http_version end def status_code @state.http_status end # # HTTP::Parser callbacks # def on_header_field(_response, field) append_header if @reading_header_value @field << field end def on_header_value(_response, value) @reading_header_value = true @field_value << value end def on_headers_complete(_reposse) append_header if @reading_header_value @finished[:headers] = true end def on_body(_response, chunk) if @chunk @chunk << chunk else @chunk = chunk end end def read(size) return if @chunk.nil? if @chunk.bytesize <= size chunk = @chunk @chunk = nil else chunk = @chunk.byteslice(0, size) @chunk[0, size] = "" end chunk end def on_message_complete(_response) @finished[:message] = true end def reset @state.reset! @finished = Hash.new(false) @headers = HTTP::Headers.new @reading_header_value = false @field = +"" @field_value = +"" @chunk = nil end def finished? @finished[:message] end private def append_header @headers.add(@field, @field_value) @reading_header_value = false @field_value = +"" @field = +"" end end end end http-4.4.1/lib/http/headers/0000755000004100000410000000000013640204124015663 5ustar www-datawww-datahttp-4.4.1/lib/http/headers/known.rb0000644000004100000410000000553113640204124017350 0ustar www-datawww-data# frozen_string_literal: true module HTTP class Headers # Content-Types that are acceptable for the response. ACCEPT = "Accept" # Content-codings that are acceptable in the response. ACCEPT_ENCODING = "Accept-Encoding" # The age the object has been in a proxy cache in seconds. AGE = "Age" # Authentication credentials for HTTP authentication. AUTHORIZATION = "Authorization" # Used to specify directives that must be obeyed by all caching mechanisms # along the request-response chain. CACHE_CONTROL = "Cache-Control" # An HTTP cookie previously sent by the server with Set-Cookie. COOKIE = "Cookie" # Control options for the current connection and list # of hop-by-hop request fields. CONNECTION = "Connection" # The length of the request body in octets (8-bit bytes). CONTENT_LENGTH = "Content-Length" # The MIME type of the body of the request # (used with POST and PUT requests). CONTENT_TYPE = "Content-Type" # The date and time that the message was sent (in "HTTP-date" format as # defined by RFC 7231 Date/Time Formats). DATE = "Date" # An identifier for a specific version of a resource, # often a message digest. ETAG = "ETag" # Gives the date/time after which the response is considered stale (in # "HTTP-date" format as defined by RFC 7231). EXPIRES = "Expires" # The domain name of the server (for virtual hosting), and the TCP port # number on which the server is listening. The port number may be omitted # if the port is the standard port for the service requested. HOST = "Host" # Allows a 304 Not Modified to be returned if content is unchanged. IF_MODIFIED_SINCE = "If-Modified-Since" # Allows a 304 Not Modified to be returned if content is unchanged. IF_NONE_MATCH = "If-None-Match" # The last modified date for the requested object (in "HTTP-date" format as # defined by RFC 7231). LAST_MODIFIED = "Last-Modified" # Used in redirection, or when a new resource has been created. LOCATION = "Location" # Authorization credentials for connecting to a proxy. PROXY_AUTHORIZATION = "Proxy-Authorization" # An HTTP cookie. SET_COOKIE = "Set-Cookie" # The form of encoding used to safely transfer the entity to the user. # Currently defined methods are: chunked, compress, deflate, gzip, identity. TRANSFER_ENCODING = "Transfer-Encoding" # Indicates what additional content codings have been applied to the # entity-body. CONTENT_ENCODING = "Content-Encoding" # The user agent string of the user agent. USER_AGENT = "User-Agent" # Tells downstream proxies how to match future request headers to decide # whether the cached response can be used rather than requesting a fresh # one from the origin server. VARY = "Vary" end end http-4.4.1/lib/http/headers/mixin.rb0000644000004100000410000000131313640204124017332 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" module HTTP class Headers # Provides shared behavior for {HTTP::Request} and {HTTP::Response}. # Expects `@headers` to be an instance of {HTTP::Headers}. # # @example Usage # # class MyHttpRequest # include HTTP::Headers::Mixin # # def initialize # @headers = HTTP::Headers.new # end # end module Mixin extend Forwardable # @return [HTTP::Headers] attr_reader :headers # @!method [] # (see HTTP::Headers#[]) def_delegator :headers, :[] # @!method []= # (see HTTP::Headers#[]=) def_delegator :headers, :[]= end end end http-4.4.1/lib/http/mime_type/0000755000004100000410000000000013640204124016240 5ustar www-datawww-datahttp-4.4.1/lib/http/mime_type/adapter.rb0000644000004100000410000000104413640204124020204 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "singleton" module HTTP module MimeType # Base encode/decode MIME type adapter class Adapter include Singleton class << self extend Forwardable def_delegators :instance, :encode, :decode end %w[encode decode].each do |operation| class_eval <<-RUBY, __FILE__, __LINE__ def #{operation}(*) fail Error, "\#{self.class} does not supports ##{operation}" end RUBY end end end end http-4.4.1/lib/http/mime_type/json.rb0000644000004100000410000000100013640204124017525 0ustar www-datawww-data# frozen_string_literal: true require "json" require "http/mime_type/adapter" module HTTP module MimeType # JSON encode/decode MIME type adapter class JSON < Adapter # Encodes object to JSON def encode(obj) return obj.to_json if obj.respond_to?(:to_json) ::JSON.dump obj end # Decodes JSON def decode(str) ::JSON.parse str end end register_adapter "application/json", JSON register_alias "application/json", :json end end http-4.4.1/lib/http/errors.rb0000644000004100000410000000107013640204124016107 0ustar www-datawww-data# frozen_string_literal: true module HTTP # Generic error class Error < StandardError; end # Generic Connection error class ConnectionError < Error; end # Generic Request error class RequestError < Error; end # Generic Response error class ResponseError < Error; end # Requested to do something when we're in the wrong state class StateError < ResponseError; end # Generic Timeout error class TimeoutError < Error; end # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError) class HeaderError < Error; end end http-4.4.1/lib/http/client.rb0000644000004100000410000001250413640204124016055 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "http/form_data" require "http/options" require "http/feature" require "http/headers" require "http/connection" require "http/redirector" require "http/uri" module HTTP # Clients make requests and receive responses class Client extend Forwardable include Chainable HTTP_OR_HTTPS_RE = %r{^https?://}i def initialize(default_options = {}) @default_options = HTTP::Options.new(default_options) @connection = nil @state = :clean end # Make an HTTP request def request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash opts = @default_options.merge(opts) req = build_request(verb, uri, opts) res = perform(req, opts) return res unless opts.follow Redirector.new(opts.follow).perform(req, res) do |request| perform(request, opts) end end # Prepare an HTTP request def build_request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash opts = @default_options.merge(opts) uri = make_request_uri(uri, opts) headers = make_request_headers(opts) body = make_request_body(opts, headers) req = HTTP::Request.new( :verb => verb, :uri => uri, :uri_normalizer => opts.feature(:normalize_uri)&.normalizer, :proxy => opts.proxy, :headers => headers, :body => body ) opts.features.inject(req) do |request, (_name, feature)| feature.wrap_request(request) end end # @!method persistent? # @see Options#persistent? # @return [Boolean] whenever client is persistent def_delegator :default_options, :persistent? # Perform a single (no follow) HTTP request def perform(req, options) verify_connection!(req.uri) @state = :dirty @connection ||= HTTP::Connection.new(req, options) unless @connection.failed_proxy_connect? @connection.send_request(req) @connection.read_headers! end res = Response.new( :status => @connection.status_code, :version => @connection.http_version, :headers => @connection.headers, :proxy_headers => @connection.proxy_response_headers, :connection => @connection, :encoding => options.encoding, :uri => req.uri ) res = options.features.inject(res) do |response, (_name, feature)| feature.wrap_response(response) end @connection.finish_response if req.verb == :head @state = :clean res rescue close raise end def close @connection.close if @connection @connection = nil @state = :clean end private # Verify our request isn't going to be made against another URI def verify_connection!(uri) if default_options.persistent? && uri.origin != default_options.persistent raise StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{uri.origin}" # We re-create the connection object because we want to let prior requests # lazily load the body as long as possible, and this mimics prior functionality. elsif @connection && (!@connection.keep_alive? || @connection.expired?) close # If we get into a bad state (eg, Timeout.timeout ensure being killed) # close the connection to prevent potential for mixed responses. elsif @state == :dirty close end end # Merges query params if needed # # @param [#to_s] uri # @return [URI] def make_request_uri(uri, opts) uri = uri.to_s if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE uri = "#{default_options.persistent}#{uri}" end uri = HTTP::URI.parse uri if opts.params && !opts.params.empty? uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a) end # Some proxies (seen on WEBRick) fail if URL has # empty path (e.g. `http://example.com`) while it's RFC-complaint: # http://tools.ietf.org/html/rfc1738#section-3.1 uri.path = "/" if uri.path.empty? uri end # Creates request headers with cookies (if any) merged in def make_request_headers(opts) headers = opts.headers # Tell the server to keep the conn open headers[Headers::CONNECTION] = default_options.persistent? ? Connection::KEEP_ALIVE : Connection::CLOSE cookies = opts.cookies.values unless cookies.empty? cookies = opts.headers.get(Headers::COOKIE).concat(cookies).join("; ") headers[Headers::COOKIE] = cookies end headers end # Create the request body object to send def make_request_body(opts, headers) case when opts.body opts.body when opts.form form = make_form_data(opts.form) headers[Headers::CONTENT_TYPE] ||= form.content_type form when opts.json body = MimeType[:json].encode opts.json headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}" body end end def make_form_data(form) return form if form.is_a? HTTP::FormData::Multipart return form if form.is_a? HTTP::FormData::Urlencoded HTTP::FormData.create(form) end end end http-4.4.1/lib/http/request/0000755000004100000410000000000013640204124015740 5ustar www-datawww-datahttp-4.4.1/lib/http/request/writer.rb0000644000004100000410000000626413640204124017611 0ustar www-datawww-data# frozen_string_literal: true require "http/headers" module HTTP class Request class Writer # CRLF is the universal HTTP delimiter CRLF = "\r\n" # Chunked data termintaor. ZERO = "0" # Chunked transfer encoding CHUNKED = "chunked" # End of a chunked transfer CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}" def initialize(socket, body, headers, headline) @body = body @socket = socket @headers = headers @request_header = [headline] end # Adds headers to the request header from the headers array def add_headers @headers.each do |field, value| @request_header << "#{field}: #{value}" end end # Stream the request to a socket def stream add_headers add_body_type_headers send_request end # Send headers needed to connect through proxy def connect_through_proxy add_headers write(join_headers) end # Adds the headers to the header array for the given request body we are working # with def add_body_type_headers return if @headers[Headers::CONTENT_LENGTH] || chunked? @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}" end # Joins the headers specified in the request into a correctly formatted # http request header string def join_headers # join the headers array with crlfs, stick two on the end because # that ends the request header @request_header.join(CRLF) + CRLF * 2 end # Writes HTTP request data into the socket. def send_request each_chunk { |chunk| write chunk } rescue Errno::EPIPE # server doesn't need any more data nil end # Yields chunks of request data that should be sent to the socket. # # It's important to send the request in a single write call when possible # in order to play nicely with Nagle's algorithm. Making two writes in a # row triggers a pathological case where Nagle is expecting a third write # that never happens. def each_chunk data = join_headers @body.each do |chunk| data << encode_chunk(chunk) yield data data.clear end yield data unless data.empty? yield CHUNKED_END if chunked? end # Returns the chunk encoded for to the specified "Transfer-Encoding" header. def encode_chunk(chunk) if chunked? chunk.bytesize.to_s(16) << CRLF << chunk << CRLF else chunk end end # Returns true if the request should be sent in chunked encoding. def chunked? @headers[Headers::TRANSFER_ENCODING] == CHUNKED end private def write(data) until data.empty? length = @socket.write(data) break unless data.bytesize > length data = data.byteslice(length..-1) end rescue Errno::EPIPE raise rescue IOError, SocketError, SystemCallError => ex raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace end end end end http-4.4.1/lib/http/request/body.rb0000644000004100000410000000554013640204124017226 0ustar www-datawww-data# frozen_string_literal: true module HTTP class Request class Body attr_reader :source def initialize(source) @source = source validate_source_type! end # Returns size which should be used for the "Content-Length" header. # # @return [Integer] def size if @source.is_a?(String) @source.bytesize elsif @source.respond_to?(:read) raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size) @source.size elsif @source.nil? 0 else raise RequestError, "cannot determine size of body: #{@source.inspect}" end end # Yields chunks of content to be streamed to the request body. # # @yieldparam [String] def each(&block) if @source.is_a?(String) yield @source elsif @source.respond_to?(:read) IO.copy_stream(@source, ProcIO.new(block)) rewind(@source) elsif @source.is_a?(Enumerable) @source.each(&block) end self end # Request bodies are equivalent when they have the same source. def ==(other) self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf end private def rewind(io) io.rewind if io.respond_to? :rewind rescue Errno::ESPIPE, Errno::EPIPE # Pipe IOs respond to `:rewind` but fail when you call it. # # Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI, # but *EPIPE* on jRuby. # # - **ESPIPE** -- "Illegal seek." # Invalid seek operation (such as on a pipe). # # - **EPIPE** -- "Broken pipe." # There is no process reading from the other end of a pipe. Every # library function that returns this error code also generates # a SIGPIPE signal; this signal terminates the program if not handled # or blocked. Thus, your program will never actually see EPIPE unless # it has handled or blocked SIGPIPE. # # See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html nil end def validate_source_type! return if @source.is_a?(String) return if @source.respond_to?(:read) return if @source.is_a?(Enumerable) return if @source.nil? raise RequestError, "body of wrong type: #{@source.class}" end # This class provides a "writable IO" wrapper around a proc object, with # #write simply calling the proc, which we can pass in as the # "destination IO" in IO.copy_stream. class ProcIO def initialize(block) @block = block end def write(data) @block.call(data) data.bytesize end end end end end http-4.4.1/lib/http/request.rb0000644000004100000410000001455213640204124016274 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "base64" require "time" require "http/errors" require "http/headers" require "http/request/body" require "http/request/writer" require "http/version" require "http/uri" module HTTP class Request extend Forwardable include HTTP::Headers::Mixin # The method given was not understood class UnsupportedMethodError < RequestError; end # The scheme of given URI was not understood class UnsupportedSchemeError < RequestError; end # Default User-Agent header value USER_AGENT = "http.rb/#{HTTP::VERSION}" METHODS = [ # RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 :options, :get, :head, :post, :put, :delete, :trace, :connect, # RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV :propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock, # RFC 3648: WebDAV Ordered Collections Protocol :orderpatch, # RFC 3744: WebDAV Access Control Protocol :acl, # RFC 6352: vCard Extensions to WebDAV -- CardDAV :report, # RFC 5789: PATCH Method for HTTP :patch, # draft-reschke-webdav-search: WebDAV Search :search ].freeze # Allowed schemes SCHEMES = %i[http https ws wss].freeze # Default ports of supported schemes PORTS = { :http => 80, :https => 443, :ws => 80, :wss => 443 }.freeze # Method is given as a lowercase symbol e.g. :get, :post attr_reader :verb # Scheme is normalized to be a lowercase symbol e.g. :http, :https attr_reader :scheme attr_reader :uri_normalizer # "Request URI" as per RFC 2616 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html attr_reader :uri attr_reader :proxy, :body, :version # @option opts [String] :version # @option opts [#to_s] :verb HTTP request method # @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER) # @option opts [HTTP::URI, #to_s] :uri # @option opts [Hash] :headers # @option opts [Hash] :proxy # @option opts [String, Enumerable, IO, nil] :body def initialize(opts) @verb = opts.fetch(:verb).to_s.downcase.to_sym @uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER @uri = @uri_normalizer.call(opts.fetch(:uri)) @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb) raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme) @proxy = opts[:proxy] || {} @version = opts[:version] || "1.1" @headers = prepare_headers(opts[:headers]) @body = prepare_body(opts[:body]) end # Returns new Request with updated uri def redirect(uri, verb = @verb) headers = self.headers.dup headers.delete(Headers::HOST) self.class.new( :verb => verb, :uri => @uri.join(uri), :headers => headers, :proxy => proxy, :body => body.source, :version => version, :uri_normalizer => uri_normalizer ) end # Stream the request to a socket def stream(socket) include_proxy_headers if using_proxy? && !@uri.https? Request::Writer.new(socket, body, headers, headline).stream end # Is this request using a proxy? def using_proxy? proxy && proxy.keys.size >= 2 end # Is this request using an authenticated proxy? def using_authenticated_proxy? proxy && proxy.keys.size >= 4 end def include_proxy_headers headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers) include_proxy_authorization_header if using_authenticated_proxy? end # Compute and add the Proxy-Authorization header def include_proxy_authorization_header headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header end def proxy_authorization_header digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}") "Basic #{digest}" end # Setup tunnel through proxy for SSL request def connect_using_proxy(socket) Request::Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy end # Compute HTTP request header for direct or proxy request def headline request_uri = if using_proxy? && !uri.https? uri.omit(:fragment) else uri.request_uri end "#{verb.to_s.upcase} #{request_uri} HTTP/#{version}" end # Compute HTTP request header SSL proxy connection def proxy_connect_header "CONNECT #{host}:#{port} HTTP/#{version}" end # Headers to send with proxy connect request def proxy_connect_headers connect_headers = HTTP::Headers.coerce( Headers::HOST => headers[Headers::HOST], Headers::USER_AGENT => headers[Headers::USER_AGENT] ) connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy? connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers) connect_headers end # Host for tcp socket def socket_host using_proxy? ? proxy[:proxy_address] : host end # Port for tcp socket def socket_port using_proxy? ? proxy[:proxy_port] : port end # Human-readable representation of base request info. # # @example # # req.inspect # # => # # # @return [String] def inspect "#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>" end private # @!attribute [r] host # @return [String] def_delegator :@uri, :host # @!attribute [r] port # @return [Fixnum] def port @uri.port || @uri.default_port end # @return [String] Default host (with port if needed) header value. def default_host_header_value PORTS[@scheme] != port ? "#{host}:#{port}" : host end def prepare_body(body) body.is_a?(Request::Body) ? body : Request::Body.new(body) end def prepare_headers(headers) headers = HTTP::Headers.coerce(headers || {}) headers[Headers::HOST] ||= default_host_header_value headers[Headers::USER_AGENT] ||= USER_AGENT headers end end end http-4.4.1/lib/http/options.rb0000644000004100000410000001165413640204124016277 0ustar www-datawww-data# frozen_string_literal: true # rubocop:disable Metrics/ClassLength require "http/headers" require "openssl" require "socket" require "http/uri" module HTTP class Options @default_socket_class = TCPSocket @default_ssl_socket_class = OpenSSL::SSL::SSLSocket @default_timeout_class = HTTP::Timeout::Null @available_features = {} class << self attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class attr_reader :available_features def new(options = {}) # rubocop:disable Style/OptionHash return options if options.is_a?(self) super end def defined_options @defined_options ||= [] end def register_feature(name, impl) @available_features[name] = impl end protected def def_option(name, reader_only: false, &interpreter) defined_options << name.to_sym interpreter ||= lambda { |v| v } if reader_only attr_reader name else attr_accessor name protected :"#{name}=" end define_method(:"with_#{name}") do |value| dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) } end end end def initialize(options = {}) # rubocop:disable Style/OptionHash defaults = { :response => :auto, :proxy => {}, :timeout_class => self.class.default_timeout_class, :timeout_options => {}, :socket_class => self.class.default_socket_class, :nodelay => false, :ssl_socket_class => self.class.default_ssl_socket_class, :ssl => {}, :keep_alive_timeout => 5, :headers => {}, :cookies => {}, :encoding => nil, :features => {} } opts_w_defaults = defaults.merge(options) opts_w_defaults[:headers] = HTTP::Headers.coerce(opts_w_defaults[:headers]) opts_w_defaults.each { |(k, v)| self[k] = v } end def_option :headers do |new_headers| headers.merge(new_headers) end def_option :cookies do |new_cookies| new_cookies.each_with_object cookies.dup do |(k, v), jar| cookie = k.is_a?(Cookie) ? k : Cookie.new(k.to_s, v.to_s) jar[cookie.name] = cookie.cookie_value end end def_option :encoding do |encoding| self.encoding = Encoding.find(encoding) end def_option :features, :reader_only => true do |new_features| # Normalize features from: # # [{feature_one: {opt: 'val'}}, :feature_two] # # into: # # {feature_one: {opt: 'val'}, feature_two: {}} normalized_features = new_features.each_with_object({}) do |feature, h| if feature.is_a?(Hash) h.merge!(feature) else h[feature] = {} end end features.merge(normalized_features) end def features=(features) @features = features.each_with_object({}) do |(name, opts_or_feature), h| h[name] = if opts_or_feature.is_a?(Feature) opts_or_feature else unless (feature = self.class.available_features[name]) argument_error! "Unsupported feature: #{name}" end feature.new(**opts_or_feature) end end end %w[ proxy params form json body response socket_class nodelay ssl_socket_class ssl_context ssl keep_alive_timeout timeout_class timeout_options ].each do |method_name| def_option method_name end def_option :follow, :reader_only => true def follow=(value) @follow = case when !value then nil when true == value then {} when value.respond_to?(:fetch) then value else argument_error! "Unsupported follow options: #{value}" end end def_option :persistent, :reader_only => true def persistent=(value) @persistent = value ? HTTP::URI.parse(value).origin : nil end def persistent? !persistent.nil? end def merge(other) h1 = to_hash h2 = other.to_hash merged = h1.merge(h2) do |k, v1, v2| case k when :headers v1.merge(v2) else v2 end end self.class.new(merged) end def to_hash hash_pairs = self.class. defined_options. flat_map { |opt_name| [opt_name, send(opt_name)] } Hash[*hash_pairs] end def dup dupped = super yield(dupped) if block_given? dupped end def feature(name) features[name] end protected def []=(option, val) send(:"#{option}=", val) end private def argument_error!(message) raise(Error, message, caller(1..-1)) end end end http-4.4.1/lib/http/content_type.rb0000644000004100000410000000112413640204124017306 0ustar www-datawww-data# frozen_string_literal: true module HTTP ContentType = Struct.new(:mime_type, :charset) do MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)} CHARSET_RE = /;\s*charset=([^;]+)/i class << self # Parse string and return ContentType struct def parse(str) new mime_type(str), charset(str) end private # :nodoc: def mime_type(str) m = str.to_s[MIME_TYPE_RE, 1] m && m.strip.downcase end # :nodoc: def charset(str) m = str.to_s[CHARSET_RE, 1] m && m.strip.delete('"') end end end end http-4.4.1/lib/http/features/0000755000004100000410000000000013640204124016066 5ustar www-datawww-datahttp-4.4.1/lib/http/features/auto_deflate.rb0000644000004100000410000000555613640204124021062 0ustar www-datawww-data# frozen_string_literal: true require "zlib" require "tempfile" require "http/request/body" module HTTP module Features class AutoDeflate < Feature attr_reader :method def initialize(**) super @method = @opts.key?(:method) ? @opts[:method].to_s : "gzip" raise Error, "Only gzip and deflate methods are supported" unless %w[gzip deflate].include?(@method) end def wrap_request(request) return request unless method return request if request.body.size.zero? # We need to delete Content-Length header. It will be set automatically by HTTP::Request::Writer request.headers.delete(Headers::CONTENT_LENGTH) request.headers[Headers::CONTENT_ENCODING] = method Request.new( :version => request.version, :verb => request.verb, :uri => request.uri, :headers => request.headers, :proxy => request.proxy, :body => deflated_body(request.body), :uri_normalizer => request.uri_normalizer ) end def deflated_body(body) case method when "gzip" GzippedBody.new(body) when "deflate" DeflatedBody.new(body) end end HTTP::Options.register_feature(:auto_deflate, self) class CompressedBody < HTTP::Request::Body def initialize(uncompressed_body) @body = uncompressed_body @compressed = nil end def size compress_all! unless @compressed @compressed.size end def each(&block) return to_enum __method__ unless block if @compressed compressed_each(&block) else compress(&block) end self end private def compressed_each while (data = @compressed.read(Connection::BUFFER_SIZE)) yield data end ensure @compressed.close! end def compress_all! @compressed = Tempfile.new("http-compressed_body", :binmode => true) compress { |data| @compressed.write(data) } @compressed.rewind end end class GzippedBody < CompressedBody def compress(&block) gzip = Zlib::GzipWriter.new(BlockIO.new(block)) @body.each { |chunk| gzip.write(chunk) } ensure gzip.finish end class BlockIO def initialize(block) @block = block end def write(data) @block.call(data) end end end class DeflatedBody < CompressedBody def compress deflater = Zlib::Deflate.new @body.each { |chunk| yield deflater.deflate(chunk) } yield deflater.finish ensure deflater.close end end end end end http-4.4.1/lib/http/features/logging.rb0000644000004100000410000000271313640204124020044 0ustar www-datawww-data# frozen_string_literal: true module HTTP module Features # Log requests and responses. Request verb and uri, and Response status are # logged at `info`, and the headers and bodies of both are logged at # `debug`. Be sure to specify the logger when enabling the feature: # # HTTP.use(logging: {logger: Logger.new(STDOUT)}).get("https://example.com/") # class Logging < Feature attr_reader :logger def initialize(logger: NullLogger.new) @logger = logger end def wrap_request(request) logger.info { "> #{request.verb.to_s.upcase} #{request.uri}" } logger.debug do headers = request.headers.map { |name, value| "#{name}: #{value}" }.join("\n") body = request.body.source headers + "\n\n" + body.to_s end request end def wrap_response(response) logger.info { "< #{response.status}" } logger.debug do headers = response.headers.map { |name, value| "#{name}: #{value}" }.join("\n") body = response.body.to_s headers + "\n\n" + body end response end class NullLogger %w[fatal error warn info debug].each do |level| define_method(level.to_sym) do |*_args| nil end define_method(:"#{level}?") do true end end end HTTP::Options.register_feature(:logging, self) end end end http-4.4.1/lib/http/features/normalize_uri.rb0000644000004100000410000000050213640204124021267 0ustar www-datawww-data# frozen_string_literal: true require "http/uri" module HTTP module Features class NormalizeUri < Feature attr_reader :normalizer def initialize(normalizer: HTTP::URI::NORMALIZER) @normalizer = normalizer end HTTP::Options.register_feature(:normalize_uri, self) end end end http-4.4.1/lib/http/features/instrumentation.rb0000644000004100000410000000371513640204124021664 0ustar www-datawww-data# frozen_string_literal: true module HTTP module Features # Instrument requests and responses. Expects an # ActiveSupport::Notifications-compatible instrumenter. Defaults to use a # namespace of 'http' which may be overridden with a `:namespace` param. # Emits a single event like `"request.{namespace}"`, eg `"request.http"`. # Be sure to specify the instrumenter when enabling the feature: # # HTTP # .use(instrumentation: {instrumenter: ActiveSupport::Notifications.instrumenter}) # .get("https://example.com/") # # Emits two events on every request: # # * `start_request.http` before the request is made, so you can log the reqest being started # * `request.http` after the response is recieved, and contains `start` # and `finish` so the duration of the request can be calculated. # class Instrumentation < Feature attr_reader :instrumenter, :name def initialize(instrumenter: NullInstrumenter.new, namespace: "http") @instrumenter = instrumenter @name = "request.#{namespace}" end def wrap_request(request) # Emit a separate "start" event, so a logger can print the request # being run without waiting for a response instrumenter.instrument("start_#{name}", :request => request) {} instrumenter.start(name, :request => request) request end def wrap_response(response) instrumenter.finish(name, :response => response) response end HTTP::Options.register_feature(:instrumentation, self) class NullInstrumenter def instrument(name, payload = {}) start(name, payload) begin yield payload if block_given? ensure finish name, payload end end def start(_name, _payload) true end def finish(_name, _payload) true end end end end end http-4.4.1/lib/http/features/auto_inflate.rb0000644000004100000410000000213013640204124021061 0ustar www-datawww-data# frozen_string_literal: true require "set" module HTTP module Features class AutoInflate < Feature SUPPORTED_ENCODING = Set.new(%w[deflate gzip x-gzip]).freeze private_constant :SUPPORTED_ENCODING def wrap_response(response) return response unless supported_encoding?(response) options = { :status => response.status, :version => response.version, :headers => response.headers, :proxy_headers => response.proxy_headers, :connection => response.connection, :body => stream_for(response.connection) } options[:uri] = response.uri if response.uri Response.new(options) end def stream_for(connection) Response::Body.new(Response::Inflater.new(connection)) end private def supported_encoding?(response) content_encoding = response.headers.get(Headers::CONTENT_ENCODING).first content_encoding && SUPPORTED_ENCODING.include?(content_encoding) end HTTP::Options.register_feature(:auto_inflate, self) end end end http-4.4.1/lib/http/mime_type.rb0000644000004100000410000000327713640204124016576 0ustar www-datawww-data# frozen_string_literal: true module HTTP # MIME type encode/decode adapters module MimeType class << self # Associate MIME type with adapter # # @example # # module JsonAdapter # class << self # def encode(obj) # # encode logic here # end # # def decode(str) # # decode logic here # end # end # end # # HTTP::MimeType.register_adapter 'application/json', MyJsonAdapter # # @param [#to_s] type # @param [#encode, #decode] adapter # @return [void] def register_adapter(type, adapter) adapters[type.to_s] = adapter end # Returns adapter associated with MIME type # # @param [#to_s] type # @raise [Error] if no adapter found # @return [Class] def [](type) adapters[normalize type] || raise(Error, "Unknown MIME type: #{type}") end # Register a shortcut for MIME type # # @example # # HTTP::MimeType.register_alias 'application/json', :json # # @param [#to_s] type # @param [#to_sym] shortcut # @return [void] def register_alias(type, shortcut) aliases[shortcut.to_sym] = type.to_s end # Resolves type by shortcut if possible # # @param [#to_s] type # @return [String] def normalize(type) aliases.fetch type, type.to_s end private # :nodoc: def adapters @adapters ||= {} end # :nodoc: def aliases @aliases ||= {} end end end end # built-in mime types require "http/mime_type/json" http-4.4.1/lib/http/redirector.rb0000644000004100000410000000564513640204124016751 0ustar www-datawww-data# frozen_string_literal: true require "set" require "http/headers" module HTTP class Redirector # Notifies that we reached max allowed redirect hops class TooManyRedirectsError < ResponseError; end # Notifies that following redirects got into an endless loop class EndlessRedirectError < TooManyRedirectsError; end # HTTP status codes which indicate redirects REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze # Codes which which should raise StateError in strict mode if original # request was any of {UNSAFE_VERBS} STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze # Insecure http verbs, which should trigger StateError in strict mode # upon {STRICT_SENSITIVE_CODES} UNSAFE_VERBS = %i[put delete post].to_set.freeze # Verbs which will remain unchanged upon See Other response. SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze # @!attribute [r] strict # Returns redirector policy. # @return [Boolean] attr_reader :strict # @!attribute [r] max_hops # Returns maximum allowed hops. # @return [Fixnum] attr_reader :max_hops # @param [Hash] opts # @option opts [Boolean] :strict (true) redirector hops policy # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops def initialize(opts = {}) # rubocop:disable Style/OptionHash @strict = opts.fetch(:strict, true) @max_hops = opts.fetch(:max_hops, 5).to_i end # Follows redirects until non-redirect response found def perform(request, response) @request = request @response = response @visited = [] while REDIRECT_CODES.include? @response.status.code @visited << "#{@request.verb} #{@request.uri}" raise TooManyRedirectsError if too_many_hops? raise EndlessRedirectError if endless_loop? @response.flush # XXX(ixti): using `Array#inject` to return `nil` if no Location header. @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+)) @response = yield @request end @response end private # Check if we reached max amount of redirect hops # @return [Boolean] def too_many_hops? 1 <= @max_hops && @max_hops < @visited.count end # Check if we got into an endless loop # @return [Boolean] def endless_loop? 2 <= @visited.count(@visited.last) end # Redirect policy for follow # @return [Request] def redirect_to(uri) raise StateError, "no Location header in redirect" unless uri verb = @request.verb code = @response.status.code if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code) raise StateError, "can't follow #{@response.status} redirect" if @strict verb = :get end verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code @request.redirect(uri, verb) end end end http-4.4.1/lib/http/response.rb0000644000004100000410000001071113640204124016433 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "http/headers" require "http/content_type" require "http/mime_type" require "http/response/status" require "http/response/inflater" require "http/uri" require "http/cookie_jar" require "time" module HTTP class Response extend Forwardable include HTTP::Headers::Mixin # @return [Status] attr_reader :status # @return [String] attr_reader :version # @return [Body] attr_reader :body # @return [URI, nil] attr_reader :uri # @return [Hash] attr_reader :proxy_headers # Inits a new instance # # @option opts [Integer] :status Status code # @option opts [String] :version HTTP version # @option opts [Hash] :headers # @option opts [Hash] :proxy_headers # @option opts [HTTP::Connection] :connection # @option opts [String] :encoding Encoding to use when reading body # @option opts [String] :body # @option opts [String] :uri def initialize(opts) @version = opts.fetch(:version) @uri = HTTP::URI.parse(opts.fetch(:uri)) if opts.include? :uri @status = HTTP::Response::Status.new(opts.fetch(:status)) @headers = HTTP::Headers.coerce(opts[:headers] || {}) @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {}) if opts.include?(:body) @body = opts.fetch(:body) else connection = opts.fetch(:connection) encoding = opts[:encoding] || charset || Encoding::BINARY @body = Response::Body.new(connection, :encoding => encoding) end end # @!method reason # @return (see HTTP::Response::Status#reason) def_delegator :status, :reason # @!method code # @return (see HTTP::Response::Status#code) def_delegator :status, :code # @!method to_s # (see HTTP::Response::Body#to_s) def_delegator :body, :to_s alias to_str to_s # @!method readpartial # (see HTTP::Response::Body#readpartial) def_delegator :body, :readpartial # @!method connection # (see HTTP::Response::Body#connection) def_delegator :body, :connection # Returns an Array ala Rack: `[status, headers, body]` # # @return [Array(Fixnum, Hash, String)] def to_a [status.to_i, headers.to_h, body.to_s] end # Flushes body and returns self-reference # # @return [Response] def flush body.to_s self end # Value of the Content-Length header. # # @return [nil] if Content-Length was not given, or it's value was invalid # (not an integer, e.g. empty string or string with non-digits). # @return [Integer] otherwise def content_length # http://greenbytes.de/tech/webdav/rfc7230.html#rfc.section.3.3.3 # Clause 3: "If a message is received with both a Transfer-Encoding # and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. return nil if @headers.include?(Headers::TRANSFER_ENCODING) value = @headers[Headers::CONTENT_LENGTH] return nil unless value begin Integer(value) rescue ArgumentError nil end end # Parsed Content-Type header # # @return [HTTP::ContentType] def content_type @content_type ||= ContentType.parse headers[Headers::CONTENT_TYPE] end # @!method mime_type # MIME type of response (if any) # @return [String, nil] def_delegator :content_type, :mime_type # @!method charset # Charset of response (if any) # @return [String, nil] def_delegator :content_type, :charset def cookies @cookies ||= headers.each_with_object CookieJar.new do |(k, v), jar| jar.parse(v, uri) if k == Headers::SET_COOKIE end end def chunked? return false unless @headers.include?(Headers::TRANSFER_ENCODING) encoding = @headers.get(Headers::TRANSFER_ENCODING) # TODO: "chunked" is frozen in the request writer. How about making it accessible? encoding.last == "chunked" end # Parse response body with corresponding MIME type adapter. # # @param [#to_s] as Parse as given MIME type # instead of the one determined from headers # @raise [HTTP::Error] if adapter not found # @return [Object] def parse(as = nil) MimeType[as || mime_type].decode to_s end # Inspect a response def inspect "#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>" end end end http-4.4.1/lib/http/headers.rb0000644000004100000410000001234313640204124016213 0ustar www-datawww-data# frozen_string_literal: true require "forwardable" require "http/errors" require "http/headers/mixin" require "http/headers/known" module HTTP # HTTP Headers container. class Headers extend Forwardable include Enumerable # Matches HTTP header names when in "Canonical-Http-Format" CANONICAL_NAME_RE = /^[A-Z][a-z]*(?:-[A-Z][a-z]*)*$/ # Matches valid header field name according to RFC. # @see http://tools.ietf.org/html/rfc7230#section-3.2 COMPLIANT_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/ # Class constructor. def initialize @pile = [] end # Sets header. # # @param (see #add) # @return [void] def set(name, value) delete(name) add(name, value) end alias []= set # Removes header. # # @param [#to_s] name header name # @return [void] def delete(name) name = normalize_header name.to_s @pile.delete_if { |k, _| k == name } end # Appends header. # # @param [#to_s] name header name # @param [Array<#to_s>, #to_s] value header value(s) to be appended # @return [void] def add(name, value) name = normalize_header name.to_s Array(value).each { |v| @pile << [name, v.to_s] } end # Returns list of header values if any. # # @return [Array] def get(name) name = normalize_header name.to_s @pile.select { |k, _| k == name }.map { |_, v| v } end # Smart version of {#get}. # # @return [nil] if header was not set # @return [String] if header has exactly one value # @return [Array] if header has more than one value def [](name) values = get(name) case values.count when 0 then nil when 1 then values.first else values end end # Tells whenever header with given `name` is set or not. # # @return [Boolean] def include?(name) name = normalize_header name.to_s @pile.any? { |k, _| k == name } end # Returns Rack-compatible headers Hash # # @return [Hash] def to_h Hash[keys.map { |k| [k, self[k]] }] end alias to_hash to_h # Returns headers key/value pairs. # # @return [Array<[String, String]>] def to_a @pile.map { |pair| pair.map(&:dup) } end # Returns human-readable representation of `self` instance. # # @return [String] def inspect "#<#{self.class} #{to_h.inspect}>" end # Returns list of header names. # # @return [Array] def keys @pile.map { |k, _| k }.uniq end # Compares headers to another Headers or Array of key/value pairs # # @return [Boolean] def ==(other) return false unless other.respond_to? :to_a @pile == other.to_a end # Calls the given block once for each key/value pair in headers container. # # @return [Enumerator] if no block given # @return [Headers] self-reference def each return to_enum(__method__) unless block_given? @pile.each { |arr| yield(arr) } self end # @!method empty? # Returns `true` if `self` has no key/value pairs # # @return [Boolean] def_delegator :@pile, :empty? # @!method hash # Compute a hash-code for this headers container. # Two conatiners with the same content will have the same hash code. # # @see http://www.ruby-doc.org/core/Object.html#method-i-hash # @return [Fixnum] def_delegator :@pile, :hash # Properly clones internal key/value storage. # # @api private def initialize_copy(orig) super @pile = to_a end # Merges `other` headers into `self`. # # @see #merge # @return [void] def merge!(other) self.class.coerce(other).to_h.each { |name, values| set name, values } end # Returns new instance with `other` headers merged in. # # @see #merge! # @return [Headers] def merge(other) dup.tap { |dupped| dupped.merge! other } end class << self # Coerces given `object` into Headers. # # @raise [Error] if object can't be coerced # @param [#to_hash, #to_h, #to_a] object # @return [Headers] def coerce(object) unless object.is_a? self object = case when object.respond_to?(:to_hash) then object.to_hash when object.respond_to?(:to_h) then object.to_h when object.respond_to?(:to_a) then object.to_a else raise Error, "Can't coerce #{object.inspect} to Headers" end end headers = new object.each { |k, v| headers.add k, v } headers end alias [] coerce end private # Transforms `name` to canonical HTTP header capitalization # # @param [String] name # @raise [HeaderError] if normalized name does not # match {HEADER_NAME_RE} # @return [String] canonical HTTP header name def normalize_header(name) return name if name =~ CANONICAL_NAME_RE normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") return normalized if normalized =~ COMPLIANT_NAME_RE raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" end end end http-4.4.1/CONTRIBUTING.md0000644000004100000410000000135313640204124014756 0ustar www-datawww-data# Help and Discussion If you need help or just want to talk about the http.rb, visit the http.rb Google Group: https://groups.google.com/forum/#!forum/httprb You can join by email by sending a message to: [httprb+subscribe@googlegroups.com](mailto:httprb+subscribe@googlegroups.com) # Reporting bugs The best way to report a bug is by providing a reproduction script. A half working script with comments for the parts you were unable to automate is still appreciated. In any case, specify following info in description of your issue: - What you're trying to accomplish - What you expected to happen - What actually happened - The exception backtrace(s), if any - Version of gem or commit ref you are using - Version of ruby you are using http-4.4.1/logo.png0000644000004100000410000001735313640204124014202 0ustar www-datawww-dataPNG  IHDR^oPLTETTTkkkіBBB   }}} ֣ę| ###gggɣ  ???  xxxy  XXX䂂,(&&& Χ  +   ttt&    &󭭭___KKKGGG... ooobbb[[[;;;222  ˠ))) ~ PJE777! skf]W ~yOOO{ f :晙;70-(! Em}e_$&t 1^"0$% ⢟wsWQPIKG@8++   蘕51)%ވvp`\(752X 釁zzzHE6C;876l֒ꑌyvWTTRݮѤwticAATLG86(Ϲɍ-,Ǿ,IDATxݿKcAX*LaRhq&1@!9H +A? &V*XXYV%1~zou?y3/AAA`%3XiStO_)x’P;B.o(O(+ ޽0CA)R   IN?P(}8+!}r    :H1~Q MG)G:N1Ջ<\6 ( EDܑt6zg[>i0~6KD,dyRPy kkghh="֘RRF2F{gBbiq&{ K) vj.|eSqn{$1oPzj)فzpN^i1CC<I5&V TwPk> s|UDzu^PJx -J\%#_ Y0IGGeJ e0v0ڇeG0KCcIrn|ᠼjiep/"U90G}kEfEu&h#Ӄ 6ភI& ^Q]ph gR *p*i_V8K4NiqSrzKȂsWp4I>ToZGbhQ/ ig4 Wu<٧b98)<'הɃ˸=9d0CY{@kzSsB "=gWlZ> ۯ=KIG8Es**\)>Ȃ3񎉡R4;En Ƚ-%8b=4p$ЮMw{~A?qb՚ p a4],kE jm84pr$5MWqa?pbϜ8L{->4K$O| HTĖ#kl.NN}$3|39[u#@0/s7gkZ< B{2m}2-u@zj,: 骘Dק?‚ƾ NY | s&%wzs)[7YcCr?u O ͚d2݆ Dȕ}*Κhҵ Ny0_4Q9ێg :lҝkMlmȚ[Jm#l}(ÖP{s."M̹ =U 36 6Z쥥rxlr'w%q*zϐ"b`t"%Z&Hbu]ޞvY=oꩂ%Z #>$?z&-- ĺRc},l@@4irE> %d5Pk(6%hL cvIXe$;%4:f!MZi[k&-dłǑ4)YimI6Ykk@(-tI !aκY7U۔lvUr' 5[xܔ߆g ASYƴ, S w|Á@Hji~܀.i:Pt]NҎ;uP{b U>uٷ |҃_8 a%,쭧_o'=;-.T!xe ;/-o_FwNN,\kG/f{hmtyg$we0c~M|{^(JT)6R4.XJaY*))4H!$*DPHyah)g{~ֳ3?s)֎e%& 1k"*#LY(V{+KNt)_ڶ cy((5=K/mBQ3* ]ڴ/,>w{euH7@1u;8bx/=nS@$d +,49[ :1Y2J1,>6DCHgnW QcO!vߵPY'9"lo,#{ =m UmRHprW[_}kUD[<eB>^U9*FL;e.>'+rDHj܇~Nmjaq#0|-5q4ڊ'{tNč t(X;ȜrŌMk2w N8TV6 Ziq38%)LZQ_#zA>e}@V >9&ОGBY^!q_ᮽzpN׈rv Dpe FZ=m}IIIF"x$Zo4JEkqv%)?Ʉ(Ӗ݂>yvIW';ƁeE9*H)W+4<.u g!v#@L?KR_RIVM"XpG3qGf|M(JG'"-;b B]LQw.3gOhYM Y? *5ԛx羪TLx]O#r?o/@%o`?x#g q @_aFJ+cxkfE%*79&LkPwdFbLJ M6{Lrs܊zi٩ZZ1c_?4`:fW'BXW¸pCneq"J#1.u4'dt?[ L< x'>w^[?\D"XYR.-.+rAU_r/:Afq?'x'x'xrk- tT!fيo\?L?_2'yoٵYc-m嬅8xWjp+o][v՜(?pwnIVoYg6c9XGݮi|y$ߵ'mm纏\pakr:5NO#m1a+-vS,}A[POuot_ $g_z2[A#vY }kʕyş|_}qXPYfdzݜ6B4zy`1Fz !/;(}9AnLw kJ /Xj/_N(uCg, tD!^D)$o;:5};%\RD1pBf ! p©º QQxUZ.O &iCp ,~b&80O@:ۺ [0,))m("v"|xy&d`F N5v^8}z 9vf@d&nAH!S@x3v Tù 5%$M\ϺniD;U(z0s&)DF,՝}};7TT44%fp­ygU"a &͛ ` M%=-._& ._䒦J$& )6"?\9ZubuKM`) tuܔ2]W1A p!x!p[3m0B`AH꾒 089{NKYeggh3HI7 1э.ܹsg x!z <0w/ LH%萐̀F߲i77 1nAQв nD;|Dh m]C!fگ Hg 8.'4L!@ R+iV0~tCi-,s $Vy֒gTjtrIQodT;mmBOr0o-@!4]E{Lf@L1!Ag,;HOWO~G[1 D B00\ nPpCP%JZ^R2^Nuj!^ MyTUWvb2 Ynڳ=$.!&4̆l H^/(k0HQk[ccgw6Kp`[L-` x kFZ)SB,w~[ :d؂4i :ho-$ !KFH)Q[e t aB&A ,?N"^VK44.!###pu[ {|L%(Ja VFݼT %F( Y!S2p ڋhw-* q д %gQg%RNA/̃hRNrX!N"QX8  ǏW=vb~PB+uj 8۷k󮡚D+V&/k+*P-+ UurnTB x' B-I)'yl#a (ں^NiT2)´/ :A^B߽pBvȖ[@>*Wxf6~@:x lvT#GoϜ9ٚ )) J&\ퟆ _,"&7r /wpNlБo8 R4rier sVP޼0' G$Ԟ̉^zg$ f@M4"^>9xcuBN@q{2 zxC*Bl?Dg:t)0Qi N9qd`C`91  qVmQ@a=Oȡ"젂>^Ɔ 92ں:p'ɱo0?71,#{QHyG<Ƌ#`*- oߴIEgxf}CԃÃpl2fߞ Bhy@3@i7HD†@I[UP 26n1'^A,2[n(A#[]6Ɲ ~%5=V`4A4SVղB,LƘ̠XA#m|p-( dRm/PGb0.A5JBPHbҭ9;,:O$ pkpXuf!@T Ir;YC,㑂0fwd` Ƙ)P&xٮJNdO؅K$,ӤFa$J pvs{*=I8v!$\U+cAHɲ M4FnO&y Vpw|L'qaPk+ OS NMÕ'V٫j;A;2 (:Jg7^S ėO,Pu,\j^_y-Kl_@TUUUUUv@S%6pC 3T8@!2>_]@; 0 ӻ,}8qocIENDB`http-4.4.1/Guardfile0000644000004100000410000000071313640204124014351 0ustar www-datawww-data# frozen_string_literal: true # More info at https://github.com/guard/guard#readme guard :rspec, :cmd => "GUARD_RSPEC=1 bundle exec rspec --no-profile" do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # RSpec files rspec = dsl.rspec watch(rspec.spec_helper) { rspec.spec_dir } watch(rspec.spec_support) { rspec.spec_dir } watch(rspec.spec_files) # Ruby files ruby = dsl.ruby dsl.watch_spec_files_for(ruby.lib_files) end http-4.4.1/.yardopts0000644000004100000410000000005613640204124014372 0ustar www-datawww-data--markup-provider=redcarpet --markup=markdown http-4.4.1/Gemfile0000644000004100000410000000142113640204124014014 0ustar www-datawww-data# frozen_string_literal: true source "https://rubygems.org" ruby RUBY_VERSION gem "rake" group :development do gem "guard-rspec", :require => false gem "nokogiri", :require => false gem "pry", :require => false platform :ruby_20 do gem "pry-debugger", :require => false gem "pry-stack_explorer", :require => false end end group :test do gem "activemodel", :require => false # Used by certificate_authority gem "certificate_authority", :require => false gem "backports" gem "coveralls", :require => false gem "simplecov", ">= 0.9" gem "rspec", "~> 3.0" gem "rspec-its" gem "rubocop", "= 0.49.1" gem "yardstick" end group :doc do gem "kramdown" gem "yard" end # Specify your gem's dependencies in http.gemspec gemspec http-4.4.1/CHANGES.md0000644000004100000410000005455413640204124014132 0ustar www-datawww-data## 4.4.1 (2020-03-29) * Backport [#590](https://github.com/httprb/http/pull/590) Fix parser failing on some edge cases. ([@ixti]) ## 4.4.0 (2020-03-25) * Backport [#587](https://github.com/httprb/http/pull/587) Fix redirections when server responds with multiple Location headers. ([@ixti]) * Backport [#599](https://github.com/httprb/http/pull/599) Allow passing HTTP::FormData::{Multipart,UrlEncoded} object directly. ([@ixti]) ## 4.3.0 (2020-01-09) * Backport [#581](https://github.com/httprb/http/pull/581) Add Ruby-2.7 compatibility. ([@ixti], [@janko]) ## 4.2.0 (2019-10-22) * Backport [#489](https://github.com/httprb/http/pull/489) Fix HTTP parser. ([@ixti], [@fxposter]) ## 4.1.1 (2019-03-12) * Add `HTTP::Headers::ACCEPT_ENCODING` constant. ([@ixti]) ## 4.1.0 (2019-03-11) * [#533](https://github.com/httprb/http/pull/533) Add URI normalizer feature that allows to swap default URI normalizer. ([@mamoonraja]) ## 4.0.5 (2019-02-15) * Backport [#532](https://github.com/httprb/http/pull/532) from master. Fix pipes support in request bodies. ([@ixti]) ## 4.0.4 (2019-02-12) * Backport [#506](https://github.com/httprb/http/pull/506) from master. Skip auto-deflate when there is no body. ([@Bonias]) ## 4.0.3 (2019-01-18) * Fix missing URL in response wrapped by auto inflate. ([@ixti]) * Provide `HTTP::Request#inspect` method for debugging purposes. ([@ixti]) ## 4.0.2 (2019-01-15) * [#506](https://github.com/httprb/http/pull/506) Fix instrumentation feature. ([@paul]) ## 4.0.1 (2019-01-14) * [#515](https://github.com/httprb/http/pull/515) Fix `#build_request` and `#request` to respect default options. ([@RickCSong]) ## 4.0.0 (2018-10-15) * [#482](https://github.com/httprb/http/pull/482) [#499](https://github.com/httprb/http/pull/499) Introduce new features injection API with 2 new feaures: instrumentation (compatible with ActiveSupport::Notification) and logging. ([@paul]) * [#473](https://github.com/httprb/http/pull/473) Handle early responses. ([@janko-m]) * [#468](https://github.com/httprb/http/pull/468) Rewind `HTTP::Request::Body#source` once `#each` is complete. ([@ixti]) * [#467](https://github.com/httprb/http/pull/467) Drop Ruby 2.2 support. ([@ixti]) * [#436](https://github.com/httprb/http/pull/436) Raise ConnectionError when writing to socket fails. ([@janko-m]) * [#438](https://github.com/httprb/http/pull/438) Expose `HTTP::Request::Body#source`. ([@janko-m]) * [#446](https://github.com/httprb/http/pull/446) Simplify setting a timeout. ([@mikegee]) * [#451](https://github.com/httprb/http/pull/451) Reduce memory usage when reading response body. ([@janko-m]) * [#458](https://github.com/httprb/http/pull/458) Extract HTTP::Client#build_request method. ([@tycoon]) * [#462](https://github.com/httprb/http/pull/462) Fix HTTP::Request#headline to allow two leading slashes in path. ([@scarfacedeb]) * [#454](https://github.com/httprb/http/pull/454) [#464](https://github.com/httprb/http/pull/464) [#384](https://github.com/httprb/http/issues/384) Fix #readpartial not respecting max length argument. ([@janko-m], [@marshall-lee]) ## 3.3.0 (2018-04-25) This version backports some of the fixes and improvements made to development version of the HTTP gem: * [#458](https://github.com/httprb/http/pull/458) Extract HTTP::Client#build_request method. ([@tycoon]) ## 3.2.1 (2018-04-24) * [#468](https://github.com/httprb/http/pull/468) Rewind `HTTP::Request::Body#source` once `#each` is complete. ([@ixti]) ## 3.2.0 (2018-04-22) This version backports one change we missed to backport in previous release: * Reduce memory usage when reading response body ([@janko-m]) ## 3.1.0 (2018-04-22) This version backports some of the fixes and improvements made to development version of the HTTP gem: * Fix for `#readpartial` to respect max length argument. ([@janko-m], [@marshall-lee]) * Fix for `HTTP::Request#headline` to allow two leading slashes in path. ([@scarfacedeb]) * Fix query string building for string with newlines. ([@mikegee]) * Deallocate temporary strings in `Response::Body#to_s`. ([@janko-m]) * Add `Request::Body#source`. ([@janko-m]) ## 3.0.0 (2017-10-01) * Drop support of Ruby `2.0` and Ruby `2.1`. ([@ixti]) * [#410](https://github.com/httprb/http/pull/410) Infer `Host` header upon redirects. ([@janko-m]) * [#409](https://github.com/httprb/http/pull/409) Enables request body streaming on any IO object. ([@janko-m]) * [#413](https://github.com/httprb/http/issues/413), [#414](https://github.com/httprb/http/pull/414) Fix encoding of body chunks. ([@janko-m]) * [#368](https://github.com/httprb/http/pull/368), [#357](https://github.com/httprb/http/issues/357) Fix timeout issue. ([@HoneyryderChuck]) ## 2.2.2 (2017-04-27) * [#404](https://github.com/httprb/http/issues/404), [#405](https://github.com/httprb/http/pull/405) Make keepalive timeout configurable. ([@nestegg]) ## 2.2.1 (2017-02-06) * [#395](https://github.com/httprb/http/issues/395) Fix regression of API, that broke webmock integration. ([@ixti]) ## 2.2.0 (2017-02-03) * [#375](https://github.com/httprb/http/pull/375) Add support for automatic Gzip/Inflate ([@Bonias]) * [#390](https://github.com/httprb/http/pull/390) Add REPORT to the list of valid HTTP verbs ([@ixti]) ## 2.1.0 (2016-11-08) * [#370](https://github.com/httprb/http/issues/370) Add Headers#include? ([@ixti]) * [#364](https://github.com/httprb/http/issues/364) Add HTTP::Response#connection ([@janko-m]) * [#362](https://github.com/httprb/http/issues/362) connect_ssl uses connect_timeout (Closes #359) ([@TiagoCardoso1983]) ## 2.0.3 (2016-08-03) * [#365](https://github.com/httprb/http/issues/365) Add `HTTP::Response#content_length` ([@janko-m]) * [#335](https://github.com/httprb/http/issues/335), [#360](https://github.com/httprb/http/pull/360) Set `Content-Length: 0` header for `nil` bodies. ([@britishtea]) ## 2.0.2 (2016-06-24) * [#353](https://github.com/httprb/http/pull/353) Avoid a dependency cycle between Client and Connection classes. ([@jhbabon]) ## 2.0.1 (2016-05-12) * [#341](https://github.com/httprb/http/pull/341) Refactor some string manipulations so they are more performant (up to 3-4x faster) and more concise. ([@tonyta]) * [#339](https://github.com/httprb/http/pull/341) Always use byte methods when writing/slicing the write buffer. ([@zanker]) ## 2.0.0 (2016-04-23) * [#333](https://github.com/httprb/http/pull/333) Fix HTTPS request headline when sent via proxy. ([@Connorhd]) * [#331](https://github.com/httprb/http/pull/331) Add `#informational?`, `#success?`, `#redirect?`, `#client_error?` and `#server_error?` helpers to `Response::Status`. ([@mwitek]) * [#330](https://github.com/httprb/http/pull/330) Support custom CONNECT headers (request/response) during HTTPS proxy requests. ([@smudge]) * [#319](https://github.com/httprb/http/pull/319) Drop Ruby 1.9.x support. ([@ixti]) ## 1.0.4 (2016-03-19) * [#320](https://github.com/httprb/http/pull/320) Fix timeout regression. ([@tarcieri]) ## 1.0.3 (2016-03-16) * [#314](https://github.com/httprb/http/pull/314) Validate charset before forcing encoding. ([@kylekyle]) * [#318](https://github.com/httprb/http/pull/318) Remove redundant string allocations upon header names normalization. ([@ixti]) ## 1.0.2 (2016-01-15) * [#295](https://github.com/httprb/http/pull/295): Fix redirect following when used with persistent mode. ([@ixti]) ## 1.0.1 (2015-12-27) * [#283](https://github.com/httprb/http/pull/283): Use io/wait on supported platforms. ([@tarcieri]) ## 1.0.0 (2015-12-25) * [#265](https://github.com/httprb/http/pull/265/): Remove deprecations ([@tarcieri]): - HTTP::Chainable#with_follow (use #follow) - HTTP::Chainable#with, #with_headers (use #headers) - HTTP::Chainable#auth(:basic, ...) (use #basic_auth) - HTTP::Chainable#default_headers (use #default_options[:headers]) - HTTP::Headers#append (use #add) - HTTP::Options#[] hash-like API deprecated in favor of explicit methods - HTTP::Request#request_header (use #headline) - HTTP::Response::STATUS_CODES (use HTTP::Status::REASONS) - HTTP::Response::SYMBOL_TO_STATUS_CODE (no replacement) - HTTP::Response#status_code (use #status or #code) - HTTP::Response::Status#symbolize (use #to_sym) * [#269](https://github.com/httprb/http/pull/269/): Close connection in case of error during request. ([@ixti]) * [#271](https://github.com/httprb/http/pull/271/): High-level exception wrappers for low-level I/O errors. ([@ixti]) * [#273](https://github.com/httprb/http/pull/273/): Add encoding option. ([@connorhd]) * [#275](https://github.com/httprb/http/pull/275/): Support for disabling Nagle's algorithm with `HTTP.nodelay`. ([@nerdrew]) * [#276](https://github.com/httprb/http/pull/276) Use Encoding::BINARY as the default encoding for HTTP::Response::Body. ([@tarcieri]) * [#278](https://github.com/httprb/http/pull/278) Use an options hash for HTTP::Request initializer API. ([@ixti]) * [#279](https://github.com/httprb/http/pull/279) Send headers and body in one write if possible. This avoids a pathological case in Nagle's algorithm. ([@tarcieri]) * [#281](https://github.com/httprb/http/pull/281) Remove legacy 'Http' constant alias to 'HTTP'. ([@tarcieri]) ## 0.9.9 (2016-03-16) * *BACKPORT* [#318](https://github.com/httprb/http/pull/318) Remove redundant string allocations upon header names normalization. ([@ixti]) * *BACKPORT* [#295](https://github.com/httprb/http/pull/295): Fix redirect following when used with persistent mode. ([@ixti]) ## 0.9.8 (2015-09-29) * [#260](https://github.com/httprb/http/pull/260): Fixed global timeout persisting time left across requests when reusing connections. ([@zanker]) ## 0.9.7 (2015-09-19) * [#258](https://github.com/httprb/http/pull/258): Unified strategy for handling exception-based and exceptionless non-blocking I/O. Fixes SSL support on JRuby 9000. ([@tarcieri]) ## 0.9.6 (2015-09-06) * [#254](https://github.com/httprb/http/pull/254): Removed use of an ActiveSupport specific method #present? ([@tarcieri]) ## 0.9.5 (2015-09-06) * [#252](https://github.com/httprb/http/pull/252): Fixed infinite hang/timeout when a request contained more than ~16,363 bytes. ([@zanker]) ## 0.9.4 (2015-08-26) * [#246](https://github.com/httprb/http/issues/246): Fixes regression when body streaming was failing on some URIs. ([@zanker]) * [#243](https://github.com/httprb/http/issues/243): Fixes require timeout statements. ([@ixti]) ## 0.9.3 (2015-08-19) * [#246](https://github.com/httprb/http/issues/246): Fixed request URI normalization. ([@ixti]) - Avoids query component normalization - Omits fragment component in headline ## 0.9.2 (2015-08-18) * Fixed exceptionless NIO EOF handling. ([@zanker]) ## 0.9.1 (2015-08-14) * [#246](https://github.com/httprb/http/issues/246): Fix params special-chars escaping. ([@ixti]) ## 0.9.0 (2015-07-23) * [#240](https://github.com/httprb/http/pull/240): Support for caching removed. ([@tarcieri]) * JRuby 9000 compatibility ## 0.8.14 (2015-08-19) * Backport request URI normalization fixes from master. ([@ixti]) ## 0.8.13 (2015-08-14) * Backport params special-chars escaping fix from `v0.9.1`. ([@ixti]) ## 0.8.12 (2015-05-26) * Fix `HTTP.timeout` API (was loosing previously defined options). ([@ixti]) ## 0.8.11 (2015-05-22) * [#229](https://github.com/httprb/http/pull/229): SNI support for HTTPS connections. ([@tarcieri]) * [#227](https://github.com/httprb/http/pull/227): Use "http.rb" in the User-Agent string. ([@tarcieri]) ## 0.8.10 (2015-05-14) * Fix cookie headers generation. ([@ixti]) ## 0.8.9 (2015-05-11) * Add cookies support. ([@ixti]) * [#219](https://github.com/httprb/http/pull/219): Enforce stringified body encoding. ([@Connorhd]) ## 0.8.8 (2015-05-09) * [#217](https://github.com/httprb/http/issues/217): Fix CONNECT header for proxies. ([@Connorhd]) ## 0.8.7 (2015-05-08) * Fix `HTTP.timeout` API with options only given. ([@ixti]) ## 0.8.6 (2015-05-08) * [#215](https://github.com/httprb/http/pull/215): Reset global timeouts after the request finishes. ([@zanker]) ## 0.8.5 (2015-05-06) * [#205](https://github.com/httprb/http/issues/205): Add simple timeouts configuration API. ([@ixti]) * Deprecate `Request#request_header`. Use `Request#headline` instead. ([@ixti]) ## 0.8.4 (2015-04-23) * Deprecate `#default_headers` and `#default_headers=`. ([@ixti]) * [#207](https://github.com/httprb/http/issues/207): Deprecate chainable methods with `with_` prefix. ([@ixti]) * [#186](https://github.com/httprb/http/pull/186): Add support of HTTPS connections through proxy. ([@Connorhd]) ## 0.8.3 (2015-04-07) * [#206](https://github.com/httprb/http/issues/206): Fix request headline. ([@ixti]) * Remove deprecated `Request#__method__`. ([@ixti]) ## 0.8.2 (2015-04-06) * [#203](https://github.com/httprb/http/issues/203): Fix Celluloid::IO compatibility. ([@ixti]) * Cleanup obsolete code. ([@zanker]) ## 0.8.1 (2015-04-02) * [#202](https://github.com/httprb/http/issues/202): Add missing `require "resolv"`. ([@ixti]) * [#200](https://github.com/httprb/http/issues/200), [#201](https://github.com/httprb/http/pull/201): Add block-form `#persistent` calls. ([@ixti]) ## 0.8.0 (2015-04-01) * [#199](https://github.com/httprb/http/pull/199): Properly handle WaitWritable for SSL. ([@zanker]) * [#197](https://github.com/httprb/http/pull/197): Add support for non-ASCII URis. ([@ixti]) * [#187](https://github.com/httprb/http/pull/187), [#194](https://github.com/httprb/http/pull/194), [#195](https://github.com/httprb/http/pull/195): Add configurable connection timeouts. ([@zanker]) * [#179](https://github.com/httprb/http/pull/179): Refactor requests redirect following logic. ([@ixti]) * Support for persistent HTTP connections ([@zanker]) * [#77](https://github.com/httprb/http/issues/77), [#177](https://github.com/httprb/http/pull/177): Add caching support. ([@Asmod4n], [@pezra]) * [#176](https://github.com/httprb/http/pull/176): Improve servers used in specs boot up. Issue was initially raised up by [@olegkovalenko]. ([@ixti]) * Reflect FormData rename changes (FormData -> HTTP::FormData). ([@ixti]) * [#173](https://github.com/httprb/http/pull/173): `HTTP::Headers` now raises `HTTP::InvalidHeaderNameError` in case of (surprise) invalid HTTP header field name (e.g.`"Foo:Bar"`). ([@ixti]) ## 0.7.3 (2015-03-24) * SECURITY FIX: http.rb failed to call the `#post_connection_check` method on SSL connections. This method implements hostname verification, and without it `http.rb` was vulnerable to MitM attacks. The problem was corrected by calling `#post_connection_check` (CVE-2015-1828) ([@zanker]) ## 0.7.2 (2015-03-02) * Swap from `form_data` to `http-form_data` (changed gem name). ## 0.7.1 (2015-01-03) * Gemspec fixups * Remove superfluous space in HTTP::Response inspection ## 0.7.0 (2015-01-02) * [#73](https://github.com/httprb/http/issues/73), [#167](https://github.com/httprb/http/pull/167): Add support of multipart form data. ([@ixti]) * Fix URI path normalization: `https://github.com` -> `https://github.com/`. ([@ixti]) * [#163](https://github.com/httprb/http/pull/163), [#166](https://github.com/httprb/http/pull/166), [#152](https://github.com/httprb/http/issues/152): Fix handling of EOF which caused infinite loop. ([@mickm], [@ixti]) * Drop Ruby 1.8.7 support. ([@ixti]) * [#150](https://github.com/httprb/http/issues/150): Fix default Host header value. ([@ixti]) * Remove BearerToken authorization header. ([@ixti]) * `#auth` sugar now accepts only string value of Authorization header. Calling `#auth(:basic, opts)` is deprecated, use `#basic_auth(opts)` instead. ([@ixti]) * Fix handling of chunked responses without Content-Length header. ([@ixti]) * Remove `HTTP::Request#method` and deprecate `HTTP::Request#__method__` ([@sferik]) * Deprecate `HTTP::Response::STATUS_CODES`, use `HTTP::Response::Status::REASONS` instead ([@ixti]) * Deprecate `HTTP::Response::SYMBOL_TO_STATUS_CODE` ([@ixti]) * Deprecate `HTTP::Response#status_code` ([@ixti]) * `HTTP::Response#status` now returns `HTTP::Response::Status`. ([@ixti]) * `HTTP::Response#reason` and `HTTP::Response#code` are proxies them to corresponding methods of `HTTP::Response#status` ([@ixti]) * Rename `HTTP.with_follow` to `HTTP.follow` and mark former one as being deprecated ([@ixti]) * Delegate `HTTP::Response#readpartial` to `HTTP::Response::Body` ([@ixti]) ## 0.6.4 (2015-03-25) * SECURITY FIX: http.rb failed to call the `#post_connection_check` method on SSL connections. This method implements hostname verification, and without it `http.rb` was vulnerable to MitM attacks. The problem was corrected by calling `#post_connection_check` (CVE-2015-1828) ([@zanker], backported by [@nicoolas25]) ## 0.6.3 (2014-11-14) * [#166](https://github.com/httprb/http/pull/166): Backported EOF fix from master branch. ([@ixti]) ## 0.6.2 (2014-08-06) * [#150](https://github.com/httprb/http/issues/150): Fix default Host header value. ([@ixti]) * Deprecate BearerToken authorization header. ([@ixti]) * Fix handling of chunked responses without Content-Length header. ([@ixti]) * Rename `HTTP.with_follow` to `HTTP.follow` and mark former one as being deprecated ([@ixti]) ## 0.6.1 (2014-05-07) * Fix request `Content-Length` calculation for Unicode ([@challengee]) * Add `Response#flush` ([@ixti]) * Fix `Response::Body#readpartial` default size ([@hannesg], [@ixti]) * Add missing `CRLF` for chunked bodies ([@hannesg]) * Fix forgotten CGI require ([@ixti]) * Improve README ([@tarcieri]) ## 0.6.0 (2014-04-04) * Rename `HTTP::Request#method` to `HTTP::Request#verb` ([@krainboltgreene]) * Add `HTTP::ResponseBody` class ([@tarcieri]) * Change API of response on `HTTP::Client.request` and "friends" (`#get`, `#post`, etc) ([@tarcieri]) * Add `HTTP::Response#readpartial` ([@tarcieri]) * Add `HTTP::Headers` class ([@ixti]) * Fix and improve following redirects ([@ixti]) * Add `HTTP::Request#redirect` ([@ixti]) * Add `HTTP::Response#content_type` ([@ixti]) * Add `HTTP::Response#mime_type` ([@ixti]) * Add `HTTP::Response#charset` ([@ixti]) * Improve error message upon invalid URI scheme ([@ixti]) * Consolidate errors under common `HTTP::Error` namespace ([@ixti]) * Add easy way of adding Authorization header ([@ixti]) * Fix proxy support ([@hundredwatt]) * Fix and improve query params handing ([@jwinter]) * Change API of custom MIME type parsers ([@ixti]) * Remove `HTTP::Chainable#with_response` ([@ixti]) * Remove `HTTP::Response::BodyDelegator` ([@ixti]) * Remove `HTTP::Response#parsed_body` ([@ixti]) * Bump up input buffer from 4K to 16K ([@tarcieri]) ``` ruby # Main API change you will mention is that `request` method and it's # syntax sugar helpers like `get`, `post`, etc. now returns Response # object instead of BodyDelegator: response = HTTP.get "http://example.com" raw_body = HTTP.get("http://example.com").to_s parsed_body = HTTP.get("http://example.com/users.json").parse # Second major change in API is work with request/response headers # It is now delegated to `HTTP::Headers` class, so you can check it's # documentation for details, here we will only outline main difference. # Duckface (`=`) does not appends headers anymore request[:content_type] = "text/plain" request[:content_type] = "text/html" request[:content_type] # => "text/html" # In order to add multiple header values, you should pass array: request[:cookie] = ["foo=bar", "woo=hoo"] request[:cookie] # => ["foo=bar", "woo=hoo"] # or call `#add` on headers: request.headers.add :accept, "text/plain" request.headers.add :accept, "text/html" request[:accept] # => ["text/plain", "text/html"] # Also, you can now read body in chunks (stream): res = HTTP.get "http://example.com" File.open "/tmp/dummy.bin", "wb" do |io| while (chunk = res.readpartial) io << chunk end end ``` [Changes discussion](https://github.com/httprb/http.rb/issues/116) ## 0.5.1 (2014-05-27) * Backports redirector fixes from 0.6.0 ([@ixti]) * EOL of 0.5.X branch. ## 0.5.0 (2013-09-14) * Add query string support * New response delegator allows HTTP.get(uri).response * HTTP::Chainable#stream provides a shorter alias for with_response(:object) * Better string inspect for HTTP::Response * Curb compatibility layer removed ## 0.4.0 (2012-10-12) * Fix bug accessing https URLs * Fix several instances of broken redirect handling * Add default user agent * Many additional minor bugfixes ## 0.3.0 (2012-09-01) * New implementation based on tmm1's http_parser.rb instead of Net::HTTP * Support for following redirects * Support for request body through {:body => ...} option * HTTP#with_response (through Chainable) ## 0.2.0 (2012-03-05) * Request and response objects * Callback system * Internal refactoring ensuring true chainability * Use the certified gem to ensure SSL certificate verification ## 0.1.0 (2012-01-26) * Testing against WEBrick * Curb compatibility (require 'http/compat/curb') ## 0.0.1 (2011-10-11) * Initial half-baked release ## 0.0.0 (2011-10-06) * Vapoware release to claim the "http" gem name >:D [@tarcieri]: https://github.com/tarcieri [@zanker]: https://github.com/zanker [@ixti]: https://github.com/ixti [@Connorhd]: https://github.com/Connorhd [@Asmod4n]: https://github.com/Asmod4n [@pezra]: https://github.com/pezra [@olegkovalenko]: https://github.com/olegkovalenko [@mickm]: https://github.com/mickm [@sferik]: https://github.com/sferik [@nicoolas25]: https://github.com/nicoolas25 [@challengee]: https://github.com/challengee [@hannesg]: https://github.com/hannesg [@krainboltgreene]: https://github.com/krainboltgreene [@hundredwatt]: https://github.com/hundredwatt [@jwinter]: https://github.com/jwinter [@nerdrew]: https://github.com/nerdrew [@kylekyle]: https://github.com/kylekyle [@smudge]: https://github.com/smudge [@mwitek]: https://github.com/mwitek [@tonyta]: https://github.com/tonyta [@jhbabon]: https://github.com/jhbabon [@britishtea]: https://github.com/britishtea [@janko-m]: https://github.com/janko-m [@Bonias]: https://github.com/Bonias [@HoneyryderChuck]: https://github.com/HoneyryderChuck [@marshall-lee]: https://github.com/marshall-lee [@scarfacedeb]: https://github.com/scarfacedeb [@mikegee]: https://github.com/mikegee [@tycoon]: https://github.com/tycooon [@paul]: https://github.com/paul [@RickCSong]: https://github.com/RickCSong [@fxposter]: https://github.com/fxposter [@mamoonraja]: https://github.com/mamoonraja http-4.4.1/.coveralls.yml0000644000004100000410000000003113640204124015310 0ustar www-datawww-dataservice-name: travis-pro http-4.4.1/LICENSE.txt0000644000004100000410000000213413640204124014346 0ustar www-datawww-dataCopyright (c) 2011-2016 Tony Arcieri, Erik Michaels-Ober, Alexey V. Zapparov, Zachary Anker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.