http-2 0.6.3 → 0.7.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.
@@ -3,11 +3,36 @@ module HTTP2
3
3
  # Default connection and stream flow control window (64KB).
4
4
  DEFAULT_FLOW_WINDOW = 65535
5
5
 
6
+ # Default header table size
7
+ DEFAULT_HEADER_SIZE = 4096
8
+
9
+ # Default stream_limit
10
+ DEFAULT_MAX_CONCURRENT_STREAMS = 100
11
+
12
+ # Default values for SETTINGS frame, as defined by the spec.
13
+ SPEC_DEFAULT_CONNECTION_SETTINGS = {
14
+ settings_header_table_size: 4096,
15
+ settings_enable_push: 1, # enabled for servers
16
+ settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
17
+ settings_initial_window_size: 65535,
18
+ settings_max_frame_size: 16384,
19
+ settings_max_header_list_size: 2**31 - 1, # unlimited
20
+ }.freeze
21
+
22
+ DEFAULT_CONNECTION_SETTINGS = {
23
+ settings_header_table_size: 4096,
24
+ settings_enable_push: 1, # enabled for servers
25
+ settings_max_concurrent_streams: 100,
26
+ settings_initial_window_size: 65535, #
27
+ settings_max_frame_size: 16384,
28
+ settings_max_header_list_size: 2**31 - 1, # unlimited
29
+ }.freeze
30
+
6
31
  # Default stream priority (lower values are higher priority).
7
- DEFAULT_PRIORITY = 2**30
32
+ DEFAULT_WEIGHT = 16
8
33
 
9
34
  # Default connection "fast-fail" preamble string as defined by the spec.
10
- CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
35
+ CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
11
36
 
12
37
  # Connection encapsulates all of the connection, stream, flow-control,
13
38
  # error management, and other processing logic required for a well-behaved
@@ -28,11 +53,17 @@ module HTTP2
28
53
 
29
54
  # Size of current connection flow control window (by default, set to
30
55
  # infinity, but is automatically updated on receipt of peer settings).
31
- attr_reader :window
56
+ attr_reader :local_window
57
+ attr_reader :remote_window
58
+ alias :window :local_window
59
+
60
+ # Current settings value for local and peer
61
+ attr_reader :local_settings
62
+ attr_reader :remote_settings
32
63
 
33
- # Maximum number of concurrent streams allowed by the peer (automatically
34
- # updated on receipt of peer settings).
35
- attr_reader :stream_limit
64
+ # Pending settings value
65
+ # Sent but not ack'ed settings
66
+ attr_reader :pending_settings
36
67
 
37
68
  # Number of active streams between client and server (reserved streams
38
69
  # are not counted towards the stream limit).
@@ -40,14 +71,23 @@ module HTTP2
40
71
 
41
72
  # Initializes new connection object.
42
73
  #
43
- def initialize(streams: 100, window: DEFAULT_FLOW_WINDOW)
44
- @stream_limit = streams
74
+ def initialize(**settings)
75
+ @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
76
+ @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
77
+
78
+ @compressor = Header::Compressor.new(settings)
79
+ @decompressor = Header::Decompressor.new(settings)
80
+
45
81
  @active_stream_count = 0
46
82
  @streams = {}
83
+ @pending_settings = []
47
84
 
48
85
  @framer = Framer.new
49
- @window = window
50
- @window_limit = window
86
+
87
+ @local_window_limit = @local_settings[:settings_initial_window_size]
88
+ @local_window = @local_window_limit
89
+ @remote_window_limit = @remote_settings[:settings_initial_window_size]
90
+ @remote_window = @remote_window_limit
51
91
 
52
92
  @recv_buffer = Buffer.new
53
93
  @send_buffer = []
@@ -60,11 +100,11 @@ module HTTP2
60
100
  # @param priority [Integer]
61
101
  # @param window [Integer]
62
102
  # @param parent [Stream]
63
- def new_stream(priority: DEFAULT_PRIORITY, parent: nil)
103
+ def new_stream(**args)
64
104
  raise ConnectionClosed.new if @state == :closed
65
- raise StreamLimitExceeded.new if @active_stream_count == @stream_limit
105
+ raise StreamLimitExceeded.new if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
66
106
 
67
- stream = activate_stream(@stream_id, priority, parent)
107
+ stream = activate_stream(id: @stream_id, **args)
68
108
  @stream_id += 2
