http-2 0.11.0 → 1.0.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -9
  3. data/lib/http/2/base64.rb +45 -0
  4. data/lib/http/2/client.rb +19 -6
  5. data/lib/http/2/connection.rb +235 -163
  6. data/lib/http/2/emitter.rb +7 -5
  7. data/lib/http/2/error.rb +24 -1
  8. data/lib/http/2/extensions.rb +53 -0
  9. data/lib/http/2/flow_buffer.rb +91 -33
  10. data/lib/http/2/framer.rb +184 -157
  11. data/lib/http/2/header/compressor.rb +157 -0
  12. data/lib/http/2/header/decompressor.rb +144 -0
  13. data/lib/http/2/header/encoding_context.rb +337 -0
  14. data/lib/http/2/{huffman.rb → header/huffman.rb} +25 -19
  15. data/lib/http/2/{huffman_statemachine.rb → header/huffman_statemachine.rb} +2 -0
  16. data/lib/http/2/header.rb +35 -0
  17. data/lib/http/2/server.rb +47 -20
  18. data/lib/http/2/stream.rb +130 -61
  19. data/lib/http/2/version.rb +3 -1
  20. data/lib/http/2.rb +14 -13
  21. data/sig/client.rbs +9 -0
  22. data/sig/connection.rbs +93 -0
  23. data/sig/emitter.rbs +13 -0
  24. data/sig/error.rbs +35 -0
  25. data/sig/extensions.rbs +5 -0
  26. data/sig/flow_buffer.rbs +21 -0
  27. data/sig/frame_buffer.rbs +13 -0
  28. data/sig/framer.rbs +54 -0
  29. data/sig/header/compressor.rbs +27 -0
  30. data/sig/header/decompressor.rbs +22 -0
  31. data/sig/header/encoding_context.rbs +34 -0
  32. data/sig/header/huffman.rbs +9 -0
  33. data/sig/header.rbs +27 -0
  34. data/sig/next.rbs +101 -0
  35. data/sig/server.rbs +12 -0
  36. data/sig/stream.rbs +91 -0
  37. metadata +38 -79
  38. data/.autotest +0 -20
  39. data/.coveralls.yml +0 -1
  40. data/.gitignore +0 -20
  41. data/.gitmodules +0 -3
  42. data/.rspec +0 -5
  43. data/.rubocop.yml +0 -93
  44. data/.rubocop_todo.yml +0 -131
  45. data/.travis.yml +0 -17
  46. data/Gemfile +0 -16
  47. data/Guardfile +0 -18
  48. data/Guardfile.h2spec +0 -12
  49. data/LICENSE +0 -21
  50. data/Rakefile +0 -49
  51. data/example/Gemfile +0 -3
  52. data/example/README.md +0 -44
  53. data/example/client.rb +0 -122
  54. data/example/helper.rb +0 -19
  55. data/example/keys/server.crt +0 -20
  56. data/example/keys/server.key +0 -27
  57. data/example/server.rb +0 -139
  58. data/example/upgrade_client.rb +0 -153
  59. data/example/upgrade_server.rb +0 -203
  60. data/http-2.gemspec +0 -22
  61. data/lib/http/2/buffer.rb +0 -76
  62. data/lib/http/2/compressor.rb +0 -572
  63. data/lib/tasks/generate_huffman_table.rb +0 -166
  64. data/spec/buffer_spec.rb +0 -28
  65. data/spec/client_spec.rb +0 -188
  66. data/spec/compressor_spec.rb +0 -666
  67. data/spec/connection_spec.rb +0 -681
  68. data/spec/emitter_spec.rb +0 -54
  69. data/spec/framer_spec.rb +0 -487
  70. data/spec/h2spec/h2spec.darwin +0 -0
  71. data/spec/h2spec/output/non_secure.txt +0 -317
  72. data/spec/helper.rb +0 -147
  73. data/spec/hpack_test_spec.rb +0 -84
  74. data/spec/huffman_spec.rb +0 -68
  75. data/spec/server_spec.rb +0 -52
  76. data/spec/stream_spec.rb +0 -878
  77. data/spec/support/deep_dup.rb +0 -55
  78. data/spec/support/duplicable.rb +0 -98
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTP2
2
4
  # Default connection and stream flow control window (64KB).
3
5
  DEFAULT_FLOW_WINDOW = 65_535
