plum 0.0.3 → 0.1.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.
@@ -2,11 +2,19 @@ using Plum::BinaryString
2
2
 
3
3
  module Plum
4
4
  module FrameFactory
5
+ # Creates a RST_STREAM frame.
6
+ # @param stream_id [Integer] The stream ID.
7
+ # @param error_type [Symbol] The error type defined in RFC 7540 Section 7.
5
8
  def rst_stream(stream_id, error_type)
6
9
  payload = "".push_uint32(HTTPError::ERROR_CODES[error_type])
7
10
  Frame.new(type: :rst_stream, stream_id: stream_id, payload: payload)
8
11
  end
9
12
 
13
+ # Creates a GOAWAY frame.
14
+ # @param last_id [Integer] The biggest processed stream ID.
15
+ # @param error_type [Symbol] The error type defined in RFC 7540 Section 7.
16
+ # @param message [String] Additional debug data.
17
+ # @see RFC 7540 Section 6.8
10
18
  def goaway(last_id, error_type, message = "")
11
19
  payload = "".push_uint32((last_id || 0) | (0 << 31))
12
20
  .push_uint32(HTTPError::ERROR_CODES[error_type])
@@ -14,15 +22,24 @@ module Plum
14
22
  Frame.new(type: :goaway, stream_id: 0, payload: payload)
15
23
  end
16
24
 
25
+ # Creates a SETTINGS frame.
26
+ # @param ack [Symbol] Pass :ack to create an ACK frame.
27
+ # @param args [Hash<Symbol, Integer>] The settings values to send.
17
28
  def settings(ack = nil, **args)
18
29
  payload = args.inject("") {|payload, (key, value)|
19
30
  id = Frame::SETTINGS_TYPE[key] or raise ArgumentError.new("invalid settings type")
20
31
  payload.push_uint16(id)
21
32
  payload.push_uint32(value)
22
33
  }
23
- Frame.new(type: :settings, stream_id: 0, flags: [ack].compact, payload: payload)
34
+ Frame.new(type: :settings, stream_id: 0, flags: [ack], payload: payload)
24
35
  end
25
36
 
37
+ # Creates a PING frame.
38
+ # @overload ping(ack, payload)
39
+ # @param ack [Symbol] Pass :ack to create an ACK frame.
40
+ # @param payload [String] 8 bytes length data to send.
41
+ # @overload ping(payload = "plum\x00\x00\x00\x00")
42
+ # @param payload [String] 8 bytes length data to send.
26
43
  def ping(arg1 = "plum\x00\x00\x00\x00", arg2 = nil)
27
44
  if !arg2
28
45
  raise ArgumentError.new("data must be 8 octets") if arg1.bytesize != 8
@@ -32,22 +49,39 @@ module Plum
32
49
  end
33
50
  end
34
51
 
52
+ # Creates a DATA frame.
53
+ # @param stream_id [Integer] The stream ID.
54
+ # @param payload [String] Payload.
55
+ # @param flags [Array<Symbol>] Flags.
35
56
  def data(stream_id, payload, *flags)
36
- Frame.new(type: :data, stream_id: stream_id, flags: flags.compact, payload: payload.to_s)
57
+ Frame.new(type: :data, stream_id: stream_id, flags: flags, payload: payload)
37
58
  end
38
59
 
60
+ # Creates a DATA frame.
61
+ # @param stream_id [Integer] The stream ID.
62
+ # @param encoded [String] Headers.
63
+ # @param flags [Array<Symbol>] Flags.
39
64
  def headers(stream_id, encoded, *flags)
40
- Frame.new(type: :headers, stream_id: stream_id, flags: flags.compact, payload: encoded)
65
+ Frame.new(type: :headers, stream_id: stream_id, flags: flags, payload: encoded)
41
66
  end
42
67
 
68
+ # Creates a PUSH_PROMISE frame.
69
+ # @param stream_id [Integer] The stream ID.
70
+ # @param new_id [Integer] The stream ID to create.
71
+ # @param encoded [String] Request headers.
72
+ # @param flags [Array<Symbol>] Flags.
43
73
  def push_promise(stream_id, new_id, encoded, *flags)
