plum 0.0.1 → 0.0.2

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