http-2-next 0.1.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.
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ module HTTP2Next
5
+ # HTTP 2.0 server connection class that implements appropriate header
6
+ # compression / decompression algorithms and stream management logic.
7
+ #
8
+ # Your code is responsible for feeding request data to the server object,
9
+ # which in turn performs all of the necessary HTTP 2.0 decoding / encoding,
10
+ # state management, and the rest. A simple example:
11
+ #
12
+ # @example
13
+ # socket = YourTransport.new
14
+ #
15
+ # conn = HTTP2Next::Server.new
16
+ # conn.on(:stream) do |stream|
17
+ # ...
18
+ # end
19
+ #
20
+ # while bytes = socket.read
21
+ # conn << bytes
22
+ # end
23
+ #
24
+ class Server < Connection
25
+ # Initialize new HTTP 2.0 server object.
26
+ def initialize(**settings)
27
+ @stream_id = 2
28
+ @state = :waiting_magic
29
+
30
+ @local_role = :server
31
+ @remote_role = :client
32
+
33
+ super
34
+ end
35
+
36
+ # GET / HTTP/1.1
37
+ # Host: server.example.com
38
+ # Connection: Upgrade, HTTP2-Settings
39
+ # Upgrade: h2c
40
+ # HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
41
+ #
42
+ # Requests that contain a payload body MUST be sent in their entirety
43
+ # before the client can send HTTP/2 frames. This means that a large
44
+ # request can block the use of the connection until it is completely sent.
45
+ #
46
+ # If concurrency of an initial request with subsequent requests is
47
+ # important, an OPTIONS request can be used to perform the upgrade to
48
+ # HTTP/2, at the cost of an additional round trip.
49
+ #
50
+ # HTTP/1.1 101 Switching Protocols
51
+ # Connection: Upgrade
52
+ # Upgrade: h2c
53
+ #
54
+ # [ HTTP/2 connection ...
55
+ #
56
+ # - The first HTTP/2 frame sent by the server MUST be a server
57
+ # connection preface (Section 3.5) consisting of a SETTINGS frame.
58
+ # - Upon receiving the 101 response, the client MUST send a connection
59
+ # preface (Section 3.5), which includes a SETTINGS frame.
60
+ #
61
+ # The HTTP/1.1 request that is sent prior to upgrade is assigned a stream
62
+ # identifier of 1 (see Section 5.1.1) with default priority values
63
+ # (Section 5.3.5). Stream 1 is implicitly "half-closed" from the client
64
+ # toward the server (see Section 5.1), since the request is completed as
65
+ # an HTTP/1.1 request. After commencing the HTTP/2 connection, stream 1
66
+ # is used for the response.
67
+ #
68
+ def upgrade(settings, headers, body)
69
+ @h2c_upgrade = :start
70
+
71
+ # Pretend that we've received the preface
72
+ # - puts us into :waiting_connection_preface state
73
+ # - emits a SETTINGS frame to the client
74
+ receive(CONNECTION_PREFACE_MAGIC)
75
+
76
+ # Process received HTTP2-Settings payload
77
+ buf = HTTP2Next::Buffer.new Base64.urlsafe_decode64(settings.to_s)
78
+ header = @framer.common_header(
79
+ length: buf.bytesize,
80
+ type: :settings,
81
+ stream: 0,
82
+ flags: []
83
+ )
84
+ buf.prepend(header)
85
+ receive(buf)
86
+
87
+ # Activate stream (id: 1) with on HTTP/1.1 request parameters
88
+ stream = activate_stream(id: 1)
89
+ emit(:stream, stream)
90
+
91
+ headers_frame = {
92
+ type: :headers,
93
+ stream: 1,
94
+ weight: DEFAULT_WEIGHT,
95
+ dependency: 0,
96
+ exclusive: false,
97
+ payload: headers
98
+ }
99
+
100
+ if body.empty?
101
+ headers_frame[:flags] = [:end_stream]
102
+ stream << headers_frame
103
+ else
104
+ stream << headers_frame
105
+ stream << { type: :data, stream: 1, payload: body, flags: [:end_stream] }
106
+ end
107
+
108
+ # Mark h2c upgrade as finished
109
+ @h2c_upgrade = :finished
110
+
111
+ # Transition back to :waiting_magic and wait for client's preface
112
+ @state = :waiting_magic
113
+ end
114
+
115
+ private
116
+
117
+ # Handle locally initiated server-push event emitted by the stream.
118
+ #
119
+ # @param args [Array]
120
+ # @param callback [Proc]
121
+ def promise(*args)
122
+ parent, headers, flags = *args
123
+ promise = new_stream(parent: parent)
124
+ promise.send(
125
+ type: :push_promise,
126
+ flags: flags,
127
+ stream: parent.id,
128
+ promise_stream: promise.id,
129
+ payload: headers.to_a
130
+ )
131
+
132
+ yield(promise)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,692 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ # A single HTTP 2.0 connection can multiplex multiple streams in parallel:
5
+ # multiple requests and responses can be in flight simultaneously and stream
6
+ # data can be interleaved and prioritized.
7
+ #
8
+ # This class encapsulates all of the state, transition, flow-control, and
9
+ # error management as defined by the HTTP 2.0 specification. All you have
10
+ # to do is subscribe to appropriate events (marked with ":" prefix in
11
+ # diagram below) and provide your application logic to handle request
12
+ # and response processing.
13
+ #
14
+ # +--------+
15
+ # PP | | PP
16
+ # ,--------| idle |--------.
17
+ # / | | \
18
+ # v +--------+ v
19
+ # +----------+ | +----------+
20
+ # | | | H | |
21
+ # ,---|:reserved | | |:reserved |---.
22
+ # | | (local) | v | (remote) | |
23
+ # | +----------+ +--------+ +----------+ |
24
+ # | | :active | | :active | |
25
+ # | | ,-------|:active |-------. | |
26
+ # | | H / ES | | ES \ H | |
27
+ # | v v +--------+ v v |
28
+ # | +-----------+ | +-----------+ |
29
+ # | |:half_close| | |:half_close| |
30
+ # | | (remote) | | | (local) | |
31
+ # | +-----------+ | +-----------+ |
32
+ # | | v | |
33
+ # | | ES/R +--------+ ES/R | |
34
+ # | `----------->| |<-----------' |
35
+ # | R | :close | R |
36
+ # `-------------------->| |<--------------------'
37
+ # +--------+
38
+ class Stream
39
+ include FlowBuffer
40
+ include Emitter
41
+ include Error
42
+
43
+ # Stream ID (odd for client initiated streams, even otherwise).
44
+ attr_reader :id
45
+
46
+ # Stream state as defined by HTTP 2.0.
47
+ attr_reader :state
48
+
49
+ # Request parent stream of push stream.
50
+ attr_reader :parent
51
+
52
+ # Stream priority as set by initiator.
53
+ attr_reader :weight
54
+ attr_reader :dependency
55
+
56
+ # Size of current stream flow control window.
57
+ attr_reader :local_window
58
+ attr_reader :remote_window
59
+ alias window local_window
60
+
61
+ # Reason why connection was closed.
62
+ attr_reader :closed
63
+
64
+ # Initializes new stream.
65
+ #
66
+ # Note that you should never have to call this directly. To create a new
67
+ # client initiated stream, use Connection#new_stream. Similarly, Connection
68
+ # will emit new stream objects, when new stream frames are received.
69
+ #
70
+ # @param id [Integer]
71
+ # @param weight [Integer]
72
+ # @param dependency [Integer]
73
+ # @param exclusive [Boolean]
74
+ # @param window [Integer]
75
+ # @param parent [Stream]
76
+ # @param state [Symbol]
77
+ def initialize(connection:, id:, weight: 16, dependency: 0, exclusive: false, parent: nil, state: :idle)
78
+ stream_error(:protocol_error, "stream can't depend on itself") if id == dependency
79
+
80
+ @connection = connection
81
+ @id = id
82
+ @weight = weight
83
+ @dependency = dependency
84
+ process_priority(weight: weight, dependency: dependency, exclusive: exclusive)
85
+ @local_window_max_size = connection.local_settings[:settings_initial_window_size]
86
+ @local_window = connection.local_settings[:settings_initial_window_size]
87
+ @remote_window = connection.remote_settings[:settings_initial_window_size]
88
+ @parent = parent
89
+ @state = state
90
+ @error = false
91
+ @closed = false
92
+ @send_buffer = []
93
+ @_method = @_content_length = nil
94
+ @_waiting_on_trailers = false
95
+ @received_data = false
96
+
97
+ on(:window) { |v| @remote_window = v }
98
+ on(:local_window) { |v| @local_window_max_size = @local_window = v }
99
+ end
100
+
101
+ def closed?
102
+ @state == :closed
103
+ end
104
+
105
+ # Processes incoming HTTP 2.0 frames. The frames must be decoded upstream.
106
+ #
107
+ # @param frame [Hash]
108
+ def receive(frame)
109
+ transition(frame, false)
110
+
111
+ case frame[:type]
112
+ when :data
113
+ # 6.1. DATA
114
+ # If a DATA frame is received whose stream is not in "open" or
115
+ # "half closed (local)" state, the recipient MUST respond with a
116
+ # stream error (Section 5.4.2) of type STREAM_CLOSED.
117
+ stream_error(:stream_closed) unless @state == :open ||
118
+ @state == :half_closed_local ||
119
+ @state == :half_closing || @state == :closing
120
+ @received_data = true
121
+ calculate_content_length(frame[:length])
122
+ update_local_window(frame)
123
+ # Emit DATA frame
124
+ emit(:data, frame[:payload]) unless frame[:ignore]
125
+ calculate_window_update(@local_window_max_size)
126
+ when :headers
127
+ stream_error(:stream_closed) if @state == :closed || @state == :remote_closed
128
+ @_method ||= frame[:method]
129
+ @_content_length ||= frame[:content_length]
130
+ @_trailers ||= frame[:trailer]
131
+ if @_waiting_on_trailers
132
+ verify_trailers(frame)
133
+ else
134
+ verify_pseudo_headers(frame)
135
+ end
136
+ emit(:headers, frame[:payload]) unless frame[:ignore]
137
+ @_waiting_on_trailers = !@_trailers.nil?
138
+ when :push_promise
139
+ emit(:promise_headers, frame[:payload]) unless frame[:ignore]
140
+ when :continuation
141
+ stream_error(:stream_closed) if @state == :closed || @state == :remote_closed
142
+ stream_error(:protocol_error) if @received_data
143
+ when :priority
144
+ process_priority(frame)
145
+ when :window_update
146
+ process_window_update(frame: frame)
147
+ when :altsvc
148
+ # 4. The ALTSVC HTTP/2 Frame
149
+ # An ALTSVC frame on a
150
+ # stream other than stream 0 containing non-empty "Origin" information
151
+ # is invalid and MUST be ignored.
152
+ emit(frame[:type], frame) if !frame[:origin] || frame[:origin].empty?
153
+ when :blocked
154
+ emit(frame[:type], frame)
155
+ end
156
+
157
+ complete_transition(frame)
158
+ end
159
+ alias << receive
160
+
161
+ REQUEST_MANDATORY_HEADERS = %w[:scheme :method :authority :path].freeze
162
+ RESPONSE_MANDATORY_HEADERS = %w[:status].freeze
163
+
164
+ def verify_pseudo_headers(frame)
165
+ headers = frame[:payload]
166
+ return if headers.is_a?(Buffer)
167
+
168
+ mandatory_headers = @id.odd? ? REQUEST_MANDATORY_HEADERS : RESPONSE_MANDATORY_HEADERS
169
+ pseudo_headers = headers.take_while do |field, value|
170
+ # use this loop to validate pseudo-headers
171
+ stream_error(:protocol_error, msg: "path is empty") if field == ":path" && value.empty?
172
+ field.start_with?(":")
173
+ end.map(&:first)
174
+ return if mandatory_headers.size == pseudo_headers.size &&
175
+ (mandatory_headers - pseudo_headers).empty?
176
+
177
+ stream_error(:protocol_error, msg: "invalid pseudo-headers")
178
+ end
179
+
180
+ def verify_trailers(frame)
181
+ stream_error(:protocol_error, msg: "trailer headers frame must close the stream") unless end_stream?(frame)
182
+ return unless @_trailers
183
+
184
+ trailers = frame[:payload]
185
+ return if trailers.is_a?(Buffer)
186
+
187
+ trailers.each do |field, _|
188
+ @_trailers.delete(field)
189
+ break if @_trailers.empty?
190
+ end
191
+ stream_error(:protocol_error, msg: "didn't receive all expected trailer headers") unless @_trailers.empty?
192
+ end
193
+
194
+ def calculate_content_length(data_length)
195
+ return unless @_content_length
196
+
197
+ @_content_length -= data_length
198
+ return if @_content_length >= 0
199
+
200
+ stream_error(:protocol_error, msg: "received more data than what was defined in content-length")
201
+ end
202
+
203
+ # Processes outgoing HTTP 2.0 frames. Data frames may be automatically
204
+ # split and buffered based on maximum frame size and current stream flow
205
+ # control window size.
206
+ #
207
+ # @param frame [Hash]
208
+ def send(frame)
209
+ process_priority(frame) if frame[:type] == :priority
210
+
211
+ case frame[:type]
212
+ when :data
213
+ # @remote_window is maintained in send_data
214
+ send_data(frame)
215
+ when :window_update
216
+ manage_state(frame) do
217
+ @local_window += frame[:increment]
218
+ emit(:frame, frame)
219
+ end
220
+ else
221
+ manage_state(frame) do
222
+ emit(:frame, frame)
223
+ end
224
+ end
225
+ end
226
+
227
+ # Sends a HEADERS frame containing HTTP response headers.
228
+ # All pseudo-header fields MUST appear in the header block before regular header fields.
229
+ #
230
+ # @param headers [Array or Hash] Array of key-value pairs or Hash
231
+ # @param end_headers [Boolean] indicates that no more headers will be sent
232
+ # @param end_stream [Boolean] indicates that no payload will be sent
233
+ def headers(headers, end_headers: true, end_stream: false)
234
+ flags = []
235
+ flags << :end_headers if end_headers
236
+ flags << :end_stream if end_stream || @_method == "HEAD"
237
+
238
+ send(type: :headers, flags: flags, payload: headers)
239
+ end
240
+
241
+ def promise(headers, end_headers: true, &block)
242
+ raise ArgumentError, "must provide callback" unless block_given?
243
+
244
+ flags = end_headers ? [:end_headers] : []
245
+ emit(:promise, self, headers, flags, &block)
246
+ end
247
+
248
+ # Sends a PRIORITY frame with new stream priority value (can only be
249
+ # performed by the client).
250
+ #
251
+ # @param weight [Integer] new stream weight value
252
+ # @param dependency [Integer] new stream dependency stream
253
+ def reprioritize(weight: 16, dependency: 0, exclusive: false)
254
+ stream_error if @id.even?
255
+ send(type: :priority, weight: weight, dependency: dependency, exclusive: exclusive)
256
+ end
257
+
258
+ # Sends DATA frame containing response payload.
259
+ #
260
+ # @param payload [String]
261
+ # @param end_stream [Boolean] indicates last response DATA frame
262
+ def data(payload, end_stream: true)
263
+ # Split data according to each frame is smaller enough
264
+ # TODO: consider padding?
265
+ max_size = @connection.remote_settings[:settings_max_frame_size]
266
+
267
+ if payload.bytesize > max_size
268
+ payload = chunk_data(payload, max_size) do |chunk|
269
+ send(type: :data, flags: [], payload: chunk)
270
+ end
271
+ end
272
+
273
+ flags = []
274
+ flags << :end_stream if end_stream
275
+ send(type: :data, flags: flags, payload: payload)
276
+ end
277
+
278
+ # Chunk data into max_size, yield each chunk, then return final chunk
279
+ #
280
+ def chunk_data(payload, max_size)
281
+ total = payload.bytesize
282
+ cursor = 0
283
+ while (total - cursor) > max_size
284
+ yield payload.byteslice(cursor, max_size)
285
+ cursor += max_size
286
+ end
287
+ payload.byteslice(cursor, total - cursor)
288
+ end
289
+
290
+ # Sends a RST_STREAM frame which closes current stream - this does not
291
+ # close the underlying connection.
292
+ #
293
+ # @param error [:Symbol] optional reason why stream was closed
294
+ def close(error = :stream_closed)
295
+ send(type: :rst_stream, error: error)
296
+ end
297
+
298
+ # Sends a RST_STREAM indicating that the stream is no longer needed.
299
+ def cancel
300
+ send(type: :rst_stream, error: :cancel)
301
+ end
302
+
303
+ # Sends a RST_STREAM indicating that the stream has been refused prior
304
+ # to performing any application processing.
305
+ def refuse
306
+ send(type: :rst_stream, error: :refused_stream)
307
+ end
308
+
309
+ # Sends a WINDOW_UPDATE frame to the peer.
310
+ #
311
+ # @param increment [Integer]
312
+ def window_update(increment)
313
+ # emit stream-level WINDOW_UPDATE unless stream is closed
314
+ return if @state == :closed || @state == :remote_closed
315
+
316
+ send(type: :window_update, increment: increment)
317
+ end
318
+
319
+ private
320
+
321
+ # HTTP 2.0 Stream States
322
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.1
323
+ #
324
+ # +--------+
325
+ # send PP | | recv PP
326
+ # ,--------| idle |--------.
327
+ # / | | \
328
+ # v +--------+ v
329
+ # +----------+ | +----------+
330
+ # | | | send H/ | |
331
+ # ,-----| reserved | | recv H | reserved |-----.
332
+ # | | (local) | | | (remote) | |
333
+ # | +----------+ v +----------+ |
334
+ # | | +--------+ | |
335
+ # | | recv ES | | send ES | |
336
+ # | send H | ,-------| open |-------. | recv H |
337
+ # | | / | | \ | |
338
+ # | v v +--------+ v v |
339
+ # | +----------+ | +----------+ |
340
+ # | | half | | | half | |
341
+ # | | closed | | send R/ | closed | |
342
+ # | | (remote) | | recv R | (local) | |
343
+ # | +----------+ | +----------+ |
344
+ # | | | | |
345
+ # | | send ES/ | recv ES/ | |
346
+ # | | send R/ v send R/ | |
347
+ # | | recv R +--------+ recv R | |
348
+ # | send R/ `----------->| |<-----------' send R/ |
349
+ # | recv R | closed | recv R |
350
+ # `---------------------->| |<----------------------'
351
+ # +--------+
352
+ #
353
+ def transition(frame, sending)
354
+ case @state
355
+
356
+ # All streams start in the "idle" state. In this state, no frames
357
+ # have been exchanged.
358
+ # The following transitions are valid from this state:
359
+ # * Sending or receiving a HEADERS frame causes the stream to
360
+ # become "open". The stream identifier is selected as described
361
+ # in Section 5.1.1. The same HEADERS frame can also cause a
362
+ # stream to immediately become "half closed".
363
+ # * Sending a PUSH_PROMISE frame reserves an idle stream for later
364
+ # use. The stream state for the reserved stream transitions to
365
+ # "reserved (local)".
366
+ # * Receiving a PUSH_PROMISE frame reserves an idle stream for
367
+ # later use. The stream state for the reserved stream
368
+ # transitions to "reserved (remote)".
369
+ # Receiving any frames other than HEADERS, PUSH_PROMISE or PRIORITY
370
+ # on a stream in this state MUST be treated as a connection error
371
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
372
+
373
+ when :idle
374
+ if sending
375
+ case frame[:type]
376
+ when :push_promise then event(:reserved_local)
377
+ when :headers
378
+ if end_stream?(frame)
379
+ event(:half_closed_local)
380
+ else
381
+ event(:open)
382
+ end
383
+ when :rst_stream then event(:local_rst)
384
+ when :priority then process_priority(frame)
385
+ else stream_error
386
+ end
387
+ else
388
+ case frame[:type]
389
+ when :push_promise then event(:reserved_remote)
390
+ when :headers
391
+ if end_stream?(frame)
392
+ event(:half_closed_remote)
393
+ else
394
+ event(:open)
395
+ end
396
+ when :priority then process_priority(frame)
397
+ else stream_error(:protocol_error)
398
+ end
399
+ end
400
+
401
+ # A stream in the "reserved (local)" state is one that has been
402
+ # promised by sending a PUSH_PROMISE frame. A PUSH_PROMISE frame
403
+ # reserves an idle stream by associating the stream with an open
404
+ # stream that was initiated by the remote peer (see Section 8.2).
405
+ # In this state, only the following transitions are possible:
406
+ # * The endpoint can send a HEADERS frame. This causes the stream
407
+ # to open in a "half closed (remote)" state.
408
+ # * Either endpoint can send a RST_STREAM frame to cause the stream
409
+ # to become "closed". This releases the stream reservation.
410
+ # An endpoint MUST NOT send any type of frame other than HEADERS,
411
+ # RST_STREAM, or PRIORITY in this state.
412
+ # A PRIORITY or WINDOW_UPDATE frame MAY be received in this state.
413
+ # Receiving any type of frame other than RST_STREAM, PRIORITY or
414
+ # WINDOW_UPDATE on a stream in this state MUST be treated as a
415
+ # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
416
+ when :reserved_local
417
+ @state = if sending
418
+ case frame[:type]
419
+ when :headers then event(:half_closed_remote)
420
+ when :rst_stream then event(:local_rst)
421
+ else stream_error
422
+ end
423
+ else
424
+ case frame[:type]
425
+ when :rst_stream then event(:remote_rst)
426
+ when :priority, :window_update then @state
427
+ else stream_error
428
+ end
429
+ end
430
+
431
+ # A stream in the "reserved (remote)" state has been reserved by a
432
+ # remote peer.
433
+ # In this state, only the following transitions are possible:
434
+ # * Receiving a HEADERS frame causes the stream to transition to
435
+ # "half closed (local)".
436
+ # * Either endpoint can send a RST_STREAM frame to cause the stream
437
+ # to become "closed". This releases the stream reservation.
438
+ # An endpoint MAY send a PRIORITY frame in this state to
439
+ # reprioritize the reserved stream. An endpoint MUST NOT send any
440
+ # type of frame other than RST_STREAM, WINDOW_UPDATE, or PRIORITY in
441
+ # this state.
442
+ # Receiving any type of frame other than HEADERS, RST_STREAM or
443
+ # PRIORITY on a stream in this state MUST be treated as a connection
444
+ # error (Section 5.4.1) of type PROTOCOL_ERROR.
445
+ when :reserved_remote
446
+ @state = if sending
447
+ case frame[:type]
448
+ when :rst_stream then event(:local_rst)
449
+ when :priority, :window_update then @state
450
+ else stream_error
451
+ end
452
+ else
453
+ case frame[:type]
454
+ when :headers then event(:half_closed_local)
455
+ when :rst_stream then event(:remote_rst)
456
+ else stream_error
457
+ end
458
+ end
459
+
460
+ # A stream in the "open" state may be used by both peers to send
461
+ # frames of any type. In this state, sending peers observe
462
+ # advertised stream level flow control limits (Section 5.2).
463
+ # From this state either endpoint can send a frame with an
464
+ # END_STREAM flag set, which causes the stream to transition into
465
+ # one of the "half closed" states: an endpoint sending an END_STREAM
466
+ # flag causes the stream state to become "half closed (local)"; an
467
+ # endpoint receiving an END_STREAM flag causes the stream state to
468
+ # become "half closed (remote)".
469
+ # Either endpoint can send a RST_STREAM frame from this state,
470
+ # causing it to transition immediately to "closed".
471
+ when :open
472
+ if sending
473
+ case frame[:type]
474
+ when :data, :headers, :continuation
475
+ event(:half_closed_local) if end_stream?(frame)
476
+ when :rst_stream then event(:local_rst)
477
+ end
478
+ else
479
+ case frame[:type]
480
+ when :data, :headers, :continuation
481
+ event(:half_closed_remote) if end_stream?(frame)
482
+ when :rst_stream then event(:remote_rst)
483
+ end
484
+ end
485
+
486
+ # A stream that is in the "half closed (local)" state cannot be used
487
+ # for sending frames. Only WINDOW_UPDATE, PRIORITY and RST_STREAM
488
+ # frames can be sent in this state.
489
+ # A stream transitions from this state to "closed" when a frame that
490
+ # contains an END_STREAM flag is received, or when either peer sends
491
+ # a RST_STREAM frame.
492
+ # A receiver can ignore WINDOW_UPDATE frames in this state, which
493
+ # might arrive for a short period after a frame bearing the
494
+ # END_STREAM flag is sent.
495
+ # PRIORITY frames received in this state are used to reprioritize
496
+ # streams that depend on the current stream.
497
+ when :half_closed_local
498
+ if sending
499
+ case frame[:type]
500
+ when :rst_stream
501
+ event(:local_rst)
502
+ when :priority
503
+ process_priority(frame)
504
+ when :window_update
505
+ # nop here
506
+ else
507
+ stream_error
508
+ end
509
+ else
510
+ case frame[:type]
511
+ when :data, :headers, :continuation
512
+ event(:remote_closed) if end_stream?(frame)
513
+ when :rst_stream then event(:remote_rst)
514
+ when :priority
515
+ process_priority(frame)
516
+ when :window_update
517
+ # nop here
518
+ end
519
+ end
520
+
521
+ # A stream that is "half closed (remote)" is no longer being used by
522
+ # the peer to send frames. In this state, an endpoint is no longer
523
+ # obligated to maintain a receiver flow control window if it
524
+ # performs flow control.
525
+ # If an endpoint receives additional frames for a stream that is in
526
+ # this state, other than WINDOW_UPDATE, PRIORITY or RST_STREAM, it
527
+ # MUST respond with a stream error (Section 5.4.2) of type
528
+ # STREAM_CLOSED.
529
+ # A stream that is "half closed (remote)" can be used by the
530
+ # endpoint to send frames of any type. In this state, the endpoint
531
+ # continues to observe advertised stream level flow control limits
532
+ # (Section 5.2).
533
+ # A stream can transition from this state to "closed" by sending a
534
+ # frame that contains an END_STREAM flag, or when either peer sends
535
+ # a RST_STREAM frame.
536
+ when :half_closed_remote
537
+ if sending
538
+ case frame[:type]
539
+ when :data, :headers, :continuation
540
+ event(:local_closed) if end_stream?(frame)
541
+ when :rst_stream then event(:local_rst)
542
+ end
543
+ else
544
+ case frame[:type]
545
+ when :rst_stream then event(:remote_rst)
546
+ when :priority
547
+ process_priority(frame)
548
+ when :window_update
549
+ # nop
550
+ else
551
+ stream_error(:stream_closed)
552
+ end
553
+ end
554
+
555
+ # The "closed" state is the terminal state.
556
+ # An endpoint MUST NOT send frames other than PRIORITY on a closed
557
+ # stream. An endpoint that receives any frame other than PRIORITY
558
+ # after receiving a RST_STREAM MUST treat that as a stream error
559
+ # (Section 5.4.2) of type STREAM_CLOSED. Similarly, an endpoint
560
+ # that receives any frames after receiving a frame with the
561
+ # END_STREAM flag set MUST treat that as a connection error
562
+ # (Section 5.4.1) of type STREAM_CLOSED, unless the frame is
563
+ # permitted as described below.
564
+ # WINDOW_UPDATE or RST_STREAM frames can be received in this state
565
+ # for a short period after a DATA or HEADERS frame containing an
566
+ # END_STREAM flag is sent. Until the remote peer receives and
567
+ # processes RST_STREAM or the frame bearing the END_STREAM flag, it
568
+ # might send frames of these types. Endpoints MUST ignore
569
+ # WINDOW_UPDATE or RST_STREAM frames received in this state, though
570
+ # endpoints MAY choose to treat frames that arrive a significant
571
+ # time after sending END_STREAM as a connection error
572
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
573
+ # PRIORITY frames can be sent on closed streams to prioritize
574
+ # streams that are dependent on the closed stream. Endpoints SHOULD
575
+ # process PRIORITY frames, though they can be ignored if the stream
576
+ # has been removed from the dependency tree (see Section 5.3.4).
577
+ # If this state is reached as a result of sending a RST_STREAM
578
+ # frame, the peer that receives the RST_STREAM might have already
579
+ # sent - or enqueued for sending - frames on the stream that cannot
580
+ # be withdrawn. An endpoint MUST ignore frames that it receives on
581
+ # closed streams after it has sent a RST_STREAM frame. An endpoint
582
+ # MAY choose to limit the period over which it ignores frames and
583
+ # treat frames that arrive after this time as being in error.
584
+ # Flow controlled frames (i.e., DATA) received after sending
585
+ # RST_STREAM are counted toward the connection flow control window.
586
+ # Even though these frames might be ignored, because they are sent
587
+ # before the sender receives the RST_STREAM, the sender will
588
+ # consider the frames to count against the flow control window.
589
+ # An endpoint might receive a PUSH_PROMISE frame after it sends
590
+ # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved"
591
+ # even if the associated stream has been reset. Therefore, a
592
+ # RST_STREAM is needed to close an unwanted promised stream.
593
+ when :closed
594
+ if sending
595
+ case frame[:type]
596
+ when :rst_stream # ignore
597
+ when :priority
598
+ process_priority(frame)
599
+ else
600
+ stream_error(:stream_closed) unless frame[:type] == :rst_stream
601
+ end
602
+ elsif frame[:type] == :priority
603
+ process_priority(frame)
604
+ else
605
+ case @closed
606
+ when :remote_rst, :remote_closed
607
+ case frame[:type]
608
+ when :rst_stream, :window_update # nop here
609
+ else
610
+ stream_error(:stream_closed)
611
+ end
612
+ when :local_rst, :local_closed
613
+ frame[:ignore] = true if frame[:type] != :window_update
614
+ end
615
+ end
616
+ end
617
+ end
618
+
619
+ def event(newstate)
620
+ case newstate
621
+ when :open
622
+ @state = newstate
623
+ emit(:active)
624
+
625
+ when :reserved_local, :reserved_remote
626
+ @state = newstate
627
+ emit(:reserved)
628
+
629
+ when :half_closed_local, :half_closed_remote
630
+ @closed = newstate
631
+ emit(:active) unless @state == :open
632
+ @state = :half_closing
633
+
634
+ when :local_closed, :remote_closed, :local_rst, :remote_rst
635
+ @closed = newstate
636
+ @state = :closing
637
+ end
638
+
639
+ @state
640
+ end
641
+
642
+ def complete_transition(frame)
643
+ case @state
644
+ when :closing
645
+ @state = :closed
646
+ emit(:close, frame[:error])
647
+ when :half_closing
648
+ @state = @closed
649
+ emit(:half_close)
650
+ end
651
+ end
652
+
653
+ def process_priority(frame)
654
+ @weight = frame[:weight]
655
+ @dependency = frame[:dependency]
656
+ emit(
657
+ :priority,
658
+ weight: frame[:weight],
659
+ dependency: frame[:dependency],
660
+ exclusive: frame[:exclusive]
661
+ )
662
+ # TODO: implement dependency tree housekeeping
663
+ # Latest draft defines a fairly complex priority control.
664
+ # See https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.3
665
+ # We currently have no prioritization among streams.
666
+ # We should add code here.
667
+ end
668
+
669
+ def end_stream?(frame)
670
+ case frame[:type]
671
+ when :data, :headers, :continuation
672
+ frame[:flags].include?(:end_stream)
673
+ else false
674
+ end
675
+ end
676
+
677
+ def stream_error(error = :internal_error, msg: nil)
678
+ @error = error
679
+ close(error) if @state != :closed
680
+
681
+ raise Error.types[error], msg
682
+ end
683
+ alias error stream_error
684
+
685
+ def manage_state(frame)
686
+ transition(frame, true)
687
+ frame[:stream] ||= @id
688
+ yield
689
+ complete_transition(frame)
690
+ end
691
+ end
692
+ end