diff --git a/lib/async/http/endpoint.rb b/lib/async/http/endpoint.rb index 995d2f9e..f4b1651b 100644 --- a/lib/async/http/endpoint.rb +++ b/lib/async/http/endpoint.rb @@ -8,7 +8,7 @@ require 'io/endpoint/host_endpoint' require 'io/endpoint/ssl_endpoint' -require_relative 'protocol/http1' +require_relative 'protocol/http' require_relative 'protocol/https' module Async @@ -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..e4024dbf 100644 --- a/lib/async/http/protocol/http1.rb +++ b/lib/async/http/protocol/http1.rb @@ -2,11 +2,12 @@ # Released under the MIT License. # Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2023, by Thomas Morgan. require_relative 'http1/client' require_relative 'http1/server' -require 'io/stream/buffered' +require 'io/stream' module Async module HTTP @@ -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..bc560d76 100644 --- a/lib/async/http/protocol/http2.rb +++ b/lib/async/http/protocol/http2.rb @@ -2,11 +2,12 @@ # Released under the MIT License. # Copyright, 2018-2024, by Samuel Williams. +# Copyright, 2023, by Thomas Morgan. require_relative 'http2/client' require_relative 'http2/server' -require 'io/stream/buffered' +require 'io/stream' module Async module HTTP @@ -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