plum 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/rhenium/plum.png?branch=master)](https://travis-ci.org/rhenium/plum) [![Code Climate](https://codeclimate.com/github/rhenium/plum/badges/gpa.svg)](https://codeclimate.com/github/rhenium/plum) [![Test Coverage](https://codeclimate.com/github/rhenium/plum/badges/coverage.svg)](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
|