plum 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e9aa768df8ce054c286452ca40c8e6f5671fda5
4
- data.tar.gz: 3a5dafa95ec6dd94b6b354091a8eee73b88f3214
3
+ metadata.gz: aa11d64c7ed018b7ede5e9275adcd9abdb091003
4
+ data.tar.gz: 5fb20e87686039b5dec5f9b053719935b9788c06
5
5
  SHA512:
6
- metadata.gz: 3110fdf853b9ab1a7966adff6b24a3665bf792853a63bc814e295cee3a343761894b2fa1a48157a19c349998415bfee7710f3242400081ea8863b48f30028d4f
7
- data.tar.gz: 061d1e6292eb1cd9d2fc6cd6db1daceb4e483970c577cf504f23deb23a407e44535fa27c57a819159198f41c7b248c015940485bab620a565cca84b0a7f14620
6
+ metadata.gz: 1b0095b8f3510be4442524baadcb27a385530753ff0e63ea4eb80bd2ff1755757bbe913ac7977ded238dcb5cc46ad6e6830d720254640a59b4b5fd2e20f8ff1a
7
+ data.tar.gz: 87e4540bed0f1ec556b177fe16e208527acd43abcea2ef2edbd40fac3070348c7b74891b5d9acf5fe56dbca372940c5c4f771fc60ee05fd29dc26a1106f3ee89
data/README.md CHANGED
@@ -4,11 +4,10 @@ A minimal implementation of HTTP/2 server.
4
4
  ## Requirements
5
5
  * OpenSSL 1.0.2+
6
6
  * Ruby 2.2 with [ALPN support](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff) or latest Ruby 2.3.0-dev.
7
+ * [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) if you use "http" URI scheme.
7
8
 
8
9
  ## TODO
9
- * "http" URIs support (upgrade from HTTP/1.1)
10
- * Stream Priority (RFC 7540 5.3)
11
- * Better API
10
+ * **Better API**
12
11
 
13
12
  ## License
14
13
  MIT License
@@ -11,6 +11,7 @@ CONTENT_TYPES = {
11
11
  /\.jpg$/ => "image/jpeg",
12
12
  /\.css$/ => "text/css",
13
13
  /\.js$/ => "application/javascript",
14
+ /\.atom$/ => "application/atom+xml",
14
15
  }
15
16
 
16
17
  $LOAD_PATH << File.expand_path("../../lib", __FILE__)
