protocol-http2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'ping_frame'
22
+
23
+ module Protocol
24
+ module HTTP2
25
+ class Settings
26
+ HEADER_TABLE_SIZE = 0x1
27
+ ENABLE_PUSH = 0x2
28
+ MAXIMUM_CONCURRENT_STREAMS = 0x3
29
+ INITIAL_WINDOW_SIZE = 0x4
30
+ MAXIMUM_FRAME_SIZE = 0x5
31
+ MAXIMUM_HEADER_LIST_SIZE = 0x6
32
+
33
+ # Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets.
34
+ attr_accessor :header_table_size
35
+
36
+ # 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.
37
+ attr :enable_push
38
+
39
+ def enable_push= value
40
+ if @enable_push == 0 || @enable_push == 1
41
+ @enable_push = value
42
+ else
43
+ raise ProtocolError, "Invalid value for enable_push: #{value}"
44
+ end
45
+ end
46
+
47
+ def enable_push?
48
+ @enable_push != 0
49
+ end
50
+
51
+ # Indicates the maximum number of concurrent streams that the sender will allow.
52
+ attr_accessor :maximum_concurrent_streams
53
+
54
+ # Indicates the sender's initial window size (in octets) for stream-level flow control.
55
+ attr :initial_window_size
56
+
57
+ def initial_window_size= value
58
+ if value <= MAXIMUM_ALLOWED_WINDOW_SIZE
59
+ @initial_window_size = value
60
+ else
61
+ raise FlowControlError, "Invalid value for initial_window_size: #{value} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}"
62
+ end
63
+ end
64
+
65
+ # Indicates the size of the largest frame payload that the sender is willing to receive, in octets.
66
+ attr :maximum_frame_size
67
+
68
+ def maximum_frame_size= value
69
+ if value <= MAXIMUM_ALLOWED_FRAME_SIZE
70
+ @maximum_frame_size = value
71
+ else
72
+ raise ProtocolError, "Invalid value for maximum_frame_size: #{value} > #{MAXIMUM_ALLOWED_FRAME_SIZE}"
73
+ end
74
+ end
75
+
76
+ # This advisory setting informs a peer of the maximum size of header list that the sender is prepared to accept, in octets.
77
+ attr_accessor :maximum_header_list_size
78
+
79
+ def initialize
80
+ # These limits are taken from the RFC:
81
+ # https://tools.ietf.org/html/rfc7540#section-6.5.2
82
+ @header_table_size = 4096
83
+ @enable_push = 1
84
+ @maximum_concurrent_streams = 0xFFFFFFFF
85
+ @initial_window_size = 0xFFFF # 2**16 - 1
86
+ @maximum_frame_size = 0x4000 # 2**14
87
+ @maximum_header_list_size = 0xFFFFFFFF
88
+ end
89
+
90
+ ASSIGN = [
91
+ nil,
92
+ :header_table_size=,
93
+ :enable_push=,
94
+ :maximum_concurrent_streams=,
95
+ :initial_window_size=,
96
+ :maximum_frame_size=,
97
+ :maximum_header_list_size=,
98
+ ]
99
+
100
+ def update(changes)
101
+ changes.each do |key, value|
102
+ if name = ASSIGN[key]
103
+ self.send(name, value)
104
+ end
105
+ end
106
+ end
107
+
108
+ def difference(other)
109
+ changes = []
110
+
111
+ if @header_table_size != other.header_table_size
112
+ changes << [HEADER_TABLE_SIZE, @header_table_size]
113
+ end
114
+
115
+ if @enable_push != other.enable_push
116
+ changes << [ENABLE_PUSH, @enable_push ? 1 : 0]
117
+ end
118
+
119
+ if @maximum_concurrent_streams != other.maximum_concurrent_streams
120
+ changes << [MAXIMUM_CONCURRENT_STREAMS, @maximum_concurrent_streams]
121
+ end
122
+
123
+ if @initial_window_size != other.initial_window_size
124
+ changes << [INITIAL_WINDOW_SIZE, @initial_window_size]
125
+ end
126
+
127
+ if @maximum_frame_size != other.maximum_frame_size
128
+ changes << [MAXIMUM_FRAME_SIZE, @maximum_frame_size]
129
+ end
130
+
131
+ if @maximum_header_list_size != other.maximum_header_list_size
132
+ changes << [MAXIMUM_HEADER_LIST_SIZE, @maximum_header_list_size]
133
+ end
134
+
135
+ return changes
136
+ end
137
+ end
138
+
139
+ class PendingSettings
140
+ def initialize(current = Settings.new)
141
+ @current = current
142
+ @pending = current.dup
143
+
144
+ @queue = []
145
+ end
146
+
147
+ attr :current
148
+ attr :pending
149
+
150
+ def append(changes)
151
+ @queue << changes
152
+ @pending.update(changes)
153
+ end
154
+
155
+ def acknowledge
156
+ if changes = @queue.shift
157
+ @current.update(changes)
158
+
159
+ return changes
160
+ else
161
+ raise ProtocolError.new("Cannot acknowledge settings, no changes pending")
162
+ end
163
+ end
164
+
165
+ def header_table_size
166
+ @current.header_table_size
167
+ end
168
+
169
+ def enable_push
170
+ @current.enable_push
171
+ end
172
+
173
+ def maximum_concurrent_streams
174
+ @current.maximum_concurrent_streams
175
+ end
176
+
177
+ def initial_window_size
178
+ @current.initial_window_size
179
+ end
180
+
181
+ def maximum_frame_size
182
+ @current.maximum_frame_size
183
+ end
184
+
185
+ def maximum_header_list_size
186
+ @current.maximum_header_list_size
187
+ end
188
+ end
189
+
190
+ # 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".
191
+ #
192
+ # +-------------------------------+
193
+ # | Identifier (16) |
194
+ # +-------------------------------+-------------------------------+
195
+ # | Value (32) |
196
+ # +---------------------------------------------------------------+
197
+ #
198
+ class SettingsFrame < Frame
199
+ TYPE = 0x4
200
+ FORMAT = "nN".freeze
201
+
202
+ include Acknowledgement
203
+
204
+ def connection?
205
+ true
206
+ end
207
+
208
+ def unpack
209
+ super.scan(/....../).map{|s| s.unpack(FORMAT)}
210
+ end
211
+
212
+ def pack(settings = [])
213
+ super settings.map{|s| s.pack(FORMAT)}.join
214
+ end
215
+
216
+ def apply(connection)
217
+ connection.receive_settings(self)
218
+ end
219
+
220
+ def read_payload(stream)
221
+ super
222
+
223
+ if @stream_id != 0
224
+ raise ProtocolError, "Settings apply to connection only, but stream_id was given"
225
+ end
226
+
227
+ if acknowledgement? and @length != 0
228
+ raise FrameSizeError, "Settings acknowledgement must not contain payload: #{@payload.inspect}"
229
+ end
230
+
231
+ if (@length % 6) != 0
232
+ raise FrameSizeError, "Invalid frame length"
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,372 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'connection'
22
+ require_relative 'flow_control'
23
+
24
+ module Protocol
25
+ module HTTP2
26
+ # A single HTTP 2.0 connection can multiplex multiple streams in parallel:
27
+ # multiple requests and responses can be in flight simultaneously and stream
28
+ # data can be interleaved and prioritized.
29
+ #
30
+ # This class encapsulates all of the state, transition, flow-control, and
31
+ # error management as defined by the HTTP 2.0 specification. All you have
32
+ # to do is subscribe to appropriate events (marked with ":" prefix in
33
+ # diagram below) and provide your application logic to handle request
34
+ # and response processing.
35
+ #
36
+ # +--------+
37
+ # send PP | | recv PP
38
+ # ,--------| idle |--------.
39
+ # / | | \
40
+ # v +--------+ v
41
+ # +----------+ | +----------+
42
+ # | | | send H / | |
43
+ # ,------| reserved | | recv H | reserved |------.
44
+ # | | (local) | | | (remote) | |
45
+ # | +----------+ v +----------+ |
46
+ # | | +--------+ | |
47
+ # | | recv ES | | send ES | |
48
+ # | send H | ,-------| open |-------. | recv H |
49
+ # | | / | | \ | |
50
+ # | v v +--------+ v v |
51
+ # | +----------+ | +----------+ |
52
+ # | | half | | | half | |
53
+ # | | closed | | send R / | closed | |
54
+ # | | (remote) | | recv R | (local) | |
55
+ # | +----------+ | +----------+ |
56
+ # | | | | |
57
+ # | | send ES / | recv ES / | |
58
+ # | | send R / v send R / | |
59
+ # | | recv R +--------+ recv R | |
60
+ # | send R / `----------->| |<-----------' send R / |
61
+ # | recv R | closed | recv R |
62
+ # `----------------------->| |<----------------------'
63
+ # +--------+
64
+ #
65
+ # send: endpoint sends this frame
66
+ # recv: endpoint receives this frame
67
+ #
68
+ # H: HEADERS frame (with implied CONTINUATIONs)
69
+ # PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
70
+ # ES: END_STREAM flag
71
+ # R: RST_STREAM frame
72
+ #
73
+ class Stream
74
+ include FlowControl
75
+
76
+ def initialize(connection, id = connection.next_stream_id)
77
+ @connection = connection
78
+ @id = id
79
+
80
+ @connection.streams[@id] = self
81
+
82
+ @state = :idle
83
+
84
+ @priority = nil
85
+ @local_window = Window.new(connection.local_settings.initial_window_size)
86
+ @remote_window = Window.new(connection.remote_settings.initial_window_size)
87
+
88
+ @headers = nil
89
+ @data = nil
90
+ end
91
+
92
+ # Stream ID (odd for client initiated streams, even otherwise).
93
+ attr :id
94
+
95
+ # Stream state as defined by HTTP 2.0.
96
+ attr :state
97
+
98
+ attr :headers
99
+ attr :data
100
+
101
+ attr :local_window
102
+ attr :remote_window
103
+
104
+ def maximum_frame_size
105
+ @connection.available_frame_size
106
+ end
107
+
108
+ def write_frame(frame)
109
+ @connection.write_frame(frame)
110
+ end
111
+
112
+ def closed?
113
+ @state == :closed or @state == :reset
114
+ end
115
+
116
+ def send_headers?
117
+ @state == :idle or @state == :reserved_local or @state == :open or @state == :half_closed_remote
118
+ end
119
+
120
+ def send_failure(status, reason)
121
+ if send_headers?
122
+ send_headers(nil, [
123
+ [':status', status.to_s],
124
+ ['reason', reason]
125
+ ], END_STREAM)
126
+ else
127
+ send_reset_stream(PROTOCOL_ERROR)
128
+ end
129
+
130
+ return nil
131
+ end
132
+
133
+ private def write_headers(priority, headers, flags = 0)
134
+ data = @connection.encode_headers(headers)
135
+
136
+ frame = HeadersFrame.new(@id, flags)
137
+ frame.pack(priority, data, maximum_size: @connection.maximum_frame_size)
138
+
139
+ write_frame(frame)
140
+
141
+ return frame
142
+ end
143
+
144
+ #The HEADERS frame is used to open a stream, and additionally carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", "reserved (local)", "open", or "half-closed (remote)" state.
145
+ def send_headers(*args)
146
+ if @state == :idle
147
+ frame = write_headers(*args)
148
+
149
+ if frame.end_stream?
150
+ @state = :half_closed_local
151
+ else
152
+ @state = :open
153
+ end
154
+ elsif @state == :reserved_local
155
+ frame = write_headers(*args)
156
+
157
+ @state = :half_closed_remote
158
+ elsif @state == :open
159
+ frame = write_headers(*args)
160
+
161
+ if frame.end_stream?
162
+ @state = :half_closed_local
163
+ end
164
+ elsif @state == :half_closed_remote
165
+ frame = write_headers(*args)
166
+
167
+ if frame.end_stream?
168
+ close!
169
+ end
170
+ else
171
+ raise ProtocolError, "Cannot send headers in state: #{@state}"
172
+ end
173
+ end
174
+
175
+ def consume_remote_window(frame)
176
+ super
177
+
178
+ @connection.consume_remote_window(frame)
179
+ end
180
+
181
+ private def write_data(data, flags = 0, **options)
182
+ frame = DataFrame.new(@id, flags)
183
+ frame.pack(data, **options)
184
+
185
+ # This might fail if the data payload was too big:
186
+ consume_remote_window(frame)
187
+
188
+ write_frame(frame)
189
+
190
+ return frame
191
+ end
192
+
193
+ def send_data(*args)
194
+ if @state == :open
195
+ frame = write_data(*args)
196
+
197
+ if frame.end_stream?
198
+ @state = :half_closed_local
199
+ end
200
+ elsif @state == :half_closed_remote
201
+ frame = write_data(*args)
202
+
203
+ if frame.end_stream?
204
+ close!
205
+ end
206
+ else
207
+ raise ProtocolError, "Cannot send data in state: #{@state}"
208
+ end
209
+ end
210
+
211
+ def close!(state = :closed)
212
+ @state = state
213
+
214
+ @connection.streams.delete(@id)
215
+ end
216
+
217
+ def send_reset_stream(error_code = 0)
218
+ if @state != :idle and @state != :closed
219
+ frame = ResetStreamFrame.new(@id)
220
+ frame.pack(error_code)
221
+
222
+ write_frame(frame)
223
+
224
+ close!(:reset)
225
+ else
226
+ raise ProtocolError, "Cannot reset stream in state: #{@state}"
227
+ end
228
+ end
229
+
230
+ private def process_headers(frame)
231
+ # Receiving request headers:
232
+ priority, data = frame.unpack
233
+
234
+ if priority
235
+ @priority = priority
236
+ end
237
+
238
+ @connection.decode_headers(data)
239
+ end
240
+
241
+ def receive_headers(frame)
242
+ if @state == :idle
243
+ if frame.end_stream?
244
+ @state = :half_closed_remote
245
+ else
246
+ @state = :open
247
+ end
248
+
249
+ @headers = process_headers(frame)
250
+ elsif @state == :reserved_remote
251
+ @state = :half_closed_local
252
+
253
+ @headers = process_headers(frame)
254
+ elsif @state == :open
255
+ if frame.end_stream?
256
+ @state = :half_closed_remote
257
+ end
258
+
259
+ @headers = process_headers(frame)
260
+ elsif @state == :half_closed_local
261
+ if frame.end_stream?
262
+ close!
263
+ end
264
+
265
+ @headers = process_headers(frame)
266
+ elsif @state == :reset
267
+ # ignore...
268
+ else
269
+ raise ProtocolError, "Cannot receive headers in state: #{@state}"
270
+ end
271
+ end
272
+
273
+ # DATA frames are subject to flow control and can only be sent when a stream is in the "open" or "half-closed (remote)" state. The entire DATA frame payload is included in flow control, including the Pad Length and Padding fields if present. If a DATA frame is received whose stream is not in "open" or "half-closed (local)" state, the recipient MUST respond with a stream error of type STREAM_CLOSED.
274
+ def receive_data(frame)
275
+ if @state == :open
276
+ consume_local_window(frame)
277
+
278
+ if frame.end_stream?
279
+ @state = :half_closed_remote
280
+ end
281
+
282
+ @data = frame.unpack
283
+ elsif @state == :half_closed_local
284
+ consume_local_window(frame)
285
+
286
+ if frame.end_stream?
287
+ close!
288
+ end
289
+
290
+ @data = frame.unpack
291
+ elsif @state == :reset
292
+ # ignore...
293
+ else
294
+ raise ProtocolError, "Cannot receive data in state: #{@state}"
295
+ end
296
+ end
297
+
298
+ def receive_priority(frame)
299
+ @priority = frame.unpack
300
+ end
301
+
302
+ def receive_reset_stream(frame)
303
+ if @state != :idle and @state != :closed
304
+ close!
305
+
306
+ return frame.unpack
307
+ else
308
+ raise ProtocolError, "Cannot reset stream in state: #{@state}"
309
+ end
310
+ end
311
+
312
+ # A normal request is client request -> server response -> client.
313
+ # A push promise is server request -> client -> server response -> client.
314
+ # The server generates the same set of headers as if the client was sending a request, and sends these to the client. The client can reject the request by resetting the (new) stream. Otherwise, the server will start sending a response as if the client had send the request.
315
+ private def write_push_promise(stream_id, headers, flags = 0, **options)
316
+ data = @connection.encode_headers(headers)
317
+
318
+ frame = PushPromiseFrame.new(@id, flags)
319
+ frame.pack(stream_id, data, maximum_size: @connection.maximum_frame_size)
320
+
321
+ write_frame(frame)
322
+
323
+ return frame
324
+ end
325
+
326
+ def reserved_local!
327
+ if @state == :idle
328
+ @state = :reserved_local
329
+ else
330
+ raise ProtocolError, "Cannot reserve stream in state: #{@state}"
331
+ end
332
+ end
333
+
334
+ def reserved_remote!
335
+ if @state == :idle
336
+ @state = :reserved_remote
337
+ else
338
+ raise ProtocolError, "Cannot reserve stream in state: #{@state}"
339
+ end
340
+ end
341
+
342
+ def create_promise_stream(headers, stream_id)
343
+ @connection.create_stream(stream_id)
344
+ end
345
+
346
+ # Server push is semantically equivalent to a server responding to a request; however, in this case, that request is also sent by the server, as a PUSH_PROMISE frame.
347
+ # @param headers [Hash] contains a complete set of request header fields that the server attributes to the request.
348
+ def send_push_promise(headers, stream_id = @connection.next_stream_id)
349
+ if @state == :open or @state == :half_closed_remote
350
+ promised_stream = self.create_promise_stream(headers, stream_id)
351
+ promised_stream.reserved_local!
352
+
353
+ write_push_promise(promised_stream.id, headers)
354
+
355
+ return promised_stream
356
+ else
357
+ raise ProtocolError, "Cannot send push promise in state: #{@state}"
358
+ end
359
+ end
360
+
361
+ def receive_push_promise(frame)
362
+ promised_stream_id, data = frame.unpack
363
+ headers = @connection.decode_headers(data)
364
+
365
+ stream = self.create_promise_stream(headers, promised_stream_id)
366
+ stream.reserved_remote!
367
+
368
+ return stream, headers
369
+ end
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Protocol
22
+ module HTTP2
23
+ VERSION = "0.1.0"
24
+ end
25
+ end