@@ -10,31 +12,33 @@ module HTTP2
10
12
 
11
13
  # Default values for SETTINGS frame, as defined by the spec.
12
14
  SPEC_DEFAULT_CONNECTION_SETTINGS = {
13
- settings_header_table_size: 4096,
14
- settings_enable_push: 1, # enabled for servers
15
- settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
16
- settings_initial_window_size: 65_535,
17
- settings_max_frame_size: 16_384,
18
- settings_max_header_list_size: 2**31 - 1, # unlimited
15
+ settings_header_table_size: 4096,
16
+ settings_enable_push: 1, # enabled for servers
17
+ settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
18
+ settings_initial_window_size: 65_535,
19
+ settings_max_frame_size: 16_384,
20
+ settings_max_header_list_size: (2 << 30) - 1 # unlimited
19
21
  }.freeze
20
22
 
21
23
  DEFAULT_CONNECTION_SETTINGS = {
22
- settings_header_table_size: 4096,
23
- settings_enable_push: 1, # enabled for servers
24
- settings_max_concurrent_streams: 100,
25
- settings_initial_window_size: 65_535,
26
- settings_max_frame_size: 16_384,
27
- settings_max_header_list_size: 2**31 - 1, # unlimited
24
+ settings_header_table_size: 4096,
25
+ settings_enable_push: 1, # enabled for servers
26
+ settings_max_concurrent_streams: 100,
27
+ settings_initial_window_size: 65_535,
28
+ settings_max_frame_size: 16_384,
29
+ settings_max_header_list_size: (2 << 30) - 1 # unlimited
28
30
  }.freeze
29
31
 
30
32
  # Default stream priority (lower values are higher priority).
31
- DEFAULT_WEIGHT = 16
33
+ DEFAULT_WEIGHT = 16
32
34
 
33
35
  # Default connection "fast-fail" preamble string as defined by the spec.
34
- CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
36
+ CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
+
38
+ REQUEST_MANDATORY_HEADERS = %w[:scheme :method :authority :path].freeze
39
+ RESPONSE_MANDATORY_HEADERS = %w[:status].freeze
35
40
 
36
- # Time to hold recently closed streams until purge (seconds)
37
- RECENTLY_CLOSED_STREAMS_TTL = 15
41
+ EMPTY = [].freeze
38
42
 
39
43
  # Connection encapsulates all of the connection, stream, flow-control,
40
44
  # error management, and other processing logic required for a well-behaved
@@ -43,24 +47,25 @@ module HTTP2
43
47
  # Note that this class should not be used directly. Instead, you want to
44
48
  # use either Client or Server class to drive the HTTP 2.0 exchange.
45
49
  #
46
- # rubocop:disable ClassLength
50
+ # rubocop:disable Metrics/ClassLength
47
51
  class Connection
48
52
  include FlowBuffer
49
53
  include Emitter
50
54
  include Error
51
55
 
56
+ using StringExtensions
57
+
52
58
  # Connection state (:new, :closed).
53
59
  attr_reader :state
54
60
 
55
61
  # Size of current connection flow control window (by default, set to
56
62
  # infinity, but is automatically updated on receipt of peer settings).
57
63
  attr_reader :local_window
58
- attr_reader :remote_window
64
+ attr_reader :remote_window, :remote_settings
59
65
  alias window local_window
60
66
 
61
67
  # Current settings value for local and peer
62
68
  attr_reader :local_settings
63
- attr_reader :remote_settings
64
69
 
65
70
  # Pending settings value
66
71
  # Sent but not ack'ed settings
@@ -68,36 +73,38 @@ module HTTP2
68
73
 
69
74
  # Number of active streams between client and server (reserved streams
70
75
  # are not counted towards the stream limit).
71
- attr_reader :active_stream_count
76
+ attr_accessor :active_stream_count
72
77
 
73
78
  # Initializes new connection object.
74
79
  #
75
- def initialize(**settings)
80
+ def initialize(settings = {})
76
81
  @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
77
82
  @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
78
83
 
79
- @compressor = Header::Compressor.new(**settings)
80
- @decompressor = Header::Decompressor.new(**settings)
84
+ @compressor = Header::Compressor.new(settings)
85
+ @decompressor = Header::Decompressor.new(settings)
81
86
 