@@ -0,0 +1,127 @@
1
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
2
+ require "plum"
3
+ require "socket"
4
+ require "cgi"
5
+
6
+ def log(con, stream, s)
7
+ prefix = "[%02x;%02x] " % [con, stream]
8
+ if s.is_a?(Enumerable)
9
+ puts s.map {|a| prefix + a.to_s }.join("\n")
10
+ else
11
+ puts prefix + s.to_s
12
+ end
13
+ end
14
+
15
+ tcp_server = TCPServer.new("0.0.0.0", 40080)
16
+
17
+ loop do
18
+ begin
19
+ sock = tcp_server.accept
20
+ id = sock.fileno
21
+ puts "#{id}: accept!"
22
+ rescue => e
23
+ STDERR.puts e
24
+ next
25
+ end
26
+
27
+ plum = Plum::HTTPConnection.new(sock)
28
+
29
+ plum.on(:frame) do |frame|
30
+ log(id, frame.stream_id, "recv: #{frame.inspect}")
31
+ end
32
+
33
+ plum.on(:send_frame) do |frame|
34
+ log(id, frame.stream_id, "send: #{frame.inspect}")
35
+ end
36
+
37
+ plum.on(:connection_error) do |exception|
38
+ puts exception
39
+ puts exception.backtrace
40
+ end
41
+
42
+ plum.on(:stream) do |stream|
43
+ stream.on(:stream_error) do |exception|
44
+ puts exception
45
+ puts exception.backtrace
46
+ end
47
+
48
+ stream.on(:send_deferred) do |frame|
49
+ log(id, frame.stream_id, "send (deferred): #{frame.inspect}")
50
+ end
51
+
52
+ headers = data = nil
53
+
54
+ stream.on(:open) do
55
+ headers = nil
56
+ data = ""
57
+ end
58
+
59
+ stream.on(:headers) do |headers_|
60
+ log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" })
61
+ headers = headers_.to_h
62
+ end
63
+
64
+ stream.on(:data) do |data_|
65
+ log(id, stream.id, data_)
66
+ data << data_
67
+ end
68
+
69
+ stream.on(:end_stream) do
70
+ case [headers[":method"], headers[":path"]]
71
+ when ["GET", "/"]
72
+ body = "Hello World! <a href=/abc.html>ABC</a> <a href=/fgsd>Not found</a>"
73
+ body << <<-EOF
74
+ <form action=post.page method=post>
75
+ <input type=text name=key value=default_value>
76
+ <input type=submit>
77
+ </form>
78
+ EOF
79
+ stream.respond({
80
+ ":status": "200",
81
+ "server": "plum",
82
+ "content-type": "text/html",
83
+ "content-length": body.size
84
+ }, body)
85
+ when ["POST", "/post.page"]
86
+ body = "Posted value is: #{CGI.unescape(data).gsub("<", "&lt;").gsub(">", "&gt;")}<br> <a href=/>Back to top page</a>"
87
+ stream.respond({
88
+ ":status": "200",
89
+ "server": "plum",
90
+ "content-type": "text/html",
91
+ "content-length": body.size
92
+ }, body)
93
+ else
94
+ body = "Page not found! <a href=/>Back to top page</a>"
95
+ stream.respond({
96
+ ":status": "404",
97
+ "server": "plum",
98
+ "content-type": "text/html",
99
+ "content-length": body.size
100
+ }, body)
101
+ end
102
+ end
103
+ end
104
+
105
+ Thread.new {
106
+ begin
107
+ plum.run
108
+ rescue Plum::LegacyHTTPError
109
+ data = "Use modern web browser with HTTP/2 support."
110
+
111
+ resp = ""
112
+ resp << "HTTP/1.1 505 HTTP Version Not Supported\r\n"
113
+ resp << "Content-Type: text/plain\r\n"
114
+ resp << "Content-Length: #{data.bytesize}\r\n"
115
+ resp << "Server: plum/#{Plum::VERSION}\r\n"
116
+ resp << "\r\n"
117
+ resp << data
118
+
119
+ sock.write(resp)
120
+ rescue
121
+ puts $!
122
+ puts $!.backtrace
123
+ ensure
124
+ sock.close
125
+ end
126
+ }
127
+ end
@@ -36,7 +36,7 @@ loop do
36
36
  begin
37
37
  sock = ssl_server.accept
38
38
  id = sock.io.fileno
39
- puts "#{id}: accept!"
39
+ puts "#{id}: accept! #{sock.cipher.inspect}"
40
40
  rescue => e
41
41
  STDERR.puts e
42
42
  next
data/lib/plum.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "openssl"
2
2
  require "socket"
3
+ require "base64"
3
4
  require "plum/version"
4
5
  require "plum/errors"
5
6
  require "plum/binary_string"
@@ -55,9 +55,7 @@ module Plum
55
55
  return if new_data.empty?
56
56
  @buffer << new_data
57
57
 
58
- if @state == :negotiation
59
- negotiate!
60
- end
58
+ negotiate! if @state == :negotiation
61
59
 
62
60
  if @state != :negotiation
63
61
  while frame = Frame.parse!(@buffer)
@@ -87,8 +85,20 @@ module Plum
87
85
  @io.write(frame.assemble)
88
86
  end
89
87
 
88
+ def negotiate!
89
+ unless CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
90
+ raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
91
+ end
92
+
93
+ if @buffer.bytesize >= 24
94
+ @buffer.byteshift(24)
95
+ @state = :waiting_settings
96
+ settings(@local_settings)
97
+ end
98
+ end
99
+
90
100
  def new_stream(stream_id, **args)
91
- if @streams.size > 0 && @streams.keys.last >= stream_id
101
+ if @streams.size > 0 && @streams.keys.max >= stream_id
92
102
  raise Plum::ConnectionError.new(:protocol_error)
93
103
  end
94
104
 
@@ -99,26 +109,25 @@ module Plum
99
109
  end
100
110
 
101
111
  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
112
+ if @state == :waiting_settings && frame.type != :settings
113
+ raise ConnectionError.new(:protocol_error)
114
+ end
115
+
116
+ if @state == :waiting_continuation
108
117
  if frame.type != :continuation || frame.stream_id != @continuation_id
109
- raise Plum::ConnectionError.new(:protocol_error)
118
+ raise ConnectionError.new(:protocol_error)
110
119
  end
111
120
 
112
121
  if frame.flags.include?(:end_headers)
113
122
  @state = :open
