From 9f892ba33a6fb617d7a0dc74c470ae8d29f65cd3 Mon Sep 17 00:00:00 2001 From: thomas morgan Date: Thu, 24 Aug 2023 12:15:28 -0600 Subject: [PATCH] add Async::HTTP::Protocol::HTTP to auto-detect h1,h2 for inbound http:// connections --- async-http.gemspec | 2 +- lib/async/http/endpoint.rb | 2 +- lib/async/http/protocol/http.rb | 55 +++++++++++++++++++++++++++++++ lib/async/http/protocol/http1.rb | 5 +-- lib/async/http/protocol/http10.rb | 5 +-- lib/async/http/protocol/http11.rb | 5 +-- lib/async/http/protocol/http2.rb | 5 +-- test/async/http/endpoint.rb | 2 +- test/async/http/protocol/http.rb | 37 +++++++++++++++++++++ 9 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 lib/async/http/protocol/http.rb create mode 100755 test/async/http/protocol/http.rb diff --git a/async-http.gemspec b/async-http.gemspec index da7108ce..be94ab82 100644 --- a/async-http.gemspec +++ b/async-http.gemspec @@ -27,7 +27,7 @@ Gem::Specification.new do |spec| spec.add_dependency "async", ">= 2.10.2" spec.add_dependency "async-pool", ">= 0.6.1" spec.add_dependency "io-endpoint", "~> 0.10.0" - spec.add_dependency "io-stream", "~> 0.3.0" + spec.add_dependency "io-stream", "~> 0.4.0" spec.add_dependency "protocol-http", "~> 0.26.0" spec.add_dependency "protocol-http1", "~> 0.19.0" spec.add_dependency "protocol-http2", "~> 0.17.0" diff --git a/lib/async/http/endpoint.rb b/lib/async/http/endpoint.rb index 995d2f9e..1ce47eaa 100644 --- a/lib/async/http/endpoint.rb +++ b/lib/async/http/endpoint.rb @@ -84,7 +84,7 @@ def protocol if secure? Protocol::HTTPS else - Protocol::HTTP1 + Protocol::HTTP end end end diff --git a/lib/async/http/protocol/http.rb b/lib/async/http/protocol/http.rb new file mode 100644 index 00000000..c37616a3 --- /dev/null +++ b/lib/async/http/protocol/http.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Thomas Morgan. + +require_relative 'http1' +require_relative 'http2' + +module Async + module HTTP + module Protocol + # HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2 + # connection preface. + module HTTP + HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize + + def self.protocol_for(stream) + # Detect HTTP/2 connection preface + # https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4 + preface = stream.peek do |read_buffer| + if read_buffer.bytesize >= HTTP2_PREFACE_SIZE + break read_buffer[0, HTTP2_PREFACE_SIZE] + elsif read_buffer.bytesize > 0 + # If partial read_buffer already doesn't match, no need to wait for more bytes. + break read_buffer unless HTTP2_PREFACE[read_buffer] + end + end + + if preface == HTTP2_PREFACE + HTTP2 + else + HTTP1 + end + end + + # Only inbound connections can detect HTTP1 vs HTTP2 for http://. + # Outbound connections default to HTTP1. + def self.client(peer, **options) + HTTP1.client(peer, **options) + end + + def self.server(peer, **options) + stream = ::IO::Stream(peer) + + return protocol_for(stream).server(stream, **options) + end + + def self.names + ["h2", "http/1.1", "http/1.0"] + end + end + end + end +end diff --git a/lib/async/http/protocol/http1.rb b/lib/async/http/protocol/http1.rb index e30f3410..c979aa3d 100644 --- a/lib/async/http/protocol/http1.rb +++ b/lib/async/http/protocol/http1.rb @@ -2,6 +2,7 @@ # Released under the MIT License. # Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2023, by Thomas Morgan. require_relative 'http1/client' require_relative 'http1/server' @@ -23,13 +24,13 @@ def self.trailer? end def self.client(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Server.new(stream, VERSION) end diff --git a/lib/async/http/protocol/http10.rb b/lib/async/http/protocol/http10.rb index 9066d693..5d5e9b35 100755 --- a/lib/async/http/protocol/http10.rb +++ b/lib/async/http/protocol/http10.rb @@ -2,6 +2,7 @@ # Released under the MIT License. # Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2023, by Thomas Morgan. require_relative 'http1' @@ -20,13 +21,13 @@ def self.trailer? end def self.client(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Server.new(stream, VERSION) end diff --git a/lib/async/http/protocol/http11.rb b/lib/async/http/protocol/http11.rb index 083dc141..46a3762a 100644 --- a/lib/async/http/protocol/http11.rb +++ b/lib/async/http/protocol/http11.rb @@ -3,6 +3,7 @@ # Released under the MIT License. # Copyright, 2017-2024, by Samuel Williams. # Copyright, 2018, by Janko Marohnić. +# Copyright, 2023, by Thomas Morgan. require_relative 'http1' @@ -21,13 +22,13 @@ def self.trailer? end def self.client(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Client.new(stream, VERSION) end def self.server(peer) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) return HTTP1::Server.new(stream, VERSION) end diff --git a/lib/async/http/protocol/http2.rb b/lib/async/http/protocol/http2.rb index 7a75a34a..a39c4108 100644 --- a/lib/async/http/protocol/http2.rb +++ b/lib/async/http/protocol/http2.rb @@ -2,6 +2,7 @@ # Released under the MIT License. # Copyright, 2018-2024, by Samuel Williams. +# Copyright, 2023, by Thomas Morgan. require_relative 'http2/client' require_relative 'http2/server' @@ -37,7 +38,7 @@ def self.trailer? } def self.client(peer, settings = CLIENT_SETTINGS) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) client = Client.new(stream) client.send_connection_preface(settings) @@ -47,7 +48,7 @@ def self.client(peer, settings = CLIENT_SETTINGS) end def self.server(peer, settings = SERVER_SETTINGS) - stream = ::IO::Stream::Buffered.wrap(peer) + stream = ::IO::Stream(peer) server = Server.new(stream) server.read_connection_preface(settings) diff --git a/test/async/http/endpoint.rb b/test/async/http/endpoint.rb index e04169bd..c16f4ef6 100644 --- a/test/async/http/endpoint.rb +++ b/test/async/http/endpoint.rb @@ -155,7 +155,7 @@ describe Async::HTTP::Endpoint.parse("http://www.google.com/search") do it "should select the correct protocol" do - expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP1 + expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP end it "should parse the correct hostname" do diff --git a/test/async/http/protocol/http.rb b/test/async/http/protocol/http.rb new file mode 100755 index 00000000..0de6e7c7 --- /dev/null +++ b/test/async/http/protocol/http.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Thomas Morgan. + +require 'async/http/protocol/http' +require 'async/http/a_protocol' + +describe Async::HTTP::Protocol::HTTP do + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} + + with 'http11 client' do + it 'should make a successful request' do + response = client.get('/') + expect(response).to be(:success?) + expect(response.version).to be == 'HTTP/1.1' + response.read + end + end + + with 'http2 client' do + def make_client(endpoint, **options) + options[:protocol] = Async::HTTP::Protocol::HTTP2 + super + end + + it 'should make a successful request' do + response = client.get('/') + expect(response).to be(:success?) + expect(response.version).to be == 'HTTP/2' + response.read + end + end + end +end