69
109
 
70
110
  stream
@@ -76,7 +116,7 @@ module HTTP2
76
116
  # @param blk [Proc] callback to execute when PONG is received
77
117
  def ping(payload, &blk)
78
118
  send({type: :ping, stream: 0, payload: payload})
79
- once(:pong, &blk) if blk
119
+ once(:ack, &blk) if blk
80
120
  end
81
121
 
82
122
  # Sends a GOAWAY frame indicating that the peer should stop creating
@@ -97,20 +137,17 @@ module HTTP2
97
137
  @state = :closed
98
138
  end
99
139
 
100
- # Sends a connection SETTINGS frame to the peer. Setting window size
101
- # to Float::INFINITY disables flow control.
140
+ # Sends a connection SETTINGS frame to the peer.
141
+ # The values are reflected when the corresponding ACK is received.
102
142
  #
103
- # @param stream_limit [Integer] maximum number of concurrent streams
104
- # @param window_limit [Float] maximum flow window size
105
- def settings(stream_limit: @stream_limit, window_limit: @window_limit)
106
- payload = { settings_max_concurrent_streams: stream_limit }
107
- if window_limit.to_f.infinite?
108
- payload[:settings_flow_control_options] = 1
109
- else
110
- payload[:settings_initial_window_size] = window_limit
111
- end
112
-
143
+ # @param settings [Array or Hash]
144
+ def settings(payload)
145
+ payload = payload.to_a
146
+ check = validate_settings(@local_role, payload)
147
+ check and connection_error
148
+ @pending_settings << payload
113
149
  send({type: :settings, stream: 0, payload: payload})
150
+ @pending_settings << payload
114
151
  end
115
152
 
116
153
  # Decodes incoming bytes into HTTP 2.0 frames and routes them to
@@ -128,23 +165,27 @@ module HTTP2
128
165
  #
129
166
  # Client connection header is 24 byte connection header followed by
130
167
  # SETTINGS frame. Server connection header is SETTINGS frame only.
131
- if @state == :new
168
+ if @state == :waiting_magic
132
169
  if @recv_buffer.size < 24
133
- if !CONNECTION_HEADER.start_with? @recv_buffer
170
+ if !CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
134
171
  raise HandshakeError.new
135
172
  else
136
- return
173
+ return # maybe next time
137
174
  end
138
175
 
139
- elsif @recv_buffer.read(24) != CONNECTION_HEADER
176
+ elsif @recv_buffer.read(24) != CONNECTION_PREFACE_MAGIC
140
177
  raise HandshakeError.new
141
178
  else
142
- @state = :connection_header
143
- settings(stream_limit: @stream_limit, window_limit: @window_limit)
179
+ # MAGIC is OK. Send our settings
180
+ @state = :waiting_connection_preface
181
+ payload = @local_settings.select {|k,v| v != SPEC_DEFAULT_CONNECTION_SETTINGS[k]}
182
+ settings(payload)
144
183
  end
145
184
  end
146
185
 
147
186
  while frame = @framer.parse(@recv_buffer) do
187
+ emit(:frame_received, frame)
188
+
148
189
  # Header blocks MUST be transmitted as a contiguous sequence of frames
149
190
  # with no interleaved frames of any other type, or from any other stream.
150
191
  if !@continuation.empty?
@@ -156,21 +197,14 @@ module HTTP2
156
197
  @continuation << frame
157
198
  return if !frame[:flags].include? :end_headers
158
199
 
159
- headers = @continuation.collect do |chunk|
160
- decode_headers(chunk)
161
- chunk[:payload]
162
- end.flatten(1)
200
+ payload = @continuation.map {|f| f[:payload]}.join
163
201
 
164
202
  frame = @continuation.shift
165
203
  @continuation.clear
166
204
 
167
205
  frame.delete(:length)
168
- frame[:payload] = headers
169
- frame[:flags] << if frame[:type] == :push_promise
170
- :end_push_promise
171
- else
172
- :end_headers
173
- end
206
+ frame[:payload] = Buffer.new(payload)
207
+ frame[:flags] << :end_headers
174
208
  end
175
209
 
176
210
  # SETTINGS frames always apply to a connection, never a single stream.
@@ -200,8 +234,10 @@ module HTTP2
200
234
 
201
235
  stream = @streams[frame[:stream]]
202
236
  if stream.nil?
