http-2 1.2.2 → 1.3.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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/lib/http/2/base64.rb +8 -8
- data/lib/http/2/client.rb +1 -1
- data/lib/http/2/connection.rb +70 -60
- data/lib/http/2/framer.rb +3 -3
- data/lib/http/2/header/compressor.rb +13 -39
- data/lib/http/2/header/decompressor.rb +29 -32
- data/lib/http/2/header/encoding_context.rb +90 -63
- data/lib/http/2/header/huffman.rb +3 -10
- data/lib/http/2/server.rb +2 -5
- data/lib/http/2/settings.rb +23 -6
- data/lib/http/2/stream.rb +7 -6
- data/lib/http/2/version.rb +1 -1
- data/lib/http/2.rb +2 -0
- data/sig/2.rbs +7 -15
- data/sig/connection.rbs +7 -13
- data/sig/header/compressor.rbs +3 -3
- data/sig/header/decompressor.rbs +10 -3
- data/sig/header/encoding_context.rbs +14 -6
- data/sig/header.rbs +3 -2
- data/sig/server.rbs +2 -0
- data/sig/settings.rbs +73 -0
- data/sig/stream.rbs +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19c1750fca12fb4ff94e29361824fc7f64052b4a320adee6d739c1acffc3c4ab
|
|
4
|
+
data.tar.gz: cd41bb795576baae733b69a364444323b77702512a031d425ac1f8f62c402311
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a33812ab5c84f41d51b66ce93d0616ffed1592276df92f9be71fb714b2288d96e4b728222a80d5cd7501907259bbe924bdc332a6a61f18ab227e6bdeccfddca
|
|
7
|
+
data.tar.gz: d366d161f3a126e4a1394ccc13266fda028a1f7a949629a90a3ba1b86fb89a08971dc5f08cd3d03beef6f340444dd83d27b5110ca7ddca7aba3a80593115ced7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Ilya Grigorik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/lib/http/2/base64.rb
CHANGED
|
@@ -30,16 +30,16 @@ elsif !defined?(Base64)
|
|
|
30
30
|
str.tr!("+/", "-_")
|
|
31
31
|
str
|
|
32
32
|
end
|
|
33
|
-
end
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
def urlsafe_decode64(str)
|
|
35
|
+
if !str.end_with?("=") && str.length % 4 != 0
|
|
36
|
+
str = str.ljust((str.length + 3) & ~3, "=")
|
|
37
|
+
str.tr!("-_", "+/")
|
|
38
|
+
else
|
|
39
|
+
str = str.tr("-_", "+/")
|
|
40
|
+
end
|
|
41
|
+
strict_decode64(str)
|
|
41
42
|
end
|
|
42
|
-
strict_decode64(str)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
data/lib/http/2/client.rb
CHANGED
|
@@ -64,7 +64,7 @@ module HTTP2
|
|
|
64
64
|
@state = :connected
|
|
65
65
|
emit(:frame, CONNECTION_PREFACE_MAGIC)
|
|
66
66
|
|
|
67
|
-
payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
|
|
67
|
+
payload = @local_settings.connection_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
|
|
68
68
|
settings(payload)
|
|
69
69
|
end
|
|
70
70
|
|
data/lib/http/2/connection.rb
CHANGED
|
@@ -10,28 +10,6 @@ module HTTP2
|
|
|
10
10
|
# Default stream_limit
|
|
11
11
|
DEFAULT_MAX_CONCURRENT_STREAMS = 100
|
|
12
12
|
|
|
13
|
-
# Default values for SETTINGS frame, as defined by the spec.
|
|
14
|
-
SPEC_DEFAULT_CONNECTION_SETTINGS = {
|
|
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
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
DEFAULT_CONNECTION_SETTINGS = {
|
|
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
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
# Default stream priority (lower values are higher priority).
|
|
33
|
-
DEFAULT_WEIGHT = 16
|
|
34
|
-
|
|
35
13
|
# Default connection "fast-fail" preamble string as defined by the spec.
|
|
36
14
|
CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
37
15
|
|
|
@@ -78,27 +56,26 @@ module HTTP2
|
|
|
78
56
|
attr_accessor :active_stream_count
|
|
79
57
|
|
|
80
58
|
# Initializes new connection object.
|
|
81
|
-
#
|
|
82
59
|
def initialize(settings = {})
|
|
83
|
-
@local_settings =
|
|
84
|
-
@remote_settings =
|
|
60
|
+
@local_settings = Settings.new(**settings)
|
|
61
|
+
@remote_settings = Settings.new(settings_max_concurrent_streams: Framer::MAX_STREAM_ID)
|
|
85
62
|
|
|
86
|
-
@compressor = Header::Compressor.new(
|
|
87
|
-
@decompressor = Header::Decompressor.new(
|
|
63
|
+
@compressor = Header::Compressor.new(@local_settings)
|
|
64
|
+
@decompressor = Header::Decompressor.new(@local_settings)
|
|
88
65
|
|
|
89
|
-
@active_stream_count =
|
|
90
|
-
|
|
66
|
+
@active_stream_count =
|
|
67
|
+
@promised_stream_count =
|
|
68
|
+
@last_promised_stream_id =
|
|
69
|
+
@last_stream_id = 0
|
|
91
70
|
@streams = {}
|
|
92
71
|
@streams_recently_closed = {}
|
|
93
72
|
@oldest_stream_recently_closed = nil
|
|
94
73
|
@pending_settings = []
|
|
95
74
|
|
|
96
|
-
@framer = Framer.new(@local_settings
|
|
75
|
+
@framer = Framer.new(@local_settings.settings_max_frame_size)
|
|
97
76
|
|
|
98
|
-
@local_window_limit = @local_settings
|
|
99
|
-
@
|
|
100
|
-
@remote_window_limit = @remote_settings[:settings_initial_window_size]
|
|
101
|
-
@remote_window = @remote_window_limit
|
|
77
|
+
@local_window = @local_window_limit = @local_settings.settings_initial_window_size
|
|
78
|
+
@remote_window = @remote_window_limit = @remote_settings.settings_initial_window_size
|
|
102
79
|
|
|
103
80
|
@recv_buffer = "".b
|
|
104
81
|
@continuation = []
|
|
@@ -137,7 +114,7 @@ module HTTP2
|
|
|
137
114
|
|
|
138
115
|
stream = activate_stream(
|
|
139
116
|
id: @stream_id,
|
|
140
|
-
max_concurrent_streams: @remote_settings
|
|
117
|
+
max_concurrent_streams: @remote_settings.settings_max_concurrent_streams,
|
|
141
118
|
**args
|
|
142
119
|
)
|
|
143
120
|
@last_stream_id = stream.id
|
|
@@ -213,7 +190,7 @@ module HTTP2
|
|
|
213
190
|
elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC
|
|
214
191
|
# MAGIC is OK. Send our settings
|
|
215
192
|
@state = :waiting_connection_preface
|
|
216
|
-
payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
|
|
193
|
+
payload = @local_settings.connection_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
|
|
217
194
|
settings(payload)
|
|
218
195
|
else
|
|
219
196
|
raise HandshakeError
|
|
@@ -254,7 +231,7 @@ module HTTP2
|
|
|
254
231
|
# prevent HTTP/2 CONTINUATION FLOOD
|
|
255
232
|
# same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
|
|
256
233
|
# different mitigation (connection closed, instead of 400 response)
|
|
257
|
-
unless @continuation_size < @local_settings
|
|
234
|
+
unless @continuation_size < @local_settings.settings_max_frame_size
|
|
258
235
|
connection_error(:protocol_error,
|
|
259
236
|
msg: "too many continuations received")
|
|
260
237
|
end
|
|
@@ -311,20 +288,25 @@ module HTTP2
|
|
|
311
288
|
# Skip only this frame: later ones may serve completing streams.
|
|
312
289
|
next if closed? && (stream_id > @last_stream_id || !@streams.key?(stream_id))
|
|
313
290
|
|
|
314
|
-
stream = @streams[stream_id]
|
|
315
|
-
if stream.nil?
|
|
291
|
+
unless (stream = @streams[stream_id])
|
|
316
292
|
verify_pseudo_headers(frame)
|
|
317
293
|
|
|
318
294
|
verify_stream_order(stream_id)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
295
|
+
|
|
296
|
+
# TODO: remove EMPTY_HASH after dropping support for ruby < 3.4 (recent rubies can
|
|
297
|
+
# receive nil as kwarg splat)
|
|
298
|
+
priority = EMPTY_HASH
|
|
299
|
+
if frame[:flags].anybits?(PRIORITY)
|
|
300
|
+
connection_error(:protocol_error, msg: "stream can't depend on itself") if stream_id == frame[:dependency]
|
|
301
|
+
|
|
302
|
+
priority = frame.slice(:weight, :dependency, :exclusive)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
stream = activate_stream(id: stream_id, **priority)
|
|
326
306
|
end
|
|
327
307
|
|
|
308
|
+
# streams are only emitted in a non-idle state.
|
|
309
|
+
emit(:stream, stream)
|
|
328
310
|
stream << frame
|
|
329
311
|
|
|
330
312
|
when :push_promise
|
|
@@ -352,6 +334,8 @@ module HTTP2
|
|
|
352
334
|
parent = @streams[stream_id]
|
|
353
335
|
pid = frame[:promise_stream]
|
|
354
336
|
|
|
337
|
+
validate_push_promise(pid)
|
|
338
|
+
|
|
355
339
|
# if PUSH parent is recently closed, RST_STREAM the push
|
|
356
340
|
if @streams_recently_closed[stream_id]
|
|
357
341
|
send(type: :rst_stream, stream: pid, error: :refused_stream)
|
|
@@ -377,6 +361,12 @@ module HTTP2
|
|
|
377
361
|
|
|
378
362
|
_verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS)
|
|
379
363
|
verify_stream_order(pid)
|
|
364
|
+
|
|
365
|
+
if @promised_stream_count >= @local_settings.settings_max_concurrent_streams
|
|
366
|
+
send(type: :rst_stream, stream: pid, error: :refused_stream)
|
|
367
|
+
next
|
|
368
|
+
end
|
|
369
|
+
|
|
380
370
|
stream = activate_stream(id: pid, parent: parent)
|
|
381
371
|
emit(:promise, stream)
|
|
382
372
|
stream << frame
|
|
@@ -391,7 +381,7 @@ module HTTP2
|
|
|
391
381
|
case frame_type
|
|
392
382
|
# Priority signaling is deprecated (RFC 9113 Section 5.3.2), but
|
|
393
383
|
# a PRIORITY frame may still name a stream id not opened yet:
|
|
394
|
-
#
|
|
384
|
+
# retain its priority without creating a stream until it opens.
|
|
395
385
|
when :priority
|
|
396
386
|
# After a GOAWAY, a PRIORITY for a stream already used and
|
|
397
387
|
# closed must not resurrect it: reprioritizing a closed
|
|
@@ -399,16 +389,12 @@ module HTTP2
|
|
|
399
389
|
# the draining connection open.
|
|
400
390
|
next if closed? && stream_id <= @last_stream_id
|
|
401
391
|
|
|
402
|
-
stream
|
|
403
|
-
id: stream_id,
|
|
404
|
-
weight: frame[:weight] || DEFAULT_WEIGHT,
|
|
405
|
-
dependency: frame[:dependency] || 0,
|
|
406
|
-
exclusive: frame[:exclusive] || false
|
|
407
|
-
)
|
|
392
|
+
connection_error(:protocol_error, msg: "stream can't depend on itself") if frame[:stream] == frame[:dependency]
|
|
408
393
|
|
|
409
|
-
|
|
410
|
-
stream << frame
|
|
394
|
+
priority = frame.slice(:weight, :dependency, :exclusive)
|
|
411
395
|
|
|
396
|
+
stream = activate_stream(id: stream_id, **priority)
|
|
397
|
+
stream << frame
|
|
412
398
|
# WINDOW_UPDATE can be sent by a peer that has sent a frame
|
|
413
399
|
# bearing the END_STREAM flag. This means that a receiver could
|
|
414
400
|
# receive a WINDOW_UPDATE frame on a "half-closed (remote)" or
|
|
@@ -577,6 +563,16 @@ module HTTP2
|
|
|
577
563
|
end
|
|
578
564
|
end
|
|
579
565
|
|
|
566
|
+
def validate_push_promise(stream_id)
|
|
567
|
+
connection_error(:protocol_error, msg: "clients cannot send PUSH_PROMISE") if @remote_role == :client
|
|
568
|
+
connection_error(:protocol_error, msg: "push promises are disabled") if @local_settings.settings_enable_push.zero?
|
|
569
|
+
unless stream_id.positive? && stream_id.even? && stream_id > @last_promised_stream_id
|
|
570
|
+
connection_error(:protocol_error, msg: "invalid promised stream ID")
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
@last_promised_stream_id = stream_id
|
|
574
|
+
end
|
|
575
|
+
|
|
580
576
|
# Validate settings parameters. See sepc Section 6.5.2.
|
|
581
577
|
#
|
|
582
578
|
# @param role [Symbol] The sender's role: :client or :server
|
|
@@ -687,12 +683,15 @@ module HTTP2
|
|
|
687
683
|
end
|
|
688
684
|
|
|
689
685
|
when :settings_header_table_size
|
|
690
|
-
#
|
|
686
|
+
# The value is the maximum table size for the encoder of the other
|
|
687
|
+
# side: our setting limits the peer's encoder (our decompressor),
|
|
688
|
+
# and the peer's setting limits our encoder (our compressor).
|
|
689
|
+
# The table itself changes with the next dynamic table size update.
|
|
691
690
|
case side
|
|
692
691
|
when :local
|
|
693
|
-
@
|
|
692
|
+
@decompressor.max_table_size = v
|
|
694
693
|
when :remote
|
|
695
|
-
@
|
|
694
|
+
@compressor.max_table_size = v
|
|
696
695
|
end
|
|
697
696
|
|
|
698
697
|
when :settings_enable_push
|
|
@@ -701,6 +700,13 @@ module HTTP2
|
|
|
701
700
|
when :settings_max_frame_size
|
|
702
701
|
@framer.remote_max_frame_size = v
|
|
703
702
|
|
|
703
|
+
when :settings_max_header_list_size
|
|
704
|
+
case side
|
|
705
|
+
when :remote
|
|
706
|
+
@decompressor.max_header_list_size = v
|
|
707
|
+
# when :local
|
|
708
|
+
# not enforcing this on the client side
|
|
709
|
+
end
|
|
704
710
|
# else # ignore unknown settings
|
|
705
711
|
end
|
|
706
712
|
end
|
|
@@ -752,7 +758,7 @@ module HTTP2
|
|
|
752
758
|
#: @type var payload: String
|
|
753
759
|
headers_frame[:payload] = payload
|
|
754
760
|
|
|
755
|
-
max_frame_size = @remote_settings
|
|
761
|
+
max_frame_size = @remote_settings.settings_max_frame_size
|
|
756
762
|
|
|
757
763
|
# if single frame, return immediately
|
|
758
764
|
if payload.bytesize <= max_frame_size
|
|
@@ -796,7 +802,7 @@ module HTTP2
|
|
|
796
802
|
# @param priority [Integer]
|
|
797
803
|
# @param window [Integer]
|
|
798
804
|
# @param parent [Stream]
|
|
799
|
-
def activate_stream(id:, max_concurrent_streams: @local_settings
|
|
805
|
+
def activate_stream(id:, max_concurrent_streams: @local_settings.settings_max_concurrent_streams, **args)
|
|
800
806
|
connection_error(msg: "Stream ID already exists") if @streams.key?(id)
|
|
801
807
|
|
|
802
808
|
# SETTINGS_MAX_CONCURRENT_STREAMS limits the number of concurrent streams that the sender
|
|
@@ -805,9 +811,11 @@ module HTTP2
|
|
|
805
811
|
raise StreamLimitExceeded if @active_stream_count >= max_concurrent_streams
|
|
806
812
|
|
|
807
813
|
stream = Stream.new(connection: self, id: id, **args)
|
|
814
|
+
@promised_stream_count += 1 if stream.parent
|
|
808
815
|
|
|
809
816
|
stream.once(:close) do
|
|
810
817
|
@streams.delete(id)
|
|
818
|
+
@promised_stream_count -= 1 if stream.parent
|
|
811
819
|
|
|
812
820
|
# A graceful GOAWAY leaves the connection :closing until the
|
|
813
821
|
# tracked streams complete (Section 6.8); the last one to close
|
|
@@ -835,6 +843,8 @@ module HTTP2
|
|
|
835
843
|
true
|
|
836
844
|
end
|
|
837
845
|
@oldest_stream_recently_closed = new_oldest
|
|
846
|
+
else
|
|
847
|
+
@oldest_stream_recently_closed ||= now
|
|
838
848
|
end
|
|
839
849
|
|
|
840
850
|
@streams_recently_closed[id] = now
|
data/lib/http/2/framer.rb
CHANGED
|
@@ -130,10 +130,10 @@ module HTTP2
|
|
|
130
130
|
|
|
131
131
|
stream_id = frame.fetch(:stream, 0)
|
|
132
132
|
|
|
133
|
-
raise CompressionError, "Stream ID (#{stream_id}) is
|
|
133
|
+
raise CompressionError, "Stream ID (#{stream_id}) is outside the valid range" unless stream_id.between?(0, MAX_STREAM_ID)
|
|
134
134
|
|
|
135
|
-
if type == :window_update && frame[:increment]
|
|
136
|
-
raise CompressionError, "Window increment (#{frame[:increment]}) is
|
|
135
|
+
if type == :window_update && !frame[:increment].between?(1, MAX_WINDOWINC)
|
|
136
|
+
raise CompressionError, "Window increment (#{frame[:increment]}) is outside the valid range"
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
flags = frame[:flags]
|
|
@@ -7,18 +7,18 @@ module HTTP2
|
|
|
7
7
|
include PackingExtensions
|
|
8
8
|
include BufferUtils
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@cc = EncodingContext.new(options)
|
|
10
|
+
def initialize(settings = Settings.new)
|
|
11
|
+
@cc = EncodingContext.new(settings)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
# Set dynamic table size
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
# Set the maximum dynamic table +size+ allowed by the peer's
|
|
15
|
+
# SETTINGS_HEADER_TABLE_SIZE. The next header block starts with a
|
|
16
|
+
# dynamic table size update when the table size has to change.
|
|
17
|
+
def max_table_size=(size)
|
|
18
|
+
@cc.max_table_size = size
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Encodes
|
|
21
|
+
# Encodes +i+ via integer representation into +buffer+ at the offset set by +offset+.
|
|
22
22
|
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
|
|
23
23
|
#
|
|
24
24
|
# If I < 2^N - 1, encode I on N bits
|
|
@@ -30,11 +30,6 @@ module HTTP2
|
|
|
30
30
|
# I = I / 128
|
|
31
31
|
# encode (I) on 8 bits
|
|
32
32
|
#
|
|
33
|
-
# @param i [Integer] value to encode
|
|
34
|
-
# @param n [Integer] number of available bits
|
|
35
|
-
# @param buffer [String] buffer to pack bytes into
|
|
36
|
-
# @param offset [Integer] offset to insert packed bytes in buffer
|
|
37
|
-
# @return [String] binary string
|
|
38
33
|
def integer(i, n, buffer:, offset: buffer.size)
|
|
39
34
|
limit = (1 << n) - 1
|
|
40
35
|
return pack([i], "C", buffer: buffer, offset: offset) if i < limit
|
|
@@ -65,16 +60,8 @@ module HTTP2
|
|
|
65
60
|
# * If the bit 7 of the first byte is 0, the string value is
|
|
66
61
|
# represented as a list of UTF-8 encoded octets.
|
|
67
62
|
#
|
|
68
|
-
# +@options [:huffman]+ controls whether to use Huffman encoding:
|
|
69
|
-
# :never Do not use Huffman encoding
|
|
70
|
-
# :always Always use Huffman encoding
|
|
71
|
-
# :shorter Use Huffman when the result is strictly shorter
|
|
72
|
-
#
|
|
73
|
-
# @param str [String]
|
|
74
|
-
# @param buffer [String]
|
|
75
|
-
# @return [String] binary string
|
|
76
63
|
def string(str, buffer = "".b)
|
|
77
|
-
case @cc.
|
|
64
|
+
case @cc.settings.huffman
|
|
78
65
|
when :always
|
|
79
66
|
huffman_string(str, buffer)
|
|
80
67
|
when :never
|
|
@@ -93,11 +80,7 @@ module HTTP2
|
|
|
93
80
|
end
|
|
94
81
|
end
|
|
95
82
|
|
|
96
|
-
# Encodes header command with appropriate header representation
|
|
97
|
-
#
|
|
98
|
-
# @param h [Hash] header command
|
|
99
|
-
# @param buffer [String]
|
|
100
|
-
# @return [Buffer]
|
|
83
|
+
# Encodes +h+ header command with appropriate header representation into +buffer+.
|
|
101
84
|
def header(h, buffer = "".b)
|
|
102
85
|
type = h[:type]
|
|
103
86
|
rep = HEADREP[type]
|
|
@@ -128,9 +111,6 @@ module HTTP2
|
|
|
128
111
|
end
|
|
129
112
|
|
|
130
113
|
# Encodes provided list of HTTP headers.
|
|
131
|
-
#
|
|
132
|
-
# @param headers [Array] +[[name, value], ...]+
|
|
133
|
-
# @return [Buffer]
|
|
134
114
|
def encode(headers)
|
|
135
115
|
buffer = "".b
|
|
136
116
|
headers.partition { |f, _| f.start_with? ":" }.each do |hs|
|
|
@@ -144,9 +124,7 @@ module HTTP2
|
|
|
144
124
|
|
|
145
125
|
private
|
|
146
126
|
|
|
147
|
-
#
|
|
148
|
-
# @param buffer [String]
|
|
149
|
-
# @return [String] binary string
|
|
127
|
+
# encodes +str+ into +buffer+ using Huffman encoding.
|
|
150
128
|
def huffman_string(str, buffer = "".b)
|
|
151
129
|
huffman_offset = buffer.bytesize
|
|
152
130
|
buffer << "\x00".b
|
|
@@ -162,18 +140,14 @@ module HTTP2
|
|
|
162
140
|
buffer
|
|
163
141
|
end
|
|
164
142
|
|
|
165
|
-
#
|
|
166
|
-
# @param buffer [String]
|
|
167
|
-
# @return [String] binary string
|
|
143
|
+
# encodes +str+ into +buffer+.
|
|
168
144
|
def plain_string(str, plain = "".b)
|
|
169
145
|
integer(str.bytesize, 7, buffer: plain)
|
|
170
146
|
append_str(plain, str)
|
|
171
147
|
plain
|
|
172
148
|
end
|
|
173
149
|
|
|
174
|
-
#
|
|
175
|
-
# @param huffman_offset [Integer] buffer offset where huffman string was introduced
|
|
176
|
-
# @return [String] binary string
|
|
150
|
+
# encodes the huffman string size from +buffer+ into the string at the offset indicated by +huffman_offset+
|
|
177
151
|
def set_huffman_size(buffer, huffman_offset)
|
|
178
152
|
integer(buffer.bytesize - huffman_offset, 7, buffer: buffer, offset: huffman_offset)
|
|
179
153
|
buffer.setbyte(huffman_offset, buffer.getbyte(huffman_offset) | 0x80)
|
|
@@ -5,32 +5,32 @@ module HTTP2
|
|
|
5
5
|
# Responsible for decoding received headers and maintaining compression
|
|
6
6
|
# context of the opposing peer. Decompressor must be initialized with
|
|
7
7
|
# appropriate starting context based on local role: client or server.
|
|
8
|
-
#
|
|
9
|
-
# @example
|
|
10
|
-
# server_role = Decompressor.new(:request)
|
|
11
|
-
# client_role = Decompressor.new(:response)
|
|
12
8
|
class Decompressor
|
|
13
9
|
include Error
|
|
14
10
|
include BufferUtils
|
|
15
11
|
|
|
16
12
|
FORBIDDEN_HEADERS = %w[connection te].freeze
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
attr_writer :max_header_list_size
|
|
15
|
+
|
|
16
|
+
def initialize(settings = Settings.new)
|
|
17
|
+
@cc = EncodingContext.new(settings)
|
|
18
|
+
@max_header_list_size = settings.settings_max_header_list_size
|
|
19
|
+
@accept_invalid_headers = false
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def table_size=(size)
|
|
26
|
-
@cc.table_size = size
|
|
22
|
+
def accept_invalid_headers!
|
|
23
|
+
@accept_invalid_headers = true
|
|
27
24
|
end
|
|
28
25
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
# Set the maximum dynamic table +size+ allowed by our acknowledged
|
|
27
|
+
# SETTINGS_HEADER_TABLE_SIZE. When the current table is larger, the
|
|
28
|
+
# next header block has to start with a dynamic table size update.
|
|
29
|
+
def max_table_size=(size)
|
|
30
|
+
@cc.max_table_size = size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Decodes integer value from provided +buffer+.
|
|
34
34
|
def integer(buf, n)
|
|
35
35
|
limit = (1 << n) - 1
|
|
36
36
|
if n.zero?
|
|
@@ -55,11 +55,9 @@ module HTTP2
|
|
|
55
55
|
i
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# Decodes string value from provided
|
|
58
|
+
# Decodes string value from provided +buf+.
|
|
59
59
|
#
|
|
60
|
-
#
|
|
61
|
-
# @return [String] UTF-8 encoded string
|
|
62
|
-
# @raise [CompressionError] when input is malformed
|
|
60
|
+
# raises CompressionError when input is malformed
|
|
63
61
|
def string(buf)
|
|
64
62
|
raise CompressionError, "invalid header block fragment" if buf.empty?
|
|
65
63
|
|
|
@@ -72,10 +70,7 @@ module HTTP2
|
|
|
72
70
|
str.force_encoding(Encoding::UTF_8)
|
|
73
71
|
end
|
|
74
72
|
|
|
75
|
-
# Decodes header command from provided
|
|
76
|
-
#
|
|
77
|
-
# @param buf [Buffer]
|
|
78
|
-
# @return [Hash] command
|
|
73
|
+
# Decodes header command from provided +buf+.
|
|
79
74
|
def header(buf)
|
|
80
75
|
peek = buf.getbyte(0)
|
|
81
76
|
|
|
@@ -109,13 +104,10 @@ module HTTP2
|
|
|
109
104
|
end
|
|
110
105
|
end
|
|
111
106
|
|
|
112
|
-
# Decodes and processes header commands within provided
|
|
113
|
-
#
|
|
114
|
-
# @param buf [Buffer]
|
|
115
|
-
# @param frame [HTTP2::Frame, nil]
|
|
116
|
-
# @return [Array] +[[name, value], ...]
|
|
107
|
+
# Decodes and processes header commands within provided +buf+.
|
|
117
108
|
def decode(buf, frame = nil)
|
|
118
|
-
list = []
|
|
109
|
+
list = [] #: Array[header_pair]
|
|
110
|
+
header_list_size = 0
|
|
119
111
|
decoding_pseudo_headers = true
|
|
120
112
|
@cc.listen_on_table do
|
|
121
113
|
until buf.empty?
|
|
@@ -123,12 +115,17 @@ module HTTP2
|
|
|
123
115
|
next if field.nil?
|
|
124
116
|
|
|
125
117
|
is_pseudo_header = field.start_with?(":")
|
|
126
|
-
if !decoding_pseudo_headers && is_pseudo_header
|
|
118
|
+
if !decoding_pseudo_headers && is_pseudo_header && !@accept_invalid_headers
|
|
127
119
|
raise ProtocolError, "one or more pseudo headers encountered after regular headers"
|
|
128
120
|
end
|
|
129
121
|
|
|
130
122
|
decoding_pseudo_headers = is_pseudo_header
|
|
131
|
-
|
|
123
|
+
if !@accept_invalid_headers && FORBIDDEN_HEADERS.include?(field)
|
|
124
|
+
raise ProtocolError, "invalid header received: #{field}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
header_list_size += field.bytesize + value.bytesize + 32
|
|
128
|
+
raise ProtocolError, "header list exceeds configured maximum" if header_list_size > @max_header_list_size
|
|
132
129
|
|
|
133
130
|
if frame
|
|
134
131
|
case field
|