82
87
  @active_stream_count = 0
88
+ @last_activated_stream = 0
89
+ @last_stream_id = 0
83
90
  @streams = {}
84
91
  @streams_recently_closed = {}
85
92
  @pending_settings = []
86
93
 
87
- @framer = Framer.new
94
+ @framer = Framer.new(@local_settings[:settings_max_frame_size])
88
95
 
89
96
  @local_window_limit = @local_settings[:settings_initial_window_size]
90
97
  @local_window = @local_window_limit
91
98
  @remote_window_limit = @remote_settings[:settings_initial_window_size]
92
99
  @remote_window = @remote_window_limit
93
100
 
94
- @recv_buffer = Buffer.new
95
- @send_buffer = []
101
+ @recv_buffer = "".b
96
102
  @continuation = []
97
103
  @error = nil
98
104
 
99
105
  @h2c_upgrade = nil
100
106
  @closed_since = nil
107
+ @received_frame = false
101
108
  end
102
109
 
103
110
  def closed?
@@ -110,10 +117,14 @@ module HTTP2
110
117
  # @param window [Integer]
111
118
  # @param parent [Stream]
112
119
  def new_stream(**args)
113
- fail ConnectionClosed if @state == :closed
114
- fail StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
120
+ raise ConnectionClosed if @state == :closed
121
+ raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
122
+
123
+ connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_activated_stream
115
124
 
116
125
  stream = activate_stream(id: @stream_id, **args)
126
+ @last_activated_stream = stream.id
127
+
117
128
  @stream_id += 2
118
129
 
119
130
  stream
@@ -140,15 +151,15 @@ module HTTP2
140
151
  # @param payload [String]
141
152
  def goaway(error = :no_error, payload = nil)
142
153
  last_stream = if (max = @streams.max)
143
- max.first
144
- else
145
- 0
146
- end
154
+ max.first
155
+ else
156
+ 0
157
+ end
147
158
 
148
159
  send(type: :goaway, last_stream: last_stream,
149
160
  error: error, payload: payload)
150
161
  @state = :closed
151
- @closed_since = Time.now
162
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
163
  end
153
164
 
154
165
  # Sends a WINDOW_UPDATE frame to the peer.
@@ -165,7 +176,7 @@ module HTTP2
165
176
  # @param settings [Array or Hash]
166
177
  def settings(payload)
167
178
  payload = payload.to_a
168
- connection_error if validate_settings(@local_role, payload)
179
+ validate_settings(@local_role, payload)
169
180
  @pending_settings << payload
170
181
  send(type: :settings, stream: 0, payload: payload)
171
182
  @pending_settings << payload
@@ -188,33 +199,56 @@ module HTTP2
188
199
  # SETTINGS frame. Server connection header is SETTINGS frame only.
189
200
  if @state == :waiting_magic
190
201
  if @recv_buffer.size < 24
191
- if !CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
192
- fail HandshakeError
193
- else
194
- return # maybe next time
195
- end
202
+ raise HandshakeError unless CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
203
+
204
+ return # maybe next time
196
205
  elsif @recv_buffer.read(24) == CONNECTION_PREFACE_MAGIC
197
206
  # MAGIC is OK. Send our settings
198
207
  @state = :waiting_connection_preface
199
208
  payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
200
209
  settings(payload)
201
210
  else
202
- fail HandshakeError
211
+ raise HandshakeError
203
212
  end
204
213
  end
205
214
 
206
215
  while (frame = @framer.parse(@recv_buffer))
216
+ if is_a?(Client) && !@received_frame
217
+ connection_error(:protocol_error, msg: "didn't receive settings") if frame[:type] != :settings
218
+ @received_frame = true
219
+ end
220
+
221
+ # Implementations MUST discard frames
222
+ # that have unknown or unsupported types.
223
+ if frame[:type].nil?
224
+ # However, extension frames that appear in
225
+ # the middle of a header block (Section 4.3) are not permitted; these
226
+ # MUST be treated as a connection error (Section 5.4.1) of type
227
+ # PROTOCOL_ERROR.
228
+ connection_error(:protocol_error) unless @continuation.empty?
229
+ next
230
+ end
231
+
207
232
  emit(:frame_received, frame)
208
233
 
209
234
  # Header blocks MUST be transmitted as a contiguous sequence of frames
