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
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