114
123
  @continuation_id = nil
115
124
  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
125
+ end
126
+
127
+ if [:headers].include?(frame.type)
128
+ if !frame.flags.include?(:end_headers)
129
+ @state = :waiting_continuation
130
+ @continuation_id = frame.stream_id
122
131
  end
123
132
  end
124
133
  end
@@ -133,9 +142,7 @@ module Plum
133
142
  if @streams.key?(frame.stream_id)
134
143
  stream = @streams[frame.stream_id]
135
144
  else
136
- if frame.stream_id.even? # stream started by client must have odd ID
137
- raise Plum::ConnectionError.new(:protocol_error)
138
- end
145
+ raise ConnectionError.new(:protocol_error) if frame.stream_id.even? # stream started by client must have odd ID
139
146
  stream = new_stream(frame.stream_id)
140
147
  end
141
148
  stream.receive_frame(frame)
@@ -164,21 +171,27 @@ module Plum
164
171
  end
165
172
  end
166
173
 
167
- def receive_settings(frame)
174
+ def receive_settings(frame, send_ack: true)
168
175
  if frame.flags.include?(:ack)
169
176
  raise ConnectionError.new(:frame_size_error) if frame.length != 0
177
+ callback(:settings_ack)
170
178
  return
179
+ else
180
+ raise ConnectionError.new(:frame_size_error) if frame.length % 6 != 0
171
181
  end
172
182
 
173
- raise ConnectionError.new(:frame_size_error) if frame.length % 6 != 0
174
-
175
183
  old_remote_settings = @remote_settings.dup
176
184
  @remote_settings.merge!(frame.parse_settings)
177
185
  apply_remote_settings(old_remote_settings)
178
186
 
179
187
  callback(:remote_settings, @remote_settings, old_remote_settings)
180
188
 
181
- send_immediately Frame.settings(:ack)
189
+ send_immediately Frame.settings(:ack) if send_ack
190
+
191
+ if @state == :waiting_settings
192
+ @state = :open
193
+ callback(:negotiated)
194
+ end
182
195
  end
183
196
 
184
197
  def apply_remote_settings(old_remote_settings)
@@ -190,10 +203,10 @@ module Plum
190
203
  raise Plum::ConnectionError.new(:frame_size_error) if frame.length != 8
191
204
 
192
205
  if frame.flags.include?(:ack)
193
- on(:ping_ack)
206
+ callback(:ping_ack)
194
207
  else
195
- on(:ping)
196
208
  opaque_data = frame.payload
209
+ callback(:ping, opaque_data)
197
210
  send_immediately Frame.ping(:ack, opaque_data)
198
211
  end
199
212
  end
data/lib/plum/errors.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Plum
2
2
  class Error < StandardError; end
3
+ class LegacyHTTPError < Error; end
3
4
  class HPACKError < Error; end
4
5
  class HTTPError < Error