203
- stream = activate_stream(frame[:stream],
204
- frame[:priority] || DEFAULT_PRIORITY)
237
+ stream = activate_stream(id: frame[:stream],
238
+ weight: frame[:weight] || DEFAULT_WEIGHT,
239
+ dependency: frame[:dependency] || 0,
240
+ exclusive: frame[:exclusive] || false)
205
241
  emit(:stream, stream)
206
242
  end
207
243
 
@@ -209,8 +245,8 @@ module HTTP2
209
245
 
210
246
  when :push_promise
211
247
  # The last frame in a sequence of PUSH_PROMISE/CONTINUATION
212
- # frames MUST have the END_PUSH_PROMISE/END_HEADERS flag set
213
- if !frame[:flags].include? :end_push_promise
248
+ # frames MUST have the END_HEADERS flag set
249
+ if !frame[:flags].include? :end_headers
214
250
  @continuation << frame
215
251
  return
216
252
  end
@@ -248,7 +284,7 @@ module HTTP2
248
284
  end
249
285
  end
250
286
 
251
- stream = activate_stream(pid, DEFAULT_PRIORITY, parent)
287
+ stream = activate_stream(id: pid, parent: parent)
252
288
  emit(:promise, stream)
253
289
  stream << frame
254
290
  else
@@ -263,7 +299,7 @@ module HTTP2
263
299
  end
264
300
  end
265
301
 
266
- rescue
302
+ rescue => e
267
303
  connection_error
268
304
  end
269
305
  alias :<< :receive
@@ -277,6 +313,7 @@ module HTTP2
277
313
  # @note all frames are currently delivered in FIFO order.
278
314
  # @param frame [Hash]
279
315
  def send(frame)
316
+ emit(:frame_sent, frame)
280
317
  if frame[:type] == :data
281
318
  send_data(frame, true)
282
319
 
@@ -288,7 +325,9 @@ module HTTP2
288
325
  goaway(frame[:error])
289
326
  end
290
327
  else
291
- emit(:frame, encode(frame))
328
+ # HEADERS and PUSH_PROMISE may generate CONTINUATION
329
+ frames = encode(frame)
330
+ frames.each {|f| emit(:frame, f) }
292
331
  end
293
332
  end
294
333
  end
@@ -296,14 +335,18 @@ module HTTP2
296
335
  # Applies HTTP 2.0 binary encoding to the frame.
297
336
  #
298
337
  # @param frame [Hash]
299
- # @return [Buffer] encoded frame
338
+ # @return [Array of Buffer] encoded frame
300
339
  def encode(frame)
340
+ frames = []
341
+
301
342
  if frame[:type] == :headers ||
302
343
  frame[:type] == :push_promise
303
- encode_headers(frame)
344
+ frames = encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
345
+ else
346
+ frames = [frame] # otherwise one frame
304
347
  end
305
348
 
306
- @framer.generate(frame)
349
+ frames.map {|f| @framer.generate(f) }
307
350
  end
308
351
 
309
352
  # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
@@ -327,26 +370,25 @@ module HTTP2
327
370
  # @param frame [Hash]
328
371
  def connection_management(frame)
329
372
  case @state
330
- when :connection_header
331
- # SETTINGS frames MUST be sent at the start of a connection.
332
- connection_settings(frame)
373
+ when :waiting_connection_preface
374
+ # The first frame MUST be a SETTINGS frame at the start of a connection.
333
375
  @state = :connected
376
+ connection_settings(frame)
334
377
 
335
378
  when :connected
336
379
  case frame[:type]
337
380
  when :settings
338
381
  connection_settings(frame)
339
382
  when :window_update
340
- flow_control_allowed?
341
- @window += frame[:increment]
383
+ @remote_window += frame[:increment]
342
384
  send_data(nil, true)
343
385
  when :ping
344
- if frame[:flags].include? :pong
345
- emit(:pong, frame[:payload])
386
+ if frame[:flags].include? :ack
387
+ emit(:ack, frame[:payload])
346
388
  else
347
389
  send({
348
390
  type: :ping, stream: 0,
349
- flags: [:pong], payload: frame[:payload]
391
+ flags: [:ack], payload: frame[:payload]
350
392
  })
351
393
  end
352
394
  when :goaway
@@ -355,7 +397,8 @@ module HTTP2
355
397
  # for new streams.
356
398
  @state = :closed
