quicsilver 0.2.0 → 0.4.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
- data/.github/workflows/ci.yml +4 -5
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +81 -0
- data/Gemfile.lock +26 -4
- data/README.md +95 -31
- data/Rakefile +95 -3
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +1 -1
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +111 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +18 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +260 -0
- data/examples/simple_client_test.rb +2 -2
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/ext/quicsilver/extconf.rb +14 -0
- data/ext/quicsilver/quicsilver.c +568 -181
- data/lib/quicsilver/client/client.rb +349 -0
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +176 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
- data/lib/quicsilver/protocol/frame_parser.rb +142 -0
- data/lib/quicsilver/protocol/frame_reader.rb +55 -0
- data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +275 -0
- data/lib/quicsilver/protocol/response_encoder.rb +97 -0
- data/lib/quicsilver/protocol/response_parser.rb +141 -0
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/quicsilver.bundle +0 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +138 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +610 -0
- data/lib/quicsilver/transport/configuration.rb +141 -0
- data/lib/quicsilver/transport/connection.rb +379 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +55 -14
- data/lib/rackup/handler/quicsilver.rb +1 -2
- data/quicsilver.gemspec +13 -3
- metadata +125 -21
- data/benchmarks/benchmark.rb +0 -68
- data/examples/setup_certs.sh +0 -57
- data/lib/quicsilver/client.rb +0 -261
- data/lib/quicsilver/connection.rb +0 -42
- data/lib/quicsilver/event_loop.rb +0 -38
- data/lib/quicsilver/http3/request_encoder.rb +0 -133
- data/lib/quicsilver/http3/request_parser.rb +0 -176
- data/lib/quicsilver/http3/response_encoder.rb +0 -186
- data/lib/quicsilver/http3/response_parser.rb +0 -160
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/quic_stream.rb +0 -36
- data/lib/quicsilver/request_registry.rb +0 -48
- data/lib/quicsilver/server.rb +0 -355
- data/lib/quicsilver/server_configuration.rb +0 -78
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
class RequestHandler
|
|
6
|
+
SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :adapter
|
|
9
|
+
|
|
10
|
+
def initialize(app:, configuration:, request_registry:, cancelled_streams:, cancelled_mutex:)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
@request_registry = request_registry
|
|
13
|
+
@cancelled_streams = cancelled_streams
|
|
14
|
+
@cancelled_mutex = cancelled_mutex
|
|
15
|
+
@adapter = Protocol::Adapter.new(app)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(connection, stream, early_data: false)
|
|
19
|
+
request = parse_request(connection, stream, early_data: early_data)
|
|
20
|
+
return unless request
|
|
21
|
+
|
|
22
|
+
response = @adapter.call(request)
|
|
23
|
+
|
|
24
|
+
send_response(connection, stream, request, response)
|
|
25
|
+
rescue Server::DrainTimeoutError
|
|
26
|
+
Quicsilver.logger.debug("Request interrupted by drain: stream #{stream.stream_id}")
|
|
27
|
+
rescue Protocol::FrameError => e
|
|
28
|
+
Quicsilver.logger.error("Frame error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
29
|
+
Quicsilver.connection_shutdown(connection.handle, e.error_code, false) rescue nil
|
|
30
|
+
rescue Protocol::MessageError => e
|
|
31
|
+
Quicsilver.logger.error("Message error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
32
|
+
Quicsilver.stream_reset(stream.stream_handle, e.error_code) if stream.writable?
|
|
33
|
+
rescue => e
|
|
34
|
+
Quicsilver.logger.error("Error handling request: #{e.class} - #{e.message}")
|
|
35
|
+
Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
|
|
36
|
+
connection.send_error(stream, 500, "Internal Server Error") if stream.writable?
|
|
37
|
+
ensure
|
|
38
|
+
@request_registry.complete(stream.stream_id) if @request_registry.include?(stream.stream_id)
|
|
39
|
+
@cancelled_mutex.synchronize { @cancelled_streams.delete(stream.stream_id) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse_request(connection, stream, early_data: false)
|
|
45
|
+
parser = Protocol::RequestParser.new(
|
|
46
|
+
stream.data,
|
|
47
|
+
max_body_size: @configuration.max_body_size,
|
|
48
|
+
max_header_size: @configuration.max_header_size,
|
|
49
|
+
max_header_count: @configuration.max_header_count,
|
|
50
|
+
max_frame_payload_size: @configuration.max_frame_payload_size
|
|
51
|
+
)
|
|
52
|
+
parser.parse
|
|
53
|
+
parser.validate_headers!
|
|
54
|
+
|
|
55
|
+
headers = parser.headers
|
|
56
|
+
unless headers && !headers.empty?
|
|
57
|
+
connection.send_error(stream, 400, "Bad Request") if stream.writable?
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
method = headers[":method"]
|
|
62
|
+
|
|
63
|
+
if @configuration.early_data_policy == :reject &&
|
|
64
|
+
early_data && !SAFE_METHODS.include?(method)
|
|
65
|
+
connection.send_error(stream, 425, "Too Early") if stream.writable?
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
request, body = @adapter.build_request(headers)
|
|
70
|
+
request.headers.add("quicsilver-early-data", early_data.to_s)
|
|
71
|
+
|
|
72
|
+
# Wire interim_response so apps can send 103 Early Hints.
|
|
73
|
+
# Falcon mode: app calls request.send_interim_response(103, headers)
|
|
74
|
+
# Rack mode: bridged to rack.early_hints via EarlyHintsMiddleware
|
|
75
|
+
request.interim_response = ->(status, headers) {
|
|
76
|
+
connection.send_informational(stream, status, headers)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if body && parser.body && parser.body.size > 0
|
|
80
|
+
parser.body.rewind
|
|
81
|
+
body_data = parser.body.read
|
|
82
|
+
body.write(body_data) unless body_data.empty?
|
|
83
|
+
end
|
|
84
|
+
body&.close_write
|
|
85
|
+
|
|
86
|
+
connection.apply_stream_priority(stream, parser.priority)
|
|
87
|
+
|
|
88
|
+
@request_registry.track(
|
|
89
|
+
stream.stream_id, connection.handle,
|
|
90
|
+
path: headers[":path"] || "/", method: method || "GET"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
request
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def send_response(connection, stream, request, response)
|
|
97
|
+
if cancelled_stream?(stream.stream_id)
|
|
98
|
+
Quicsilver.logger.debug("Skipping response for cancelled stream #{stream.stream_id}")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise "Stream handle not found for stream #{stream.stream_id}" unless stream.writable?
|
|
103
|
+
|
|
104
|
+
headers = response.headers
|
|
105
|
+
|
|
106
|
+
# Extract trailers before flattening (Protocol::HTTP::Headers tracks
|
|
107
|
+
# trailer! state that a plain Hash would lose — needed for gRPC).
|
|
108
|
+
trailers = if headers.respond_to?(:trailer?) && headers.trailer?
|
|
109
|
+
trailer_hash = {}
|
|
110
|
+
headers.trailer.each { |name, value| trailer_hash[name] = value }
|
|
111
|
+
trailer_hash
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
response_headers = {}
|
|
115
|
+
if headers.respond_to?(:header)
|
|
116
|
+
headers.header.each { |name, value| response_headers[name] = value }
|
|
117
|
+
else
|
|
118
|
+
headers&.each { |name, value| response_headers[name] = value }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Protocol-rack moves content-length from headers to body.length —
|
|
122
|
+
# re-add it so the HTTP/3 response includes the header.
|
|
123
|
+
if !response_headers.key?("content-length") && response.body&.length
|
|
124
|
+
response_headers["content-length"] = response.body.length.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
body = response.body || []
|
|
128
|
+
connection.send_response(stream, response.status, response_headers, body,
|
|
129
|
+
head_request: request.head?, trailers: trailers)
|
|
130
|
+
@request_registry.complete(stream.stream_id)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cancelled_stream?(stream_id)
|
|
134
|
+
@cancelled_mutex.synchronize { @cancelled_streams.include?(stream_id) }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
class RequestRegistry
|
|
6
|
+
def initialize
|
|
7
|
+
@requests = {}
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def track(stream_id, connection_handle, path:, method:, started_at: Time.now)
|
|
12
|
+
@mutex.synchronize do
|
|
13
|
+
@requests[stream_id] = {
|
|
14
|
+
connection_handle: connection_handle,
|
|
15
|
+
path: path,
|
|
16
|
+
method: method,
|
|
17
|
+
started_at: started_at
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def complete(stream_id)
|
|
23
|
+
@mutex.synchronize { @requests.delete(stream_id) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def active_count
|
|
27
|
+
@mutex.synchronize { @requests.size }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def active_requests
|
|
31
|
+
@mutex.synchronize { @requests.dup }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def requests_older_than(seconds)
|
|
35
|
+
cutoff = Time.now - seconds
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@requests.select { |_, r| r[:started_at] < cutoff }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def empty?
|
|
42
|
+
@mutex.synchronize { @requests.empty? }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def include?(stream_id)
|
|
46
|
+
@mutex.synchronize { @requests.key?(stream_id) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|