tipi 0.43 → 0.45
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 +12 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +14 -7
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +1 -1
- 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_utils.rb +1 -1
- data/examples/hello.rb +5 -0
- 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 +46 -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/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +351 -0
- data/lib/tipi/controller/web_stock.rb +631 -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 +8 -8
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +32 -27
- data/lib/tipi/http2_adapter.rb +10 -10
- data/lib/tipi/http2_stream.rb +14 -14
- 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/test_http_server.rb +14 -14
- data/tipi.gemspec +3 -2
- metadata +30 -5
data/lib/tipi/acme.rb
CHANGED
@@ -35,32 +35,41 @@ module Tipi
|
|
35
35
|
IP_REGEXP = /^\d+\.\d+\.\d+\.\d+$/
|
36
36
|
|
37
37
|
def setup_sni_callback
|
38
|
-
@master_ctx.servername_cb = proc
|
39
|
-
p servername_cb: name
|
40
|
-
state = { ctx: nil }
|
41
|
-
|
42
|
-
if name =~ IP_REGEXP
|
43
|
-
@master_ctx
|
44
|
-
else
|
45
|
-
@requests << [name, state]
|
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
|
53
|
-
end
|
38
|
+
@master_ctx.servername_cb = proc { |_socket, name| get_ctx(name) }
|
54
39
|
end
|
55
|
-
|
40
|
+
|
41
|
+
def get_ctx(name)
|
42
|
+
state = { ctx: nil }
|
43
|
+
|
44
|
+
ready_ctx = @contexts[name]
|
45
|
+
return ready_ctx if ready_ctx
|
46
|
+
return @master_ctx if name =~ IP_REGEXP
|
47
|
+
|
48
|
+
@requests << [name, state]
|
49
|
+
wait_for_ctx(state)
|
50
|
+
# Eventually we might want to return an error returned in
|
51
|
+
# state[:error]. For the time being we handle errors by returning the
|
52
|
+
# master context
|
53
|
+
state[:ctx] || @master_ctx
|
54
|
+
rescue => e
|
55
|
+
@master_ctx
|
56
|
+
end
|
57
|
+
|
58
|
+
MAX_WAIT_FOR_CTX_DURATION = 30
|
59
|
+
|
56
60
|
def wait_for_ctx(state)
|
61
|
+
t0 = Time.now
|
57
62
|
period = 0.00001
|
58
63
|
while !state[:ctx] && !state[:error]
|
59
64
|
orig_sleep period
|
60
|
-
|
65
|
+
if period < 0.1
|
66
|
+
period *= 2
|
67
|
+
elsif Time.now - t0 > MAX_WAIT_FOR_CTX_DURATION
|
68
|
+
raise "Timeout waiting for certificate provisioning"
|
69
|
+
end
|
61
70
|
end
|
62
71
|
end
|
63
|
-
|
72
|
+
|
64
73
|
def run
|
65
74
|
loop do
|
66
75
|
name, state = @requests.shift
|
@@ -69,13 +78,13 @@ module Tipi
|
|
69
78
|
state[:error] = e if state
|
70
79
|
end
|
71
80
|
end
|
72
|
-
|
81
|
+
|
73
82
|
LOCALHOST_REGEXP = /\.?localhost$/.freeze
|
74
83
|
|
75
84
|
def get_context(name)
|
76
85
|
@contexts[name] = setup_context(name)
|
77
86
|
end
|
78
|
-
|
87
|
+
|
79
88
|
def setup_context(name)
|
80
89
|
ctx = provision_context(name)
|
81
90
|
transfer_ctx_settings(ctx)
|
@@ -100,7 +109,7 @@ module Tipi
|
|
100
109
|
end
|
101
110
|
|
102
111
|
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
103
|
-
|
112
|
+
|
104
113
|
def parse_certificate(certificate)
|
105
114
|
certificate
|
106
115
|
.scan(CERTIFICATE_REGEXP)
|
@@ -126,17 +135,17 @@ module Tipi
|
|
126
135
|
@localhost_authority ||= Localhost::Authority.fetch
|
127
136
|
@localhost_authority.server_context
|
128
137
|
end
|
129
|
-
|
138
|
+
|
130
139
|
def private_key
|
131
140
|
@private_key ||= OpenSSL::PKey::RSA.new(4096)
|
132
141
|
end
|
133
|
-
|
142
|
+
|
134
143
|
ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
|
135
|
-
|
144
|
+
|
136
145
|
def acme_client
|
137
146
|
@acme_client ||= setup_acme_client
|
138
147
|
end
|
139
|
-
|
148
|
+
|
140
149
|
def setup_acme_client
|
141
150
|
client = Acme::Client.new(
|
142
151
|
private_key: private_key,
|
@@ -148,13 +157,12 @@ module Tipi
|
|
148
157
|
)
|
149
158
|
client
|
150
159
|
end
|
151
|
-
|
160
|
+
|
152
161
|
def provision_certificate(name)
|
153
|
-
p provision_certificate: name
|
154
162
|
order = acme_client.new_order(identifiers: [name])
|
155
163
|
authorization = order.authorizations.first
|
156
164
|
challenge = authorization.http
|
157
|
-
|
165
|
+
|
158
166
|
@challenge_handler.add(challenge)
|
159
167
|
challenge.request_validation
|
160
168
|
while challenge.status == 'pending'
|
@@ -162,8 +170,7 @@ module Tipi
|
|
162
170
|
challenge.reload
|
163
171
|
end
|
164
172
|
raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid'
|
165
|
-
|
166
|
-
p challenge_status: challenge.status
|
173
|
+
|
167
174
|
private_key = OpenSSL::PKey::RSA.new(4096)
|
168
175
|
csr = Acme::Client::CertificateRequest.new(
|
169
176
|
private_key: private_key,
|
@@ -189,33 +196,33 @@ module Tipi
|
|
189
196
|
}
|
190
197
|
end
|
191
198
|
end
|
192
|
-
|
199
|
+
|
193
200
|
class HTTPChallengeHandler
|
194
201
|
def initialize
|
195
202
|
@challenges = {}
|
196
203
|
end
|
197
|
-
|
204
|
+
|
198
205
|
def add(challenge)
|
199
206
|
path = "/.well-known/acme-challenge/#{challenge.token}"
|
200
207
|
@challenges[path] = challenge
|
201
208
|
end
|
202
|
-
|
209
|
+
|
203
210
|
def remove(challenge)
|
204
211
|
path = "/.well-known/acme-challenge/#{challenge.token}"
|
205
212
|
@challenges.delete(path)
|
206
213
|
end
|
207
|
-
|
214
|
+
|
208
215
|
def call(req)
|
209
216
|
challenge = @challenges[req.path]
|
210
|
-
|
217
|
+
|
211
218
|
# handle incoming request
|
212
219
|
challenge = @challenges[req.path]
|
213
220
|
return req.respond(nil, ':status' => 400) unless challenge
|
214
|
-
|
221
|
+
|
215
222
|
req.respond(challenge.file_content, 'content-type' => challenge.content_type)
|
216
223
|
end
|
217
|
-
end
|
218
|
-
|
224
|
+
end
|
225
|
+
|
219
226
|
class CertificateStore
|
220
227
|
def set(name, private_key:, certificate:, expired_stamp:)
|
221
228
|
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
|