plum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +7 -0
  6. data/LICENSE +21 -0
  7. data/README.md +14 -0
  8. data/Rakefile +12 -0
  9. data/bin/.gitkeep +0 -0
  10. data/examples/local_server.rb +206 -0
  11. data/examples/static_server.rb +157 -0
  12. data/lib/plum.rb +21 -0
  13. data/lib/plum/binary_string.rb +74 -0
  14. data/lib/plum/connection.rb +201 -0
  15. data/lib/plum/connection_utils.rb +38 -0
  16. data/lib/plum/errors.rb +35 -0
  17. data/lib/plum/event_emitter.rb +19 -0
  18. data/lib/plum/flow_control.rb +97 -0
  19. data/lib/plum/frame.rb +163 -0
  20. data/lib/plum/frame_factory.rb +53 -0
  21. data/lib/plum/frame_utils.rb +50 -0
  22. data/lib/plum/hpack/constants.rb +331 -0
  23. data/lib/plum/hpack/context.rb +55 -0
  24. data/lib/plum/hpack/decoder.rb +145 -0
  25. data/lib/plum/hpack/encoder.rb +105 -0
  26. data/lib/plum/hpack/huffman.rb +42 -0
  27. data/lib/plum/http_connection.rb +33 -0
  28. data/lib/plum/https_connection.rb +24 -0
  29. data/lib/plum/stream.rb +217 -0
  30. data/lib/plum/stream_utils.rb +58 -0
  31. data/lib/plum/version.rb +3 -0
  32. data/plum.gemspec +29 -0
  33. data/test/plum/connection/test_handle_frame.rb +70 -0
  34. data/test/plum/hpack/test_context.rb +63 -0
  35. data/test/plum/hpack/test_decoder.rb +291 -0
  36. data/test/plum/hpack/test_encoder.rb +49 -0
  37. data/test/plum/hpack/test_huffman.rb +36 -0
  38. data/test/plum/stream/test_handle_frame.rb +262 -0
  39. data/test/plum/test_binary_string.rb +64 -0
  40. data/test/plum/test_connection.rb +96 -0
  41. data/test/plum/test_connection_utils.rb +29 -0
  42. data/test/plum/test_error.rb +13 -0
  43. data/test/plum/test_flow_control.rb +167 -0
  44. data/test/plum/test_frame.rb +59 -0
  45. data/test/plum/test_frame_factory.rb +56 -0
  46. data/test/plum/test_frame_utils.rb +46 -0
  47. data/test/plum/test_https_connection.rb +37 -0
  48. data/test/plum/test_stream.rb +32 -0
  49. data/test/plum/test_stream_utils.rb +16 -0
  50. data/test/server.crt +19 -0
  51. data/test/server.csr +16 -0
  52. data/test/server.key +27 -0
  53. data/test/test_helper.rb +28 -0
  54. data/test/utils/assertions.rb +60 -0
  55. data/test/utils/server.rb +63 -0
  56. metadata +234 -0
