http-1.0.2/0000755000004100000410000000000012663604777012543 5ustar www-datawww-datahttp-1.0.2/Rakefile0000644000004100000410000000332212663604777014210 0ustar www-datawww-data#!/usr/bin/env rake require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new task :test => :spec begin require "rubocop/rake_task" RuboCop::RakeTask.new rescue LoadError task :rubocop do $stderr.puts "RuboCop is disabled" end end 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 "Unassigned" == desc || "(Unused)" == 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 task :default => [:spec, :rubocop, :verify_measurements] http-1.0.2/Gemfile0000644000004100000410000000122312663604777014034 0ustar www-datawww-datasource "https://rubygems.org" gem "rake" gem "rack-cache", "~> 1.2" group :development do gem "celluloid-io" gem "guard" gem "guard-rspec", :require => false gem "nokogiri", :require => false gem "pry" platforms :ruby_19, :ruby_20 do gem "pry-debugger" gem "pry-stack_explorer" end end group :test do gem "backports" gem "coveralls" gem "simplecov", ">= 0.9" gem "json", ">= 1.8.1" gem "rubocop", "= 0.35.1" gem "rspec", "~> 3.0" gem "rspec-its" gem "yardstick" gem "certificate_authority" end group :doc do gem "kramdown" gem "yard" end # Specify your gem's dependencies in http.gemspec gemspec http-1.0.2/.coveralls.yml0000644000004100000410000000003112663604777015330 0ustar www-datawww-dataservice-name: travis-pro http-1.0.2/.rspec0000644000004100000410000000012012663604777013651 0ustar www-datawww-data--backtrace --color --format=documentation --order random --require spec_helper http-1.0.2/logo.png0000644000004100000410000001735312663604777014222 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-1.0.2/LICENSE.txt0000644000004100000410000000213412663604777014366 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. http-1.0.2/spec/0000755000004100000410000000000012663604777013475 5ustar www-datawww-datahttp-1.0.2/spec/regression_specs.rb0000644000004100000410000000066012663604777017401 0ustar www-datawww-datarequire "spec_helper" RSpec.describe "Regression testing" do describe "#248" do it "does not fails with github" do github_uri = "http://github.com/" expect { HTTP.get(github_uri).to_s }.not_to raise_error end it "does not failes ith 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 end http-1.0.2/spec/spec_helper.rb0000644000004100000410000000636112663604777016321 0ustar www-datawww-data# coding: utf-8 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" # Are we in a flaky environment? def flaky_env? defined?(JRUBY_VERSION) && ENV["CI"] end # 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.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-1.0.2/spec/lib/0000755000004100000410000000000012663604777014243 5ustar www-datawww-datahttp-1.0.2/spec/lib/http_spec.rb0000644000004100000410000003030112663604777016556 0ustar www-datawww-data# 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 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 %w(global null per_operation).each do |timeout| context "with a #{timeout} timeout" do [16_000, 16_500, 17_000, 34_000, 68_000].each do |size| [0, rand(0..100), rand(100..1000)].each do |fuzzer| context "with a #{size} body and #{fuzzer} of fuzzing" do let(:client) { HTTP.timeout(timeout, :read => 2, :write => 2, :connect => 2) } let(:characters) { ("A".."Z").to_a } let(:request_body) do (size + fuzzer).times.map { |i| characters[i % characters.length] }.join end it "returns a large body" do response = client.post("#{dummy.endpoint}/echo-body", :body => request_body) expect(response.body.to_s).to eq(request_body) expect(response.headers["Content-Length"].to_i).to eq(request_body.length) end end end 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 end it "fails when :pass is not given" do expect { HTTP.basic_auth :user => "[USER]" }.to raise_error end it "fails when :user is not given" do expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error 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 end describe ".timeout" do context "without timeout type" 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 "with :null type" do subject(:client) { HTTP.timeout :null, :read => 123 } it "sets timeout_class to Null" do expect(client.default_options.timeout_class). to be HTTP::Timeout::Null end end context "with :per_operation type" do subject(:client) { HTTP.timeout :per_operation, :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 "with :global type" do subject(:client) { HTTP.timeout :global, :read => 123 } it "sets timeout_class to Global" do expect(client.default_options.timeout_class). to be HTTP::Timeout::Global end it "sets given timeout options" do expect(client.default_options.timeout_options). to eq :read_timeout => 123 end end it "fails with unknown timeout type" do expect { HTTP.timeout(:foobar, :read => 123) }. to raise_error(ArgumentError, /foobar/) 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 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:000" }.to raise_error HTTP::ConnectionError 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 end http-1.0.2/spec/lib/http/0000755000004100000410000000000012663604777015222 5ustar www-datawww-datahttp-1.0.2/spec/lib/http/options/0000755000004100000410000000000012663604777016715 5ustar www-datawww-datahttp-1.0.2/spec/lib/http/options/proxy_spec.rb0000644000004100000410000000135112663604777021435 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/options/merge_spec.rb0000644000004100000410000000416012663604777021354 0ustar www-datawww-data 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 => {}) 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) end end http-1.0.2/spec/lib/http/options/body_spec.rb0000644000004100000410000000044712663604777021216 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/options/new_spec.rb0000644000004100000410000000154712663604777021054 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/options/form_spec.rb0000644000004100000410000000046612663604777021225 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/options/json_spec.rb0000644000004100000410000000046612663604777021233 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/options/headers_spec.rb0000644000004100000410000000101612663604777021665 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/content_type_spec.rb0000644000004100000410000000321212663604777021272 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/response/0000755000004100000410000000000012663604777017060 5ustar www-datawww-datahttp-1.0.2/spec/lib/http/response/body_spec.rb0000644000004100000410000000204212663604777021352 0ustar www-datawww-dataRSpec.describe HTTP::Response::Body do let(:client) { double(:sequence_id => 0) } let(:chunks) { ["Hello, ", "World!"] } before { allow(client).to receive(:readpartial) { chunks.shift } } subject(:body) { described_class.new client, 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) { [""] } 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 client" do expect(client).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 client readpartial without specific size" do expect(client).to receive(:readpartial).with no_args body.readpartial end end end end http-1.0.2/spec/lib/http/response/status_spec.rb0000644000004100000410000000667012663604777021753 0ustar www-datawww-dataRSpec.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 Fixnum } 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 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-1.0.2/spec/lib/http/options_spec.rb0000644000004100000410000000040412663604777020252 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/headers/0000755000004100000410000000000012663604777016635 5ustar www-datawww-datahttp-1.0.2/spec/lib/http/headers/mixin_spec.rb0000644000004100000410000000132612663604777021322 0ustar www-datawww-dataRSpec.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-1.0.2/spec/lib/http/client_spec.rb0000644000004100000410000002143212663604777020041 0ustar www-datawww-data# 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 url = "http://git.io/jNeY" client = HTTP.follow expect(client.get(url).to_s).to include "support for non-ascii URIs" 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 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://github.com/httprb/http.rb/pull/197/ö無" client = HTTP.follow expect(client.get(url).to_s).to include "support for non-ascii URIs" 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 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[0] } 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 end end http-1.0.2/spec/lib/http/response_spec.rb0000644000004100000410000000733412663604777020426 0ustar www-datawww-dataRSpec.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 "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 let(:headers) { {:content_type => "text/plain"} } let(:body) { double :to_s => "foobar" } it "returns human-friendly response representation" do expect(response.inspect). to eq '#"text/plain"}>' end 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 end http-1.0.2/spec/lib/http/redirector_spec.rb0000644000004100000410000003216312663604777020730 0ustar www-datawww-dataRSpec.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 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-1.0.2/spec/lib/http/request/0000755000004100000410000000000012663604777016712 5ustar www-datawww-datahttp-1.0.2/spec/lib/http/request/writer_spec.rb0000644000004100000410000000503112663604777021564 0ustar www-datawww-data# coding: utf-8 RSpec.describe HTTP::Request::Writer do let(:io) { StringIO.new } let(:body) { "" } let(:headers) { HTTP::Headers.new } let(:headerstart) { "GET /test HTTP/1.1" } subject(:writer) { described_class.new(io, body, headers, headerstart) } describe "#initalize" do context "when body is nil" do let(:body) { nil } it "does not raise an error" do expect { writer }.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 { writer }.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 { writer }.not_to raise_error end end context "when body is not string, enumerable or nil" do let(:body) { 123 } it "raises an error" do expect { writer }.to raise_error(HTTP::RequestError) end end end describe "#stream" do context "when body is Enumerable" do let(:body) { %w(bees cows) } let(:headers) { HTTP::Headers.coerce "Transfer-Encoding" => "chunked" } it "writes a chunked request from an Enumerable correctly" do writer.stream expect(io.string).to end_with "\r\n4\r\nbees\r\n4\r\ncows\r\n0\r\n\r\n" end it "writes Transfer-Encoding header only once" do writer.stream expect(io.string).to start_with "#{headerstart}\r\nTransfer-Encoding: chunked\r\n\r\n" end context "when Transfer-Encoding not set" do let(:headers) { HTTP::Headers.new } specify { expect { writer.stream }.to raise_error(HTTP::RequestError) } end context "when Transfer-Encoding is not chunked" do let(:headers) { HTTP::Headers.coerce "Transfer-Encoding" => "gzip" } specify { expect { writer.stream }.to raise_error(HTTP::RequestError) } end end context "when body is a unicode String" do let(:body) { "Привет, мир!" } it "properly calculates Content-Length if needed" do writer.stream expect(io.string).to start_with "#{headerstart}\r\nContent-Length: 21\r\n\r\n" end context "when Content-Length explicitly set" do let(:headers) { HTTP::Headers.coerce "Content-Length" => 12 } it "keeps given value" do writer.stream expect(io.string).to start_with "#{headerstart}\r\nContent-Length: 12\r\n\r\n" end end end end end http-1.0.2/spec/lib/http/request_spec.rb0000644000004100000410000001362312663604777020256 0ustar www-datawww-data# 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 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" } end end end http-1.0.2/spec/lib/http/headers_spec.rb0000644000004100000410000003047312663604777020203 0ustar www-datawww-dataRSpec.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::InvalidHeaderNameError end it "fails with invalid header name" do expect { headers.set "foo bar", "baz" }. to raise_error HTTP::InvalidHeaderNameError 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::InvalidHeaderNameError end it "fails with invalid header name" do expect { headers.delete "foo bar" }. to raise_error HTTP::InvalidHeaderNameError 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::InvalidHeaderNameError end it "fails with invalid header name" do expect { headers.add "foo bar", "baz" }. to raise_error HTTP::InvalidHeaderNameError 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::InvalidHeaderNameError end it "fails with invalid header name" do expect { headers.get "foo bar" }. to raise_error HTTP::InvalidHeaderNameError 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 "#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-1.0.2/spec/support/0000755000004100000410000000000012663604777015211 5ustar www-datawww-datahttp-1.0.2/spec/support/servers/0000755000004100000410000000000012663604777016702 5ustar www-datawww-datahttp-1.0.2/spec/support/servers/config.rb0000644000004100000410000000014612663604777020475 0ustar www-datawww-datamodule ServerConfig def addr config[:BindAddress] end def port config[:Port] end end http-1.0.2/spec/support/servers/runner.rb0000644000004100000410000000040012663604777020532 0ustar www-datawww-datamodule ServerRunner def run_server(name, &block) let! name do server = block.call Thread.new { server.start } server end after do send(name).shutdown end end end RSpec.configure { |c| c.extend ServerRunner } http-1.0.2/spec/support/dummy_server.rb0000644000004100000410000000147212663604777020263 0ustar www-datawww-datarequire "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 = {}) 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-1.0.2/spec/support/capture_warning.rb0000644000004100000410000000017712663604777020733 0ustar www-datawww-datadef capture_warning old_stderr = $stderr $stderr = StringIO.new yield $stderr.string ensure $stderr = old_stderr end http-1.0.2/spec/support/ssl_helper.rb0000644000004100000410000000417612663604777017706 0ustar www-datawww-datarequire "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)}} 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-1.0.2/spec/support/http_handling_shared.rb0000644000004100000410000001351012663604777021707 0ustar www-datawww-dataRSpec.shared_context "HTTP handling" do describe "timeouts" do let(:conn_timeout) { 1 } let(:read_timeout) { 1 } let(:write_timeout) { 1 } let(:options) do { :timeout_class => timeout_class, :timeout_options => { :connect_timeout => conn_timeout, :read_timeout => read_timeout, :write_timeout => write_timeout } } end context "without timeouts" do let(:timeout_class) { HTTP::Timeout::Null } let(:conn_timeout) { 0 } let(:read_timeout) { 0 } let(:write_timeout) { 0 } it "works" do expect(client.get(server.endpoint).body.to_s).to eq("") end end context "with a per operation timeout" do let(:timeout_class) { HTTP::Timeout::PerOperation } let(:response) { client.get(server.endpoint).body.to_s } 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" do skip "flaky environment" if flaky_env? 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" do # TODO: investigate sporadic JRuby timeouts on CI skip "flaky environment" if flaky_env? expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error end end end end context "with a global timeout" do let(:timeout_class) { HTTP::Timeout::Global } let(:conn_timeout) { 0 } let(:read_timeout) { 1 } let(:write_timeout) { 0 } 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(:read_timeout) { 2.5 } it "does not timeout" do # TODO: investigate sporadic JRuby timeouts on CI skip "flaky environment" if flaky_env? client.get("#{server.endpoint}/sleep").body.to_s client.get("#{server.endpoint}/sleep").body.to_s end 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" do skip "flaky environment" if flaky_env? 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" do skip "flaky environment" if flaky_env? 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" do skip "flaky environment" if flaky_env? expect(sockets_used).to_not include("") expect(sockets_used.uniq.length).to eq(2) end end end end http-1.0.2/spec/support/black_hole.rb0000644000004100000410000000010112663604777017611 0ustar www-datawww-datamodule BlackHole def self.method_missing(*) self end end http-1.0.2/spec/support/proxy_server.rb0000644000004100000410000000147012663604777020307 0ustar www-datawww-datarequire "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-1.0.2/spec/support/dummy_server/0000755000004100000410000000000012663604777017732 5ustar www-datawww-datahttp-1.0.2/spec/support/dummy_server/servlet.rb0000644000004100000410000000677512663604777021762 0ustar www-datawww-data# encoding: UTF-8 class DummyServer < WEBrick::HTTPServer 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 end end http-1.0.2/.travis.yml0000644000004100000410000000050712663604777014656 0ustar www-datawww-databundler_args: --without development doc env: global: - JRUBY_OPTS="$JRUBY_OPTS --debug" language: ruby rvm: - 1.9.3 - 2.0.0 - 2.1 - 2.2 - 2.3.0 - jruby - jruby-head - ruby-head - rbx-2 matrix: allow_failures: - rvm: jruby-head - rvm: ruby-head - rvm: rbx-2 fast_finish: true sudo: false http-1.0.2/lib/0000755000004100000410000000000012663604777013311 5ustar www-datawww-datahttp-1.0.2/lib/http.rb0000644000004100000410000000102112663604777014607 0ustar www-datawww-datarequire "http/parser" 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/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_method :[], :headers end end http-1.0.2/lib/http/0000755000004100000410000000000012663604777014270 5ustar www-datawww-datahttp-1.0.2/lib/http/timeout/0000755000004100000410000000000012663604777015756 5ustar www-datawww-datahttp-1.0.2/lib/http/timeout/global.rb0000644000004100000410000000706512663604777017553 0ustar www-datawww-datarequire "timeout" require "http/timeout/per_operation" module HTTP module Timeout class Global < PerOperation attr_reader :time_left, :total_timeout def initialize(*args) super reset_counter end # To future me: Don't remove this again, past you was smarter. def reset_counter @time_left = connect_timeout + read_timeout + write_timeout @total_timeout = time_left 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) perform_io { read_nonblock(size) } end # Write to the socket def write(data) perform_io { write_nonblock(data) } end alias_method :<<, :write private if RUBY_VERSION < "2.1.0" def read_nonblock(size) @socket.read_nonblock(size) end def write_nonblock(data) @socket.write_nonblock(data) end else def read_nonblock(size) @socket.read_nonblock(size, :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 if RUBY_VERSION < "2.0.0" # Wait for a socket to become readable def wait_readable_or_timeout IO.select([@socket], nil, nil, time_left) log_time end # Wait for a socket to become writable def wait_writable_or_timeout IO.select(nil, [@socket], nil, time_left) log_time end else require "io/wait" # 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 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 fail TimeoutError, "Timed out after using the allocated #{total_timeout} seconds" end reset_timer end end end end http-1.0.2/lib/http/timeout/per_operation.rb0000644000004100000410000000530112663604777021150 0ustar www-datawww-datarequire "timeout" require "http/timeout/null" module HTTP module Timeout class PerOperation < Null CONNECT_TIMEOUT = 0.25 WRITE_TIMEOUT = 0.25 READ_TIMEOUT = 0.25 attr_reader :read_timeout, :write_timeout, :connect_timeout 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 do rescue_writable do socket.connect_nonblock end end end # NIO with exceptions if RUBY_VERSION < "2.1.0" # Read data from the socket def readpartial(size) rescue_readable do socket.read_nonblock(size) 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) loop do # JRuby may still raise exceptions on SSL sockets even though # we explicitly specify `:exception => false` result = rescue_readable do socket.read_nonblock(size, :exception => false) end if result.nil? return :eof elsif result != :wait_readable return result end unless socket.to_io.wait_readable(read_timeout) fail TimeoutError, "Read timed out after #{read_timeout} seconds" end end end # Write data to the socket def write(data) loop do # JRuby may still raise exceptions on SSL sockets even though # we explicitly specify `:exception => false` result = rescue_writable do socket.write_nonblock(data, :exception => false) end return result unless result == :wait_writable unless socket.to_io.wait_writable(write_timeout) fail TimeoutError, "Read timed out after #{write_timeout} seconds" end end end end # rubocop:enable Metrics/BlockNesting end end end http-1.0.2/lib/http/timeout/null.rb0000644000004100000410000000501512663604777017256 0ustar www-datawww-datarequire "forwardable" module HTTP module Timeout class Null extend Forwardable def_delegators :@socket, :close, :closed? attr_reader :options, :socket def initialize(options = {}) @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) @socket.readpartial(size) rescue EOFError :eof end # Write to the socket def write(data) @socket.write(data) end alias_method :<<, :write # These cops can be re-eanbled after we go Ruby 2.0+ only # rubocop:disable Lint/UselessAccessModifier, Metrics/BlockNesting private if RUBY_VERSION < "2.0.0" # Retry reading def rescue_readable yield rescue IO::WaitReadable retry if IO.select([socket], nil, nil, read_timeout) raise TimeoutError, "Read timed out after #{read_timeout} seconds" end # Retry writing def rescue_writable yield rescue IO::WaitWritable retry if IO.select(nil, [socket], nil, write_timeout) raise TimeoutError, "Write timed out after #{write_timeout} seconds" end else require "io/wait" # Retry reading def rescue_readable yield rescue IO::WaitReadable retry if socket.to_io.wait_readable(read_timeout) raise TimeoutError, "Read timed out after #{read_timeout} seconds" end # Retry writing def rescue_writable yield rescue IO::WaitWritable retry if socket.to_io.wait_writable(write_timeout) raise TimeoutError, "Write timed out after #{write_timeout} seconds" end end end end end http-1.0.2/lib/http/headers.rb0000644000004100000410000001204012663604777016225 0ustar www-datawww-datarequire "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_HEADER = /^[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 HEADER_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_method :[]=, :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 # Returns Rack-compatible headers Hash # # @return [Hash] def to_h Hash[keys.map { |k| [k, self[k]] }] end alias_method :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 fail Error, "Can't coerce #{object.inspect} to Headers" end end headers = new object.each { |k, v| headers.add k, v } headers end alias_method :[], :coerce end private # Transforms `name` to canonical HTTP header capitalization # # @param [String] name # @raise [InvalidHeaderNameError] if normalized name does not # match {HEADER_NAME_RE} # @return [String] canonical HTTP header name def normalize_header(name) normalized = name[CANONICAL_HEADER] normalized ||= name.split(/[\-_]/).map(&:capitalize).join("-") return normalized if normalized =~ HEADER_NAME_RE fail InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}" end end end http-1.0.2/lib/http/redirector.rb0000644000004100000410000000540212663604777016760 0ustar www-datawww-datarequire "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 = [:put, :delete, :post].to_set.freeze # Verbs which will remain unchanged upon See Other response. SEE_OTHER_ALLOWED_VERBS = [: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 = {}) @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}" fail TooManyRedirectsError if too_many_hops? fail EndlessRedirectError if endless_loop? @response.flush @request = redirect_to @response.headers[Headers::LOCATION] @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) fail 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) fail 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-1.0.2/lib/http/connection.rb0000644000004100000410000001301312663604777016752 0ustar www-datawww-datarequire "forwardable" require "http/client" require "http/headers" require "http/response/parser" module HTTP # A connection to the HTTP server class Connection extend Forwardable # Attempt to read this much data BUFFER_SIZE = 16_384 # HTTP/1.0 HTTP_1_0 = "1.0".freeze # HTTP/1.1 HTTP_1_1 = "1.1".freeze # @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 @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 SocketError, SystemCallError => e raise ConnectionError, "failed to connect: #{e}" 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) if @pending_response fail StateError, "Tried to send a request while one is pending already. Make sure you read off the body." elsif @pending_request fail StateError, "Tried to send a request while a response is pending. Make sure you've fully read the body from the request." end @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 if read_more(size) == :eof finished = true else finished = @parser.finished? end chunk = @parser.chunk finish_response if finished chunk.to_s end # Reads data from socket up until headers are loaded # @return [void] def read_headers! loop do if read_more(BUFFER_SIZE) == :eof fail EOFError unless @parser.headers? break else break if @parser.headers? end end set_keep_alive rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e raise ConnectionError, "failed to read headers: #{e}" 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! if @parser.status_code == 200 @parser.reset @pending_response = false return end @failed_proxy_connect = true 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 case @parser.http_version when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive @keep_alive = @parser.headers[Headers::CONNECTION] == Client::KEEP_ALIVE when HTTP_1_1 # HTTP/1.1 is opt-out @keep_alive = @parser.headers[Headers::CONNECTION] != Client::CLOSE else # Anything else we assume doesn't supportit @keep_alive = false end end # Feeds some more data into parser # @return [void] def read_more(size) return if @parser.finished? value = @socket.readpartial(size) if value == :eof :eof elsif value @parser << value end end end end http-1.0.2/lib/http/errors.rb0000644000004100000410000000075612663604777016141 0ustar www-datawww-datamodule 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 name is invalid class InvalidHeaderNameError < Error; end end http-1.0.2/lib/http/response.rb0000644000004100000410000000606412663604777016461 0ustar www-datawww-datarequire "forwardable" require "http/headers" require "http/content_type" require "http/mime_type" require "http/response/status" 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 [Body] attr_reader :body # @return [URI, nil] attr_reader :uri # Inits a new instance # # @option opts [Integer] :status Status code # @option opts [String] :version HTTP version # @option opts [Hash] :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] || {}) if opts.include?(:connection) connection = opts.fetch(:connection) encoding = opts[:encoding] || charset || Encoding::BINARY @body = Response::Body.new(connection, encoding) else @body = opts.fetch(:body) 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_method :to_str, :to_s # @!method readpartial # (see HTTP::Response::Body#readpartial) def_delegator :body, :readpartial # 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 # 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 # 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 [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-1.0.2/lib/http/uri.rb0000644000004100000410000000106612663604777015417 0ustar www-datawww-datarequire "addressable/uri" module HTTP class URI < Addressable::URI # @private HTTP_SCHEME = "http".freeze # @private HTTPS_SCHEME = "https".freeze # @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 [String] human-readable representation of URI def inspect format("#<%s:%#0x URI:%s>", self.class, object_id, to_s) end end end http-1.0.2/lib/http/mime_type.rb0000644000004100000410000000323712663604777016612 0ustar www-datawww-datamodule 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] || fail(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-1.0.2/lib/http/response/0000755000004100000410000000000012663604777016126 5ustar www-datawww-datahttp-1.0.2/lib/http/response/status.rb0000644000004100000410000000632412663604777020003 0ustar www-datawww-datarequire "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 fail Error, "Can't coerce #{object.class}(#{object}) to #{self}" end alias_method :[], :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 # 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) fail 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-1.0.2/lib/http/response/parser.rb0000644000004100000410000000200212663604777017741 0ustar www-datawww-datamodule HTTP class Response class Parser attr_reader :headers def initialize @parser = HTTP::Parser.new(self) reset end def add(data) @parser << data end alias_method :<<, :add def headers? !!@headers end def http_version @parser.http_version.join(".") end def status_code @parser.status_code end # # HTTP::Parser callbacks # def on_headers_complete(headers) @headers = headers end def on_body(chunk) if @chunk @chunk << chunk else @chunk = chunk end end def chunk chunk = @chunk @chunk = nil chunk end def on_message_complete @finished = true end def reset @parser.reset! @finished = false @headers = nil @chunk = nil end def finished? @finished end end end end http-1.0.2/lib/http/response/status/0000755000004100000410000000000012663604777017451 5ustar www-datawww-datahttp-1.0.2/lib/http/response/status/reasons.rb0000644000004100000410000000453512663604777021457 0ustar www-datawww-data# 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", 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-1.0.2/lib/http/response/body.rb0000644000004100000410000000310612663604777017410 0ustar www-datawww-datarequire "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? def initialize(client, encoding = Encoding::BINARY) @client = client @streaming = nil @contents = nil @encoding = encoding end # (see HTTP::Client#readpartial) def readpartial(*args) stream! @client.readpartial(*args) 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 fail StateError, "body is being streamed" unless @streaming.nil? begin @streaming = false @contents = "".force_encoding(@encoding) while (chunk = @client.readpartial) @contents << chunk.force_encoding(@encoding) end rescue @contents = nil raise end @contents end alias_method :to_str, :to_s # Assert that the body is actively being streamed def stream! fail 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 end end end http-1.0.2/lib/http/options.rb0000644000004100000410000000667312663604777016324 0ustar www-datawww-datarequire "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 class << self attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class def new(options = {}) return options if options.is_a?(self) super end def defined_options @defined_options ||= [] end protected def def_option(name, &interpreter) defined_options << name.to_sym interpreter ||= lambda { |v| v } attr_accessor name protected :"#{name}=" define_method(:"with_#{name}") do |value| dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) } end end end def initialize(options = {}) 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 } 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 |headers| self.headers.merge(headers) end def_option :cookies do |cookies| cookies.each_with_object self.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 %w( proxy params form json body follow response socket_class nodelay ssl_socket_class ssl_context ssl persistent keep_alive_timeout timeout_class timeout_options ).each do |method_name| def_option method_name end 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 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 protected def []=(option, val) send(:"#{option}=", val) end private def argument_error!(message) fail(Error, message, caller[1..-1]) end end end http-1.0.2/lib/http/mime_type/0000755000004100000410000000000012663604777016260 5ustar www-datawww-datahttp-1.0.2/lib/http/mime_type/json.rb0000644000004100000410000000074012663604777017557 0ustar www-datawww-datarequire "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.load str end end register_adapter "application/json", JSON register_alias "application/json", :json end end http-1.0.2/lib/http/mime_type/adapter.rb0000644000004100000410000000100512663604777020221 0ustar www-datawww-datarequire "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-1.0.2/lib/http/content_type.rb0000644000004100000410000000112512663604777017327 0ustar www-datawww-datamodule 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) md = str.to_s.match MIME_TYPE_RE md && md[1].to_s.strip.downcase end # :nodoc: def charset(str) md = str.to_s.match CHARSET_RE md && md[1].to_s.strip.gsub(/^"|"$/, "") end end end end http-1.0.2/lib/http/client.rb0000644000004100000410000001116612663604777016100 0ustar www-datawww-datarequire "forwardable" require "http/form_data" require "http/options" 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 KEEP_ALIVE = "Keep-Alive".freeze CLOSE = "close".freeze 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 = {}) opts = @default_options.merge(opts) uri = make_request_uri(uri, opts) headers = make_request_headers(opts) body = make_request_body(opts, headers) proxy = opts.proxy req = HTTP::Request.new( :verb => verb, :uri => uri, :headers => headers, :proxy => proxy, :body => body ) res = perform(req, opts) return res unless opts.follow Redirector.new(opts.follow).perform(req, res) do |request| perform(request, opts) 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, :connection => @connection, :encoding => options.encoding, :uri => req.uri ) @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 fail 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 = [uri.query, HTTP::URI.form_encode(opts.params)].compact.join("&") 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 if default_options.persistent? headers[Headers::CONNECTION] = KEEP_ALIVE else headers[Headers::CONNECTION] = CLOSE end 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 = HTTP::FormData.create opts.form headers[Headers::CONTENT_TYPE] ||= form.content_type headers[Headers::CONTENT_LENGTH] ||= form.content_length form.to_s when opts.json headers[Headers::CONTENT_TYPE] ||= "application/json" MimeType[:json].encode opts.json end end end end http-1.0.2/lib/http/request.rb0000644000004100000410000001313212663604777016305 0ustar www-datawww-datarequire "forwardable" require "base64" require "time" require "http/errors" require "http/headers" 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}".freeze # RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 METHODS = [:options, :get, :head, :post, :put, :delete, :trace, :connect] # RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV METHODS.concat [:propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock] # RFC 3648: WebDAV Ordered Collections Protocol METHODS.concat [:orderpatch] # RFC 3744: WebDAV Access Control Protocol METHODS.concat [:acl] # draft-dusseault-http-patch: PATCH Method for HTTP METHODS.concat [:patch] # draft-reschke-webdav-search: WebDAV Search METHODS.concat [:search] # Allowed schemes SCHEMES = [:http, :https, :ws, :wss] # Default ports of supported schemes PORTS = { :http => 80, :https => 443, :ws => 80, :wss => 443 } # 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 # "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 [HTTP::URI, #to_s] :uri # @option opts [Hash] :headers # @option opts [Hash] :proxy # @option opts [String] :body def initialize(opts) @verb = opts.fetch(:verb).to_s.downcase.to_sym @uri = normalize_uri(opts.fetch :uri) @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme fail(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb) fail(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme) @proxy = opts[:proxy] || {} @body = opts[:body] @version = opts[:version] || "1.1" @headers = HTTP::Headers.coerce(opts[:headers] || {}) @headers[Headers::HOST] ||= default_host_header_value @headers[Headers::USER_AGENT] ||= USER_AGENT end # Returns new Request with updated uri def redirect(uri, verb = @verb) req = self.class.new( :verb => verb, :uri => @uri.join(uri), :headers => headers, :proxy => proxy, :body => body, :version => version ) req[Headers::HOST] = req.uri.host req end # Stream the request to a socket def stream(socket) include_proxy_authorization_header if using_authenticated_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 # 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 = using_proxy? ? uri : uri.omit(:scheme, :authority) "#{verb.to_s.upcase} #{request_uri.omit :fragment} 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 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 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 # @return [HTTP::URI] URI with all componentes but query being normalized. def normalize_uri(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 end end http-1.0.2/lib/http/version.rb0000644000004100000410000000005312663604777016300 0ustar www-datawww-datamodule HTTP VERSION = "1.0.2".freeze end http-1.0.2/lib/http/headers/0000755000004100000410000000000012663604777015703 5ustar www-datawww-datahttp-1.0.2/lib/http/headers/mixin.rb0000644000004100000410000000125412663604777017356 0ustar www-datawww-datarequire "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-1.0.2/lib/http/headers/known.rb0000644000004100000410000000534212663604777017370 0ustar www-datawww-datamodule HTTP class Headers # Content-Types that are acceptable for the response. ACCEPT = "Accept".freeze # The age the object has been in a proxy cache in seconds. AGE = "Age".freeze # Authentication credentials for HTTP authentication. AUTHORIZATION = "Authorization".freeze # Used to specify directives that must be obeyed by all caching mechanisms # along the request-response chain. CACHE_CONTROL = "Cache-Control".freeze # An HTTP cookie previously sent by the server with Set-Cookie. COOKIE = "Cookie".freeze # Control options for the current connection and list # of hop-by-hop request fields. CONNECTION = "Connection".freeze # The length of the request body in octets (8-bit bytes). CONTENT_LENGTH = "Content-Length".freeze # The MIME type of the body of the request # (used with POST and PUT requests). CONTENT_TYPE = "Content-Type".freeze # The date and time that the message was sent (in "HTTP-date" format as # defined by RFC 7231 Date/Time Formats). DATE = "Date".freeze # An identifier for a specific version of a resource, # often a message digest. ETAG = "ETag".freeze # Gives the date/time after which the response is considered stale (in # "HTTP-date" format as defined by RFC 7231). EXPIRES = "Expires".freeze # 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".freeze # Allows a 304 Not Modified to be returned if content is unchanged. IF_MODIFIED_SINCE = "If-Modified-Since".freeze # Allows a 304 Not Modified to be returned if content is unchanged. IF_NONE_MATCH = "If-None-Match".freeze # The last modified date for the requested object (in "HTTP-date" format as # defined by RFC 7231). LAST_MODIFIED = "Last-Modified".freeze # Used in redirection, or when a new resource has been created. LOCATION = "Location".freeze # Authorization credentials for connecting to a proxy. PROXY_AUTHORIZATION = "Proxy-Authorization".freeze # An HTTP cookie. SET_COOKIE = "Set-Cookie".freeze # 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".freeze # The user agent string of the user agent. USER_AGENT = "User-Agent".freeze # 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".freeze end end http-1.0.2/lib/http/chainable.rb0000644000004100000410000001436312663604777016532 0ustar www-datawww-datarequire "base64" require "http/headers" module HTTP module Chainable # Request a get sans response body # @param uri # @option options [Hash] def head(uri, options = {}) request :head, uri, options end # Get a resource # @param uri # @option options [Hash] def get(uri, options = {}) request :get, uri, options end # Post to a resource # @param uri # @option options [Hash] def post(uri, options = {}) request :post, uri, options end # Put to a resource # @param uri # @option options [Hash] def put(uri, options = {}) request :put, uri, options end # Delete a resource # @param uri # @option options [Hash] def delete(uri, options = {}) request :delete, uri, options end # Echo the request back to the client # @param uri # @option options [Hash] def trace(uri, options = {}) request :trace, uri, options end # Return the methods supported on the given URI # @param uri # @option options [Hash] def options(uri, options = {}) request :options, uri, options end # Convert to a transparent TCP/IP tunnel # @param uri # @option options [Hash] def connect(uri, options = {}) request :connect, uri, options end # Apply partial modifications to a resource # @param uri # @option options [Hash] def patch(uri, options = {}) request :patch, uri, options end # Make an HTTP request with the given verb # @param uri # @option options [Hash] def request(verb, uri, options = {}) branch(options).request verb, uri end # @overload(options = {}) # Syntax sugar for `timeout(:per_operation, options)` # @overload(klass, options = {}) # @param [#to_sym] klass # @param [Hash] options # @option options [Float] :read Read timeout # @option options [Float] :write Write timeout # @option options [Float] :connect Connect timeout def timeout(klass, options = {}) if klass.is_a? Hash options = klass klass = :per_operation end klass = case klass.to_sym when :null then HTTP::Timeout::Null when :global then HTTP::Timeout::Global when :per_operation then HTTP::Timeout::PerOperation else fail ArgumentError, "Unsupported Timeout class: #{klass}" end [: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) # Flags as persistent # @param [String] host # @raise [Request::Error] if Host is invalid # @return [HTTP::Client] Persistent client # @overload persistent(host, &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) p_client = branch default_options.with_persistent host return p_client unless block_given? yield p_client ensure p_client.close 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) if [2, 4].include?(proxy_hash.keys.size) branch default_options.with_proxy(proxy_hash) else fail(RequestError, "invalid HTTP proxy: #{proxy_hash}") end end alias_method :through, :via # Make client follow redirects. # @param opts # @return [HTTP::Client] # @see Redirector#initialize def follow(opts = {}) branch default_options.with_follow opts 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 private # :nodoc: def branch(options) HTTP::Client.new(options) end end end http-1.0.2/lib/http/request/0000755000004100000410000000000012663604777015760 5ustar www-datawww-datahttp-1.0.2/lib/http/request/writer.rb0000644000004100000410000000602112663604777017620 0ustar www-datawww-datarequire "http/headers" module HTTP class Request class Writer # CRLF is the universal HTTP delimiter CRLF = "\r\n".freeze # Chunked data termintaor. ZERO = "0".freeze # Chunked transfer encoding CHUNKED = "chunked".freeze # End of a chunked transfer CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}".freeze # Types valid to be used as body source VALID_BODY_TYPES = [String, NilClass, Enumerable] def initialize(socket, body, headers, headline) @body = body @socket = socket @headers = headers @request_header = [headline] validate_body_type! 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 if @body.is_a?(String) && !@headers[Headers::CONTENT_LENGTH] @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.bytesize}" elsif @body.is_a?(Enumerable) && CHUNKED != @headers[Headers::TRANSFER_ENCODING] fail(RequestError, "invalid transfer encoding") end 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 def send_request headers = join_headers # 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. case @body when NilClass write(headers) when String write(headers << @body) when Enumerable write(headers) @body.each do |chunk| write(chunk.bytesize.to_s(16) << CRLF << chunk << CRLF) end write(CHUNKED_END) else fail TypeError, "invalid body type: #{@body.class}" end end private def write(data) until data.empty? length = @socket.write(data) if data.length > length data = data[length..-1] else break end end end def validate_body_type! return if VALID_BODY_TYPES.any? { |type| @body.is_a? type } fail RequestError, "body of wrong type: #{@body.class}" end end end end http-1.0.2/.rubocop.yml0000644000004100000410000000236512663604777015023 0ustar www-datawww-dataAllCops: DisplayCopNames: true Metrics/BlockNesting: Max: 2 Metrics/ClassLength: CountComments: false Max: 125 Metrics/PerceivedComplexity: Max: 8 Metrics/CyclomaticComplexity: Max: 8 # TODO: Lower to 6 Metrics/LineLength: AllowURI: true Max: 143 # TODO: Lower to 80 Metrics/MethodLength: CountComments: false Max: 22 # TODO: Lower to 15 Metrics/ModuleLength: CountComments: false Max: 120 Metrics/ParameterLists: Max: 5 CountKeywordArgs: true Metrics/AbcSize: Enabled: false Style/CollectionMethods: PreferredMethods: collect: 'map' reduce: 'inject' find: 'detect' find_all: 'select' Style/Documentation: Enabled: false Style/DotPosition: EnforcedStyle: trailing Style/DoubleNegation: Enabled: false Style/EachWithObject: Enabled: false Style/Encoding: Enabled: false Style/HashSyntax: EnforcedStyle: hash_rockets Style/Lambda: Enabled: false Style/SingleSpaceBeforeFirstArg: Enabled: false Style/SpaceAroundOperators: MultiSpaceAllowedForOperators: - "=" - "=>" - "||" - "||=" - "&&" - "&&=" Style/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Style/StringLiterals: EnforcedStyle: double_quotes Style/TrivialAccessors: Enabled: false http-1.0.2/metadata.yml0000644000004100000410000001332112663604777015046 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: http version: !ruby/object:Gem::Version version: 1.0.2 platform: ruby authors: - Tony Arcieri - Erik Michaels-Ober - Alexey V. Zapparov - Zachary Anker autorequire: bindir: bin cert_chain: [] date: 2016-01-15 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: http_parser.rb requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.6.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.6.0 - !ruby/object:Gem::Dependency name: http-form_data requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.0.1 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.0.1 - !ruby/object:Gem::Dependency name: http-cookie requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' - !ruby/object:Gem::Dependency name: addressable requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.3' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.3' - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' description: 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. email: - bascule@gmail.com executables: [] extensions: [] extra_rdoc_files: [] files: - ".coveralls.yml" - ".gitignore" - ".rspec" - ".rubocop.yml" - ".travis.yml" - ".yardopts" - CHANGES.md - CONTRIBUTING.md - Gemfile - Guardfile - LICENSE.txt - README.md - Rakefile - http.gemspec - lib/http.rb - lib/http/chainable.rb - lib/http/client.rb - lib/http/connection.rb - lib/http/content_type.rb - lib/http/errors.rb - lib/http/headers.rb - lib/http/headers/known.rb - lib/http/headers/mixin.rb - lib/http/mime_type.rb - lib/http/mime_type/adapter.rb - lib/http/mime_type/json.rb - lib/http/options.rb - lib/http/redirector.rb - lib/http/request.rb - lib/http/request/writer.rb - lib/http/response.rb - lib/http/response/body.rb - lib/http/response/parser.rb - lib/http/response/status.rb - lib/http/response/status/reasons.rb - lib/http/timeout/global.rb - lib/http/timeout/null.rb - lib/http/timeout/per_operation.rb - lib/http/uri.rb - lib/http/version.rb - logo.png - spec/lib/http/client_spec.rb - spec/lib/http/content_type_spec.rb - spec/lib/http/headers/mixin_spec.rb - spec/lib/http/headers_spec.rb - spec/lib/http/options/body_spec.rb - spec/lib/http/options/form_spec.rb - spec/lib/http/options/headers_spec.rb - spec/lib/http/options/json_spec.rb - spec/lib/http/options/merge_spec.rb - spec/lib/http/options/new_spec.rb - spec/lib/http/options/proxy_spec.rb - spec/lib/http/options_spec.rb - spec/lib/http/redirector_spec.rb - spec/lib/http/request/writer_spec.rb - spec/lib/http/request_spec.rb - spec/lib/http/response/body_spec.rb - spec/lib/http/response/status_spec.rb - spec/lib/http/response_spec.rb - spec/lib/http_spec.rb - spec/regression_specs.rb - spec/spec_helper.rb - spec/support/black_hole.rb - spec/support/capture_warning.rb - spec/support/dummy_server.rb - spec/support/dummy_server/servlet.rb - spec/support/http_handling_shared.rb - spec/support/proxy_server.rb - spec/support/servers/config.rb - spec/support/servers/runner.rb - spec/support/ssl_helper.rb homepage: https://github.com/httprb/http.rb licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.2.5 signing_key: specification_version: 4 summary: HTTP should be easy test_files: - spec/lib/http/client_spec.rb - spec/lib/http/content_type_spec.rb - spec/lib/http/headers/mixin_spec.rb - spec/lib/http/headers_spec.rb - spec/lib/http/options/body_spec.rb - spec/lib/http/options/form_spec.rb - spec/lib/http/options/headers_spec.rb - spec/lib/http/options/json_spec.rb - spec/lib/http/options/merge_spec.rb - spec/lib/http/options/new_spec.rb - spec/lib/http/options/proxy_spec.rb - spec/lib/http/options_spec.rb - spec/lib/http/redirector_spec.rb - spec/lib/http/request/writer_spec.rb - spec/lib/http/request_spec.rb - spec/lib/http/response/body_spec.rb - spec/lib/http/response/status_spec.rb - spec/lib/http/response_spec.rb - spec/lib/http_spec.rb - spec/regression_specs.rb - spec/spec_helper.rb - spec/support/black_hole.rb - spec/support/capture_warning.rb - spec/support/dummy_server.rb - spec/support/dummy_server/servlet.rb - spec/support/http_handling_shared.rb - spec/support/proxy_server.rb - spec/support/servers/config.rb - spec/support/servers/runner.rb - spec/support/ssl_helper.rb has_rdoc: http-1.0.2/.gitignore0000644000004100000410000000024612663604777014535 0ustar www-datawww-data*.gem .bundle .config .rvmrc .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc lib/bundler/man measurement pkg rdoc spec/reports test/tmp test/version_tmp tmp http-1.0.2/.yardopts0000644000004100000410000000005612663604777014412 0ustar www-datawww-data--markup-provider=redcarpet --markup=markdown http-1.0.2/CONTRIBUTING.md0000644000004100000410000000131312663604777014772 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 http-1.0.2/CHANGES.md0000644000004100000410000003405312663604777014142 0ustar www-datawww-data## 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.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 http-1.0.2/http.gemspec0000644000004100000410000000236312663604777015073 0ustar www-datawww-datalib = 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.rb" 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.add_runtime_dependency "http_parser.rb", "~> 0.6.0" gem.add_runtime_dependency "http-form_data", "~> 1.0.1" gem.add_runtime_dependency "http-cookie", "~> 1.0" gem.add_runtime_dependency "addressable", "~> 2.3" gem.add_development_dependency "bundler", "~> 1.0" end http-1.0.2/README.md0000644000004100000410000002672612663604777014037 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=master)](https://travis-ci.org/httprb/http) [![Code Climate](https://codeclimate.com/github/httprb/http.svg?branch=master)](https://codeclimate.com/github/httprb/http) [![Coverage Status](https://coveralls.io/repos/httprb/http/badge.svg?branch=master)](https://coveralls.io/r/httprb/http) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/httprb/http/blob/master/LICENSE.txt) ## 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 | |--------------------------|-----------| | curb (persistent) | 2.519088 | | em-http-request | 2.731645 | | Typhoeus | 2.851911 | | StreamlyFFI (persistent) | 2.853786 | | http.rb (persistent) | 2.970702 | | http.rb | 3.588964 | | HTTParty | 3.931913 | | Net::HTTP | 3.959342 | | Net::HTTP (persistent) | 4.043674 | | open-uri | 4.479817 | | Excon (persistent) | 4.618361 | | Excon | 4.701262 | | RestClient | 26.832668 | 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: gem "http" And then execute: $ bundle Or install it yourself as: $ gem install http Inside of your Ruby program do: require "http" ...to pull it in as a dependency. ## Documentation [Please see the http.rb wiki](https://github.com/httprb/http/wiki) for more detailed documentation and usage notes. ## Basic Usage Here's some simple examples to get you started: ### GET requests ```ruby >> HTTP.get("https://github.com").to_s => "> HTTP.get("https://github.com") => #"text/html; charset=UTF-8", "Date"=>"Fri, ...> => #"text/html; ...> ``` 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`: ```ruby >> HTTP.get("https://github.com").body.readpartial => " {:foo => "42"}) ``` Making GET requests with query string parameters is as simple. ```ruby HTTP.get("http://example.com/resource", :params => {:foo => "bar"}) ``` Want to POST with a specific body, JSON for instance? ```ruby HTTP.post("http://example.com/resource", :json => { :foo => "42" }) ``` Or just a plain body? ```ruby HTTP.post("http://example.com/resource", :body => "foo=42&bar=baz") ``` Posting a file? ``` ruby HTTP.post("http://examplc.com/resource", :form => { :username => "ixti", :avatar => HTTP::FormData::File.new("/home/ixit/avatar.png") }) ``` It's easy! ### Proxy Support Making request behind proxy is as simple as making them directly. Just specify hostname (or IP address) of your proxy server and its port, and here you go: ```ruby HTTP.via("proxy-hostname.local", 8080) .get("http://example.com/resource") ``` Proxy needs authentication? No problem: ```ruby HTTP.via("proxy-hostname.local", 8080, "username", "password") .get("http://example.com/resource") ``` ### Adding Headers The HTTP gem uses the concept of chaining to simplify requests. Let's say you want to get the latest commit of this library from GitHub in JSON format. One way we could do this is by tacking a filename on the end of the URL: ```ruby HTTP.get("https://github.com/httprb/http/commit/HEAD.json") ``` The GitHub API happens to support this approach, but really this is a bit of a hack that makes it easy for people typing URLs into the address bars of browsers to perform the act of content negotiation. Since we have access to the full, raw power of HTTP, we can perform content negotiation the way HTTP intends us to, by using the Accept header: ```ruby HTTP.headers(:accept => "application/json") .get("https://github.com/httprb/http/commit/HEAD") ``` This requests JSON from GitHub. GitHub is smart enough to understand our request and returns a response with `Content-Type: application/json`. Shorter alias exists for `HTTP.headers`: ```ruby HTTP[:accept => "application/json"] .get("https://github.com/httprb/http/commit/HEAD") ``` ### Authorization Header With [HTTP Basic Authentication](http://tools.ietf.org/html/rfc2617) using a username and password: ```ruby HTTP.basic_auth(:user => "user", :pass => "pass") # "Basic dXNlcjpwYXNz"}> ``` Or with plain as-is value: ```ruby HTTP.auth("Bearer VGhlIEhUVFAgR2VtLCBST0NLUw") # "Bearer VGhlIEhUVFAgR2VtLCBST0NLUw"}> ``` And Chain all together! ```ruby HTTP.basic_auth(:user => "user", :pass => "pass") .headers("Cookie" => "9wq3w") .get("https://example.com") ``` ### Content Negotiation As important a concept as content negotiation is to HTTP, it sure should be easy, right? But usually it's not, and so we end up adding ".json" onto the ends of our URLs because the existing mechanisms make it too hard. It should be easy: ```ruby HTTP.accept(:json).get("https://github.com/httprb/http/commit/HEAD") ``` This adds the appropriate Accept header for retrieving a JSON response for the given resource. ### Reuse HTTP connection: HTTP Keep-Alive If you need to make many successive requests against the same host, you can create client with persistent connection to the host: ``` ruby begin # create HTTP client with persistent connection to api.icndb.com: http = HTTP.persistent "http://api.icndb.com" # issue multiple requests using same connection: jokes = 100.times.map { http.get("/jokes/random").to_s } ensure # close underlying connection when you don't need it anymore http.close if http end ``` If the optional code block is given, it will be passed the client with persistent connection to the host as an argument and `client.close` will be automatically called when the block terminates. The value of the block will be returned: ``` ruby jokes = HTTP.persistent "http://api.icndb.com" do |http| 100.times.map { http.get("/jokes/random").to_s } end ``` ##### NOTICE You must consume response before sending next request via persistent connection. That means you need to call `#to_s`, `#parse` or `#flush` on response object. In the example above we used `http.get("/jokes/random").to_s` to get response bodies. That works perfectly fine, because `#to_s` reads off the response. Sometimes you don't need response body, or need whole response object to access it's status, headers etc instead. You can either call `#to_s` to make sure response was flushed and then use response object itself, or use `#flush` (syntax sugar for `#tap(&:to_s)` that will do that for you: ``` ruby contents = HTTP.persistent "http://en.wikipedia.org" do |http| %w(Hypertext_Transfer_Protocol Git GitHub Linux Hurd).map do http.get("/wiki/#{target}").flush end end ``` ### Timeouts By default, HTTP does not timeout on a request. You can enable per operation (each read/write/connect call) or global (sum of all read/write/connect calls). Per operation timeouts are what `Net::HTTP` and the majority of HTTP clients do: ``` ruby HTTP.timeout(:per_operation, :write => 2, :connect => 5, :read => 10) .get "http://example.com" # For convinience, you can omit timeout type in this case. So following has # same result as the above: HTTP.timeout(:write => 2, :connect => 5, :read => 10).get "http://example.com" ``` Global timeouts let you set an upper bound of how long a request can take, without having to rely on `Timeout.timeout`: ``` ruby HTTP.timeout(:global, :write => 1, :connect => 1, :read => 1) .get "http://example.com" ``` Uses a timeout of 3 seconds, for the entire `get` call. *Warning!* You cannot use Celluloid::IO with timeouts currently. ## Supported Ruby Versions This library aims to support and is [tested against][travis] the following Ruby versions: * Ruby 1.9.3 * Ruby 2.0.0 * Ruby 2.1.x * Ruby 2.2.x * Ruby 2.3.x * JRuby 1.7.x * JRuby 9000+ 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-2016 Tony Arcieri, Erik Michaels-Ober, Alexey V. Zapparov, Zachary Anker. See LICENSE.txt for further details. http-1.0.2/Guardfile0000644000004100000410000000065412663604777014375 0ustar www-datawww-data# 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