5
6
  ERROR_CODES = {
@@ -5,20 +5,23 @@ module Plum
5
5
  class Encoder
6
6
  include HPACK::Context
7
7
 
8
- def initialize(dynamic_table_limit)
9
- super
8
+ def initialize(dynamic_table_limit, indexing: true, huffman: true)
9
+ super(dynamic_table_limit)
10
+ @indexing = indexing
11
+ @huffman = huffman
10
12
  end
11
13
 
12
14
  def encode(headers)
13
15
  out = ""
14
16
  headers.each do |name, value|
15
- name = name.to_s; value = value.to_s
17
+ name = name.to_s.b
18
+ value = value.to_s.b
16
19
  if index = search(name, value)
17
20
  out << encode_indexed(index)
18
21
  elsif index = search(name, nil)
19
- out << encode_half_indexed(index, value, true) # incremental indexing
22
+ out << encode_half_indexed(index, value)
20
23
  else
21
- out << encode_literal(name, value, true) # incremental indexing
24
+ out << encode_literal(name, value)
22
25
  end
23
26
  end
24
27
  out.force_encoding(Encoding::BINARY)
@@ -36,14 +39,14 @@ module Plum
36
39
  # +---+---------------------------+
37
40
  # | Value String (Length octets) |
38
41
  # +-------------------------------+
39
- def encode_literal(name, value, indexing = true)
40
- if indexing
42
+ def encode_literal(name, value)
43
+ if @indexing
41
44
  store(name, value)
42
45
  fb = "\x40"
43
46
  else
44
47
  fb = "\x00"
45
48
  end
46
- fb << encode_string(name) << encode_string(value)
49
+ fb.force_encoding(Encoding::BINARY) << encode_string(name) << encode_string(value)
47
50
  end
48
51
 
49
52
  # +---+---+---+---+---+---+---+---+
@@ -53,8 +56,8 @@ module Plum
53
56
  # +---+---------------------------+
54
57
  # | Value String (Length octets) |
55
58
  # +-------------------------------+
56
- def encode_half_indexed(index, value, indexing = true)
57
- if indexing
59
+ def encode_half_indexed(index, value)
60
+ if @indexing
58
61
  store(fetch(index)[0], value)
59
62
  fb = encode_integer(index, 6)
60
63
  fb.setbyte(0, fb.uint8 | 0b01000000)
@@ -88,18 +91,29 @@ module Plum
88
91
  end
89
92
  out.push_uint8(value)
90
93
  end
94
+ out.force_encoding(Encoding::BINARY)
91
95
  end
92
96
 
93
97
  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
98
+ if @huffman
99
+ hs = encode_string_huffman(str)
100
+ ps = encode_string_plain(str)
101
+ hs.bytesize < ps.bytesize ? hs : ps
99
102
  else
100
- encode_integer(str.bytesize, 7) << str
103
+ encode_string_plain(str)
101
104
  end
102
105
  end
106
+
107
+ def encode_string_plain(str)
108
+ encode_integer(str.bytesize, 7) << str.force_encoding(Encoding::BINARY)
109
+ end
110
+
111
+ def encode_string_huffman(str)
112
+ 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
116
+ end
103
117
  end
104
118
  end
105
119
  end
@@ -1,33 +1,76 @@
1
+ using Plum::BinaryString
2
+
1
3
  module Plum
2
4
  class HTTPConnection < Connection
3
5
  def initialize(io, local_settings = {})
6
+ require "http/parser"
4
7
  super
8
+ @_headers = nil
9
+ @_body = ""
10
+ @_http_parser = setup_parser
5
11
  end
6
12
 
7
13
  private
8
14
  def negotiate!
9
- if @buffer.bytesize >= 4
10
- if CLIENT_CONNECTION_PREFACE.start_with?(@buffer)
11
- negotiate_with_knowledge
15
+ super
16
+ rescue ConnectionError
17
+ # Upgrade from HTTP/1.1
18
+ offset = @_http_parser << @buffer
19
+ @buffer.byteshift(offset)
20
+ end
21
+
22
+ def setup_parser
23
+ parser = HTTP::Parser.new
24
+ parser.on_headers_complete = proc {|_headers|
25
+ @_headers = _headers.map {|n, v| [n.downcase, v] }.to_h
26
+ }
27
+ parser.on_body = proc {|chunk| @_body << chunk }
28
+ parser.on_message_complete = proc {|env|
29
+ connection = @_headers["connection"] || ""
30
+ upgrade = @_headers["upgrade"] || ""
31
+ settings = @_headers["http2-settings"]
32
+
33
+ if (connection.split(", ").sort == ["Upgrade", "HTTP2-Settings"].sort &&
34
+ upgrade.split(", ").include?("h2c") &&
35
+ settings != nil)
36
+ switch_protocol(settings)
12
37
  else
13
- negotiate_with_upgrade
38
+ raise LegacyHTTPError.new
14
39
  end
15
- end
16
- # next
40
+ }
41
+
42
+ parser
17
43
  end
18
44
 
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
45
+ def switch_protocol(settings)
46
+ self.on(:negotiated) {
47
+ _frame = Frame.new(type: :settings, stream_id: 0, payload: Base64.urlsafe_decode64(settings))
48
+ receive_settings(_frame, send_ack: false) # HTTP2-Settings
49
+ process_first_request
50
+ }
51
+
52
+ resp = ""
53
+ resp << "HTTP/1.1 101 Switching Protocols\r\n"
54
+ resp << "Connection: Upgrade\r\n"
55
+ resp << "Upgrade: h2c\r\n"
56
+ resp << "Server: plum/#{Plum::VERSION}\r\n"
57
+ resp << "\r\n"
58
+
59
+ io.write(resp)
27
60
  end
28
61
 
29
- def negotiate_with_upgrade
30
- raise NotImplementedError, "Parsing HTTP/1.1 is hard..."
62
+ def process_first_request
63
+ encoder = HPACK::Encoder.new(0, indexing: false) # don't pollute connection's HPACK context
64
+ stream = new_stream(1)
65
+ max_frame_size = local_settings[:max_frame_size]
66
+ headers = @_headers.merge({ ":method" => @_http_parser.http_method,
67
+ ":path" => @_http_parser.request_url,
68
+ ":authority" => @_headers["host"] })
69
+ .reject {|n, v| ["connection", "http2-settings", "upgrade", "host"].include?(n) }
70
+
71
+ headers_s = Frame.headers(1, encoder.encode(headers), :end_headers).split_headers(max_frame_size) # stream ID is 1
72
+ data_s = Frame.data(1, @_body, :end_stream).split_data(max_frame_size)
73
+ (headers_s + data_s).each {|frag| stream.receive_frame(frag) }
31
74
  end
32
75
  end
33
76
  end
@@ -1,24 +1,31 @@
1
- using Plum::BinaryString
2
-
3
1
  module Plum
4
2
  class HTTPSConnection < Connection
5
3
  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)
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) {
7
+ raise ConnectionError.new(:inadequate_security)
8
+ }
18
9
  end
