tipi 0.42 → 0.47
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/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +1 -3
- data/CHANGELOG.md +27 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +35 -29
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +45 -21
- data/bin/benchmark +0 -0
- data/bin/h1pd +0 -0
- data/bm.png +0 -0
- data/df/agent.rb +1 -1
- data/df/sample_agent.rb +2 -2
- data/df/server.rb +2 -0
- data/df/server_utils.rb +12 -15
- data/examples/hello.rb +5 -0
- data/examples/hello.ru +3 -3
- data/examples/http_server.js +1 -1
- data/examples/http_server_graceful.rb +1 -1
- data/examples/https_server.rb +41 -18
- data/examples/rack_server_forked.rb +26 -0
- data/examples/rack_server_https_forked.rb +1 -1
- data/examples/websocket_demo.rb +1 -1
- data/lib/tipi/acme.rb +51 -39
- data/lib/tipi/cli.rb +79 -16
- data/lib/tipi/config_dsl.rb +13 -13
- data/lib/tipi/configuration.rb +2 -2
- data/lib/tipi/controller/bare_polyphony.rb +0 -0
- data/lib/tipi/controller/bare_stock.rb +10 -0
- data/lib/tipi/controller/extensions.rb +37 -0
- data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +353 -0
- data/lib/tipi/controller/web_stock.rb +635 -0
- data/lib/tipi/controller.rb +12 -0
- data/lib/tipi/digital_fabric/agent.rb +3 -3
- data/lib/tipi/digital_fabric/agent_proxy.rb +11 -5
- data/lib/tipi/digital_fabric/executive.rb +1 -1
- data/lib/tipi/digital_fabric/protocol.rb +1 -1
- data/lib/tipi/digital_fabric/service.rb +12 -8
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +36 -30
- data/lib/tipi/http2_adapter.rb +10 -10
- data/lib/tipi/http2_stream.rb +14 -15
- data/lib/tipi/rack_adapter.rb +2 -2
- data/lib/tipi/response_extensions.rb +1 -1
- data/lib/tipi/supervisor.rb +75 -0
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +3 -3
- data/lib/tipi.rb +4 -83
- data/test/coverage.rb +2 -2
- data/test/helper.rb +0 -1
- data/test/test_http_server.rb +14 -14
- data/test/test_request.rb +1 -1
- data/tipi.gemspec +6 -7
- metadata +58 -53
- data/ext/tipi/extconf.rb +0 -13
- data/ext/tipi/http1_parser.c +0 -823
- data/ext/tipi/http1_parser.h +0 -18
- data/ext/tipi/tipi_ext.c +0 -5
- data/security/http1.rb +0 -12
- data/test/test_http1_parser.rb +0 -586
data/examples/https_server.rb
CHANGED
@@ -10,27 +10,50 @@ authority = Localhost::Authority.fetch
|
|
10
10
|
opts = {
|
11
11
|
reuse_addr: true,
|
12
12
|
dont_linger: true,
|
13
|
-
secure_context: authority.server_context
|
14
13
|
}
|
15
14
|
|
16
15
|
puts "pid: #{Process.pid}"
|
17
16
|
puts 'Listening on port 1234...'
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
17
|
+
|
18
|
+
ctx = authority.server_context
|
19
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, opts)
|
20
|
+
loop do
|
21
|
+
socket = server.accept
|
22
|
+
client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
23
|
+
client.sync_close = true
|
24
|
+
spin do
|
25
|
+
state = {}
|
26
|
+
accept_thread = Thread.new do
|
27
|
+
puts "call client accept"
|
28
|
+
client.accept
|
29
|
+
state[:result] = :ok
|
30
|
+
rescue Exception => e
|
31
|
+
puts error: e
|
32
|
+
state[:result] = e
|
33
|
+
end
|
34
|
+
"wait for accept thread"
|
35
|
+
accept_thread.join
|
36
|
+
"accept thread done"
|
37
|
+
if state[:result].is_a?(Exception)
|
38
|
+
puts "Exception in SSL handshake: #{state[:result].inspect}"
|
39
|
+
next
|
40
|
+
end
|
41
|
+
Tipi.client_loop(client, opts) do |req|
|
42
|
+
p path: req.path
|
43
|
+
if req.path == '/stream'
|
44
|
+
req.send_headers('Foo' => 'Bar')
|
45
|
+
sleep 0.5
|
46
|
+
req.send_chunk("foo\n")
|
47
|
+
sleep 0.5
|
48
|
+
req.send_chunk("bar\n", done: true)
|
49
|
+
elsif req.path == '/upload'
|
50
|
+
body = req.read
|
51
|
+
req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
|
52
|
+
else
|
53
|
+
req.respond("Hello world!\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
client ? client.close : socket.close
|
31
58
|
end
|
32
|
-
# req.send_headers
|
33
|
-
# req.send_chunk("Method: #{req.method}\n")
|
34
|
-
# req.send_chunk("Path: #{req.path}\n")
|
35
|
-
# req.send_chunk("Query: #{req.query.inspect}\n", done: true)
|
36
59
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
|
6
|
+
app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
|
7
|
+
unless File.file?(app_path)
|
8
|
+
STDERR.puts "Please provide rack config file (there are some in the examples directory.)"
|
9
|
+
exit!
|
10
|
+
end
|
11
|
+
|
12
|
+
app = Tipi::RackAdapter.load(app_path)
|
13
|
+
opts = { reuse_addr: true, dont_linger: true }
|
14
|
+
|
15
|
+
server = Tipi.listen('0.0.0.0', 1234, opts)
|
16
|
+
puts 'listening on port 1234'
|
17
|
+
|
18
|
+
child_pids = []
|
19
|
+
4.times do
|
20
|
+
child_pids << Polyphony.fork do
|
21
|
+
puts "forked pid: #{Process.pid}"
|
22
|
+
server.each(&app)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
|
data/examples/websocket_demo.rb
CHANGED
data/lib/tipi/acme.rb
CHANGED
@@ -10,10 +10,11 @@ module Tipi
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class CertificateManager
|
13
|
-
def initialize(master_ctx:, store:, challenge_handler:)
|
13
|
+
def initialize(master_ctx:, store:, challenge_handler:, valid_hosts:)
|
14
14
|
@master_ctx = master_ctx
|
15
15
|
@store = store
|
16
16
|
@challenge_handler = challenge_handler
|
17
|
+
@valid_hosts = valid_hosts
|
17
18
|
@contexts = {}
|
18
19
|
@requests = Polyphony::Queue.new
|
19
20
|
@worker = spin { run }
|
@@ -35,32 +36,45 @@ module Tipi
|
|
35
36
|
IP_REGEXP = /^\d+\.\d+\.\d+\.\d+$/
|
36
37
|
|
37
38
|
def setup_sni_callback
|
38
|
-
@master_ctx.servername_cb = proc
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
wait_for_ctx(state)
|
47
|
-
p name: name, error: state if state[:error]
|
48
|
-
# Eventually we might want to return an error returned in
|
49
|
-
# state[:error]. For the time being we handle errors by returning the
|
50
|
-
# master context
|
51
|
-
state[:ctx] || @master_ctx
|
52
|
-
end
|
39
|
+
@master_ctx.servername_cb = proc { |_socket, name| get_ctx(name) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_ctx(name)
|
43
|
+
state = { ctx: nil }
|
44
|
+
|
45
|
+
if @valid_hosts
|
46
|
+
return nil unless @valid_hosts.include?(name)
|
53
47
|
end
|
48
|
+
|
49
|
+
ready_ctx = @contexts[name]
|
50
|
+
return ready_ctx if ready_ctx
|
51
|
+
return @master_ctx if name =~ IP_REGEXP
|
52
|
+
|
53
|
+
@requests << [name, state]
|
54
|
+
wait_for_ctx(state)
|
55
|
+
# Eventually we might want to return an error returned in
|
56
|
+
# state[:error]. For the time being we handle errors by returning the
|
57
|
+
# master context
|
58
|
+
state[:ctx] || @master_ctx
|
59
|
+
rescue => e
|
60
|
+
@master_ctx
|
54
61
|
end
|
55
|
-
|
62
|
+
|
63
|
+
MAX_WAIT_FOR_CTX_DURATION = 30
|
64
|
+
|
56
65
|
def wait_for_ctx(state)
|
66
|
+
t0 = Time.now
|
57
67
|
period = 0.00001
|
58
68
|
while !state[:ctx] && !state[:error]
|
59
69
|
orig_sleep period
|
60
|
-
|
70
|
+
if period < 0.1
|
71
|
+
period *= 2
|
72
|
+
elsif Time.now - t0 > MAX_WAIT_FOR_CTX_DURATION
|
73
|
+
raise "Timeout waiting for certificate provisioning"
|
74
|
+
end
|
61
75
|
end
|
62
76
|
end
|
63
|
-
|
77
|
+
|
64
78
|
def run
|
65
79
|
loop do
|
66
80
|
name, state = @requests.shift
|
@@ -69,13 +83,13 @@ module Tipi
|
|
69
83
|
state[:error] = e if state
|
70
84
|
end
|
71
85
|
end
|
72
|
-
|
86
|
+
|
73
87
|
LOCALHOST_REGEXP = /\.?localhost$/.freeze
|
74
88
|
|
75
89
|
def get_context(name)
|
76
90
|
@contexts[name] = setup_context(name)
|
77
91
|
end
|
78
|
-
|
92
|
+
|
79
93
|
def setup_context(name)
|
80
94
|
ctx = provision_context(name)
|
81
95
|
transfer_ctx_settings(ctx)
|
@@ -100,7 +114,7 @@ module Tipi
|
|
100
114
|
end
|
101
115
|
|
102
116
|
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
103
|
-
|
117
|
+
|
104
118
|
def parse_certificate(certificate)
|
105
119
|
certificate
|
106
120
|
.scan(CERTIFICATE_REGEXP)
|
@@ -126,17 +140,17 @@ module Tipi
|
|
126
140
|
@localhost_authority ||= Localhost::Authority.fetch
|
127
141
|
@localhost_authority.server_context
|
128
142
|
end
|
129
|
-
|
143
|
+
|
130
144
|
def private_key
|
131
145
|
@private_key ||= OpenSSL::PKey::RSA.new(4096)
|
132
146
|
end
|
133
|
-
|
147
|
+
|
134
148
|
ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
|
135
|
-
|
149
|
+
|
136
150
|
def acme_client
|
137
151
|
@acme_client ||= setup_acme_client
|
138
152
|
end
|
139
|
-
|
153
|
+
|
140
154
|
def setup_acme_client
|
141
155
|
client = Acme::Client.new(
|
142
156
|
private_key: private_key,
|
@@ -148,13 +162,12 @@ module Tipi
|
|
148
162
|
)
|
149
163
|
client
|
150
164
|
end
|
151
|
-
|
165
|
+
|
152
166
|
def provision_certificate(name)
|
153
|
-
p provision_certificate: name
|
154
167
|
order = acme_client.new_order(identifiers: [name])
|
155
168
|
authorization = order.authorizations.first
|
156
169
|
challenge = authorization.http
|
157
|
-
|
170
|
+
|
158
171
|
@challenge_handler.add(challenge)
|
159
172
|
challenge.request_validation
|
160
173
|
while challenge.status == 'pending'
|
@@ -162,8 +175,7 @@ module Tipi
|
|
162
175
|
challenge.reload
|
163
176
|
end
|
164
177
|
raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid'
|
165
|
-
|
166
|
-
p challenge_status: challenge.status
|
178
|
+
|
167
179
|
private_key = OpenSSL::PKey::RSA.new(4096)
|
168
180
|
csr = Acme::Client::CertificateRequest.new(
|
169
181
|
private_key: private_key,
|
@@ -189,33 +201,33 @@ module Tipi
|
|
189
201
|
}
|
190
202
|
end
|
191
203
|
end
|
192
|
-
|
204
|
+
|
193
205
|
class HTTPChallengeHandler
|
194
206
|
def initialize
|
195
207
|
@challenges = {}
|
196
208
|
end
|
197
|
-
|
209
|
+
|
198
210
|
def add(challenge)
|
199
211
|
path = "/.well-known/acme-challenge/#{challenge.token}"
|
200
212
|
@challenges[path] = challenge
|
201
213
|
end
|
202
|
-
|
214
|
+
|
203
215
|
def remove(challenge)
|
204
216
|
path = "/.well-known/acme-challenge/#{challenge.token}"
|
205
217
|
@challenges.delete(path)
|
206
218
|
end
|
207
|
-
|
219
|
+
|
208
220
|
def call(req)
|
209
221
|
challenge = @challenges[req.path]
|
210
|
-
|
222
|
+
|
211
223
|
# handle incoming request
|
212
224
|
challenge = @challenges[req.path]
|
213
225
|
return req.respond(nil, ':status' => 400) unless challenge
|
214
|
-
|
226
|
+
|
215
227
|
req.respond(challenge.file_content, 'content-type' => challenge.content_type)
|
216
228
|
end
|
217
|
-
end
|
218
|
-
|
229
|
+
end
|
230
|
+
|
219
231
|
class CertificateStore
|
220
232
|
def set(name, private_key:, certificate:, expired_stamp:)
|
221
233
|
raise NotImplementedError
|
data/lib/tipi/cli.rb
CHANGED
@@ -2,29 +2,92 @@
|
|
2
2
|
|
3
3
|
require 'tipi'
|
4
4
|
require 'fileutils'
|
5
|
+
require 'tipi/supervisor'
|
6
|
+
require 'optparse'
|
5
7
|
|
6
8
|
module Tipi
|
9
|
+
DEFAULT_OPTS = {
|
10
|
+
app_type: :web,
|
11
|
+
mode: :polyphony,
|
12
|
+
workers: 1,
|
13
|
+
threads: 1,
|
14
|
+
listen: ['http', 'localhost', 1234],
|
15
|
+
path: '.',
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.opts_from_argv(argv)
|
19
|
+
opts = DEFAULT_OPTS.dup
|
20
|
+
parser = OptionParser.new do |o|
|
21
|
+
o.banner = "Usage: tipi [options] path"
|
22
|
+
o.on('-h', '--help', 'Show this help') { puts o; exit }
|
23
|
+
o.on('-wNUM', '--workers NUM', 'Number of worker processes (default: 1)') do |v|
|
24
|
+
opts[:workers] = v
|
25
|
+
end
|
26
|
+
o.on('-tNUM', '--threads NUM', 'Number of worker threads (default: 1)') do |v|
|
27
|
+
opts[:threads] = v
|
28
|
+
opts[:mode] = :stock
|
29
|
+
end
|
30
|
+
o.on('-c', '--compatibility', 'Use compatibility mode') do
|
31
|
+
opts[:mode] = :stock
|
32
|
+
end
|
33
|
+
o.on('-lSPEC', '--listen SPEC', 'Setup HTTP listener') do |v|
|
34
|
+
opts[:listen] = parse_listen_spec('http', v)
|
35
|
+
end
|
36
|
+
o.on('-sSPEC', '--secure SPEC', 'Setup HTTPS listener (for localhost)') do |v|
|
37
|
+
opts[:listen] = parse_listen_spec('https', v)
|
38
|
+
end
|
39
|
+
o.on('-fSPEC', '--full-service SPEC', 'Setup HTTP/HTTPS listeners (with automatic certificates)') do |v|
|
40
|
+
opts[:listen] = parse_listen_spec('full', v)
|
41
|
+
end
|
42
|
+
o.on('-v', '--verbose', 'Verbose output') do
|
43
|
+
opts[:verbose] = true
|
44
|
+
end
|
45
|
+
end.parse!(argv)
|
46
|
+
opts[:path] = argv.shift unless argv.empty?
|
47
|
+
verify_path(opts[:path])
|
48
|
+
opts
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.parse_listen_spec(type, spec)
|
52
|
+
[type, *spec.split(':').map { |s| str_to_native_type(s) }]
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.str_to_native_type(str)
|
56
|
+
case str
|
57
|
+
when /^\d+$/
|
58
|
+
str.to_i
|
59
|
+
else
|
60
|
+
str
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.verify_path(path)
|
65
|
+
return if File.file?(path) || File.directory?(path)
|
66
|
+
|
67
|
+
puts "Invalid path specified #{opts[:path]}"
|
68
|
+
exit!
|
69
|
+
end
|
70
|
+
|
7
71
|
module CLI
|
8
|
-
BANNER =
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
"
|
17
|
-
|
18
|
-
def self.start
|
19
|
-
|
20
|
-
|
72
|
+
BANNER =
|
73
|
+
"\n" +
|
74
|
+
" ooo\n" +
|
75
|
+
" oo\n" +
|
76
|
+
" o\n" +
|
77
|
+
" \\|/ Tipi - a better web server for a better world\n" +
|
78
|
+
" / \\ \n" +
|
79
|
+
" / \\ https://github.com/digital-fabric/tipi\n" +
|
80
|
+
"⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺\n"
|
81
|
+
|
82
|
+
def self.start(argv = ARGV.dup)
|
83
|
+
opts = Tipi.opts_from_argv(argv)
|
84
|
+
display_banner if STDOUT.tty? && !opts[:silent]
|
85
|
+
|
86
|
+
Tipi::Supervisor.run(opts)
|
21
87
|
end
|
22
88
|
|
23
89
|
def self.display_banner
|
24
90
|
puts BANNER
|
25
|
-
puts
|
26
91
|
end
|
27
92
|
end
|
28
93
|
end
|
29
|
-
|
30
|
-
__END__
|
data/lib/tipi/config_dsl.rb
CHANGED
@@ -4,28 +4,28 @@ module Tipi
|
|
4
4
|
module Configuration
|
5
5
|
class Interpreter
|
6
6
|
# make_blank_slate
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(assembler)
|
9
9
|
@assembler = assembler
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def gzip_response
|
13
13
|
@assembler.emit 'req = Tipi::GZip.wrap(req)'
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def log(out)
|
17
17
|
@assembler.wrap_current_frame 'logger.log_request(req) do |req|'
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def error(&block)
|
21
21
|
assembler.emit_exception_handler &block
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def match(pattern, &block)
|
25
25
|
@assembler.emit_conditional "if req.path =~ #{pattern.inspect}", &block
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
class Assembler
|
30
30
|
def self.from_source(code)
|
31
31
|
new.from_source code
|
@@ -36,7 +36,7 @@ module Tipi
|
|
36
36
|
@app_procs = {}
|
37
37
|
@interpreter = Interpreter.new self
|
38
38
|
@interpreter.instance_eval code
|
39
|
-
|
39
|
+
|
40
40
|
loop do
|
41
41
|
frame = @stack.pop
|
42
42
|
return assemble_app_proc(frame).join("\n") if @stack.empty?
|
@@ -51,7 +51,7 @@ module Tipi
|
|
51
51
|
body: []
|
52
52
|
}
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def add_frame(&block)
|
56
56
|
@stack.push new_frame
|
57
57
|
yield
|
@@ -67,20 +67,20 @@ module Tipi
|
|
67
67
|
@stack.push wrapper
|
68
68
|
@stack.push frame
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def emit(code)
|
72
72
|
@stack.last[:body] << code
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
def emit_prelude(code)
|
76
76
|
@stack.last[:prelude] << code
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
def emit_exception_handler(&block)
|
80
80
|
proc_id = add_app_proc block
|
81
81
|
@stack.last[:rescue_proc_id] = proc_id
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
def emit_block(conditional, &block)
|
85
85
|
proc_id = add_app_proc block
|
86
86
|
@stack.last[:branched] = true
|
@@ -93,7 +93,7 @@ module Tipi
|
|
93
93
|
@app_procs[id] = proc
|
94
94
|
id
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
def assemble_frame(frame)
|
98
98
|
indent = 0
|
99
99
|
lines = []
|
data/lib/tipi/configuration.rb
CHANGED
@@ -12,7 +12,7 @@ module Tipi
|
|
12
12
|
old_runner&.stop
|
13
13
|
end
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def run(config)
|
17
17
|
start_listeners(config)
|
18
18
|
config[:forked] ? forked_supervise(config) : simple_supervise(config)
|
@@ -29,7 +29,7 @@ module Tipi
|
|
29
29
|
suspend
|
30
30
|
# supervise(restart: true)
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def forked_supervise(config)
|
34
34
|
config[:forked].times do
|
35
35
|
spin { Polyphony.watch_process { simple_supervise(config) } }
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tipi'
|
4
|
+
|
5
|
+
module Kernel
|
6
|
+
def run(app = nil, &block)
|
7
|
+
Tipi.app = app || block
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Tipi
|
12
|
+
class << self
|
13
|
+
attr_writer :app
|
14
|
+
|
15
|
+
def app
|
16
|
+
return @app if @app
|
17
|
+
|
18
|
+
raise 'No app define. The app to run should be set using `Tipi.app = ...`'
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_sites(site_map)
|
22
|
+
sites = site_map.each_with_object({}) { |(k, v), h| h[k] = v.to_proc }
|
23
|
+
valid_hosts = sites.keys
|
24
|
+
|
25
|
+
@app = ->(req) {
|
26
|
+
handler = sites[req.host]
|
27
|
+
if handler
|
28
|
+
handler.call(req)
|
29
|
+
else
|
30
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
31
|
+
end
|
32
|
+
}
|
33
|
+
|
34
|
+
@app.define_singleton_method(:valid_hosts) { valid_hosts }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|