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 +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
|