210
235
  # with no interleaved frames of any other type, or from any other stream.
211
236
  unless @continuation.empty?
212
- unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
213
- connection_error
214
- end
237
+ connection_error unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
215
238
 
216
239
  @continuation << frame
217
- return unless frame[:flags].include? :end_headers
240
+ unless frame[:flags].include? :end_headers
241
+ buffered_payload = @continuation.sum { |f| f[:payload].bytesize }
242
+ # prevent HTTP/2 CONTINUATION FLOOD
243
+ # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
244
+ # different mitigation (connection closed, instead of 400 response)
245
+ unless buffered_payload < @local_settings[:settings_max_frame_size]
246
+ connection_error(:protocol_error,
247
+ msg: "too many continuations received")
248
+ end
249
+
250
+ next
251
+ end
218
252
 
219
253
  payload = @continuation.map { |f| f[:payload] }.join
220
254
 
@@ -222,7 +256,7 @@ module HTTP2
222
256
  @continuation.clear
223
257
 
224
258
  frame.delete(:length)
225
- frame[:payload] = Buffer.new(payload)
259
+ frame[:payload] = payload
226
260
  frame[:flags] << :end_headers
227
261
  end
228
262
 
@@ -232,19 +266,20 @@ module HTTP2
232
266
  # anything other than 0x0, the endpoint MUST respond with a connection
233
267
  # error (Section 5.4.1) of type PROTOCOL_ERROR.
234
268
  if connection_frame?(frame)
269
+ connection_error(:protocol_error) unless frame[:stream].zero?
235
270
  connection_management(frame)
236
271
  else
237
272
  case frame[:type]
238
273
  when :headers
239
274
  # When server receives even-numbered stream identifier,
240
275
  # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
241
- connection_error if frame[:stream].even? && self.is_a?(Server)
276
+ connection_error if frame[:stream].even? && is_a?(Server)
242
277
 
243
278
  # The last frame in a sequence of HEADERS/CONTINUATION
244
279
  # frames MUST have the END_HEADERS flag set.
245
280
  unless frame[:flags].include? :end_headers
246
281
  @continuation << frame
247
- return
282
+ next
248
283
  end
249
284
 
250
285
  # After sending a GOAWAY frame, the sender can discard frames
@@ -257,12 +292,15 @@ module HTTP2
257
292
 
258
293
  stream = @streams[frame[:stream]]
259
294
  if stream.nil?
295
+ verify_pseudo_headers(frame)
296
+
260
297
  stream = activate_stream(
261
- id: frame[:stream],
262
- weight: frame[:weight] || DEFAULT_WEIGHT,
298
+ id: frame[:stream],
299
+ weight: frame[:weight] || DEFAULT_WEIGHT,
263
300
  dependency: frame[:dependency] || 0,
264
- exclusive: frame[:exclusive] || false,
301
+ exclusive: frame[:exclusive] || false
265
302
  )
303
+ verify_stream_order(stream.id)
266
304
  emit(:stream, stream)
267
305
  end
268
306
 
@@ -298,7 +336,7 @@ module HTTP2
298
336
  return
299
337
  end
300
338
 
301
- connection_error(msg: 'missing parent ID') if parent.nil?
339
+ connection_error(msg: "missing parent ID") if parent.nil?
302
340
 
303
341
  unless parent.state == :open || parent.state == :half_closed_local
304
342
  # An endpoint might receive a PUSH_PROMISE frame after it sends
@@ -315,7 +353,9 @@ module HTTP2
315
353
  end
316
354
  end
317
355
 
356
+ _verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS)
318
357
  stream = activate_stream(id: pid, parent: parent)
358
+ verify_stream_order(stream.id)
319
359
  emit(:promise, stream)
320
360
  stream << frame
321
361
  else
@@ -333,10 +373,10 @@ module HTTP2
333
373
  # unused or closed parent stream.
334
374
  when :priority
335
375
  stream = activate_stream(
336
- id: frame[:stream],
337
- weight: frame[:weight] || DEFAULT_WEIGHT,
376
+ id: frame[:stream],
377
+ weight: frame[:weight] || DEFAULT_WEIGHT,
338
378
  dependency: frame[:dependency] || 0,
339
- exclusive: frame[:exclusive] || false,
379
+ exclusive: frame[:exclusive] || false
340
380
  )
