Skip to content

Commit

Permalink
add Async::HTTP::Protocol::HTTP to auto-detect h1,h2 for inbound http…
Browse files Browse the repository at this point in the history
…:// connections
  • Loading branch information
zarqman authored and ioquatix committed Apr 23, 2024
1 parent fff9990 commit 9f892ba
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 11 deletions.
2 changes: 1 addition & 1 deletion async-http.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/async/http/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def protocol
if secure?
Protocol::HTTPS
else
Protocol::HTTP1
Protocol::HTTP
end
end
end
Expand Down
55 changes: 55 additions & 0 deletions lib/async/http/protocol/http.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions lib/async/http/protocol/http1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/async/http/protocol/http10.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Released under the MIT License.
# Copyright, 2017-2024, by Samuel Williams.
# Copyright, 2023, by Thomas Morgan.

require_relative 'http1'

Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/async/http/protocol/http11.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/async/http/protocol/http2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/async/http/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions test/async/http/protocol/http.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 9f892ba

Please sign in to comment.