357
399
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
358
-
400
+ when :altsvc, :blocked
401
+ emit(frame[:type], frame)
359
402
  else
360
403
  connection_error
361
404
  end
@@ -364,7 +407,60 @@ module HTTP2
364
407
  end
365
408
  end
366
409
 
367
- # Update local connection settings based on parameters set by the peer.
410
+ # Validate settings parameters. See sepc Section 6.5.2.
411
+ #
412
+ # @param role [Symbol] The sender's role: :client or :server
413
+ # @return nil if no error. Exception object in case of any error.
414
+ def validate_settings(role, settings)
415
+ settings.each do |key,v|
416
+ case key
417
+ when :settings_header_table_size
418
+ # Any value is valid
419
+ when :settings_enable_push
420
+ case role
421
+ when :server
422
+ # Section 8.2
423
+ # Clients MUST reject any attempt to change the
424
+ # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
425
+ # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
426
+ unless v == 0
427
+ return ProtocolError.new("invalid #{key} value")
428
+ end
429
+ when :client
430
+ # Any value other than 0 or 1 MUST be treated as a
431
+ # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
432
+ unless v == 0 || v == 1
433
+ return ProtocolError.new("invalid #{key} value")
434
+ end
435
+ end
436
+ when :settings_max_concurrent_streams
437
+ # Any value is valid
438
+ when :settings_initial_window_size
439
+ # Values above the maximum flow control window size of 2^31-1 MUST
440
+ # be treated as a connection error (Section 5.4.1) of type
441
+ # FLOW_CONTROL_ERROR.
442
+ unless v <= 0x7fffffff
443
+ return FlowControlError.new("invalid #{key} value")
444
+ end
445
+ when :settings_max_frame_size
446
+ # The initial value is 2^14 (16,384) octets. The value advertised
447
+ # by an endpoint MUST be between this initial value and the maximum
448
+ # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
449
+ # Values outside this range MUST be treated as a connection error
450
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
451
+ unless 16384 <= v && v <= 16777215
452
+ return ProtocolError.new("invalid #{key} value")
453
+ end
454
+ when :settings_max_header_list_size
455
+ # Any value is valid
456
+ else
457
+ # ignore unknown settings
458
+ end
459
+ end
460
+ nil
461
+ end
462
+
463
+ # Update connection settings based on parameters set by the peer.
368
464
  #
369
465
  # @param frame [Hash]
370
466
  def connection_settings(frame)
@@ -372,35 +468,85 @@ module HTTP2
372
468
  connection_error
373
469
  end
374
470
 
375
- frame[:payload].each do |key,v|
471
+ # Apply settings.
472
+ # side =
473
+ # local: previously sent and pended our settings should be effective
474
+ # remote: just received peer settings should immediately be effective
475
+ settings, side = \
476
+ if frame[:flags].include?(:ack)
477
+ # Process pending settings we have sent.
478
+ [@pending_settings.shift, :local]
479
+ else
480
+ check = validate_settings(@remote_role, frame[:payload])
481
+ check and connection_error(check)
482
+ [frame[:payload], :remote]
483
+ end
484
+
485
+ settings.each do |key,v|
486
+ case side
487
+ when :local
488
+ @local_settings[key] = v
489
+ when :remote
490
+ @remote_settings[key] = v
491
+ end
492
+
376
493
  case key
377
494
  when :settings_max_concurrent_streams
378
- @stream_limit = v
495
+ # Do nothing.
496
+ # The value controls at the next attempt of stream creation.
379
497
 
380
- # A change to SETTINGS_INITIAL_WINDOW_SIZE could cause the available
381
- # space in a flow control window to become negative. A sender MUST
382
- # track the negative flow control window, and MUST NOT send new flow
383
- # controlled frames until it receives WINDOW_UPDATE frames that cause
384
- # the flow control window to become positive.
385
498
  when :settings_initial_window_size
386
- flow_control_allowed?
387
- @window = @window - @window_limit + v
388
- @streams.each do |id, stream|
389
- stream.emit(:window, stream.window - @window_limit + v)
390
- end
499
+ # A change to SETTINGS_INITIAL_WINDOW_SIZE could cause the available
500
+ # space in a flow control window to become negative. A sender MUST
501
+ # track the negative flow control window, and MUST NOT send new flow
502
+ # controlled frames until it receives WINDOW_UPDATE frames that cause
503
+ # the flow control window to become positive.
504
+ case side
505
+ when :local
506
+ @local_window = @local_window - @local_window_limit + v
507
+ @streams.each do |id, stream|
508
+ stream.emit(:local_window, stream.local_window - @local_window_limit + v)
509
+ end
391
510
 
