Skip to content

Commit

Permalink
Add configurable and defaultable concepts.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Mar 11, 2025
1 parent 21e0198 commit 182bff8
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 22 deletions.
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/defaultable.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
module Defaultable
def self.extended(base)
base.const_set(:DEFAULT, base.new)
end

# The default instance of the protocol.
def default
self::DEFAULT
end

# 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
end
end
end
37 changes: 22 additions & 15 deletions lib/async/http/protocol/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,33 @@
# Copyright, 2024, by Thomas Morgan.
# Copyright, 2024, by Samuel Williams.

require_relative "defaultable"

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

# 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 self.protocol_for(stream)
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 @@ -33,38 +43,35 @@ def self.protocol_for(stream)
end

if preface == HTTP2_PREFACE
HTTP2
@http2
else
HTTP1
@http1
end
end

# 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 self.client(peer, **options)
options = options[protocol] || {}
def client(peer, **options)
options = options[@http1] || {}

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

# Create a server for an inbound connection. Able to detect HTTP1 vs HTTP2.
# 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 self.server(peer, **options)
def server(peer, **options)
stream = ::IO::Stream(peer)
protocol = protocol_for(stream)
options = options[protocol] || {}

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

# @returns [Array] The names of the supported protocols.
def self.names
["h2", "http/1.1", "http/1.0"]
end
extend Defaultable
end
end
end
Expand Down
4 changes: 4 additions & 0 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,6 +15,8 @@ module Async
module HTTP
module Protocol
module HTTP1
extend Configurable

VERSION = "HTTP/1.1"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
Expand Down
2 changes: 2 additions & 0 deletions lib/async/http/protocol/http10.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Async
module HTTP
module Protocol
module HTTP10
extend Configurable

VERSION = "HTTP/1.0"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
Expand Down
2 changes: 2 additions & 0 deletions lib/async/http/protocol/http11.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module Async
module HTTP
module Protocol
module HTTP11
extend Configurable

VERSION = "HTTP/1.1"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
Expand Down
4 changes: 4 additions & 0 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,6 +15,8 @@ module Async
module HTTP
module Protocol
module HTTP2
extend Configurable

VERSION = "HTTP/2"

# @returns [Boolean] Whether the protocol supports bidirectional communication.
Expand Down
27 changes: 20 additions & 7 deletions lib/async/http/protocol/https.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
# Copyright, 2018-2024, by Samuel Williams.
# Copyright, 2019, by Brian Morearty.

require_relative "defaultable"

require_relative "http10"
require_relative "http11"

require_relative "http2"

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
class HTTPS
# The protocol classes for each supported protocol.
HANDLERS = {
"h2" => HTTP2,
Expand All @@ -22,13 +23,23 @@ module HTTPS
nil => HTTP11,
}

def initialize(handlers = HANDLERS, **options)
@handlers = handlers
@options = options
end

def add(name, protocol, **options)
@handlers[name] = protocol
@options[protocol] = options
end

# Determine the protocol of the peer and return the appropriate protocol class.
#
# Use TLS Application Layer Protocol Negotiation (ALPN) to determine the protocol.
#
# @parameter peer [IO] The peer to communicate with.
# @returns [Class] The protocol class to use.
def self.protocol_for(peer)
def protocol_for(peer)
# alpn_protocol is only available if openssl v1.0.2+
name = peer.alpn_protocol

Expand All @@ -45,7 +56,7 @@ def self.protocol_for(peer)
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the client instance.
def self.client(peer, **options)
def client(peer, **options)
protocol = protocol_for(peer)
options = options[protocol] || {}

Expand All @@ -56,17 +67,19 @@ def self.client(peer, **options)
#
# @parameter peer [IO] The peer to communicate with.
# @parameter options [Hash] Options to pass to the server instance.
def self.server(peer, **options)
def server(peer, **options)
protocol = protocol_for(peer)
options = options[protocol] || {}

protocol.server(peer, **options)
end

# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN).
def self.names
HANDLERS.keys.compact
def names
@handlers.keys.compact
end

extend Defaultable
end
end
end
Expand Down
24 changes: 24 additions & 0 deletions test/async/http/protocol/http1.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

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

require "async/http/protocol/http"
require "async/http/a_protocol"

describe Async::HTTP::Protocol::HTTP1 do
with ".new" do
it "can configure the protocol" do
protocol = subject.new(
persistent: false,
maximum_line_length: 4096,
)

expect(protocol.options).to have_keys(
persistent: be == false,
maximum_line_length: be == 4096,
)
end
end
end

0 comments on commit 182bff8

Please sign in to comment.