Skip to content

Allow passing through configuration to underlying protocol. #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions lib/async/http/protocol/configurable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

module Async
module HTTP
module Protocol
class Configured
def initialize(protocol, **options)
@protocol = protocol
@options = options
end

# @attribute [Protocol] The underlying protocol.
attr :protocol

# @attribute [Hash] The options to pass to the protocol.
attr :options

def client(peer, **options)
options = @options.merge(options)
@protocol.client(peer, **options)
end

def server(peer, **options)
options = @options.merge(options)
@protocol.server(peer, **options)
end

def names
@protocol.names
end
end

module Configurable
def new(**options)
Configured.new(self, **options)
end
end
end
end
end
36 changes: 36 additions & 0 deletions lib/async/http/protocol/defaulton.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

module Async
module HTTP
module Protocol
# This module provides a default instance of the protocol, which can be used to create clients and servers. The name is a play on "Default" + "Singleton".
module Defaulton
def self.extended(base)
base.instance_variable_set(:@default, base.new)
end

attr_accessor :default

# Create a client for an outbound connection, using the default instance.
def client(peer, **options)
default.client(peer, **options)
end

# Create a server for an inbound connection, using the default instance.
def server(peer, **options)
default.server(peer, **options)
end

# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN), using the default instance.
def names
default.names
end
end

private_constant :Defaulton
end
end
end
54 changes: 38 additions & 16 deletions lib/async/http/protocol/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,33 @@
# Copyright, 2024, by Thomas Morgan.
# Copyright, 2024, by Samuel Williams.

require_relative "defaulton"

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
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2 connection preface.
class 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)
# Create a new HTTP protocol instance.
#
# @parameter http1 [HTTP1] The HTTP/1 protocol instance.
# @parameter http2 [HTTP2] The HTTP/2 protocol instance.
def initialize(http1: HTTP1, http2: HTTP2)
@http1 = http1
@http2 = http2
end

# Determine if the inbound connection is HTTP/1 or HTTP/2.
#
# @parameter stream [IO::Stream] The stream to detect the protocol for.
# @returns [Class] The protocol class to use.
def 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|
Expand All @@ -29,27 +43,35 @@ def self.protocol_for(stream)
end

if preface == HTTP2_PREFACE
HTTP2
@http2
else
HTTP1
@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)
# Create a client for an outbound connection. Defaults to HTTP/1 for plaintext connections.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
def client(peer, **options)
options = options[@http1] || {}

return @http1.client(peer, **options)
end

def self.server(peer, **options)
stream = ::IO::Stream(peer)
# Create a server for an inbound connection. Able to detect HTTP1 and HTTP2.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
def server(peer, **options)
stream = IO::Stream(peer)
protocol = protocol_for(stream)
options = options[protocol] || {}

return protocol_for(stream).server(stream, **options)
return protocol.server(stream, **options)
end

def self.names
["h2", "http/1.1", "http/1.0"]
end
extend Defaulton
end
end
end
Expand Down
23 changes: 19 additions & 4 deletions lib/async/http/protocol/http1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# Copyright, 2017-2024, by Samuel Williams.
# Copyright, 2024, by Thomas Morgan.

require_relative "configurable"

require_relative "http1/client"
require_relative "http1/server"

Expand All @@ -13,28 +15,41 @@ module Async
module HTTP
module Protocol
module HTTP1
extend Configurable

VERSION = "HTTP/1.1"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
def self.bidirectional?
true
end

# @returns [Boolean] Whether the protocol supports trailers.
def self.trailer?
true
end

