pax_global_header00006660000000000000000000000064143651531640014522gustar00rootroot0000000000000052 comment=e26d685db121dd6b9e1e5e9f600512ab7d7f3b71 ruby-async-http-0.59.5/000077500000000000000000000000001436515316400146735ustar00rootroot00000000000000ruby-async-http-0.59.5/.editorconfig000066400000000000000000000000641436515316400173500ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 2 ruby-async-http-0.59.5/.github/000077500000000000000000000000001436515316400162335ustar00rootroot00000000000000ruby-async-http-0.59.5/.github/workflows/000077500000000000000000000000001436515316400202705ustar00rootroot00000000000000ruby-async-http-0.59.5/.github/workflows/documentation.yaml000066400000000000000000000014131436515316400240240ustar00rootroot00000000000000name: Documentation permissions: contents: write on: push: branches: - main permissions: contents: write env: CONSOLE_OUTPUT: XTerm BUNDLE_WITH: maintenance jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.1" bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Prepare GitHub Pages run: bundle exec bake github:pages:prepare --directory docs - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static --force no - name: Deploy GitHub Pages run: bundle exec bake github:pages:commit --directory docs ruby-async-http-0.59.5/.github/workflows/test-async-head.yaml000066400000000000000000000010401436515316400241400ustar00rootroot00000000000000name: Test Async HEAD on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm BUNDLE_GEMFILE: gems/async-head.rb jobs: test: runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu ruby: - head steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test ruby-async-http-0.59.5/.github/workflows/test-async-v1.yaml000066400000000000000000000010331436515316400235670ustar00rootroot00000000000000name: Test Async v1 on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm BUNDLE_GEMFILE: gems/async-v1.rb jobs: test: runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu ruby: - 2.7 steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test ruby-async-http-0.59.5/.github/workflows/test-external.yaml000066400000000000000000000011461436515316400237550ustar00rootroot00000000000000name: Test External on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "2.7" - "3.0" - "3.1" steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test:external ruby-async-http-0.59.5/.github/workflows/test.yaml000066400000000000000000000020541436515316400221340ustar00rootroot00000000000000name: Test on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - "2.7" - "3.0" - "3.1" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Installing packages (ubuntu) if: matrix.os == 'ubuntu' run: sudo apt-get install apache2-utils - name: Run tests timeout-minutes: 10 run: bundle exec bake test ruby-async-http-0.59.5/.gitignore000066400000000000000000000002001436515316400166530ustar00rootroot00000000000000.tags /.bundle/ /.yardoc /gems.locked /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ .rspec_status .covered.db /h2spec ruby-async-http-0.59.5/.rspec000066400000000000000000000000671436515316400160130ustar00rootroot00000000000000--format documentation --warnings --require spec_helperruby-async-http-0.59.5/async-http.gemspec000066400000000000000000000026031436515316400203330ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/async/http/version" Gem::Specification.new do |spec| spec.name = "async-http" spec.version = Async::HTTP::VERSION spec.summary = "A HTTP client and server library." spec.authors = ["Samuel Williams", "Brian Morearty", "Bruno Sutic", "Janko Marohnić", "Adam Daniels", "Cyril Roelandt", "Denis Talakevich", "Ian Ker-Seymer", "Igor Sidorov", "Marco Concetto Rudilosso", "Olle Jonsson", "Orgad Shaneh", "Stefan Wrobel", "TheAthlete", "Trevor Turk", "samshadwell"] spec.license = "MIT" spec.cert_chain = ['release.cert'] spec.signing_key = File.expand_path('~/.gem/release.pem') spec.homepage = "https://github.com/socketry/async-http" spec.files = Dir.glob('{bake,lib}/**/*', File::FNM_DOTMATCH, base: __dir__) spec.add_dependency "async", ">= 1.25" spec.add_dependency "async-io", ">= 1.28" spec.add_dependency "async-pool", ">= 0.2" spec.add_dependency "protocol-http", "~> 0.23" spec.add_dependency "protocol-http1", "~> 0.14.0" spec.add_dependency "protocol-http2", "~> 0.14.0" spec.add_dependency "traces", ">= 0.8.0" spec.add_development_dependency "async-container", "~> 0.14" spec.add_development_dependency "async-rspec", "~> 1.10" spec.add_development_dependency "covered" spec.add_development_dependency "localhost" spec.add_development_dependency "rack-test" spec.add_development_dependency "rspec", "~> 3.6" end ruby-async-http-0.59.5/bake/000077500000000000000000000000001436515316400155755ustar00rootroot00000000000000ruby-async-http-0.59.5/bake/async/000077500000000000000000000000001436515316400167125ustar00rootroot00000000000000ruby-async-http-0.59.5/bake/async/http.rb000066400000000000000000000041051436515316400202160ustar00rootroot00000000000000 # Fetch the specified URL and print the response. # @param url [String] the URL to parse and fetch. # @param method [String] the HTTP method to use. def fetch(url, method:) require 'async/http/internet' require 'kernel/sync' terminal = Console::Terminal.for($stdout) terminal[:request] = terminal.style(:blue, nil, :bold) terminal[:response] = terminal.style(:green, nil, :bold) terminal[:length] = terminal.style(nil, nil, :bold) terminal[:key] = terminal.style(nil, nil, :bold) terminal[:chunk_0] = terminal.style(:blue) terminal[:chunk_1] = terminal.style(:cyan) align = 20 format_body = proc do |body, terminal| if body if length = body.length terminal.print(:body, "body with length ", :length, length, "B") else terminal.print(:body, "body without length") end else terminal.print(:body, "no body") end end.curry Sync do internet = Async::HTTP::Internet.new response = internet.send(method.downcase.to_sym, url) terminal.print_line( :request, method.rjust(align), :reset, ": ", url ) terminal.print_line( :response, "version".rjust(align), :reset, ": ", response.version ) terminal.print_line( :response, "status".rjust(align), :reset, ": ", response.status, ) terminal.print_line( :response, "body".rjust(align), :reset, ": ", format_body[response.body], ) response.headers.each do |key, value| terminal.print_line( :key, key.rjust(align), :reset, ": ", :value, value.inspect ) end if body = response.body index = 0 style = [:chunk_0, :chunk_1] response.body.each do |chunk| terminal.print(style[index % 2], chunk) index += 1 end end response.finish if trailer = response.headers.trailer trailer.each do |key, value| terminal.print_line( :key, key.rjust(align), :reset, ": ", :value, value.inspect ) end end internet.close end end # GET the specified URL and print the response. def get(url) self.fetch(url, method: "GET") end # HEAD the specified URL and print the response. def head(url) self.fetch(url, method: "HEAD") end ruby-async-http-0.59.5/bake/async/http/000077500000000000000000000000001436515316400176715ustar00rootroot00000000000000ruby-async-http-0.59.5/bake/async/http/h2spec.rb000066400000000000000000000016231436515316400214040ustar00rootroot00000000000000 def build # Fetch the code: system "go get github.com/spf13/cobra" system "go get github.com/summerwind/h2spec" # This builds `h2spec` into the current directory system "go build ~/go/src/github.com/summerwind/h2spec/cmd/h2spec/h2spec.go" end def test server do system("./h2spec", "-p", "7272") end end private def server require 'async' require 'async/container' require 'async/http/server' require 'async/io/host_endpoint' endpoint = Async::IO::Endpoint.tcp('127.0.0.1', 7272) container = Async::Container.new Console.logger.info(self){"Starting server..."} container.run(count: 1) do server = Async::HTTP::Server.for(endpoint, protocol: Async::HTTP::Protocol::HTTP2, scheme: "https") do |request| Protocol::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]] end Async do server.run end end yield if block_given? ensure container&.stop end ruby-async-http-0.59.5/config/000077500000000000000000000000001436515316400161405ustar00rootroot00000000000000ruby-async-http-0.59.5/config/external.yaml000066400000000000000000000007511436515316400206510ustar00rootroot00000000000000falcon: url: https://github.com/socketry/falcon.git command: bundle exec rspec async-rest: url: https://github.com/socketry/async-rest.git command: bundle exec rspec async-websocket: url: https://github.com/socketry/async-websocket.git command: bundle exec sus async-http-faraday: url: https://github.com/socketry/async-http-faraday.git command: bundle exec rspec # async-http-cache: # url: https://github.com/socketry/async-http-cache.git # command: bundle exec rspec ruby-async-http-0.59.5/examples/000077500000000000000000000000001436515316400165115ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/compare/000077500000000000000000000000001436515316400201375ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/compare/benchmark.rb000077500000000000000000000023301436515316400224170ustar00rootroot00000000000000#!/usr/bin/env ruby require 'benchmark' require 'httpx' require 'async' require 'async/barrier' require 'async/semaphore' require 'async/http/internet' URL = "https://www.codeotaku.com/index" REPEATS = 10 Benchmark.bmbm do |x| x.report("async-http") do Async do internet = Async::HTTP::Internet.new i = 0 while i < REPEATS response = internet.get(URL) response.read i += 1 end ensure internet&.close end end x.report("async-http (pipelined)") do Async do |task| internet = Async::HTTP::Internet.new semaphore = Async::Semaphore.new(100, parent: task) barrier = Async::Barrier.new(parent: semaphore) # Warm up the connection pool... response = internet.get(URL) response.read i = 0 while i < REPEATS barrier.async do response = internet.get(URL) response.read end i += 1 end barrier.wait ensure internet&.close end end x.report("httpx") do i = 0 while i < REPEATS response = HTTPX.get(URL) response.read i += 1 end end x.report("httpx (pipelined)") do urls = [URL] * REPEATS responses = HTTPX.get(*urls) responses.each do |response| response.read end end end ruby-async-http-0.59.5/examples/compare/gems.rb000066400000000000000000000001371436515316400214200ustar00rootroot00000000000000 source "https://rubygems.org" gem "benchmark-ips" gem "async" gem "async-http" gem "httpx" ruby-async-http-0.59.5/examples/download/000077500000000000000000000000001436515316400203205ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/download/chunked.rb000077500000000000000000000041551436515316400222760ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require 'async/clock' require 'async/barrier' require 'async/semaphore' require_relative '../../lib/async/http/endpoint' require_relative '../../lib/async/http/client' Async do url = "https://static.openfoodfacts.org/data/en.openfoodfacts.org.products.csv" endpoint = Async::HTTP::Endpoint.parse(url) client = Async::HTTP::Client.new(endpoint) headers = {'user-agent' => 'curl/7.69.1', 'accept' => '*/*'} file = File.open("products.csv", "w") Console.logger.info(self) {"Saving download to #{Dir.pwd}"} begin response = client.head(endpoint.path, headers) content_length = nil if response.success? unless response.headers['accept-ranges'].include?('bytes') raise "Does not advertise support for accept-ranges: bytes!" end unless content_length = response.body&.length raise "Could not determine length of response!" end end ensure response&.close end Console.logger.info(self) {"Content length: #{content_length/(1024**2)}MiB"} parts = [] offset = 0 chunk_size = 1024*1024 start_time = Async::Clock.now amount = 0 while offset < content_length byte_range_start = offset byte_range_end = [offset + chunk_size, content_length].min parts << (byte_range_start...byte_range_end) offset += chunk_size end Console.logger.info(self) {"Breaking download into #{parts.size} parts..."} semaphore = Async::Semaphore.new(8) barrier = Async::Barrier.new(parent: semaphore) while !parts.empty? barrier.async do part = parts.shift Console.logger.info(self) {"Issuing range request range: bytes=#{part.min}-#{part.max}"} response = client.get(endpoint.path, [ ["range", "bytes=#{part.min}-#{part.max-1}"], *headers ]) if response.success? Console.logger.info(self) {"Got response: #{response}... writing data for #{part}."} written = file.pwrite(response.read, part.min) amount += written duration = Async::Clock.now - start_time Console.logger.info(self) {"Rate: #{((amount.to_f/(1024**2))/duration).round(2)}MiB/s"} end end end barrier.wait ensure client&.close end ruby-async-http-0.59.5/examples/fetch/000077500000000000000000000000001436515316400176025ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/fetch/README.md000066400000000000000000000001251436515316400210570ustar00rootroot00000000000000# Fetch This was an experiment to see how browsers handle bi-directional streaming. ruby-async-http-0.59.5/examples/fetch/config.ru000066400000000000000000000006661436515316400214270ustar00rootroot00000000000000 require 'rack' class Echo def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) if request.path_info == "/echo" if output = request.body return [200, {}, output.body] else return [200, {}, ["Hello World?"]] end else return @app.call(env) end end end use Echo use Rack::Static, :urls => [''], :root => 'public', :index => 'index.html' run lambda{|env| [404, {}, []]} ruby-async-http-0.59.5/examples/fetch/gems.rb000066400000000000000000000000311436515316400210540ustar00rootroot00000000000000 gem 'rack' gem 'falcon' ruby-async-http-0.59.5/examples/fetch/public/000077500000000000000000000000001436515316400210605ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/fetch/public/index.html000066400000000000000000000004551436515316400230610ustar00rootroot00000000000000 Streaming Fetch

Streams

Sent

Received

