Skip to content

Commit c0618ba

Browse files
authored
Allow passing through configuration to underlying protocol. (#198)
1 parent 4308c82 commit c0618ba

File tree

14 files changed

+408
-43
lines changed

14 files changed

+408
-43
lines changed
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
module Async
7+
module HTTP
8+
module Protocol
9+
class Configured
10+
def initialize(protocol, **options)
11+
@protocol = protocol
12+
@options = options
13+
end
14+
15+
# @attribute [Protocol] The underlying protocol.
16+
attr :protocol
17+
18+
# @attribute [Hash] The options to pass to the protocol.
19+
attr :options
20+
21+
def client(peer, **options)
22+
options = @options.merge(options)
23+
@protocol.client(peer, **options)
24+
end
25+
26+
def server(peer, **options)
27+
options = @options.merge(options)
28+
@protocol.server(peer, **options)
29+
end
30+
31+
def names
32+
@protocol.names
33+
end
34+
end
35+
36+
module Configurable
37+
def new(**options)
38+
Configured.new(self, **options)
39+
end
40+
end
41+
end
42+
end
43+
end

lib/async/http/protocol/defaulton.rb

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
module Async
7+
module HTTP
8+
module Protocol
9+
# 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".
10+
module Defaulton
11+
def self.extended(base)
12+
base.instance_variable_set(:@default, base.new)
13+
end
14+
15+
attr_accessor :default
16+
17+
# Create a client for an outbound connection, using the default instance.
18+
def client(peer, **options)
19+
default.client(peer, **options)
20+
end
21+
22+
# Create a server for an inbound connection, using the default instance.
23+
def server(peer, **options)
24+
default.server(peer, **options)
25+
end
26+
27+
# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN), using the default instance.
28+
def names
29+
default.names
30+
end
31+
end
32+
33+
private_constant :Defaulton
34+
end
35+
end
36+
end

lib/async/http/protocol/http.rb

+38-16
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,33 @@
44
# Copyright, 2024, by Thomas Morgan.
55
# Copyright, 2024, by Samuel Williams.
66

7+
require_relative "defaulton"
8+
79
require_relative "http1"
810
require_relative "http2"
911

1012
module Async
1113
module HTTP
1214
module Protocol
13-
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2
14-
# connection preface.
15-
module HTTP
15+
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2 connection preface.
16+
class HTTP
1617
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
1718
HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize
1819

19-
def self.protocol_for(stream)
20+
# Create a new HTTP protocol instance.
21+
#
22+
# @parameter http1 [HTTP1] The HTTP/1 protocol instance.
23+
# @parameter http2 [HTTP2] The HTTP/2 protocol instance.
24+
def initialize(http1: HTTP1, http2: HTTP2)
25+
@http1 = http1
26+
@http2 = http2
27+
end
28+
29+
# Determine if the inbound connection is HTTP/1 or HTTP/2.
30+
#
31+
# @parameter stream [IO::Stream] The stream to detect the protocol for.
32+
# @returns [Class] The protocol class to use.
33+
def protocol_for(stream)
2034
# Detect HTTP/2 connection preface
2135
# https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4
2236
preface = stream.peek do |read_buffer|
@@ -29,27 +43,35 @@ def self.protocol_for(stream)
2943
end
3044

3145
if preface == HTTP2_PREFACE
32-
HTTP2
46+
@http2
3347
else
34-
HTTP1
48+
@http1
3549
end
3650
end
3751

38-
# Only inbound connections can detect HTTP1 vs HTTP2 for http://.
39-
# Outbound connections default to HTTP1.
40-
def self.client(peer, **options)
41-
HTTP1.client(peer, **options)
52+
# Create a client for an outbound connection. Defaults to HTTP/1 for plaintext connections.
53+
#
54+
# @parameter peer [IO] The peer to communicate with.
55+
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
56+
def client(peer, **options)
57+
options = options[@http1] || {}
58+
59+
return @http1.client(peer, **options)
4260
end
4361

44-
def self.server(peer, **options)
45-
stream = ::IO::Stream(peer)
62+
# Create a server for an inbound connection. Able to detect HTTP1 and HTTP2.
63+
#
64+
# @parameter peer [IO] The peer to communicate with.
65+
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
66+
def server(peer, **options)
67+
stream = IO::Stream(peer)
68+
protocol = protocol_for(stream)
69+
options = options[protocol] || {}
4670

47-
return protocol_for(stream).server(stream, **options)
71+
return protocol.server(stream, **options)
4872
end
4973

50-
def self.names
51-
["h2", "http/1.1", "http/1.0"]
52-
end
74+
extend Defaulton
5375
end
5476
end
5577
end

lib/async/http/protocol/http1.rb

+19-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# Copyright, 2017-2024, by Samuel Williams.
55
# Copyright, 2024, by Thomas Morgan.
66

7+
require_relative "configurable"
8+
79
require_relative "http1/client"
810
require_relative "http1/server"
911

@@ -13,28 +15,41 @@ module Async
1315
module HTTP
1416
module Protocol
1517
module HTTP1
18+
extend Configurable
19+
1620
VERSION = "HTTP/1.1"
1721

22+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
1823
def self.bidirectional?
1924
true
2025
end
2126

27+
# @returns [Boolean] Whether the protocol supports trailers.
2228
def self.trailer?
2329
true
2430
end
2531

26-
def self.client(peer)
32+
# Create a client for an outbound connection.
33+
#
34+
# @parameter peer [IO] The peer to communicate with.
35+
# @parameter options [Hash] Options to pass to the client instance.
36+
def self.client(peer, **options)
2737
stream = ::IO::Stream(peer)
2838

29-
return HTTP1::Client.new(stream, VERSION)
39+
return HTTP1::Client.new(stream, VERSION, **options)
3040
end
3141

32-
def self.server(peer)
42+
# Create a server for an inbound connection.
43+
#
44+
# @parameter peer [IO] The peer to communicate with.
45+
# @parameter options [Hash] Options to pass to the server instance.
46+
def self.server(peer, **options)
3347
stream = ::IO::Stream(peer)
3448

35-
return HTTP1::Server.new(stream, VERSION)
49+
return HTTP1::Server.new(stream, VERSION, **options)
3650
end
3751

52+
# @returns [Array] The names of the supported protocol.
3853
def self.names
3954
["http/1.1", "http/1.0"]
4055
end

lib/async/http/protocol/http1/connection.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ module HTTP
1414
module Protocol
1515
module HTTP1
1616
class Connection < ::Protocol::HTTP1::Connection
17-
def initialize(stream, version)
18-
super(stream)
17+
def initialize(stream, version, **options)
18+
super(stream, **options)
1919

20+
# 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).
2021
@version = version
2122
end
2223

lib/async/http/protocol/http10.rb

+17-4
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,41 @@ module Async
1010
module HTTP
1111
module Protocol
1212
module HTTP10
13+
extend Configurable
14+
1315
VERSION = "HTTP/1.0"
1416

17+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
1518
def self.bidirectional?
1619
false
1720
end
1821

22+
# @returns [Boolean] Whether the protocol supports trailers.
1923
def self.trailer?
2024
false
2125
end
2226

23-
def self.client(peer)
27+
# Create a client for an outbound connection.
28+
#
29+
# @parameter peer [IO] The peer to communicate with.
30+
# @parameter options [Hash] Options to pass to the client instance.
31+
def self.client(peer, **options)
2432
stream = ::IO::Stream(peer)
2533

26-
return HTTP1::Client.new(stream, VERSION)
34+
return HTTP1::Client.new(stream, VERSION, **options)
2735
end
2836

29-
def self.server(peer)
37+
# Create a server for an inbound connection.
38+
#
39+
# @parameter peer [IO] The peer to communicate with.
40+
# @parameter options [Hash] Options to pass to the server instance.
41+
def self.server(peer, **options)
3042
stream = ::IO::Stream(peer)
3143

32-
return HTTP1::Server.new(stream, VERSION)
44+
return HTTP1::Server.new(stream, VERSION, **options)
3345
end
3446

47+
# @returns [Array] The names of the supported protocol.
3548
def self.names
3649
["http/1.0"]
3750
end

lib/async/http/protocol/http11.rb

+17-4
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,41 @@ module Async
1111
module HTTP
1212
module Protocol
1313
module HTTP11
14+
extend Configurable
15+
1416
VERSION = "HTTP/1.1"
1517

18+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
1619
def self.bidirectional?
1720
true
1821
end
1922

23+
# @returns [Boolean] Whether the protocol supports trailers.
2024
def self.trailer?
2125
true
2226
end
2327

24-
def self.client(peer)
28+
# Create a client for an outbound connection.
29+
#
30+
# @parameter peer [IO] The peer to communicate with.
31+
# @parameter options [Hash] Options to pass to the client instance.
32+
def self.client(peer, **options)
2533
stream = ::IO::Stream(peer)
2634

27-
return HTTP1::Client.new(stream, VERSION)
35+
return HTTP1::Client.new(stream, VERSION, **options)
2836
end
2937

30-
def self.server(peer)
38+
# Create a server for an inbound connection.
39+
#
40+
# @parameter peer [IO] The peer to communicate with.
41+
# @parameter options [Hash] Options to pass to the server instance.
42+
def self.server(peer, **options)
3143
stream = ::IO::Stream(peer)
3244

33-
return HTTP1::Server.new(stream, VERSION)
45+
return HTTP1::Server.new(stream, VERSION, **options)
3446
end
3547

48+
# @returns [Array] The names of the supported protocol.
3649
def self.names
3750
["http/1.1"]
3851
end

lib/async/http/protocol/http2.rb

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# Copyright, 2018-2024, by Samuel Williams.
55
# Copyright, 2024, by Thomas Morgan.
66

7+
require_relative "configurable"
8+
79
require_relative "http2/client"
810
require_relative "http2/server"
911

@@ -13,23 +15,29 @@ module Async
1315
module HTTP
1416
module Protocol
1517
module HTTP2
18+
extend Configurable
19+
1620
VERSION = "HTTP/2"
1721

22+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
1823
def self.bidirectional?
1924
true
2025
end
2126

27+
# @returns [Boolean] Whether the protocol supports trailers.
2228
def self.trailer?
2329
true
2430
end
2531

32+
# The default settings for the client.
2633
CLIENT_SETTINGS = {
2734
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
2835
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
2936
::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000,
3037
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
3138
}
3239

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

42-
def self.client(peer, settings = CLIENT_SETTINGS)
50+
# Create a client for an outbound connection.
51+
#
52+
# @parameter peer [IO] The peer to communicate with.
53+
# @parameter options [Hash] Options to pass to the client instance.
54+
def self.client(peer, settings: CLIENT_SETTINGS)
4355
stream = ::IO::Stream(peer)
4456
client = Client.new(stream)
4557

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

52-
def self.server(peer, settings = SERVER_SETTINGS)
64+
# Create a server for an inbound connection.
65+
#
66+
# @parameter peer [IO] The peer to communicate with.
67+
# @parameter options [Hash] Options to pass to the server instance.
68+
def self.server(peer, settings: SERVER_SETTINGS)
5369
stream = ::IO::Stream(peer)
5470
server = Server.new(stream)
5571

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

78+
# @returns [Array] The names of the supported protocol.
6279
def self.names
6380
["h2"]
6481
end

0 commit comments

Comments
 (0)