44
74
  payload = "".push_uint32(0 << 31 | new_id)
45
75
  .push(encoded)
46
- Frame.new(type: :push_promise, stream_id: stream_id, flags: flags.compact, payload: payload)
76
+ Frame.new(type: :push_promise, stream_id: stream_id, flags: flags, payload: payload)
47
77
  end
48
78
 
79
+ # Creates a CONTINUATION frame.
80
+ # @param stream_id [Integer] The stream ID.
81
+ # @param payload [String] Payload.
82
+ # @param flags [Array<Symbol>] Flags.
49
83
  def continuation(stream_id, payload, *flags)
50
- Frame.new(type: :continuation, stream_id: stream_id, flags: flags.compact, payload: payload)
84
+ Frame.new(type: :continuation, stream_id: stream_id, flags: flags, payload: payload)
51
85
  end
52
86
  end
53
87
  end
@@ -3,7 +3,6 @@ using Plum::BinaryString
3
3
  module Plum
4
4
  module FrameUtils
5
5
  # Splits the DATA frame into multiple frames if the payload size exceeds max size.
6
- #
7
6
  # @param max [Integer] The maximum size of a frame payload.
8
7
  # @return [Array<Frame>] The splitted frames.
9
8
  def split_data(max)
@@ -18,7 +17,6 @@ module Plum
18
17
  end
19
18
 
20
19
  # Splits the HEADERS or PUSH_PROMISE frame into multiple frames if the payload size exceeds max size.
21
- #
22
20
  # @param max [Integer] The maximum size of a frame payload.
23
21
  # @return [Array<Frame>] The splitted frames.
24
22
  def split_headers(max)
@@ -34,7 +32,6 @@ module Plum
34
32
  end
35
33
 
36
34
  # Parses SETTINGS frame payload. Ignores unknown settings type (see RFC7540 6.5.2).
37
- #
38
35
  # @return [Hash<Symbol, Integer>] The parsed strings.
39
36
  def parse_settings
40
37
  settings = {}
@@ -3,7 +3,7 @@ module Plum
3
3
  # RFC7541 Appendix A
4
4
  # index is starting from 0
5
5
  STATIC_TABLE = [
6
- [":authority"],
6
+ [":authority", ""],
7
7
  [":method", "GET"],
8
8
  [":method", "POST"],
9
9
  [":path", "/"],
@@ -19,52 +19,52 @@ module Plum
19
19
  [":status", "500"],
20
20
  ["accept-charset"],
21
21
  ["accept-encoding", "gzip, deflate"],
22
- ["accept-language"],
23
- ["accept-ranges"],
24
- ["accept"],
25
- ["access-control-allow-origin"],
26
- ["age"],
27
- ["allow"],
28
- ["authorization"],
29
- ["cache-control"],
30
- ["content-disposition"],
31
- ["content-encoding"],
32
- ["content-language"],
33
- ["content-length"],
34
- ["content-location"],
35
- ["content-range"],
36
- ["content-type"],
37
- ["cookie"],
38
- ["date"],
39
- ["etag"],
40
- ["expect"],
41
- ["expires"],
42
- ["from"],
43
- ["host"],
44
- ["if-match"],
45
- ["if-modified-since"],
46
- ["if-none-match"],
47
- ["if-range"],
48
- ["if-unmodified-since"],
49
- ["last-modified"],
50
- ["link"],
51
- ["location"],
52
- ["max-forwards"],
53
- ["proxy-authenticate"],
54
- ["proxy-authorization"],
55
- ["range"],
56
- ["referer"],
57
- ["refresh"],
58
- ["retry-after"],
59
- ["server"],
60
- ["set-cookie"],
61
- ["strict-transport-security"],
62
- ["transfer-encoding"],
63
- ["user-agent"],
64
- ["vary"],
65
- ["via"],
66
- ["www-authenticate"],
67
- ]
22
+ ["accept-language", ""],
23
+ ["accept-ranges", ""],
24
+ ["accept", ""],
25
+ ["access-control-allow-origin", ""],
26
+ ["age", ""],
27
+ ["allow", ""],
28
+ ["authorization", ""],
29
+ ["cache-control", ""],
30
+ ["content-disposition", ""],
31
+ ["content-encoding", ""],
32
+ ["content-language", ""],
33
+ ["content-length", ""],
34
+ ["content-location", ""],
35
+ ["content-range", ""],
36
+ ["content-type", ""],
37
+ ["cookie", ""],
38
+ ["date", ""],
39
+ ["etag", ""],
40
+ ["expect", ""],
41
+ ["expires", ""],
42
+ ["from", ""],
43
+ ["host", ""],
44
+ ["if-match", ""],
45
+ ["if-modified-since", ""],
46
+ ["if-none-match", ""],
47
+ ["if-range", ""],
48
+ ["if-unmodified-since", ""],
49
+ ["last-modified", ""],
50
+ ["link", ""],
51
+ ["location", ""],
52
+ ["max-forwards", ""],
53
+ ["proxy-authenticate", ""],
54
+ ["proxy-authorization", ""],
55
+ ["range", ""],
56
+ ["referer", ""],
57
+ ["refresh", ""],
58
+ ["retry-after", ""],
59
+ ["server", ""],
60
+ ["set-cookie", ""],
61
+ ["strict-transport-security", ""],
62
+ ["transfer-encoding", ""],
63
+ ["user-agent", ""],
64
+ ["vary", ""],
65
+ ["via", ""],
66
+ ["www-authenticate", ""],
67
+ ].freeze
68
68
 