19
- else
20
- raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
21
10
  end
11
+
12
+ super
22
13
  end
14
+
15
+ CIPHER_BLACKLIST = %w(
16
+ NULL-MD5 NULL-SHA EXP-RC4-MD5 RC4-MD5 RC4-SHA EXP-RC2-CBC-MD5 IDEA-CBC-SHA EXP-DES-CBC-SHA DES-CBC-SHA DES-CBC3-SHA
17
+ DH-DSS-DES-CBC-SHA DH-DSS-DES-CBC3-SHA DH-RSA-DES-CBC-SHA DH-RSA-DES-CBC3-SHA EXP-EDH-DSS-DES-CBC-SHA EDH-DSS-DES-CBC-SHA EDH-DSS-DES-CBC3-SHA EXP-EDH-RSA-DES-CBC-SHA EDH-RSA-DES-CBC-SHA EDH-RSA-DES-CBC3-SHA
18
+ EXP-ADH-RC4-MD5 ADH-RC4-MD5 EXP-ADH-DES-CBC-SHA ADH-DES-CBC-SHA ADH-DES-CBC3-SHA AES128-SHA DH-DSS-AES128-SHA DH-RSA-AES128-SHA DHE-DSS-AES128-SHA DHE-RSA-AES128-SHA
19
+ ADH-AES128-SHA AES256-SHA DH-DSS-AES256-SHA DH-RSA-AES256-SHA DHE-DSS-AES256-SHA DHE-RSA-AES256-SHA ADH-AES256-SHA NULL-SHA256 AES128-SHA256 AES256-SHA256
20
+ DH-DSS-AES128-SHA256 DH-RSA-AES128-SHA256 DHE-DSS-AES128-SHA256 CAMELLIA128-SHA DH-DSS-CAMELLIA128-SHA DH-RSA-CAMELLIA128-SHA DHE-DSS-CAMELLIA128-SHA DHE-RSA-CAMELLIA128-SHA ADH-CAMELLIA128-SHA DHE-RSA-AES128-SHA256
21
+ DH-DSS-AES256-SHA256 DH-RSA-AES256-SHA256 DHE-DSS-AES256-SHA256 DHE-RSA-AES256-SHA256 ADH-AES128-SHA256 ADH-AES256-SHA256 CAMELLIA256-SHA DH-DSS-CAMELLIA256-SHA DH-RSA-CAMELLIA256-SHA DHE-DSS-CAMELLIA256-SHA
22
+ DHE-RSA-CAMELLIA256-SHA ADH-CAMELLIA256-SHA PSK-RC4-SHA PSK-3DES-EDE-CBC-SHA PSK-AES128-CBC-SHA PSK-AES256-CBC-SHA SEED-SHA DH-DSS-SEED-SHA DH-RSA-SEED-SHA DHE-DSS-SEED-SHA
23
+ DHE-RSA-SEED-SHA ADH-SEED-SHA AES128-GCM-SHA256 AES256-GCM-SHA384 DH-RSA-AES128-GCM-SHA256 DH-RSA-AES256-GCM-SHA384 DH-DSS-AES128-GCM-SHA256 DH-DSS-AES256-GCM-SHA384 ADH-AES128-GCM-SHA256 ADH-AES256-GCM-SHA384
24
+ ECDH-ECDSA-NULL-SHA ECDH-ECDSA-RC4-SHA ECDH-ECDSA-DES-CBC3-SHA ECDH-ECDSA-AES128-SHA ECDH-ECDSA-AES256-SHA ECDHE-ECDSA-NULL-SHA ECDHE-ECDSA-RC4-SHA ECDHE-ECDSA-DES-CBC3-SHA ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA
25
+ ECDH-RSA-NULL-SHA ECDH-RSA-RC4-SHA ECDH-RSA-DES-CBC3-SHA ECDH-RSA-AES128-SHA ECDH-RSA-AES256-SHA ECDHE-RSA-NULL-SHA ECDHE-RSA-RC4-SHA ECDHE-RSA-DES-CBC3-SHA ECDHE-RSA-AES128-SHA ECDHE-RSA-AES256-SHA
26
+ 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
+ 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
+ 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
+ )
23
30
  end