392
- @window_limit = v
511
+ @local_window_limit = v
512
+ when :remote
513
+ @remote_window = @remote_window - @remote_window_limit + v
514
+ @streams.each do |id, stream|
515
+ # Event name is :window, not :remote_window
516
+ stream.emit(:window, stream.remote_window - @remote_window_limit + v)
517
+ end
393
518
 
394
- # Flow control can be disabled the entire connection using the
395
- # SETTINGS_FLOW_CONTROL_OPTIONS setting. This setting ends all forms
396
- # of flow control. An implementation that does not wish to perform
397
- # flow control can use this in the initial SETTINGS exchange.
398
- when :settings_flow_control_options
399
- flow_control_allowed?
519
+ @remote_window_limit = v
520
+ end
400
521
 
401
- if v == 1
402
- @window = @window_limit = Float::INFINITY
522
+ when :settings_header_table_size
523
+ # Setting header table size might cause some headers evicted
524
+ case side
525
+ when :local
526
+ @decompressor.set_table_size(v)
527
+ when :remote
528
+ @compressor.set_table_size(v)
403
529
  end
530
+
531
+ when :settings_enable_push
532
+ # nothing to do
533
+
534
+ when :settings_max_frame_size
535
+ # nothing to do
536
+
537
+ else
538
+ # ignore unknown settings
539
+ end
540
+ end
541
+
542
+ case side
543
+ when :local
544
+ # Received a settings_ack. Notify application layer.
545
+ emit(:settings_ack, frame, @pending_settings.size)
546
+ when :remote
547
+ if @state != :closed
548
+ # Send ack to peer
549
+ send({type: :settings, stream: 0, payload: [], flags: [:ack]})
404
550
  end
405
551
  end
406
552
  end
@@ -425,21 +571,34 @@ module HTTP2
425
571
  # Encode headers payload and update connection compressor state.
426
572
  #
427
573
  # @param frame [Hash]
574
+ # @return [Array of Frame]
428
575
  def encode_headers(frame)
429
- if !frame[:payload].is_a? String
430
- frame[:payload] = @compressor.encode(frame[:payload])
576
+ payload = frame[:payload]
577
+ unless payload.is_a? String
578
+ payload = @compressor.encode(payload)
431
579
  end
432
580
 
433
- rescue Exception => e
434
- connection_error(:compression_error, msg: e.message)
435
- end
581
+ frames = []
436
582
 
437
- # Once disabled, no further flow control operations are permitted.
438
- #
439
- def flow_control_allowed?
440
- if @window_limit == Float::INFINITY
441
- connection_error(:flow_control_error)
583
+ while payload.size > 0
584
+ cont = frame.dup
585
+ cont[:type] = :continuation
586
+ cont[:flags] = []
587
+ cont[:payload] = payload.slice!(0, @remote_settings[:settings_max_frame_size])
588
+ frames << cont
589
+ end
590
+ if frames.empty?
591
+ frames = [frame]
592
+ else
593
+ frames.first[:type] = frame[:type]
594
+ frames.first[:flags] = frame[:flags] - [:end_headers]
595
+ frames.last[:flags] << :end_headers
442
596
  end
597
+
598
+ frames
599
+
600
+ rescue Exception => e
601
+ [connection_error(:compression_error, msg: e.message)]
443
602
  end
444
603
 
445
604
  # Activates new incoming or outgoing stream and registers appropriate
@@ -449,12 +608,12 @@ module HTTP2
449
608
  # @param priority [Integer]
450
609
  # @param window [Integer]
451
610
  # @param parent [Stream]
452
- def activate_stream(id, priority, parent = nil)
611
+ def activate_stream(id: nil, **args)
453
612
  if @streams.key?(id)
454
613
  connection_error(msg: 'Stream ID already exists')
455
614
  end
456
615
 
457
- stream = Stream.new(id, priority, @window_limit, parent)
616
+ stream = Stream.new({connection: self, id: id}.merge(args))
458
617
 
459
618
  # Streams that are in the "open" state, or either of the "half closed"
460
619
  # states count toward the maximum number of streams that an endpoint is