plum 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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