plum 0.0.2 → 0.0.3
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/.travis.yml +2 -2
- data/README.md +9 -3
- data/lib/plum/connection.rb +6 -6
- data/lib/plum/connection_utils.rb +5 -0
- data/lib/plum/errors.rb +10 -1
- data/lib/plum/flow_control.rb +2 -0
- data/lib/plum/http_connection.rb +1 -1
- data/lib/plum/stream.rb +1 -0
- data/lib/plum/stream_utils.rb +11 -4
- data/lib/plum/version.rb +1 -1
- data/test/plum/test_https_connection.rb +4 -4
- metadata +3 -4
- data/examples/local_server.rb +0 -207
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4fae3d5fc60fdcda63be201fe0349e497ed0c4a2
|
4
|
+
data.tar.gz: e9a1ac852f57acfef9d1eb9585c5b5cca427ce3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f66ed1a0dda7c39d14b0126d871b0c9978069c2b1a27bcb541d9dc9e0e5c0654d86439ff5c27a650d877f827a3a924ee20028f38245d5a9ceb09f3f69a1998c
|
7
|
+
data.tar.gz: 95a3eb71873bb321e4ee683820b4057205a2fde167d38b1e09f605f0dd70e606f0ba6f67e1b3fd6d9a6a08e12c77167d1aee32cd7bec034278892f33651c682f
|
data/.travis.yml
CHANGED
@@ -7,8 +7,8 @@ install:
|
|
7
7
|
- echo openssl_version=1.0.2d >> $rvm_path/user/db
|
8
8
|
- rvm pkg install openssl
|
9
9
|
- $rvm_path/usr/bin/openssl version
|
10
|
-
- rvm install 2.2.
|
11
|
-
- rvm use 2.2.
|
10
|
+
- rvm install 2.2.3-alpn --patch https://gist.githubusercontent.com/rhenium/b1711edcc903e8887a51/raw/2309e469f5a3ba15917d804ac61b19e62b3d8faf/ruby-openssl-alpn-no-tests-and-docs.patch --with-openssl-dir=$rvm_path/usr
|
11
|
+
- rvm use 2.2.3-alpn
|
12
12
|
- bundle install
|
13
13
|
script:
|
14
14
|
- bundle exec rake test
|
data/README.md
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
# Plum [](https://travis-ci.org/rhenium/plum) [](https://codeclimate.com/github/rhenium/plum) [](https://codeclimate.com/github/rhenium/plum/coverage)
|
2
2
|
A minimal implementation of HTTP/2 server.
|
3
3
|
|
4
|
+
## Examples
|
5
|
+
* examples/ - Minimal usage.
|
6
|
+
* [rhenium/plum-server](https://github.com/rhenium/plum-server) - A example server for https://rhe.jp and http://rhe.jp.
|
7
|
+
|
4
8
|
## Requirements
|
5
|
-
*
|
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)
|
7
|
-
*
|
9
|
+
* Ruby
|
10
|
+
* Ruby 2.2 with [ALPN support patch](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support patch (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff)
|
11
|
+
* or latest Ruby 2.3.0-dev
|
12
|
+
* OpenSSL 1.0.2 or newer (HTTP/2 requires ALPN)
|
13
|
+
* [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) (HTTP/1.1 parser; if you use "http" URI scheme)
|
8
14
|
|
9
15
|
## TODO
|
10
16
|
* **Better API**
|
data/lib/plum/connection.rb
CHANGED
@@ -74,7 +74,7 @@ module Plum
|
|
74
74
|
#
|
75
75
|
# @param args [Hash] The argument to pass to Stram.new.
|
76
76
|
def reserve_stream(**args)
|
77
|
-
next_id = (
|
77
|
+
next_id = (@streams.keys.select(&:even?).max || 0) + 2
|
78
78
|
stream = new_stream(next_id, state: :reserved_local, **args)
|
79
79
|
stream
|
80
80
|
end
|
@@ -98,10 +98,6 @@ module Plum
|
|
98
98
|
end
|
99
99
|
|
100
100
|
def new_stream(stream_id, **args)
|
101
|
-
if @streams.size > 0 && @streams.keys.max >= stream_id
|
102
|
-
raise Plum::ConnectionError.new(:protocol_error)
|
103
|
-
end
|
104
|
-
|
105
101
|
stream = Stream.new(self, stream_id, **args)
|
106
102
|
callback(:stream, stream)
|
107
103
|
@streams[stream_id] = stream
|
@@ -142,7 +138,10 @@ module Plum
|
|
142
138
|
if @streams.key?(frame.stream_id)
|
143
139
|
stream = @streams[frame.stream_id]
|
144
140
|
else
|
145
|
-
|
141
|
+
if frame.stream_id.even? || (@streams.size > 0 && @streams.keys.select(&:odd?).max >= frame.stream_id)
|
142
|
+
raise Plum::ConnectionError.new(:protocol_error)
|
143
|
+
end
|
144
|
+
|
146
145
|
stream = new_stream(frame.stream_id)
|
147
146
|
end
|
148
147
|
stream.receive_frame(frame)
|
@@ -162,6 +161,7 @@ module Plum
|
|
162
161
|
when :ping
|
163
162
|
receive_ping(frame)
|
164
163
|
when :goaway
|
164
|
+
callback(:goaway, frame)
|
165
165
|
goaway
|
166
166
|
close
|
167
167
|
when :data, :headers, :priority, :rst_stream, :push_promise, :continuation
|
@@ -26,6 +26,11 @@ module Plum
|
|
26
26
|
send_immediately Frame.goaway(last_id, error_type)
|
27
27
|
end
|
28
28
|
|
29
|
+
# Returns whether peer enables server push or not
|
30
|
+
def push_enabled?
|
31
|
+
@remote_settings[:enable_push] == 1
|
32
|
+
end
|
33
|
+
|
29
34
|
private
|
30
35
|
def update_local_settings(new_settings)
|
31
36
|
old_settings = @local_settings.dup
|
data/lib/plum/errors.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
module Plum
|
2
2
|
class Error < StandardError; end
|
3
|
-
class LegacyHTTPError < Error; end
|
4
3
|
class HPACKError < Error; end
|
5
4
|
class HTTPError < Error
|
6
5
|
ERROR_CODES = {
|
@@ -33,4 +32,14 @@ module Plum
|
|
33
32
|
end
|
34
33
|
class ConnectionError < HTTPError; end
|
35
34
|
class StreamError < HTTPError; end
|
35
|
+
|
36
|
+
class LegacyHTTPError < Error
|
37
|
+
attr_reader :headers, :data, :parser
|
38
|
+
|
39
|
+
def initialize(headers, data, parser)
|
40
|
+
@headers = headers
|
41
|
+
@data = data
|
42
|
+
@parser = parser
|
43
|
+
end
|
44
|
+
end
|
36
45
|
end
|
data/lib/plum/flow_control.rb
CHANGED
data/lib/plum/http_connection.rb
CHANGED
data/lib/plum/stream.rb
CHANGED
data/lib/plum/stream_utils.rb
CHANGED
@@ -2,9 +2,9 @@ using Plum::BinaryString
|
|
2
2
|
|
3
3
|
module Plum
|
4
4
|
module StreamUtils
|
5
|
-
# Responds to HTTP request.
|
5
|
+
# Responds to a HTTP request.
|
6
6
|
#
|
7
|
-
# @param headers [
|
7
|
+
# @param headers [Enumerable<String, String>] The response headers.
|
8
8
|
# @param body [String, IO] The response body.
|
9
9
|
def respond(headers, body = nil, end_stream: true) # TODO: priority, padding
|
10
10
|
if body
|
@@ -17,7 +17,7 @@ module Plum
|
|
17
17
|
|
18
18
|
# Reserves a stream to server push. Sends PUSH_PROMISE and create new stream.
|
19
19
|
#
|
20
|
-
# @param headers [
|
20
|
+
# @param headers [Enumerable<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
|
21
21
|
# @return [Stream] The stream to send push response.
|
22
22
|
def promise(headers)
|
23
23
|
stream = @connection.reserve_stream(weight: self.weight + 1, parent: self)
|
@@ -29,7 +29,10 @@ module Plum
|
|
29
29
|
stream
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
# Sends response headers. If the encoded frame is larger than MAX_FRAME_SIZE, the headers will be splitted into HEADERS frame and CONTINUATION frame(s).
|
33
|
+
#
|
34
|
+
# @param headers [Enumerable<String, String>] The response headers.
|
35
|
+
# @param end_stream [Boolean] Set END_STREAM flag or not.
|
33
36
|
def send_headers(headers, end_stream:)
|
34
37
|
max = @connection.remote_settings[:max_frame_size]
|
35
38
|
encoded = @connection.hpack_encoder.encode(headers)
|
@@ -40,6 +43,10 @@ module Plum
|
|
40
43
|
@state = :half_closed_local if end_stream
|
41
44
|
end
|
42
45
|
|
46
|
+
# Sends DATA frame. If the data is larger than MAX_FRAME_SIZE, DATA frame will be splitted.
|
47
|
+
#
|
48
|
+
# @param data [String, IO] The data to send.
|
49
|
+
# @param end_stream [Boolean] Set END_STREAM flag or not.
|
43
50
|
def send_data(data, end_stream: true)
|
44
51
|
max = @connection.remote_settings[:max_frame_size]
|
45
52
|
if data.is_a?(IO)
|
data/lib/plum/version.rb
CHANGED
@@ -47,7 +47,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
|
|
47
47
|
|
48
48
|
server_thread = Thread.new {
|
49
49
|
begin
|
50
|
-
timeout(3) {
|
50
|
+
Timeout.timeout(3) {
|
51
51
|
sock = ssl_server.accept
|
52
52
|
plum = HTTPSConnection.new(sock)
|
53
53
|
assert_connection_error(:inadequate_security) {
|
@@ -55,7 +55,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
|
|
55
55
|
plum.run
|
56
56
|
}
|
57
57
|
}
|
58
|
-
rescue
|
58
|
+
rescue Timeout::Error
|
59
59
|
flunk "server timeout"
|
60
60
|
ensure
|
61
61
|
tcp_server.close
|
@@ -64,7 +64,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
|
|
64
64
|
client_thread = Thread.new {
|
65
65
|
sock = TCPSocket.new("127.0.0.1", LISTEN_PORT)
|
66
66
|
begin
|
67
|
-
timeout(3) {
|
67
|
+
Timeout.timeout(3) {
|
68
68
|
ctx = OpenSSL::SSL::SSLContext.new.tap {|ctx|
|
69
69
|
ctx.alpn_protocols = ["h2"]
|
70
70
|
ctx.ciphers = "AES256-GCM-SHA384"
|
@@ -74,7 +74,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
|
|
74
74
|
ssl.write Connection::CLIENT_CONNECTION_PREFACE
|
75
75
|
ssl.write Frame.settings.assemble
|
76
76
|
}
|
77
|
-
rescue
|
77
|
+
rescue Timeout::Error
|
78
78
|
flunk "client timeout"
|
79
79
|
ensure
|
80
80
|
sock.close
|
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- rhenium
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -151,7 +151,6 @@ files:
|
|
151
151
|
- README.md
|
152
152
|
- Rakefile
|
153
153
|
- bin/.gitkeep
|
154
|
-
- examples/local_server.rb
|
155
154
|
- examples/non_tls_server.rb
|
156
155
|
- examples/static_server.rb
|
157
156
|
- lib/plum.rb
|
@@ -220,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
219
|
version: '0'
|
221
220
|
requirements: []
|
222
221
|
rubyforge_project:
|
223
|
-
rubygems_version: 2.4.5
|
222
|
+
rubygems_version: 2.4.5.1
|
224
223
|
signing_key:
|
225
224
|
specification_version: 4
|
226
225
|
summary: A minimal implementation of HTTP/2 server.
|
data/examples/local_server.rb
DELETED
@@ -1,207 +0,0 @@
|
|
1
|
-
DEBUG = ENV["DEBUG"] || false
|
2
|
-
HOST = ENV["HOST"]
|
3
|
-
PORT = ENV["PORT"]
|
4
|
-
DOCUMENT_ROOT = ENV["DOCUMENT_ROOT"] || "/srv/http"
|
5
|
-
TLS_CERT = ENV["TLS_CERT"]
|
6
|
-
TLS_KEY = ENV["TLS_KEY"]
|
7
|
-
|
8
|
-
CONTENT_TYPES = {
|
9
|
-
/\.html$/ => "text/html",
|
10
|
-
/\.png$/ => "image/png",
|
11
|
-
/\.jpg$/ => "image/jpeg",
|
12
|
-
/\.css$/ => "text/css",
|
13
|
-
/\.js$/ => "application/javascript",
|
14
|
-
/\.atom$/ => "application/atom+xml",
|
15
|
-
}
|
16
|
-
|
17
|
-
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
18
|
-
require "plum"
|
19
|
-
require "openssl"
|
20
|
-
require "socket"
|
21
|
-
begin
|
22
|
-
require "oga"
|
23
|
-
HAVE_OGA = true
|
24
|
-
rescue LoadError
|
25
|
-
puts "Oga is needed for parsing HTML"
|
26
|
-
HAVE_OGA = false
|
27
|
-
end
|
28
|
-
|
29
|
-
begin
|
30
|
-
require "sslkeylog/autotrace" # for debug
|
31
|
-
rescue LoadError
|
32
|
-
end
|
33
|
-
|
34
|
-
def log(con, stream, s)
|
35
|
-
prefix = "[%02x;%02x] " % [con, stream]
|
36
|
-
if s.is_a?(Enumerable)
|
37
|
-
puts s.map {|a| prefix + a.to_s }.join("\n")
|
38
|
-
else
|
39
|
-
puts prefix + s.to_s
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def content_type(filename)
|
44
|
-
exp, ct = CONTENT_TYPES.lazy.select {|pat, e| pat =~ filename }.first
|
45
|
-
ct || "texp/plain"
|
46
|
-
end
|
47
|
-
|
48
|
-
def assets(file)
|
49
|
-
if /\.html$/ =~ File.basename(file)
|
50
|
-
doc = Oga.parse_html(File.read(file))
|
51
|
-
assets = []
|
52
|
-
doc.xpath("img").each {|img| assets << img.get("src") }
|
53
|
-
doc.xpath("//html/head/link[@rel='stylesheet']").each {|css| assets << css.get("href") }
|
54
|
-
doc.xpath("script").each {|js| assets << js.get("src") }
|
55
|
-
assets.compact.uniq.map {|path|
|
56
|
-
if path.include?("//")
|
57
|
-
next nil
|
58
|
-
end
|
59
|
-
|
60
|
-
if path.start_with?("/")
|
61
|
-
pa = File.expand_path(DOCUMENT_ROOT + path)
|
62
|
-
else
|
63
|
-
pa = File.expand_path(path, file)
|
64
|
-
end
|
65
|
-
|
66
|
-
if pa.start_with?(DOCUMENT_ROOT) & File.exist?(pa)
|
67
|
-
pa
|
68
|
-
else
|
69
|
-
nil
|
70
|
-
end
|
71
|
-
}.compact
|
72
|
-
else
|
73
|
-
[]
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
78
|
-
ctx.ssl_version = :TLSv1_2
|
79
|
-
ctx.alpn_select_cb = -> protocols {
|
80
|
-
raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
|
81
|
-
"h2"
|
82
|
-
}
|
83
|
-
ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
|
84
|
-
OpenSSL::PKey::EC.new("prime256v1")
|
85
|
-
}
|
86
|
-
|
87
|
-
ctx.cert = OpenSSL::X509::Certificate.new File.read(TLS_CERT)
|
88
|
-
ctx.key = OpenSSL::PKey::RSA.new File.read(TLS_KEY)
|
89
|
-
tcp_server = TCPServer.new(HOST, PORT)
|
90
|
-
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
|
91
|
-
|
92
|
-
loop do
|
93
|
-
begin
|
94
|
-
sock = ssl_server.accept
|
95
|
-
id = sock.io.fileno
|
96
|
-
puts "#{id}: accept!"
|
97
|
-
rescue => e
|
98
|
-
puts e
|
99
|
-
next
|
100
|
-
end
|
101
|
-
|
102
|
-
plum = Plum::HTTPSConnection.new(sock)
|
103
|
-
|
104
|
-
plum.on(:frame) do |frame|
|
105
|
-
log(id, frame.stream_id, "recv: #{frame.inspect}")
|
106
|
-
end if DEBUG
|
107
|
-
|
108
|
-
plum.on(:send_frame) do |frame|
|
109
|
-
log(id, frame.stream_id, "send: #{frame.inspect}")
|
110
|
-
end if DEBUG
|
111
|
-
|
112
|
-
plum.on(:remote_settings) do |settings|
|
113
|
-
log(id, 0, settings.map {|name, value| "#{name}: #{value}" }) if DEBUG
|
114
|
-
end
|
115
|
-
|
116
|
-
plum.on(:connection_error) do |exception|
|
117
|
-
puts exception
|
118
|
-
puts exception.backtrace
|
119
|
-
end if DEBUG
|
120
|
-
|
121
|
-
plum.on(:stream) do |stream|
|
122
|
-
log(id, stream.id, "stream open")
|
123
|
-
stream.on(:stream_error) do |exception|
|
124
|
-
puts exception
|
125
|
-
puts exception.backtrace
|
126
|
-
end if DEBUG
|
127
|
-
|
128
|
-
headers = data = nil
|
129
|
-
|
130
|
-
stream.on(:open) do
|
131
|
-
headers = nil
|
132
|
-
data = ""
|
133
|
-
end
|
134
|
-
|
135
|
-
stream.on(:headers) do |headers_|
|
136
|
-
log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" }) if DEBUG
|
137
|
-
headers = headers_.to_h
|
138
|
-
end
|
139
|
-
|
140
|
-
stream.on(:data) do |data_|
|
141
|
-
log(id, stream.id, data_) if DEBUG
|
142
|
-
data << data_
|
143
|
-
end
|
144
|
-
|
145
|
-
stream.on(:end_stream) do
|
146
|
-
if headers[":method"] == "GET"
|
147
|
-
file = File.expand_path(DOCUMENT_ROOT + headers[":path"])
|
148
|
-
file << "/index.html" if Dir.exist?(file)
|
149
|
-
if file.start_with?(DOCUMENT_ROOT) && File.exist?(file)
|
150
|
-
io = File.open(file)
|
151
|
-
size = File.stat(file).size
|
152
|
-
i_sts = assets(file).map {|asset|
|
153
|
-
i_st = stream.promise({
|
154
|
-
":authority": headers[":authority"],
|
155
|
-
":method": "GET",
|
156
|
-
":scheme": "https",
|
157
|
-
":path": asset[DOCUMENT_ROOT.size..-1]
|
158
|
-
})
|
159
|
-
[i_st, asset]
|
160
|
-
}
|
161
|
-
stream.respond({
|
162
|
-
":status": "200",
|
163
|
-
"server": "plum/#{Plum::VERSION}",
|
164
|
-
"content-type": content_type(file),
|
165
|
-
"content-length": size
|
166
|
-
}, io)
|
167
|
-
i_sts.each do |i_st, asset|
|
168
|
-
aio = File.open(asset)
|
169
|
-
asize = File.stat(asset).size
|
170
|
-
i_st.respond({
|
171
|
-
":status": "200",
|
172
|
-
"server": "plum/#{Plum::VERSION}",
|
173
|
-
"content-type": content_type(asset),
|
174
|
-
"content-length": asize
|
175
|
-
}, aio)
|
176
|
-
end
|
177
|
-
else
|
178
|
-
body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") + "\n" + data
|
179
|
-
stream.respond({
|
180
|
-
":status": "404",
|
181
|
-
"server": "plum/#{Plum::VERSION}",
|
182
|
-
"content-type": "text/plain",
|
183
|
-
"content-length": body.bytesize
|
184
|
-
}, body)
|
185
|
-
end
|
186
|
-
else
|
187
|
-
# Not implemented
|
188
|
-
body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") << "\n" << data
|
189
|
-
stream.respond({
|
190
|
-
":status": "501",
|
191
|
-
"server": "plum/#{Plum::VERSION}",
|
192
|
-
"content-type": "text/plain",
|
193
|
-
"content-length": body.bytesize
|
194
|
-
}, body)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
Thread.new {
|
200
|
-
begin
|
201
|
-
plum.run
|
202
|
-
rescue
|
203
|
-
puts $!
|
204
|
-
puts $!.backtrace
|
205
|
-
end
|
206
|
-
}
|
207
|
-
end
|