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 +4 -4
- data/README.md +2 -3
- data/examples/local_server.rb +1 -0
- data/examples/non_tls_server.rb +127 -0
- data/examples/static_server.rb +1 -1
- data/lib/plum.rb +1 -0
- data/lib/plum/connection.rb +39 -26
- data/lib/plum/errors.rb +1 -0
- data/lib/plum/hpack/encoder.rb +30 -16
- data/lib/plum/http_connection.rb +59 -16
- data/lib/plum/https_connection.rb +23 -16
- data/lib/plum/version.rb +1 -1
- data/plum.gemspec +1 -0
- data/test/plum/hpack/test_context.rb +2 -2
- data/test/plum/hpack/test_encoder.rb +24 -7
- data/test/plum/stream/test_handle_frame.rb +1 -1
- data/test/plum/test_event_emitter.rb +31 -0
- data/test/plum/test_http_connection.rb +62 -0
- data/test/plum/test_https_connection.rb +51 -0
- metadata +22 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa11d64c7ed018b7ede5e9275adcd9abdb091003
|
4
|
+
data.tar.gz: 5fb20e87686039b5dec5f9b053719935b9788c06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
10
|
-
* Stream Priority (RFC 7540 5.3)
|
11
|
-
* Better API
|
10
|
+
* **Better API**
|
12
11
|
|
13
12
|
## License
|
14
13
|
MIT License
|
data/examples/local_server.rb
CHANGED
@@ -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("<", "<").gsub(">", ">")}<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
|
data/examples/static_server.rb
CHANGED
data/lib/plum.rb
CHANGED
data/lib/plum/connection.rb
CHANGED
@@ -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.
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
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
data/lib/plum/hpack/encoder.rb
CHANGED
@@ -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
|
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
|
22
|
+
out << encode_half_indexed(index, value)
|
20
23
|
else
|
21
|
-
out << encode_literal(name, value
|
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
|
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
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
data/lib/plum/http_connection.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
38
|
+
raise LegacyHTTPError.new
|
14
39
|
end
|
15
|
-
|
16
|
-
|
40
|
+
}
|
41
|
+
|
42
|
+
parser
|
17
43
|
end
|
18
44
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
30
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
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
|
-
|
57
|
+
klass = Class.new {
|
58
58
|
include Plum::HPACK::Context
|
59
59
|
public *Plum::HPACK::Context.private_instance_methods
|
60
60
|
}
|
61
|
-
|
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(
|
47
|
-
Plum::HPACK::Encoder.new(
|
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
|
-
|
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.
|
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-
|
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
|
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
|