tipi 0.39 → 0.43
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/workflows/test.yml +4 -0
- data/.gitignore +5 -1
- data/CHANGELOG.md +30 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +62 -25
- data/Rakefile +7 -3
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/df/server.rb +16 -87
- data/df/server_utils.rb +175 -0
- data/examples/full_service.rb +13 -0
- data/examples/http1_parser.rb +55 -0
- data/examples/http_server.rb +15 -3
- data/examples/http_server_forked.rb +3 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +26 -0
- data/examples/https_server.rb +3 -0
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +89 -1
- data/lib/tipi/acme.rb +308 -0
- data/lib/tipi/cli.rb +30 -0
- data/lib/tipi/digital_fabric/agent.rb +7 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +16 -8
- data/lib/tipi/digital_fabric/executive.rb +6 -2
- data/lib/tipi/digital_fabric/protocol.rb +18 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +77 -49
- data/lib/tipi/http1_adapter.rb +91 -100
- data/lib/tipi/http2_adapter.rb +21 -6
- data/lib/tipi/http2_stream.rb +54 -44
- data/lib/tipi/rack_adapter.rb +2 -53
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +0 -27
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +11 -7
- metadata +79 -26
- data/e +0 -0
data/df/server_utils.rb
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
5
|
+
require 'tipi/digital_fabric'
|
|
6
|
+
require 'tipi/digital_fabric/executive'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
require 'time'
|
|
10
|
+
require 'polyphony/extensions/debug'
|
|
11
|
+
|
|
12
|
+
FileUtils.cd(__dir__)
|
|
13
|
+
|
|
14
|
+
@service = DigitalFabric::Service.new(token: 'foobar')
|
|
15
|
+
@executive = DigitalFabric::Executive.new(@service, { host: '@executive.realiteq.net' })
|
|
16
|
+
|
|
17
|
+
@pid = Process.pid
|
|
18
|
+
|
|
19
|
+
def log(msg, **ctx)
|
|
20
|
+
text = format(
|
|
21
|
+
"%s (%d) %s\n",
|
|
22
|
+
Time.now.strftime('%Y-%m-%d %H:%M:%S.%3N'),
|
|
23
|
+
@pid,
|
|
24
|
+
msg
|
|
25
|
+
)
|
|
26
|
+
STDOUT.orig_write text
|
|
27
|
+
return if ctx.empty?
|
|
28
|
+
|
|
29
|
+
ctx.each { |k, v| STDOUT.orig_write format(" %s: %s\n", k, v.inspect) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def listen_http
|
|
33
|
+
spin(:http_listener) do
|
|
34
|
+
opts = {
|
|
35
|
+
reuse_addr: true,
|
|
36
|
+
dont_linger: true,
|
|
37
|
+
}
|
|
38
|
+
log('Listening for HTTP on localhost:10080')
|
|
39
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)
|
|
40
|
+
id = 0
|
|
41
|
+
loop do
|
|
42
|
+
client = server.accept
|
|
43
|
+
# log("Accept HTTP connection", client: client)
|
|
44
|
+
spin("http#{id += 1}") do
|
|
45
|
+
@service.incr_connection_count
|
|
46
|
+
Tipi.client_loop(client, opts) { |req| @service.http_request(req) }
|
|
47
|
+
ensure
|
|
48
|
+
# log("Done with HTTP connection", client: client)
|
|
49
|
+
@service.decr_connection_count
|
|
50
|
+
end
|
|
51
|
+
rescue Polyphony::BaseException
|
|
52
|
+
raise
|
|
53
|
+
rescue Exception => e
|
|
54
|
+
log 'HTTP accept (unknown) error', error: e, backtrace: e.backtrace
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
|
60
|
+
|
|
61
|
+
def listen_https
|
|
62
|
+
spin(:https_listener) do
|
|
63
|
+
private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
|
|
64
|
+
c = IO.read('../../reality/ssl/cacert.pem')
|
|
65
|
+
certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
|
66
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
67
|
+
ctx.security_level = 0
|
|
68
|
+
cert = certificates.shift
|
|
69
|
+
log "SSL Certificate expires: #{cert.not_after.inspect}"
|
|
70
|
+
ctx.add_certificate(cert, private_key, certificates)
|
|
71
|
+
ctx.ciphers = 'ECDH+aRSA'
|
|
72
|
+
ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION
|
|
73
|
+
ctx.min_version = OpenSSL::SSL::SSL3_VERSION
|
|
74
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
75
|
+
|
|
76
|
+
# TODO: further limit ciphers
|
|
77
|
+
# ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
|
|
78
|
+
# ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/tls.rb
|
|
79
|
+
|
|
80
|
+
opts = {
|
|
81
|
+
reuse_addr: true,
|
|
82
|
+
dont_linger: true,
|
|
83
|
+
secure_context: ctx,
|
|
84
|
+
alpn_protocols: Tipi::ALPN_PROTOCOLS
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
log('Listening for HTTPS on localhost:10443')
|
|
88
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
|
|
89
|
+
id = 0
|
|
90
|
+
loop do
|
|
91
|
+
client = server.accept rescue nil
|
|
92
|
+
next unless client
|
|
93
|
+
|
|
94
|
+
# log('Accept HTTPS client connection', client: client)
|
|
95
|
+
spin("https#{id += 1}") do
|
|
96
|
+
@service.incr_connection_count
|
|
97
|
+
Tipi.client_loop(client, opts) { |req| @service.http_request(req) }
|
|
98
|
+
rescue => e
|
|
99
|
+
log('Error while handling HTTPS client', client: client, error: e, backtrace: e.backtrace)
|
|
100
|
+
ensure
|
|
101
|
+
# log("Done with HTTP connection", client: client)
|
|
102
|
+
@service.decr_connection_count
|
|
103
|
+
end
|
|
104
|
+
# rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e
|
|
105
|
+
# log('HTTPS accept error', error: e)
|
|
106
|
+
rescue Polyphony::BaseException
|
|
107
|
+
raise
|
|
108
|
+
rescue Exception => e
|
|
109
|
+
log 'HTTPS listener error: ', error: e, backtrace: e.backtrace
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
UNIX_SOCKET_PATH = '/tmp/df.sock'
|
|
115
|
+
def listen_unix
|
|
116
|
+
spin(:unix_listener) do
|
|
117
|
+
log("Listening on #{UNIX_SOCKET_PATH}")
|
|
118
|
+
FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
|
|
119
|
+
socket = UNIXServer.new(UNIX_SOCKET_PATH)
|
|
120
|
+
|
|
121
|
+
id = 0
|
|
122
|
+
loop do
|
|
123
|
+
client = socket.accept
|
|
124
|
+
# log('Accept Unix connection', client: client)
|
|
125
|
+
spin("unix#{id += 1}") do
|
|
126
|
+
Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
|
|
127
|
+
end
|
|
128
|
+
rescue Polyphony::BaseException
|
|
129
|
+
raise
|
|
130
|
+
rescue Exception => e
|
|
131
|
+
log 'Unix accept error', error: e, backtrace: e.backtrace
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def listen_df
|
|
137
|
+
spin(:df_listener) do
|
|
138
|
+
opts = {
|
|
139
|
+
reuse_addr: true,
|
|
140
|
+
reuse_port: true,
|
|
141
|
+
dont_linger: true,
|
|
142
|
+
}
|
|
143
|
+
log('Listening for DF connections on localhost:4321')
|
|
144
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 4321, opts)
|
|
145
|
+
|
|
146
|
+
id = 0
|
|
147
|
+
loop do
|
|
148
|
+
client = server.accept
|
|
149
|
+
# log('Accept DF connection', client: client)
|
|
150
|
+
spin("df#{id += 1}") do
|
|
151
|
+
Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
|
|
152
|
+
end
|
|
153
|
+
rescue Polyphony::BaseException
|
|
154
|
+
raise
|
|
155
|
+
rescue Exception => e
|
|
156
|
+
log 'DF accept (unknown) error', error: e, backtrace: e.backtrace
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if ENV['TRACE'] == '1'
|
|
162
|
+
Thread.backend.trace_proc = proc do |event, fiber, value, pri|
|
|
163
|
+
fiber_id = fiber.tag || fiber.inspect
|
|
164
|
+
case event
|
|
165
|
+
when :fiber_schedule
|
|
166
|
+
log format("=> %s %s %s %s", event, fiber_id, value.inspect, pri ? '(priority)' : '')
|
|
167
|
+
when :fiber_run
|
|
168
|
+
log format("=> %s %s %s", event, fiber_id, value.inspect)
|
|
169
|
+
when :fiber_create, :fiber_terminate
|
|
170
|
+
log format("=> %s %s", event, fiber_id)
|
|
171
|
+
else
|
|
172
|
+
log format("=> %s", event)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
5
|
+
|
|
6
|
+
::Exception.__disable_sanitized_backtrace__ = true
|
|
7
|
+
|
|
8
|
+
certificate_db_path = File.expand_path('certificate_store.db', __dir__)
|
|
9
|
+
certificate_store = Tipi::ACME::SQLiteCertificateStore.new(certificate_db_path)
|
|
10
|
+
|
|
11
|
+
Tipi.full_service(
|
|
12
|
+
certificate_store: certificate_store
|
|
13
|
+
) { |req| req.respond('Hello, world!') }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'polyphony'
|
|
4
|
+
require_relative '../lib/tipi_ext'
|
|
5
|
+
|
|
6
|
+
i, o = IO.pipe
|
|
7
|
+
|
|
8
|
+
module ::Kernel
|
|
9
|
+
def trace(*args)
|
|
10
|
+
STDOUT.orig_write(format_trace(args))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_trace(args)
|
|
14
|
+
if args.first.is_a?(String)
|
|
15
|
+
if args.size > 1
|
|
16
|
+
format("%s: %p\n", args.shift, args)
|
|
17
|
+
else
|
|
18
|
+
format("%s\n", args.first)
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
format("%p\n", args.size == 1 ? args.first : args)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
f = spin do
|
|
27
|
+
parser = Tipi::HTTP1Parser.new(i)
|
|
28
|
+
while true
|
|
29
|
+
trace '*' * 40
|
|
30
|
+
headers = parser.parse_headers
|
|
31
|
+
break unless headers
|
|
32
|
+
trace headers
|
|
33
|
+
|
|
34
|
+
body = parser.read_body
|
|
35
|
+
trace "body: #{body ? body.bytesize : 0} bytes"
|
|
36
|
+
trace body if body && body.bytesize < 80
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
o << "GET /a HTTP/1.1\r\n\r\n"
|
|
41
|
+
|
|
42
|
+
# o << "GET /a HTTP/1.1\r\nContent-Length: 0\r\n\r\n"
|
|
43
|
+
|
|
44
|
+
# o << "GET / HTTP/1.1\r\nHost: localhost:10080\r\nUser-Agent: curl/7.74.0\r\nAccept: */*\r\n\r\n"
|
|
45
|
+
|
|
46
|
+
o << "post /?q=time&blah=blah HTTP/1\r\nTransfer-Encoding: chunked\r\n\r\na\r\nabcdefghij\r\n0\r\n\r\n"
|
|
47
|
+
|
|
48
|
+
data = " " * 4000000
|
|
49
|
+
o << "get /?q=time HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
|
|
50
|
+
|
|
51
|
+
o << "get /?q=time HTTP/1.1\r\nCookie: foo\r\nCookie: bar\r\n\r\n"
|
|
52
|
+
|
|
53
|
+
o.close
|
|
54
|
+
|
|
55
|
+
f.await
|
data/examples/http_server.rb
CHANGED
|
@@ -9,10 +9,19 @@ opts = {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
puts "pid: #{Process.pid}"
|
|
12
|
-
puts 'Listening on port
|
|
12
|
+
puts 'Listening on port 10080...'
|
|
13
|
+
|
|
14
|
+
# GC.disable
|
|
15
|
+
# Thread.current.backend.idle_gc_period = 60
|
|
16
|
+
|
|
17
|
+
spin_loop(interval: 10) { p Thread.backend.stats }
|
|
18
|
+
|
|
19
|
+
spin_loop(interval: 10) do
|
|
20
|
+
GC.compact
|
|
21
|
+
end
|
|
13
22
|
|
|
14
23
|
spin do
|
|
15
|
-
Tipi.serve('0.0.0.0',
|
|
24
|
+
Tipi.serve('0.0.0.0', 10080, opts) do |req|
|
|
16
25
|
if req.path == '/stream'
|
|
17
26
|
req.send_headers('Foo' => 'Bar')
|
|
18
27
|
sleep 1
|
|
@@ -20,10 +29,13 @@ spin do
|
|
|
20
29
|
sleep 1
|
|
21
30
|
req.send_chunk("bar\n")
|
|
22
31
|
req.finish
|
|
32
|
+
elsif req.path == '/upload'
|
|
33
|
+
body = req.read
|
|
34
|
+
req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
|
|
23
35
|
else
|
|
24
36
|
req.respond("Hello world!\n")
|
|
25
37
|
end
|
|
26
|
-
p req.transfer_counts
|
|
38
|
+
# p req.transfer_counts
|
|
27
39
|
end
|
|
28
40
|
p 'done...'
|
|
29
41
|
end.await
|
|
@@ -11,11 +11,13 @@ opts = {
|
|
|
11
11
|
dont_linger: true
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
server = Tipi.listen('0.0.0.0', 1234, opts)
|
|
15
|
+
|
|
14
16
|
child_pids = []
|
|
15
17
|
8.times do
|
|
16
18
|
pid = Polyphony.fork do
|
|
17
19
|
puts "forked pid: #{Process.pid}"
|
|
18
|
-
|
|
20
|
+
server.each do |req|
|
|
19
21
|
req.respond("Hello world! from pid: #{Process.pid}\n")
|
|
20
22
|
end
|
|
21
23
|
rescue Interrupt
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
5
|
+
|
|
6
|
+
opts = {
|
|
7
|
+
reuse_addr: true,
|
|
8
|
+
dont_linger: true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
puts "pid: #{Process.pid}"
|
|
12
|
+
puts 'Listening on port 4411...'
|
|
13
|
+
|
|
14
|
+
app = Tipi.route do |req|
|
|
15
|
+
req.on 'stream' do
|
|
16
|
+
req.send_headers('Foo' => 'Bar')
|
|
17
|
+
sleep 1
|
|
18
|
+
req.send_chunk("foo\n")
|
|
19
|
+
sleep 1
|
|
20
|
+
req.send_chunk("bar\n")
|
|
21
|
+
req.finish
|
|
22
|
+
end
|
|
23
|
+
req.default do
|
|
24
|
+
req.respond("Hello world!\n")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
trap('INT') { exit! }
|
|
29
|
+
Tipi.serve('0.0.0.0', 4411, opts, &app)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
opts = {
|
|
8
|
+
reuse_addr: true,
|
|
9
|
+
dont_linger: true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
puts "pid: #{Process.pid}"
|
|
13
|
+
puts 'Listening on port 4411...'
|
|
14
|
+
|
|
15
|
+
root_path = FileUtils.pwd
|
|
16
|
+
|
|
17
|
+
trap('INT') { exit! }
|
|
18
|
+
|
|
19
|
+
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
|
20
|
+
path = File.join(root_path, req.path)
|
|
21
|
+
if File.file?(path)
|
|
22
|
+
req.serve_file(path)
|
|
23
|
+
else
|
|
24
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
|
25
|
+
end
|
|
26
|
+
end
|
data/examples/https_server.rb
CHANGED
|
@@ -23,6 +23,9 @@ Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
|
|
23
23
|
req.send_chunk("foo\n")
|
|
24
24
|
sleep 0.5
|
|
25
25
|
req.send_chunk("bar\n", done: true)
|
|
26
|
+
elsif req.path == '/upload'
|
|
27
|
+
body = req.read
|
|
28
|
+
req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
|
|
26
29
|
else
|
|
27
30
|
req.respond("Hello world!\n")
|
|
28
31
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'fiber'
|
|
5
|
+
|
|
6
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
7
|
+
|
|
8
|
+
f = Fiber.new { |peer| loop { p peer: peer; _name, peer = peer.transfer nil } }
|
|
9
|
+
ctx.servername_cb = proc { |_socket, name|
|
|
10
|
+
p servername_cb: name
|
|
11
|
+
f.transfer([name, Fiber.current]).tap { |r| p result: r }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
socket = Socket.new(:INET, :STREAM).tap do |s|
|
|
15
|
+
s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
|
|
16
|
+
s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
|
17
|
+
s.bind(Socket.sockaddr_in(12345, '0.0.0.0'))
|
|
18
|
+
s.listen(Socket::SOMAXCONN)
|
|
19
|
+
end
|
|
20
|
+
server = OpenSSL::SSL::SSLServer.new(socket, ctx)
|
|
21
|
+
|
|
22
|
+
Thread.new do
|
|
23
|
+
sleep 0.5
|
|
24
|
+
socket = TCPSocket.new('127.0.0.1', 12345)
|
|
25
|
+
client = OpenSSL::SSL::SSLSocket.new(socket)
|
|
26
|
+
client.hostname = 'example.com'
|
|
27
|
+
p client: client
|
|
28
|
+
client.connect
|
|
29
|
+
rescue => e
|
|
30
|
+
p client_error: e
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
while true
|
|
34
|
+
conn = server.accept
|
|
35
|
+
p accepted: conn
|
|
36
|
+
break
|
|
37
|
+
end
|
data/examples/websocket_demo.rb
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'bundler/
|
|
4
|
-
|
|
5
|
-
gemfile do
|
|
6
|
-
source 'https://rubygems.org'
|
|
7
|
-
gem 'polyphony', '~> 0.44'
|
|
8
|
-
gem 'tipi', '~> 0.31'
|
|
9
|
-
end
|
|
10
|
-
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
11
5
|
require 'tipi/websocket'
|
|
12
6
|
|
|
13
7
|
class WebsocketClient
|
data/examples/ws_page.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<body>
|
|
7
7
|
<script>
|
|
8
8
|
var connect = function () {
|
|
9
|
-
var exampleSocket = new WebSocket("
|
|
9
|
+
var exampleSocket = new WebSocket("ws://localhost:4411/");
|
|
10
10
|
|
|
11
11
|
exampleSocket.onopen = function (event) {
|
|
12
12
|
document.querySelector('#status').innerText = 'connected';
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
<h1 id="status">disconnected</h1>
|
|
31
31
|
<h1 id="msg"></h1>
|
|
32
32
|
</body>
|
|
33
|
-
</html>
|
|
33
|
+
</html>
|
data/lib/tipi.rb
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'polyphony'
|
|
4
|
+
|
|
4
5
|
require_relative './tipi/http1_adapter'
|
|
5
6
|
require_relative './tipi/http2_adapter'
|
|
6
7
|
require_relative './tipi/configuration'
|
|
8
|
+
require_relative './tipi/response_extensions'
|
|
9
|
+
require_relative './tipi/acme'
|
|
10
|
+
|
|
11
|
+
require 'qeweney/request'
|
|
12
|
+
|
|
13
|
+
class Qeweney::Request
|
|
14
|
+
include Tipi::ResponseExtensions
|
|
15
|
+
end
|
|
7
16
|
|
|
8
17
|
module Tipi
|
|
9
18
|
ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
|
|
@@ -42,7 +51,7 @@ module Tipi
|
|
|
42
51
|
ensure
|
|
43
52
|
client.close rescue nil
|
|
44
53
|
end
|
|
45
|
-
|
|
54
|
+
|
|
46
55
|
def protocol_adapter(socket, opts)
|
|
47
56
|
use_http2 = socket.respond_to?(:alpn_protocol) &&
|
|
48
57
|
socket.alpn_protocol == H2_PROTOCOL
|
|
@@ -53,5 +62,84 @@ module Tipi
|
|
|
53
62
|
def route(&block)
|
|
54
63
|
proc { |req| req.route(&block) }
|
|
55
64
|
end
|
|
65
|
+
|
|
66
|
+
CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi')
|
|
67
|
+
CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(
|
|
68
|
+
CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def default_certificate_store
|
|
72
|
+
FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil
|
|
73
|
+
Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def full_service(
|
|
77
|
+
http_port: 10080,
|
|
78
|
+
https_port: 10443,
|
|
79
|
+
certificate_store: default_certificate_store,
|
|
80
|
+
app: nil, &block
|
|
81
|
+
)
|
|
82
|
+
app ||= block
|
|
83
|
+
raise "No app given" unless app
|
|
84
|
+
|
|
85
|
+
http_handler = ->(r) { r.redirect("https://#{r.host}#{r.path}") }
|
|
86
|
+
|
|
87
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
88
|
+
# ctx.ciphers = 'ECDH+aRSA'
|
|
89
|
+
Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
|
|
90
|
+
|
|
91
|
+
challenge_handler = Tipi::ACME::HTTPChallengeHandler.new
|
|
92
|
+
certificate_manager = Tipi::ACME::CertificateManager.new(
|
|
93
|
+
master_ctx: ctx,
|
|
94
|
+
store: certificate_store,
|
|
95
|
+
challenge_handler: challenge_handler
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
http_listener = spin do
|
|
99
|
+
opts = {
|
|
100
|
+
reuse_addr: true,
|
|
101
|
+
reuse_port: true,
|
|
102
|
+
dont_linger: true,
|
|
103
|
+
}
|
|
104
|
+
puts "Listening for HTTP on localhost:#{http_port}"
|
|
105
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', http_port, opts)
|
|
106
|
+
wrapped_handler = certificate_manager.challenge_routing_app(http_handler)
|
|
107
|
+
server.accept_loop do |client|
|
|
108
|
+
spin do
|
|
109
|
+
Tipi.client_loop(client, opts, &wrapped_handler)
|
|
110
|
+
rescue => e
|
|
111
|
+
puts "Uncaught error in HTTP listener: #{e.inspect}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
ensure
|
|
115
|
+
server.close
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
https_listener = spin do
|
|
119
|
+
opts = {
|
|
120
|
+
reuse_addr: true,
|
|
121
|
+
reuse_port: true,
|
|
122
|
+
dont_linger: true,
|
|
123
|
+
secure_context: ctx,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
puts "Listening for HTTPS on localhost:#{https_port}"
|
|
127
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', https_port, opts)
|
|
128
|
+
loop do
|
|
129
|
+
client = server.accept
|
|
130
|
+
spin do
|
|
131
|
+
Tipi.client_loop(client, opts, &app)
|
|
132
|
+
rescue => e
|
|
133
|
+
puts "Uncaught error in HTTPS listener: #{e.inspect}"
|
|
134
|
+
end
|
|
135
|
+
rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError
|
|
136
|
+
# ignore
|
|
137
|
+
end
|
|
138
|
+
ensure
|
|
139
|
+
server.close
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Fiber.await(http_listener, https_listener)
|
|
143
|
+
end
|
|
56
144
|
end
|
|
57
145
|
end
|