tipi 0.41 → 0.46
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 +3 -1
- data/.gitignore +3 -1
- data/CHANGELOG.md +34 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +53 -33
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/bm.png +0 -0
- data/df/agent.rb +1 -1
- data/df/sample_agent.rb +2 -2
- data/df/server.rb +3 -1
- data/df/server_utils.rb +48 -46
- data/examples/full_service.rb +13 -0
- data/examples/hello.rb +5 -0
- data/examples/hello.ru +3 -3
- data/examples/http1_parser.rb +10 -8
- data/examples/http_server.js +1 -1
- data/examples/http_server.rb +4 -1
- data/examples/http_server_graceful.rb +1 -1
- data/examples/https_server.rb +41 -15
- data/examples/rack_server_forked.rb +26 -0
- data/examples/rack_server_https_forked.rb +1 -1
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +1 -1
- data/lib/tipi/acme.rb +320 -0
- data/lib/tipi/cli.rb +93 -0
- 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 +5 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
- data/lib/tipi/digital_fabric/executive.rb +7 -3
- data/lib/tipi/digital_fabric/protocol.rb +3 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +17 -18
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +85 -124
- data/lib/tipi/http2_adapter.rb +29 -16
- data/lib/tipi/http2_stream.rb +52 -57
- 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 +9 -7
- data/test/coverage.rb +2 -2
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +14 -41
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +10 -10
- metadata +80 -54
- data/examples/automatic_certificate.rb +0 -193
- data/ext/tipi/extconf.rb +0 -12
- data/ext/tipi/http1_parser.c +0 -534
- data/ext/tipi/http1_parser.h +0 -18
- data/ext/tipi/tipi_ext.c +0 -5
- data/lib/tipi/http1_adapter_new.rb +0 -293
data/lib/tipi/acme.rb
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'acme-client'
|
5
|
+
require 'localhost/authority'
|
6
|
+
|
7
|
+
module Tipi
|
8
|
+
module ACME
|
9
|
+
class Error < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
class CertificateManager
|
13
|
+
def initialize(master_ctx:, store:, challenge_handler:, valid_hosts:)
|
14
|
+
@master_ctx = master_ctx
|
15
|
+
@store = store
|
16
|
+
@challenge_handler = challenge_handler
|
17
|
+
@valid_hosts = valid_hosts
|
18
|
+
@contexts = {}
|
19
|
+
@requests = Polyphony::Queue.new
|
20
|
+
@worker = spin { run }
|
21
|
+
setup_sni_callback
|
22
|
+
end
|
23
|
+
|
24
|
+
ACME_CHALLENGE_PATH_REGEXP = /\/\.well\-known\/acme\-challenge/.freeze
|
25
|
+
|
26
|
+
def challenge_routing_app(app)
|
27
|
+
->(req) do
|
28
|
+
(req.path =~ ACME_CHALLENGE_PATH_REGEXP ? @challenge_handler : app)
|
29
|
+
.(req)
|
30
|
+
rescue => e
|
31
|
+
puts "Error while handling request: #{e.inspect} (headers: #{req.headers})"
|
32
|
+
req.respond(nil, ':status' => Qeweney::Status::BAD_REQUEST)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
IP_REGEXP = /^\d+\.\d+\.\d+\.\d+$/
|
37
|
+
|
38
|
+
def setup_sni_callback
|
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)
|
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
|
61
|
+
end
|
62
|
+
|
63
|
+
MAX_WAIT_FOR_CTX_DURATION = 30
|
64
|
+
|
65
|
+
def wait_for_ctx(state)
|
66
|
+
t0 = Time.now
|
67
|
+
period = 0.00001
|
68
|
+
while !state[:ctx] && !state[:error]
|
69
|
+
orig_sleep period
|
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
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run
|
79
|
+
loop do
|
80
|
+
name, state = @requests.shift
|
81
|
+
state[:ctx] = get_context(name)
|
82
|
+
rescue => e
|
83
|
+
state[:error] = e if state
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
LOCALHOST_REGEXP = /\.?localhost$/.freeze
|
88
|
+
|
89
|
+
def get_context(name)
|
90
|
+
@contexts[name] = setup_context(name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def setup_context(name)
|
94
|
+
ctx = provision_context(name)
|
95
|
+
transfer_ctx_settings(ctx)
|
96
|
+
ctx
|
97
|
+
end
|
98
|
+
|
99
|
+
def provision_context(name)
|
100
|
+
return localhost_context if name =~ LOCALHOST_REGEXP
|
101
|
+
|
102
|
+
info = get_certificate(name)
|
103
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
104
|
+
chain = parse_certificate(info[:certificate])
|
105
|
+
cert = chain.shift
|
106
|
+
ctx.add_certificate(cert, info[:private_key], chain)
|
107
|
+
ctx
|
108
|
+
end
|
109
|
+
|
110
|
+
def transfer_ctx_settings(ctx)
|
111
|
+
ctx.alpn_protocols = @master_ctx.alpn_protocols
|
112
|
+
ctx.alpn_select_cb = @master_ctx.alpn_select_cb
|
113
|
+
ctx.ciphers = @master_ctx.ciphers
|
114
|
+
end
|
115
|
+
|
116
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
117
|
+
|
118
|
+
def parse_certificate(certificate)
|
119
|
+
certificate
|
120
|
+
.scan(CERTIFICATE_REGEXP)
|
121
|
+
.map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_expired_stamp(certificate)
|
125
|
+
chain = parse_certificate(certificate)
|
126
|
+
cert = chain.shift
|
127
|
+
cert.not_after
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_certificate(name)
|
131
|
+
entry = @store.get(name)
|
132
|
+
return entry if entry
|
133
|
+
|
134
|
+
provision_certificate(name).tap do |entry|
|
135
|
+
@store.set(name, **entry)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def localhost_context
|
140
|
+
@localhost_authority ||= Localhost::Authority.fetch
|
141
|
+
@localhost_authority.server_context
|
142
|
+
end
|
143
|
+
|
144
|
+
def private_key
|
145
|
+
@private_key ||= OpenSSL::PKey::RSA.new(4096)
|
146
|
+
end
|
147
|
+
|
148
|
+
ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
|
149
|
+
|
150
|
+
def acme_client
|
151
|
+
@acme_client ||= setup_acme_client
|
152
|
+
end
|
153
|
+
|
154
|
+
def setup_acme_client
|
155
|
+
client = Acme::Client.new(
|
156
|
+
private_key: private_key,
|
157
|
+
directory: ACME_DIRECTORY
|
158
|
+
)
|
159
|
+
account = client.new_account(
|
160
|
+
contact: 'mailto:info@noteflakes.com',
|
161
|
+
terms_of_service_agreed: true
|
162
|
+
)
|
163
|
+
client
|
164
|
+
end
|
165
|
+
|
166
|
+
def provision_certificate(name)
|
167
|
+
order = acme_client.new_order(identifiers: [name])
|
168
|
+
authorization = order.authorizations.first
|
169
|
+
challenge = authorization.http
|
170
|
+
|
171
|
+
@challenge_handler.add(challenge)
|
172
|
+
challenge.request_validation
|
173
|
+
while challenge.status == 'pending'
|
174
|
+
sleep(0.25)
|
175
|
+
challenge.reload
|
176
|
+
end
|
177
|
+
raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid'
|
178
|
+
|
179
|
+
private_key = OpenSSL::PKey::RSA.new(4096)
|
180
|
+
csr = Acme::Client::CertificateRequest.new(
|
181
|
+
private_key: private_key,
|
182
|
+
subject: { common_name: name }
|
183
|
+
)
|
184
|
+
order.finalize(csr: csr)
|
185
|
+
while order.status == 'processing'
|
186
|
+
sleep(0.25)
|
187
|
+
order.reload
|
188
|
+
end
|
189
|
+
certificate = begin
|
190
|
+
order.certificate(force_chain: 'DST Root CA X3')
|
191
|
+
rescue Acme::Client::Error::ForcedChainNotFound
|
192
|
+
order.certificate
|
193
|
+
end
|
194
|
+
expired_stamp = get_expired_stamp(certificate)
|
195
|
+
puts "Certificate for #{name} expires: #{expired_stamp.inspect}"
|
196
|
+
|
197
|
+
{
|
198
|
+
private_key: private_key,
|
199
|
+
certificate: certificate,
|
200
|
+
expired_stamp: expired_stamp
|
201
|
+
}
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class HTTPChallengeHandler
|
206
|
+
def initialize
|
207
|
+
@challenges = {}
|
208
|
+
end
|
209
|
+
|
210
|
+
def add(challenge)
|
211
|
+
path = "/.well-known/acme-challenge/#{challenge.token}"
|
212
|
+
@challenges[path] = challenge
|
213
|
+
end
|
214
|
+
|
215
|
+
def remove(challenge)
|
216
|
+
path = "/.well-known/acme-challenge/#{challenge.token}"
|
217
|
+
@challenges.delete(path)
|
218
|
+
end
|
219
|
+
|
220
|
+
def call(req)
|
221
|
+
challenge = @challenges[req.path]
|
222
|
+
|
223
|
+
# handle incoming request
|
224
|
+
challenge = @challenges[req.path]
|
225
|
+
return req.respond(nil, ':status' => 400) unless challenge
|
226
|
+
|
227
|
+
req.respond(challenge.file_content, 'content-type' => challenge.content_type)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class CertificateStore
|
232
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
233
|
+
raise NotImplementedError
|
234
|
+
end
|
235
|
+
|
236
|
+
def get(name)
|
237
|
+
raise NotImplementedError
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class InMemoryCertificateStore
|
242
|
+
def initialize
|
243
|
+
@store = {}
|
244
|
+
end
|
245
|
+
|
246
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
247
|
+
@store[name] = {
|
248
|
+
private_key: private_key,
|
249
|
+
certificate: certificate,
|
250
|
+
expired_stamp: expired_stamp
|
251
|
+
}
|
252
|
+
end
|
253
|
+
|
254
|
+
def get(name)
|
255
|
+
entry = @store[name]
|
256
|
+
return nil unless entry
|
257
|
+
if Time.now >= entry[:expired_stamp]
|
258
|
+
@store.delete(name)
|
259
|
+
return nil
|
260
|
+
end
|
261
|
+
|
262
|
+
entry
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class SQLiteCertificateStore
|
267
|
+
attr_reader :db
|
268
|
+
|
269
|
+
def initialize(path)
|
270
|
+
require 'extralite'
|
271
|
+
|
272
|
+
@db = Extralite::Database.new(path)
|
273
|
+
@db.query("
|
274
|
+
create table if not exists certificates (
|
275
|
+
name primary key not null,
|
276
|
+
private_key not null,
|
277
|
+
certificate not null,
|
278
|
+
expired_stamp not null
|
279
|
+
);"
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
284
|
+
@db.query("
|
285
|
+
insert into certificates values (?, ?, ?, ?)
|
286
|
+
", name, private_key.to_s, certificate, expired_stamp.to_i)
|
287
|
+
rescue Extralite::Error => e
|
288
|
+
p error_in_set: e
|
289
|
+
raise e
|
290
|
+
end
|
291
|
+
|
292
|
+
def get(name)
|
293
|
+
remove_expired_certificates
|
294
|
+
|
295
|
+
entry = @db.query_single_row("
|
296
|
+
select name, private_key, certificate, expired_stamp
|
297
|
+
from certificates
|
298
|
+
where name = ?
|
299
|
+
", name)
|
300
|
+
return nil unless entry
|
301
|
+
entry[:expired_stamp] = Time.at(entry[:expired_stamp])
|
302
|
+
entry[:private_key] = OpenSSL::PKey::RSA.new(entry[:private_key])
|
303
|
+
entry
|
304
|
+
rescue Extralite::Error => e
|
305
|
+
p error_in_get: e
|
306
|
+
raise e
|
307
|
+
end
|
308
|
+
|
309
|
+
def remove_expired_certificates
|
310
|
+
@db.query("
|
311
|
+
delete from certificates
|
312
|
+
where expired_stamp < ?
|
313
|
+
", Time.now.to_i)
|
314
|
+
rescue Extralite::Error => e
|
315
|
+
p error_in_remove_expired_certificates: e
|
316
|
+
raise e
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
data/lib/tipi/cli.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tipi'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'tipi/supervisor'
|
6
|
+
require 'optparse'
|
7
|
+
|
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
|
+
|
71
|
+
module CLI
|
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)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.display_banner
|
90
|
+
puts BANNER
|
91
|
+
end
|
92
|
+
end
|
93
|
+
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
|