plum 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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