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