ruby-async-http-0.59.5/examples/fetch/public/stream.js000066400000000000000000000024031436515316400227100ustar00rootroot00000000000000const inputStream = new ReadableStream({ start(controller) { interval = setInterval(() => { let string = "Hello World!"; // Add the string to the stream controller.enqueue(string); // show it on the screen let listItem = document.createElement('li'); listItem.textContent = string; sent.appendChild(listItem); }, 10000); stopButton.addEventListener('click', function() { clearInterval(interval); controller.close(); }) }, pull(controller) { // We don't really need a pull in this example }, cancel() { // This is called if the reader cancels, // so we should stop generating strings clearInterval(interval); } }); fetch("/echo", {method: 'POST', body: inputStream}) .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); function push() { reader.read().then(({done, value}) => { console.log("done:", done, "value:", value); const string = decoder.decode(value); // show it on the screen let listItem = document.createElement('li'); if (done) listItem.textContent = "" else listItem.textContent = string; received.appendChild(listItem); if (done) return; else push(); }); }; push(); }); ruby-async-http-0.59.5/examples/google/000077500000000000000000000000001436515316400177655ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/google/codeotaku.rb000077500000000000000000000012461436515316400222760ustar00rootroot00000000000000#!/usr/bin/env ruby require "async" require "async/clock" require "protocol/http/middleware" require_relative "../../lib/async/http" URL = "https://www.codeotaku.com/index" ENDPOINT = Async::HTTP::Endpoint.parse(URL) Console.logger.enable(Async::IO::Stream, Console::Logger::DEBUG) if count = ENV['COUNT']&.to_i terms = terms.first(count) end Async do |task| client = Async::HTTP::Client.new(ENDPOINT) client.get(ENDPOINT.path).finish duration = Async::Clock.measure do 20.times.map do |i| task.async do response = client.get(ENDPOINT.path) response.read $stderr.write "(#{i})" end end.map(&:wait) end pp duration ensure client.close end ruby-async-http-0.59.5/examples/google/search.rb000077500000000000000000000032701436515316400215640ustar00rootroot00000000000000#!/usr/bin/env ruby require "async" require "async/clock" require "protocol/http/middleware" require_relative "../../lib/async/http" URL = "https://www.google.com/search" ENDPOINT = Async::HTTP::Endpoint.parse(URL) # Console.logger.enable(Async::IO::Stream, Console::Logger::DEBUG) class Google < Protocol::HTTP::Middleware def search(term) Console.logger.info(self) {"Searching for #{term}..."} self.get("/search?q=#{term}", {"user-agent" => "Hi Google!"}) end end terms = %w{thoughtful fear size payment lethal modern recognise face morning sulky mountainous contain science snow uncle skirt truthful door travel snails closed rotten halting creator teeny-tiny beautiful cherries unruly level follow strip team things suggest pretty warm end cannon bad pig consider airport strengthen youthful fog three walk furry pickle moaning fax book ruddy sigh plate cakes shame stem faulty bushes dislike train sleet one colour behavior bitter suit count loutish squeak learn watery orange idiotic seat wholesale omniscient nostalgic arithmetic instruct committee puffy program cream cake whistle rely encourage war flagrant amusing fluffy prick utter wacky occur daily son check} if count = ENV.fetch('COUNT', 20)&.to_i terms = terms.first(count) end Async do |task| client = Async::HTTP::Client.new(ENDPOINT) google = Google.new(client) google.search("null").finish duration = Async::Clock.measure do counts = terms.map do |term| task.async do response = google.search(term) [term, response.read.scan(term).count] end end.map(&:wait).to_h Console.logger.info(self, name: 'counts') {counts} end Console.logger.info(self, name: 'duration') {duration} ensure google.close end ruby-async-http-0.59.5/examples/licenses/000077500000000000000000000000001436515316400203165ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/licenses/gemspect.rb000077500000000000000000000033201436515316400224530ustar00rootroot00000000000000#!/usr/bin/env ruby require 'csv' require 'json' require 'net/http' require 'protocol/http/header/authorization' class RateLimitingError < StandardError; end @user = ENV['GITHUB_USER'] @token = ENV['GITHUB_TOKEN'] unless @user && @token fail "export GITHUB_USER and GITHUB_TOKEN!" end def fetch_github_license(homepage_uri) %r{github.com/(?.+?)/(?.+)} =~ homepage_uri return nil unless repo url = URI.parse("https://api.github.com/repos/#{owner}/#{repo}/license") request = Net::HTTP::Get.new(url) request['user-agent'] = 'fetch-github-licenses' request['authorization'] = Protocol::HTTP::Header::Authorization.basic(@user, @token) response = Net::HTTP.start(url.hostname) do |http| http.request(request) end case response when Net::HTTPOK JSON.parse(response.body).dig('license', 'spdx_id') when Net::HTTPNotFound, Net::HTTPMovedPermanently, Net::HTTPForbidden nil else raise response.body end end def fetch_rubygem_license(name, version) url = URI.parse("https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json") response = Net::HTTP.get_response(url) case response when Net::HTTPOK body = JSON.parse(response.body) [name, body.dig('licenses', 0) || fetch_github_license(body['homepage_uri'])] when Net::HTTPNotFound [name, nil] # from a non rubygems remote when Net::HTTPTooManyRequests raise RateLimitingError else raise response.body end rescue RateLimitingError sleep 1 retry end threads = ARGF.map do |line| if line == "GEM\n" .. line.chomp.empty? /\A\s{4}(?[a-z].+?) \((?.+)\)\n\z/ =~ line Thread.new { fetch_rubygem_license(name, version) } if name end end.compact puts CSV.generate { |csv| threads.each { csv << _1.value } } ruby-async-http-0.59.5/examples/licenses/list.rb000077500000000000000000000035761436515316400216340ustar00rootroot00000000000000#!/usr/bin/env ruby require 'csv' require 'json' require 'async/http/internet' class RateLimitingError < StandardError; end @internet = Async::HTTP::Internet.new @user = ENV['GITHUB_USER'] @token = ENV['GITHUB_TOKEN'] unless @user && @token fail "export GITHUB_USER and GITHUB_TOKEN!" end GITHUB_HEADERS = { 'user-agent' => 'fetch-github-licenses', 'authorization' => Protocol::HTTP::Header::Authorization.basic(@user, @token) } RUBYGEMS_HEADERS = { 'user-agent' => 'fetch-github-licenses' } def fetch_github_license(homepage_uri) %r{github.com/(?.+?)/(?.+)} =~ homepage_uri return nil unless repo response = @internet.get("https://api.github.com/repos/#{owner}/#{repo}/license", GITHUB_HEADERS) case response.status when 200 return JSON.parse(response.read).dig('license', 'spdx_id') when 404 return nil else raise response.read end ensure response.finish end def fetch_rubygem_license(name, version) response = @internet.get("https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json", RUBYGEMS_HEADERS) case response.status when 200 body = JSON.parse(response.read) [name, body.dig('licenses', 0) || fetch_github_license(body['homepage_uri'])] when 404 [name, nil] # from a non rubygems remote when 429 raise RateLimitingError else raise response.read end rescue RateLimitingError response.finish Console.logger.warn(name) {"Rate limited..."} Async::Task.current.sleep(1.0) retry ensure response.finish end Sync do |parent| output = CSV.new($stdout) tasks = ARGF.map do |line| if line == "GEM\n" .. line.chomp.empty? /\A\s{4}(?[a-z].+?) \((?.+)\)\n\z/ =~ line parent.async do fetch_rubygem_license(name, version) end if name end end.compact tasks.each do |task| output << task.wait end @internet.instance_variable_get(:@clients).each do |name, client| puts client.pool end end ruby-async-http-0.59.5/examples/race/000077500000000000000000000000001436515316400174235ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/race/client.rb000077500000000000000000000012521436515316400212310ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require_relative '../../lib/async/http/internet' Console.logger.fatal! Async do |task| internet = Async::HTTP::Internet.new tasks = [] 100.times do tasks << task.async { loop do response = internet.get('http://127.0.0.1:8080/something/special') r = response.body.join if r.include?('nothing') p ['something', r] end end } end 100.times do tasks << task.async { loop do response = internet.get('http://127.0.0.1:8080/nothing/to/worry') r = response.body.join if r.include?('something') p ['nothing', r] end end } end tasks.each do |t| task.sleep 0.1 t.stop end endruby-async-http-0.59.5/examples/race/server.rb000077500000000000000000000005621436515316400212640ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require 'async/http/server' require 'async/http/endpoint' require 'async/http/protocol/response' endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:8080') app = lambda do |request| Protocol::HTTP::Response[200, {}, [request.path[1..-1]]] end server = Async::HTTP::Server.new(app, endpoint) Async do |task| server.run end ruby-async-http-0.59.5/examples/request.rb000066400000000000000000000014531436515316400205310ustar00rootroot00000000000000#!/usr/bin/env ruby # # $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) # $LOAD_PATH.unshift(File.expand_path("../../http-protocol/lib", __dir__)) require 'async' require 'async/http/client' require 'async/http/endpoint' # Console.logger.level = Logger::DEBUG Async do |task| endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") client = Async::HTTP::Client.new(endpoint) headers = { 'accept' => 'text/html', } request = Protocol::HTTP::Request.new(client.scheme, "www.google.com", "GET", "/search?q=cats", headers) puts "Sending request..." response = client.call(request) puts "Reading response status=#{response.status}..." if body = response.body while chunk = body.read puts chunk.size end end response.close puts "Finish reading response." end ruby-async-http-0.59.5/examples/request/000077500000000000000000000000001436515316400202015ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/request/http10.rb000066400000000000000000000005741436515316400216540ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require_relative '../../lib/async/http/endpoint' require '../../lib/async/http/client' Async do endpoint = Async::HTTP::Endpoint.parse("https://programming.dojo.net.nz", protocol: Async::HTTP::Protocol::HTTP10) client = Async::HTTP::Client.new(endpoint) response = client.get("programming.dojo.net.nz") puts response, response.read end ruby-async-http-0.59.5/examples/stream/000077500000000000000000000000001436515316400200045ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/stream/stop.rb000066400000000000000000000010651436515316400213200ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require 'async/http/internet' Async do |parent| internet = Async::HTTP::Internet.new connection = nil child = parent.async do response = internet.get("https://utopia-falcon-heroku.herokuapp.com/beer/index") connection = response.connection response.each do |chunk| Console.logger.info(response) {chunk} end ensure Console.logger.info(response) {"Closing response..."} response&.close end parent.sleep(5) Console.logger.info(parent) {"Killing #{child}..."} child.stop ensure internet&.close end ruby-async-http-0.59.5/examples/trenni/000077500000000000000000000000001436515316400200105ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/trenni/Gemfile000066400000000000000000000000761436515316400213060ustar00rootroot00000000000000 source 'https://rubygems.org' gem "trenni" gem "async-http" ruby-async-http-0.59.5/examples/trenni/streaming.rb000066400000000000000000000014751436515316400223350ustar00rootroot00000000000000 require 'trenni/template' require 'async' require 'async/http/body/writable' # The template, using inline text. The sleep could be anything - database query, HTTP request, redis, etc. buffer = Trenni::Buffer.new(<<-EOF) The "\#{self[:count]} bottles of \#{self[:drink]} on the wall" song! \#{index} bottles of \#{self[:drink]} on the wall, \#{index} bottles of \#{self[:drink]}, take one down, and pass it around, \#{index - 1} bottles of \#{self[:drink]} on the wall. EOF template = Trenni::Template.new(buffer) Async do body = Async::HTTP::Body::Writable.new generator = Async do template.to_string({count: 100, drink: 'coffee'}, body) end while chunk = body.read $stdout.write chunk end generator.wait end.wait ruby-async-http-0.59.5/examples/upload/000077500000000000000000000000001436515316400177755ustar00rootroot00000000000000ruby-async-http-0.59.5/examples/upload/client.rb000066400000000000000000000016271436515316400216060ustar00rootroot00000000000000#!/usr/bin/env ruby $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require 'async' require 'protocol/http/body/file' require 'async/http/body/delayed' require 'async/http/client' require 'async/http/endpoint' Async do endpoint = Async::HTTP::Endpoint.parse("http://localhost:9222") client = Async::HTTP::Client.new(endpoint, protocol: Async::HTTP::Protocol::HTTP2) headers = [ ['accept', 'text/plain'], ] body = Async::HTTP::Body::Delayed.new(Protocol::HTTP::Body::File.open(File.join(__dir__, "data.txt"), block_size: 32)) response = client.post(endpoint.path, headers, body) puts response.status # response.read -> string # response.each {|chunk| ...} # response.close (forcefully ignore data) # body = response.finish (read and buffer response) # response.save("echo.txt") response.each do |chunk| puts chunk.inspect end ensure client.close if client end puts "Done." ruby-async-http-0.59.5/examples/upload/data.txt000066400000000000000000000126741436515316400214610ustar00rootroot00000000000000The Parable of the Two Programmers Neil W. Rickert Once upon a time, unbeknownst to each other, the "Automated Accounting Applications Association" and the "Consolidated Computerized Capital Corporation" decided that they needed the identical program to perform a certain service. Automated hired a programmer-analyst, Alan, to solve their problem. Meanwhile, Consolidated decided to ask a newly-hired entry-level programmer, Charles, to tackle the job, to see if he was as good as he pretended. Alan, having had experience in difficult programming projects, decided to use the PQR structured design methodology. With this in mind he asked his department manager to assign another three programmers as a programming team. Then the team went to work, churning out preliminary reports and problem analyses. Back at Consolidated, Charles spent some time thinking about the problem. His fellow employees noticed that Charles often sat with his feet on the desk, drinking coffee. He was occasionally seen at his computer terminal, but his office mate could tell from the rhythmic striking of keys that he was actually playing Space Invaders. By now, the team at Automated was starting to write code. The programmers were spending about half their time writing and compiling code, and the rest of their time in conference, discussing the interfaces between the various modules. His office mate noticed that Charles had finally given up on Space Invaders. Instead he now divided his time between drinking coffee with his feet on the table, and scribbling on little scraps of paper. His scribbling didn't seem to be Tic-Tac-Toe, but it didn't exactly make much sense, either. Two months have gone by. The team at Automated finally releases an implementation timetable. In another two months they will have a test version of the program. Then a two month period of testing and enhancing should yield a completed version. The manager of Charles has by now tired of seeing him goof off. He decides to confront him. But as he walks into Charles' office, he is surprised to see Charles busy entering code at his terminal. He decides to postpone the confrontation, so makes some small talk and then leaves. However, he begins to keep a closer watch on Charles, so that when the opportunity presents itself he can confront him. Not looking forward to an unpleasant conversation, he is pleased to notice that Charles seems to be busy most of the time. He has even been seen to delay his lunch, and to stay after work two or three days a week. At the end of three months, Charles announces he has completed the project. He submits a 500-line program. The program appears to be clearly written, and when tested it does everything required in the specifications. In fact, it even has a few additional convenience features which might significantly improve the usability of the program. The program is put into test, and except for one quickly corrected oversight, performs well. The team at Automated has by now completed two of the four major modules required for their program. These modules are now undergoing testing while the other modules are completed. After another three weeks, Alan announces that the preliminary version is ready one week ahead of schedule. He supplies a list of the deficiencies that he expects to correct. The program is placed under test. The users find a number of bugs and deficiencies other than those listed. As Alan explains, this is no surprise. After all, this is a preliminary version in which bugs were expected. After about two more months, the team has completed its production version of the program. It consists of about 2,500 lines of code. When tested, it seems to satisfy most of the original specifications. It has omitted one or two features, and is very fussy about the format of its input data. However, the company decides to install the program. They can always train their data-entry staff to enter data in the strict format required. The program is handed over to some maintenance programmers to eventually incorporate the missing features. Sequel At first Charles' supervisor was impressed. But as he read through the source code, he realized that the project was really much simpler than he had originally thought. It now seemed apparent that this was not much of a challenge even for a beginning programmer. Charles did produce about five lines of code per day. This is perhaps a little above average. However, considering the simplicity of the program, it was nothing exceptional. Also, his supervisor remembered his two months of goofing off. At his next salary review Charles was given a raise which was about half the inflation over the period. He was not given a promotion. After about a year he became discouraged and left Consolidated. At Automated, Alan was complimented for completing his project on schedule. His supervisor looked over the program. Within a few minutes of thumbing through he saw that the company standards about structured programming were being observed. He quickly gave up attempting to read the program; however, it seemed quite incomprehensible. He realized by now that the project was really much more complex than he had originally assumed, and he congratulated Alan again on his achievement. The team had produced over three lines of code per programmer per day. This was about average, but considering the complexity of the problem, could be considered to be exceptional. Alan was given a hefty pay raise, and promoted to Systems Analyst as a reward for his achievement.ruby-async-http-0.59.5/examples/upload/server.rb000066400000000000000000000007311436515316400216310ustar00rootroot00000000000000 $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require 'logger' require 'async' require 'async/http/server' require 'async/http/endpoint' protocol = Async::HTTP::Protocol::HTTP2 endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9222', reuse_port: true) Console.logger.level = Logger::DEBUG Async do server = Async::HTTP::Server.for(endpoint, protocol: protocol) do |request| Protocol::HTTP::Response[200, {}, request.body] end server.run end ruby-async-http-0.59.5/examples/upload/upload.rb000066400000000000000000000011051436515316400216030ustar00rootroot00000000000000#!/usr/bin/env ruby require 'async' require 'protocol/http/body/file' require 'async/http/internet' Async do internet = Async::HTTP::Internet.new headers = [ ['accept', 'text/plain'], ] body = Protocol::HTTP::Body::File.open(File.join(__dir__, "data.txt")) response = internet.post("https://utopia-falcon-heroku.herokuapp.com/echo/index", headers, body) # response.read -> string # response.each {|chunk| ...} # response.close (forcefully ignore data) # body = response.finish (read and buffer response) response.save("echo.txt") ensure internet.close end ruby-async-http-0.59.5/gems.rb000066400000000000000000000010701436515316400161510ustar00rootroot00000000000000source 'https://rubygems.org' gemspec group :maintenance, optional: true do gem "bake-modernize" gem "bake-gem" gem "bake-github-pages" gem "utopia-project" end group :test do gem "bake" gem "bake-test" gem "bake-test-external" end # gem "async", path: "../async" # gem "async-io", path: "../async-io" # gem "traces", path: "../traces" # gem "protocol-http", path: "../protocol-http" # gem "protocol-http1", path: "../protocol-http1" # gem "protocol-http2", path: "../protocol-http2" # gem "protocol-hpack", path: "../protocol-hpack" gem "thread-local" ruby-async-http-0.59.5/gems/000077500000000000000000000000001436515316400156265ustar00rootroot00000000000000ruby-async-http-0.59.5/gems/async-head.rb000066400000000000000000000002201436515316400201610ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' eval_gemfile("../gems.rb") gem 'async', git: "https://github.com/socketry/async" ruby-async-http-0.59.5/gems/async-v1.rb000066400000000000000000000001601436515316400176110ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' eval_gemfile("../gems.rb") gem 'async', '~> 1.0' ruby-async-http-0.59.5/lib/000077500000000000000000000000001436515316400154415ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/000077500000000000000000000000001436515316400165565ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http.rb000066400000000000000000000024161436515316400200650ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'http/version' require_relative 'http/client' require_relative 'http/server' require_relative 'http/endpoint' ruby-async-http-0.59.5/lib/async/http/000077500000000000000000000000001436515316400175355ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/body.rb000066400000000000000000000024561436515316400210260ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/buffered' require_relative 'body/writable' module Async module HTTP module Body include ::Protocol::HTTP::Body end end end ruby-async-http-0.59.5/lib/async/http/body/000077500000000000000000000000001436515316400204725ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/body/delayed.rb000066400000000000000000000027741436515316400224400ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/wrapper' module Async module HTTP module Body class Delayed < Protocol::HTTP::Body::Wrapper def initialize(body, delay = 0.01) super(body) @delay = delay end def ready? false end def read Async::Task.current.sleep(@delay) return super end end end end end ruby-async-http-0.59.5/lib/async/http/body/hijack.rb000066400000000000000000000053411436515316400222530ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/readable' require 'protocol/http/body/stream' require_relative 'writable' module Async module HTTP module Body # A body which is designed for hijacked server responses - a response which uses a block to read and write the request and response bodies respectively. class Hijack < ::Protocol::HTTP::Body::Readable def self.response(request, status, headers, &block) ::Protocol::HTTP::Response[status, headers, self.wrap(request, &block)] end def self.wrap(request = nil, &block) self.new(block, request&.body) end def initialize(block, input = nil) @block = block @input = input @task = nil @stream = nil @output = nil end # We prefer streaming directly as it's the lowest overhead. def stream? true end def call(stream) return @block.call(stream) end attr :input # Has the producer called #finish and has the reader consumed the nil token? def empty? @output&.empty? end def ready? @output&.ready? end # Read the next available chunk. def read unless @output @output = Writable.new @stream = ::Protocol::HTTP::Body::Stream.new(@input, @output) @task = Task.current.async do |task| task.annotate "Streaming hijacked body." @block.call(@stream) end end return @output.read end def inspect "\#<#{self.class} #{@block.inspect}>" end def to_s "" end end end end end ruby-async-http-0.59.5/lib/async/http/body/pipe.rb000066400000000000000000000055741436515316400217670ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2019, by Samuel G. D. Williams. # # 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. require 'async/io/socket' require 'async/io/stream' require_relative 'writable' module Async module HTTP module Body class Pipe # If the input stream is closed first, it's likely the output stream will also be closed. def initialize(input, output = Writable.new, task: Task.current) @input = input @output = output head, tail = IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) @head = IO::Stream.new(head) @tail = tail @reader = nil @writer = nil task.async(transient: true, &self.method(:reader)) task.async(transient: true, &self.method(:writer)) end def to_io @tail end def close @reader&.stop @writer&.stop @tail.close end private # Read from the @input stream and write to the head of the pipe. def reader(task) @reader = task task.annotate "#{self.class} reader." while chunk = @input.read @head.write(chunk) @head.flush end @head.close_write ensure @input.close($!) close_head if @writer&.finished? end # Read from the head of the pipe and write to the @output stream. # If the @tail is closed, this will cause chunk to be nil, which in turn will call `@output.close` and `@head.close` def writer(task) @writer = task task.annotate "#{self.class} writer." while chunk = @head.read_partial @output.write(chunk) end ensure @output.close($!) close_head if @reader&.finished? end def close_head @head.close # Both tasks are done, don't keep references: @reader = nil @writer = nil end end end end end ruby-async-http-0.59.5/lib/async/http/body/slowloris.rb000066400000000000000000000054051436515316400230600ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'writable' require 'async/clock' module Async module HTTP module Body # A dynamic body which you can write to and read from. class Slowloris < Writable class ThroughputError < StandardError def initialize(throughput, minimum_throughput, time_since_last_write) super("Slow write: #{throughput.round(1)}bytes/s less than required #{minimum_throughput.round}bytes/s.") end end # In order for this implementation to work correctly, you need to use a LimitedQueue. # @param minimum_throughput [Integer] the minimum bytes per second otherwise this body will be forcefully closed. def initialize(*arguments, minimum_throughput: 1024, **options) super(*arguments, **options) @minimum_throughput = minimum_throughput @last_write_at = nil @last_chunk_size = nil end attr :minimum_throughput # If #read is called regularly to maintain throughput, that is good. If #read is not called, that is a problem. Throughput is dependent on data being available, from #write, so it doesn't seem particularly problimatic to do this check in #write. def write(chunk) if @last_chunk_size time_since_last_write = Async::Clock.now - @last_write_at throughput = @last_chunk_size / time_since_last_write if throughput < @minimum_throughput error = ThroughputError.new(throughput, @minimum_throughput, time_since_last_write) self.close(error) end end super.tap do @last_write_at = Async::Clock.now @last_chunk_size = chunk&.bytesize end end end end end end ruby-async-http-0.59.5/lib/async/http/body/writable.rb000066400000000000000000000061261436515316400226350ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/readable' require 'async/queue' module Async module HTTP module Body include ::Protocol::HTTP::Body # A dynamic body which you can write to and read from. class Writable < Readable class Closed < StandardError end # @param [Integer] length The length of the response body if known. # @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming. def initialize(length = nil, queue: Async::Queue.new) @queue = queue @length = length @count = 0 @finished = false @closed = false @error = nil end def length @length end # Stop generating output; cause the next call to write to fail with the given error. def close(error = nil) unless @closed @queue.enqueue(nil) @closed = true @error = error end super end def closed? @closed end def ready? !@queue.empty? end # Has the producer called #finish and has the reader consumed the nil token? def empty? @finished end # Read the next available chunk. def read return if @finished unless chunk = @queue.dequeue @finished = true end return chunk end # Write a single chunk to the body. Signal completion by calling `#finish`. def write(chunk) # If the reader breaks, the writer will break. # The inverse of this is less obvious (*) if @closed raise(@error || Closed) end @count += 1 @queue.enqueue(chunk) end alias << write def inspect "\#<#{self.class} #{@count} chunks written, #{status}>" end private def status if @finished 'finished' elsif @closed 'closing' else 'waiting' end end end end end end ruby-async-http-0.59.5/lib/async/http/client.rb000077500000000000000000000141561436515316400213520ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'async/io/endpoint' require 'async/io/stream' require 'async/pool/controller' require 'protocol/http/body/completable' require 'protocol/http/methods' require 'traces/provider' require_relative 'protocol' module Async module HTTP DEFAULT_RETRIES = 3 DEFAULT_CONNECTION_LIMIT = nil class Client < ::Protocol::HTTP::Methods # Provides a robust interface to a server. # * If there are no connections, it will create one. # * If there are already connections, it will reuse it. # * If a request fails, it will retry it up to N times if it was idempotent. # The client object will never become unusable. It internally manages persistent connections (or non-persistent connections if that's required). # @param endpoint [Endpoint] the endpoint to connnect to. # @param protocol [Protocol::HTTP1 | Protocol::HTTP2 | Protocol::HTTPS] the protocol to use. # @param scheme [String] The default scheme to set to requests. # @param authority [String] The default authority to set to requests. def initialize(endpoint, protocol: endpoint.protocol, scheme: endpoint.scheme, authority: endpoint.authority, retries: DEFAULT_RETRIES, connection_limit: DEFAULT_CONNECTION_LIMIT) @endpoint = endpoint @protocol = protocol @retries = retries @pool = make_pool(connection_limit) @scheme = scheme @authority = authority end attr :endpoint attr :protocol attr :retries attr :pool attr :scheme attr :authority def secure? @endpoint.secure? end def self.open(*arguments, **options, &block) client = self.new(*arguments, **options) return client unless block_given? begin yield client ensure client.close end end def close while @pool.busy? Console.logger.warn(self) {"Waiting for #{@protocol} pool to drain: #{@pool}"} @pool.wait end @pool.close end def call(request) request.scheme ||= self.scheme request.authority ||= self.authority attempt = 0 # We may retry the request if it is possible to do so. https://tools.ietf.org/html/draft-nottingham-httpbis-retry-01 is a good guide for how retrying requests should work. begin attempt += 1 # As we cache pool, it's possible these pool go bad (e.g. closed by remote host). In this case, we need to try again. It's up to the caller to impose a timeout on this. If this is the last attempt, we force a new connection. connection = @pool.acquire response = make_response(request, connection) # This signals that the ensure block below should not try to release the connection, because it's bound into the response which will be returned: connection = nil return response rescue Protocol::RequestFailed # This is a specific case where the entire request wasn't sent before a failure occurred. So, we can even resend non-idempotent requests. if connection @pool.release(connection) connection = nil end if attempt < @retries retry else raise end rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE if connection @pool.release(connection) connection = nil end if request.idempotent? and attempt < @retries retry else raise end ensure @pool.release(connection) if connection end end def inspect "#<#{self.class} authority=#{@authority.inspect}>" end Traces::Provider(self) do def call(request) attributes = { 'http.method': request.method, 'http.authority': request.authority || self.authority, 'http.scheme': request.scheme || self.scheme, 'http.path': request.path, } if protocol = request.protocol attributes['http.protocol'] = protocol end if length = request.body&.length attributes['http.request.length'] = length end trace('async.http.client.call', attributes: attributes) do |span| if context = self.trace_context request.headers['traceparent'] = context.to_s # request.headers['tracestate'] = context.state end super.tap do |response| if status = response&.status span['http.status_code'] = status end if length = response.body&.length span['http.response.length'] = length end end end end end protected def make_response(request, connection) response = request.call(connection) # The connection won't be released until the body is completely read/released. ::Protocol::HTTP::Body::Completable.wrap(response) do @pool.release(connection) end return response end def make_pool(connection_limit) Async::Pool::Controller.wrap(limit: connection_limit) do Console.logger.debug(self) {"Making connection to #{@endpoint.inspect}"} @protocol.client(@endpoint.connect) end end end end end ruby-async-http-0.59.5/lib/async/http/endpoint.rb000066400000000000000000000135331436515316400217070ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/io/host_endpoint' require 'async/io/ssl_endpoint' require 'async/io/ssl_socket' require_relative 'protocol/http1' require_relative 'protocol/https' module Async module HTTP # Represents a way to connect to a remote HTTP server. class Endpoint < Async::IO::Endpoint def self.parse(string, endpoint = nil, **options) url = URI.parse(string).normalize return self.new(url, endpoint, **options) end # Construct an endpoint with a specified scheme, hostname, optional path, and options. def self.for(scheme, hostname, path = "/", **options) # TODO: Consider using URI.for once it becomes available: uri_klass = URI.scheme_list[scheme.upcase] || URI::HTTP self.new( uri_klass.new(scheme, nil, hostname, nil, nil, path, nil, nil, nil).normalize, **options ) end # @option scheme [String] the scheme to use, overrides the URL scheme. # @option hostname [String] the hostname to connect to (or bind to), overrides the URL hostname (used for SNI). # @option port [Integer] the port to bind to, overrides the URL port. # @option ssl_context [OpenSSL::SSL::SSLContext] the context to use for TLS. # @option alpn_protocols [Array] the alpn protocols to negotiate. def initialize(url, endpoint = nil, **options) super(**options) raise ArgumentError, "URL must be absolute (include scheme, host): #{url}" unless url.absolute? @url = url if endpoint @endpoint = self.build_endpoint(endpoint) else @endpoint = nil end end def to_url url = @url.dup unless default_port? url.port = self.port end return url end def to_s "\#<#{self.class} #{self.to_url} #{@options}>" end def inspect "\#<#{self.class} #{self.to_url} #{@options.inspect}>" end attr :url def address endpoint.address end def secure? ['https', 'wss'].include?(self.scheme) end def protocol @options.fetch(:protocol) do if secure? Protocol::HTTPS else Protocol::HTTP1 end end end def default_port secure? ? 443 : 80 end def default_port? port == default_port end def port @options[:port] || @url.port || default_port end # The hostname is the server we are connecting to: def hostname @options[:hostname] || @url.hostname end def scheme @options[:scheme] || @url.scheme end def authority(ignore_default_port = true) if ignore_default_port and default_port? @url.hostname else "#{@url.hostname}:#{port}" end end # Return the path and query components of the given URL. def path buffer = @url.path || "/" if query = @url.query buffer = "#{buffer}?#{query}" end return buffer end def alpn_protocols @options.fetch(:alpn_protocols) {self.protocol.names} end def localhost? @url.hostname =~ /^(.*?\.)?localhost\.?$/ end # We don't try to validate peer certificates when talking to localhost because they would always be self-signed. def ssl_verify_mode if self.localhost? OpenSSL::SSL::VERIFY_NONE else OpenSSL::SSL::VERIFY_PEER end end def ssl_context @options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context| if alpn_protocols = self.alpn_protocols context.alpn_protocols = alpn_protocols end context.set_params( verify_mode: self.ssl_verify_mode ) end end def build_endpoint(endpoint = nil) endpoint ||= tcp_endpoint if secure? # Wrap it in SSL: return Async::IO::SSLEndpoint.new(endpoint, ssl_context: self.ssl_context, hostname: @url.hostname, timeout: self.timeout, ) end return endpoint end def endpoint @endpoint ||= build_endpoint end def bind(*arguments, &block) endpoint.bind(*arguments, &block) end def connect(&block) endpoint.connect(&block) end def each return to_enum unless block_given? self.tcp_endpoint.each do |endpoint| yield self.class.new(@url, endpoint, **@options) end end def key [@url, @options] end def eql? other self.key.eql? other.key end def hash self.key.hash end protected def tcp_options options = @options.dup options.delete(:scheme) options.delete(:port) options.delete(:hostname) options.delete(:ssl_context) options.delete(:alpn_protocols) options.delete(:protocol) return options end def tcp_endpoint Async::IO::Endpoint.tcp(self.hostname, port, **tcp_options) end end end end ruby-async-http-0.59.5/lib/async/http/internet.rb000066400000000000000000000063461436515316400217230ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'client' require_relative 'endpoint' require 'protocol/http/middleware' require 'protocol/http/body/buffered' require 'protocol/http/accept_encoding' module Async module HTTP class Internet def initialize(**options) @clients = Hash.new @options = options end # A cache of clients. # @attribute [Hash(URI, Client)] attr :clients def client_for(endpoint) key = host_key(endpoint) @clients.fetch(key) do @clients[key] = self.make_client(endpoint) end end # Make a request to the internet with the given `method` and `url`. # # If you provide non-frozen headers, they may be mutated. # # @parameter method [String] The request method, e.g. `GET`. # @parameter url [String] The URL to request, e.g. `https://www.codeotaku.com`. # @parameter headers [Hash | Protocol::HTTP::Headers] The headers to send with the request. # @parameter body [String | Protocol::HTTP::Body] The body to send with the request. def call(method, url, headers = nil, body = nil) endpoint = Endpoint.parse(url) client = self.client_for(endpoint) body = Body::Buffered.wrap(body) headers = ::Protocol::HTTP::Headers[headers] request = ::Protocol::HTTP::Request.new(endpoint.scheme, endpoint.authority, method, endpoint.path, nil, headers, body) return client.call(request) end def close # The order of operations here is to avoid a race condition between iterating over clients (#close may yield) and creating new clients. clients = @clients.values @clients.clear clients.each(&:close) end ::Protocol::HTTP::Methods.each do |name, verb| define_method(verb.downcase) do |url, headers = nil, body = nil| self.call(verb, url.to_str, headers, body) end end protected def make_client(endpoint) ::Protocol::HTTP::AcceptEncoding.new( Client.new(endpoint, **@options) ) end def host_key(endpoint) url = endpoint.url.dup url.path = "" url.fragment = nil url.query = nil return url end end end end ruby-async-http-0.59.5/lib/async/http/internet/000077500000000000000000000000001436515316400213655ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/internet/instance.rb000066400000000000000000000025171436515316400235230ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../internet' require 'thread/local' module Async module HTTP class Internet # Provide access to a shared thread-local instance. extend ::Thread::Local end end end ruby-async-http-0.59.5/lib/async/http/protocol.rb000066400000000000000000000035311436515316400217250ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'protocol/http1' require_relative 'protocol/https' module Async module HTTP # A protocol specifies a way in which to communicate with a remote peer. module Protocol # A protocol must implement the following interface: # class Protocol # def client(stream) -> Connection # def server(stream) -> Connection # end # A connection must implement the following interface: # class Connection # def concurrency -> can invoke call 1 or more times simultaneously. # def reusable? -> can be used again/persistent connection. # def viable? -> Boolean # def call(request) -> Response # def each -> (yield(request) -> Response) # end end end end ruby-async-http-0.59.5/lib/async/http/protocol/000077500000000000000000000000001436515316400213765ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/protocol/http1.rb000066400000000000000000000033551436515316400227710ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'http1/client' require_relative 'http1/server' module Async module HTTP module Protocol module HTTP1 VERSION = "HTTP/1.1" def self.bidirectional? true end def self.trailer? true end def self.client(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Server.new(stream, VERSION) end def self.names ["http/1.1", "http/1.0"] end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http1/000077500000000000000000000000001436515316400224365ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/protocol/http1/client.rb000066400000000000000000000072051436515316400242450ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'connection' module Async module HTTP module Protocol module HTTP1 class Client < Connection # Used by the client to send requests to the remote server. def call(request, task: Task.current) # We need to keep track of connections which are not in the initial "ready" state. @ready = false Console.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"} # Mark the start of the trailers: trailer = request.headers.trailer! # We carefully interpret https://tools.ietf.org/html/rfc7230#section-6.3.1 to implement this correctly. begin write_request(request.authority, request.method, request.path, @version, request.headers) rescue # If we fail to fully write the request and body, we can retry this request. raise RequestFailed end if request.body? body = request.body if protocol = request.protocol # This is a very tricky apect of handling HTTP/1 upgrade connections. In theory, this approach is a bit inefficient, because we spin up a task just to handle writing to the underlying stream when we could be writing to the stream directly. But we need to maintain some level of compatibility with HTTP/2. Additionally, we don't know if the upgrade request will be accepted, so starting to write the body at this point needs to be handled with care. task.async do |subtask| subtask.annotate("Upgrading request.") # If this fails, this connection will be closed. write_upgrade_body(protocol, body) end elsif request.connect? task.async do |subtask| subtask.annotate("Tunnelling body.") write_tunnel_body(@version, body) end else task.async do |subtask| subtask.annotate("Streaming body.") # Once we start writing the body, we can't recover if the request fails. That's because the body might be generated dynamically, streaming, etc. write_body(@version, body, false, trailer) end end elsif protocol = request.protocol write_upgrade_body(protocol) else write_body(@version, body, false, trailer) end response = Response.read(self, request) @ready = true return response rescue # This will ensure that #reusable? returns false. @stream.close raise end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http1/connection.rb000077500000000000000000000041051436515316400251250ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'protocol/http1' require_relative 'request' require_relative 'response' module Async module HTTP module Protocol module HTTP1 class Connection < ::Protocol::HTTP1::Connection def initialize(stream, version) super(stream) @ready = true @version = version end attr :version def http1? true end def http2? false end def read_line? @stream.read_until(CRLF) end def read_line @stream.read_until(CRLF) or raise EOFError, "Could not read line!" end def peer @stream.io end attr :count def concurrency 1 end # Can we use this connection to make requests? def viable? @ready && @stream&.connected? end def reusable? @ready && @persistent && @stream && !@stream.closed? end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http1/request.rb000066400000000000000000000041311436515316400244520ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../request' module Async module HTTP module Protocol module HTTP1 class Request < Protocol::Request def self.read(connection) if parts = connection.read_request self.new(connection, *parts) end end UPGRADE = 'upgrade' def initialize(connection, authority, method, path, version, headers, body) @connection = connection # HTTP/1 requests with an upgrade header (which can contain zero or more values) are extracted into the protocol field of the request, and we expect a response to select one of those protocols with a status code of 101 Switching Protocols. protocol = headers.delete('upgrade') super(nil, authority, method, path, version, headers, body, protocol) end def connection @connection end def hijack? true end def hijack! @connection.hijack! end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http1/response.rb000066400000000000000000000036471436515316400246330ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../response' module Async module HTTP module Protocol module HTTP1 class Response < Protocol::Response def self.read(connection, request) if parts = connection.read_response(request.method) self.new(connection, *parts) end end UPGRADE = 'upgrade' # @param reason [String] HTTP response line reason, ignored. def initialize(connection, version, status, reason, headers, body) @connection = connection protocol = headers.delete(UPGRADE) super(version, status, headers, body, protocol) end def connection @connection end def hijack? @body.nil? end def hijack! @connection.hijack! end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http1/server.rb000066400000000000000000000105331436515316400242730ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'connection' module Async module HTTP module Protocol module HTTP1 class Server < Connection def fail_request(status) @persistent = false write_response(@version, status, {}, nil) end def next_request # The default is true. return unless @persistent # Read an incoming request: return unless request = Request.read(self) unless persistent?(request.version, request.method, request.headers) @persistent = false end return request rescue Async::TimeoutError # For an interesting discussion about this behaviour, see https://trac.nginx.org/nginx/ticket/1005 # If you enable this, you will see some spec failures... # fail_request(408) raise rescue fail_request(400) raise end # Server loop. def each(task: Task.current) task.annotate("Reading #{self.version} requests for #{self.class}.") while request = next_request response = yield(request, self) body = response&.body if @stream.nil? and body.nil? # Full hijack. return end begin # If a response was generated, send it: if response trailer = response.headers.trailer! write_response(@version, response.status, response.headers) # Some operations in this method are long running, that is, it's expected that `body.call(stream)` could literally run indefinitely. In order to facilitate garbage collection, we want to nullify as many local variables before calling the streaming body. This ensures that the garbage collection can clean up as much state as possible during the long running operation, so we don't retain objects that are no longer needed. if body and protocol = response.protocol stream = write_upgrade_body(protocol) # At this point, the request body is hijacked, so we don't want to call #finish below. request = response = nil body.call(stream) elsif request.connect? and response.success? stream = write_tunnel_body(request.version) # Same as above: request = response = nil body.call(stream) else head = request.head? version = request.version # Same as above: request = nil unless body response = nil write_body(version, body, head, trailer) end # We are done with the body, you shouldn't need to call close on it: body = nil else # If the request failed to generate a response, it was an internal server error: write_response(@version, 500, {}) write_body(request.version, nil) end # Gracefully finish reading the request body if it was not already done so. request&.finish # This ensures we yield at least once every iteration of the loop and allow other fibers to execute. task.yield rescue => error raise ensure body&.close(error) end end end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http10.rb000077500000000000000000000032751436515316400230550ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'http1' module Async module HTTP module Protocol module HTTP10 VERSION = "HTTP/1.0" def self.bidirectional? false end def self.trailer? false end def self.client(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Server.new(stream, VERSION) end def self.names ["http/1.0"] end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http11.rb000066400000000000000000000032731436515316400230510ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'http1' module Async module HTTP module Protocol module HTTP11 VERSION = "HTTP/1.1" def self.bidirectional? true end def self.trailer? true end def self.client(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) stream = IO::Stream.new(peer, sync: true) return HTTP1::Server.new(stream, VERSION) end def self.names ["http/1.1"] end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2.rb000066400000000000000000000050671436515316400227740ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'http2/client' require_relative 'http2/server' module Async module HTTP module Protocol module HTTP2 VERSION = "HTTP/2" def self.bidirectional? true end def self.trailer? true end CLIENT_SETTINGS = { ::Protocol::HTTP2::Settings::ENABLE_PUSH => 0, ::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000, ::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000, } SERVER_SETTINGS = { # We choose a lower maximum concurrent streams to avoid overloading a single connection/thread. ::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 128, ::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000, ::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000, ::Protocol::HTTP2::Settings::ENABLE_CONNECT_PROTOCOL => 1, } def self.client(peer, settings = CLIENT_SETTINGS) stream = IO::Stream.new(peer, sync: true) client = Client.new(stream) client.send_connection_preface(settings) client.start_connection return client end def self.server(peer, settings = SERVER_SETTINGS) stream = IO::Stream.new(peer, sync: true) server = Server.new(stream) server.read_connection_preface(settings) server.start_connection return server end def self.names ["h2"] end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/000077500000000000000000000000001436515316400224375ustar00rootroot00000000000000ruby-async-http-0.59.5/lib/async/http/protocol/http2/client.rb000066400000000000000000000037251436515316400242510ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'connection' require_relative 'response' require 'protocol/http2/client' module Async module HTTP module Protocol module HTTP2 class Client < ::Protocol::HTTP2::Client include Connection def initialize(stream) @stream = stream framer = ::Protocol::HTTP2::Framer.new(@stream) super(framer) end def create_response Response::Stream.create(self, self.next_stream_id).response end # Used by the client to send requests to the remote server. def call(request) raise ::Protocol::HTTP2::Error, "Connection closed!" if self.closed? @count += 1 response = create_response response.send_request(request) response.wait return response end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/connection.rb000066400000000000000000000076341436515316400251350ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'stream' require 'async/semaphore' module Async module HTTP module Protocol module HTTP2 HTTPS = 'https'.freeze SCHEME = ':scheme'.freeze METHOD = ':method'.freeze PATH = ':path'.freeze AUTHORITY = ':authority'.freeze STATUS = ':status'.freeze PROTOCOL = ':protocol'.freeze CONTENT_LENGTH = 'content-length'.freeze CONNECTION = 'connection'.freeze TRAILER = 'trailer'.freeze module Connection def initialize(*) super @count = 0 @reader = nil # Writing multiple frames at the same time can cause odd problems if frames are only partially written. So we use a semaphore to ensure frames are written in their entirety. @write_frame_guard = Async::Semaphore.new(1) end def to_s "\#<#{self.class} #{@streams.count} active streams>" end attr :stream def http1? false end def http2? true end def start_connection @reader || read_in_background end def close(error = nil) @reader = nil super end def write_frame(frame) # We don't want to write multiple frames at the same time. @write_frame_guard.acquire do super end @stream.flush end def write_frames(&block) @write_frame_guard.acquire do super end @stream.flush end def read_in_background(parent: Task.current) raise RuntimeError, "Connection is closed!" if closed? parent.async(transient: true) do |task| @reader = task task.annotate("#{version} reading data for #{self.class}.") begin while !self.closed? self.consume_window self.read_frame end rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE, Async::Wrapper::Cancelled # Ignore. rescue ::Protocol::HTTP2::GoawayError => error # Error is raised if a response is actively reading from the # connection. The connection is silently closed if GOAWAY is # received outside the request/response cycle. if @reader self.close(error) end ensure # Don't call #close twice. if @reader self.close($!) end end end end attr :promises def peer @stream.io end attr :count def concurrency self.maximum_concurrent_streams end # Can we use this connection to make requests? def viable? @stream.connected? end def reusable? !self.closed? end def version VERSION end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/input.rb000066400000000000000000000041741436515316400241310ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../../body/writable' module Async module HTTP module Protocol module HTTP2 # A writable body which requests window updates when data is read from it. class Input < Body::Writable def initialize(stream, length) super(length) @stream = stream @remaining = length end def read if chunk = super # If we read a chunk fron the stream, we want to extend the window if required so more data will be provided. @stream.request_window_update end # We track the expected length and check we got what we were expecting. if @remaining if chunk @remaining -= chunk.bytesize elsif @remaining > 0 raise EOFError, "Expected #{self.length} bytes, #{@remaining} bytes short!" elsif @remaining < 0 raise EOFError, "Expected #{self.length} bytes, #{@remaining} bytes over!" end end return chunk end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/output.rb000066400000000000000000000100021436515316400243150ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/stream' module Async module HTTP module Protocol module HTTP2 class Output def initialize(stream, body, trailer = nil) @stream = stream @body = body @trailer = trailer @task = nil @window_updated = Async::Condition.new end attr :trailer def start(parent: Task.current) raise "Task already started!" if @task if @body.stream? @task = parent.async(&self.method(:stream)) else @task = parent.async(&self.method(:passthrough)) end end def window_updated(size) @window_updated.signal end def write(chunk) until chunk.empty? maximum_size = @stream.available_frame_size while maximum_size <= 0 @window_updated.wait maximum_size = @stream.available_frame_size end break unless chunk = send_data(chunk, maximum_size) end end # This method should only be called from within the context of the output task. def close(error = nil) if @stream @stream.finish_output(error) @stream = nil end end # This method should only be called from within the context of the HTTP/2 stream. def stop(error) @task&.stop @task = nil end private def stream(task) task.annotate("Streaming #{@body} to #{@stream}.") input = @stream.wait_for_input @body.call(::Protocol::HTTP::Body::Stream.new(input, self)) rescue Async::Stop # Ignore. end # Reads chunks from the given body and writes them to the stream as fast as possible. def passthrough(task) task.annotate("Writing #{@body} to #{@stream}.") while chunk = @body&.read self.write(chunk) # TODO this reduces memory usage? # chunk.clear unless chunk.frozen? # GC.start end self.close ensure @body&.close($!) @body = nil end # Send `maximum_size` bytes of data using the specified `stream`. If the buffer has no more chunks, `END_STREAM` will be sent on the final chunk. # @param maximum_size [Integer] send up to this many bytes of data. # @param stream [Stream] the stream to use for sending data frames. # @return [String, nil] any data that could not be written. def send_data(chunk, maximum_size) if chunk.bytesize <= maximum_size @stream.send_data(chunk, maximum_size: maximum_size) else @stream.send_data(chunk.byteslice(0, maximum_size), maximum_size: maximum_size) # The window was not big enough to send all the data, so we save it for next time: return chunk.byteslice(maximum_size, chunk.bytesize - maximum_size) end return nil end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/request.rb000066400000000000000000000123431436515316400244570ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../request' require_relative 'stream' module Async module HTTP module Protocol module HTTP2 # Typically used on the server side to represent an incoming request, and write the response. class Request < Protocol::Request class Stream < HTTP2::Stream def initialize(*) super @enqueued = false @request = Request.new(self) end attr :request def receive_initial_headers(headers, end_stream) headers.each do |key, value| if key == SCHEME raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if @request.scheme @request.scheme = value elsif key == AUTHORITY raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if @request.authority @request.authority = value elsif key == METHOD raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if @request.method @request.method = value elsif key == PATH raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty? raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if @request.path @request.path = value elsif key == PROTOCOL raise ::Protocol::HTTP2::HeaderError, "Request protocol already specified!" if @request.protocol @request.protocol = value elsif key == CONTENT_LENGTH raise ::Protocol::HTTP2::HeaderError, "Request content length already specified!" if @length @length = Integer(value) elsif key == CONNECTION raise ::Protocol::HTTP2::HeaderError, "Connection header is not allowed!" elsif key.start_with? ':' raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!" elsif key =~ /[A-Z]/ raise ::Protocol::HTTP2::HeaderError, "Invalid characters in header #{key}!" else add_header(key, value) end end @request.headers = @headers unless @request.valid? raise ::Protocol::HTTP2::HeaderError, "Request is missing required headers!" else # We only construct the input/body if data is coming. unless end_stream @request.body = prepare_input(@length) end # We are ready for processing: @connection.requests.enqueue(@request) end return headers end def closed(error) @request = nil super end end def initialize(stream) super(nil, nil, nil, nil, VERSION, nil) @stream = stream end attr :stream def connection @stream.connection end def valid? @scheme and @method and @path end def hijack? false end NO_RESPONSE = [ [STATUS, '500'], ] def send_response(response) if response.nil? return @stream.send_headers(nil, NO_RESPONSE, ::Protocol::HTTP2::END_STREAM) end protocol_headers = [ [STATUS, response.status], ] if protocol = response.protocol protocol_headers << [PROTOCOL, protocol] end if length = response.body&.length protocol_headers << [CONTENT_LENGTH, length] end headers = ::Protocol::HTTP::Headers::Merged.new(protocol_headers, response.headers) if body = response.body and !self.head? # This function informs the headers object that any subsequent headers are going to be trailer. Therefore, it must be called *before* sending the headers, to avoid any race conditions. trailer = response.headers.trailer! @stream.send_headers(nil, headers) @stream.send_body(body, trailer) else # Ensure the response body is closed if we are ending the stream: response.close @stream.send_headers(nil, headers, ::Protocol::HTTP2::END_STREAM) end end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/response.rb000066400000000000000000000162251436515316400246300ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../response' require_relative 'stream' module Async module HTTP module Protocol module HTTP2 # Typically used on the client side for writing a request and reading the incoming response. class Response < Protocol::Response class Stream < HTTP2::Stream def initialize(*) super @response = Response.new(self) @notification = Async::Notification.new @exception = nil end attr :response def wait_for_input # The input isn't ready until the response headers have been received: @response.wait # There is a possible race condition if you try to access @input - it might already be closed and nil. return @response.body end def accept_push_promise_stream(promised_stream_id, headers) raise ProtocolError, "Cannot accept push promise stream!" end # This should be invoked from the background reader, and notifies the task waiting for the headers that we are done. def receive_initial_headers(headers, end_stream) headers.each do |key, value| if key == STATUS @response.status = Integer(value) elsif key == PROTOCOL @response.protocol = value elsif key == CONTENT_LENGTH @length = Integer(value) else add_header(key, value) end end @response.headers = @headers if @response.valid? if !end_stream # We only construct the input/body if data is coming. @response.body = prepare_input(@length) elsif @response.head? @response.body = ::Protocol::HTTP::Body::Head.new(@length) end else send_reset_stream(::Protocol::HTTP2::Error::PROTOCOL_ERROR) end self.notify! return headers end # Notify anyone waiting on the response headers to be received (or failure). def notify! if notification = @notification @notification = nil notification.signal end end # Wait for the headers to be received or for stream reset. def wait # If you call wait after the headers were already received, it should return immediately: @notification&.wait if @exception raise @exception end end def closed(error) super if @response @response = nil end @exception = error notify! end end def initialize(stream) super(stream.connection.version, nil, nil) @stream = stream @request = nil end attr :stream attr :request def connection @stream.connection end def wait @stream.wait end def head? @request&.head? end def valid? !!@status end def build_request(headers) request = ::Protocol::HTTP::Request.new request.headers = ::Protocol::HTTP::Headers.new headers.each do |key, value| if key == SCHEME raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if request.scheme request.scheme = value elsif key == AUTHORITY raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if request.authority request.authority = value elsif key == METHOD raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if request.method request.method = value elsif key == PATH raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty? raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if request.path request.path = value elsif key.start_with? ':' raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!" else request.headers[key] = value end end @request = request end # Send a request and read it into this response. def send_request(request) @request = request # https://http2.github.io/http2-spec/#rfc.section.8.1.2.3 # All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6). pseudo_headers = [ [SCHEME, request.scheme], [METHOD, request.method], [PATH, request.path], ] # To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form (see [RFC7230], Section 5.3). Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field. if authority = request.authority pseudo_headers << [AUTHORITY, authority] end if protocol = request.protocol pseudo_headers << [PROTOCOL, protocol] end headers = ::Protocol::HTTP::Headers::Merged.new( pseudo_headers, request.headers ) if request.body.nil? @stream.send_headers(nil, headers, ::Protocol::HTTP2::END_STREAM) else if length = request.body.length # This puts it at the end of the pseudo-headers: pseudo_headers << [CONTENT_LENGTH, length] end # This function informs the headers object that any subsequent headers are going to be trailer. Therefore, it must be called *before* sending the headers, to avoid any race conditions. trailer = request.headers.trailer! begin @stream.send_headers(nil, headers) rescue raise RequestFailed end @stream.send_body(request.body, trailer) end end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/server.rb000066400000000000000000000053301436515316400242730ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'connection' require_relative 'request' require 'protocol/http2/server' module Async module HTTP module Protocol module HTTP2 class Server < ::Protocol::HTTP2::Server include Connection def initialize(stream) # Used by some generic methods in Connetion: @stream = stream framer = ::Protocol::HTTP2::Framer.new(stream) super(framer) @requests = Async::Queue.new end attr :requests def accept_stream(stream_id) super do Request::Stream.create(self, stream_id) end end def close(error = nil) if @requests # Stop the request loop: @requests.enqueue(nil) @requests = nil end super end def each(task: Task.current) task.annotate("Reading #{version} requests for #{self.class}.") # It's possible the connection has died before we get here... @requests&.async do |task, request| task.annotate("Incoming request: #{request.method} #{request.path.inspect}.") @count += 1 begin response = yield(request) rescue # We need to close the stream if the user code blows up while generating a response: request.stream.send_reset_stream(::Protocol::HTTP2::INTERNAL_ERROR) raise else request.send_response(response) end end # Maybe we should add some synchronisation here - i.e. only exit once all requests are finished. end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/http2/stream.rb000066400000000000000000000124571436515316400242700ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http2/stream' require_relative 'input' require_relative 'output' module Async module HTTP module Protocol module HTTP2 class Stream < ::Protocol::HTTP2::Stream def initialize(*) super @headers = nil # Input buffer, reading request body, or response body (receive_data): @length = nil @input = nil # Output buffer, writing request body or response body (window_updated): @output = nil end attr_accessor :headers attr :input def add_header(key, value) if key == CONNECTION raise ::Protocol::HTTP2::HeaderError, "Connection header is not allowed!" elsif key.start_with? ':' raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!" elsif key =~ /[A-Z]/ raise ::Protocol::HTTP2::HeaderError, "Invalid upper-case characters in header #{key}!" else @headers.add(key, value) end end def receive_trailing_headers(headers, end_stream) headers.each do |key, value| add_header(key, value) end end def process_headers(frame) if @headers.nil? @headers = ::Protocol::HTTP::Headers.new self.receive_initial_headers(super, frame.end_stream?) elsif frame.end_stream? self.receive_trailing_headers(super, frame.end_stream?) else raise ::Protocol::HTTP2::HeaderError, "Unable to process headers!" end # TODO this might need to be in an ensure block: if @input and frame.end_stream? @input.close($!) @input = nil end rescue ::Protocol::HTTP2::HeaderError => error Console.logger.error(self, error) send_reset_stream(error.code) end def wait_for_input return @input end # Prepare the input stream which will be used for incoming data frames. # @return [Input] the input body. def prepare_input(length) if @input.nil? @input = Input.new(self, length) else raise ArgumentError, "Input body already prepared!" end end def update_local_window(frame) consume_local_window(frame) # This is done on demand in `Input#read`: # request_window_update end def process_data(frame) data = frame.unpack if @input unless data.empty? @input.write(data) end if frame.end_stream? @input.close @input = nil end end return data rescue ::Protocol::HTTP2::ProtocolError raise rescue # Anything else... send_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR) end # Set the body and begin sending it. def send_body(body, trailer = nil) @output = Output.new(self, body, trailer) @output.start end # Called when the output terminates normally. def finish_output(error = nil) trailer = @output&.trailer @output = nil if error send_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR) else # Write trailer? if trailer&.any? send_headers(nil, trailer, ::Protocol::HTTP2::END_STREAM) else send_data(nil, ::Protocol::HTTP2::END_STREAM) end end end def window_updated(size) super @output&.window_updated(size) end # When the stream transitions to the closed state, this method is called. There are roughly two ways this can happen: # - A frame is received which causes this stream to enter the closed state. This method will be invoked from the background reader task. # - A frame is sent which causes this stream to enter the closed state. This method will be invoked from that task. # While the input stream is relatively straight forward, the output stream can trigger the second case above def closed(error) super if @input @input.close(error) @input = nil end if @output @output.stop(error) @output = nil end return self end end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/https.rb000066400000000000000000000047401436515316400230720ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'http10' require_relative 'http11' require_relative 'http2' require 'openssl' unless OpenSSL::SSL::SSLContext.instance_methods.include? :alpn_protocols= warn "OpenSSL implementation doesn't support ALPN." class OpenSSL::SSL::SSLContext def alpn_protocols= names return names end end class OpenSSL::SSL::SSLSocket def alpn_protocol return nil end end end module Async module HTTP module Protocol # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request. module HTTPS HANDLERS = { "h2" => HTTP2, "http/1.1" => HTTP11, "http/1.0" => HTTP10, nil => HTTP11, } def self.protocol_for(peer) # alpn_protocol is only available if openssl v1.0.2+ name = peer.alpn_protocol Console.logger.debug(self) {"Negotiating protocol #{name.inspect}..."} if protocol = HANDLERS[name] return protocol else raise ArgumentError, "Could not determine protocol for connection (#{name.inspect})." end end def self.client(peer) protocol_for(peer).client(peer) end def self.server(peer) protocol_for(peer).server(peer) end # Supported Application Layer Protocol Negotiation names: def self.names HANDLERS.keys.compact end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/request.rb000066400000000000000000000035721436515316400234220ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'protocol/http/request' require 'protocol/http/headers' require_relative '../body/writable' module Async module HTTP module Protocol # Failed to send the request. The request body has NOT been consumed (i.e. #read) and you should retry the request. class RequestFailed < StandardError end # This is generated by server protocols. class Request < ::Protocol::HTTP::Request def connection nil end def hijack? false end def peer if connection = self.connection connection.peer end end def remote_address @remote_address ||= peer.remote_address end def remote_address= value @remote_address = value end end end end end ruby-async-http-0.59.5/lib/async/http/protocol/response.rb000066400000000000000000000032641436515316400235660ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'protocol/http/response' require_relative '../body/writable' module Async module HTTP module Protocol # This is generated by client protocols. class Response < ::Protocol::HTTP::Response def connection nil end def hijack? false end def peer if connection = self.connection connection.peer end end def remote_address @remote_address ||= peer.remote_address end def remote_address= value @remote_address = value end end end end end ruby-async-http-0.59.5/lib/async/http/proxy.rb000066400000000000000000000107631436515316400212520ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2019, by Samuel G. D. Williams. # # 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. require_relative 'client' require_relative 'endpoint' require_relative 'body/pipe' module Async module HTTP # Wraps a client, address and headers required to initiate a connectio to a remote host using the CONNECT verb. # Behaves like a TCP endpoint for the purposes of connecting to a remote host. class Proxy class ConnectFailure < StandardError def initialize(response) super "Failed to connect: #{response.status}" @response = response end attr :response end module Client def proxy(endpoint, headers = nil) Proxy.new(self, endpoint.authority(false), headers) end # Create a client that will proxy requests through the current client. def proxied_client(endpoint, headers = nil) proxy = self.proxy(endpoint, headers) return self.class.new(proxy.wrap_endpoint(endpoint)) end def proxied_endpoint(endpoint, headers = nil) proxy = self.proxy(endpoint, headers) return proxy.wrap_endpoint(endpoint) end end # Prepare and endpoint which can establish a TCP connection to the remote system. # @param client [Async::HTTP::Client] the client which will be used as a proxy server. # @param host [String] the hostname or address to connect to. # @param port [String] the port number to connect to. # @param headers [Array] an optional list of headers to use when establishing the connection. # @see Async::IO::Endpoint#tcp def self.tcp(client, host, port, headers = nil) self.new(client, "#{host}:#{port}", headers) end # Construct a endpoint that will use the given client as a proxy for HTTP requests. # @param client [Async::HTTP::Client] the client which will be used as a proxy server. # @param endpoint [Async::HTTP::Endpoint] the endpoint to connect to. # @param headers [Array] an optional list of headers to use when establishing the connection. def self.endpoint(client, endpoint, headers = nil) proxy = self.new(client, endpoint.authority(false), headers) return proxy.endpoint(endpoint.url) end # @param client [Async::HTTP::Client] the client which will be used as a proxy server. # @param address [String] the address to connect to. # @param headers [Array] an optional list of headers to use when establishing the connection. def initialize(client, address, headers = nil) @client = client @address = address @headers = ::Protocol::HTTP::Headers[headers].freeze end attr :client # Close the underlying client connection. def close @client.close end # Establish a TCP connection to the specified host. # @return [Socket] a connected bi-directional socket. def connect(&block) input = Body::Writable.new response = @client.connect(@address.to_s, @headers, input) if response.success? pipe = Body::Pipe.new(response.body, input) return pipe.to_io unless block_given? begin yield pipe.to_io ensure pipe.close end else # This ensures we don't leave a response dangling: response.close raise ConnectFailure, response end end # @return [Async::HTTP::Endpoint] an endpoint that connects via the specified proxy. def wrap_endpoint(endpoint) Endpoint.new(endpoint.url, self, **endpoint.options) end end Client.prepend(Proxy::Client) end end ruby-async-http-0.59.5/lib/async/http/reference.rb000066400000000000000000000023751436515316400220270ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'protocol/http/reference' module Async module HTTP Reference = ::Protocol::HTTP::Reference end end ruby-async-http-0.59.5/lib/async/http/relative_location.rb000066400000000000000000000113411436515316400235650ustar00rootroot00000000000000# Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'client' require_relative 'endpoint' require_relative 'reference' require 'protocol/http/middleware' require 'protocol/http/body/rewindable' module Async module HTTP class TooManyRedirects < StandardError end # A client wrapper which transparently handles both relative and absolute redirects to a given maximum number of hops. # # The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch). # # | Redirect using GET | Permanent | Temporary | # |:-----------------------------------------:|:---------:|:---------:| # | Allowed | 301 | 302 | # | Preserve original method | 308 | 307 | # # For the specific details of the redirect handling, see: # - 301 Moved Permanently. # - 302 Found. # - 307 Temporary Redirect. # class RelativeLocation < ::Protocol::HTTP::Middleware # Header keys which should be deleted when changing a request from a POST to a GET as defined by . PROHIBITED_GET_HEADERS = [ 'content-encoding', 'content-language', 'content-location', 'content-type', ] # maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects. def initialize(app, maximum_hops = 3) super(app) @maximum_hops = maximum_hops end # The maximum number of hops which will limit the number of redirects until an error is thrown. attr :maximum_hops def redirect_with_get?(request, response) # We only want to switch to GET if the request method is something other than get, e.g. POST. if request.method != GET # According to the RFC, we should only switch to GET if the response is a 301 or 302: return response.status == 301 || response.status == 302 end end def call(request) # We don't want to follow redirects for HEAD requests: return super if request.head? if body = request.body # We need to cache the body as it might be submitted multiple times if we get a response status of 307 or 308: body = ::Protocol::HTTP::Body::Rewindable.new(body) request.body = body end hops = 0 while hops <= @maximum_hops response = super(request) if response.redirection? hops += 1 # Get the redirect location: unless location = response.headers['location'] return response end response.finish uri = URI.parse(location) if uri.absolute? return response else request.path = Reference[request.path] + location end if request.method == GET or response.preserve_method? # We (might) need to rewind the body so that it can be submitted again: body&.rewind else # We are changing the method to GET: request.method = GET # Clear the request body: request.finish body = nil # Remove any headers which are not allowed in a GET request: PROHIBITED_GET_HEADERS.each do |header| request.headers.delete(header) end end else return response end end raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!" end end end end ruby-async-http-0.59.5/lib/async/http/server.rb000077500000000000000000000071111436515316400213730ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'async/io/endpoint' require 'async/io/stream' require 'protocol/http/middleware' require 'traces/provider' require_relative 'protocol' module Async module HTTP class Server < ::Protocol::HTTP::Middleware def self.for(*arguments, **options, &block) self.new(block, *arguments, **options) end def initialize(app, endpoint, protocol: endpoint.protocol, scheme: endpoint.scheme) super(app) @endpoint = endpoint @protocol = protocol @scheme = scheme end attr :endpoint attr :protocol attr :scheme def accept(peer, address, task: Task.current) connection = @protocol.server(peer) Console.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{@protocol}"} connection.each do |request| # We set the default scheme unless it was otherwise specified. # https://tools.ietf.org/html/rfc7230#section-5.5 request.scheme ||= self.scheme # This is a slight optimization to avoid having to get the address from the socket. request.remote_address = address # Console.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"} # If this returns nil, we assume that the connection has been hijacked. self.call(request) end ensure connection&.close end def run @endpoint.accept(&self.method(:accept)) end Traces::Provider(self) do def call(request) if trace_parent = request.headers['traceparent'] self.trace_context = Traces::Context.parse(trace_parent.join, request.headers['tracestate'], remote: true) end attributes = { 'http.method': request.method, 'http.authority': request.authority, 'http.scheme': request.scheme, 'http.path': request.path, 'http.user_agent': request.headers['user-agent'], } if length = request.body&.length attributes['http.request.length'] = length end if protocol = request.protocol attributes['http.protocol'] = protocol end trace('async.http.server.call', resource: "#{request.method} #{request.path}", attributes: attributes) do |span| super.tap do |response| if status = response&.status span['http.status_code'] = status end if length = response&.body&.length span['http.response.length'] = length end end end end end end end end ruby-async-http-0.59.5/lib/async/http/statistics.rb000066400000000000000000000062551436515316400222640ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'protocol/http/body/wrapper' require 'async/clock' module Async module HTTP class Statistics def self.start self.new(Clock.now) end def initialize(start_time) @start_time = start_time end def wrap(response, &block) if response and response.body response.body = Body::Statistics.new(@start_time, response.body, block) end return response end end module Body # Invokes a callback once the body has finished reading. class Statistics < ::Protocol::HTTP::Body::Wrapper def initialize(start_time, body, callback) super(body) @sent = 0 @start_time = start_time @first_chunk_time = nil @end_time = nil @callback = callback end attr :start_time attr :first_chunk_time attr :end_time attr :sent def total_duration if @end_time @end_time - @start_time end end def first_chunk_duration if @first_chunk_time @first_chunk_time - @start_time end end def close(error = nil) complete_statistics(error) super end def read chunk = super @first_chunk_time ||= Clock.now if chunk @sent += chunk.bytesize end return chunk end def to_s parts = ["sent #{@sent} bytes"] if duration = self.total_duration parts << "took #{format_duration(duration)} in total" end if duration = self.first_chunk_duration parts << "took #{format_duration(duration)} until first chunk" end return parts.join('; ') end def inspect "#{super} | \#<#{self.class} #{self.to_s}>" end private def complete_statistics(error = nil) @end_time = Clock.now @callback.call(self, error) if @callback end def format_duration(seconds) if seconds < 1.0 return "#{(seconds * 1000.0).round(2)}ms" else return "#{seconds.round(1)}s" end end end end end end ruby-async-http-0.59.5/lib/async/http/version.rb000066400000000000000000000023051436515316400215470ustar00rootroot00000000000000# frozen_string_literal: true # # Copyright, 2017, by Samuel G. D. Williams. # # 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. module Async module HTTP VERSION = "0.59.5" end end ruby-async-http-0.59.5/readme.md000066400000000000000000000255011436515316400164550ustar00rootroot00000000000000# Async::HTTP An asynchronous client and server implementation of HTTP/1.0, HTTP/1.1 and HTTP/2 including TLS. Support for streaming requests and responses. Built on top of [async](https://github.com/socketry/async) and [async-io](https://github.com/socketry/async-io). [falcon](https://github.com/socketry/falcon) provides a rack-compatible server. [![Development Status](https://github.com/socketry/async-http/workflows/Test/badge.svg)](https://github.com/socketry/async-http/actions?workflow=Test) ## Installation Add this line to your application's Gemfile: ``` ruby gem 'async-http' ``` And then execute: $ bundle Or install it yourself as: $ gem install async-http ## Usage ### Post JSON data Here is an example showing how to post a data structure as JSON to a remote resource: ``` ruby #!/usr/bin/env ruby require 'json' require 'async' require 'async/http/internet' data = {'life' => 42} Async do # Make a new internet: internet = Async::HTTP::Internet.new # Prepare the request: headers = [['accept', 'application/json']] body = [JSON.dump(data)] # Issues a POST request: response = internet.post("https://httpbin.org/anything", headers, body) # Save the response body to a local file: pp JSON.parse(response.read) ensure # The internet is closed for business: internet.close end ``` Consider using [async-rest](https://github.com/socketry/async-rest) instead. ### Multiple Requests To issue multiple requests concurrently, you should use a barrier, e.g. ``` ruby #!/usr/bin/env ruby require 'async' require 'async/barrier' require 'async/http/internet' TOPICS = ["ruby", "python", "rust"] Async do internet = Async::HTTP::Internet.new barrier = Async::Barrier.new # Spawn an asynchronous task for each topic: TOPICS.each do |topic| barrier.async do response = internet.get "https://www.google.com/search?q=#{topic}" puts "Found #{topic}: #{response.read.scan(topic).size} times." end end # Ensure we wait for all requests to complete before continuing: barrier.wait ensure internet&.close end ``` #### Limiting Requests If you need to limit the number of simultaneous requests, use a semaphore. ``` ruby #!/usr/bin/env ruby require 'async' require 'async/barrier' require 'async/semaphore' require 'async/http/internet' TOPICS = ["ruby", "python", "rust"] Async do internet = Async::HTTP::Internet.new barrier = Async::Barrier.new semaphore = Async::Semaphore.new(2, parent: barrier) # Spawn an asynchronous task for each topic: TOPICS.each do |topic| semaphore.async do response = internet.get "https://www.google.com/search?q=#{topic}" puts "Found #{topic}: #{response.read.scan(topic).size} times." end end # Ensure we wait for all requests to complete before continuing: barrier.wait ensure internet&.close end ``` ### Persistent Connections To keep connections alive, install the `thread-local` gem, require `async/http/internet/instance`, and use the `instance`, e.g. ``` ruby #!/usr/bin/env ruby require 'async' require 'async/http/internet/instance' Async do internet = Async::HTTP::Internet.instance response = internet.get "https://www.google.com/search?q=test" puts "Found #{response.read.size} results." end ``` ### Downloading a File Here is an example showing how to download a file and save it to a local path: ``` ruby #!/usr/bin/env ruby require 'async' require 'async/http/internet' Async do # Make a new internet: internet = Async::HTTP::Internet.new # Issues a GET request to Google: response = internet.get("https://www.google.com/search?q=kittens") # Save the response body to a local file: response.save("/tmp/search.html") ensure # The internet is closed for business: internet.close end ``` ### Basic Client/Server Here is a basic example of a client/server running in the same reactor: ``` ruby #!/usr/bin/env ruby require 'async' require 'async/http/server' require 'async/http/client' require 'async/http/endpoint' require 'async/http/protocol/response' endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9294') app = lambda do |request| Protocol::HTTP::Response[200, {}, ["Hello World"]] end server = Async::HTTP::Server.new(app, endpoint) client = Async::HTTP::Client.new(endpoint) Async do |task| server_task = task.async do server.run end response = client.get("/") puts response.status puts response.read server_task.stop end ``` ### Advanced Verification You can hook into SSL certificate verification to improve server verification. ``` ruby require 'async' require 'async/http' # These are generated from the certificate chain that the server presented. trusted_fingerprints = { "dac9024f54d8f6df94935fb1732638ca6ad77c13" => true, "e6a3b45b062d509b3382282d196efe97d5956ccb" => true, "07d63f4c05a03f1c306f9941b8ebf57598719ea2" => true, "e8d994f44ff20dc78dbff4e59d7da93900572bbf" => true, } Async do endpoint = Async::HTTP::Endpoint.parse("https://www.codeotaku.com/index") # This is a quick hack/POC: ssl_context = endpoint.ssl_context ssl_context.verify_callback = proc do |verified, store_context| certificate = store_context.current_cert fingerprint = OpenSSL::Digest::SHA1.new(certificate.to_der).to_s if trusted_fingerprints.include? fingerprint true else Console.logger.warn("Untrusted Certificate Fingerprint"){fingerprint} false end end endpoint = endpoint.with(ssl_context: ssl_context) client = Async::HTTP::Client.new(endpoint) response = client.get(endpoint.path) pp response.status, response.headers.fields, response.read end ``` ### Timeouts Here's a basic example with a timeout: ``` ruby #!/usr/bin/env ruby require 'async/http/internet' Async do |task| internet = Async::HTTP::Internet.new # Request will timeout after 2 seconds task.with_timeout(2) do response = internet.get "https://httpbin.org/delay/10" end rescue Async::TimeoutError puts "The request timed out" ensure internet&.close end ``` ## Performance On a 4-core 8-thread i7, running `ab` which uses discrete (non-keep-alive) connections: $ ab -c 8 -t 10 http://127.0.0.1:9294/ This is ApacheBench, Version 2.3 <$Revision: 1757674 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 5000 requests Completed 10000 requests Completed 15000 requests Completed 20000 requests Completed 25000 requests Completed 30000 requests Completed 35000 requests Completed 40000 requests Completed 45000 requests Completed 50000 requests Finished 50000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 9294 Document Path: / Document Length: 13 bytes Concurrency Level: 8 Time taken for tests: 1.869 seconds Complete requests: 50000 Failed requests: 0 Total transferred: 2450000 bytes HTML transferred: 650000 bytes Requests per second: 26755.55 [#/sec] (mean) Time per request: 0.299 [ms] (mean) Time per request: 0.037 [ms] (mean, across all concurrent requests) Transfer rate: 1280.29 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 0 0 0.2 0 6 Waiting: 0 0 0.2 0 6 Total: 0 0 0.2 0 6 Percentage of the requests served within a certain time (ms) 50% 0 66% 0 75% 0 80% 0 90% 0 95% 1 98% 1 99% 1 100% 6 (longest request) On a 4-core 8-thread i7, running `wrk`, which uses 8 keep-alive connections: $ wrk -c 8 -d 10 -t 8 http://127.0.0.1:9294/ Running 10s test @ http://127.0.0.1:9294/ 8 threads and 8 connections Thread Stats Avg Stdev Max +/- Stdev Latency 217.69us 0.99ms 23.21ms 97.39% Req/Sec 12.18k 1.58k 17.67k 83.21% 974480 requests in 10.10s, 60.41MB read Requests/sec: 96485.00 Transfer/sec: 5.98MB According to these results, the cost of handling connections is quite high, while general throughput seems pretty decent. ## Semantic Model ### Scheme HTTP/1 has an implicit scheme determined by the kind of connection made to the server (either `http` or `https`), while HTTP/2 models this explicitly and the client indicates this in the request using the `:scheme` pseudo-header (typically `https`). To normalize this, `Async::HTTP::Client` and `Async::HTTP::Server` have a default scheme which is used if none is supplied. ### Version HTTP/1 has an explicit version while HTTP/2 does not expose the version in any way. ### Reason HTTP/1 responses contain a reason field which is largely irrelevant. HTTP/2 does not support this field. ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## See Also - [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency. - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`. - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets. - [async-rest](https://github.com/socketry/async-rest) — A RESTful resource layer built on top of `async-http`. - [async-http-faraday](https://github.com/socketry/async-http-faraday) — A faraday adapter to use `async-http`. ## License Released under the MIT license. Copyright, 2018, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 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. ruby-async-http-0.59.5/release.cert000066400000000000000000000033141436515316400171730ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= -----END CERTIFICATE----- ruby-async-http-0.59.5/spec/000077500000000000000000000000001436515316400156255ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/000077500000000000000000000000001436515316400167425ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/000077500000000000000000000000001436515316400177215ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/body/000077500000000000000000000000001436515316400206565ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/body/hijack_spec.rb000066400000000000000000000035641436515316400234560ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/body/hijack' RSpec.describe Async::HTTP::Body::Hijack do include_context Async::RSpec::Reactor let(:content) {"Hello World!"} describe '#call' do let(:stream) {Async::HTTP::Body::Writable.new} subject do described_class.wrap do |stream| 3.times do stream.write(content) end stream.close end end it "should generate body using direct invocation" do subject.call(stream) 3.times do expect(stream.read).to be == content end expect(stream.read).to be_nil expect(stream).to be_empty end it "should generate body using stream" do 3.times do expect(subject.read).to be == content end expect(subject.read).to be_nil expect(subject).to be_empty end end end ruby-async-http-0.59.5/spec/async/http/body/pipe_spec.rb000066400000000000000000000046371436515316400231640ustar00rootroot00000000000000# Copyright, 2020, by Samuel G. D. Williams. # # 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. require 'async' require 'async/http/body/pipe' require 'async/http/body/writable' RSpec.describe Async::HTTP::Body::Pipe do let(:input) { Async::HTTP::Body::Writable.new } let(:pipe) { described_class.new(input) } let(:data) { 'Hello World!' } describe '#to_io' do include_context Async::RSpec::Reactor let(:io) { pipe.to_io } before do Async::Task.current.async do |task| # input writer task first, second = data.split(' ') input.write("#{first} ") task.sleep(input_write_duration) if input_write_duration > 0 input.write(second) input.close end end after { io.close } shared_examples :returns_io_socket do it 'returns an io socket' do expect(io).to be_a(Async::IO::Socket) expect(io.read).to eq data end end context 'when reading blocks' do let(:input_write_duration) { 0.01 } include_examples :returns_io_socket end context 'when reading does not block' do let(:input_write_duration) { 0 } include_examples :returns_io_socket end end describe 'going out of reactor scope' do context 'when pipe is closed' do it 'finishes' do Async { pipe.close } end end context 'when pipe is not closed' do it 'finishes' do # ensures pipe background tasks are transient Async { pipe } end end end end ruby-async-http-0.59.5/spec/async/http/body/slowloris_spec.rb000066400000000000000000000033331436515316400242540ustar00rootroot00000000000000# Copyright, 2019, by Samuel G. D. Williams. # # 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. require_relative 'writable_examples' require 'async/http/body/slowloris' RSpec.describe Async::HTTP::Body::Slowloris do include_context Async::RSpec::Reactor it_behaves_like Async::HTTP::Body::Writable it "closes body with error if throughput is not maintained" do subject.write("Hello World") sleep 0.1 expect do subject.write("Hello World") end.to raise_error(Async::HTTP::Body::Slowloris::ThroughputError, /Slow write/) end it "doesn't close body if throughput is exceeded" do subject.write("Hello World") expect do subject.write("Hello World") end.to_not raise_error end end ruby-async-http-0.59.5/spec/async/http/body/writable_examples.rb000066400000000000000000000066211436515316400247170ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/server' require 'async/http/client' require 'async/reactor' require 'async/http/body' require 'protocol/http/body/deflate' require 'async/http/body/writable' require 'async/http/endpoint' require 'async/io/ssl_socket' require 'async/rspec/ssl' RSpec.shared_examples_for Async::HTTP::Body::Writable do it "can write and read data" do 3.times do |i| subject.write("Hello World #{i}") expect(subject.read).to be == "Hello World #{i}" end end it "can buffer data in order" do 3.times do |i| subject.write("Hello World #{i}") end 3.times do |i| expect(subject.read).to be == "Hello World #{i}" end end context '#join' do it "can join chunks" do 3.times do |i| subject.write("#{i}") end subject.close expect(subject.join).to be == "012" end end context '#each' do it "can read all data in order" do 3.times do |i| subject.write("Hello World #{i}") end subject.close 3.times do |i| chunk = subject.read expect(chunk).to be == "Hello World #{i}" end end it "can propagate failures" do reactor.async do expect do subject.each do |chunk| raise RuntimeError.new("It was too big!") end end.to raise_error(RuntimeError, /big/) end expect{ subject.write("Beep boop") # This will cause a failure. Async::Task.current.yield subject.write("Beep boop") # This will fail. }.to raise_error(RuntimeError, /big/) end it "can propagate failures in nested bodies" do nested = Protocol::HTTP::Body::Deflate.for(subject) reactor.async do expect do nested.each do |chunk| raise RuntimeError.new("It was too big!") end end.to raise_error(RuntimeError, /big/) end expect{ subject.write("Beep boop") # This will cause a failure. Async::Task.current.yield subject.write("Beep boop") # This will fail. }.to raise_error(RuntimeError, /big/) end it "will stop after finishing" do output_task = reactor.async do subject.each do |chunk| expect(chunk).to be == "Hello World!" end end subject.write("Hello World!") subject.close expect(subject).to_not be_empty Async::Task.current.yield expect(output_task).to be_finished expect(subject).to be_empty end end end ruby-async-http-0.59.5/spec/async/http/body/writable_spec.rb000066400000000000000000000024331436515316400240300ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'writable_examples' RSpec.describe Async::HTTP::Body::Writable do include_context Async::RSpec::Reactor it_behaves_like Async::HTTP::Body::Writable end ruby-async-http-0.59.5/spec/async/http/body_spec.rb000066400000000000000000000073161436515316400222240ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/body' require 'async/http/server' require 'async/http/client' require 'async/http/endpoint' require 'async/io/ssl_socket' require 'async/rspec/reactor' require 'localhost/authority' RSpec.shared_examples Async::HTTP::Body do let(:client) {Async::HTTP::Client.new(client_endpoint, protocol: described_class)} it "can stream requests" do server = Async::HTTP::Server.for(server_endpoint, protocol: described_class) do |request| input = request.body output = Async::HTTP::Body::Writable.new Async::Task.current.async do |task| input.each do |chunk| output.write(chunk.reverse) end output.close end Protocol::HTTP::Response[200, [], output] end server_task = reactor.async do server.run end output = Async::HTTP::Body::Writable.new reactor.async do |task| output.write("Hello World!") output.close end response = client.post("/", {}, output) expect(response).to be_success expect(response.read).to be == "!dlroW olleH" server_task.stop client.close end it "can stream response" do notification = Async::Notification.new server = Async::HTTP::Server.for(server_endpoint, protocol: described_class) do |request| body = Async::HTTP::Body::Writable.new Async::Task.current.async do |task| 10.times do |i| body.write("#{i}") notification.wait end body.close end Protocol::HTTP::Response[200, {}, body] end server_task = reactor.async do server.run end response = client.get("/") expect(response).to be_success j = 0 # This validates interleaving response.body.each do |line| expect(line.to_i).to be == j j += 1 notification.signal end server_task.stop client.close end end RSpec.describe Async::HTTP::Protocol::HTTP1, timeout: 2 do include_context Async::RSpec::Reactor let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9296', reuse_port: true)} let(:client_endpoint) {endpoint} let(:server_endpoint) {endpoint} it_should_behave_like Async::HTTP::Body end RSpec.describe Async::HTTP::Protocol::HTTPS, timeout: 2 do include_context Async::RSpec::Reactor let(:authority) {Localhost::Authority.new} let(:server_context) {authority.server_context} let(:client_context) {authority.client_context} # Shared port for localhost network tests. let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:9296", ssl_context: server_context)} let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:9296", ssl_context: client_context)} it_should_behave_like Async::HTTP::Body endruby-async-http-0.59.5/spec/async/http/client/000077500000000000000000000000001436515316400211775ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/client/google_spec.rb000066400000000000000000000030501436515316400240100ustar00rootroot00000000000000# Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'async/http/client' require 'async/http/endpoint' RSpec.describe Async::HTTP::Client, timeout: 5 do include_context Async::RSpec::Reactor let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.google.com')} let(:client) {Async::HTTP::Client.new(endpoint)} it 'can fetch remote resource' do response = client.get('/', 'accept' => '*/*') response.finish expect(response).to_not be_failure client.close end end ruby-async-http-0.59.5/spec/async/http/client_spec.rb000066400000000000000000000055421436515316400225440ustar00rootroot00000000000000# Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'server_context' require 'async/http/server' require 'async/http/client' require 'async/reactor' require 'async/io/ssl_socket' require 'async/http/endpoint' require 'protocol/http/accept_encoding' RSpec.describe Async::HTTP::Client, timeout: 5 do describe Async::HTTP::Protocol::HTTP1 do include_context Async::HTTP::Server let(:protocol) {described_class} it "client can get resource" do response = client.get("/") response.read expect(response).to be_success end end context 'non-existant host' do include_context Async::RSpec::Reactor let(:endpoint) {Async::HTTP::Endpoint.parse('http://the.future')} let(:client) {Async::HTTP::Client.new(endpoint)} it "should fail to connect" do expect do client.get("/") end.to raise_error(SocketError, /not known/) end end describe Async::HTTP::Protocol::HTTPS do include_context Async::RSpec::Reactor let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.codeotaku.com')} let(:client) {Async::HTTP::Client.new(endpoint)} it "should specify hostname" do expect(endpoint.hostname).to be == "www.codeotaku.com" expect(client.authority).to be == "www.codeotaku.com" end it "can request remote resource" do 2.times do response = client.get("/index") expect(response).to be_success response.finish end client.close end it "can request remote resource with compression" do compressor = Protocol::HTTP::AcceptEncoding.new(client) response = compressor.get("/index", {'accept-encoding' => 'gzip'}) expect(response).to be_success expect(response.body).to be_kind_of Async::HTTP::Body::Inflate expect(response.read).to be_start_with('') client.close end end end ruby-async-http-0.59.5/spec/async/http/endpoint_spec.rb000066400000000000000000000113171436515316400231030ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/endpoint' RSpec.describe Async::HTTP::Endpoint do it "should fail to parse relative url" do expect{ described_class.parse("/foo/bar") }.to raise_error(ArgumentError, /absolute/) end describe '#port' do let(:url_string) {"https://localhost:9292"} it "extracts port from URL" do endpoint = Async::HTTP::Endpoint.parse(url_string) expect(endpoint.port).to eq 9292 end it "extracts port from options" do endpoint = Async::HTTP::Endpoint.parse(url_string, port: 9000) expect(endpoint.port).to eq 9000 end end describe '#hostname' do describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292") do it {is_expected.to have_attributes(hostname: '127.0.0.1')} it "should be connecting to 127.0.0.1" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') expect(subject.endpoint.endpoint).to have_attributes(hostname: '127.0.0.1') end end describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292", hostname: 'localhost') do it {is_expected.to have_attributes(hostname: 'localhost')} it {is_expected.to_not be_localhost} it "should be connecting to localhost" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') expect(subject.endpoint.endpoint).to have_attributes(hostname: 'localhost') end end end describe '.for' do context Async::HTTP::Endpoint.for("http", "localhost") do it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/")} it {is_expected.to_not be_secure} end context Async::HTTP::Endpoint.for("http", "localhost", "/foo") do it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/foo")} end end describe '#secure?' do subject {Async::HTTP::Endpoint.parse(description)} context 'http://localhost' do it { is_expected.to_not be_secure } end context 'https://localhost' do it { is_expected.to be_secure } end context 'with scheme: https' do subject {Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https')} it { is_expected.to be_secure } end end describe '#localhost?' do subject {Async::HTTP::Endpoint.parse(description)} context 'http://localhost' do it { is_expected.to be_localhost } end context 'http://hello.localhost' do it { is_expected.to be_localhost } end context 'http://localhost.' do it { is_expected.to be_localhost } end context 'http://hello.localhost.' do it { is_expected.to be_localhost } end context 'http://localhost.com' do it { is_expected.to_not be_localhost } end end describe '#path' do it "can normal urls" do endpoint = Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") expect(endpoint.path).to be == "/bar?baz" end it "can handle websocket urls" do endpoint = Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") expect(endpoint.path).to be == "/bar?baz" end end end RSpec.describe "http://www.google.com/search" do let(:endpoint) {Async::HTTP::Endpoint.parse(subject)} it "should be valid endpoint" do expect{endpoint}.to_not raise_error end it "should select the correct protocol" do expect(endpoint.protocol).to be Async::HTTP::Protocol::HTTP1 end it "should parse the correct hostname" do expect(endpoint.hostname).to be == "www.google.com" end it "should not be equal if path is different" do other = Async::HTTP::Endpoint.parse('http://www.google.com/search?q=ruby') expect(endpoint).to_not be_eql other end end ruby-async-http-0.59.5/spec/async/http/internet/000077500000000000000000000000001436515316400215515ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/internet/instance_spec.rb000066400000000000000000000025771436515316400247270ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/internet/instance' require 'async/reactor' RSpec.describe Async::HTTP::Internet, timeout: 5 do describe '.instance' do it "returns an internet instance" do expect(Async::HTTP::Internet.instance).to be_kind_of(Async::HTTP::Internet) end end end ruby-async-http-0.59.5/spec/async/http/internet_spec.rb000066400000000000000000000034621436515316400231150ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/internet' require 'async/reactor' require 'json' RSpec.describe Async::HTTP::Internet, timeout: 5 do include_context Async::RSpec::Reactor let(:headers) {[['accept', '*/*'], ['user-agent', 'async-http']]} after do subject.close end it "can fetch remote website" do response = subject.get("https://www.codeotaku.com/index", headers) expect(response).to be_success response.close end let(:sample) {{"hello" => "world"}} let(:body) {[JSON.dump(sample)]} it "can fetch remote json" do response = subject.post("https://httpbin.org/anything", headers, body) expect(response).to be_success expect{JSON.parse(response.read)}.to_not raise_error end end ruby-async-http-0.59.5/spec/async/http/performance_spec.rb000077500000000000000000000057571436515316400236020ustar00rootroot00000000000000# Copyright, 2017, by Samuel G. D. Williams. # # 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. require 'async/http/server' require 'async/http/client' require_relative 'server_context' require 'async/container' require 'etc' RSpec.shared_examples_for 'client benchmark' do let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', timeout: 0.8, reuse_port: true)} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, []] end end let(:url) {endpoint.url.to_s} let(:repeats) {1000} let(:concurrency) {Etc.nprocessors || 2} before do Sync do # We bind the endpoint before running the server so that we know incoming connections will be accepted: @bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint) end # I feel a dedicated class might be better than this hack: allow(@bound_endpoint).to receive(:protocol).and_return(endpoint.protocol) allow(@bound_endpoint).to receive(:scheme).and_return(endpoint.scheme) @container = Async::Container.new GC.disable @container.run(count: concurrency) do |instance| Async do instance.ready! server.run end end @bound_endpoint.close end after do @container.stop GC.enable end it "runs benchmark", timeout: nil do if ab = `which ab`.chomp! system(ab, "-k", "-n", (concurrency*repeats).to_s, "-c", concurrency.to_s, url) end if wrk = `which wrk`.chomp! system(wrk, "-c", concurrency.to_s, "-d", "2", "-t", concurrency.to_s, url) end end end RSpec.describe Async::HTTP::Server do describe Protocol::HTTP::Middleware::Okay do let(:server) do Async::HTTP::Server.new( Protocol::HTTP::Middleware::Okay, @bound_endpoint ) end include_examples 'client benchmark' end describe 'multiple chunks' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do Protocol::HTTP::Response[200, {}, "Hello World".chars] end end include_examples 'client benchmark' end end ruby-async-http-0.59.5/spec/async/http/protocol/000077500000000000000000000000001436515316400215625ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/protocol/http10_spec.rb000066400000000000000000000024351436515316400242450ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/protocol/http10' require_relative 'shared_examples' RSpec.describe Async::HTTP::Protocol::HTTP10, timeout: 2 do it_behaves_like Async::HTTP::Protocol end ruby-async-http-0.59.5/spec/async/http/protocol/http11/000077500000000000000000000000001436515316400227035ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/async/http/protocol/http11/desync_spec.rb000066400000000000000000000044451436515316400255360ustar00rootroot00000000000000# Copyright, 2021, by Samuel G. D. Williams. # # 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. require_relative '../../server_context' require 'async/http/protocol/http11' RSpec.describe Async::HTTP::Protocol::HTTP11, timeout: 30 do include_context Async::HTTP::Server let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, [request.path]] end end around do |example| current = Console.logger.level Console.logger.fatal! example.run ensure Console.logger.level = current end it "doesn't desync responses" do tasks = [] task = Async::Task.current backtraces = [] 100.times do tasks << task.async{ loop do response = client.get('/a') expect(response.read).to be == "/a" rescue Exception => exception backtraces << exception&.backtrace raise ensure response&.close end } end 100.times do tasks << task.async{ loop do response = client.get('/b') expect(response.read).to be == "/b" rescue Exception => exception backtraces << exception&.backtrace raise ensure response&.close end } end tasks.each do |child| task.sleep 0.01 child.stop end puts "Backtraces" pp backtraces.sort.uniq end end ruby-async-http-0.59.5/spec/async/http/protocol/http11_spec.rb000077500000000000000000000043161436515316400242510ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/protocol/http11' require_relative 'shared_examples' RSpec.describe Async::HTTP::Protocol::HTTP11, timeout: 2 do it_behaves_like Async::HTTP::Protocol context 'head request' do include_context Async::HTTP::Server let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, ["Hello", "World"]] end end it "doesn't reply with body" do 5.times do response = client.head("/") expect(response).to be_success expect(response.version).to be == "HTTP/1.1" expect(response.body).to be_empty response.read end end end context 'raw response' do include_context Async::HTTP::Server let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| peer = request.hijack! peer.write( "#{request.version} 200 It worked!\r\n" + "connection: close\r\n" + "\r\n" + "Hello World!" ) peer.close nil end end it "reads raw response" do response = client.get("/") expect(response.read).to be == "Hello World!" end end end ruby-async-http-0.59.5/spec/async/http/protocol/http2_spec.rb000066400000000000000000000065301436515316400241660ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async/http/protocol/http2' require_relative 'shared_examples' RSpec.describe Async::HTTP::Protocol::HTTP2, timeout: 2 do it_behaves_like Async::HTTP::Protocol context 'bad requests' do include_context Async::HTTP::Server it "should fail with explicit authority" do expect do client.post("/", [[':authority', 'foo']]) end.to raise_error(Protocol::HTTP2::StreamError) end end context 'closed streams' do include_context Async::HTTP::Server it 'should delete stream after response stream is closed' do response = client.get("/") connection = response.connection response.read expect(connection.streams).to be_empty end end context 'host header' do include_context Async::HTTP::Server let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] end end # We specify nil for the authority - it won't be sent. let!(:client) {Async::HTTP::Client.new(endpoint, authority: nil)} it "should not send :authority header if host header is present" do response = client.post("/", [['host', 'foo']]) expect(response.headers).to include('host') expect(response.headers['host']).to be == 'foo' # TODO Should HTTP/2 respect host header? expect(response.read).to be == "Authority: nil" end end context 'stopping requests' do include_context Async::HTTP::Server let(:notification) {Async::Notification.new} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| body = Async::HTTP::Body::Writable.new reactor.async do |task| begin 100.times do |i| body.write("Chunk #{i}") task.sleep (0.01) end rescue # puts "Response generation failed: #{$!}" ensure body.close notification.signal end end Protocol::HTTP::Response[200, {}, body] end end let(:pool) {client.pool} it "should close stream without closing connection" do expect(pool).to be_empty response = client.get("/") expect(pool).to_not be_empty response.close notification.wait expect(response.stream.connection).to be_reusable end end end ruby-async-http-0.59.5/spec/async/http/protocol/shared_examples.rb000066400000000000000000000322151436515316400252560ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative '../server_context' require 'async' require 'async/clock' require 'async/http/client' require 'async/http/server' require 'async/http/endpoint' require 'async/http/body/hijack' require 'tempfile' require 'protocol/http/body/file' require 'async/rspec/profile' RSpec.shared_examples_for Async::HTTP::Protocol do include_context Async::HTTP::Server it "should have valid scheme" do expect(client.scheme).to be == "http" end context "huge body", timeout: 600 do let(:body) {Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, body] end end it "client can download data quickly" do |example| response = client.get("/") expect(response).to be_success data_size = 0 duration = Async::Clock.measure do while chunk = response.body.read data_size += chunk.bytesize chunk.clear end response.finish end size_mbytes = data_size / 1024**2 example.reporter.message "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" end end context 'buffered body' do let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} let(:response) {Protocol::HTTP::Response[200, {}, body]} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| response end end it "response body should be closed" do expect(body).to receive(:close).and_call_original # expect(response).to receive(:close).and_call_original expect(client.get("/", {}).read).to be == "Hello World" end end context 'empty body' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[204] end end it 'properly handles no content responses' do expect(client.get("/", {}).read).to be_nil end end context 'with trailer', if: described_class.bidirectional? do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| if trailer = request.headers['trailer'] expect(request.headers).to_not include('etag') request.finish expect(request.headers).to include('etag') Protocol::HTTP::Response[200, [], "request trailer"] else headers = Protocol::HTTP::Headers.new headers.add('trailer', 'etag') body = Async::HTTP::Body::Writable.new Async do |task| body.write("response trailer") task.sleep(0.01) headers.add('etag', 'abcd') body.close end Protocol::HTTP::Response[200, headers, body] end end end it "can send request trailer" do headers = Protocol::HTTP::Headers.new headers.add('trailer', 'etag') body = Async::HTTP::Body::Writable.new Async do |task| body.write("Hello") task.sleep(0.01) headers.add('etag', 'abcd') body.close end response = client.post("/", headers, body) expect(response.read).to be == "request trailer" expect(response).to be_success end it "can receive response trailer" do response = client.get("/") expect(response.headers).to include('trailer') headers = response.headers expect(headers).to_not include('etag') expect(response.read).to be == "response trailer" expect(response).to be_success # It was sent as a trailer. expect(headers).to include('etag') end end context 'with working server' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| if request.method == 'POST' # We stream the request body directly to the response. Protocol::HTTP::Response[200, {}, request.body] elsif request.method == 'GET' expect(request.body).to be nil Protocol::HTTP::Response[200, { 'remote-address' => request.remote_address.inspect }, ["#{request.method} #{request.version}"]] else Protocol::HTTP::Response[200, {}, ["Hello World"]] end end end it "should have valid scheme" do expect(server.scheme).to be == "http" end it "disconnects slow clients" do response = client.get("/") response.read # We expect this connection to be closed: connection = response.connection reactor.sleep(1.0) response = client.get("/") response.read expect(connection).to_not be_reusable # client.close # reactor.sleep(0.1) # reactor.print_hierarchy end context 'using GET method' do let(:expected) {"GET #{protocol::VERSION}"} it "can handle many simultaneous requests", timeout: 10 do |example| duration = Async::Clock.measure do 10.times do tasks = 100.times.collect do Async do client.get("/") end end tasks.each do |task| response = task.wait expect(response).to be_success expect(response.read).to eq expected end end end example.reporter.message "Pool: #{client.pool}" example.reporter.message "Duration = #{duration.round(2)}" end context 'with response' do let(:response) {client.get("/")} after {response.finish} it "can finish gracefully" do expect(response).to be_success end it "is successful" do expect(response).to be_success expect(response.read).to eq expected end it "provides content length" do expect(response.body.length).to_not be_nil end let(:tempfile) {Tempfile.new} it "can save to disk" do response.save(tempfile.path) expect(tempfile.read).to eq expected tempfile.close end it "has remote-address header" do expect(response.headers['remote-address']).to_not be_nil end it "has protocol version" do expect(response.version).to_not be_nil end end end context 'HEAD' do let(:response) {client.head("/")} after {response.finish} it "is successful and without body" do expect(response).to be_success expect(response.body).to_not be_nil expect(response.body).to be_empty expect(response.body.length).to_not be_nil expect(response.read).to be_nil end end context 'POST' do let(:response) {client.post("/", {}, ["Hello", " ", "World"])} after {response.finish} it "is successful" do expect(response).to be_success expect(response.read).to be == "Hello World" expect(client.pool).to_not be_busy end it "can buffer response" do buffer = response.finish expect(buffer.join).to be == "Hello World" expect(client.pool).to_not be_busy end it "should not contain content-length response header" do expect(response.headers).to_not include('content-length') end it "fails gracefully when closing connection" do client.pool.acquire do |connection| connection.stream.close end end end end context 'content length' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] end end it "can send push promises" do response = client.post("/test", [], ["Hello World!"]) expect(response).to be_success expect(response.body.length).to be == 18 expect(response.read).to be == "Content Length: 12" end end context 'hijack with nil response' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| nil end end it "fails with appropriate error" do response = client.get("/") expect(response).to be_server_failure end end context 'partial hijack' do let(:content) {"Hello World!"} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| stream.write content stream.write content stream.close end end end it "reads hijacked body" do response = client.get("/") expect(response.read).to be == (content*2) end end context 'body with incorrect length' do let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, bad_body] end end it "fails with appropriate error" do response = client.get("/") expect do response.read end.to raise_error(EOFError) end end context 'streaming server' do let!(:sent_chunks) {[]} let(:server) do chunks = sent_chunks Async::HTTP::Server.for(@bound_endpoint) do |request| body = Async::HTTP::Body::Writable.new Async::Reactor.run do |task| 10.times do |i| chunk = "Chunk #{i}" chunks << chunk body.write chunk task.sleep 0.25 end body.finish end Protocol::HTTP::Response[200, {}, body] end end it "can cancel response" do response = client.get("/") expect(response.body.read).to be == "Chunk 0" response.close expect(sent_chunks).to be == ["Chunk 0"] end end context 'hijack server' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| if request.hijack? io = request.hijack! io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" io.flush io.close else Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] end end end it "will hijack response if possible" do response = client.get("/") expect(response.read).to include("Hijack") end end context 'broken server' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| raise RuntimeError.new('simulated failure') end end it "can't get /" do expect do response = client.get("/") end.to raise_error(Exception) end end context 'slow server' do let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', reuse_port: true, timeout: 0.1)} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Async::Task.current.sleep(endpoint.timeout * 2) Protocol::HTTP::Response[200, {}, []] end end it "can't get /" do expect do client.get("/") end.to raise_error(Async::TimeoutError) end end context 'bi-directional streaming', if: described_class.bidirectional? do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| # Echo the request body back to the client. Protocol::HTTP::Response[200, {}, request.body] end end it "can read from request body and write response body simultaneously" do body = Async::HTTP::Body::Writable.new # Ideally, the flow here is as follows: # 1/ Client writes headers to server. # 2/ Client starts writing data to server (in async task). # 3/ Client reads headers from server. # 4a/ Client reads data from server. # 4b/ Client finishes sending data to server. response = client.post(endpoint.path, [], body) expect(response).to be_success body.write "." count = 0 response.each do |chunk| if chunk.bytesize > 32 body.close else count += 1 body.write chunk*2 Async::Task.current.sleep(0.1) end end expect(count).to be == 6 end end context 'multiple client requests' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, [request.path]] end end around do |example| current = Console.logger.level Console.logger.fatal! example.run ensure Console.logger.level = current end it "doesn't cancel all requests" do tasks = [] task = Async::Task.current stopped = [] 10.times do tasks << task.async { begin loop do client.get('http://127.0.0.1:8080/a').finish end ensure stopped << 'a' end } end 10.times do tasks << task.async { begin loop do client.get('http://127.0.0.1:8080/b').finish end ensure stopped << 'b' end } end tasks.each do |child| task.sleep 0.01 child.stop end expect(stopped.sort).to be == stopped end end end ruby-async-http-0.59.5/spec/async/http/proxy_spec.rb000066400000000000000000000167071436515316400224540ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require 'async' require 'async/http/proxy' require 'async/http/protocol' require 'async/http/body/hijack' require_relative 'server_context' RSpec.shared_examples_for Async::HTTP::Proxy do include_context Async::HTTP::Server describe '.proxied_endpoint' do it "can construct valid endpoint" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_endpoint = client.proxied_endpoint(endpoint) expect(proxied_endpoint).to be_kind_of(Async::HTTP::Endpoint) end end describe '.proxied_client' do it "can construct valid client" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_client = client.proxied_client(endpoint) expect(proxied_client).to be_kind_of(Async::HTTP::Client) end end context 'CONNECT' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| chunk = stream.read stream.close_read stream.write(chunk) stream.close end end end let(:data) {"Hello World!"} it "can connect and hijack connection" do input = Async::HTTP::Body::Writable.new response = client.connect("127.0.0.1:1234", [], input) expect(response).to be_success input.write(data) input.close expect(response.read).to be == data end end context 'echo server' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| expect(request.path).to be == "localhost:1" Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| while chunk = stream.read_partial(1024) stream.write(chunk) stream.flush end stream.close end end end let(:data) {"Hello World!"} it "can connect to remote system using block" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) expect(proxy.client.pool).to be_empty proxy.connect do |peer| stream = Async::IO::Stream.new(peer) stream.write(data) stream.close_write expect(stream.read).to be == data end proxy.close expect(proxy.client.pool).to be_empty end it "can connect to remote system" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) expect(proxy.client.pool).to be_empty stream = Async::IO::Stream.new(proxy.connect) stream.write(data) stream.close_write expect(stream.read).to be == data stream.close proxy.close expect(proxy.client.pool).to be_empty end end context 'proxied client' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| expect(request.method).to be == "CONNECT" unless authorization_lambda.call(request) next Protocol::HTTP::Response[407, [], nil] end host, port = request.path.split(":", 2) endpoint = Async::IO::Endpoint.tcp(host, port) Console.logger.debug(self) {"Making connection to #{endpoint}..."} Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| upstream = Async::IO::Stream.new(endpoint.connect) Console.logger.debug(self) {"Connected to #{upstream}..."} reader = Async do |task| task.annotate "Upstream reader." while chunk = upstream.read_partial stream.write(chunk) stream.flush end ensure Console.logger.debug(self) {"Finished reading from upstream..."} stream.close_write end writer = Async do |task| task.annotate "Upstream writer." while chunk = stream.read_partial upstream.write(chunk) upstream.flush end rescue Async::Wrapper::Cancelled #ignore ensure Console.logger.debug(self) {"Finished writing to upstream..."} upstream.close_write end reader.wait writer.wait ensure upstream.close stream.close end end end let(:authorization_lambda) { ->(request) {true} } it 'can get insecure website' do endpoint = Async::HTTP::Endpoint.parse("http://www.google.com") proxy_client = client.proxied_client(endpoint) response = proxy_client.get("/search") expect(response).to_not be_failure # The response would be a redirect: expect(response).to be_redirection response.finish # The proxy.connnect response is not being released correctly - after pipe is done: expect(proxy_client.pool).to_not be_empty proxy_client.close expect(proxy_client.pool).to be_empty pp client end it 'can get secure website' do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") proxy_client = client.proxied_client(endpoint) response = proxy_client.get("/search") expect(response).to_not be_failure expect(response.read).to_not be_empty proxy_client.close end context 'authorization header required' do let(:authorization_lambda) do ->(request) {request.headers['proxy-authorization'] == 'supersecretpassword' } end context 'request includes headers' do let(:headers) { [['Proxy-Authorization', 'supersecretpassword']] } it 'succeeds' do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") proxy_client = client.proxied_client(endpoint, headers) response = proxy_client.get('/search') expect(response).to_not be_failure expect(response.read).to_not be_empty proxy_client.close end end context 'request does not include headers' do it 'does not succeed' do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") proxy_client = client.proxied_client(endpoint) expect do # Why is this response not 407? Because the response should come from the proxied connection, but that connection failed to be established. Because of that, there is no response. If we respond here with 407, it would be indistinguisable from the remote server returning 407. That would be an odd case, but none-the-less a valid one. response = proxy_client.get('/search') end.to raise_error(Async::HTTP::Proxy::ConnectFailure) proxy_client.close end end end end end RSpec.describe Async::HTTP::Protocol::HTTP10 do it_behaves_like Async::HTTP::Proxy end RSpec.describe Async::HTTP::Protocol::HTTP11 do it_behaves_like Async::HTTP::Proxy end RSpec.describe Async::HTTP::Protocol::HTTP2 do it_behaves_like Async::HTTP::Proxy end ruby-async-http-0.59.5/spec/async/http/relative_location_spec.rb000066400000000000000000000074161436515316400247730ustar00rootroot00000000000000# Copyright, 2018, by Samuel G. D. Williams. # # 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. require_relative 'server_context' require 'async/http/relative_location' require 'async/http/server' RSpec.describe Async::HTTP::RelativeLocation do include_context Async::HTTP::Server let(:protocol) {Async::HTTP::Protocol::HTTP1} subject {described_class.new(@client, 1)} context 'server redirections' do context '301' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| case request.path when '/home' Protocol::HTTP::Response[301, {'location' => '/'}, []] when '/' Protocol::HTTP::Response[301, {'location' => '/index.html'}, []] when '/index.html' Protocol::HTTP::Response[200, {}, [request.method]] end end end it 'should redirect POST to GET' do response = subject.post('/') expect(response).to be_success expect(response.read).to be == "GET" end context 'limiting redirects' do it 'should allow the maximum number of redirects' do response = subject.get('/') response.finish expect(response).to be_success end it 'should fail with maximum redirects' do expect{ response = subject.get('/home') }.to raise_error(Async::HTTP::TooManyRedirects, /maximum/) end end end context '302' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| case request.path when '/' Protocol::HTTP::Response[302, {'location' => '/index.html'}, []] when '/index.html' Protocol::HTTP::Response[200, {}, [request.method]] end end end it 'should redirect POST to GET' do response = subject.post('/') expect(response).to be_success expect(response.read).to be == "GET" end end context '307' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| case request.path when '/' Protocol::HTTP::Response[307, {'location' => '/index.html'}, []] when '/index.html' Protocol::HTTP::Response[200, {}, [request.method]] end end end it 'should redirect with same method' do response = subject.post('/') expect(response).to be_success expect(response.read).to be == "POST" end end context '308' do let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| case request.path when '/' Protocol::HTTP::Response[308, {'location' => '/index.html'}, []] when '/index.html' Protocol::HTTP::Response[200, {}, [request.method]] end end end it 'should redirect with same method' do response = subject.post('/') expect(response).to be_success expect(response.read).to be == "POST" end end end end ruby-async-http-0.59.5/spec/async/http/retry_spec.rb000066400000000000000000000035501436515316400224300ustar00rootroot00000000000000# Copyright, 2020, by Samuel G. D. Williams. # # 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. require_relative 'server_context' require 'async/http/client' require 'async/http/endpoint' RSpec.describe 'consistent retry behaviour' do include_context Async::HTTP::Server let(:protocol) {Async::HTTP::Protocol::HTTP1} let(:delay) {0.1} let(:retries) {2} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Async::Task.current.sleep(delay) Protocol::HTTP::Response[200, {}, []] end end def make_request(body) # This causes the first request to fail with "SocketError" which is retried: Async::Task.current.with_timeout(delay / 2, SocketError) do return client.get('/', {}, body) end end specify 'with nil body' do make_request(nil) end specify 'with empty array body' do make_request([]) end end ruby-async-http-0.59.5/spec/async/http/server_context.rb000066400000000000000000000043741436515316400233300ustar00rootroot00000000000000# Copyright, 2019, by Samuel G. D. Williams. # # 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. require 'async/http/server' require 'async/http/client' require 'async/http/endpoint' require 'async/io/shared_endpoint' RSpec.shared_context Async::HTTP::Server do include_context Async::RSpec::Reactor let(:protocol) {described_class} let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', timeout: 0.8, reuse_port: true, protocol: protocol)} let(:retries) {1} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| Protocol::HTTP::Response[200, {}, []] end end before do # We bind the endpoint before running the server so that we know incoming connections will be accepted: @bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint) # I feel a dedicated class might be better than this hack: allow(@bound_endpoint).to receive(:protocol).and_return(endpoint.protocol) allow(@bound_endpoint).to receive(:scheme).and_return(endpoint.scheme) @server_task = Async do server.run end @client = Async::HTTP::Client.new(endpoint, protocol: endpoint.protocol, retries: retries) end after do @client&.close @server_task&.stop @bound_endpoint&.close end let(:client) {@client} end ruby-async-http-0.59.5/spec/async/http/ssl_spec.rb000066400000000000000000000053431436515316400220660ustar00rootroot00000000000000# Copyright, 2019, by Samuel G. D. Williams. # # 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. require 'async/http/server' require 'async/http/client' require 'async/http/endpoint' require 'async/io/ssl_socket' require 'async/rspec/reactor' require 'async/rspec/ssl' RSpec.describe Async::HTTP::Server, timeout: 5 do include_context Async::RSpec::Reactor include_context Async::RSpec::SSL::ValidCertificate describe "application layer protocol negotiation" do let(:server_context) do OpenSSL::SSL::SSLContext.new.tap do |context| context.cert = certificate context.alpn_select_cb = lambda do |protocols| protocols.last end context.key = key end end let(:client_context) do OpenSSL::SSL::SSLContext.new.tap do |context| context.cert_store = certificate_store context.alpn_protocols = ["h2", "http/1.1"] context.verify_mode = OpenSSL::SSL::VERIFY_PEER end end # Shared port for localhost network tests. let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: server_context)} let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: client_context)} it "client can get a resource via https" do server = Async::HTTP::Server.for(server_endpoint, protocol: Async::HTTP::Protocol::HTTP1) do |request| Protocol::HTTP::Response[200, {}, ['Hello World']] end client = Async::HTTP::Client.new(client_endpoint) Async do |task| server_task = task.async do server.run end response = client.get("/") expect(response).to be_success expect(response.read).to be == "Hello World" client.close server_task.stop end end end endruby-async-http-0.59.5/spec/async/http/statistics_spec.rb000066400000000000000000000036071436515316400234600ustar00rootroot00000000000000# Copyright, 2017, by Samuel G. D. Williams. # # 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. require_relative 'server_context' require 'async/http/statistics' RSpec.describe Async::HTTP::Statistics, timeout: 5 do include_context Async::HTTP::Server let(:protocol) {Async::HTTP::Protocol::HTTP1} let(:server) do Async::HTTP::Server.for(@bound_endpoint) do |request| statistics = described_class.start response = Protocol::HTTP::Response[200, {}, ["Hello ", "World!"]] statistics.wrap(response) do |statistics, error| expect(statistics.sent).to be == 12 expect(error).to be_nil end.tap do |response| expect(response.body).to receive(:complete_statistics).and_call_original end end end it "client can get resource" do response = client.get("/") expect(response.read).to be == "Hello World!" expect(response).to be_success end end ruby-async-http-0.59.5/spec/rack/000077500000000000000000000000001436515316400165455ustar00rootroot00000000000000ruby-async-http-0.59.5/spec/rack/test_spec.rb000066400000000000000000000034141436515316400210650ustar00rootroot00000000000000# Copyright, 2019, by Samuel G. D. Williams. # # 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. require 'rack/test' require 'rack/builder' require 'async' require 'async/http' RSpec.describe Rack::Test do include_context Async::RSpec::Reactor include Rack::Test::Methods let(:app) do Rack::Builder.new do def body(*chunks) body = Async::HTTP::Body::Writable.new Async do |task| chunks.each do |chunk| body.write(chunk) task.sleep(0.1) end body.close end return body end # This echos the body back. run lambda { |env| [200, {}, body("Hello", " ", "World", "!")] } end end it "can read response body" do get "/" expect(last_response.body).to be == "Hello World!" end end ruby-async-http-0.59.5/spec/spec_helper.rb000066400000000000000000000032001436515316400204360ustar00rootroot00000000000000# Copyright, 2019, by Samuel G. D. Williams. # # 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. require 'traces' require 'bundler/setup' require 'covered/rspec' require 'async/rspec' ENV['TRACES_BACKEND'] ||= 'traces/backend/test' RSpec.shared_context 'docstring as description' do let(:description) {self.class.metadata.fetch(:description_args).first} end RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" config.include_context 'docstring as description' config.expect_with :rspec do |c| c.syntax = :expect end end