http-2 0.11.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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