plum 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE +21 -0
- data/README.md +14 -0
- data/Rakefile +12 -0
- data/bin/.gitkeep +0 -0
- data/examples/local_server.rb +206 -0
- data/examples/static_server.rb +157 -0
- data/lib/plum.rb +21 -0
- data/lib/plum/binary_string.rb +74 -0
- data/lib/plum/connection.rb +201 -0
- data/lib/plum/connection_utils.rb +38 -0
- data/lib/plum/errors.rb +35 -0
- data/lib/plum/event_emitter.rb +19 -0
- data/lib/plum/flow_control.rb +97 -0
- data/lib/plum/frame.rb +163 -0
- data/lib/plum/frame_factory.rb +53 -0
- data/lib/plum/frame_utils.rb +50 -0
- data/lib/plum/hpack/constants.rb +331 -0
- data/lib/plum/hpack/context.rb +55 -0
- data/lib/plum/hpack/decoder.rb +145 -0
- data/lib/plum/hpack/encoder.rb +105 -0
- data/lib/plum/hpack/huffman.rb +42 -0
- data/lib/plum/http_connection.rb +33 -0
- data/lib/plum/https_connection.rb +24 -0
- data/lib/plum/stream.rb +217 -0
- data/lib/plum/stream_utils.rb +58 -0
- data/lib/plum/version.rb +3 -0
- data/plum.gemspec +29 -0
- data/test/plum/connection/test_handle_frame.rb +70 -0
- data/test/plum/hpack/test_context.rb +63 -0
- data/test/plum/hpack/test_decoder.rb +291 -0
- data/test/plum/hpack/test_encoder.rb +49 -0
- data/test/plum/hpack/test_huffman.rb +36 -0
- data/test/plum/stream/test_handle_frame.rb +262 -0
- data/test/plum/test_binary_string.rb +64 -0
- data/test/plum/test_connection.rb +96 -0
- data/test/plum/test_connection_utils.rb +29 -0
- data/test/plum/test_error.rb +13 -0
- data/test/plum/test_flow_control.rb +167 -0
- data/test/plum/test_frame.rb +59 -0
- data/test/plum/test_frame_factory.rb +56 -0
- data/test/plum/test_frame_utils.rb +46 -0
- data/test/plum/test_https_connection.rb +37 -0
- data/test/plum/test_stream.rb +32 -0
- data/test/plum/test_stream_utils.rb +16 -0
- data/test/server.crt +19 -0
- data/test/server.csr +16 -0
- data/test/server.key +27 -0
- data/test/test_helper.rb +28 -0
- data/test/utils/assertions.rb +60 -0
- data/test/utils/server.rb +63 -0
- metadata +234 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8e9aa768df8ce054c286452ca40c8e6f5671fda5
|
4
|
+
data.tar.gz: 3a5dafa95ec6dd94b6b354091a8eee73b88f3214
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3110fdf853b9ab1a7966adff6b24a3665bf792853a63bc814e295cee3a343761894b2fa1a48157a19c349998415bfee7710f3242400081ea8863b48f30028d4f
|
7
|
+
data.tar.gz: 061d1e6292eb1cd9d2fc6cd6db1daceb4e483970c577cf504f23deb23a407e44535fa27c57a819159198f41c7b248c015940485bab620a565cca84b0a7f14620
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
language: ruby
|
2
|
+
addons:
|
3
|
+
code_climate:
|
4
|
+
repo_token: f5092ab344fac7f2de9d7332e00597642a4d24e3d560f7d7f329172a2e5a2def
|
5
|
+
install:
|
6
|
+
- echo openssl_url=https://www.openssl.org/source >> $rvm_path/user/db
|
7
|
+
- echo openssl_version=1.0.2d >> $rvm_path/user/db
|
8
|
+
- rvm pkg install openssl
|
9
|
+
- $rvm_path/usr/bin/openssl version
|
10
|
+
- rvm install 2.2.2-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.2-alpn
|
12
|
+
- bundle install
|
13
|
+
script:
|
14
|
+
- bundle exec rake test
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
guard :minitest, env: { "SKIP_COVERAGE" => true } do
|
2
|
+
# with Minitest::Unit
|
3
|
+
watch(%r{^test/(.*)\/?test_(.*)\.rb$})
|
4
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) {|m| ["test/#{m[1]}test_#{m[2]}.rb", "test/#{m[1]}#{m[2]}"] }
|
5
|
+
watch(%r{^test/test_helper\.rb$}) { "test" }
|
6
|
+
watch(%r{^test/utils/.*\.rb}) { "test" }
|
7
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Kazuki Yamaguchi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,14 @@
|
|
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
|
+
A minimal implementation of HTTP/2 server.
|
3
|
+
|
4
|
+
## Requirements
|
5
|
+
* OpenSSL 1.0.2+
|
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
|
+
|
8
|
+
## TODO
|
9
|
+
* "http" URIs support (upgrade from HTTP/1.1)
|
10
|
+
* Stream Priority (RFC 7540 5.3)
|
11
|
+
* Better API
|
12
|
+
|
13
|
+
## License
|
14
|
+
MIT License
|
data/Rakefile
ADDED
data/bin/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,206 @@
|
|
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
|
+
}
|
15
|
+
|
16
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
17
|
+
require "plum"
|
18
|
+
require "openssl"
|
19
|
+
require "socket"
|
20
|
+
begin
|
21
|
+
require "oga"
|
22
|
+
HAVE_OGA = true
|
23
|
+
rescue LoadError
|
24
|
+
puts "Oga is needed for parsing HTML"
|
25
|
+
HAVE_OGA = false
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require "sslkeylog/autotrace" # for debug
|
30
|
+
rescue LoadError
|
31
|
+
end
|
32
|
+
|
33
|
+
def log(con, stream, s)
|
34
|
+
prefix = "[%02x;%02x] " % [con, stream]
|
35
|
+
if s.is_a?(Enumerable)
|
36
|
+
puts s.map {|a| prefix + a.to_s }.join("\n")
|
37
|
+
else
|
38
|
+
puts prefix + s.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def content_type(filename)
|
43
|
+
exp, ct = CONTENT_TYPES.lazy.select {|pat, e| pat =~ filename }.first
|
44
|
+
ct || "texp/plain"
|
45
|
+
end
|
46
|
+
|
47
|
+
def assets(file)
|
48
|
+
if /\.html$/ =~ File.basename(file)
|
49
|
+
doc = Oga.parse_html(File.read(file))
|
50
|
+
assets = []
|
51
|
+
doc.xpath("img").each {|img| assets << img.get("src") }
|
52
|
+
doc.xpath("//html/head/link[@rel='stylesheet']").each {|css| assets << css.get("href") }
|
53
|
+
doc.xpath("script").each {|js| assets << js.get("src") }
|
54
|
+
assets.compact.uniq.map {|path|
|
55
|
+
if path.include?("//")
|
56
|
+
next nil
|
57
|
+
end
|
58
|
+
|
59
|
+
if path.start_with?("/")
|
60
|
+
pa = File.expand_path(DOCUMENT_ROOT + path)
|
61
|
+
else
|
62
|
+
pa = File.expand_path(path, file)
|
63
|
+
end
|
64
|
+
|
65
|
+
if pa.start_with?(DOCUMENT_ROOT) & File.exist?(pa)
|
66
|
+
pa
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
}.compact
|
71
|
+
else
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
77
|
+
ctx.ssl_version = :TLSv1_2
|
78
|
+
ctx.alpn_select_cb = -> protocols {
|
79
|
+
raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
|
80
|
+
"h2"
|
81
|
+
}
|
82
|
+
ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
|
83
|
+
OpenSSL::PKey::EC.new("prime256v1")
|
84
|
+
}
|
85
|
+
|
86
|
+
ctx.cert = OpenSSL::X509::Certificate.new File.read(TLS_CERT)
|
87
|
+
ctx.key = OpenSSL::PKey::RSA.new File.read(TLS_KEY)
|
88
|
+
tcp_server = TCPServer.new(HOST, PORT)
|
89
|
+
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
|
90
|
+
|
91
|
+
loop do
|
92
|
+
begin
|
93
|
+
sock = ssl_server.accept
|
94
|
+
id = sock.io.fileno
|
95
|
+
puts "#{id}: accept!"
|
96
|
+
rescue => e
|
97
|
+
puts e
|
98
|
+
next
|
99
|
+
end
|
100
|
+
|
101
|
+
plum = Plum::HTTPSConnection.new(sock)
|
102
|
+
|
103
|
+
plum.on(:frame) do |frame|
|
104
|
+
log(id, frame.stream_id, "recv: #{frame.inspect}")
|
105
|
+
end if DEBUG
|
106
|
+
|
107
|
+
plum.on(:send_frame) do |frame|
|
108
|
+
log(id, frame.stream_id, "send: #{frame.inspect}")
|
109
|
+
end if DEBUG
|
110
|
+
|
111
|
+
plum.on(:remote_settings) do |settings|
|
112
|
+
log(id, 0, settings.map {|name, value| "#{name}: #{value}" }) if DEBUG
|
113
|
+
end
|
114
|
+
|
115
|
+
plum.on(:connection_error) do |exception|
|
116
|
+
puts exception
|
117
|
+
puts exception.backtrace
|
118
|
+
end if DEBUG
|
119
|
+
|
120
|
+
plum.on(:stream) do |stream|
|
121
|
+
log(id, stream.id, "stream open")
|
122
|
+
stream.on(:stream_error) do |exception|
|
123
|
+
puts exception
|
124
|
+
puts exception.backtrace
|
125
|
+
end if DEBUG
|
126
|
+
|
127
|
+
headers = data = nil
|
128
|
+
|
129
|
+
stream.on(:open) do
|
130
|
+
headers = nil
|
131
|
+
data = ""
|
132
|
+
end
|
133
|
+
|
134
|
+
stream.on(:headers) do |headers_|
|
135
|
+
log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" }) if DEBUG
|
136
|
+
headers = headers_.to_h
|
137
|
+
end
|
138
|
+
|
139
|
+
stream.on(:data) do |data_|
|
140
|
+
log(id, stream.id, data_) if DEBUG
|
141
|
+
data << data_
|
142
|
+
end
|
143
|
+
|
144
|
+
stream.on(:end_stream) do
|
145
|
+
if headers[":method"] == "GET"
|
146
|
+
file = File.expand_path(DOCUMENT_ROOT + headers[":path"])
|
147
|
+
file << "/index.html" if Dir.exist?(file)
|
148
|
+
if file.start_with?(DOCUMENT_ROOT) && File.exist?(file)
|
149
|
+
io = File.open(file)
|
150
|
+
size = File.stat(file).size
|
151
|
+
i_sts = assets(file).map {|asset|
|
152
|
+
i_st = stream.promise({
|
153
|
+
":authority": headers[":authority"],
|
154
|
+
":method": "GET",
|
155
|
+
":scheme": "https",
|
156
|
+
":path": asset[DOCUMENT_ROOT.size..-1]
|
157
|
+
})
|
158
|
+
[i_st, asset]
|
159
|
+
}
|
160
|
+
stream.respond({
|
161
|
+
":status": "200",
|
162
|
+
"server": "plum/#{Plum::VERSION}",
|
163
|
+
"content-type": content_type(file),
|
164
|
+
"content-length": size
|
165
|
+
}, io)
|
166
|
+
i_sts.each do |i_st, asset|
|
167
|
+
aio = File.open(asset)
|
168
|
+
asize = File.stat(asset).size
|
169
|
+
i_st.respond({
|
170
|
+
":status": "200",
|
171
|
+
"server": "plum/#{Plum::VERSION}",
|
172
|
+
"content-type": content_type(asset),
|
173
|
+
"content-length": asize
|
174
|
+
}, aio)
|
175
|
+
end
|
176
|
+
else
|
177
|
+
body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") + "\n" + data
|
178
|
+
stream.respond({
|
179
|
+
":status": "404",
|
180
|
+
"server": "plum/#{Plum::VERSION}",
|
181
|
+
"content-type": "text/plain",
|
182
|
+
"content-length": body.bytesize
|
183
|
+
}, body)
|
184
|
+
end
|
185
|
+
else
|
186
|
+
# Not implemented
|
187
|
+
body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") << "\n" << data
|
188
|
+
stream.respond({
|
189
|
+
":status": "501",
|
190
|
+
"server": "plum/#{Plum::VERSION}",
|
191
|
+
"content-type": "text/plain",
|
192
|
+
"content-length": body.bytesize
|
193
|
+
}, body)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
Thread.new {
|
199
|
+
begin
|
200
|
+
plum.run
|
201
|
+
rescue
|
202
|
+
puts $!
|
203
|
+
puts $!.backtrace
|
204
|
+
end
|
205
|
+
}
|
206
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
2
|
+
require "plum"
|
3
|
+
require "openssl"
|
4
|
+
require "socket"
|
5
|
+
require "cgi"
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "sslkeylog/autotrace"
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
def log(con, stream, s)
|
13
|
+
prefix = "[%02x;%02x] " % [con, stream]
|
14
|
+
if s.is_a?(Enumerable)
|
15
|
+
puts s.map {|a| prefix + a.to_s }.join("\n")
|
16
|
+
else
|
17
|
+
puts prefix + s.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
22
|
+
ctx.ssl_version = :TLSv1_2
|
23
|
+
ctx.alpn_select_cb = -> protocols {
|
24
|
+
raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
|
25
|
+
"h2"
|
26
|
+
}
|
27
|
+
ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
|
28
|
+
OpenSSL::PKey::EC.new("prime256v1")
|
29
|
+
}
|
30
|
+
ctx.cert = OpenSSL::X509::Certificate.new File.read(".crt.local")
|
31
|
+
ctx.key = OpenSSL::PKey::RSA.new File.read(".key.local")
|
32
|
+
tcp_server = TCPServer.new("0.0.0.0", 40443)
|
33
|
+
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
|
34
|
+
|
35
|
+
loop do
|
36
|
+
begin
|
37
|
+
sock = ssl_server.accept
|
38
|
+
id = sock.io.fileno
|
39
|
+
puts "#{id}: accept!"
|
40
|
+
rescue => e
|
41
|
+
STDERR.puts e
|
42
|
+
next
|
43
|
+
end
|
44
|
+
|
45
|
+
plum = Plum::HTTPSConnection.new(sock)
|
46
|
+
|
47
|
+
plum.on(:frame) do |frame|
|
48
|
+
log(id, frame.stream_id, "recv: #{frame.inspect}")
|
49
|
+
end
|
50
|
+
|
51
|
+
plum.on(:send_frame) do |frame|
|
52
|
+
log(id, frame.stream_id, "send: #{frame.inspect}")
|
53
|
+
end
|
54
|
+
|
55
|
+
plum.on(:connection_error) do |exception|
|
56
|
+
puts exception
|
57
|
+
puts exception.backtrace
|
58
|
+
end
|
59
|
+
|
60
|
+
plum.on(:stream) do |stream|
|
61
|
+
stream.on(:stream_error) do |exception|
|
62
|
+
puts exception
|
63
|
+
puts exception.backtrace
|
64
|
+
end
|
65
|
+
|
66
|
+
stream.on(:send_deferred) do |frame|
|
67
|
+
log(id, frame.stream_id, "send (deferred): #{frame.inspect}")
|
68
|
+
end
|
69
|
+
|
70
|
+
headers = data = nil
|
71
|
+
|
72
|
+
stream.on(:open) do
|
73
|
+
headers = nil
|
74
|
+
data = ""
|
75
|
+
end
|
76
|
+
|
77
|
+
stream.on(:headers) do |headers_|
|
78
|
+
log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" })
|
79
|
+
headers = headers_.to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
stream.on(:data) do |data_|
|
83
|
+
log(id, stream.id, data_)
|
84
|
+
data << data_
|
85
|
+
end
|
86
|
+
|
87
|
+
stream.on(:end_stream) do
|
88
|
+
case [headers[":method"], headers[":path"]]
|
89
|
+
when ["GET", "/"]
|
90
|
+
body = "Hello World! <a href=/abc.html>ABC</a> <a href=/fgsd>Not found</a>"
|
91
|
+
body << <<-EOF
|
92
|
+
<form action=post.page method=post>
|
93
|
+
<input type=text name=key value=default_value>
|
94
|
+
<input type=submit>
|
95
|
+
</form>
|
96
|
+
EOF
|
97
|
+
stream.respond({
|
98
|
+
":status": "200",
|
99
|
+
"server": "plum",
|
100
|
+
"content-type": "text/html",
|
101
|
+
"content-length": body.size
|
102
|
+
}, body)
|
103
|
+
when ["GET", "/abc.html"]
|
104
|
+
body = "ABC! <a href=/>Back to top page</a><br><img src=/image.nyan>"
|
105
|
+
i_stream = stream.promise({
|
106
|
+
":authority": "localhost:40443",
|
107
|
+
":method": "GET",
|
108
|
+
":scheme": "https",
|
109
|
+
":path": "/image.nyan"
|
110
|
+
})
|
111
|
+
stream.respond({
|
112
|
+
":status": "200",
|
113
|
+
"server": "plum",
|
114
|
+
"content-type": "text/html",
|
115
|
+
"content-length": body.size
|
116
|
+
}, body)
|
117
|
+
image = ("iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAACVBMVEX///93o0jG/4mTMy20AAAA" <<
|
118
|
+
"bklEQVQ4y2NgoAoIRQJkCoSimIdTgJGBBU1ABE1A1AVdBQuaACu6gCALhhZ0axlZCDgMWYAB6ilU" <<
|
119
|
+
"35IoADEMxWyyBDD45AhQCFahM0kXWIVu3sAJrILzyBcgytoFeATABBcXWohhCEC14BCgGAAAX1ZQ" <<
|
120
|
+
"ZtJp0zAAAAAASUVORK5CYII=").unpack("m")[0]
|
121
|
+
i_stream.respond({
|
122
|
+
":status": "200",
|
123
|
+
"server": "plum",
|
124
|
+
"content-type": "image/png",
|
125
|
+
"content-length": image.size
|
126
|
+
}, image)
|
127
|
+
when ["POST", "/post.page"]
|
128
|
+
body = "Posted value is: #{CGI.unescape(data).gsub("<", "<").gsub(">", ">")}<br> <a href=/>Back to top page</a>"
|
129
|
+
stream.respond({
|
130
|
+
":status": "200",
|
131
|
+
"server": "plum",
|
132
|
+
"content-type": "text/html",
|
133
|
+
"content-length": body.size
|
134
|
+
}, body)
|
135
|
+
else
|
136
|
+
body = "Page not found! <a href=/>Back to top page</a>"
|
137
|
+
stream.respond({
|
138
|
+
":status": "404",
|
139
|
+
"server": "plum",
|
140
|
+
"content-type": "text/html",
|
141
|
+
"content-length": body.size
|
142
|
+
}, body)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
Thread.new {
|
148
|
+
begin
|
149
|
+
plum.run
|
150
|
+
rescue
|
151
|
+
puts $!
|
152
|
+
puts $!.backtrace
|
153
|
+
ensure
|
154
|
+
sock.close
|
155
|
+
end
|
156
|
+
}
|
157
|
+
end
|