def self.client(peer)
# Create a client for an outbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the client instance.
def self.client(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Client.new(stream, VERSION)
return HTTP1::Client.new(stream, VERSION, **options)
end

def self.server(peer)
# Create a server for an inbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the server instance.
def self.server(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Server.new(stream, VERSION)
return HTTP1::Server.new(stream, VERSION, **options)
end

# @returns [Array] The names of the supported protocol.
def self.names
["http/1.1", "http/1.0"]
end
Expand Down
5 changes: 3 additions & 2 deletions lib/async/http/protocol/http1/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ module HTTP
module Protocol
module HTTP1
class Connection < ::Protocol::HTTP1::Connection
def initialize(stream, version)
super(stream)
def initialize(stream, version, **options)
super(stream, **options)

# On the client side, we need to send the HTTP version with the initial request. On the server side, there are some scenarios (bad request) where we don't know the request version. In those cases, we use this value, which is either hard coded based on the protocol being used, OR could be negotiated during the connection setup (e.g. ALPN).
@version = version
end

Expand Down
21 changes: 17 additions & 4 deletions lib/async/http/protocol/http10.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,41 @@ module Async
module HTTP
module Protocol
module HTTP10
extend Configurable

VERSION = "HTTP/1.0"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
def self.bidirectional?
false
end

# @returns [Boolean] Whether the protocol supports trailers.
def self.trailer?
false
end

def self.client(peer)
# Create a client for an outbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the client instance.
def self.client(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Client.new(stream, VERSION)
return HTTP1::Client.new(stream, VERSION, **options)
end

def self.server(peer)
# Create a server for an inbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the server instance.
def self.server(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Server.new(stream, VERSION)
return HTTP1::Server.new(stream, VERSION, **options)
end

# @returns [Array] The names of the supported protocol.
def self.names
["http/1.0"]
end
Expand Down
21 changes: 17 additions & 4 deletions lib/async/http/protocol/http11.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,41 @@ module Async
module HTTP
module Protocol
module HTTP11
extend Configurable

VERSION = "HTTP/1.1"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
def self.bidirectional?
true
end

# @returns [Boolean] Whether the protocol supports trailers.
def self.trailer?
true
end

def self.client(peer)
# Create a client for an outbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the client instance.
def self.client(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Client.new(stream, VERSION)
return HTTP1::Client.new(stream, VERSION, **options)
end

def self.server(peer)
# Create a server for an inbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the server instance.
def self.server(peer, **options)
stream = ::IO::Stream(peer)

return HTTP1::Server.new(stream, VERSION)
return HTTP1::Server.new(stream, VERSION, **options)
end

# @returns [Array] The names of the supported protocol.
def self.names
["http/1.1"]
end
Expand Down
21 changes: 19 additions & 2 deletions lib/async/http/protocol/http2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# Copyright, 2018-2024, by Samuel Williams.
# Copyright, 2024, by Thomas Morgan.

require_relative "configurable"

require_relative "http2/client"
require_relative "http2/server"

Expand All @@ -13,23 +15,29 @@ module Async
module HTTP
module Protocol
module HTTP2
extend Configurable

VERSION = "HTTP/2"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
def self.bidirectional?
true
end

# @returns [Boolean] Whether the protocol supports trailers.
def self.trailer?
true
end

# The default settings for the client.
CLIENT_SETTINGS = {
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000,
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
}

# The default settings for the server.
SERVER_SETTINGS = {
# We choose a lower maximum concurrent streams to avoid overloading a single connection/thread.
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 128,
Expand All @@ -39,7 +47,11 @@ def self.trailer?
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
}

def self.client(peer, settings = CLIENT_SETTINGS)
# Create a client for an outbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the client instance.
def self.client(peer, settings: CLIENT_SETTINGS)
stream = ::IO::Stream(peer)
client = Client.new(stream)

Expand All @@ -49,7 +61,11 @@ def self.client(peer, settings = CLIENT_SETTINGS)
return client
end

def self.server(peer, settings = SERVER_SETTINGS)
# Create a server for an inbound connection.
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the server instance.
def self.server(peer, settings: SERVER_SETTINGS)
stream = ::IO::Stream(peer)
server = Server.new(stream)

Expand All @@ -59,6 +75,7 @@ def self.server(peer, settings = SERVER_SETTINGS)
return server
end

# @returns [Array] The names of the supported protocol.
def self.names
["h2"]
end
Expand Down
Loading
Loading