tipi 0.43 → 0.45
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,631 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ever'
|
4
|
+
require 'localhost/authority'
|
5
|
+
require 'http/parser'
|
6
|
+
require 'qeweney'
|
7
|
+
require 'tipi/rack_adapter'
|
8
|
+
|
9
|
+
module Tipi
|
10
|
+
class Listener
|
11
|
+
def initialize(server, &handler)
|
12
|
+
@server = server
|
13
|
+
@handler = handler
|
14
|
+
end
|
15
|
+
|
16
|
+
def accept
|
17
|
+
socket, _addrinfo = @server.accept
|
18
|
+
@handler.call(socket)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Connection
|
23
|
+
def io_ready
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class HTTP1Connection < Connection
|
29
|
+
attr_reader :io
|
30
|
+
|
31
|
+
def initialize(io, evloop, &app)
|
32
|
+
@io = io
|
33
|
+
@evloop = evloop
|
34
|
+
@parser = Http::Parser.new(self)
|
35
|
+
@app = app
|
36
|
+
setup_read_request
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_read_request
|
40
|
+
@request_complete = nil
|
41
|
+
@request = nil
|
42
|
+
@response_buffer = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_headers_complete(headers)
|
46
|
+
headers = normalize_headers(headers)
|
47
|
+
headers[':path'] = @parser.request_url
|
48
|
+
headers[':method'] = @parser.http_method.downcase
|
49
|
+
scheme = (proto = headers['x-forwarded-proto']) ?
|
50
|
+
proto.downcase : scheme_from_connection
|
51
|
+
headers[':scheme'] = scheme
|
52
|
+
@request = Qeweney::Request.new(headers, self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def normalize_headers(headers)
|
56
|
+
headers.each_with_object({}) do |(k, v), h|
|
57
|
+
k = k.downcase
|
58
|
+
hk = h[k]
|
59
|
+
if hk
|
60
|
+
hk = h[k] = [hk] unless hk.is_a?(Array)
|
61
|
+
v.is_a?(Array) ? hk.concat(v) : hk << v
|
62
|
+
else
|
63
|
+
h[k] = v
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def scheme_from_connection
|
69
|
+
@io.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_body(chunk)
|
73
|
+
@request.buffer_body_chunk(chunk)
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_message_complete
|
77
|
+
@request_complete = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def io_ready
|
81
|
+
if !@request_complete
|
82
|
+
handle_read_request
|
83
|
+
else
|
84
|
+
handle_write_response
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_read_request
|
89
|
+
result = @io.read_nonblock(16384, exception: false)
|
90
|
+
case result
|
91
|
+
when :wait_readable
|
92
|
+
watch_io(false)
|
93
|
+
when :wait_writable
|
94
|
+
watch_io(true)
|
95
|
+
when nil
|
96
|
+
close_io
|
97
|
+
else
|
98
|
+
@parser << result
|
99
|
+
if @request_complete
|
100
|
+
handle_request
|
101
|
+
# @response = handle_request(@request_headers, @request_body)
|
102
|
+
# handle_write_response
|
103
|
+
else
|
104
|
+
watch_io(false)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
rescue HTTP::Parser::Error, SystemCallError, IOError
|
108
|
+
close_io
|
109
|
+
end
|
110
|
+
|
111
|
+
def watch_io(rw)
|
112
|
+
@evloop.watch_io(self, @io, rw, true)
|
113
|
+
# @evloop.emit([:watch_io, self, @io, rw, true])
|
114
|
+
end
|
115
|
+
|
116
|
+
def close_io
|
117
|
+
@evloop.emit([:close_io, self, @io])
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_request
|
121
|
+
@app.call(@request)
|
122
|
+
# req = Qeweney::Request.new(headers, self)
|
123
|
+
# response_body = "Hello, world!"
|
124
|
+
# "HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# response API
|
128
|
+
|
129
|
+
CRLF = "\r\n"
|
130
|
+
CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
|
131
|
+
|
132
|
+
# Sends response including headers and body. Waits for the request to complete
|
133
|
+
# if not yet completed. The body is sent using chunked transfer encoding.
|
134
|
+
# @param request [Qeweney::Request] HTTP request
|
135
|
+
# @param body [String] response body
|
136
|
+
# @param headers
|
137
|
+
def respond(request, body, headers)
|
138
|
+
formatted_headers = format_headers(headers, body, false)
|
139
|
+
request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
140
|
+
if body
|
141
|
+
handle_write(formatted_headers + body)
|
142
|
+
else
|
143
|
+
handle_write(formatted_headers)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Sends response headers. If empty_response is truthy, the response status
|
148
|
+
# code will default to 204, otherwise to 200.
|
149
|
+
# @param request [Qeweney::Request] HTTP request
|
150
|
+
# @param headers [Hash] response headers
|
151
|
+
# @param empty_response [boolean] whether a response body will be sent
|
152
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
153
|
+
# @return [void]
|
154
|
+
def send_headers(request, headers, empty_response: false, chunked: true)
|
155
|
+
formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
|
156
|
+
request.tx_incr(formatted_headers.bytesize)
|
157
|
+
handle_write(formatted_headers)
|
158
|
+
end
|
159
|
+
|
160
|
+
def http1_1?(request)
|
161
|
+
request.headers[':protocol'] == 'http/1.1'
|
162
|
+
end
|
163
|
+
|
164
|
+
# Sends a response body chunk. If no headers were sent, default headers are
|
165
|
+
# sent using #send_headers. if the done option is true(thy), an empty chunk
|
166
|
+
# will be sent to signal response completion to the client.
|
167
|
+
# @param request [Qeweney::Request] HTTP request
|
168
|
+
# @param chunk [String] response body chunk
|
169
|
+
# @param done [boolean] whether the response is completed
|
170
|
+
# @return [void]
|
171
|
+
def send_chunk(request, chunk, done: false)
|
172
|
+
data = +''
|
173
|
+
data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
|
174
|
+
data << "0\r\n\r\n" if done
|
175
|
+
return if data.empty?
|
176
|
+
|
177
|
+
request.tx_incr(data.bytesize)
|
178
|
+
handle_write(data)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Finishes the response to the current request. If no headers were sent,
|
182
|
+
# default headers are sent using #send_headers.
|
183
|
+
# @return [void]
|
184
|
+
def finish(request)
|
185
|
+
request.tx_incr(5)
|
186
|
+
handle_write("0\r\n\r\n")
|
187
|
+
end
|
188
|
+
|
189
|
+
INTERNAL_HEADER_REGEXP = /^:/.freeze
|
190
|
+
|
191
|
+
# Formats response headers into an array. If empty_response is true(thy),
|
192
|
+
# the response status code will default to 204, otherwise to 200.
|
193
|
+
# @param headers [Hash] response headers
|
194
|
+
# @param body [boolean] whether a response body will be sent
|
195
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
196
|
+
# @return [String] formatted response headers
|
197
|
+
def format_headers(headers, body, chunked)
|
198
|
+
status = headers[':status']
|
199
|
+
status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
|
200
|
+
lines = format_status_line(body, status, chunked)
|
201
|
+
headers.each do |k, v|
|
202
|
+
next if k =~ INTERNAL_HEADER_REGEXP
|
203
|
+
|
204
|
+
collect_header_lines(lines, k, v)
|
205
|
+
end
|
206
|
+
lines << CRLF
|
207
|
+
lines
|
208
|
+
end
|
209
|
+
|
210
|
+
def format_status_line(body, status, chunked)
|
211
|
+
if !body
|
212
|
+
empty_status_line(status)
|
213
|
+
else
|
214
|
+
with_body_status_line(status, body, chunked)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def empty_status_line(status)
|
219
|
+
if status == 204
|
220
|
+
+"HTTP/1.1 #{status}\r\n"
|
221
|
+
else
|
222
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def with_body_status_line(status, body, chunked)
|
227
|
+
if chunked
|
228
|
+
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
229
|
+
else
|
230
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def collect_header_lines(lines, key, value)
|
235
|
+
if value.is_a?(Array)
|
236
|
+
value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
|
237
|
+
else
|
238
|
+
lines << "#{key}: #{value}\r\n"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def handle_write(data = nil)
|
243
|
+
if data
|
244
|
+
if @response_buffer
|
245
|
+
@response_buffer << data
|
246
|
+
else
|
247
|
+
@response_buffer = +data
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
result = @io.write_nonblock(@response_buffer, exception: false)
|
252
|
+
case result
|
253
|
+
when :wait_readable
|
254
|
+
watch_io(false)
|
255
|
+
when :wait_writable
|
256
|
+
watch_io(true)
|
257
|
+
when nil
|
258
|
+
close_io
|
259
|
+
else
|
260
|
+
setup_read_request
|
261
|
+
watch_io(false)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class Controller
|
267
|
+
def initialize(opts)
|
268
|
+
@opts = opts
|
269
|
+
@path = File.expand_path(@opts['path'])
|
270
|
+
@service = prepare_service
|
271
|
+
end
|
272
|
+
|
273
|
+
WORKER_COUNT_RANGE = (1..32).freeze
|
274
|
+
|
275
|
+
def run
|
276
|
+
worker_count = (@opts['workers'] || 1).to_i.clamp(WORKER_COUNT_RANGE)
|
277
|
+
return run_worker if worker_count == 1
|
278
|
+
|
279
|
+
supervise_workers(worker_count)
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
def supervise_workers(worker_count)
|
285
|
+
supervisor = spin do
|
286
|
+
worker_count.times do
|
287
|
+
pid = fork { run_worker }
|
288
|
+
puts "Forked worker pid: #{pid}"
|
289
|
+
Process.wait(pid)
|
290
|
+
puts "Done worker pid: #{pid}"
|
291
|
+
end
|
292
|
+
# supervise(restart: :always)
|
293
|
+
rescue Polyphony::Terminate
|
294
|
+
# TODO: find out how Terminate can leak like that (it's supposed to be
|
295
|
+
# caught in Fiber#run)
|
296
|
+
end
|
297
|
+
# trap('SIGTERM') { supervisor.terminate(true) }
|
298
|
+
# trap('SIGINT') do
|
299
|
+
# trap('SIGINT') { exit! }
|
300
|
+
# supervisor.terminate(true)
|
301
|
+
# end
|
302
|
+
|
303
|
+
# supervisor.await
|
304
|
+
end
|
305
|
+
|
306
|
+
def run_worker
|
307
|
+
@evloop = Ever::Loop.new
|
308
|
+
start_server(@service)
|
309
|
+
trap('SIGTERM') { @evloop.stop }
|
310
|
+
trap('SIGINT') do
|
311
|
+
trap('SIGINT') { exit! }
|
312
|
+
@evloop.stop
|
313
|
+
end
|
314
|
+
run_evloop
|
315
|
+
end
|
316
|
+
|
317
|
+
def run_evloop
|
318
|
+
@evloop.each do |event|
|
319
|
+
case event
|
320
|
+
when Listener
|
321
|
+
event.accept
|
322
|
+
when Connection
|
323
|
+
event.io_ready
|
324
|
+
when Array
|
325
|
+
cmd, key, io, rw, oneshot = event
|
326
|
+
case cmd
|
327
|
+
when :watch_io
|
328
|
+
@evloop.watch_io(key, io, rw, oneshot)
|
329
|
+
when :close_io
|
330
|
+
io.close
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def prepare_service
|
337
|
+
if File.file?(@path)
|
338
|
+
File.extname(@path) == '.ru' ? rack_service : tipi_service
|
339
|
+
elsif File.directory?(@path)
|
340
|
+
static_service
|
341
|
+
else
|
342
|
+
raise "Invalid path specified #{@path}"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def start_app
|
347
|
+
if File.extname(@path) == '.ru'
|
348
|
+
start_rack_app
|
349
|
+
else
|
350
|
+
require(@path)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def rack_service
|
355
|
+
puts "Loading Rack app from #{@path}"
|
356
|
+
app = Tipi::RackAdapter.load(@path)
|
357
|
+
web_service(app)
|
358
|
+
end
|
359
|
+
|
360
|
+
def tipi_service
|
361
|
+
puts "Loading Tipi app from #{@path}"
|
362
|
+
require(@path)
|
363
|
+
app = Object.send(:app)
|
364
|
+
web_service(app)
|
365
|
+
end
|
366
|
+
|
367
|
+
def static_service
|
368
|
+
puts "Serving static files from #{@path}"
|
369
|
+
app = proc do |req|
|
370
|
+
p req: req
|
371
|
+
full_path = find_path(@path, req.path)
|
372
|
+
if full_path
|
373
|
+
req.serve_file(full_path)
|
374
|
+
else
|
375
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
web_service(app)
|
379
|
+
end
|
380
|
+
|
381
|
+
def web_service(app)
|
382
|
+
app = add_connection_headers(app)
|
383
|
+
|
384
|
+
prepare_listener(@opts['listen'], app)
|
385
|
+
end
|
386
|
+
|
387
|
+
def prepare_listener(spec, app)
|
388
|
+
case spec.shift
|
389
|
+
when 'http'
|
390
|
+
case spec.size
|
391
|
+
when 2
|
392
|
+
host, port = spec
|
393
|
+
port ||= 80
|
394
|
+
when 1
|
395
|
+
host = '0.0.0.0'
|
396
|
+
port = spec.first || 80
|
397
|
+
else
|
398
|
+
raise "Invalid listener spec"
|
399
|
+
end
|
400
|
+
prepare_http_listener(port, app)
|
401
|
+
when 'https'
|
402
|
+
case spec.size
|
403
|
+
when 2
|
404
|
+
host, port = spec
|
405
|
+
port ||= 80
|
406
|
+
when 1
|
407
|
+
host = 'localhost'
|
408
|
+
port = spec.first || 80
|
409
|
+
else
|
410
|
+
raise "Invalid listener spec"
|
411
|
+
end
|
412
|
+
port ||= 443
|
413
|
+
prepare_https_listener(host, port, app)
|
414
|
+
when 'full'
|
415
|
+
host, http_port, https_port = spec
|
416
|
+
http_port ||= 80
|
417
|
+
https_port ||= 443
|
418
|
+
prepare_full_service_listeners(host, http_port, https_port, app)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def prepare_http_listener(port, app)
|
423
|
+
puts "Listening for HTTP on localhost:#{port}"
|
424
|
+
|
425
|
+
proc do
|
426
|
+
start_listener('HTTP', port) do |socket|
|
427
|
+
start_client(socket, &app)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def start_client(socket, &app)
|
433
|
+
conn = HTTP1Connection.new(socket, @evloop, &app)
|
434
|
+
conn.watch_io(false)
|
435
|
+
end
|
436
|
+
|
437
|
+
LOCALHOST_REGEXP = /^(.+\.)?localhost$/.freeze
|
438
|
+
|
439
|
+
def prepare_https_listener(host, port, app)
|
440
|
+
localhost = host =~ LOCALHOST_REGEXP
|
441
|
+
return prepare_localhost_https_listener(port, app) if localhost
|
442
|
+
|
443
|
+
raise "No certificate found for #{host}"
|
444
|
+
# TODO: implement loading certificate
|
445
|
+
end
|
446
|
+
|
447
|
+
def prepare_localhost_https_listener(port, app)
|
448
|
+
puts "Listening for HTTPS on localhost:#{port}"
|
449
|
+
|
450
|
+
authority = Localhost::Authority.fetch
|
451
|
+
ctx = authority.server_context
|
452
|
+
ctx.ciphers = 'ECDH+aRSA'
|
453
|
+
Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
|
454
|
+
|
455
|
+
proc do
|
456
|
+
https_listener = spin_accept_loop('HTTPS', port) do |socket|
|
457
|
+
start_https_connection_fiber(socket, ctx, nil, app)
|
458
|
+
rescue Exception => e
|
459
|
+
puts "Exception in https_listener block: #{e.inspect}\n#{e.backtrace.inspect}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def prepare_full_service_listeners(host, http_port, https_port, app)
|
465
|
+
puts "Listening for HTTP on localhost:#{http_port}"
|
466
|
+
puts "Listening for HTTPS on localhost:#{https_port}"
|
467
|
+
|
468
|
+
redirect_host = (https_port == 443) ? host : "#{host}:#{https_port}"
|
469
|
+
redirect_app = ->(r) { r.redirect("https://#{redirect_host}#{r.path}") }
|
470
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
471
|
+
ctx.ciphers = 'ECDH+aRSA'
|
472
|
+
Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
|
473
|
+
certificate_store = create_certificate_store
|
474
|
+
|
475
|
+
proc do
|
476
|
+
challenge_handler = Tipi::ACME::HTTPChallengeHandler.new
|
477
|
+
certificate_manager = Tipi::ACME::CertificateManager.new(
|
478
|
+
master_ctx: ctx,
|
479
|
+
store: certificate_store,
|
480
|
+
challenge_handler: challenge_handler
|
481
|
+
)
|
482
|
+
http_app = certificate_manager.challenge_routing_app(redirect_app)
|
483
|
+
|
484
|
+
http_listener = spin_accept_loop('HTTP', http_port) do |socket|
|
485
|
+
Tipi.client_loop(socket, @opts, &http_app)
|
486
|
+
end
|
487
|
+
|
488
|
+
ssl_accept_thread_pool = Polyphony::ThreadPool.new(4)
|
489
|
+
|
490
|
+
https_listener = spin_accept_loop('HTTPS', https_port) do |socket|
|
491
|
+
start_https_connection_fiber(socket, ctx, ssl_accept_thread_pool, app)
|
492
|
+
rescue Exception => e
|
493
|
+
puts "Exception in https_listener block: #{e.inspect}\n#{e.backtrace.inspect}"
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
end
|
498
|
+
|
499
|
+
INVALID_PATH_REGEXP = /\/?(\.\.|\.)\//
|
500
|
+
|
501
|
+
def find_path(base, path)
|
502
|
+
return nil if path =~ INVALID_PATH_REGEXP
|
503
|
+
|
504
|
+
full_path = File.join(base, path)
|
505
|
+
return full_path if File.file?(full_path)
|
506
|
+
return find_path(full_path, 'index') if File.directory?(full_path)
|
507
|
+
|
508
|
+
qualified = "#{full_path}.html"
|
509
|
+
return qualified if File.file?(qualified)
|
510
|
+
|
511
|
+
nil
|
512
|
+
end
|
513
|
+
|
514
|
+
SOCKET_OPTS = {
|
515
|
+
reuse_addr: true,
|
516
|
+
reuse_port: true,
|
517
|
+
dont_linger: true,
|
518
|
+
}.freeze
|
519
|
+
|
520
|
+
def start_listener(name, port, &block)
|
521
|
+
host = '0.0.0.0'
|
522
|
+
socket = ::Socket.new(:INET, :STREAM).tap do |s|
|
523
|
+
s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
|
524
|
+
s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
525
|
+
s.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, [0, 0].pack('ii'))
|
526
|
+
addr = ::Socket.sockaddr_in(port, host)
|
527
|
+
s.bind(addr)
|
528
|
+
s.listen(Socket::SOMAXCONN)
|
529
|
+
end
|
530
|
+
listener = Listener.new(socket, &block)
|
531
|
+
@evloop.watch_io(listener, socket, false, false)
|
532
|
+
end
|
533
|
+
|
534
|
+
def spin_accept_loop(name, port, &block)
|
535
|
+
spin do
|
536
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', port, SOCKET_OPTS)
|
537
|
+
loop do
|
538
|
+
socket = server.accept
|
539
|
+
spin_connection_handler(name, socket, block)
|
540
|
+
rescue Polyphony::BaseException => e
|
541
|
+
raise
|
542
|
+
rescue Exception => e
|
543
|
+
puts "#{name} listener uncaught exception: #{e.inspect}"
|
544
|
+
end
|
545
|
+
ensure
|
546
|
+
finalize_listener(server) if server
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def spin_connection_handler(name, socket, block)
|
551
|
+
spin do
|
552
|
+
block.(socket)
|
553
|
+
rescue Polyphony::BaseException
|
554
|
+
raise
|
555
|
+
rescue Exception => e
|
556
|
+
puts "Uncaught error in #{name} handler: #{e.inspect}"
|
557
|
+
p e.backtrace
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def finalize_listener(server)
|
562
|
+
fiber = Fiber.current
|
563
|
+
gracefully_terminate_conections(fiber) if fiber.graceful_shutdown?
|
564
|
+
server.close
|
565
|
+
rescue Polyphony::BaseException
|
566
|
+
raise
|
567
|
+
rescue Exception => e
|
568
|
+
trace "Exception in finalize_listener: #{e.inspect}"
|
569
|
+
end
|
570
|
+
|
571
|
+
def gracefully_terminate_conections(fiber)
|
572
|
+
supervisor = spin { supervise }.detach
|
573
|
+
fiber.attach_all_children_to(supervisor)
|
574
|
+
|
575
|
+
# terminating the supervisor will
|
576
|
+
supervisor.terminate(true)
|
577
|
+
end
|
578
|
+
|
579
|
+
def add_connection_headers(app)
|
580
|
+
app
|
581
|
+
# proc do |req|
|
582
|
+
# conn = req.adapter.conn
|
583
|
+
# # req.headers[':peer'] = conn.peeraddr(false)[2]
|
584
|
+
# req.headers[':scheme'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
585
|
+
# app.(req)
|
586
|
+
# end
|
587
|
+
end
|
588
|
+
|
589
|
+
def ssl_accept(client)
|
590
|
+
client.accept
|
591
|
+
true
|
592
|
+
rescue Polyphony::BaseException
|
593
|
+
raise
|
594
|
+
rescue Exception => e
|
595
|
+
p e
|
596
|
+
e
|
597
|
+
end
|
598
|
+
|
599
|
+
def start_https_connection_fiber(socket, ctx, thread_pool, app)
|
600
|
+
client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
601
|
+
client.sync_close = true
|
602
|
+
|
603
|
+
result = thread_pool ?
|
604
|
+
thread_pool.process { ssl_accept(client) } : ssl_accept(client)
|
605
|
+
|
606
|
+
if result.is_a?(Exception)
|
607
|
+
puts "Exception in SSL handshake: #{result.inspect}"
|
608
|
+
return
|
609
|
+
end
|
610
|
+
|
611
|
+
Tipi.client_loop(client, @opts, &app)
|
612
|
+
rescue => e
|
613
|
+
puts "Uncaught error in HTTPS connection fiber: #{e.inspect} bt: #{e.backtrace.inspect}"
|
614
|
+
ensure
|
615
|
+
(client ? client.close : socket.close) rescue nil
|
616
|
+
end
|
617
|
+
|
618
|
+
CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi').freeze
|
619
|
+
CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(
|
620
|
+
CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db').freeze
|
621
|
+
|
622
|
+
def create_certificate_store
|
623
|
+
FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil
|
624
|
+
Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)
|
625
|
+
end
|
626
|
+
|
627
|
+
def start_server(service)
|
628
|
+
service.call
|
629
|
+
end
|
630
|
+
end
|
631
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# get opts from STDIN
|
7
|
+
opts = JSON.parse(ARGV[0]) rescue nil
|
8
|
+
mod_path = "./controller/#{opts['app_type']}_#{opts['mode']}"
|
9
|
+
require_relative mod_path
|
10
|
+
|
11
|
+
controller = Tipi::Controller.new(opts)
|
12
|
+
controller.run
|
@@ -46,7 +46,7 @@ module DigitalFabric
|
|
46
46
|
df_upgrade
|
47
47
|
@connected = true
|
48
48
|
@msgpack_reader = MessagePack::Unpacker.new
|
49
|
-
|
49
|
+
|
50
50
|
process_incoming_requests
|
51
51
|
rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError
|
52
52
|
log 'Disconnected' if @connected
|
@@ -70,9 +70,9 @@ module DigitalFabric
|
|
70
70
|
Upgrade: df
|
71
71
|
DF-Token: %s
|
72
72
|
DF-Mount: %s
|
73
|
-
|
73
|
+
|
74
74
|
HTTP
|
75
|
-
|
75
|
+
|
76
76
|
def df_upgrade
|
77
77
|
@socket << format(UPGRADE_REQUEST, @token, mount_point)
|
78
78
|
while (line = @socket.gets)
|
@@ -56,7 +56,7 @@ module DigitalFabric
|
|
56
56
|
def unmount
|
57
57
|
return unless @mounted
|
58
58
|
|
59
|
-
@service.unmount(self)
|
59
|
+
@service.unmount(self)
|
60
60
|
@mounted = nil
|
61
61
|
end
|
62
62
|
|
@@ -147,11 +147,17 @@ module DigitalFabric
|
|
147
147
|
msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
|
148
148
|
send_df_message(msg)
|
149
149
|
while (message = receive)
|
150
|
+
kind = message[Protocol::Attribute::KIND]
|
150
151
|
unless t1
|
151
152
|
t1 = Time.now
|
152
|
-
|
153
|
+
if kind == Protocol::HTTP_RESPONSE
|
154
|
+
headers = message[Protocol::Attribute::HttpResponse::HEADERS]
|
155
|
+
status = (headers && headers[':status']) || 200
|
156
|
+
if status < Qeweney::Status::BAD_REQUEST
|
157
|
+
@service.record_latency_measurement(t1 - t0, req)
|
158
|
+
end
|
159
|
+
end
|
153
160
|
end
|
154
|
-
kind = message[Protocol::Attribute::KIND]
|
155
161
|
attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
|
156
162
|
return if http_request_message(id, req, kind, attributes)
|
157
163
|
end
|
@@ -241,7 +247,7 @@ module DigitalFabric
|
|
241
247
|
else
|
242
248
|
req.send_headers(headers) if headers && !req.headers_sent?
|
243
249
|
req.send_chunk(body, done: complete) if body or complete
|
244
|
-
|
250
|
+
|
245
251
|
if complete && transfer_count_key
|
246
252
|
rx, tx = req.transfer_counts
|
247
253
|
send_transfer_count(transfer_count_key, rx, tx)
|
@@ -285,7 +291,7 @@ module DigitalFabric
|
|
285
291
|
response = receive
|
286
292
|
case response[0]
|
287
293
|
when Protocol::WS_RESPONSE
|
288
|
-
headers = response[2] || {}
|
294
|
+
headers = response[2] || {}
|
289
295
|
status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
|
290
296
|
if status != Qeweney::Status::SWITCHING_PROTOCOLS
|
291
297
|
req.respond(nil, headers)
|