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
data/lib/plum.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "openssl"
2
+ require "socket"
3
+ require "plum/version"
4
+ require "plum/errors"
5
+ require "plum/binary_string"
6
+ require "plum/event_emitter"
7
+ require "plum/hpack/constants"
8
+ require "plum/hpack/huffman"
9
+ require "plum/hpack/context"
10
+ require "plum/hpack/decoder"
11
+ require "plum/hpack/encoder"
12
+ require "plum/frame_utils"
13
+ require "plum/frame_factory"
14
+ require "plum/frame"
15
+ require "plum/flow_control"
16
+ require "plum/connection_utils"
17
+ require "plum/connection"
18
+ require "plum/https_connection"
19
+ require "plum/http_connection"
20
+ require "plum/stream_utils"
21
+ require "plum/stream"
@@ -0,0 +1,74 @@
1
+ module Plum
2
+ module BinaryString
3
+ refine String do
4
+ # Reads a 8-bit unsigned integer.
5
+ # @param pos [Integer] The start position to read.
6
+ def uint8(pos = 0)
7
+ byteslice(pos, 1).unpack("C")[0]
8
+ end
9
+
10
+ # Reads a 16-bit unsigned integer.
11
+ # @param pos [Integer] The start position to read.
12
+ def uint16(pos = 0)
13
+ byteslice(pos, 2).unpack("n")[0]
14
+ end
15
+
16
+ # Reads a 24-bit unsigned integer.
17
+ # @param pos [Integer] The start position to read.
18
+ def uint24(pos = 0)
19
+ (uint16(pos) << 8) | uint8(pos + 2)
20
+ end
21
+
22
+ # Reads a 32-bit unsigned integer.
23
+ # @param pos [Integer] The start position to read.
24
+ def uint32(pos = 0)
25
+ byteslice(pos, 4).unpack("N")[0]
26
+ end
27
+
28
+ # Appends a 8-bit unsigned integer to this string.
29
+ def push_uint8(val)
30
+ self << [val].pack("C")
31
+ end
32
+
33
+ # Appends a 16-bit unsigned integer to this string.
34
+ def push_uint16(val)
35
+ self << [val].pack("n")
36
+ end
37
+
38
+ # Appends a 24-bit unsigned integer to this string.
39
+ def push_uint24(val)
40
+ push_uint16(val >> 8)
41
+ push_uint8(val & ((1 << 8) - 1))
42
+ end
43
+
44
+ # Appends a 32-bit unsigned integer to this string.
45
+ def push_uint32(val)
46
+ self << [val].pack("N")
47
+ end
48
+
49
+ alias push <<
50
+
51
+ # Takes from beginning and cut specified *octets* from this String.
52
+ # @param count [Integer] The amount.
53
+ def byteshift(count)
54
+ force_encoding(Encoding::BINARY)
55
+ slice!(0, count)
56
+ end
57
+
58
+ def each_byteslice(n, &blk)
59
+ if block_given?
60
+ pos = 0
61
+ while pos < self.bytesize
62
+ yield byteslice(pos, n)
63
+ pos += n
64
+ end
65
+ else
66
+ Enumerator.new do |y|
67
+ each_byteslice(n) {|ss| y << ss }
68
+ end
69
+ # I want to write `enum_for(__method__, n)`!
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,201 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ class Connection
5
+ include EventEmitter
6
+ include FlowControl
7
+ include ConnectionUtils
8
+
9
+ CLIENT_CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
10
+
11
+ DEFAULT_SETTINGS = {
12
+ header_table_size: 4096, # octets
13
+ enable_push: 1, # 1: enabled, 0: disabled
14
+ max_concurrent_streams: 1 << 30, # (1 << 31) / 2
15
+ initial_window_size: 65535, # octets; <= 2 ** 31 - 1
16
+ max_frame_size: 16384, # octets; <= 2 ** 24 - 1
17
+ max_header_list_size: (1 << 32) - 1 # Fixnum
18
+ }
19
+
20
+ attr_reader :hpack_encoder, :hpack_decoder
21
+ attr_reader :local_settings, :remote_settings
22
+ attr_reader :state, :streams, :io
23
+
24
+ def initialize(io, local_settings = {})
25
+ @io = io
26
+ @local_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }.merge!(local_settings)
27
+ @remote_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }
28
+ @buffer = "".force_encoding(Encoding::BINARY)
29
+ @streams = {}
30
+ @state = :negotiation
31
+ @hpack_decoder = HPACK::Decoder.new(@local_settings[:header_table_size])
32
+ @hpack_encoder = HPACK::Encoder.new(@remote_settings[:header_table_size])
33
+ initialize_flow_control(send: @remote_settings[:initial_window_size],
34
+ recv: @local_settings[:initial_window_size])
35
+ end
36
+ private :initialize
37
+
38
+ # Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
39
+ def run
40
+ while !@io.closed? && !@io.eof?
41
+ receive @io.readpartial(1024)
42
+ end
43
+ end
44
+
45
+ # Closes the io.
46
+ def close
47
+ # TODO: server MAY wait streams
48
+ @io.close
49
+ end
50
+
51
+ # Receives the specified data and process.
52
+ #
53
+ # @param new_data [String] The data received from the peer.
54
+ def receive(new_data)
55
+ return if new_data.empty?
56
+ @buffer << new_data
57
+
58
+ if @state == :negotiation
59
+ negotiate!
60
+ end
61
+
62
+ if @state != :negotiation
63
+ while frame = Frame.parse!(@buffer)
64
+ callback(:frame, frame)
65
+ receive_frame(frame)
66
+ end
67
+ end
68
+ rescue ConnectionError => e
69
+ callback(:connection_error, e)
70
+ goaway(e.http2_error_type)
71
+ close
72
+ end
73
+ alias << receive
74
+
75
+ # Reserves a new stream to server push.
76
+ #
77
+ # @param args [Hash] The argument to pass to Stram.new.
78
+ def reserve_stream(**args)
79
+ next_id = ((@streams.keys.last / 2).to_i + 1) * 2
80
+ stream = new_stream(next_id, state: :reserved_local, **args)
81
+ stream
82
+ end
83
+
84
+ private
85
+ def send_immediately(frame)
86
+ callback(:send_frame, frame)
87
+ @io.write(frame.assemble)
88
+ end
89
+
90
+ def new_stream(stream_id, **args)
91
+ if @streams.size > 0 && @streams.keys.last >= stream_id
92
+ raise Plum::ConnectionError.new(:protocol_error)
93
+ end
94
+
95
+ stream = Stream.new(self, stream_id, **args)
96
+ callback(:stream, stream)
97
+ @streams[stream_id] = stream
98
+ stream
99
+ end
100
+
101
+ def validate_received_frame(frame)
102
+ case @state
103
+ when :waiting_settings
104
+ raise ConnectionError.new(:protocol_error) if frame.type != :settings
105
+ @state = :negotiated
106
+ callback(:negotiated)
107
+ when :waiting_continuation
108
+ if frame.type != :continuation || frame.stream_id != @continuation_id
109
+ raise Plum::ConnectionError.new(:protocol_error)
110
+ end
111
+
112
+ if frame.flags.include?(:end_headers)
113
+ @state = :open
114
+ @continuation_id = nil
115
+ end
116
+ else
117
+ if [:headers].include?(frame.type)
118
+ if !frame.flags.include?(:end_headers)
119
+ @state = :waiting_continuation
120
+ @continuation_id = frame.stream_id
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def receive_frame(frame)
127
+ validate_received_frame(frame)
128
+ consume_recv_window(frame)
129
+
130
+ if frame.stream_id == 0
131
+ receive_control_frame(frame)
132
+ else
133
+ if @streams.key?(frame.stream_id)
134
+ stream = @streams[frame.stream_id]
135
+ else
136
+ if frame.stream_id.even? # stream started by client must have odd ID
137
+ raise Plum::ConnectionError.new(:protocol_error)
138
+ end
139
+ stream = new_stream(frame.stream_id)
140
+ end
141
+ stream.receive_frame(frame)
142
+ end
143
+ end
144
+
145
+ def receive_control_frame(frame)
146
+ if frame.length > @local_settings[:max_frame_size]
147
+ raise ConnectionError.new(:frame_size_error)
148
+ end
149
+
150
+ case frame.type
151
+ when :settings
152
+ receive_settings(frame)
153
+ when :window_update
154
+ receive_window_update(frame)
155
+ when :ping
156
+ receive_ping(frame)
157
+ when :goaway
158
+ goaway
159
+ close
160
+ when :data, :headers, :priority, :rst_stream, :push_promise, :continuation
161
+ raise Plum::ConnectionError.new(:protocol_error)
162
+ else
163
+ # MUST ignore unknown frame type.
164
+ end
165
+ end
166
+
167
+ def receive_settings(frame)
168
+ if frame.flags.include?(:ack)
169
+ raise ConnectionError.new(:frame_size_error) if frame.length != 0
170
+ return
171
+ end
172
+
173
+ raise ConnectionError.new(:frame_size_error) if frame.length % 6 != 0
174
+
175
+ old_remote_settings = @remote_settings.dup
176
+ @remote_settings.merge!(frame.parse_settings)
177
+ apply_remote_settings(old_remote_settings)
178
+
179
+ callback(:remote_settings, @remote_settings, old_remote_settings)
180
+
181
+ send_immediately Frame.settings(:ack)
182
+ end
183
+
184
+ def apply_remote_settings(old_remote_settings)
185
+ @hpack_encoder.limit = @remote_settings[:header_table_size]
186
+ update_send_initial_window_size(@remote_settings[:initial_window_size] - old_remote_settings[:initial_window_size])
187
+ end
188
+
189
+ def receive_ping(frame)
190
+ raise Plum::ConnectionError.new(:frame_size_error) if frame.length != 8
191
+
192
+ if frame.flags.include?(:ack)
193
+ on(:ping_ack)
194
+ else
195
+ on(:ping)
196
+ opaque_data = frame.payload
197
+ send_immediately Frame.ping(:ack, opaque_data)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,38 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module ConnectionUtils
5
+ # Sends local settings to the peer.
6
+ #
7
+ # @param kwargs [Hash<Symbol, Integer>]
8
+ def settings(**kwargs)
9
+ send_immediately Frame.settings(**kwargs)
10
+ update_local_settings(kwargs)
11
+ end
12
+
13
+ # Sends a PING frame to the peer.
14
+ #
15
+ # @param data [String] Must be 8 octets.
16
+ # @raise [ArgumentError] If the data is not 8 octets.
17
+ def ping(data = "plum\x00\x00\x00\x00")
18
+ send_immediately Frame.ping(data)
19
+ end
20
+
21
+ # Sends GOAWAY frame to the peer and closes the connection.
22
+ #
23
+ # @param error_type [Symbol] The error type to be contained in the GOAWAY frame.
24
+ def goaway(error_type = :no_error)
25
+ last_id = @streams.keys.max || 0
26
+ send_immediately Frame.goaway(last_id, error_type)
27
+ end
28
+
29
+ private
30
+ def update_local_settings(new_settings)
31
+ old_settings = @local_settings.dup
32
+ @local_settings.merge!(new_settings)
33
+
34
+ @hpack_decoder.limit = @local_settings[:header_table_size]
35
+ update_recv_initial_window_size(@local_settings[:initial_window_size] - old_settings[:initial_window_size])
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ module Plum
2
+ class Error < StandardError; end
3
+ class HPACKError < Error; end
4
+ class HTTPError < Error
5
+ ERROR_CODES = {
6
+ no_error: 0x00,
7
+ protocol_error: 0x01,
8
+ internal_error: 0x02,
9
+ flow_control_error: 0x03,
10
+ settings_timeout: 0x04,
11
+ stream_closed: 0x05,
12
+ frame_size_error: 0x06,
13
+ refused_stream: 0x07,
14
+ cancel: 0x08,
15
+ compression_error: 0x09,
16
+ connect_error: 0x0a,
17
+ enhance_your_calm: 0x0b,
18
+ inadequate_security: 0x0c,
19
+ http_1_1_required: 0x0d
20
+ }
21
+
22
+ attr_reader :http2_error_type
23
+
24
+ def initialize(type, message = nil)
25
+ @http2_error_type = type
26
+ super(message)
27
+ end
28
+
29
+ def http2_error_code
30
+ ERROR_CODES[@http2_error_type]
31
+ end
32
+ end
33
+ class ConnectionError < HTTPError; end
34
+ class StreamError < HTTPError; end
35
+ end
@@ -0,0 +1,19 @@
1
+ module Plum
2
+ module EventEmitter
3
+ # Registers an event handler to specified event. An event can have multiple handlers.
4
+ # @param name [String] The name of event.
5
+ # @yield Gives event-specific parameters.
6
+ def on(name, &blk)
7
+ callbacks[name] << blk
8
+ end
9
+
10
+ private
11
+ def callback(name, *args)
12
+ callbacks[name].each {|cb| cb.call(*args) }
13
+ end
14
+
15
+ def callbacks
16
+ @callbacks ||= Hash.new {|hash, key| hash[key] = [] }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,97 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module FlowControl
5
+ attr_reader :send_remaining_window, :recv_remaining_window
6
+
7
+ # Sends frame respecting inner-stream flow control.
8
+ #
9
+ # @param frame [Frame] The frame to be sent.
10
+ def send(frame)
11
+ case frame.type
12
+ when :data
13
+ @send_buffer << frame
14
+ callback(:send_deferred, frame) if @send_remaining_window < frame.length
15
+ consume_send_buffer
16
+ else
17
+ send_immediately frame
18
+ end
19
+ end
20
+
21
+ # Increases receiving window size. Sends WINDOW_UPDATE frame to the peer.
22
+ #
23
+ # @param wsi [Integer] The amount to increase receiving window size. The legal range is 1 to 2^32-1.
24
+ def window_update(wsi)
25
+ @recv_remaining_window += wsi
26
+ payload = "".push_uint32(wsi & ~(1 << 31))
27
+ sid = (Stream === self) ? self.id : 0
28
+ send_immediately Frame.new(type: :window_update, stream_id: sid, payload: payload)
29
+ end
30
+
31
+ protected
32
+ def update_send_initial_window_size(diff)
33
+ @send_remaining_window += diff
34
+ consume_send_buffer
35
+
36
+ if Connection === self
37
+ @streams.values.each do |stream|
38
+ stream.update_send_initial_window_size(diff)
39
+ end
40
+ end
41
+ end
42
+
43
+ def update_recv_initial_window_size(diff)
44
+ @recv_remaining_window += diff
45
+ if Connection === self
46
+ @streams.values.each do |stream|
47
+ stream.update_recv_initial_window_size(diff)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+ def initialize_flow_control(send:, recv:)
54
+ @send_buffer = []
55
+ @send_remaining_window = send
56
+ @recv_remaining_window = recv
57
+ end
58
+
59
+ def consume_recv_window(frame)
60
+ case frame.type
61
+ when :data
62
+ @recv_remaining_window -= frame.length
63
+ if @recv_remaining_window < 0
64
+ local_error = (Connection === self) ? ConnectionError : StreamError
65
+ raise local_error.new(:flow_control_error)
66
+ end
67
+ end
68
+ end
69
+
70
+ def consume_send_buffer
71
+ while frame = @send_buffer.first
72
+ break if frame.length > @send_remaining_window
73
+ @send_buffer.shift
74
+ @send_remaining_window -= frame.length
75
+ send_immediately frame
76
+ end
77
+ end
78
+
79
+ def receive_window_update(frame)
80
+ if frame.length != 4
81
+ raise Plum::ConnectionError.new(:frame_size_error)
82
+ end
83
+
84
+ r_wsi = frame.payload.uint32
85
+ r = r_wsi >> 31
86
+ wsi = r_wsi & ~(1 << 31)
87
+
88
+ if wsi == 0
89
+ local_error = (Connection === self) ? ConnectionError : StreamError
90
+ raise local_error.new(:protocol_error)
91
+ end
92
+
93
+ @send_remaining_window += wsi
94
+ consume_send_buffer
95
+ end
96
+ end
97
+ end