24
31
  end
data/lib/plum/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plum
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/plum.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "http_parser.rb"
22
23
  spec.add_development_dependency "rake"
23
24
  spec.add_development_dependency "yard"
24
25
  spec.add_development_dependency "minitest", "~> 5.7.0"
@@ -54,10 +54,10 @@ class HPACKContextTest < Minitest::Test
54
54
 
55
55
  private
56
56
  def new_context(limit = 1 << 31)
57
- @c ||= Class.new {
57
+ klass = Class.new {
58
58
  include Plum::HPACK::Context
59
59
  public *Plum::HPACK::Context.private_instance_methods
60
60
  }
61
- @c.new(limit)
61
+ klass.new(limit)
62
62
  end
63
63
  end
@@ -3,25 +3,25 @@ require "test_helper"
3
3
  class HPACKEncoderTest < Minitest::Test
4
4
  # C.1.1
5
5
  def test_hpack_encode_integer_small
6
- result = new_encoder.__send__(:encode_integer, 10, 5)
6
+ result = new_encoder(1 << 31).__send__(:encode_integer, 10, 5)
7
7
  assert_equal([0b00001010].pack("C*"), result)
8
8
  end
9
9
 
10
10
  # C.1.2
11
11
  def test_hpack_encode_integer_big
12
- result = new_encoder.__send__(:encode_integer, 1337, 5)
12
+ result = new_encoder(1 << 31).__send__(:encode_integer, 1337, 5)
13
13
  assert_equal([0b00011111, 0b10011010, 0b00001010].pack("C*"), result)
14
14
  end
15
15
 
16
16
  # C.1.3
17
17
  def test_hpack_encode_integer_8prefix
18
- result = new_encoder.__send__(:encode_integer, 42, 8)
18
+ result = new_encoder(1 << 31).__send__(:encode_integer, 42, 8)
19
19
  assert_equal([0b00101010].pack("C*"), result)
20
20
  end
21
21
 
22
22
  def test_hpack_encode_single
23
23
  headers = [["custom-key", "custom-header"]]
24
- encoded = new_encoder.encode(headers)
24
+ encoded = new_encoder(1 << 31).encode(headers)
25
25
  decoded = new_decoder.decode(encoded)
26
26
  assert_equal(headers, decoded)
27
27
  end
@@ -33,17 +33,34 @@ class HPACKEncoderTest < Minitest::Test
33
33
  [":path", "/"],
34
34
  [":authority", "www.example.com"]
35
35
  ]
36
- encoded = new_encoder.encode(headers)
36
+ encoded = new_encoder(1 << 31).encode(headers)
37
37
  decoded = new_decoder.decode(encoded)
38
38
  assert_equal(headers, decoded)
39
39
  end
40
40
 
41
+ def test_hpack_encode_without_indexing
42
+ encoder = new_encoder(1 << 31, indexing: false)
43
+ headers1 = [["custom-key", "custom-header"]]
44
+ ret1 = encoder.encode(headers1)
45
+ assert_equal([], encoder.dynamic_table)
46
+ headers2 = [[":method", "custom-header"]]
47
+ ret2 = encoder.encode(headers2)
48
+ assert_equal([], encoder.dynamic_table)
49
+ end
50
+
51
+ def test_hpack_encode_without_huffman
52
+ encoder = new_encoder(1 << 31, huffman: false)
53
+ headers = [["custom-key", "custom-header"]]
54
+ ret = encoder.encode(headers)
55
+ assert_equal("\x40\x0acustom-key\x0dcustom-header", ret)
56
+ end
57
+
41
58
  private
42
59
  def new_decoder(settings_header_table_size = 1 << 31)
