protocol-http2 0.22.1 → 0.24.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/context/getting-started.md +82 -0
- data/context/index.yaml +12 -0
- data/lib/protocol/http2/client.rb +28 -1
- data/lib/protocol/http2/connection.rb +93 -1
- data/lib/protocol/http2/continuation_frame.rb +42 -4
- data/lib/protocol/http2/data_frame.rb +11 -1
- data/lib/protocol/http2/error.rb +17 -2
- data/lib/protocol/http2/flow_controlled.rb +13 -1
- data/lib/protocol/http2/frame.rb +47 -2
- data/lib/protocol/http2/framer.rb +16 -1
- data/lib/protocol/http2/goaway_frame.rb +11 -1
- data/lib/protocol/http2/headers_frame.rb +15 -1
- data/lib/protocol/http2/padded.rb +12 -1
- data/lib/protocol/http2/ping_frame.rb +17 -1
- data/lib/protocol/http2/priority_update_frame.rb +8 -0
- data/lib/protocol/http2/push_promise_frame.rb +10 -1
- data/lib/protocol/http2/reset_stream_frame.rb +10 -1
- data/lib/protocol/http2/server.rb +27 -1
- data/lib/protocol/http2/settings_frame.rb +58 -1
- data/lib/protocol/http2/stream.rb +48 -1
- data/lib/protocol/http2/version.rb +4 -2
- data/lib/protocol/http2/window.rb +25 -1
- data/lib/protocol/http2/window_update_frame.rb +10 -1
- data/license.md +1 -1
- data/readme.md +52 -0
- data/releases.md +185 -0
- data.tar.gz.sig +0 -0
- metadata +6 -4
- 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: 163e984c0ac8a9a19c5dd3fd4f09c7b4be35622413a562ff80f14dece63f86e3
|
|
4
|
+
data.tar.gz: 5329a5966f521513c7bc275a426c42264aa2fcc5078b39dbb0c84945cf18240b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 301bdf39a174d8db27486c568abf7c01926ed0c290502e3580500a461e6e6e5ac296630b4dce3c56207f314ee5bab29d7ff18eead4ad54d5ea2326043a70f118
|
|
7
|
+
data.tar.gz: 58f22287bd88d4c2844d1742ecbc2bf57f8640ec6b35400ade3aa565101715c1282ed59d354b8a3c5c77103e83f87ce3764865ea5ff335721f63a14bcf527c48
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
``` bash
|
|
10
|
+
$ bundle add protocol-http2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
This gem provides a low-level implementation of the HTTP/2 protocol. It is designed to be used in conjunction with other libraries to provide a complete HTTP/2 client or server. However, it is straight forward to give examples of how to use the library directly.
|
|
16
|
+
|
|
17
|
+
### Client
|
|
18
|
+
|
|
19
|
+
Here is a basic HTTP/2 client:
|
|
20
|
+
|
|
21
|
+
``` ruby
|
|
22
|
+
require "async"
|
|
23
|
+
require "async/io/stream"
|
|
24
|
+
require "async/http/endpoint"
|
|
25
|
+
require "protocol/http2/client"
|
|
26
|
+
|
|
27
|
+
Async do
|
|
28
|
+
endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens")
|
|
29
|
+
|
|
30
|
+
peer = endpoint.connect
|
|
31
|
+
|
|
32
|
+
puts "Connected to #{peer.inspect}"
|
|
33
|
+
|
|
34
|
+
# IO Buffering:
|
|
35
|
+
stream = Async::IO::Stream.new(peer)
|
|
36
|
+
|
|
37
|
+
framer = Protocol::HTTP2::Framer.new(stream)
|
|
38
|
+
client = Protocol::HTTP2::Client.new(framer)
|
|
39
|
+
|
|
40
|
+
puts "Sending connection preface..."
|
|
41
|
+
client.send_connection_preface
|
|
42
|
+
|
|
43
|
+
puts "Creating stream..."
|
|
44
|
+
stream = client.create_stream
|
|
45
|
+
|
|
46
|
+
headers = [
|
|
47
|
+
[":scheme", endpoint.scheme],
|
|
48
|
+
[":method", "GET"],
|
|
49
|
+
[":authority", "www.google.com"],
|
|
50
|
+
[":path", endpoint.path],
|
|
51
|
+
["accept", "*/*"],
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
|
|
55
|
+
stream.send_headers(headers, Protocol::HTTP2::END_STREAM)
|
|
56
|
+
|
|
57
|
+
puts "Waiting for response..."
|
|
58
|
+
$count = 0
|
|
59
|
+
|
|
60
|
+
def stream.process_headers(frame)
|
|
61
|
+
headers = super
|
|
62
|
+
puts "Got response headers: #{headers} (#{frame.end_stream?})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def stream.receive_data(frame)
|
|
66
|
+
data = super
|
|
67
|
+
|
|
68
|
+
$count += data.scan(/kittens/).count
|
|
69
|
+
|
|
70
|
+
puts "Got response data: #{data.bytesize}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
until stream.closed?
|
|
74
|
+
frame = client.read_frame
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "Got #{$count} kittens!"
|
|
78
|
+
|
|
79
|
+
puts "Closing client..."
|
|
80
|
+
client.close
|
|
81
|
+
end
|
|
82
|
+
```
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: A low level implementation of the HTTP/2 protocol.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://socketry.github.io/protocol-http2/
|
|
7
|
+
source_code_uri: https://github.com/socketry/protocol-http2.git
|
|
8
|
+
files:
|
|
9
|
+
- path: getting-started.md
|
|
10
|
+
title: Getting Started
|
|
11
|
+
description: This guide explains how to use the `protocol-http2` gem to implement
|
|
12
|
+
a basic HTTP/2 client.
|
|
@@ -1,29 +1,51 @@
|
|
|
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 "connection"
|
|
7
7
|
|
|
8
8
|
module Protocol
|
|
9
9
|
module HTTP2
|
|
10
|
+
# Represents an HTTP/2 client connection.
|
|
11
|
+
# Manages client-side protocol semantics including stream ID allocation,
|
|
12
|
+
# connection preface handling, and push promise processing.
|
|
10
13
|
class Client < Connection
|
|
14
|
+
# Initialize a new HTTP/2 client connection.
|
|
15
|
+
# @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
|
|
11
16
|
def initialize(framer)
|
|
12
17
|
super(framer, 1)
|
|
13
18
|
end
|
|
14
19
|
|
|
20
|
+
# Check if the given stream ID represents a locally-initiated stream.
|
|
21
|
+
# Client streams have odd numbered IDs.
|
|
22
|
+
# @parameter id [Integer] The stream ID to check.
|
|
23
|
+
# @returns [bool] True if the stream ID is locally-initiated.
|
|
15
24
|
def local_stream_id?(id)
|
|
16
25
|
id.odd?
|
|
17
26
|
end
|
|
18
27
|
|
|
28
|
+
# Check if the given stream ID represents a remotely-initiated stream.
|
|
29
|
+
# Server streams have even numbered IDs.
|
|
30
|
+
# @parameter id [Integer] The stream ID to check.
|
|
31
|
+
# @returns [bool] True if the stream ID is remotely-initiated.
|
|
19
32
|
def remote_stream_id?(id)
|
|
20
33
|
id.even?
|
|
21
34
|
end
|
|
22
35
|
|
|
36
|
+
# Check if the given stream ID is valid for remote initiation.
|
|
37
|
+
# Server-initiated streams must have even numbered IDs.
|
|
38
|
+
# @parameter stream_id [Integer] The stream ID to validate.
|
|
39
|
+
# @returns [bool] True if the stream ID is valid for remote initiation.
|
|
23
40
|
def valid_remote_stream_id?(stream_id)
|
|
24
41
|
stream_id.even?
|
|
25
42
|
end
|
|
26
43
|
|
|
44
|
+
# Send the HTTP/2 connection preface and initial settings.
|
|
45
|
+
# This must be called once when the connection is first established.
|
|
46
|
+
# @parameter settings [Array] Optional settings to send with the connection preface.
|
|
47
|
+
# @raises [ProtocolError] If called when not in the new state.
|
|
48
|
+
# @yields Allows custom processing during preface exchange.
|
|
27
49
|
def send_connection_preface(settings = [])
|
|
28
50
|
if @state == :new
|
|
29
51
|
@framer.write_connection_preface
|
|
@@ -42,10 +64,15 @@ module Protocol
|
|
|
42
64
|
end
|
|
43
65
|
end
|
|
44
66
|
|
|
67
|
+
# Clients cannot create push promise streams.
|
|
68
|
+
# @raises [ProtocolError] Always, as clients cannot initiate push promises.
|
|
45
69
|
def create_push_promise_stream
|
|
46
70
|
raise ProtocolError, "Cannot create push promises from client!"
|
|
47
71
|
end
|
|
48
72
|
|
|
73
|
+
# Process a push promise frame received from the server.
|
|
74
|
+
# @parameter frame [PushPromiseFrame] The push promise frame to process.
|
|
75
|
+
# @returns [Array(Stream, Hash) | Nil] The promised stream and request headers, or nil if no associated stream.
|
|
49
76
|
def receive_push_promise(frame)
|
|
50
77
|
if frame.stream_id == 0
|
|
51
78
|
raise ProtocolError, "Cannot receive headers for stream 0!"
|
|
@@ -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-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2023, by Marco Concetto Rudilosso.
|
|
6
6
|
|
|
7
7
|
require_relative "framer"
|
|
@@ -12,9 +12,14 @@ require "protocol/http/header/priority"
|
|
|
12
12
|
|
|
13
13
|
module Protocol
|
|
14
14
|
module HTTP2
|
|
15
|
+
# This is the core connection class that handles HTTP/2 protocol semantics including
|
|
16
|
+
# stream management, settings negotiation, and frame processing.
|
|
15
17
|
class Connection
|
|
16
18
|
include FlowControlled
|
|
17
19
|
|
|
20
|
+
# Initialize a new HTTP/2 connection.
|
|
21
|
+
# @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
|
|
22
|
+
# @parameter local_stream_id [Integer] The starting stream ID for locally-initiated streams.
|
|
18
23
|
def initialize(framer, local_stream_id)
|
|
19
24
|
super()
|
|
20
25
|
|
|
@@ -41,10 +46,15 @@ module Protocol
|
|
|
41
46
|
@remote_window = Window.new
|
|
42
47
|
end
|
|
43
48
|
|
|
49
|
+
# The connection stream ID (always 0 for connection-level operations).
|
|
50
|
+
# @returns [Integer] Always returns 0 for the connection itself.
|
|
44
51
|
def id
|
|
45
52
|
0
|
|
46
53
|
end
|
|
47
54
|
|
|
55
|
+
# Access streams by ID, with 0 returning the connection itself.
|
|
56
|
+
# @parameter id [Integer] The stream ID to look up.
|
|
57
|
+
# @returns [Connection | Stream | Nil] The connection (if id=0), stream, or nil.
|
|
48
58
|
def [] id
|
|
49
59
|
if id.zero?
|
|
50
60
|
self
|
|
@@ -89,6 +99,9 @@ module Protocol
|
|
|
89
99
|
@state == :closed || @framer.nil?
|
|
90
100
|
end
|
|
91
101
|
|
|
102
|
+
# Remove a stream from the active streams collection.
|
|
103
|
+
# @parameter id [Integer] The stream ID to remove.
|
|
104
|
+
# @returns [Stream | Nil] The removed stream, or nil if not found.
|
|
92
105
|
def delete(id)
|
|
93
106
|
@streams.delete(id)
|
|
94
107
|
end
|
|
@@ -96,6 +109,12 @@ module Protocol
|
|
|
96
109
|
# Close the underlying framer and all streams.
|
|
97
110
|
def close(error = nil)
|
|
98
111
|
# The underlying socket may already be closed by this point.
|
|
112
|
+
|
|
113
|
+
# If there are active streams when the connection closes, it's an error for those streams, even if the connection itself closed cleanly:
|
|
114
|
+
if @streams.any? and error.nil?
|
|
115
|
+
error = EOFError.new("Connection closed with #{@streams.size} active stream(s)!")
|
|
116
|
+
end
|
|
117
|
+
|
|
99
118
|
@streams.each_value{|stream| stream.close(error)}
|
|
100
119
|
@streams.clear
|
|
101
120
|
|
|
@@ -106,10 +125,17 @@ module Protocol
|
|
|
106
125
|
end
|
|
107
126
|
end
|
|
108
127
|
|
|
128
|
+
# Encode headers using HPACK compression.
|
|
129
|
+
# @parameter headers [Array] The headers to encode.
|
|
130
|
+
# @parameter buffer [String] Optional buffer for encoding output.
|
|
131
|
+
# @returns [String] The encoded header block.
|
|
109
132
|
def encode_headers(headers, buffer = String.new.b)
|
|
110
133
|
HPACK::Compressor.new(buffer, @encoder, table_size_limit: @remote_settings.header_table_size).encode(headers)
|
|
111
134
|
end
|
|
112
135
|
|
|
136
|
+
# Decode headers using HPACK decompression.
|
|
137
|
+
# @parameter data [String] The encoded header block data.
|
|
138
|
+
# @returns [Array] The decoded headers.
|
|
113
139
|
def decode_headers(data)
|
|
114
140
|
HPACK::Decompressor.new(data, @decoder, table_size_limit: @local_settings.header_table_size).decode
|
|
115
141
|
end
|
|
@@ -141,6 +167,9 @@ module Protocol
|
|
|
141
167
|
end
|
|
142
168
|
end
|
|
143
169
|
|
|
170
|
+
# Execute a block within a synchronized context.
|
|
171
|
+
# This method provides a synchronization primitive for thread safety.
|
|
172
|
+
# @yields The block to execute within the synchronized context.
|
|
144
173
|
def synchronize
|
|
145
174
|
yield
|
|
146
175
|
end
|
|
@@ -171,6 +200,8 @@ module Protocol
|
|
|
171
200
|
raise
|
|
172
201
|
end
|
|
173
202
|
|
|
203
|
+
# Send updated settings to the remote peer.
|
|
204
|
+
# @parameter changes [Hash] The settings changes to send.
|
|
174
205
|
def send_settings(changes)
|
|
175
206
|
@local_settings.append(changes)
|
|
176
207
|
|
|
@@ -197,6 +228,9 @@ module Protocol
|
|
|
197
228
|
self.close!
|
|
198
229
|
end
|
|
199
230
|
|
|
231
|
+
# Process a GOAWAY frame from the remote peer.
|
|
232
|
+
# @parameter frame [GoawayFrame] The GOAWAY frame to process.
|
|
233
|
+
# @raises [GoawayError] If the frame indicates a connection error.
|
|
200
234
|
def receive_goaway(frame)
|
|
201
235
|
# We capture the last stream that was processed.
|
|
202
236
|
@remote_stream_id, error_code, message = frame.unpack
|
|
@@ -209,6 +243,8 @@ module Protocol
|
|
|
209
243
|
end
|
|
210
244
|
end
|
|
211
245
|
|
|
246
|
+
# Write a single frame to the connection.
|
|
247
|
+
# @parameter frame [Frame] The frame to write.
|
|
212
248
|
def write_frame(frame)
|
|
213
249
|
synchronize do
|
|
214
250
|
@framer.write_frame(frame)
|
|
@@ -217,6 +253,10 @@ module Protocol
|
|
|
217
253
|
@framer.flush
|
|
218
254
|
end
|
|
219
255
|
|
|
256
|
+
# Write multiple frames within a synchronized block.
|
|
257
|
+
# @yields {|framer| ...} The framer for writing multiple frames.
|
|
258
|
+
# @parameter framer [Framer] The framer instance.
|
|
259
|
+
# @raises [EOFError] If the connection is closed.
|
|
220
260
|
def write_frames
|
|
221
261
|
if @framer
|
|
222
262
|
synchronize do
|
|
@@ -229,6 +269,8 @@ module Protocol
|
|
|
229
269
|
end
|
|
230
270
|
end
|
|
231
271
|
|
|
272
|
+
# Update local settings and adjust stream window capacities.
|
|
273
|
+
# @parameter changes [Hash] The settings changes to apply locally.
|
|
232
274
|
def update_local_settings(changes)
|
|
233
275
|
capacity = @local_settings.initial_window_size
|
|
234
276
|
|
|
@@ -239,6 +281,8 @@ module Protocol
|
|
|
239
281
|
@local_window.desired = capacity
|
|
240
282
|
end
|
|
241
283
|
|
|
284
|
+
# Update remote settings and adjust stream window capacities.
|
|
285
|
+
# @parameter changes [Hash] The settings changes to apply to remote peer.
|
|
242
286
|
def update_remote_settings(changes)
|
|
243
287
|
capacity = @remote_settings.initial_window_size
|
|
244
288
|
|
|
@@ -273,12 +317,17 @@ module Protocol
|
|
|
273
317
|
end
|
|
274
318
|
end
|
|
275
319
|
|
|
320
|
+
# Transition the connection to the open state.
|
|
321
|
+
# @returns [Connection] Self for method chaining.
|
|
276
322
|
def open!
|
|
277
323
|
@state = :open
|
|
278
324
|
|
|
279
325
|
return self
|
|
280
326
|
end
|
|
281
327
|
|
|
328
|
+
# Receive and process a SETTINGS frame from the remote peer.
|
|
329
|
+
# @parameter frame [SettingsFrame] The settings frame to process.
|
|
330
|
+
# @raises [ProtocolError] If the connection is in an invalid state.
|
|
282
331
|
def receive_settings(frame)
|
|
283
332
|
if @state == :new
|
|
284
333
|
# We transition to :open when we receive acknowledgement of first settings frame:
|
|
@@ -290,6 +339,8 @@ module Protocol
|
|
|
290
339
|
end
|
|
291
340
|
end
|
|
292
341
|
|
|
342
|
+
# Send a PING frame to the remote peer.
|
|
343
|
+
# @parameter data [String] The 8-byte ping payload data.
|
|
293
344
|
def send_ping(data)
|
|
294
345
|
if @state != :closed
|
|
295
346
|
frame = PingFrame.new
|
|
@@ -301,6 +352,9 @@ module Protocol
|
|
|
301
352
|
end
|
|
302
353
|
end
|
|
303
354
|
|
|
355
|
+
# Process a PING frame from the remote peer.
|
|
356
|
+
# @parameter frame [PingFrame] The ping frame to process.
|
|
357
|
+
# @raises [ProtocolError] If ping is received in invalid state.
|
|
304
358
|
def receive_ping(frame)
|
|
305
359
|
if @state != :closed
|
|
306
360
|
# This is handled in `read_payload`:
|
|
@@ -318,6 +372,9 @@ module Protocol
|
|
|
318
372
|
end
|
|
319
373
|
end
|
|
320
374
|
|
|
375
|
+
# Process a DATA frame from the remote peer.
|
|
376
|
+
# @parameter frame [DataFrame] The data frame to process.
|
|
377
|
+
# @raises [ProtocolError] If data is received for invalid stream.
|
|
321
378
|
def receive_data(frame)
|
|
322
379
|
update_local_window(frame)
|
|
323
380
|
|
|
@@ -330,6 +387,10 @@ module Protocol
|
|
|
330
387
|
end
|
|
331
388
|
end
|
|
332
389
|
|
|
390
|
+
# Check if the given stream ID is valid for remote initiation.
|
|
391
|
+
# This method should be overridden by client/server implementations.
|
|
392
|
+
# @parameter stream_id [Integer] The stream ID to validate.
|
|
393
|
+
# @returns [Boolean] True if the stream ID is valid for remote initiation.
|
|
333
394
|
def valid_remote_stream_id?(stream_id)
|
|
334
395
|
false
|
|
335
396
|
end
|
|
@@ -366,6 +427,10 @@ module Protocol
|
|
|
366
427
|
end
|
|
367
428
|
end
|
|
368
429
|
|
|
430
|
+
# Create a push promise stream.
|
|
431
|
+
# This method should be overridden by client/server implementations.
|
|
432
|
+
# @yields {|stream| ...} Optional block to configure the created stream.
|
|
433
|
+
# @returns [Stream] The created push promise stream.
|
|
369
434
|
def create_push_promise_stream(&block)
|
|
370
435
|
create_stream(&block)
|
|
371
436
|
end
|
|
@@ -397,10 +462,16 @@ module Protocol
|
|
|
397
462
|
end
|
|
398
463
|
end
|
|
399
464
|
|
|
465
|
+
# Receive and process a PUSH_PROMISE frame.
|
|
466
|
+
# @parameter frame [PushPromiseFrame] The push promise frame.
|
|
467
|
+
# @raises [ProtocolError] Always raises as push promises are not supported.
|
|
400
468
|
def receive_push_promise(frame)
|
|
401
469
|
raise ProtocolError, "Unable to receive push promise!"
|
|
402
470
|
end
|
|
403
471
|
|
|
472
|
+
# Receive and process a PRIORITY_UPDATE frame.
|
|
473
|
+
# @parameter frame [PriorityUpdateFrame] The priority update frame.
|
|
474
|
+
# @raises [ProtocolError] If the stream ID is invalid.
|
|
404
475
|
def receive_priority_update(frame)
|
|
405
476
|
if frame.stream_id != 0
|
|
406
477
|
raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
|
|
@@ -414,14 +485,25 @@ module Protocol
|
|
|
414
485
|
end
|
|
415
486
|
end
|
|
416
487
|
|
|
488
|
+
# Check if the given stream ID represents a client-initiated stream.
|
|
489
|
+
# Client streams always have odd numbered IDs.
|
|
490
|
+
# @parameter id [Integer] The stream ID to check.
|
|
491
|
+
# @returns [Boolean] True if the stream ID is client-initiated.
|
|
417
492
|
def client_stream_id?(id)
|
|
418
493
|
id.odd?
|
|
419
494
|
end
|
|
420
495
|
|
|
496
|
+
# Check if the given stream ID represents a server-initiated stream.
|
|
497
|
+
# Server streams always have even numbered IDs.
|
|
498
|
+
# @parameter id [Integer] The stream ID to check.
|
|
499
|
+
# @returns [Boolean] True if the stream ID is server-initiated.
|
|
421
500
|
def server_stream_id?(id)
|
|
422
501
|
id.even?
|
|
423
502
|
end
|
|
424
503
|
|
|
504
|
+
# Check if the given stream ID represents an idle stream.
|
|
505
|
+
# @parameter id [Integer] The stream ID to check.
|
|
506
|
+
# @returns [Boolean] True if the stream ID is idle (not yet used).
|
|
425
507
|
def idle_stream_id?(id)
|
|
426
508
|
if id.even?
|
|
427
509
|
# Server-initiated streams are even.
|
|
@@ -450,6 +532,9 @@ module Protocol
|
|
|
450
532
|
end
|
|
451
533
|
end
|
|
452
534
|
|
|
535
|
+
# Receive and process a RST_STREAM frame.
|
|
536
|
+
# @parameter frame [ResetStreamFrame] The reset stream frame.
|
|
537
|
+
# @raises [ProtocolError] If the frame is invalid for connection context.
|
|
453
538
|
def receive_reset_stream(frame)
|
|
454
539
|
if frame.connection?
|
|
455
540
|
raise ProtocolError, "Cannot reset connection!"
|
|
@@ -475,6 +560,8 @@ module Protocol
|
|
|
475
560
|
end
|
|
476
561
|
end
|
|
477
562
|
|
|
563
|
+
# Receive and process a WINDOW_UPDATE frame.
|
|
564
|
+
# @parameter frame [WindowUpdateFrame] The window update frame.
|
|
478
565
|
def receive_window_update(frame)
|
|
479
566
|
if frame.connection?
|
|
480
567
|
super
|
|
@@ -494,10 +581,15 @@ module Protocol
|
|
|
494
581
|
end
|
|
495
582
|
end
|
|
496
583
|
|
|
584
|
+
# Receive and process a CONTINUATION frame.
|
|
585
|
+
# @parameter frame [ContinuationFrame] The continuation frame.
|
|
586
|
+
# @raises [ProtocolError] Always raises as unexpected continuation frames are not supported.
|
|
497
587
|
def receive_continuation(frame)
|
|
498
588
|
raise ProtocolError, "Received unexpected continuation: #{frame.class}"
|
|
499
589
|
end
|
|
500
590
|
|
|
591
|
+
# Receive and process a generic frame (default handler).
|
|
592
|
+
# @parameter frame [Frame] The frame to receive.
|
|
501
593
|
def receive_frame(frame)
|
|
502
594
|
# ignore.
|
|
503
595
|
end
|
|
@@ -1,31 +1,52 @@
|
|
|
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 "frame"
|
|
7
7
|
|
|
8
8
|
module Protocol
|
|
9
9
|
module HTTP2
|
|
10
|
+
# Module for frames that can be continued with CONTINUATION frames.
|
|
10
11
|
module Continued
|
|
12
|
+
# @constant [Integer] The maximum number of continuation frames to read to prevent resource exhaustion.
|
|
13
|
+
LIMIT = 8
|
|
14
|
+
|
|
15
|
+
# Initialize a continuable frame.
|
|
16
|
+
# @parameter arguments [Array] Arguments passed to parent constructor.
|
|
11
17
|
def initialize(*)
|
|
12
18
|
super
|
|
13
19
|
|
|
14
20
|
@continuation = nil
|
|
15
21
|
end
|
|
16
22
|
|
|
23
|
+
# Check if this frame has continuation frames.
|
|
24
|
+
# @returns [Boolean] True if there are continuation frames.
|
|
17
25
|
def continued?
|
|
18
26
|
!!@continuation
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
# Check if this is the last header block fragment.
|
|
30
|
+
# @returns [Boolean] True if the END_HEADERS flag is set.
|
|
21
31
|
def end_headers?
|
|
22
32
|
flag_set?(END_HEADERS)
|
|
23
33
|
end
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
# Read the frame and any continuation frames from the stream.
|
|
36
|
+
#
|
|
37
|
+
# There is an upper limit to the number of continuation frames that can be read to prevent resource exhaustion. If the limit is 0, only one frame will be read (the initial frame). Otherwise, the limit decrements with each continuation frame read.
|
|
38
|
+
#
|
|
39
|
+
# @parameter stream [IO] The stream to read from.
|
|
40
|
+
# @parameter maximum_frame_size [Integer] Maximum allowed frame size.
|
|
41
|
+
# @parameter limit [Integer] The maximum number of continuation frames to read.
|
|
42
|
+
def read(stream, maximum_frame_size, limit = LIMIT)
|
|
43
|
+
super(stream, maximum_frame_size)
|
|
27
44
|
|
|
28
45
|
unless end_headers?
|
|
46
|
+
if limit.zero?
|
|
47
|
+
raise ProtocolError, "Too many continuation frames!"
|
|
48
|
+
end
|
|
49
|
+
|
|
29
50
|
continuation = ContinuationFrame.new
|
|
30
51
|
continuation.read_header(stream)
|
|
31
52
|
|
|
@@ -38,12 +59,14 @@ module Protocol
|
|
|
38
59
|
raise ProtocolError, "Invalid stream id: #{continuation.stream_id} for continuation of stream id: #{@stream_id}!"
|
|
39
60
|
end
|
|
40
61
|
|
|
41
|
-
continuation.read(stream, maximum_frame_size)
|
|
62
|
+
continuation.read(stream, maximum_frame_size, limit - 1)
|
|
42
63
|
|
|
43
64
|
@continuation = continuation
|
|
44
65
|
end
|
|
45
66
|
end
|
|
46
67
|
|
|
68
|
+
# Write the frame and any continuation frames to the stream.
|
|
69
|
+
# @parameter stream [IO] The stream to write to.
|
|
47
70
|
def write(stream)
|
|
48
71
|
super
|
|
49
72
|
|
|
@@ -54,6 +77,9 @@ module Protocol
|
|
|
54
77
|
|
|
55
78
|
attr_accessor :continuation
|
|
56
79
|
|
|
80
|
+
# Pack data into this frame, creating continuation frames if needed.
|
|
81
|
+
# @parameter data [String] The data to pack.
|
|
82
|
+
# @parameter options [Hash] Options including maximum_size.
|
|
57
83
|
def pack(data, **options)
|
|
58
84
|
maximum_size = options[:maximum_size]
|
|
59
85
|
|
|
@@ -75,6 +101,8 @@ module Protocol
|
|
|
75
101
|
end
|
|
76
102
|
end
|
|
77
103
|
|
|
104
|
+
# Unpack data from this frame and any continuation frames.
|
|
105
|
+
# @returns [String] The complete unpacked data.
|
|
78
106
|
def unpack
|
|
79
107
|
if @continuation.nil?
|
|
80
108
|
super
|
|
@@ -95,11 +123,21 @@ module Protocol
|
|
|
95
123
|
|
|
96
124
|
TYPE = 0x9
|
|
97
125
|
|
|
126
|
+
# Read the frame and any continuation frames from the stream.
|
|
127
|
+
# @parameter stream [IO] The stream to read from.
|
|
128
|
+
# @parameter maximum_frame_size [Integer] Maximum allowed frame size.
|
|
129
|
+
# @parameter limit [Integer] The maximum number of continuation frames to read.
|
|
130
|
+
def read(stream, maximum_frame_size, limit = 8)
|
|
131
|
+
super
|
|
132
|
+
end
|
|
133
|
+
|
|
98
134
|
# This is only invoked if the continuation is received out of the normal flow.
|
|
99
135
|
def apply(connection)
|
|
100
136
|
connection.receive_continuation(self)
|
|
101
137
|
end
|
|
102
138
|
|
|
139
|
+
# Get a string representation of the continuation frame.
|
|
140
|
+
# @returns [String] Human-readable frame information.
|
|
103
141
|
def inspect
|
|
104
142
|
"\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} length=#{@length || 0}b>"
|
|
105
143
|
end
|
|
@@ -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 "frame"
|
|
7
7
|
require_relative "padded"
|
|
@@ -25,10 +25,16 @@ module Protocol
|
|
|
25
25
|
|
|
26
26
|
TYPE = 0x0
|
|
27
27
|
|
|
28
|
+
# Check if this frame marks the end of the stream.
|
|
29
|
+
# @returns [Boolean] True if the END_STREAM flag is set.
|
|
28
30
|
def end_stream?
|
|
29
31
|
flag_set?(END_STREAM)
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# Pack data into the frame, handling empty data as stream end.
|
|
35
|
+
# @parameter data [String | Nil] The data to pack into the frame.
|
|
36
|
+
# @parameter arguments [Array] Additional arguments passed to super.
|
|
37
|
+
# @parameter options [Hash] Additional options passed to super.
|
|
32
38
|
def pack(data, *arguments, **options)
|
|
33
39
|
if data
|
|
34
40
|
super
|
|
@@ -38,10 +44,14 @@ module Protocol
|
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
# Apply this DATA frame to a connection for processing.
|
|
48
|
+
# @parameter connection [Connection] The connection to apply the frame to.
|
|
41
49
|
def apply(connection)
|
|
42
50
|
connection.receive_data(self)
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
# Provide a readable representation of the frame for debugging.
|
|
54
|
+
# @returns [String] A formatted string representation of the frame.
|
|
45
55
|
def inspect
|
|
46
56
|
"\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} #{@length || 0}b>"
|
|
47
57
|
end
|
data/lib/protocol/http2/error.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 "protocol/http/error"
|
|
7
7
|
|
|
@@ -57,11 +57,14 @@ module Protocol
|
|
|
57
57
|
# connection must be aborted.
|
|
58
58
|
class HandshakeError < Error
|
|
59
59
|
end
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
# Raised by stream or connection handlers, results in GOAWAY frame
|
|
62
62
|
# which signals termination of the current connection. You *cannot*
|
|
63
63
|
# recover from this exception, or any exceptions subclassed from it.
|
|
64
64
|
class ProtocolError < Error
|
|
65
|
+
# Initialize a protocol error with message and error code.
|
|
66
|
+
# @parameter message [String] The error message.
|
|
67
|
+
# @parameter code [Integer] The HTTP/2 error code.
|
|
65
68
|
def initialize(message, code = PROTOCOL_ERROR)
|
|
66
69
|
super(message)
|
|
67
70
|
|
|
@@ -71,26 +74,36 @@ module Protocol
|
|
|
71
74
|
attr :code
|
|
72
75
|
end
|
|
73
76
|
|
|
77
|
+
# Represents an error specific to stream operations.
|
|
74
78
|
class StreamError < ProtocolError
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
# Represents an error for operations on closed streams.
|
|
77
82
|
class StreamClosed < StreamError
|
|
83
|
+
# Initialize a stream closed error.
|
|
84
|
+
# @parameter message [String] The error message.
|
|
78
85
|
def initialize(message)
|
|
79
86
|
super message, STREAM_CLOSED
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
|
|
90
|
+
# Represents a GOAWAY-related protocol error.
|
|
83
91
|
class GoawayError < ProtocolError
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
# When the frame payload does not match expectations.
|
|
87
95
|
class FrameSizeError < ProtocolError
|
|
96
|
+
# Initialize a frame size error.
|
|
97
|
+
# @parameter message [String] The error message.
|
|
88
98
|
def initialize(message)
|
|
89
99
|
super message, FRAME_SIZE_ERROR
|
|
90
100
|
end
|
|
91
101
|
end
|
|
92
102
|
|
|
103
|
+
# Represents a header processing error.
|
|
93
104
|
class HeaderError < StreamClosed
|
|
105
|
+
# Initialize a header error.
|
|
106
|
+
# @parameter message [String] The error message.
|
|
94
107
|
def initialize(message)
|
|
95
108
|
super(message)
|
|
96
109
|
end
|
|
@@ -98,6 +111,8 @@ module Protocol
|
|
|
98
111
|
|
|
99
112
|
# Raised on invalid flow control frame or command.
|
|
100
113
|
class FlowControlError < ProtocolError
|
|
114
|
+
# Initialize a flow control error.
|
|
115
|
+
# @parameter message [String] The error message.
|
|
101
116
|
def initialize(message)
|
|
102
117
|
super message, FLOW_CONTROL_ERROR
|
|
103
118
|
end
|