341
381
 
342
382
  emit(:stream, stream)
@@ -348,24 +388,26 @@ module HTTP2
348
388
  # "closed" stream. A receiver MUST NOT treat this as an error
349
389
  # (see Section 5.1).
350
390
  when :window_update
351
- process_window_update(frame)
391
+ stream = @streams_recently_closed[frame[:stream]]
392
+ connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
393
+ process_window_update(frame: frame, encode: true)
352
394
  else
353
395
  # An endpoint that receives an unexpected stream identifier
354
396
  # MUST respond with a connection error of type PROTOCOL_ERROR.
355
- connection_error
397
+ connection_error(msg: "stream does not exist")
356
398
  end
357
399
  end
358
400
  end
359
401
  end
360
402
  end
361
-
362
403
  rescue StandardError => e
363
404
  raise if e.is_a?(Error::Error)
405
+
364
406
  connection_error(e: e)
365
407
  end
366
408
 
367
- def <<(*args)
368
- receive(*args)
409
+ def <<(data)
410
+ receive(data)
369
411
  end
370
412
 
371
413
  private
@@ -381,17 +423,16 @@ module HTTP2
381
423
  if frame[:type] == :data
382
424
  send_data(frame, true)
383
425
 
384
- else
426
+ elsif frame[:type] == :rst_stream && frame[:error] == :protocol_error
385
427
  # An endpoint can end a connection at any time. In particular, an
386
428
  # endpoint MAY choose to treat a stream error as a connection error.
387
- if frame[:type] == :rst_stream && frame[:error] == :protocol_error
388
- goaway(frame[:error])
389
- else
390
- # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
391
- # RST_STREAM that are not protocol errors
392
- frames = encode(frame)
393
- frames.each { |f| emit(:frame, f) }
394
- end
429
+
430
+ goaway(frame[:error])
431
+ else
432
+ # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
433
+ # RST_STREAM that are not protocol errors
434
+ frames = encode(frame)
435
+ frames.each { |f| emit(:frame, f) }
395
436
  end
396
437
  end
397
438
 
@@ -401,10 +442,10 @@ module HTTP2
401
442
  # @return [Array of Buffer] encoded frame
402
443
  def encode(frame)
403
444
  frames = if frame[:type] == :headers || frame[:type] == :push_promise
404
- encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
405
- else
406
- [frame] # otherwise one frame
407
- end
445
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
446
+ else
447
+ [frame] # otherwise one frame
448
+ end
408
449
 
409
450
  frames.map { |f| @framer.generate(f) }
410
451
  end
@@ -440,28 +481,26 @@ module HTTP2
440
481
  when :settings
441
482
  connection_settings(frame)
442
483
  when :window_update
443
- @remote_window += frame[:increment]
444
- send_data(nil, true)
484
+ process_window_update(frame: frame, encode: true)
445
485
  when :ping
446
- if frame[:flags].include? :ack
447
- emit(:ack, frame[:payload])
448
- else
449
- send(type: :ping, stream: 0,
450
- flags: [:ack], payload: frame[:payload])
451
- end
486
+ ping_management(frame)
452
487
  when :goaway
453
488
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
454
489
  # the connection, although a new connection can be established
455
490
  # for new streams.
456
491
  @state = :closed
457
- @closed_since = Time.now
492
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
458
493
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
459
494
  when :altsvc
460
495
  # 4. The ALTSVC HTTP/2 Frame
461
496
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
462
497
  # information is invalid and MUST be ignored.
463
- if frame[:origin] && !frame[:origin].empty?
464
- emit(frame[:type], frame)
498
+ emit(frame[:type], frame) if frame[:origin] && !frame[:origin].empty?
499
+ when :origin
500
+ return if @h2c_upgrade || !frame[:flags].empty?
501
+
502
+ frame[:payload].each do |origin|
503
+ emit(frame[:type], origin)
465
504
  end
466
505
  when :blocked
467
506
  emit(frame[:type], frame)
@@ -469,12 +508,28 @@ module HTTP2
469
508
  connection_error
470
509
  end
471
510
  when :closed
472
- connection_error if (Time.now - @closed_since) > 15
511
+ case frame[:type]
512
+ when :goaway
513
+ connection_error
514
+ when :ping
515
+ ping_management(frame)
516
+ else
517
+ connection_error if (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @closed_since) > 15
518
+ end
473
519
  else