43
60
  Plum::HPACK::Decoder.new(settings_header_table_size)
44
61
  end
45
62
 
46
- def new_encoder(settings_header_table_size = 1 << 31)
47
- Plum::HPACK::Encoder.new(settings_header_table_size)
63
+ def new_encoder(*args)
64
+ Plum::HPACK::Encoder.new(*args)
48
65
  end
49
66
  end
@@ -160,7 +160,7 @@ class StreamHandleFrameTest < Minitest::Test
160
160
  header_block = HPACK::Encoder.new(0).encode([[":path", "/"]])
161
161
  payload = "".push_uint32((1 << 31) | parent.id)
162
162
  .push_uint8(50)
163
- .push(header_block)
163
+ .push(header_block)
164
164
  stream.receive_frame(Frame.new(type: :headers,
165
165
  stream_id: stream.id,
166
166
  flags: [:end_headers, :priority],
@@ -0,0 +1,31 @@
1
+ require "test_helper"
2
+
3
+ using BinaryString
4
+ class EventEmitterTest < Minitest::Test
5
+ def test_simple
6
+ ret = nil
7
+ emitter = new_emitter
8
+ emitter.on(:event) {|arg| ret = arg }
9
+ emitter.callback(:event, 123)
10
+ assert_equal(123, ret)
11
+ end
12
+
13
+ def test_multiple
14
+ ret1 = nil; ret2 = nil
15
+ emitter = new_emitter
16
+ emitter.on(:event) {|arg| ret1 = arg }
17
+ emitter.on(:event) {|arg| ret2 = arg }
18
+ emitter.callback(:event, 123)
19
+ assert_equal(123, ret1)
20
+ assert_equal(123, ret2)
21
+ end
22
+
23
+ private
24
+ def new_emitter
25
+ klass = Class.new {
26
+ include EventEmitter
27
+ public *EventEmitter.private_instance_methods
28
+ }
29
+ klass.new
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+
5
+ class HTTPConnectionNegotiationTest < Minitest::Test
6
+ ## with Prior Knowledge (same as over TLS)
7
+ def test_server_must_raise_cprotocol_error_non_settings_after_magic
8
+ con = HTTPConnection.new(StringIO.new)
9
+ con << Connection::CLIENT_CONNECTION_PREFACE
10
+ assert_connection_error(:protocol_error) {
11
+ con << Frame.new(type: :window_update, stream_id: 0, payload: "".push_uint32(1)).assemble
12
+ }
13
+ end
14
+
15
+ def test_server_accept_fragmented_magic
16
+ magic = Connection::CLIENT_CONNECTION_PREFACE
17
+ con = HTTPConnection.new(StringIO.new)
18
+ assert_no_error {
19
+ con << magic[0...5]
20
+ con << magic[5..-1]
21
+ con << Frame.new(type: :settings, stream_id: 0).assemble
22
+ }
23
+ end
24
+
25
+ ## with HTTP/1.1 Upgrade
26
+ def test_server_accept_upgrade
27
+ io = StringIO.new
28
+ con = HTTPConnection.new(io)
29
+ heads = nil
30
+ con.on(:stream) {|stream|
31
+ stream.on(:headers) {|_h| heads = _h.to_h }
32
+ }
33
+ req = "GET / HTTP/1.1\r\n" <<
34
+ "Host: rhe.jp\r\n" <<
35
+ "User-Agent: nya\r\n" <<
36
+ "Upgrade: h2c\r\n" <<
37
+ "Connection: HTTP2-Settings, Upgrade\r\n" <<
38
+ "HTTP2-Settings: \r\n" <<
39
+ "\r\n"
40
+ con << req
41
+ assert(io.string.include?("HTTP/1.1 101 "), "Response is not HTTP/1.1 101: #{io.string}")
42
+ assert_no_error {
43
+ con << Connection::CLIENT_CONNECTION_PREFACE
44
+ con << Frame.new(type: :settings, stream_id: 0).assemble
45
+ }
46
+ assert_equal(:half_closed_remote, con.streams[1].state)
47
+ assert_equal({ ":method" => "GET", ":path" => "/", ":authority" => "rhe.jp", "user-agent" => "nya"}, heads)
48
+ end
49
+
50
+ def test_server_deny_non_upgrade
51
+ io = StringIO.new
52
+ con = HTTPConnection.new(io)
53
+ req = "GET / HTTP/1.1\r\n" <<
54
+ "Host: rhe.jp\r\n" <<
55
+ "User-Agent: nya\r\n" <<
56
+ "Connection: close\r\n" <<
57
+ "\r\n"
58
+ assert_raises(LegacyHTTPError) {
59
+ con << req
60
+ }
61
+ end
62
+ end
@@ -34,4 +34,55 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
34
34
  con << Frame.new(type: :settings, stream_id: 0).assemble
35
35
  }
36
36
  end
37
+
38
+ def test_inadequate_security_ssl_socket
39
+ run = false
40
+
41
+ ctx = OpenSSL::SSL::SSLContext.new
42
+ ctx.alpn_select_cb = -> protocols { "h2" }
43
+ ctx.cert = OpenSSL::X509::Certificate.new File.read(File.expand_path("../../server.crt", __FILE__))
44
+ ctx.key = OpenSSL::PKey::RSA.new File.read(File.expand_path("../../server.key", __FILE__))
45
+ tcp_server = TCPServer.new("127.0.0.1", LISTEN_PORT)
46
+ ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
47
+
48
+ server_thread = Thread.new {
49
+ begin
50
+ timeout(3) {
51
+ sock = ssl_server.accept
52
+ plum = HTTPSConnection.new(sock)
53
+ assert_connection_error(:inadequate_security) {
54
+ run = true
55
+ plum.run
56
+ }
57
+ }
58
+ rescue TimeoutError
59
+ flunk "server timeout"
60
+ ensure
61
+ tcp_server.close
62
+ end
63
+ }
64
+ client_thread = Thread.new {
65
+ sock = TCPSocket.new("127.0.0.1", LISTEN_PORT)
66
+ begin
67
+ timeout(3) {
68
+ ctx = OpenSSL::SSL::SSLContext.new.tap {|ctx|
69
+ ctx.alpn_protocols = ["h2"]
70
+ ctx.ciphers = "AES256-GCM-SHA384"
71
+ }
72
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
73
+ ssl.connect
74
+ ssl.write Connection::CLIENT_CONNECTION_PREFACE
75
+ ssl.write Frame.settings.assemble
76
+ }
77
+ rescue TimeoutError
78
+ flunk "client timeout"
79
+ ensure
80
+ sock.close
81
+ end
82
+ }
83
+ client_thread.join
84
+ server_thread.join
85
+
86
+ flunk "test not run" unless run
87
+ end
37
88
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - rhenium
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-13 00:00:00.000000000 Z
11
+ date: 2015-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -138,6 +152,7 @@ files:
138
152
  - Rakefile
139
153
  - bin/.gitkeep
140
154
  - examples/local_server.rb
155
+ - examples/non_tls_server.rb
141
156
  - examples/static_server.rb
142
157
  - lib/plum.rb
143
158
  - lib/plum/binary_string.rb
@@ -170,10 +185,12 @@ files:
170
185
  - test/plum/test_connection.rb
171
186
  - test/plum/test_connection_utils.rb
172
187
  - test/plum/test_error.rb
188
+ - test/plum/test_event_emitter.rb
173
189
  - test/plum/test_flow_control.rb
174
190
  - test/plum/test_frame.rb
175
191
  - test/plum/test_frame_factory.rb
176
192
  - test/plum/test_frame_utils.rb
193
+ - test/plum/test_http_connection.rb
177
194
  - test/plum/test_https_connection.rb
178
195
  - test/plum/test_stream.rb
179
196
  - test/plum/test_stream_utils.rb
@@ -203,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
220
  version: '0'
204
221
  requirements: []
205
222
  rubyforge_project:
206
- rubygems_version: 2.5.0
223
+ rubygems_version: 2.4.5
207
224
  signing_key:
208
225
  specification_version: 4
209
226
  summary: A minimal implementation of HTTP/2 server.
@@ -218,10 +235,12 @@ test_files:
218
235
  - test/plum/test_connection.rb
219
236
  - test/plum/test_connection_utils.rb
220
237
  - test/plum/test_error.rb
238
+ - test/plum/test_event_emitter.rb
221
239
  - test/plum/test_flow_control.rb
222
240
  - test/plum/test_frame.rb
223
241
  - test/plum/test_frame_factory.rb
224
242
  - test/plum/test_frame_utils.rb
243
+ - test/plum/test_http_connection.rb
225
244
  - test/plum/test_https_connection.rb
226
245
  - test/plum/test_stream.rb
227
246
  - test/plum/test_stream_utils.rb