nghttp3 0.0.1
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 +7 -0
- data/.clang-format +1 -0
- data/.vscode/extensions.json +6 -0
- data/.vscode/settings.json +10 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +20 -0
- data/ext/nghttp3/extconf.rb +18 -0
- data/ext/nghttp3/nghttp3.c +300 -0
- data/ext/nghttp3/nghttp3.h +65 -0
- data/ext/nghttp3/nghttp3_callbacks.c +713 -0
- data/ext/nghttp3/nghttp3_connection.c +1070 -0
- data/ext/nghttp3/nghttp3_nv.c +87 -0
- data/ext/nghttp3/nghttp3_qpack.c +680 -0
- data/ext/nghttp3/nghttp3_settings.c +188 -0
- data/lib/nghttp3/client.rb +236 -0
- data/lib/nghttp3/headers.rb +113 -0
- data/lib/nghttp3/request.rb +147 -0
- data/lib/nghttp3/response.rb +126 -0
- data/lib/nghttp3/server.rb +253 -0
- data/lib/nghttp3/stream_manager.rb +116 -0
- data/lib/nghttp3/version.rb +5 -0
- data/lib/nghttp3.rb +16 -0
- data/sig/nghttp3/callbacks.rbs +30 -0
- data/sig/nghttp3/client.rbs +38 -0
- data/sig/nghttp3/connection.rbs +85 -0
- data/sig/nghttp3/error.rbs +46 -0
- data/sig/nghttp3/headers.rbs +37 -0
- data/sig/nghttp3/info.rbs +7 -0
- data/sig/nghttp3/nv.rbs +9 -0
- data/sig/nghttp3/qpack.rbs +46 -0
- data/sig/nghttp3/request.rbs +35 -0
- data/sig/nghttp3/response.rbs +34 -0
- data/sig/nghttp3/server.rbs +33 -0
- data/sig/nghttp3/settings.rbs +25 -0
- data/sig/nghttp3/stream_manager.rbs +26 -0
- data/sig/nghttp3.rbs +64 -0
- metadata +83 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nghttp3
|
|
4
|
+
# An HTTP/3 response object
|
|
5
|
+
#
|
|
6
|
+
# Can be used for both:
|
|
7
|
+
# - Client-side: receiving response from server (populated via callbacks)
|
|
8
|
+
# - Server-side: building response to send to client
|
|
9
|
+
class Response
|
|
10
|
+
# @return [Integer] stream ID
|
|
11
|
+
attr_reader :stream_id
|
|
12
|
+
|
|
13
|
+
# @return [Integer, nil] HTTP status code
|
|
14
|
+
attr_accessor :status
|
|
15
|
+
|
|
16
|
+
# @return [Headers] response headers
|
|
17
|
+
attr_reader :headers
|
|
18
|
+
|
|
19
|
+
# @return [String, nil] response body (for simple responses)
|
|
20
|
+
attr_accessor :body
|
|
21
|
+
|
|
22
|
+
# Create a new response
|
|
23
|
+
# @param stream_id [Integer] the stream ID this response belongs to
|
|
24
|
+
# @param status [Integer, nil] HTTP status code
|
|
25
|
+
# @param headers [Hash, Headers, nil] response headers
|
|
26
|
+
# @param body [String, nil] response body
|
|
27
|
+
def initialize(stream_id:, status: nil, headers: nil, body: nil)
|
|
28
|
+
@stream_id = stream_id
|
|
29
|
+
@status = status
|
|
30
|
+
@headers = headers.is_a?(Headers) ? headers : Headers.new(headers || {})
|
|
31
|
+
@body = body
|
|
32
|
+
@body_chunks = []
|
|
33
|
+
@headers_sent = false
|
|
34
|
+
@finished = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convert to NV array for low-level Connection API (server-side)
|
|
38
|
+
# @return [Array<NV>] array of NV objects for status and headers
|
|
39
|
+
def to_nv_array
|
|
40
|
+
nvs = []
|
|
41
|
+
nvs << NV.new(":status", @status.to_s) if @status
|
|
42
|
+
@headers.each { |name, value| nvs << NV.new(name, value) }
|
|
43
|
+
nvs
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mark headers as sent (for streaming responses)
|
|
47
|
+
# @param extra_headers [Hash] additional headers to merge before sending
|
|
48
|
+
# @return [self]
|
|
49
|
+
def write_headers(extra_headers = {})
|
|
50
|
+
raise InvalidStateError, "Headers already sent" if @headers_sent
|
|
51
|
+
@headers.merge!(extra_headers) unless extra_headers.empty?
|
|
52
|
+
@headers_sent = true
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if headers have been sent
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def headers_sent?
|
|
59
|
+
@headers_sent
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Write a chunk of body data (for streaming responses)
|
|
63
|
+
# @param chunk [String] body chunk to write
|
|
64
|
+
# @return [self]
|
|
65
|
+
def write(chunk)
|
|
66
|
+
raise InvalidStateError, "Response already finished" if @finished
|
|
67
|
+
@body_chunks << chunk.to_s
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get all body chunks written so far
|
|
72
|
+
# @return [Array<String>]
|
|
73
|
+
def body_chunks
|
|
74
|
+
@body_chunks.dup
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get combined body from chunks
|
|
78
|
+
# @return [String]
|
|
79
|
+
def body_from_chunks
|
|
80
|
+
@body_chunks.join
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Mark the response as finished
|
|
84
|
+
# @return [self]
|
|
85
|
+
def finish
|
|
86
|
+
@finished = true
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if the response is finished
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def finished?
|
|
93
|
+
@finished
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if response has a body
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def body?
|
|
99
|
+
(@body && !@body.empty?) || !@body_chunks.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get the effective body (either set body or joined chunks)
|
|
103
|
+
# @return [String, nil]
|
|
104
|
+
def effective_body
|
|
105
|
+
return @body if @body && !@body.empty?
|
|
106
|
+
return nil if @body_chunks.empty?
|
|
107
|
+
body_from_chunks
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Append data to body (for client-side receiving)
|
|
111
|
+
# @param data [String] data to append
|
|
112
|
+
# @return [self]
|
|
113
|
+
def append_body(data)
|
|
114
|
+
@body ||= +""
|
|
115
|
+
@body << data
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# String representation
|
|
120
|
+
def inspect
|
|
121
|
+
status_str = @status ? @status.to_s : "pending"
|
|
122
|
+
finished_str = @finished ? " finished" : ""
|
|
123
|
+
"#<#{self.class} stream_id=#{@stream_id} status=#{status_str}#{finished_str}>"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nghttp3
|
|
4
|
+
# High-level HTTP/3 server
|
|
5
|
+
#
|
|
6
|
+
# Wraps the low-level Connection API with automatic request handling,
|
|
7
|
+
# convenient response building, and stream management.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# server = Nghttp3::Server.new
|
|
11
|
+
# server.bind_streams(control: 3, qpack_encoder: 7, qpack_decoder: 11)
|
|
12
|
+
#
|
|
13
|
+
# server.on_request do |request, response|
|
|
14
|
+
# response.status = 200
|
|
15
|
+
# response.headers["content-type"] = "text/html"
|
|
16
|
+
# response.body = "<h1>Hello!</h1>"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # Feed data from QUIC layer
|
|
20
|
+
# server.read_stream(stream_id, received_data, fin: false)
|
|
21
|
+
#
|
|
22
|
+
# # Pump data to QUIC layer
|
|
23
|
+
# server.pump_writes do |stream_id, data, fin|
|
|
24
|
+
# quic.write(stream_id, data, fin)
|
|
25
|
+
# end
|
|
26
|
+
class Server
|
|
27
|
+
# @return [Connection] the underlying low-level connection
|
|
28
|
+
attr_reader :connection
|
|
29
|
+
|
|
30
|
+
# @return [Settings] the settings used for this server
|
|
31
|
+
attr_reader :settings
|
|
32
|
+
|
|
33
|
+
# @return [Hash{Integer => Request}] received requests by stream ID
|
|
34
|
+
attr_reader :requests
|
|
35
|
+
|
|
36
|
+
# @return [Hash{Integer => Response}] responses by stream ID
|
|
37
|
+
attr_reader :responses
|
|
38
|
+
|
|
39
|
+
# Create a new HTTP/3 server
|
|
40
|
+
# @param settings [Settings, nil] settings to use (defaults to Settings.default)
|
|
41
|
+
def initialize(settings: nil)
|
|
42
|
+
@settings = settings || Settings.default
|
|
43
|
+
@callbacks = setup_callbacks
|
|
44
|
+
@connection = Connection.server_new(@settings, @callbacks)
|
|
45
|
+
@stream_manager = StreamManager.new(is_server: true)
|
|
46
|
+
@request_handler = nil
|
|
47
|
+
@requests = {}
|
|
48
|
+
@responses = {}
|
|
49
|
+
@building_requests = {} # Requests being built (headers not complete)
|
|
50
|
+
@streams_bound = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Bind control and QPACK streams
|
|
54
|
+
#
|
|
55
|
+
# Must be called before handling requests. The stream IDs should be
|
|
56
|
+
# unidirectional streams opened by the QUIC layer.
|
|
57
|
+
#
|
|
58
|
+
# @param control [Integer] control stream ID
|
|
59
|
+
# @param qpack_encoder [Integer] QPACK encoder stream ID
|
|
60
|
+
# @param qpack_decoder [Integer] QPACK decoder stream ID
|
|
61
|
+
# @return [self]
|
|
62
|
+
def bind_streams(control:, qpack_encoder:, qpack_decoder:)
|
|
63
|
+
@connection.bind_control_stream(control)
|
|
64
|
+
@connection.bind_qpack_streams(qpack_encoder, qpack_decoder)
|
|
65
|
+
@stream_manager.register_stream(control, type: :uni)
|
|
66
|
+
@stream_manager.register_stream(qpack_encoder, type: :uni)
|
|
67
|
+
@stream_manager.register_stream(qpack_decoder, type: :uni)
|
|
68
|
+
@streams_bound = true
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if streams are bound
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def streams_bound?
|
|
75
|
+
@streams_bound
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Register a request handler
|
|
79
|
+
#
|
|
80
|
+
# The handler is called when a complete request is received.
|
|
81
|
+
# The handler should set response.status and response.body (or use streaming).
|
|
82
|
+
#
|
|
83
|
+
# @yield [request, response] for each incoming request
|
|
84
|
+
# @yieldparam request [Request] the incoming request
|
|
85
|
+
# @yieldparam response [Response] the response object to populate
|
|
86
|
+
# @return [self]
|
|
87
|
+
def on_request(&block)
|
|
88
|
+
@request_handler = block
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Pump pending writes to the QUIC layer
|
|
93
|
+
#
|
|
94
|
+
# @yield [stream_id, data, fin] for each pending write
|
|
95
|
+
# @yieldparam stream_id [Integer] the stream ID
|
|
96
|
+
# @yieldparam data [String] the data to write
|
|
97
|
+
# @yieldparam fin [Boolean] true if this is the final data for the stream
|
|
98
|
+
# @yieldreturn [Integer] number of bytes accepted by QUIC layer
|
|
99
|
+
# @return [self]
|
|
100
|
+
def pump_writes
|
|
101
|
+
while (result = @connection.writev_stream)
|
|
102
|
+
stream_id = result[:stream_id]
|
|
103
|
+
data = result[:data]
|
|
104
|
+
fin = result[:fin]
|
|
105
|
+
|
|
106
|
+
bytes_written = yield(stream_id, data, fin) if block_given?
|
|
107
|
+
bytes_written ||= data.bytesize
|
|
108
|
+
|
|
109
|
+
@connection.add_write_offset(stream_id, bytes_written)
|
|
110
|
+
end
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Read data from QUIC layer into HTTP/3 connection
|
|
115
|
+
# @param stream_id [Integer] stream ID
|
|
116
|
+
# @param data [String] received data
|
|
117
|
+
# @param fin [Boolean] true if this is the final data for the stream
|
|
118
|
+
# @return [Integer] number of bytes consumed
|
|
119
|
+
def read_stream(stream_id, data, fin: false)
|
|
120
|
+
@connection.read_stream(stream_id, data, fin: fin)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Notify that bytes have been acknowledged by the peer
|
|
124
|
+
# @param stream_id [Integer] stream ID
|
|
125
|
+
# @param n [Integer] number of bytes acknowledged
|
|
126
|
+
# @return [self]
|
|
127
|
+
def add_ack_offset(stream_id, n)
|
|
128
|
+
@connection.add_ack_offset(stream_id, n)
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Close the server connection
|
|
133
|
+
# @return [nil]
|
|
134
|
+
def close
|
|
135
|
+
@connection.close
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if the connection is closed
|
|
139
|
+
# @return [Boolean]
|
|
140
|
+
def closed?
|
|
141
|
+
@connection.closed?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def setup_callbacks
|
|
147
|
+
Callbacks.new
|
|
148
|
+
.on_begin_headers { |stream_id| on_begin_headers(stream_id) }
|
|
149
|
+
.on_recv_header { |stream_id, name, value, flags| on_recv_header(stream_id, name, value, flags) }
|
|
150
|
+
.on_end_headers { |stream_id, fin| on_end_headers(stream_id, fin) }
|
|
151
|
+
.on_recv_data { |stream_id, data| on_recv_data(stream_id, data) }
|
|
152
|
+
.on_end_stream { |stream_id| on_end_stream(stream_id) }
|
|
153
|
+
.on_stream_close { |stream_id, app_error_code| on_stream_close(stream_id, app_error_code) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def on_begin_headers(stream_id)
|
|
157
|
+
# Start building a new request
|
|
158
|
+
@building_requests[stream_id] = {
|
|
159
|
+
method: nil,
|
|
160
|
+
scheme: nil,
|
|
161
|
+
authority: nil,
|
|
162
|
+
path: nil,
|
|
163
|
+
headers: Headers.new,
|
|
164
|
+
body: nil
|
|
165
|
+
}
|
|
166
|
+
@stream_manager.register_stream(stream_id, type: :bidi)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def on_recv_header(stream_id, name, value, _flags)
|
|
170
|
+
req = @building_requests[stream_id]
|
|
171
|
+
return unless req
|
|
172
|
+
|
|
173
|
+
case name
|
|
174
|
+
when ":method"
|
|
175
|
+
req[:method] = value
|
|
176
|
+
when ":scheme"
|
|
177
|
+
req[:scheme] = value
|
|
178
|
+
when ":authority"
|
|
179
|
+
req[:authority] = value
|
|
180
|
+
when ":path"
|
|
181
|
+
req[:path] = value
|
|
182
|
+
else
|
|
183
|
+
req[:headers][name] = value
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def on_end_headers(stream_id, fin)
|
|
188
|
+
# Headers complete, create immutable Request
|
|
189
|
+
req_data = @building_requests[stream_id]
|
|
190
|
+
return unless req_data
|
|
191
|
+
|
|
192
|
+
@requests[stream_id] = Request.new(
|
|
193
|
+
method: req_data[:method] || "GET",
|
|
194
|
+
scheme: req_data[:scheme] || "https",
|
|
195
|
+
authority: req_data[:authority],
|
|
196
|
+
path: req_data[:path] || "/",
|
|
197
|
+
headers: req_data[:headers]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Create response object
|
|
201
|
+
@responses[stream_id] = Response.new(stream_id: stream_id)
|
|
202
|
+
|
|
203
|
+
# If request has no body (fin=true), process it immediately
|
|
204
|
+
process_request(stream_id) if fin
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def on_recv_data(stream_id, data)
|
|
208
|
+
req_data = @building_requests[stream_id]
|
|
209
|
+
if req_data
|
|
210
|
+
req_data[:body] ||= +""
|
|
211
|
+
req_data[:body] << data
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def on_end_stream(stream_id)
|
|
216
|
+
# Request complete with body
|
|
217
|
+
req_data = @building_requests.delete(stream_id)
|
|
218
|
+
if req_data && @requests[stream_id].nil?
|
|
219
|
+
# Headers weren't finalized yet, create request with body
|
|
220
|
+
@requests[stream_id] = Request.new(
|
|
221
|
+
method: req_data[:method] || "GET",
|
|
222
|
+
scheme: req_data[:scheme] || "https",
|
|
223
|
+
authority: req_data[:authority],
|
|
224
|
+
path: req_data[:path] || "/",
|
|
225
|
+
headers: req_data[:headers],
|
|
226
|
+
body: req_data[:body]
|
|
227
|
+
)
|
|
228
|
+
@responses[stream_id] ||= Response.new(stream_id: stream_id)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
process_request(stream_id)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def on_stream_close(stream_id, _app_error_code)
|
|
235
|
+
@building_requests.delete(stream_id)
|
|
236
|
+
@stream_manager.close_stream(stream_id)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def process_request(stream_id)
|
|
240
|
+
request = @requests[stream_id]
|
|
241
|
+
response = @responses[stream_id]
|
|
242
|
+
return unless request && response && @request_handler
|
|
243
|
+
|
|
244
|
+
# Call the request handler
|
|
245
|
+
@request_handler.call(request, response)
|
|
246
|
+
|
|
247
|
+
# Submit the response if status is set
|
|
248
|
+
if response.status
|
|
249
|
+
@connection.submit_response(stream_id, response.to_nv_array, body: response.effective_body)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nghttp3
|
|
4
|
+
# Internal stream ID management
|
|
5
|
+
#
|
|
6
|
+
# HTTP/3 uses different stream ID allocation for clients and servers:
|
|
7
|
+
# - Client-initiated bidirectional streams: 0, 4, 8, 12, ...
|
|
8
|
+
# - Server-initiated bidirectional streams: 1, 5, 9, 13, ...
|
|
9
|
+
# - Client-initiated unidirectional streams: 2, 6, 10, 14, ...
|
|
10
|
+
# - Server-initiated unidirectional streams: 3, 7, 11, 15, ...
|
|
11
|
+
#
|
|
12
|
+
# @private
|
|
13
|
+
class StreamManager
|
|
14
|
+
# Stream types
|
|
15
|
+
BIDI_CLIENT = 0x00
|
|
16
|
+
BIDI_SERVER = 0x01
|
|
17
|
+
UNI_CLIENT = 0x02
|
|
18
|
+
UNI_SERVER = 0x03
|
|
19
|
+
|
|
20
|
+
def initialize(is_server:)
|
|
21
|
+
@is_server = is_server
|
|
22
|
+
# Next stream IDs by type
|
|
23
|
+
@next_bidi_stream_id = is_server ? 1 : 0
|
|
24
|
+
@next_uni_stream_id = is_server ? 3 : 2
|
|
25
|
+
# Track active streams
|
|
26
|
+
@active_streams = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Allocate a new bidirectional stream ID
|
|
30
|
+
# @return [Integer] new stream ID
|
|
31
|
+
def allocate_bidi_stream_id
|
|
32
|
+
id = @next_bidi_stream_id
|
|
33
|
+
@next_bidi_stream_id += 4
|
|
34
|
+
@active_streams[id] = {type: :bidi, state: :open}
|
|
35
|
+
id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Allocate a new unidirectional stream ID
|
|
39
|
+
# @return [Integer] new stream ID
|
|
40
|
+
def allocate_uni_stream_id
|
|
41
|
+
id = @next_uni_stream_id
|
|
42
|
+
@next_uni_stream_id += 4
|
|
43
|
+
@active_streams[id] = {type: :uni, state: :open}
|
|
44
|
+
id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Register an externally-opened stream (e.g., from QUIC layer)
|
|
48
|
+
# @param stream_id [Integer] stream ID
|
|
49
|
+
# @param type [Symbol] :bidi or :uni
|
|
50
|
+
def register_stream(stream_id, type: :bidi)
|
|
51
|
+
@active_streams[stream_id] = {type: type, state: :open}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Close a stream
|
|
55
|
+
# @param stream_id [Integer] stream ID to close
|
|
56
|
+
def close_stream(stream_id)
|
|
57
|
+
if @active_streams[stream_id]
|
|
58
|
+
@active_streams[stream_id][:state] = :closed
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Remove a stream from tracking
|
|
63
|
+
# @param stream_id [Integer] stream ID to remove
|
|
64
|
+
def remove_stream(stream_id)
|
|
65
|
+
@active_streams.delete(stream_id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if stream is active
|
|
69
|
+
# @param stream_id [Integer] stream ID
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def stream_active?(stream_id)
|
|
72
|
+
@active_streams[stream_id]&.dig(:state) == :open
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get all active stream IDs
|
|
76
|
+
# @return [Array<Integer>]
|
|
77
|
+
def active_stream_ids
|
|
78
|
+
@active_streams.select { |_, v| v[:state] == :open }.keys
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get stream info
|
|
82
|
+
# @param stream_id [Integer] stream ID
|
|
83
|
+
# @return [Hash, nil] stream info or nil if not found
|
|
84
|
+
def stream_info(stream_id)
|
|
85
|
+
@active_streams[stream_id]&.dup
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if this is a client-initiated stream
|
|
89
|
+
# @param stream_id [Integer] stream ID
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def client_initiated?(stream_id)
|
|
92
|
+
(stream_id & 0x01) == 0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if this is a server-initiated stream
|
|
96
|
+
# @param stream_id [Integer] stream ID
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def server_initiated?(stream_id)
|
|
99
|
+
(stream_id & 0x01) == 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if this is a bidirectional stream
|
|
103
|
+
# @param stream_id [Integer] stream ID
|
|
104
|
+
# @return [Boolean]
|
|
105
|
+
def bidirectional?(stream_id)
|
|
106
|
+
(stream_id & 0x02) == 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if this is a unidirectional stream
|
|
110
|
+
# @param stream_id [Integer] stream ID
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def unidirectional?(stream_id)
|
|
113
|
+
(stream_id & 0x02) == 2
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/nghttp3.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "nghttp3/version"
|
|
4
|
+
require_relative "nghttp3/nghttp3"
|
|
5
|
+
|
|
6
|
+
# High-level API
|
|
7
|
+
require_relative "nghttp3/headers"
|
|
8
|
+
require_relative "nghttp3/request"
|
|
9
|
+
require_relative "nghttp3/response"
|
|
10
|
+
require_relative "nghttp3/stream_manager"
|
|
11
|
+
require_relative "nghttp3/client"
|
|
12
|
+
require_relative "nghttp3/server"
|
|
13
|
+
|
|
14
|
+
module Nghttp3
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Nghttp3
|
|
2
|
+
class Callbacks
|
|
3
|
+
def initialize: () -> void
|
|
4
|
+
|
|
5
|
+
# Stream data callbacks
|
|
6
|
+
def on_acked_stream_data: () { (Integer stream_id, Integer datalen) -> void } -> self
|
|
7
|
+
def on_stream_close: () { (Integer stream_id, Integer app_error_code) -> void } -> self
|
|
8
|
+
def on_recv_data: () { (Integer stream_id, String data) -> void } -> self
|
|
9
|
+
def on_deferred_consume: () { (Integer stream_id, Integer consumed) -> void } -> self
|
|
10
|
+
|
|
11
|
+
# Header callbacks
|
|
12
|
+
def on_begin_headers: () { (Integer stream_id) -> void } -> self
|
|
13
|
+
def on_recv_header: () { (Integer stream_id, String name, String value, Integer flags) -> void } -> self
|
|
14
|
+
def on_end_headers: () { (Integer stream_id, bool fin) -> void } -> self
|
|
15
|
+
|
|
16
|
+
# Trailer callbacks
|
|
17
|
+
def on_begin_trailers: () { (Integer stream_id) -> void } -> self
|
|
18
|
+
def on_recv_trailer: () { (Integer stream_id, String name, String value, Integer flags) -> void } -> self
|
|
19
|
+
def on_end_trailers: () { (Integer stream_id, bool fin) -> void } -> self
|
|
20
|
+
|
|
21
|
+
# Stream control callbacks
|
|
22
|
+
def on_stop_sending: () { (Integer stream_id, Integer app_error_code) -> void } -> self
|
|
23
|
+
def on_end_stream: () { (Integer stream_id) -> void } -> self
|
|
24
|
+
def on_reset_stream: () { (Integer stream_id, Integer app_error_code) -> void } -> self
|
|
25
|
+
|
|
26
|
+
# Connection callbacks
|
|
27
|
+
def on_shutdown: () { (Integer id) -> void } -> self
|
|
28
|
+
def on_recv_settings: () { (Hash[Symbol, untyped] settings) -> void } -> self
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Nghttp3
|
|
2
|
+
class Client
|
|
3
|
+
attr_reader connection: Connection
|
|
4
|
+
attr_reader settings: Settings
|
|
5
|
+
attr_reader responses: Hash[Integer, Response]
|
|
6
|
+
attr_reader pending_requests: Hash[Integer, Request]
|
|
7
|
+
|
|
8
|
+
def initialize: (?settings: Settings?) -> void
|
|
9
|
+
|
|
10
|
+
def bind_streams: (control: Integer, qpack_encoder: Integer, qpack_decoder: Integer) -> self
|
|
11
|
+
def streams_bound?: () -> bool
|
|
12
|
+
|
|
13
|
+
def submit: (Request request) -> Integer
|
|
14
|
+
|
|
15
|
+
def get: (String url, ?headers: Hash[String, String]) -> Integer
|
|
16
|
+
def post: (String url, ?body: String?, ?headers: Hash[String, String]) -> Integer
|
|
17
|
+
def put: (String url, ?body: String?, ?headers: Hash[String, String]) -> Integer
|
|
18
|
+
def delete: (String url, ?headers: Hash[String, String]) -> Integer
|
|
19
|
+
def head: (String url, ?headers: Hash[String, String]) -> Integer
|
|
20
|
+
|
|
21
|
+
def pump_writes: () { (Integer stream_id, String data, bool fin) -> Integer? } -> self
|
|
22
|
+
def read_stream: (Integer stream_id, String data, ?fin: bool) -> Integer
|
|
23
|
+
def add_ack_offset: (Integer stream_id, Integer n) -> self
|
|
24
|
+
|
|
25
|
+
def close: () -> nil
|
|
26
|
+
def closed?: () -> bool
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def setup_callbacks: () -> Callbacks
|
|
31
|
+
def on_begin_headers: (Integer stream_id) -> void
|
|
32
|
+
def on_recv_header: (Integer stream_id, String name, String value, Integer flags) -> void
|
|
33
|
+
def on_end_headers: (Integer stream_id, bool fin) -> void
|
|
34
|
+
def on_recv_data: (Integer stream_id, String data) -> void
|
|
35
|
+
def on_end_stream: (Integer stream_id) -> void
|
|
36
|
+
def on_stream_close: (Integer stream_id, Integer app_error_code) -> void
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Nghttp3
|
|
2
|
+
class Connection
|
|
3
|
+
# Creates a new client connection
|
|
4
|
+
def self.client_new: (?Settings? settings, ?Callbacks? callbacks) -> Connection
|
|
5
|
+
|
|
6
|
+
# Creates a new server connection
|
|
7
|
+
def self.server_new: (?Settings? settings, ?Callbacks? callbacks) -> Connection
|
|
8
|
+
|
|
9
|
+
# Binds the control stream
|
|
10
|
+
def bind_control_stream: (Integer stream_id) -> self
|
|
11
|
+
|
|
12
|
+
# Binds QPACK encoder and decoder streams
|
|
13
|
+
def bind_qpack_streams: (Integer encoder_stream_id, Integer decoder_stream_id) -> self
|
|
14
|
+
|
|
15
|
+
# Closes the connection
|
|
16
|
+
def close: () -> nil
|
|
17
|
+
|
|
18
|
+
# Returns true if the connection is closed
|
|
19
|
+
def closed?: () -> bool
|
|
20
|
+
|
|
21
|
+
# Returns true if this is a server connection
|
|
22
|
+
def server?: () -> bool
|
|
23
|
+
|
|
24
|
+
# Returns true if this is a client connection
|
|
25
|
+
def client?: () -> bool
|
|
26
|
+
|
|
27
|
+
# Stream operations
|
|
28
|
+
|
|
29
|
+
# Reads data on a stream from the QUIC layer
|
|
30
|
+
def read_stream: (Integer stream_id, String data, ?fin: bool) -> Integer
|
|
31
|
+
|
|
32
|
+
# Gets stream data to send to the QUIC layer
|
|
33
|
+
def writev_stream: () -> { stream_id: Integer, fin: bool, data: String }?
|
|
34
|
+
|
|
35
|
+
# Tells the connection that n bytes have been accepted by the QUIC layer
|
|
36
|
+
def add_write_offset: (Integer stream_id, Integer n) -> self
|
|
37
|
+
|
|
38
|
+
# Tells the connection that n bytes have been acknowledged by the remote peer
|
|
39
|
+
def add_ack_offset: (Integer stream_id, Integer n) -> self
|
|
40
|
+
|
|
41
|
+
# Marks a stream as blocked due to QUIC flow control
|
|
42
|
+
def block_stream: (Integer stream_id) -> self
|
|
43
|
+
|
|
44
|
+
# Marks a stream as unblocked
|
|
45
|
+
def unblock_stream: (Integer stream_id) -> self
|
|
46
|
+
|
|
47
|
+
# Returns true if the stream is writable
|
|
48
|
+
def stream_writable?: (Integer stream_id) -> bool
|
|
49
|
+
|
|
50
|
+
# Closes the stream with the given error code
|
|
51
|
+
def close_stream: (Integer stream_id, Integer app_error_code) -> self
|
|
52
|
+
|
|
53
|
+
# Prevents any further write operations on the stream
|
|
54
|
+
def shutdown_stream_write: (Integer stream_id) -> self
|
|
55
|
+
|
|
56
|
+
# Resumes a stream that was blocked for input data
|
|
57
|
+
def resume_stream: (Integer stream_id) -> self
|
|
58
|
+
|
|
59
|
+
# HTTP operations
|
|
60
|
+
|
|
61
|
+
# Submits an HTTP request (client only)
|
|
62
|
+
def submit_request: (Integer stream_id, Array[NV] headers, ?body: String?) ?{ (Integer stream_id) -> (String | Symbol | nil) } -> self
|
|
63
|
+
|
|
64
|
+
# Submits an HTTP response (server only)
|
|
65
|
+
def submit_response: (Integer stream_id, Array[NV] headers, ?body: String?) ?{ (Integer stream_id) -> (String | Symbol | nil) } -> self
|
|
66
|
+
|
|
67
|
+
# Submits a 1xx informational response
|
|
68
|
+
def submit_info: (Integer stream_id, Array[NV] headers) -> self
|
|
69
|
+
|
|
70
|
+
# Submits trailer headers (implicitly ends the stream)
|
|
71
|
+
def submit_trailers: (Integer stream_id, Array[NV] trailers) -> self
|
|
72
|
+
|
|
73
|
+
# Signals the intention to shut down the connection gracefully
|
|
74
|
+
def submit_shutdown_notice: () -> self
|
|
75
|
+
|
|
76
|
+
# Initiates graceful shutdown
|
|
77
|
+
def shutdown: () -> self
|
|
78
|
+
|
|
79
|
+
# Associates arbitrary data with a stream
|
|
80
|
+
def set_stream_user_data: (Integer stream_id, untyped data) -> self
|
|
81
|
+
|
|
82
|
+
# Returns data associated with a stream
|
|
83
|
+
def get_stream_user_data: (Integer stream_id) -> untyped
|
|
84
|
+
end
|
|
85
|
+
end
|