File: settings_frame.rb

package info (click to toggle)
ruby-protocol-http2 0.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 472 kB
  • sloc: ruby: 3,627; makefile: 4
file content (295 lines) | stat: -rw-r--r-- 9,538 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2025, by Samuel Williams.

require_relative "ping_frame"

module Protocol
	module HTTP2
		# HTTP/2 connection settings container and management.
		class Settings
			HEADER_TABLE_SIZE = 0x1
			ENABLE_PUSH = 0x2
			MAXIMUM_CONCURRENT_STREAMS = 0x3
			INITIAL_WINDOW_SIZE = 0x4
			MAXIMUM_FRAME_SIZE = 0x5
			MAXIMUM_HEADER_LIST_SIZE = 0x6
			ENABLE_CONNECT_PROTOCOL = 0x8
			NO_RFC7540_PRIORITIES = 0x9
			
			ASSIGN = [
				nil,
				:header_table_size=,
				:enable_push=,
				:maximum_concurrent_streams=,
				:initial_window_size=,
				:maximum_frame_size=,
				:maximum_header_list_size=,
				nil,
				:enable_connect_protocol=,
				:no_rfc7540_priorities=,
			]
			
			# Initialize settings with default values from HTTP/2 specification.
			def initialize
				# These limits are taken from the RFC:
				# https://tools.ietf.org/html/rfc7540#section-6.5.2
				@header_table_size = 4096
				@enable_push = 1
				@maximum_concurrent_streams = 0xFFFFFFFF
				@initial_window_size = 0xFFFF # 2**16 - 1
				@maximum_frame_size = 0x4000 # 2**14
				@maximum_header_list_size = 0xFFFFFFFF
				@enable_connect_protocol = 0
				@no_rfc7540_priorities = 0
			end
			
			# Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets.
			attr_accessor :header_table_size
			
			# This setting can be used to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame if it receives this parameter set to a value of 0.
			attr :enable_push
			
			# Set the server push enable flag.
			# @parameter value [Integer] Must be 0 (disabled) or 1 (enabled).
			# @raises [ProtocolError] If the value is invalid.
			def enable_push= value
				if value == 0 or value == 1
					@enable_push = value
				else
					raise ProtocolError, "Invalid value for enable_push: #{value}"
				end
			end
			
			# Check if server push is enabled.
			# @returns [Boolean] True if server push is enabled.
			def enable_push?
				@enable_push == 1
			end
			
			# Indicates the maximum number of concurrent streams that the sender will allow.
			attr_accessor :maximum_concurrent_streams
			
			# Indicates the sender's initial window size (in octets) for stream-level flow control.
			attr :initial_window_size
			
			# Set the initial window size for stream-level flow control.
			# @parameter value [Integer] The window size in octets.
			# @raises [ProtocolError] If the value exceeds the maximum allowed.
			def initial_window_size= value
				if value <= MAXIMUM_ALLOWED_WINDOW_SIZE
					@initial_window_size = value
				else
					raise ProtocolError, "Invalid value for initial_window_size: #{value} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}"
				end
			end
			
			# Indicates the size of the largest frame payload that the sender is willing to receive, in octets.
			attr :maximum_frame_size
			
			# Set the maximum frame size the sender is willing to receive.
			# @parameter value [Integer] The maximum frame size in octets.
			# @raises [ProtocolError] If the value is outside the allowed range.
			def maximum_frame_size= value
				if value > MAXIMUM_ALLOWED_FRAME_SIZE
					raise ProtocolError, "Invalid value for maximum_frame_size: #{value} > #{MAXIMUM_ALLOWED_FRAME_SIZE}"
				elsif value < MINIMUM_ALLOWED_FRAME_SIZE
					raise ProtocolError, "Invalid value for maximum_frame_size: #{value} < #{MINIMUM_ALLOWED_FRAME_SIZE}"
				else
					@maximum_frame_size = value
				end
			end
			
			# This advisory setting informs a peer of the maximum size of header list that the sender is prepared to accept, in octets.
			attr_accessor :maximum_header_list_size
			
			attr :enable_connect_protocol
			
			# Set the CONNECT protocol enable flag.
			# @parameter value [Integer] Must be 0 (disabled) or 1 (enabled).
			# @raises [ProtocolError] If the value is invalid.
			def enable_connect_protocol= value
				if value == 0 or value == 1
					@enable_connect_protocol = value
				else
					raise ProtocolError, "Invalid value for enable_connect_protocol: #{value}"
				end
			end
			
			# Check if CONNECT protocol is enabled.
			# @returns [Boolean] True if CONNECT protocol is enabled.
			def enable_connect_protocol?
				@enable_connect_protocol == 1
			end
			
			attr :no_rfc7540_priorities
			
			# Set the RFC 7540 priorities disable flag.
			# @parameter value [Integer] Must be 0 (enabled) or 1 (disabled).
			# @raises [ProtocolError] If the value is invalid.
			def no_rfc7540_priorities= value
				if value == 0 or value == 1
					@no_rfc7540_priorities = value
				else
					raise ProtocolError, "Invalid value for no_rfc7540_priorities: #{value}"
				end
			end
			
			# Check if RFC 7540 priorities are disabled.
			# @returns [Boolean] True if RFC 7540 priorities are disabled.
			def no_rfc7540_priorities?
				@no_rfc7540_priorities == 1
			end
			
			# Update settings with a hash of changes.
			# @parameter changes [Hash] Hash of setting keys and values to update.
			def update(changes)
				changes.each do |key, value|
					if name = ASSIGN[key]
						self.send(name, value)
					end
				end
			end
		end
		
		# Manages pending settings changes that haven't been acknowledged yet.
		class PendingSettings
			# Initialize with current settings.
			# @parameter current [Settings] The current settings object.
			def initialize(current = Settings.new)
				@current = current
				@pending = current.dup
				
				@queue = []
			end
			
			attr :current
			attr :pending
			
			# Append changes to the pending queue.
			# @parameter changes [Hash] Hash of setting changes to queue.
			def append(changes)
				@queue << changes
				@pending.update(changes)
			end
			
			# Acknowledge the next set of pending changes.
			def acknowledge
				if changes = @queue.shift
					@current.update(changes)
					
					return changes
				else
					raise ProtocolError, "Cannot acknowledge settings, no changes pending"
				end
			end
			
			# Get the current header table size setting.
			# @returns [Integer] The header table size in octets.
			def header_table_size
				@current.header_table_size
			end
			
			# Get the current enable push setting.
			# @returns [Integer] 1 if push is enabled, 0 if disabled.
			def enable_push
				@current.enable_push
			end
			
			# Get the current maximum concurrent streams setting.
			# @returns [Integer] The maximum number of concurrent streams.
			def maximum_concurrent_streams
				@current.maximum_concurrent_streams
			end
			
			# Get the current initial window size setting.
			# @returns [Integer] The initial window size in octets.
			def initial_window_size
				@current.initial_window_size
			end
			
			# Get the current maximum frame size setting.
			# @returns [Integer] The maximum frame size in octets.
			def maximum_frame_size
				@current.maximum_frame_size
			end
			
			# Get the current maximum header list size setting.
			# @returns [Integer] The maximum header list size in octets.
			def maximum_header_list_size
				@current.maximum_header_list_size
			end
			
			# Get the current CONNECT protocol enable setting.
			# @returns [Integer] 1 if CONNECT protocol is enabled, 0 if disabled.
			def enable_connect_protocol
				@current.enable_connect_protocol
			end
		end
		
		# The SETTINGS frame conveys configuration parameters that affect how endpoints communicate, such as preferences and constraints on peer behavior. The SETTINGS frame is also used to acknowledge the receipt of those parameters. Individually, a SETTINGS parameter can also be referred to as a "setting".
		# 
		# +-------------------------------+
		# |       Identifier (16)         |
		# +-------------------------------+-------------------------------+
		# |                        Value (32)                             |
		# +---------------------------------------------------------------+
		#
		class SettingsFrame < Frame
			TYPE = 0x4
			FORMAT = "nN".freeze
			
			include Acknowledgement
			
			# Check if this frame applies to the connection level.
			# @returns [Boolean] Always returns true for SETTINGS frames.
			def connection?
				true
			end
			
			# Unpack settings parameters from the frame payload.
			# @returns [Array] Array of [key, value] pairs representing settings.
			def unpack
				if buffer = super
					# TODO String#each_slice, or #each_unpack would be nice.
					buffer.scan(/....../m).map{|s| s.unpack(FORMAT)}
				else
					[]
				end
			end
			
			# Pack settings parameters into the frame payload.
			# @parameter settings [Array] Array of [key, value] pairs to pack.
			def pack(settings = [])
				super(settings.map{|s| s.pack(FORMAT)}.join)
			end
			
			# Apply this SETTINGS frame to a connection for processing.
			# @parameter connection [Connection] The connection to apply the frame to.
			def apply(connection)
				connection.receive_settings(self)
			end
			
			# Read and validate the SETTINGS frame payload.
			# @parameter stream [IO] The stream to read from.
			# @raises [ProtocolError] If the frame is invalid.
			# @raises [FrameSizeError] If the frame length is invalid.
			def read_payload(stream)
				super
				
				if @stream_id != 0
					raise ProtocolError, "Settings apply to connection only, but stream_id was given"
				end
				
				if acknowledgement? and @length != 0
					raise FrameSizeError, "Settings acknowledgement must not contain payload: #{@payload.inspect}"
				end
				
				if (@length % 6) != 0
					raise FrameSizeError, "Invalid frame length"
				end
			end
		end
	end
end