async-http 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/async/http/h2spec.rb +1 -1
- data/lib/async/http/client.rb +2 -2
- data/lib/async/http/endpoint.rb +1 -1
- data/lib/async/http/protocol/configurable.rb +43 -0
- data/lib/async/http/protocol/defaulton.rb +36 -0
- data/lib/async/http/protocol/http.rb +38 -16
- data/lib/async/http/protocol/http1/client.rb +11 -4
- data/lib/async/http/protocol/http1/connection.rb +3 -2
- data/lib/async/http/protocol/http1/request.rb +29 -5
- data/lib/async/http/protocol/http1/server.rb +1 -1
- data/lib/async/http/protocol/http1.rb +19 -4
- data/lib/async/http/protocol/http10.rb +17 -4
- data/lib/async/http/protocol/http11.rb +17 -4
- data/lib/async/http/protocol/http2/client.rb +0 -2
- data/lib/async/http/protocol/http2/connection.rb +2 -3
- data/lib/async/http/protocol/http2/request.rb +2 -2
- data/lib/async/http/protocol/http2/response.rb +5 -2
- data/lib/async/http/protocol/http2/server.rb +0 -2
- data/lib/async/http/protocol/http2/stream.rb +1 -1
- data/lib/async/http/protocol/http2.rb +19 -2
- data/lib/async/http/protocol/https.rb +45 -11
- data/lib/async/http/proxy.rb +2 -2
- data/lib/async/http/server.rb +2 -2
- data/lib/async/http/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +8 -4
- data/releases.md +81 -0
- data.tar.gz.sig +0 -0
- metadata +22 -25
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 605cb4a26c37dbfdaa3225fb306b3c2bc9e01da814094babb8e46cdb05594999
|
4
|
+
data.tar.gz: 40c28864c03492bc20d8aa02947210841ca5f2c75b841e222c90d8f3a36102af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95eb6bb5d079d59b9ed9698d648bd709a7f60ae1a5833547cadd9a11def31e6e8d99b929650df676c0ed215a0719bd604ffc7c57c6e047cdefaa0ac11ff2d71b
|
7
|
+
data.tar.gz: 0cdbfe53cbcf3acb1529536ddac851c09e53dd9b7c40fb1d1059abc073138f67cda426a5825966052d52a2d35f39170047c814f7ca6168b25f149fb2baf1e7e0
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/bake/async/http/h2spec.rb
CHANGED
@@ -30,7 +30,7 @@ def server
|
|
30
30
|
|
31
31
|
container = Async::Container.new
|
32
32
|
|
33
|
-
Console.
|
33
|
+
Console.info(self){"Starting server..."}
|
34
34
|
|
35
35
|
container.run(count: 1) do
|
36
36
|
server = Async::HTTP::Server.for(endpoint, protocol: Async::HTTP::Protocol::HTTP2, scheme: "https") do |request|
|
data/lib/async/http/client.rb
CHANGED
@@ -81,7 +81,7 @@ module Async
|
|
81
81
|
|
82
82
|
def close
|
83
83
|
while @pool.busy?
|
84
|
-
Console.
|
84
|
+
Console.warn(self) {"Waiting for #{@protocol} pool to drain: #{@pool}"}
|
85
85
|
@pool.wait
|
86
86
|
end
|
87
87
|
|
@@ -164,7 +164,7 @@ module Async
|
|
164
164
|
self.assign_default_tags(options[:tags] ||= {})
|
165
165
|
|
166
166
|
Async::Pool::Controller.wrap(**options) do
|
167
|
-
Console.
|
167
|
+
Console.debug(self) {"Making connection to #{@endpoint.inspect}"}
|
168
168
|
|
169
169
|
@protocol.client(@endpoint.connect)
|
170
170
|
end
|
data/lib/async/http/endpoint.rb
CHANGED
@@ -40,7 +40,7 @@ module Async
|
|
40
40
|
#
|
41
41
|
# @parameter scheme [String] The scheme to use, e.g. "http" or "https".
|
42
42
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
43
|
-
# @parameter *options [Hash] Additional options, passed to {
|
43
|
+
# @parameter *options [Hash] Additional options, passed to {initialize}.
|
44
44
|
def self.for(scheme, hostname, path = "/", **options)
|
45
45
|
# TODO: Consider using URI.for once it becomes available:
|
46
46
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
@@ -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
|
@@ -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
|
@@ -4,19 +4,33 @@
|
|
4
4
|
# Copyright, 2024, by Thomas Morgan.
|
5
5
|
# Copyright, 2024, by Samuel Williams.
|
6
6
|
|
7
|
+
require_relative "defaulton"
|
8
|
+
|
7
9
|
require_relative "http1"
|
8
10
|
require_relative "http2"
|
9
11
|
|
10
12
|
module Async
|
11
13
|
module HTTP
|
12
14
|
module Protocol
|
13
|
-
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2
|
14
|
-
|
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
|
16
17
|
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
17
18
|
HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize
|
18
19
|
|
19
|
-
|
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)
|
20
34
|
# Detect HTTP/2 connection preface
|
21
35
|
# https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4
|
22
36
|
preface = stream.peek do |read_buffer|
|
@@ -29,27 +43,35 @@ module Async
|
|
29
43
|
end
|
30
44
|
|
31
45
|
if preface == HTTP2_PREFACE
|
32
|
-
|
46
|
+
@http2
|
33
47
|
else
|
34
|
-
|
48
|
+
@http1
|
35
49
|
end
|
36
50
|
end
|
37
51
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
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)
|
42
60
|
end
|
43
61
|
|
44
|
-
|
45
|
-
|
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] || {}
|
46
70
|
|
47
|
-
return
|
71
|
+
return protocol.server(stream, **options)
|
48
72
|
end
|
49
73
|
|
50
|
-
|
51
|
-
["h2", "http/1.1", "http/1.0"]
|
52
|
-
end
|
74
|
+
extend Defaulton
|
53
75
|
end
|
54
76
|
end
|
55
77
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "connection"
|
7
7
|
|
@@ -32,14 +32,21 @@ module Async
|
|
32
32
|
|
33
33
|
# Used by the client to send requests to the remote server.
|
34
34
|
def call(request, task: Task.current)
|
35
|
-
Console.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
36
|
-
|
37
35
|
# Mark the start of the trailers:
|
38
36
|
trailer = request.headers.trailer!
|
39
37
|
|
40
38
|
# We carefully interpret https://tools.ietf.org/html/rfc7230#section-6.3.1 to implement this correctly.
|
41
39
|
begin
|
42
|
-
|
40
|
+
target = request.path
|
41
|
+
authority = request.authority
|
42
|
+
|
43
|
+
# If we are using a CONNECT request, we need to use the authority as the target:
|
44
|
+
if request.connect?
|
45
|
+
target = authority
|
46
|
+
authority = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
write_request(authority, request.method, target, @version, request.headers)
|
43
50
|
rescue
|
44
51
|
# If we fail to fully write the request and body, we can retry this request.
|
45
52
|
raise RequestFailed
|
@@ -14,9 +14,10 @@ module Async
|
|
14
14
|
module Protocol
|
15
15
|
module HTTP1
|
16
16
|
class Connection < ::Protocol::HTTP1::Connection
|
17
|
-
def initialize(stream, version)
|
18
|
-
super(stream)
|
17
|
+
def initialize(stream, version, **options)
|
18
|
+
super(stream, **options)
|
19
19
|
|
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).
|
20
21
|
@version = version
|
21
22
|
end
|
22
23
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "../request"
|
7
7
|
|
@@ -10,21 +10,45 @@ module Async
|
|
10
10
|
module Protocol
|
11
11
|
module HTTP1
|
12
12
|
class Request < Protocol::Request
|
13
|
+
def self.valid_path?(target)
|
14
|
+
if target.start_with?("/")
|
15
|
+
return true
|
16
|
+
elsif target == "*"
|
17
|
+
return true
|
18
|
+
else
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
URI_PATTERN = %r{\A(?<scheme>[^:/]+)://(?<authority>[^/]+)(?<path>.*)\z}
|
24
|
+
|
13
25
|
def self.read(connection)
|
14
|
-
|
15
|
-
|
26
|
+
connection.read_request do |authority, method, target, version, headers, body|
|
27
|
+
if method == ::Protocol::HTTP::Methods::CONNECT
|
28
|
+
# We put the target into the authority field for CONNECT requests, as per HTTP/2 semantics.
|
29
|
+
self.new(connection, nil, target, method, nil, version, headers, body)
|
30
|
+
elsif valid_path?(target)
|
31
|
+
# This is a valid request.
|
32
|
+
self.new(connection, nil, authority, method, target, version, headers, body)
|
33
|
+
elsif match = target.match(URI_PATTERN)
|
34
|
+
# We map the incoming absolute URI target to the scheme, authority, and path fields of the request.
|
35
|
+
self.new(connection, match[:scheme], match[:authority], method, match[:path], version, headers, body)
|
36
|
+
else
|
37
|
+
# This is an invalid request.
|
38
|
+
raise ::Protocol::HTTP1::BadRequest.new("Invalid request target: #{target}")
|
39
|
+
end
|
16
40
|
end
|
17
41
|
end
|
18
42
|
|
19
43
|
UPGRADE = "upgrade"
|
20
44
|
|
21
|
-
def initialize(connection, authority, method, path, version, headers, body)
|
45
|
+
def initialize(connection, scheme, authority, method, path, version, headers, body)
|
22
46
|
@connection = connection
|
23
47
|
|
24
48
|
# HTTP/1 requests with an upgrade header (which can contain zero or more values) are extracted into the protocol field of the request, and we expect a response to select one of those protocols with a status code of 101 Switching Protocols.
|
25
49
|
protocol = headers.delete("upgrade")
|
26
50
|
|
27
|
-
super(
|
51
|
+
super(scheme, authority, method, path, version, headers, body, protocol, self.public_method(:write_interim_response))
|
28
52
|
end
|
29
53
|
|
30
54
|
def connection
|
@@ -34,7 +34,7 @@ module Async
|
|
34
34
|
write_body(@version, nil)
|
35
35
|
rescue => error
|
36
36
|
# At this point, there is very little we can do to recover:
|
37
|
-
Console
|
37
|
+
Console.debug(self, "Failed to write failure response!", error)
|
38
38
|
end
|
39
39
|
|
40
40
|
def next_request
|
@@ -4,6 +4,8 @@
|
|
4
4
|
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2024, by Thomas Morgan.
|
6
6
|
|
7
|
+
require_relative "configurable"
|
8
|
+
|
7
9
|
require_relative "http1/client"
|
8
10
|
require_relative "http1/server"
|
9
11
|
|
@@ -13,28 +15,41 @@ module Async
|
|
13
15
|
module HTTP
|
14
16
|
module Protocol
|
15
17
|
module HTTP1
|
18
|
+
extend Configurable
|
19
|
+
|
16
20
|
VERSION = "HTTP/1.1"
|
17
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
18
23
|
def self.bidirectional?
|
19
24
|
true
|
20
25
|
end
|
21
26
|
|
27
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
22
28
|
def self.trailer?
|
23
29
|
true
|
24
30
|
end
|
25
31
|
|
26
|
-
|
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)
|
27
37
|
stream = ::IO::Stream(peer)
|
28
38
|
|
29
|
-
return HTTP1::Client.new(stream, VERSION)
|
39
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
30
40
|
end
|
31
41
|
|
32
|
-
|
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)
|
33
47
|
stream = ::IO::Stream(peer)
|
34
48
|
|
35
|
-
return HTTP1::Server.new(stream, VERSION)
|
49
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
36
50
|
end
|
37
51
|
|
52
|
+
# @returns [Array] The names of the supported protocol.
|
38
53
|
def self.names
|
39
54
|
["http/1.1", "http/1.0"]
|
40
55
|
end
|
@@ -10,28 +10,41 @@ module Async
|
|
10
10
|
module HTTP
|
11
11
|
module Protocol
|
12
12
|
module HTTP10
|
13
|
+
extend Configurable
|
14
|
+
|
13
15
|
VERSION = "HTTP/1.0"
|
14
16
|
|
17
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
15
18
|
def self.bidirectional?
|
16
19
|
false
|
17
20
|
end
|
18
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
19
23
|
def self.trailer?
|
20
24
|
false
|
21
25
|
end
|
22
26
|
|
23
|
-
|
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)
|
24
32
|
stream = ::IO::Stream(peer)
|
25
33
|
|
26
|
-
return HTTP1::Client.new(stream, VERSION)
|
34
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
27
35
|
end
|
28
36
|
|
29
|
-
|
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)
|
30
42
|
stream = ::IO::Stream(peer)
|
31
43
|
|
32
|
-
return HTTP1::Server.new(stream, VERSION)
|
44
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
33
45
|
end
|
34
46
|
|
47
|
+
# @returns [Array] The names of the supported protocol.
|
35
48
|
def self.names
|
36
49
|
["http/1.0"]
|
37
50
|
end
|
@@ -11,28 +11,41 @@ module Async
|
|
11
11
|
module HTTP
|
12
12
|
module Protocol
|
13
13
|
module HTTP11
|
14
|
+
extend Configurable
|
15
|
+
|
14
16
|
VERSION = "HTTP/1.1"
|
15
17
|
|
18
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
16
19
|
def self.bidirectional?
|
17
20
|
true
|
18
21
|
end
|
19
22
|
|
23
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
20
24
|
def self.trailer?
|
21
25
|
true
|
22
26
|
end
|
23
27
|
|
24
|
-
|
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)
|
25
33
|
stream = ::IO::Stream(peer)
|
26
34
|
|
27
|
-
return HTTP1::Client.new(stream, VERSION)
|
35
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
28
36
|
end
|
29
37
|
|
30
|
-
|
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)
|
31
43
|
stream = ::IO::Stream(peer)
|
32
44
|
|
33
|
-
return HTTP1::Server.new(stream, VERSION)
|
45
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
34
46
|
end
|
35
47
|
|
48
|
+
# @returns [Array] The names of the supported protocol.
|
36
49
|
def self.names
|
37
50
|
["http/1.1"]
|
38
51
|
end
|
@@ -26,10 +26,9 @@ module Async
|
|
26
26
|
TRAILER = "trailer".freeze
|
27
27
|
|
28
28
|
module Connection
|
29
|
-
def initialize(
|
29
|
+
def initialize(...)
|
30
30
|
super
|
31
31
|
|
32
|
-
@count = 0
|
33
32
|
@reader = nil
|
34
33
|
|
35
34
|
# Writing multiple frames at the same time can cause odd problems if frames are only partially written. So we use a semaphore to ensure frames are written in their entirety.
|
@@ -41,7 +40,7 @@ module Async
|
|
41
40
|
end
|
42
41
|
|
43
42
|
def to_s
|
44
|
-
"\#<#{self.class} #{@
|
43
|
+
"\#<#{self.class} #{@streams.count} active streams>"
|
45
44
|
end
|
46
45
|
|
47
46
|
def as_json(...)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "../request"
|
7
7
|
require_relative "stream"
|
@@ -99,7 +99,7 @@ module Async
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def valid?
|
102
|
-
@scheme and @method and @path
|
102
|
+
@scheme and @method and (@path or @method == ::Protocol::HTTP::Methods::CONNECT)
|
103
103
|
end
|
104
104
|
|
105
105
|
def hijack?
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "../response"
|
7
7
|
require_relative "stream"
|
@@ -204,9 +204,12 @@ module Async
|
|
204
204
|
pseudo_headers = [
|
205
205
|
[SCHEME, request.scheme],
|
206
206
|
[METHOD, request.method],
|
207
|
-
[PATH, request.path],
|
208
207
|
]
|
209
208
|
|
209
|
+
if path = request.path
|
210
|
+
pseudo_headers << [PATH, path]
|
211
|
+
end
|
212
|
+
|
210
213
|
# To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form (see [RFC7230], Section 5.3). Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
|
211
214
|
if authority = request.authority
|
212
215
|
pseudo_headers << [AUTHORITY, authority]
|
@@ -4,6 +4,8 @@
|
|
4
4
|
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2024, by Thomas Morgan.
|
6
6
|
|
7
|
+
require_relative "configurable"
|
8
|
+
|
7
9
|
require_relative "http2/client"
|
8
10
|
require_relative "http2/server"
|
9
11
|
|
@@ -13,16 +15,21 @@ module Async
|
|
13
15
|
module HTTP
|
14
16
|
module Protocol
|
15
17
|
module HTTP2
|
18
|
+
extend Configurable
|
19
|
+
|
16
20
|
VERSION = "HTTP/2"
|
17
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
18
23
|
def self.bidirectional?
|
19
24
|
true
|
20
25
|
end
|
21
26
|
|
27
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
22
28
|
def self.trailer?
|
23
29
|
true
|
24
30
|
end
|
25
31
|
|
32
|
+
# The default settings for the client.
|
26
33
|
CLIENT_SETTINGS = {
|
27
34
|
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
|
28
35
|
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
|
@@ -30,6 +37,7 @@ module Async
|
|
30
37
|
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
|
31
38
|
}
|
32
39
|
|
40
|
+
# The default settings for the server.
|
33
41
|
SERVER_SETTINGS = {
|
34
42
|
# We choose a lower maximum concurrent streams to avoid overloading a single connection/thread.
|
35
43
|
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 128,
|
@@ -39,7 +47,11 @@ module Async
|
|
39
47
|
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
|
40
48
|
}
|
41
49
|
|
42
|
-
|
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)
|
43
55
|
stream = ::IO::Stream(peer)
|
44
56
|
client = Client.new(stream)
|
45
57
|
|
@@ -49,7 +61,11 @@ module Async
|
|
49
61
|
return client
|
50
62
|
end
|
51
63
|
|
52
|
-
|
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)
|
53
69
|
stream = ::IO::Stream(peer)
|
54
70
|
server = Server.new(stream)
|
55
71
|
|
@@ -59,6 +75,7 @@ module Async
|
|
59
75
|
return server
|
60
76
|
end
|
61
77
|
|
78
|
+
# @returns [Array] The names of the supported protocol.
|
62
79
|
def self.names
|
63
80
|
["h2"]
|
64
81
|
end
|
@@ -4,16 +4,18 @@
|
|
4
4
|
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2019, by Brian Morearty.
|
6
6
|
|
7
|
+
require_relative "defaulton"
|
8
|
+
|
7
9
|
require_relative "http10"
|
8
10
|
require_relative "http11"
|
9
|
-
|
10
11
|
require_relative "http2"
|
11
12
|
|
12
13
|
module Async
|
13
14
|
module HTTP
|
14
15
|
module Protocol
|
15
16
|
# A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
|
16
|
-
|
17
|
+
class HTTPS
|
18
|
+
# The protocol classes for each supported protocol.
|
17
19
|
HANDLERS = {
|
18
20
|
"h2" => HTTP2,
|
19
21
|
"http/1.1" => HTTP11,
|
@@ -21,11 +23,27 @@ module Async
|
|
21
23
|
nil => HTTP11,
|
22
24
|
}
|
23
25
|
|
24
|
-
def
|
26
|
+
def initialize(handlers = HANDLERS, **options)
|
27
|
+
@handlers = handlers
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(name, protocol, **options)
|
32
|
+
@handlers[name] = protocol
|
33
|
+
@options[protocol] = options
|
34
|
+
end
|
35
|
+
|
36
|
+
# Determine the protocol of the peer and return the appropriate protocol class.
|
37
|
+
#
|
38
|
+
# Use TLS Application Layer Protocol Negotiation (ALPN) to determine the protocol.
|
39
|
+
#
|
40
|
+
# @parameter peer [IO] The peer to communicate with.
|
41
|
+
# @returns [Class] The protocol class to use.
|
42
|
+
def protocol_for(peer)
|
25
43
|
# alpn_protocol is only available if openssl v1.0.2+
|
26
44
|
name = peer.alpn_protocol
|
27
45
|
|
28
|
-
Console.
|
46
|
+
Console.debug(self) {"Negotiating protocol #{name.inspect}..."}
|
29
47
|
|
30
48
|
if protocol = HANDLERS[name]
|
31
49
|
return protocol
|
@@ -34,18 +52,34 @@ module Async
|
|
34
52
|
end
|
35
53
|
end
|
36
54
|
|
37
|
-
|
38
|
-
|
55
|
+
# Create a client for an outbound connection.
|
56
|
+
#
|
57
|
+
# @parameter peer [IO] The peer to communicate with.
|
58
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
59
|
+
def client(peer, **options)
|
60
|
+
protocol = protocol_for(peer)
|
61
|
+
options = options[protocol] || {}
|
62
|
+
|
63
|
+
protocol.client(peer, **options)
|
39
64
|
end
|
40
65
|
|
41
|
-
|
42
|
-
|
66
|
+
# Create a server for an inbound connection.
|
67
|
+
#
|
68
|
+
# @parameter peer [IO] The peer to communicate with.
|
69
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
70
|
+
def server(peer, **options)
|
71
|
+
protocol = protocol_for(peer)
|
72
|
+
options = options[protocol] || {}
|
73
|
+
|
74
|
+
protocol.server(peer, **options)
|
43
75
|
end
|
44
76
|
|
45
|
-
#
|
46
|
-
def
|
47
|
-
|
77
|
+
# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN).
|
78
|
+
def names
|
79
|
+
@handlers.keys.compact
|
48
80
|
end
|
81
|
+
|
82
|
+
extend Defaulton
|
49
83
|
end
|
50
84
|
end
|
51
85
|
end
|
data/lib/async/http/proxy.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "client"
|
7
7
|
require_relative "endpoint"
|
@@ -82,7 +82,7 @@ module Async
|
|
82
82
|
def connect(&block)
|
83
83
|
input = Body::Writable.new
|
84
84
|
|
85
|
-
response = @client.connect(@address
|
85
|
+
response = @client.connect(authority: @address, headers: @headers, body: input)
|
86
86
|
|
87
87
|
if response.success?
|
88
88
|
pipe = Body::Pipe.new(response.body, input)
|
data/lib/async/http/server.rb
CHANGED
@@ -45,14 +45,14 @@ module Async
|
|
45
45
|
def accept(peer, address, task: Task.current)
|
46
46
|
connection = @protocol.server(peer)
|
47
47
|
|
48
|
-
Console.
|
48
|
+
Console.debug(self) {"Incoming connnection from #{address.inspect} to #{@protocol}"}
|
49
49
|
|
50
50
|
connection.each do |request|
|
51
51
|
# We set the default scheme unless it was otherwise specified.
|
52
52
|
# https://tools.ietf.org/html/rfc7230#section-5.5
|
53
53
|
request.scheme ||= self.scheme
|
54
54
|
|
55
|
-
# Console.
|
55
|
+
# Console.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
|
56
56
|
|
57
57
|
# If this returns nil, we assume that the connection has been hijacked.
|
58
58
|
self.call(request)
|
data/lib/async/http/version.rb
CHANGED
data/license.md
CHANGED
data/readme.md
CHANGED
@@ -16,6 +16,14 @@ Please see the [project documentation](https://socketry.github.io/async-http/) f
|
|
16
16
|
|
17
17
|
Please see the [project releases](https://socketry.github.io/async-http/releases/index) for all releases.
|
18
18
|
|
19
|
+
### v0.88.0
|
20
|
+
|
21
|
+
- [Support custom protocols with options](https://socketry.github.io/async-http/releases/index#support-custom-protocols-with-options)
|
22
|
+
|
23
|
+
### v0.87.0
|
24
|
+
|
25
|
+
- [Unify HTTP/1 and HTTP/2 `CONNECT` semantics](https://socketry.github.io/async-http/releases/index#unify-http/1-and-http/2-connect-semantics)
|
26
|
+
|
19
27
|
### v0.86.0
|
20
28
|
|
21
29
|
- Add support for HTTP/2 `NO_RFC7540_PRIORITIES`. See <https://www.rfc-editor.org/rfc/rfc9218.html> for more details.
|
@@ -51,10 +59,6 @@ Please see the [project releases](https://socketry.github.io/async-http/releases
|
|
51
59
|
|
52
60
|
- [`Async::HTTP::Internet` accepts keyword arguments](https://socketry.github.io/async-http/releases/index#async::http::internet-accepts-keyword-arguments)
|
53
61
|
|
54
|
-
### v0.73.0
|
55
|
-
|
56
|
-
- [Update support for `interim_response`](https://socketry.github.io/async-http/releases/index#update-support-for-interim_response)
|
57
|
-
|
58
62
|
## See Also
|
59
63
|
|
60
64
|
- [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency.
|
data/releases.md
CHANGED
@@ -1,5 +1,86 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.88.0
|
4
|
+
|
5
|
+
### Support custom protocols with options
|
6
|
+
|
7
|
+
{ruby Async::HTTP::Protocol} contains classes for specific protocols, e.g. {ruby Async::HTTP::Protocol::HTTP1} and {ruby Async::HTTP::Protocol::HTTP2}. It also contains classes for aggregating protocols, e.g. {ruby Async::HTTP::Protocol::HTTP} and {ruby Async::HTTP::Protocol::HTTPS}. They serve as factories for creating client and server instances.
|
8
|
+
|
9
|
+
These classes are now configurable with various options, which are passed as keyword arguments to the relevant connection classes. For example, to configure an HTTP/1.1 protocol without keep-alive:
|
10
|
+
|
11
|
+
``` ruby
|
12
|
+
protocol = Async::HTTP::Protocol::HTTP1.new(persistent: false, maximum_line_length: 32)
|
13
|
+
endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292", protocol: protocol)
|
14
|
+
server = Async::HTTP::Server.for(endpoint) do |request|
|
15
|
+
Protocol::HTTP::Response[200, {}, ["Hello, world"]]
|
16
|
+
end.run
|
17
|
+
```
|
18
|
+
|
19
|
+
Making a request to the server will now close the connection after the response is received:
|
20
|
+
|
21
|
+
> curl -v http://localhost:9292
|
22
|
+
* Host localhost:9292 was resolved.
|
23
|
+
* IPv6: ::1
|
24
|
+
* IPv4: 127.0.0.1
|
25
|
+
* Trying [::1]:9292...
|
26
|
+
* Connected to localhost (::1) port 9292
|
27
|
+
* using HTTP/1.x
|
28
|
+
> GET / HTTP/1.1
|
29
|
+
> Host: localhost:9292
|
30
|
+
> User-Agent: curl/8.12.1
|
31
|
+
> Accept: */*
|
32
|
+
>
|
33
|
+
* Request completely sent off
|
34
|
+
< HTTP/1.1 200 OK
|
35
|
+
< connection: close
|
36
|
+
< content-length: 12
|
37
|
+
<
|
38
|
+
* shutting down connection #0
|
39
|
+
Hello, world
|
40
|
+
|
41
|
+
In addition, any line longer than 32 bytes will be rejected:
|
42
|
+
|
43
|
+
curl -v http://localhost:9292/012345678901234567890123456789012
|
44
|
+
* Host localhost:9292 was resolved.
|
45
|
+
* IPv6: ::1
|
46
|
+
* IPv4: 127.0.0.1
|
47
|
+
* Trying [::1]:9292...
|
48
|
+
* Connected to localhost (::1) port 9292
|
49
|
+
* using HTTP/1.x
|
50
|
+
> GET /012345678901234567890123456789012 HTTP/1.1
|
51
|
+
> Host: localhost:9292
|
52
|
+
> User-Agent: curl/8.12.1
|
53
|
+
> Accept: */*
|
54
|
+
>
|
55
|
+
* Request completely sent off
|
56
|
+
* Empty reply from server
|
57
|
+
* shutting down connection #0
|
58
|
+
curl: (52) Empty reply from server
|
59
|
+
|
60
|
+
## v0.87.0
|
61
|
+
|
62
|
+
### Unify HTTP/1 and HTTP/2 `CONNECT` semantics
|
63
|
+
|
64
|
+
HTTP/1 has a request line "target" which takes different forms depending on the kind of request. For `CONNECT` requests, the target is the authority (host and port) of the target server, e.g.
|
65
|
+
|
66
|
+
CONNECT example.com:443 HTTP/1.1
|
67
|
+
|
68
|
+
In HTTP/2, the `CONNECT` method uses the `:authority` pseudo-header to specify the target, e.g.
|
69
|
+
|
70
|
+
``` http
|
71
|
+
[HEADERS FRAME]
|
72
|
+
:method: connect
|
73
|
+
:authority: example.com:443
|
74
|
+
```
|
75
|
+
|
76
|
+
In HTTP/1, the `Request#path` attribute was previously used to store the target, and this was incorrectly mapped to the `:path` pseudo-header in HTTP/2. This has been corrected, and the `Request#authority` attribute is now used to store the target for both HTTP/1 and HTTP/2, and mapped accordingly. Thus, to make a `CONNECT` request, you should set the `Request#authority` attribute, e.g.
|
77
|
+
|
78
|
+
``` ruby
|
79
|
+
response = client.connect(authority: "example.com:443")
|
80
|
+
```
|
81
|
+
|
82
|
+
For HTTP/1, the authority is mapped back to the request line target, and for HTTP/2, it is mapped to the `:authority` pseudo-header.
|
83
|
+
|
3
84
|
## v0.86.0
|
4
85
|
|
5
86
|
- Add support for HTTP/2 `NO_RFC7540_PRIORITIES`. See <https://www.rfc-editor.org/rfc/rfc9218.html> for more details.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.88.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -26,7 +26,6 @@ authors:
|
|
26
26
|
- Trevor Turk
|
27
27
|
- Viacheslav Koval
|
28
28
|
- dependabot[bot]
|
29
|
-
autorequire:
|
30
29
|
bindir: bin
|
31
30
|
cert_chain:
|
32
31
|
- |
|
@@ -58,7 +57,7 @@ cert_chain:
|
|
58
57
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
59
58
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
60
59
|
-----END CERTIFICATE-----
|
61
|
-
date:
|
60
|
+
date: 2025-03-13 00:00:00.000000000 Z
|
62
61
|
dependencies:
|
63
62
|
- !ruby/object:Gem::Dependency
|
64
63
|
name: async
|
@@ -117,77 +116,75 @@ dependencies:
|
|
117
116
|
- !ruby/object:Gem::Version
|
118
117
|
version: '0.6'
|
119
118
|
- !ruby/object:Gem::Dependency
|
120
|
-
name:
|
119
|
+
name: metrics
|
121
120
|
requirement: !ruby/object:Gem::Requirement
|
122
121
|
requirements:
|
123
122
|
- - "~>"
|
124
123
|
- !ruby/object:Gem::Version
|
125
|
-
version: '0.
|
124
|
+
version: '0.12'
|
126
125
|
type: :runtime
|
127
126
|
prerelease: false
|
128
127
|
version_requirements: !ruby/object:Gem::Requirement
|
129
128
|
requirements:
|
130
129
|
- - "~>"
|
131
130
|
- !ruby/object:Gem::Version
|
132
|
-
version: '0.
|
131
|
+
version: '0.12'
|
133
132
|
- !ruby/object:Gem::Dependency
|
134
|
-
name: protocol-
|
133
|
+
name: protocol-http
|
135
134
|
requirement: !ruby/object:Gem::Requirement
|
136
135
|
requirements:
|
137
|
-
- - "
|
136
|
+
- - "~>"
|
138
137
|
- !ruby/object:Gem::Version
|
139
|
-
version: 0.
|
138
|
+
version: '0.49'
|
140
139
|
type: :runtime
|
141
140
|
prerelease: false
|
142
141
|
version_requirements: !ruby/object:Gem::Requirement
|
143
142
|
requirements:
|
144
|
-
- - "
|
143
|
+
- - "~>"
|
145
144
|
- !ruby/object:Gem::Version
|
146
|
-
version: 0.
|
145
|
+
version: '0.49'
|
147
146
|
- !ruby/object:Gem::Dependency
|
148
|
-
name: protocol-
|
147
|
+
name: protocol-http1
|
149
148
|
requirement: !ruby/object:Gem::Requirement
|
150
149
|
requirements:
|
151
150
|
- - "~>"
|
152
151
|
- !ruby/object:Gem::Version
|
153
|
-
version: '0.
|
152
|
+
version: '0.30'
|
154
153
|
type: :runtime
|
155
154
|
prerelease: false
|
156
155
|
version_requirements: !ruby/object:Gem::Requirement
|
157
156
|
requirements:
|
158
157
|
- - "~>"
|
159
158
|
- !ruby/object:Gem::Version
|
160
|
-
version: '0.
|
159
|
+
version: '0.30'
|
161
160
|
- !ruby/object:Gem::Dependency
|
162
|
-
name:
|
161
|
+
name: protocol-http2
|
163
162
|
requirement: !ruby/object:Gem::Requirement
|
164
163
|
requirements:
|
165
164
|
- - "~>"
|
166
165
|
- !ruby/object:Gem::Version
|
167
|
-
version: '0.
|
166
|
+
version: '0.22'
|
168
167
|
type: :runtime
|
169
168
|
prerelease: false
|
170
169
|
version_requirements: !ruby/object:Gem::Requirement
|
171
170
|
requirements:
|
172
171
|
- - "~>"
|
173
172
|
- !ruby/object:Gem::Version
|
174
|
-
version: '0.
|
173
|
+
version: '0.22'
|
175
174
|
- !ruby/object:Gem::Dependency
|
176
|
-
name:
|
175
|
+
name: traces
|
177
176
|
requirement: !ruby/object:Gem::Requirement
|
178
177
|
requirements:
|
179
178
|
- - "~>"
|
180
179
|
- !ruby/object:Gem::Version
|
181
|
-
version: '0.
|
180
|
+
version: '0.10'
|
182
181
|
type: :runtime
|
183
182
|
prerelease: false
|
184
183
|
version_requirements: !ruby/object:Gem::Requirement
|
185
184
|
requirements:
|
186
185
|
- - "~>"
|
187
186
|
- !ruby/object:Gem::Version
|
188
|
-
version: '0.
|
189
|
-
description:
|
190
|
-
email:
|
187
|
+
version: '0.10'
|
191
188
|
executables: []
|
192
189
|
extensions: []
|
193
190
|
extra_rdoc_files: []
|
@@ -207,6 +204,8 @@ files:
|
|
207
204
|
- lib/async/http/mock.rb
|
208
205
|
- lib/async/http/mock/endpoint.rb
|
209
206
|
- lib/async/http/protocol.rb
|
207
|
+
- lib/async/http/protocol/configurable.rb
|
208
|
+
- lib/async/http/protocol/defaulton.rb
|
210
209
|
- lib/async/http/protocol/http.rb
|
211
210
|
- lib/async/http/protocol/http1.rb
|
212
211
|
- lib/async/http/protocol/http1/client.rb
|
@@ -244,7 +243,6 @@ licenses:
|
|
244
243
|
metadata:
|
245
244
|
documentation_uri: https://socketry.github.io/async-http/
|
246
245
|
source_code_uri: https://github.com/socketry/async-http.git
|
247
|
-
post_install_message:
|
248
246
|
rdoc_options: []
|
249
247
|
require_paths:
|
250
248
|
- lib
|
@@ -259,8 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
259
257
|
- !ruby/object:Gem::Version
|
260
258
|
version: '0'
|
261
259
|
requirements: []
|
262
|
-
rubygems_version: 3.
|
263
|
-
signing_key:
|
260
|
+
rubygems_version: 3.6.2
|
264
261
|
specification_version: 4
|
265
262
|
summary: A HTTP client and server library.
|
266
263
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|