protocol-http2 0.22.1 → 0.23.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 +87 -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/readme.md +12 -0
- data/releases.md +4 -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: d4df424d3c1c97120be6031179d3febd9b6bcbbaeb163b24d53b03f05ae28696
|
4
|
+
data.tar.gz: baf0e59d7998a934d35eb1d33fa6205ccbae7bb6c5d352721b1d5d1ba0880131
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c7f24cd932068ecdcdf604bd9a9e3757c0fe40167844dfd351340f088507b6975a18288991c34342c15ad4217bf111a6e4dfa620c68c1bb7c83c149f0a5d21d
|
7
|
+
data.tar.gz: 404eff6b4e7a395cdcd44fd366564dbd67edc292857685d6294144b66892eda4dbdc60518cd0e2f58e034d001b45c0aeeffb389fccf49ad54464b97332bcc398
|
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-2025, 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
|
@@ -106,10 +119,17 @@ module Protocol
|
|
106
119
|
end
|
107
120
|
end
|
108
121
|
|
122
|
+
# Encode headers using HPACK compression.
|
123
|
+
# @parameter headers [Array] The headers to encode.
|
124
|
+
# @parameter buffer [String] Optional buffer for encoding output.
|
125
|
+
# @returns [String] The encoded header block.
|
109
126
|
def encode_headers(headers, buffer = String.new.b)
|
110
127
|
HPACK::Compressor.new(buffer, @encoder, table_size_limit: @remote_settings.header_table_size).encode(headers)
|
111
128
|
end
|
112
129
|
|
130
|
+
# Decode headers using HPACK decompression.
|
131
|
+
# @parameter data [String] The encoded header block data.
|
132
|
+
# @returns [Array] The decoded headers.
|
113
133
|
def decode_headers(data)
|
114
134
|
HPACK::Decompressor.new(data, @decoder, table_size_limit: @local_settings.header_table_size).decode
|
115
135
|
end
|
@@ -141,6 +161,9 @@ module Protocol
|
|
141
161
|
end
|
142
162
|
end
|
143
163
|
|
164
|
+
# Execute a block within a synchronized context.
|
165
|
+
# This method provides a synchronization primitive for thread safety.
|
166
|
+
# @yields The block to execute within the synchronized context.
|
144
167
|
def synchronize
|
145
168
|
yield
|
146
169
|
end
|
@@ -171,6 +194,8 @@ module Protocol
|
|
171
194
|
raise
|
172
195
|
end
|
173
196
|
|
197
|
+
# Send updated settings to the remote peer.
|
198
|
+
# @parameter changes [Hash] The settings changes to send.
|
174
199
|
def send_settings(changes)
|
175
200
|
@local_settings.append(changes)
|
176
201
|
|
@@ -197,6 +222,9 @@ module Protocol
|
|
197
222
|
self.close!
|
198
223
|
end
|
199
224
|
|
225
|
+
# Process a GOAWAY frame from the remote peer.
|
226
|
+
# @parameter frame [GoawayFrame] The GOAWAY frame to process.
|
227
|
+
# @raises [GoawayError] If the frame indicates a connection error.
|
200
228
|
def receive_goaway(frame)
|
201
229
|
# We capture the last stream that was processed.
|
202
230
|
@remote_stream_id, error_code, message = frame.unpack
|
@@ -209,6 +237,8 @@ module Protocol
|
|
209
237
|
end
|
210
238
|
end
|
211
239
|
|
240
|
+
# Write a single frame to the connection.
|
241
|
+
# @parameter frame [Frame] The frame to write.
|
212
242
|
def write_frame(frame)
|
213
243
|
synchronize do
|
214
244
|
@framer.write_frame(frame)
|
@@ -217,6 +247,10 @@ module Protocol
|
|
217
247
|
@framer.flush
|
218
248
|
end
|
219
249
|
|
250
|
+
# Write multiple frames within a synchronized block.
|
251
|
+
# @yields {|framer| ...} The framer for writing multiple frames.
|
252
|
+
# @parameter framer [Framer] The framer instance.
|
253
|
+
# @raises [EOFError] If the connection is closed.
|
220
254
|
def write_frames
|
221
255
|
if @framer
|
222
256
|
synchronize do
|
@@ -229,6 +263,8 @@ module Protocol
|
|
229
263
|
end
|
230
264
|
end
|
231
265
|
|
266
|
+
# Update local settings and adjust stream window capacities.
|
267
|
+
# @parameter changes [Hash] The settings changes to apply locally.
|
232
268
|
def update_local_settings(changes)
|
233
269
|
capacity = @local_settings.initial_window_size
|
234
270
|
|
@@ -239,6 +275,8 @@ module Protocol
|
|
239
275
|
@local_window.desired = capacity
|
240
276
|
end
|
241
277
|
|
278
|
+
# Update remote settings and adjust stream window capacities.
|
279
|
+
# @parameter changes [Hash] The settings changes to apply to remote peer.
|
242
280
|
def update_remote_settings(changes)
|
243
281
|
capacity = @remote_settings.initial_window_size
|
244
282
|
|
@@ -273,12 +311,17 @@ module Protocol
|
|
273
311
|
end
|
274
312
|
end
|
275
313
|
|
314
|
+
# Transition the connection to the open state.
|
315
|
+
# @returns [Connection] Self for method chaining.
|
276
316
|
def open!
|
277
317
|
@state = :open
|
278
318
|
|
279
319
|
return self
|
280
320
|
end
|
281
321
|
|
322
|
+
# Receive and process a SETTINGS frame from the remote peer.
|
323
|
+
# @parameter frame [SettingsFrame] The settings frame to process.
|
324
|
+
# @raises [ProtocolError] If the connection is in an invalid state.
|
282
325
|
def receive_settings(frame)
|
283
326
|
if @state == :new
|
284
327
|
# We transition to :open when we receive acknowledgement of first settings frame:
|
@@ -290,6 +333,8 @@ module Protocol
|
|
290
333
|
end
|
291
334
|
end
|
292
335
|
|
336
|
+
# Send a PING frame to the remote peer.
|
337
|
+
# @parameter data [String] The 8-byte ping payload data.
|
293
338
|
def send_ping(data)
|
294
339
|
if @state != :closed
|
295
340
|
frame = PingFrame.new
|
@@ -301,6 +346,9 @@ module Protocol
|
|
301
346
|
end
|
302
347
|
end
|
303
348
|
|
349
|
+
# Process a PING frame from the remote peer.
|
350
|
+
# @parameter frame [PingFrame] The ping frame to process.
|
351
|
+
# @raises [ProtocolError] If ping is received in invalid state.
|
304
352
|
def receive_ping(frame)
|
305
353
|
if @state != :closed
|
306
354
|
# This is handled in `read_payload`:
|
@@ -318,6 +366,9 @@ module Protocol
|
|
318
366
|
end
|
319
367
|
end
|
320
368
|
|
369
|
+
# Process a DATA frame from the remote peer.
|
370
|
+
# @parameter frame [DataFrame] The data frame to process.
|
371
|
+
# @raises [ProtocolError] If data is received for invalid stream.
|
321
372
|
def receive_data(frame)
|
322
373
|
update_local_window(frame)
|
323
374
|
|
@@ -330,6 +381,10 @@ module Protocol
|
|
330
381
|
end
|
331
382
|
end
|
332
383
|
|
384
|
+
# Check if the given stream ID is valid for remote initiation.
|
385
|
+
# This method should be overridden by client/server implementations.
|
386
|
+
# @parameter stream_id [Integer] The stream ID to validate.
|
387
|
+
# @returns [Boolean] True if the stream ID is valid for remote initiation.
|
333
388
|
def valid_remote_stream_id?(stream_id)
|
334
389
|
false
|
335
390
|
end
|
@@ -366,6 +421,10 @@ module Protocol
|
|
366
421
|
end
|
367
422
|
end
|
368
423
|
|
424
|
+
# Create a push promise stream.
|
425
|
+
# This method should be overridden by client/server implementations.
|
426
|
+
# @yields {|stream| ...} Optional block to configure the created stream.
|
427
|
+
# @returns [Stream] The created push promise stream.
|
369
428
|
def create_push_promise_stream(&block)
|
370
429
|
create_stream(&block)
|
371
430
|
end
|
@@ -397,10 +456,16 @@ module Protocol
|
|
397
456
|
end
|
398
457
|
end
|
399
458
|
|
459
|
+
# Receive and process a PUSH_PROMISE frame.
|
460
|
+
# @parameter frame [PushPromiseFrame] The push promise frame.
|
461
|
+
# @raises [ProtocolError] Always raises as push promises are not supported.
|
400
462
|
def receive_push_promise(frame)
|
401
463
|
raise ProtocolError, "Unable to receive push promise!"
|
402
464
|
end
|
403
465
|
|
466
|
+
# Receive and process a PRIORITY_UPDATE frame.
|
467
|
+
# @parameter frame [PriorityUpdateFrame] The priority update frame.
|
468
|
+
# @raises [ProtocolError] If the stream ID is invalid.
|
404
469
|
def receive_priority_update(frame)
|
405
470
|
if frame.stream_id != 0
|
406
471
|
raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
|
@@ -414,14 +479,25 @@ module Protocol
|
|
414
479
|
end
|
415
480
|
end
|
416
481
|
|
482
|
+
# Check if the given stream ID represents a client-initiated stream.
|
483
|
+
# Client streams always have odd numbered IDs.
|
484
|
+
# @parameter id [Integer] The stream ID to check.
|
485
|
+
# @returns [Boolean] True if the stream ID is client-initiated.
|
417
486
|
def client_stream_id?(id)
|
418
487
|
id.odd?
|
419
488
|
end
|
420
489
|
|
490
|
+
# Check if the given stream ID represents a server-initiated stream.
|
491
|
+
# Server streams always have even numbered IDs.
|
492
|
+
# @parameter id [Integer] The stream ID to check.
|
493
|
+
# @returns [Boolean] True if the stream ID is server-initiated.
|
421
494
|
def server_stream_id?(id)
|
422
495
|
id.even?
|
423
496
|
end
|
424
497
|
|
498
|
+
# Check if the given stream ID represents an idle stream.
|
499
|
+
# @parameter id [Integer] The stream ID to check.
|
500
|
+
# @returns [Boolean] True if the stream ID is idle (not yet used).
|
425
501
|
def idle_stream_id?(id)
|
426
502
|
if id.even?
|
427
503
|
# Server-initiated streams are even.
|
@@ -450,6 +526,9 @@ module Protocol
|
|
450
526
|
end
|
451
527
|
end
|
452
528
|
|
529
|
+
# Receive and process a RST_STREAM frame.
|
530
|
+
# @parameter frame [ResetStreamFrame] The reset stream frame.
|
531
|
+
# @raises [ProtocolError] If the frame is invalid for connection context.
|
453
532
|
def receive_reset_stream(frame)
|
454
533
|
if frame.connection?
|
455
534
|
raise ProtocolError, "Cannot reset connection!"
|
@@ -475,6 +554,8 @@ module Protocol
|
|
475
554
|
end
|
476
555
|
end
|
477
556
|
|
557
|
+
# Receive and process a WINDOW_UPDATE frame.
|
558
|
+
# @parameter frame [WindowUpdateFrame] The window update frame.
|
478
559
|
def receive_window_update(frame)
|
479
560
|
if frame.connection?
|
480
561
|
super
|
@@ -494,10 +575,15 @@ module Protocol
|
|
494
575
|
end
|
495
576
|
end
|
496
577
|
|
578
|
+
# Receive and process a CONTINUATION frame.
|
579
|
+
# @parameter frame [ContinuationFrame] The continuation frame.
|
580
|
+
# @raises [ProtocolError] Always raises as unexpected continuation frames are not supported.
|
497
581
|
def receive_continuation(frame)
|
498
582
|
raise ProtocolError, "Received unexpected continuation: #{frame.class}"
|
499
583
|
end
|
500
584
|
|
585
|
+
# Receive and process a generic frame (default handler).
|
586
|
+
# @parameter frame [Frame] The frame to receive.
|
501
587
|
def receive_frame(frame)
|
502
588
|
# ignore.
|
503
589
|
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
|