474
520
  connection_error
475
521
  end
476
522
  end
477
523
 
524
+ def ping_management(frame)
525
+ if frame[:flags].include? :ack
526
+ emit(:ack, frame[:payload])
527
+ else
528
+ send(type: :ping, stream: 0,
529
+ flags: [:ack], payload: frame[:payload])
530
+ end
531
+ end
532
+
478
533
  # Validate settings parameters. See sepc Section 6.5.2.
479
534
  #
480
535
  # @param role [Symbol] The sender's role: :client or :server
@@ -482,8 +537,6 @@ module HTTP2
482
537
  def validate_settings(role, settings)
483
538
  settings.each do |key, v|
484
539
  case key
485
- when :settings_header_table_size
486
- # Any value is valid
487
540
  when :settings_enable_push
488
541
  case role
489
542
  when :server
@@ -491,38 +544,41 @@ module HTTP2
491
544
  # Clients MUST reject any attempt to change the
492
545
  # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
493
546
  # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
494
- return ProtocolError.new("invalid #{key} value") unless v.zero?
547
+ next if v.zero?
548
+
549
+ connection_error(:protocol_error, msg: "invalid #{key} value")
495
550
  when :client
496
551
  # Any value other than 0 or 1 MUST be treated as a
497
552
  # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
498
- unless v.zero? || v == 1
499
- return ProtocolError.new("invalid #{key} value")
500
- end
553
+ next if v.zero? || v == 1
554
+
555
+ connection_error(:protocol_error, msg: "invalid #{key} value")
501
556
  end
502
- when :settings_max_concurrent_streams
503
- # Any value is valid
504
557
  when :settings_initial_window_size
505
558
  # Values above the maximum flow control window size of 2^31-1 MUST
506
559
  # be treated as a connection error (Section 5.4.1) of type
507
560
  # FLOW_CONTROL_ERROR.
508
- unless v <= 0x7fffffff
509
- return FlowControlError.new("invalid #{key} value")
510
- end
561
+ next if v <= 0x7fffffff
562
+
563
+ connection_error(:flow_control_error, msg: "invalid #{key} value")
511
564
  when :settings_max_frame_size
512
565
  # The initial value is 2^14 (16,384) octets. The value advertised
513
566
  # by an endpoint MUST be between this initial value and the maximum
514
567
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
515
568
  # Values outside this range MUST be treated as a connection error
516
569
  # (Section 5.4.1) of type PROTOCOL_ERROR.
517
- unless v >= 16_384 && v <= 16_777_215
518
- return ProtocolError.new("invalid #{key} value")
519
- end
520
- when :settings_max_header_list_size
570
+ next if v >= 16_384 && v <= 16_777_215
571
+
572
+ connection_error(:protocol_error, msg: "invalid #{key} value")
573
+ # when :settings_max_concurrent_streams
574
+ # Any value is valid
575
+ # when :settings_header_table_size
576
+ # Any value is valid
577
+ # when :settings_max_header_list_size
521
578
  # Any value is valid
522
579
  # else # ignore unknown settings
523
580
  end
524
581
  end
525
- nil
526
582
  end
527
583
 
528
584
  # Update connection settings based on parameters set by the peer.
@@ -536,12 +592,12 @@ module HTTP2
536
592
  # local: previously sent and pended our settings should be effective
537
593
  # remote: just received peer settings should immediately be effective
538
594
  settings, side = if frame[:flags].include?(:ack)
539
- # Process pending settings we have sent.
540
- [@pending_settings.shift, :local]
541
- else
542
- connection_error if validate_settings(@remote_role, frame[:payload])
543
- [frame[:payload], :remote]
544
- end
595
+ # Process pending settings we have sent.
596
+ [@pending_settings.shift, :local]
597
+ else
598
+ validate_settings(@remote_role, frame[:payload])
599
+ [frame[:payload], :remote]
600
+ end
545
601
 
546
602
  settings.each do |key, v|
547
603
  case side
@@ -565,14 +621,17 @@ module HTTP2
565
621
  case side
566
622
  when :local
567
623
  @local_window = @local_window - @local_window_limit + v
568
- @streams.each do |_id, stream|
624
+ @streams.each_value do |stream|
569
625
  stream.emit(:local_window, stream.local_window - @local_window_limit + v)