69
69
  HUFFMAN_TABLE = [
70
70
  "1111111111000",
@@ -324,8 +324,8 @@ module Plum
324
324
  "111111111111111111111110000",
325
325
  "11111111111111111111101110",
326
326
  "111111111111111111111111111111"
327
- ]
327
+ ].freeze
328
328
 
329
- HUFFMAN_TABLE_INVERSED = HUFFMAN_TABLE.each_with_index.to_h
329
+ HUFFMAN_TABLE_INVERSED = HUFFMAN_TABLE.each_with_index.to_h.freeze
330
330
  end
331
331
  end
@@ -16,8 +16,9 @@ module Plum
16
16
  end
17
17
 
18
18
  def store(name, value)
19
+ value = value.to_s
19
20
  @dynamic_table.unshift([name, value])
20
- @size += name.bytesize + value.to_s.bytesize + 32
21
+ @size += name.bytesize + value.bytesize + 32
21
22
  evict
22
23
  end
23
24
 
@@ -34,7 +35,7 @@ module Plum
34
35
  end
35
36
 
36
37
  def search(name, value)
37
- pr = proc {|n, v|
38
+ pr = proc { |n, v|
38
39
  n == name && (!value || v == value)
39
40
  }
40
41
 
@@ -47,7 +48,7 @@ module Plum
47
48
  def evict
48
49
  while @limit && @size > @limit
49
50
  name, value = @dynamic_table.pop
50
- @size -= name.bytesize + value.to_s.bytesize + 32
51
+ @size -= name.bytesize + value.bytesize + 32
51
52
  end
52
53
  end
53
54
  end
@@ -10,42 +10,47 @@ module Plum
10
10
  end
11
11
 
12
12
  def decode(str)
13
- str = str.dup
14
13
  headers = []
15
- headers << parse!(str) while str.size > 0
16
- headers.compact
14
+ pos = 0
15
+ lpos = str.bytesize
16
+ while pos < lpos
17
+ l, succ = parse(str, pos)
18
+ pos += succ
19
+ headers << l if l
20
+ end
21
+ headers
17
22
  end
18
23
 
19
24
  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
25
+ def parse(str, pos)
26
+ first_byte = str.getbyte(pos)
27
+ if first_byte >= 0x80 # 0b1XXXXXXX
28
+ parse_indexed(str, pos)
29
+ elsif first_byte >= 0x40 # 0b01XXXXXX
30
+ parse_indexing(str, pos)
31
+ elsif first_byte >= 0x20 # 0b001XXXXX
32
+ self.limit, succ = read_integer(str, pos, 5)
33
+ [nil, succ]
29
34
  else # 0b0000XXXX (without indexing) or 0b0001XXXX (never indexing)
30
- parse_no_indexing!(str)
35
+ parse_no_indexing(str, pos)
31
36
  end
32
37
  end
33
38
 
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
39
+ def read_integer(str, pos, prefix_length)
40
+ raise HPACKError.new("integer: end of buffer") if str.empty?
41
+ first_byte = str.getbyte(pos)
37
42
 
38
- mask = (1 << prefix_length) - 1
39
- ret = first_byte & mask
40
- return ret if ret < mask
43
+ mask = 1 << prefix_length
44
+ ret = first_byte % mask
45
+ return [ret, 1] if ret != mask - 1
41
46
 
42
47
  octets = 0
43
- while next_value = str.byteshift(1).uint8
44
- ret += (next_value & 0b01111111) << (7 * octets)
48
+ while next_value = str.uint8(pos + octets + 1)
49
+ ret += (next_value % 0x80) << (7 * octets)
45
50
  octets += 1
46
51
 
47
- if next_value < 128
48
- return ret
52
+ if next_value < 0x80
53
+ return [ret, 1 + octets]
49
54
  elsif octets == 4 # RFC 7541 5.1 tells us that we MUST have limitation. at least > 2 ** 28
50
55
  raise HPACKError.new("integer: too large integer")
51
56
  end
@@ -54,29 +59,32 @@ module Plum
54
59
  raise HPACKError.new("integer: end of buffer")
55
60
  end
56
61
 
57
- def read_string!(str)
58
- first_byte = str.uint8
59
- raise HPACKError.new("string: end of buffer") unless first_byte
62
+ def read_string(str, pos)
63
+ raise HPACKError.new("string: end of buffer") if str.empty?
64
+ first_byte = str.uint8(pos)
60
65
 
61
- huffman = (first_byte >> 7) == 1
62
- length = read_integer!(str, 7)
63
- bin = str.byteshift(length)
66
+ huffman = first_byte > 0x80
67
+ length, ilen = read_integer(str, pos, 7)
68
+ raise HTTPError.new("string: end of buffer") if str.bytesize < length
64
69
 
65
- raise HTTPError.new("string: end of buffer") if bin.bytesize < length
66
- bin = Huffman.decode(bin) if huffman
67
- bin
70
+ bin = str.byteslice(pos + ilen, length)
71
+ if huffman
72
+ [Huffman.decode(bin), ilen + length]
73
+ else
74
+ [bin, ilen + length]
75
+ end
68
76
  end
69
77
 
70
- def parse_indexed!(str)
78
+ def parse_indexed(str, pos)
71
79
  # indexed
72
80
  # +---+---+---+---+---+---+---+---+
73
81
  # | 1 | Index (7+) |
74
82
  # +---+---------------------------+
75
- index = read_integer!(str, 7)
76
- fetch(index)
83
+ index, succ = read_integer(str, pos, 7)
84
+ [fetch(index), succ]
77
85
  end
78
86
 
79
- def parse_indexing!(str)
87
+ def parse_indexing(str, pos)
80
88
  # +---+---+---+---+---+---+---+---+
81
89
  # | 0 | 1 | Index (6+) |
82
90
  # +---+---+-----------------------+
@@ -96,20 +104,21 @@ module Plum
96
104
  # +---+---------------------------+
97
105
  # | Value String (Length octets) |
98
106
  # +-------------------------------+
99
- index = read_integer!(str, 6)
107
+ index, ilen = read_integer(str, pos, 6)
100
108
  if index == 0
101
- name = read_string!(str)
109
+ name, nlen = read_string(str, pos + ilen)
102
110
  else
103
111
  name, = fetch(index)
112
+ nlen = 0
104
113
  end
105
114
 
106
- val = read_string!(str)
115
+ val, vlen = read_string(str, pos + ilen + nlen)
107
116
  store(name, val)
108
117
 
109
- [name, val]
118
+ [[name, val], ilen + nlen + vlen]
110
119
  end
111
120
 
112
- def parse_no_indexing!(str)
121
+ def parse_no_indexing(str, pos)
113
122
  # +---+---+---+---+---+---+---+---+
114
123
  # | 0 | 0 | 0 |0,1| Index (4+) |
115
124
  # +---+---+-----------------------+
@@ -129,16 +138,17 @@ module Plum
129
138
  # +---+---------------------------+
130
139
  # | Value String (Length octets) |
131
140
  # +-------------------------------+
132
- index = read_integer!(str, 4)
141
+ index, ilen = read_integer(str, pos, 4)
133
142
  if index == 0
134
- name = read_string!(str)
143
+ name, nlen = read_string(str, pos + ilen)
135
144
  else
136
145
  name, = fetch(index)
146
+ nlen = 0
137
147
  end
138
148
 
139
- val = read_string!(str)
149
+ val, vlen = read_string(str, pos + ilen + nlen)
140
150
 
141
- [name, val]
151
+ [[name, val], ilen + nlen + vlen]
142
152
  end
143
153
  end
144
154
  end
@@ -14,8 +14,8 @@ module Plum
14
14
  def encode(headers)
15
15
  out = ""
16
16
  headers.each do |name, value|
17
- name = name.to_s.b
18
- value = value.to_s.b
17
+ name = name.to_s
18
+ value = value.to_s
19
19
  if index = search(name, value)
20
20
  out << encode_indexed(index)
21
21
  elsif index = search(name, nil)
@@ -59,10 +59,9 @@ module Plum
59
59
  def encode_half_indexed(index, value)
60
60
  if @indexing
61
61
  store(fetch(index)[0], value)
62
- fb = encode_integer(index, 6)
63
- fb.setbyte(0, fb.uint8 | 0b01000000)
62
+ fb = encode_integer(index, 6, 0b01000000)
64
63
  else
65
- fb = encode_integer(index, 4)
64
+ fb = encode_integer(index, 4, 0b00000000)
66
65
  end
67
66
  fb << encode_string(value)
68
67
  end
@@ -71,27 +70,24 @@ module Plum
71
70
  # | 1 | Index (7+) |
72
71
  # +---+---------------------------+
73
72
  def encode_indexed(index)
74
- s = encode_integer(index, 7)
75
- s.setbyte(0, s.uint8 | 0b10000000)
76
- s
73
+ encode_integer(index, 7, 0b10000000)
77
74
  end
78
75
 
79
- def encode_integer(value, prefix_length)
76
+ def encode_integer(value, prefix_length, hmask)
80
77
  mask = (1 << prefix_length) - 1
81
- out = ""
82
78
 
83
79
  if value < mask
84
- out.push_uint8(value)
80
+ (value + hmask).chr.force_encoding(Encoding::BINARY)
85
81
  else
82
+ vals = [mask + hmask]
86
83
  value -= mask
87
- out.push_uint8(mask)
88
84
  while value >= mask
89
- out.push_uint8((value % 0b10000000) + 0b10000000)
90
- value >>= 7
85
+ vals << (value % 0x80) + 0x80
86
+ value /= 0x80
91
87
  end
92
- out.push_uint8(value)
88
+ vals << value
89
+ vals.pack("C*")
93
90
  end
94
- out.force_encoding(Encoding::BINARY)
95
91
  end
96
92
 
97
93
  def encode_string(str)
@@ -105,14 +101,13 @@ module Plum
105
101
  end
106
102
 
107
103
  def encode_string_plain(str)
108
- encode_integer(str.bytesize, 7) << str.force_encoding(Encoding::BINARY)
104
+ encode_integer(str.bytesize, 7, 0b00000000) << str
109
105
  end
110
106
 
111
107
  def encode_string_huffman(str)
112
108
  huffman_str = Huffman.encode(str)
113
- lenstr = encode_integer(huffman_str.bytesize, 7)
114
- lenstr.setbyte(0, lenstr.uint8(0) | 0b10000000)
115
- lenstr.force_encoding(Encoding::BINARY) << huffman_str
109
+ lenstr = encode_integer(huffman_str.bytesize, 7, 0b10000000)
110
+ lenstr << huffman_str
116
111
  end
117
112
  end
118
113
  end
@@ -2,12 +2,28 @@ using Plum::BinaryString
2
2
 
3
3
  module Plum
4
4
  class HTTPConnection < Connection
5
- def initialize(io, local_settings = {})
5
+ attr_reader :sock
6
+
7
+ def initialize(sock, local_settings = {})
6
8
  require "http/parser"
7
- super
8
9
  @_headers = nil
9
10
  @_body = ""
10
11
  @_http_parser = setup_parser
12
+ @sock = sock
13
+ super(@sock.method(:write), local_settings)
14
+ end
15
+
16
+ # Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
17
+ def run
18
+ while !@sock.closed? && !@sock.eof?
19
+ self << @sock.readpartial(1024)
20
+ end
21
+ end
22
+
23
+ # Closes the socket.
24
+ def close
25
+ super
26
+ @sock.close
11
27
  end
12
28
 
13
29
  private
@@ -56,7 +72,7 @@ module Plum
56
72
  resp << "Server: plum/#{Plum::VERSION}\r\n"
57
73
  resp << "\r\n"
58
74
 
59
- io.write(resp)
75
+ @sock.write(resp)
60
76
  end
61
77
 
62
78
  def process_first_request
@@ -1,15 +1,31 @@
1
1
  module Plum
2
2
  class HTTPSConnection < Connection
3
- def initialize(io, local_settings = {})
4
- if io.respond_to?(:cipher) # OpenSSL::SSL::SSLSocket-like
5
- if CIPHER_BLACKLIST.include?(io.cipher.first) # [cipher-suite, ssl-version, keylen, alglen]
6
- self.on(:negotiated) {
3
+ attr_reader :sock
4
+
5
+ def initialize(sock, local_settings = {})
6
+ @sock = sock
7
+ super(@sock.method(:write), local_settings)
8
+ end
9
+
10
+ # Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
11
+ def run
12
+ if @sock.respond_to?(:cipher) # OpenSSL::SSL::SSLSocket-like
13
+ if CIPHER_BLACKLIST.include?(@sock.cipher.first) # [cipher-suite, ssl-version, keylen, alglen]
14
+ on(:negotiated) {
7
15
  raise ConnectionError.new(:inadequate_security)
8
16
  }
9
17
  end
10
18
  end
11
19
 
20
+ while !@sock.closed? && !@sock.eof?
21
+ self << @sock.readpartial(1024)
22
+ end
23
+ end
24
+
25
+ # Closes the socket.
26
+ def close
12
27
  super
28
+ @sock.close
13
29
  end
14
30
 
15
31
  CIPHER_BLACKLIST = %w(
@@ -26,6 +42,6 @@ module Plum
26
42
  AECDH-NULL-SHA AECDH-RC4-SHA AECDH-DES-CBC3-SHA AECDH-AES128-SHA AECDH-AES256-SHA SRP-3DES-EDE-CBC-SHA SRP-RSA-3DES-EDE-CBC-SHA SRP-DSS-3DES-EDE-CBC-SHA SRP-AES-128-CBC-SHA SRP-RSA-AES-128-CBC-SHA
27
43
  SRP-DSS-AES-128-CBC-SHA SRP-AES-256-CBC-SHA SRP-RSA-AES-256-CBC-SHA SRP-DSS-AES-256-CBC-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDH-ECDSA-AES128-SHA256 ECDH-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384
28
44
  ECDH-RSA-AES128-SHA256 ECDH-RSA-AES256-SHA384 ECDH-ECDSA-AES128-GCM-SHA256 ECDH-ECDSA-AES256-GCM-SHA384 ECDH-RSA-AES128-GCM-SHA256 ECDH-RSA-AES256-GCM-SHA384
29
- )
45
+ ).freeze
30
46
  end
31
47
  end