@@ -0,0 +1,55 @@
1
+ module Plum
2
+ module HPACK
3
+ module Context
4
+ attr_reader :dynamic_table, :limit, :size
5
+
6
+ def limit=(value)
7
+ @limit = value
8
+ evict
9
+ end
10
+
11
+ private
12
+ def initialize(dynamic_table_limit)
13
+ @limit = dynamic_table_limit
14
+ @dynamic_table = []
15
+ @size = 0
16
+ end
17
+
18
+ def store(name, value)
19
+ @dynamic_table.unshift([name, value])
20
+ @size += name.bytesize + value.to_s.bytesize + 32
21
+ evict
22
+ end
23
+
24
+ def fetch(index)
25
+ if index == 0
26
+ raise HPACKError.new("index can't be 0")
27
+ elsif index <= STATIC_TABLE.size
28
+ STATIC_TABLE[index - 1]
29
+ elsif index <= STATIC_TABLE.size + @dynamic_table.size
30
+ @dynamic_table[index - STATIC_TABLE.size - 1]
31
+ else
32
+ raise HPACKError.new("invalid index: #{index}")
33
+ end
34
+ end
35
+
36
+ def search(name, value)
37
+ pr = proc {|n, v|
38
+ n == name && (!value || v == value)
39
+ }
40
+
41
+ si = STATIC_TABLE.index &pr
42
+ return si + 1 if si
43
+ di = @dynamic_table.index &pr
44
+ return di + STATIC_TABLE.size + 1 if di
45
+ end
46
+
47
+ def evict
48
+ while @limit && @size > @limit
49
+ name, value = @dynamic_table.pop
50
+ @size -= name.bytesize + value.to_s.bytesize + 32
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,145 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module HPACK
5
+ class Decoder
6
+ include HPACK::Context
7
+
8
+ def initialize(dynamic_table_limit)
9
+ super
10
+ end
11
+
12
+ def decode(str)
13
+ str = str.dup
14
+ headers = []
15
+ headers << parse!(str) while str.size > 0
16
+ headers.compact
17
+ end
18
+
19
+ private
20
+ def parse!(str)
21
+ first_byte = str.uint8
22
+ if first_byte >= 128 # 0b1XXXXXXX
23
+ parse_indexed!(str)
24
+ elsif first_byte >= 64 # 0b01XXXXXX
25
+ parse_indexing!(str)
26
+ elsif first_byte >= 32 # 0b001XXXXX
27
+ self.limit = read_integer!(str, 5)
28
+ nil
29
+ else # 0b0000XXXX (without indexing) or 0b0001XXXX (never indexing)
30
+ parse_no_indexing!(str)
31
+ end
32
+ end
33
+
34
+ def read_integer!(str, prefix_length)
35
+ first_byte = str.byteshift(1).uint8
36
+ raise HPACKError.new("integer: end of buffer") unless first_byte
37
+
38
+ mask = (1 << prefix_length) - 1
39
+ ret = first_byte & mask
40
+ return ret if ret < mask
41
+
42
+ octets = 0
43
+ while next_value = str.byteshift(1).uint8
44
+ ret += (next_value & 0b01111111) << (7 * octets)
45
+ octets += 1
46
+
47
+ if next_value < 128
48
+ return ret
49
+ elsif octets == 4 # RFC 7541 5.1 tells us that we MUST have limitation. at least > 2 ** 28
50
+ raise HPACKError.new("integer: too large integer")
51
+ end
52
+ end
53
+
54
+ raise HPACKError.new("integer: end of buffer")
55
+ end
56
+
57
+ def read_string!(str)
58
+ first_byte = str.uint8
59
+ raise HPACKError.new("string: end of buffer") unless first_byte
60
+
61
+ huffman = (first_byte >> 7) == 1
62
+ length = read_integer!(str, 7)
63
+ bin = str.byteshift(length)
64
+
65
+ raise HTTPError.new("string: end of buffer") if bin.bytesize < length
66
+ bin = Huffman.decode(bin) if huffman
67
+ bin
68
+ end
69
+
70
+ def parse_indexed!(str)
71
+ # indexed
72
+ # +---+---+---+---+---+---+---+---+
73
+ # | 1 | Index (7+) |
74
+ # +---+---------------------------+
75
+ index = read_integer!(str, 7)
76
+ fetch(index)
77
+ end
78
+
79
+ def parse_indexing!(str)
80
+ # +---+---+---+---+---+---+---+---+
81
+ # | 0 | 1 | Index (6+) |
82
+ # +---+---+-----------------------+
83
+ # | H | Value Length (7+) |
84
+ # +---+---------------------------+
85
+ # | Value String (Length octets) |
86
+ # +-------------------------------+
87
+ # or
88
+ # +---+---+---+---+---+---+---+---+
89
+ # | 0 | 1 | 0 |
90
+ # +---+---+-----------------------+
91
+ # | H | Name Length (7+) |
92
+ # +---+---------------------------+
93
+ # | Name String (Length octets) |
94
+ # +---+---------------------------+
95
+ # | H | Value Length (7+) |
96
+ # +---+---------------------------+
97
+ # | Value String (Length octets) |
98
+ # +-------------------------------+
99
+ index = read_integer!(str, 6)
100
+ if index == 0
101
+ name = read_string!(str)
102
+ else
103
+ name, = fetch(index)
104
+ end
105
+
106
+ val = read_string!(str)
107
+ store(name, val)
108
+
109
+ [name, val]
110
+ end
111
+
112
+ def parse_no_indexing!(str)
113
+ # +---+---+---+---+---+---+---+---+
114
+ # | 0 | 0 | 0 |0,1| Index (4+) |
115
+ # +---+---+-----------------------+
116
+ # | H | Value Length (7+) |
117
+ # +---+---------------------------+
118
+ # | Value String (Length octets) |
119
+ # +-------------------------------+
120
+ # or
121
+ # +---+---+---+---+---+---+---+---+
122
+ # | 0 | 0 | 0 |0,1| 0 |
123
+ # +---+---+-----------------------+
124
+ # | H | Name Length (7+) |
125
+ # +---+---------------------------+
126
+ # | Name String (Length octets) |
127
+ # +---+---------------------------+
128
+ # | H | Value Length (7+) |
129
+ # +---+---------------------------+
130
+ # | Value String (Length octets) |
131
+ # +-------------------------------+
132
+ index = read_integer!(str, 4)
133
+ if index == 0
134
+ name = read_string!(str)
135
+ else
136
+ name, = fetch(index)
137
+ end
138
+
139
+ val = read_string!(str)
140
+
141
+ [name, val]
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,105 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module HPACK
5
+ class Encoder
6
+ include HPACK::Context
7
+
8
+ def initialize(dynamic_table_limit)
9
+ super
10
+ end
11
+
12
+ def encode(headers)
13
+ out = ""
14
+ headers.each do |name, value|
15
+ name = name.to_s; value = value.to_s
16
+ if index = search(name, value)
17
+ out << encode_indexed(index)
18
+ elsif index = search(name, nil)
19
+ out << encode_half_indexed(index, value, true) # incremental indexing
20
+ else
21
+ out << encode_literal(name, value, true) # incremental indexing
22
+ end
23
+ end
24
+ out.force_encoding(Encoding::BINARY)
25
+ end
26
+
27
+ private
28
+ # +---+---+---+---+---+---+---+---+
29
+ # | 0 | 1 | 0 |
30
+ # +---+---+-----------------------+
31
+ # | H | Name Length (7+) |
32
+ # +---+---------------------------+
33
+ # | Name String (Length octets) |
34
+ # +---+---------------------------+
35
+ # | H | Value Length (7+) |
36
+ # +---+---------------------------+
37
+ # | Value String (Length octets) |
38
+ # +-------------------------------+
39
+ def encode_literal(name, value, indexing = true)
40
+ if indexing
41
+ store(name, value)
42
+ fb = "\x40"
43
+ else
44
+ fb = "\x00"
45
+ end
46
+ fb << encode_string(name) << encode_string(value)
47
+ end
48
+
49
+ # +---+---+---+---+---+---+---+---+
50
+ # | 0 | 1 | Index (6+) |
51
+ # +---+---+-----------------------+
52
+ # | H | Value Length (7+) |
53
+ # +---+---------------------------+
54
+ # | Value String (Length octets) |
55
+ # +-------------------------------+
56
+ def encode_half_indexed(index, value, indexing = true)
57
+ if indexing
58
+ store(fetch(index)[0], value)
59
+ fb = encode_integer(index, 6)
60
+ fb.setbyte(0, fb.uint8 | 0b01000000)
61
+ else
62
+ fb = encode_integer(index, 4)
63
+ end
64
+ fb << encode_string(value)
65
+ end
66
+
67
+ # +---+---+---+---+---+---+---+---+
68
+ # | 1 | Index (7+) |
69
+ # +---+---------------------------+
70
+ def encode_indexed(index)
71
+ s = encode_integer(index, 7)
72
+ s.setbyte(0, s.uint8 | 0b10000000)
73
+ s
74
+ end
75
+
76
+ def encode_integer(value, prefix_length)
77
+ mask = (1 << prefix_length) - 1
78
+ out = ""
79
+
80
+ if value < mask
81
+ out.push_uint8(value)
82
+ else
83
+ value -= mask
84
+ out.push_uint8(mask)
85
+ while value >= mask
86
+ out.push_uint8((value % 0b10000000) + 0b10000000)
87
+ value >>= 7
88
+ end
89
+ out.push_uint8(value)
90
+ end
91
+ end
92
+
93
+ def encode_string(str)
94
+ huffman_str = Huffman.encode(str).force_encoding(__ENCODING__)
95
+ if huffman_str.bytesize < str.bytesize
96
+ lenstr = encode_integer(huffman_str.bytesize, 7)
97
+ lenstr.setbyte(0, lenstr.uint8(0) | 0b10000000)
98
+ lenstr << huffman_str
99
+ else
100
+ encode_integer(str.bytesize, 7) << str
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,42 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module HPACK
5
+ module Huffman
6
+ extend self
7
+
8
+ # Static-Huffman-encodes the specified String.
9
+ def encode(bytestr)
10
+ out = ""
11
+ bytestr.each_byte do |b|
12
+ out << HUFFMAN_TABLE[b]
13
+ end
14
+ out << "1" * ((8 - out.bytesize) % 8)
15
+ [out].pack("B*")
16
+ end
17
+
18
+ # Static-Huffman-decodes the specified String.
19
+ def decode(encoded)
20
+ bits = encoded.unpack("B*")[0]
21
+ out = []
22
+ buf = ""
23
+ bits.each_char do |cb|
24
+ buf << cb
25
+ if c = HUFFMAN_TABLE_INVERSED[buf]
26
+ raise HPACKError.new("huffman: EOS detected") if c == 256
27
+ out << c
28
+ buf = ""
29
+ end
30
+ end
31
+
32
+ if buf.bytesize > 7
33
+ raise HPACKError.new("huffman: padding is too large (> 7 bits)")
34
+ elsif buf != "1" * buf.bytesize
35
+ raise HPACKError.new("huffman: unknown suffix: #{buf}")
36
+ else
37
+ out.pack("C*")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ module Plum
2
+ class HTTPConnection < Connection
3
+ def initialize(io, local_settings = {})
4
+ super
5
+ end
6
+
7
+ private
8
+ def negotiate!
9
+ if @buffer.bytesize >= 4
10
+ if CLIENT_CONNECTION_PREFACE.start_with?(@buffer)
11
+ negotiate_with_knowledge
12
+ else
13
+ negotiate_with_upgrade
14
+ end
15
+ end
16
+ # next
17
+ end
18
+
19
+ def negotiate_with_knowledge
20
+ if @buffer.bytesize >= 24
21
+ if @buffer.byteshift(24) == CLIENT_CONNECTION_PREFACE
22
+ @state = :waiting_settings
23
+ settings(@local_settings)
24
+ end
25
+ end
26
+ # next
27
+ end
28
+
29
+ def negotiate_with_upgrade
30
+ raise NotImplementedError, "Parsing HTTP/1.1 is hard..."
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ class HTTPSConnection < Connection
5
+ def initialize(io, local_settings = {})
6
+ super
7
+ end
8
+
9
+ private
10
+ def negotiate!
11
+ return if @buffer.empty?
12
+
13
+ if CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
14
+ if @buffer.bytesize >= 24
15
+ @buffer.byteshift(24)
16
+ @state = :waiting_settings
17
+ settings(@local_settings)
18
+ end
19
+ else
20
+ raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,217 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ class Stream
5
+ include EventEmitter
6
+ include FlowControl
7
+ include StreamUtils
8
+
9
+ attr_reader :id, :state, :connection
10
+ attr_reader :weight, :exclusive
11
+ attr_accessor :parent
12
+
13
+ def initialize(con, id, state: :idle, weight: 16, parent: nil, exclusive: false)
14
+ @connection = con
15
+ @id = id
16
+ @state = state
17
+ @continuation = []
18
+
19
+ initialize_flow_control(send: @connection.remote_settings[:initial_window_size],
20
+ recv: @connection.local_settings[:initial_window_size])
21
+ update_dependency(weight: weight, parent: parent, exclusive: exclusive)
22
+ end
23
+
24
+ # Returns the child (depending on this stream) streams.
25
+ #
26
+ # @return [Array<Stream>] The child streams.
27
+ def children
28
+ @connection.streams.values.select {|c| c.parent == self }.freeze
29
+ end
30
+
31
+ # Processes received frames for this stream. Internal use.
32
+ # @private
33
+ def receive_frame(frame)
34
+ validate_received_frame(frame)
35
+ consume_recv_window(frame)
36
+
37
+ case frame.type
38
+ when :data
39
+ receive_data(frame)
40
+ when :headers
41
+ receive_headers(frame)
42
+ when :priority
43
+ receive_priority(frame)
44
+ when :rst_stream
45
+ receive_rst_stream(frame)
46
+ when :window_update
47
+ receive_window_update(frame)
48
+ when :continuation
49
+ receive_continuation(frame)
50
+ when :ping, :goaway, :settings, :push_promise
51
+ raise ConnectionError.new(:protocol_error) # stream_id MUST be 0x00
52
+ else
53
+ # MUST ignore unknown frame
54
+ end
55
+ rescue StreamError => e
56
+ callback(:stream_error, e)
57
+ close(e.http2_error_type)
58
+ end
59
+
60
+ # Closes this stream. Sends RST_STREAM frame to the peer.
61
+ #
62
+ # @param error_type [Symbol] The error type to be contained in the RST_STREAM frame.
63
+ def close(error_type = :no_error)
64
+ @state = :closed
65
+ send_immediately Frame.rst_stream(id, error_type)
66
+ end
67
+
68
+ private
69
+ def send_immediately(frame)
70
+ callback(:send_frame, frame)
71
+ @connection.send(frame)
72
+ end
73
+
74
+ def update_dependency(weight: nil, parent: nil, exclusive: nil)
75
+ raise StreamError.new(:protocol_error, "A stream cannot depend on itself.") if parent == self
76
+ @weight = weight unless weight.nil?
77
+ @parent = parent unless parent.nil?
78
+ @exclusive = exclusive unless exclusive.nil?
79
+
80
+ if exclusive == true
81
+ parent.children.each do |child|
82
+ next if child == self
83
+ child.parent = self
84
+ end
85
+ end
86
+ end
87
+
88
+ def validate_received_frame(frame)
89
+ if frame.length > @connection.local_settings[:max_frame_size]
90
+ if [:headers, :push_promise, :continuation].include?(frame.type)
91
+ raise ConnectionError.new(:frame_size_error)
92
+ else
93
+ raise StreamError.new(:frame_size_error)
94
+ end
95
+ end
96
+ end
97
+
98
+ def receive_end_stream
99
+ callback(:end_stream)
100
+ @state = :half_closed_remote
101
+ end
102
+
103
+ def receive_data(frame)
104
+ if @state != :open && @state != :half_closed_local
105
+ raise StreamError.new(:stream_closed)
106
+ end
107
+
108
+ if frame.flags.include?(:padded)
109
+ padding_length = frame.payload.uint8(0)
110
+ if padding_length >= frame.length
111
+ raise ConnectionError.new(:protocol_error, "padding is too long")
112
+ end
113
+ body = frame.payload.byteslice(1, frame.length - padding_length - 1)
114
+ else
115
+ body = frame.payload
116
+ end
117
+ callback(:data, body)
118
+
119
+ receive_end_stream if frame.flags.include?(:end_stream)
120
+ end
121
+
122
+ def receive_complete_headers(frames)
123
+ frames = frames.dup
124
+ first = frames.shift
125
+
126
+ payload = first.payload
127
+ first_length = first.length
128
+ padding_length = 0
129
+
130
+ if first.flags.include?(:padded)
131
+ padding_length = payload.uint8
132
+ first_length -= 1 + padding_length
133
+ payload = payload.byteslice(1, first_length)
134
+ else
135
+ payload = payload.dup
136
+ end
137
+
138
+ if first.flags.include?(:priority)
139
+ receive_priority_payload(payload.byteshift(5))
140
+ first_length -= 5
141
+ end
142
+
143
+ if padding_length > first_length
144
+ raise ConnectionError.new(:protocol_error, "padding is too long")
145
+ end
146
+
147
+ frames.each do |frame|
148
+ payload << frame.payload
149
+ end
150
+
151
+ begin
152
+ decoded_headers = @connection.hpack_decoder.decode(payload)
153
+ rescue => e
154
+ raise ConnectionError.new(:compression_error, e)
155
+ end
156
+
157
+ callback(:headers, decoded_headers)
158
+
159
+ receive_end_stream if first.flags.include?(:end_stream)
160
+ end
161
+
162
+ def receive_headers(frame)
163
+ if @state == :reserved_local
164
+ raise ConnectionError.new(:protocol_error)
165
+ elsif @state == :half_closed_remote
166
+ raise StreamError.new(:stream_closed)
167
+ elsif @state == :closed
168
+ raise ConnectionError.new(:stream_closed)
169
+ end
170
+
171
+ @state = :open
172
+ callback(:open)
173
+
174
+ if frame.flags.include?(:end_headers)
175
+ receive_complete_headers([frame])
176
+ else
177
+ @continuation << frame
178
+ end
179
+ end
180
+
181
+ def receive_continuation(frame)
182
+ # state error mustn't happen: server_connection validates
183
+ @continuation << frame
184
+
185
+ if frame.flags.include?(:end_headers)
186
+ receive_complete_headers(@continuation)
187
+ @continuation.clear
188
+ end
189
+ end
190
+
191
+ def receive_priority(frame)
192
+ if frame.length != 5
193
+ raise StreamError.new(:frame_size_error)
194
+ end
195
+ receive_priority_payload(frame.payload)
196
+ end
197
+
198
+ def receive_priority_payload(payload)
199
+ esd = payload.uint32
200
+ e = esd >> 31
201
+ dependency_id = e & ~(1 << 31)
202
+ weight = payload.uint8(4)
203
+
204
+ update_dependency(weight: weight, parent: @connection.streams[dependency_id], exclusive: e == 1)
205
+ end
206
+
207
+ def receive_rst_stream(frame)
208
+ if frame.length != 4
209
+ raise ConnectionError.new(:frame_size_error)
210
+ elsif @state == :idle
211
+ raise ConnectionError.new(:protocol_error)
212
+ end
213
+
214
+ @state = :closed # MUST NOT send RST_STREAM
215
+ end
216
+ end
217
+ end