570
626
  end
571
627
 
572
628
  @local_window_limit = v
573
629
  when :remote
574
- @remote_window = @remote_window - @remote_window_limit + v
575
- @streams.each do |_id, stream|
630
+ # can adjust the initial window size for new streams by including a
631
+ # value for SETTINGS_INITIAL_WINDOW_SIZE in the SETTINGS frame.
632
+ # The connection flow-control window can only be changed using
633
+ # WINDOW_UPDATE frames.
634
+ @streams.each_value do |stream|
576
635
  # Event name is :window, not :remote_window
577
636
  stream.emit(:window, stream.remote_window - @remote_window_limit + v)
578
637
  end
@@ -593,8 +652,7 @@ module HTTP2
593
652
  # nothing to do
594
653
 
595
654
  when :settings_max_frame_size
596
- # update framer max_frame_size
597
- @framer.max_frame_size = v
655
+ @framer.remote_max_frame_size = v
598
656
 
599
657
  # else # ignore unknown settings
600
658
  end
@@ -608,6 +666,9 @@ module HTTP2
608
666
  unless @state == :closed || @h2c_upgrade == :start
609
667
  # Send ack to peer
610
668
  send(type: :settings, stream: 0, payload: [], flags: [:ack])
669
+ # when initial window size changes, we try to flush any buffered
670
+ # data.
671
+ @streams.each_value(&:flush)
611
672
  end
612
673
  end
613
674
  end
@@ -621,10 +682,7 @@ module HTTP2
621
682
  #
622
683
  # @param frame [Hash]
623
684
  def decode_headers(frame)
624
- if frame[:payload].is_a? Buffer
625
- frame[:payload] = @decompressor.decode(frame[:payload])
626
- end
627
-
685
+ frame[:payload] = @decompressor.decode(frame[:payload], frame) if frame[:payload].is_a?(String)
628
686
  rescue CompressionError => e
629
687
  connection_error(:compression_error, e: e)
630
688
  rescue ProtocolError => e
@@ -639,30 +697,36 @@ module HTTP2
639
697
  # @return [Array of Frame]
640
698
  def encode_headers(frame)
641
699
  payload = frame[:payload]
642
- payload = @compressor.encode(payload) unless payload.is_a? Buffer
700
+ begin
701
+ payload = frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
702
+ rescue StandardError => e
703
+ connection_error(:compression_error, e: e)
704
+ end
705
+
706
+ # if single frame, return immediately
707
+ return [frame] if payload.bytesize <= @remote_settings[:settings_max_frame_size]
643
708
 
644
709
  frames = []
645
710
 
646
- while payload.bytesize > 0
711
+ until payload.nil? || payload.empty?
647
712
  cont = frame.dup
648
- cont[:type] = :continuation
649
- cont[:flags] = []
650
- cont[:payload] = payload.slice!(0, @remote_settings[:settings_max_frame_size])
713
+
714
+ # first frame remains HEADERS
715
+ unless frames.empty?
716
+ cont[:type] = :continuation
717
+ cont[:flags] = EMPTY
718
+ end
719
+
720
+ cont[:payload] = payload.byteslice(0, @remote_settings[:settings_max_frame_size])
721
+ payload = payload.byteslice(@remote_settings[:settings_max_frame_size]..-1)
722
+
651
723
  frames << cont
652
724
  end
653
- if frames.empty?
654
- frames = [frame]
655
- else
656
- frames.first[:type] = frame[:type]
657
- frames.first[:flags] = frame[:flags] - [:end_headers]
658
- frames.last[:flags] << :end_headers
659
- end
660
725
 
661
- frames
726
+ frames.first[:flags].delete(:end_headers)
727
+ frames.last[:flags] = [:end_headers]
662
728
 
663
- rescue StandardError => e
664
- connection_error(:compression_error, e: e)
665
- nil
729
+ frames
666
730
  end
667
731
 
668
732
  # Activates new incoming or outgoing stream and registers appropriate
@@ -672,46 +736,54 @@ module HTTP2
672
736
  # @param priority [Integer]
673
737
  # @param window [Integer]
674
738
  # @param parent [Stream]
675
- def activate_stream(id: nil, **args)
676
- connection_error(msg: 'Stream ID already exists') if @streams.key?(id)
739
+ def activate_stream(id:, **args)
740
+ connection_error(msg: "Stream ID already exists") if @streams.key?(id)
677
741
 
678
- stream = Stream.new(**{ connection: self, id: id }.merge(args))
742
+ raise StreamLimitExceeded if @active_stream_count >= @local_settings[:settings_max_concurrent_streams]
679
743
 
680
- # Streams that are in the "open" state, or either of the "half closed"
681
- # states count toward the maximum number of streams that an endpoint is
682
- # permitted to open.
683
- stream.once(:active) { @active_stream_count += 1 }
684
- stream.once(:close) do
685
- @active_stream_count -= 1
744
+ stream = Stream.new(connection: self, id: id, **args)
686
745
 
746
+ stream.once(:close) do
687
747
  # Store a reference to the closed stream, such that we can respond
688
748
  # to any in-flight frames while close is registered on both sides.
689
749
  # References to such streams will be purged whenever another stream
690
- # is closed, with a defined RTT time window.
691
- @streams_recently_closed[id] = Time.now.to_i
692
- cleanup_recently_closed
750
+ # is closed, with a minimum of 15s RTT time window.
751
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
752
+
753
+ # TODO: use a drop_while! variant whenever there is one.
754
+ @streams_recently_closed = @streams_recently_closed.drop_while do |_, v|
755
+ (now - v) > 15
756
+ end.to_h
757
+
758
+ @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
693
759
  end
694
760
 
695
- stream.on(:promise, &method(:promise)) if self.is_a? Server
761
+ stream.on(:promise, &method(:promise)) if is_a? Server
696
762
  stream.on(:frame, &method(:send))
697
763
 
698
764
  @streams[id] = stream
699
765
  end
700
766
 
701
- # Purge recently streams closed within defined RTT time window.
702
- def cleanup_recently_closed
703
- now_ts = Time.now.to_i
704
- to_delete = []
705
- @streams_recently_closed.each do |stream_id, ts|
706
- # Ruby Hash enumeration is ordered, so once fresh stream is met we can stop searching.
707
- break if now_ts - ts < RECENTLY_CLOSED_STREAMS_TTL
708
- to_delete << stream_id
709
- end
767
+ def verify_stream_order(id)
768
+ return unless id.odd?
710
769
 
711
- to_delete.each do |stream_id|
712
- @streams.delete stream_id
713
- @streams_recently_closed.delete stream_id
714
- end
770
+ connection_error(msg: "Stream ID smaller than previous") if @last_stream_id > id
771
+ @last_stream_id = id
772
+ end
773
+
774
+ def _verify_pseudo_headers(frame, mandatory_headers)
775
+ headers = frame[:payload]
776
+ return if headers.is_a?(String)
777
+
778
+ pseudo_headers = headers.take_while do |field, value|
779
+ # use this loop to validate pseudo-headers
780
+ connection_error(:protocol_error, msg: "path is empty") if field == ":path" && value.empty?
781
+ field.start_with?(":")
782
+ end.map(&:first)
783
+ return if mandatory_headers.size == pseudo_headers.size &&
784
+ (mandatory_headers - pseudo_headers).empty?
785
+
786
+ connection_error(:protocol_error, msg: "invalid pseudo-headers")
715
787
  end
716
788
 
717
789
  # Emit GOAWAY error indicating to peer that the connection is being
@@ -728,11 +800,11 @@ module HTTP2
728
800
  def connection_error(error = :protocol_error, msg: nil, e: nil)
729
801
  goaway(error) unless @state == :closed || @state == :new
730
802
 
731
- @state, @error = :closed, error
732
- klass = error.to_s.split('_').map(&:capitalize).join
733
- msg ||= e && e.message
734
- backtrace = (e && e.backtrace) || []
735
- fail Error.const_get(klass), msg, backtrace
803
+ @state = :closed
804
+ @error = error
805
+ msg ||= e ? e.message : "protocol error"
806
+ backtrace = e ? e.backtrace : nil
807
+ raise Error.types[error], msg, backtrace
736
808
  end
737
809
  alias error connection_error
738
810
 
@@ -740,5 +812,5 @@ module HTTP2
740
812
  yield
741
813
  end
742
814
  end
743
- # rubocop:enable ClassLength
815
+ # rubocop:enable Metrics/ClassLength
744
816
  end