tipi 0.37 → 0.40
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 +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +53 -29
- data/Gemfile.lock +23 -5
- data/TODO.md +79 -5
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +63 -6
- data/examples/automatic_certificate.rb +193 -0
- data/examples/http_server.rb +11 -3
- data/examples/http_server_forked.rb +5 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +38 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +10 -1
- data/examples/https_wss_server.rb +2 -1
- data/examples/rack_server.rb +5 -0
- data/examples/rack_server_https.rb +1 -1
- data/examples/rack_server_https_forked.rb +4 -3
- data/examples/routing_server.rb +5 -4
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +6 -0
- data/lib/tipi/digital_fabric/agent.rb +16 -13
- data/lib/tipi/digital_fabric/agent_proxy.rb +79 -27
- data/lib/tipi/digital_fabric/protocol.rb +71 -14
- data/lib/tipi/digital_fabric/request_adapter.rb +7 -7
- data/lib/tipi/digital_fabric/service.rb +10 -8
- data/lib/tipi/http1_adapter.rb +87 -36
- data/lib/tipi/http2_adapter.rb +37 -4
- data/lib/tipi/http2_stream.rb +79 -22
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/test/test_http_server.rb +22 -37
- data/test/test_request.rb +4 -4
- data/tipi.gemspec +3 -2
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e78bf3b20c7ec6704c26869694cf69e389ae1c0c2f2e44c82d0104d2ecaded9
|
|
4
|
+
data.tar.gz: dac8b0014cbc3a6ee5b146f3645bd38f3d55b21d5c17d969eecaa62c1e8b9306
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa53cedcd271ba9dbc37821315d27bf14b1eac17451c912eb3dcaa3ceae3b0b738f5d10678cf390f0534edd9d352008e71dec95c46566cad513e3b2d783ea000
|
|
7
|
+
data.tar.gz: 3cf95c22b05c8eaebdf646963d930977ddb010c3ca9174f081cee674e315f5ac672059b31f833ec337c78e3ec43770f72f6bba145ad00e2bd8c1ed8d0acb2174
|
data/.github/workflows/test.yml
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,72 +1,96 @@
|
|
|
1
|
+
## 0.40 2021-06-24
|
|
2
|
+
|
|
3
|
+
- Implement serving static files using splice_chunks (nice performance boost for
|
|
4
|
+
files bigger than 1M)
|
|
5
|
+
- Call shutdown before closing socket
|
|
6
|
+
- Fix examples (thanks @timhatch!)
|
|
7
|
+
|
|
8
|
+
## 0.39 2021-06-20
|
|
9
|
+
|
|
10
|
+
- More work on DF server
|
|
11
|
+
- Fix HTTP2StreamHandler#send_headers
|
|
12
|
+
- Various fixes to HTTP/2 adapter
|
|
13
|
+
- Fix host detection for HTTP/2 connections
|
|
14
|
+
- Fix HTTP/1 adapter #respond with nil body
|
|
15
|
+
- Fix HTTP1Adapter#send_headers
|
|
16
|
+
|
|
17
|
+
## 0.38 2021-03-09
|
|
18
|
+
|
|
19
|
+
- Don't use chunked transfer encoding for non-streaming responses
|
|
20
|
+
|
|
21
|
+
## 0.37.2 2021-03-08
|
|
22
|
+
|
|
23
|
+
- Fix header formatting when header value is an array
|
|
24
|
+
|
|
1
25
|
## 0.37 2021-02-15
|
|
2
26
|
|
|
3
|
-
|
|
27
|
+
- Update upgrade mechanism to work with updated Qeweney API
|
|
4
28
|
|
|
5
29
|
## 0.36 2021-02-12
|
|
6
30
|
|
|
7
|
-
|
|
31
|
+
- Use `Qeweney::Status` constants
|
|
8
32
|
|
|
9
33
|
## 0.35 2021-02-10
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
- Extract Request class into separate [qeweney](https://github.com/digital-fabric/qeweney) gem
|
|
12
36
|
|
|
13
37
|
## 0.34 2021-02-07
|
|
14
38
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
- Implement digital fabric service and agents
|
|
40
|
+
- Add multipart and urlencoded form data parsing
|
|
41
|
+
- Improve request body reading behaviour
|
|
42
|
+
- Add more `Request` information methods
|
|
43
|
+
- Add access to connection for HTTP2 requests
|
|
44
|
+
- Allow calling `Request#send_chunk` with empty chunk
|
|
45
|
+
- Add support for handling protocol upgrades from within request handler
|
|
22
46
|
|
|
23
47
|
## 0.33 2020-11-20
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
- Update code for Polyphony 0.47.5
|
|
50
|
+
- Add support for Rack::File body to Tipi::RackAdapter
|
|
27
51
|
|
|
28
52
|
## 0.32 2020-08-14
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
- Respond with array of strings instead of concatenating for HTTP 1
|
|
55
|
+
- Use read_loop instead of readpartial
|
|
56
|
+
- Fix http upgrade test
|
|
33
57
|
|
|
34
58
|
## 0.31 2020-07-28
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
- Fix websocket server code
|
|
61
|
+
- Implement configuration layer (WIP)
|
|
62
|
+
- Improve performance of rack adapter
|
|
39
63
|
|
|
40
64
|
## 0.30 2020-07-15
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
- Rename project to Tipi
|
|
67
|
+
- Rearrange source code
|
|
68
|
+
- Remove HTTP client code (to be developed eventually into a separate gem)
|
|
69
|
+
- Fix header rendering in rack adapter (#2)
|
|
46
70
|
|
|
47
71
|
## 0.29 2020-07-06
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
- Use IO#read_loop
|
|
50
74
|
|
|
51
75
|
## 0.28 2020-07-03
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
- Update with API changes from Polyphony >= 0.41
|
|
54
78
|
|
|
55
79
|
## 0.27 2020-04-14
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
- Remove modulation dependency
|
|
58
82
|
|
|
59
83
|
## 0.26 2020-03-03
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
- Fix `Server#listen`
|
|
62
86
|
|
|
63
87
|
## 0.25 2020-02-19
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
- Ensure server socket is closed upon stopping loop
|
|
90
|
+
- Fix `Request#format_header_lines`
|
|
67
91
|
|
|
68
92
|
## 0.24 2020-01-08
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
- Move HTTP to separate polyphony-http gem
|
|
71
95
|
|
|
72
96
|
For earlier changes look at the Polyphony changelog.
|
data/Gemfile.lock
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
tipi (0.
|
|
4
|
+
tipi (0.40)
|
|
5
|
+
acme-client (~> 2.0.8)
|
|
5
6
|
http-2 (~> 0.10.0)
|
|
6
7
|
http_parser.rb (~> 0.6.0)
|
|
7
8
|
msgpack (~> 1.4.2)
|
|
8
|
-
polyphony (~> 0.
|
|
9
|
-
qeweney (~> 0.
|
|
9
|
+
polyphony (~> 0.57.0)
|
|
10
|
+
qeweney (~> 0.10.0)
|
|
10
11
|
rack (>= 2.0.8, < 2.3.0)
|
|
11
12
|
websocket (~> 1.2.8)
|
|
12
13
|
|
|
13
14
|
GEM
|
|
14
15
|
remote: https://rubygems.org/
|
|
15
16
|
specs:
|
|
17
|
+
acme-client (2.0.8)
|
|
18
|
+
faraday (>= 0.17, < 2.0.0)
|
|
16
19
|
ansi (1.5.0)
|
|
17
20
|
builder (3.2.4)
|
|
18
21
|
docile (1.3.2)
|
|
19
22
|
escape_utils (1.2.1)
|
|
23
|
+
faraday (1.4.3)
|
|
24
|
+
faraday-em_http (~> 1.0)
|
|
25
|
+
faraday-em_synchrony (~> 1.0)
|
|
26
|
+
faraday-excon (~> 1.1)
|
|
27
|
+
faraday-net_http (~> 1.0)
|
|
28
|
+
faraday-net_http_persistent (~> 1.1)
|
|
29
|
+
multipart-post (>= 1.2, < 3)
|
|
30
|
+
ruby2_keywords (>= 0.0.4)
|
|
31
|
+
faraday-em_http (1.0.0)
|
|
32
|
+
faraday-em_synchrony (1.0.0)
|
|
33
|
+
faraday-excon (1.1.0)
|
|
34
|
+
faraday-net_http (1.0.1)
|
|
35
|
+
faraday-net_http_persistent (1.1.0)
|
|
20
36
|
http-2 (0.10.2)
|
|
21
37
|
http_parser.rb (0.6.0)
|
|
22
38
|
json (2.3.1)
|
|
@@ -28,12 +44,14 @@ GEM
|
|
|
28
44
|
minitest (>= 5.0)
|
|
29
45
|
ruby-progressbar
|
|
30
46
|
msgpack (1.4.2)
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
multipart-post (2.1.1)
|
|
48
|
+
polyphony (0.57.0)
|
|
49
|
+
qeweney (0.10)
|
|
33
50
|
escape_utils (~> 1.2.1)
|
|
34
51
|
rack (2.2.3)
|
|
35
52
|
rake (12.3.3)
|
|
36
53
|
ruby-progressbar (1.10.1)
|
|
54
|
+
ruby2_keywords (0.0.4)
|
|
37
55
|
simplecov (0.17.1)
|
|
38
56
|
docile (~> 1.1)
|
|
39
57
|
json (>= 1.8, < 3)
|
data/TODO.md
CHANGED
|
@@ -1,8 +1,82 @@
|
|
|
1
|
-
|
|
1
|
+
## Add an API for reading a request body chunk into an IO (pipe)
|
|
2
|
+
|
|
3
|
+
```ruby
|
|
4
|
+
# currently
|
|
5
|
+
chunk = req.next_chunk
|
|
6
|
+
# or
|
|
7
|
+
req.each_chunk { |c| do_something(c) }
|
|
8
|
+
|
|
9
|
+
# what we'd like to do
|
|
10
|
+
r, w = IO.pipe
|
|
11
|
+
len = req.splice_chunk(w)
|
|
12
|
+
sock << "Here comes a chunk of #{len} bytes\n"
|
|
13
|
+
sock.splice(r, len)
|
|
14
|
+
|
|
15
|
+
# or:
|
|
16
|
+
r, w = IO.pipe
|
|
17
|
+
req.splice_each_chunk(w) do |len|
|
|
18
|
+
sock << "Here comes a chunk of #{len} bytes\n"
|
|
19
|
+
sock.splice(r, len)
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
# HTTP/1.1 parser
|
|
24
|
+
|
|
25
|
+
- httparser.rb is not actively updated
|
|
26
|
+
- the httparser.rb C parser code comes originally from https://github.com/nodejs/llhttp
|
|
27
|
+
- there's a Ruby gem https://github.com/metabahn/llhttp, but its API is too low-level
|
|
28
|
+
(lots of callbacks, headers need to be retained across callbacks)
|
|
29
|
+
- the basic idea is to import the C-code, then build a parser object with the following
|
|
30
|
+
callbacks:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
on_headers_complete(headers)
|
|
34
|
+
on_body_chunk(chunk)
|
|
35
|
+
on_message_complete
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- The llhttp gem's C-code is here: https://github.com/metabahn/llhttp/tree/main/mri
|
|
39
|
+
|
|
40
|
+
- Actually, if you do a C extension, instead of a callback-based API, we can
|
|
41
|
+
design a blocking API:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
parser = Tipi::HTTP1::Parser.new
|
|
45
|
+
parser.each_request(socket) do |headers|
|
|
46
|
+
request = Request.new(normalize_headers(headers))
|
|
47
|
+
handle_request(request)
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
# What about HTTP/2?
|
|
52
|
+
|
|
53
|
+
It would be a nice exercise in converting a callback-based API to a blocking
|
|
54
|
+
one:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
parser = Tipi::HTTP2::Parser.new(socket)
|
|
58
|
+
parser.each_stream(socket) do |stream|
|
|
59
|
+
spin { handle_stream(stream) }
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# DF
|
|
66
|
+
|
|
67
|
+
- Add attack protection for IP-address HTTP host:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
IPV4_REGEXP = /^\d+\.\d+\.\d+\.\d+$/.freeze
|
|
71
|
+
|
|
72
|
+
def is_attack_request?(req)
|
|
73
|
+
return true if req.host =~ IPV4_REGEXP && req.query[:q] != 'ping'
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Add attack route to Qeweney routing API
|
|
2
78
|
|
|
3
|
-
Problems to fix:
|
|
4
79
|
|
|
5
|
-
- Memory leak (in server? multi agent? multi client?)
|
|
6
80
|
|
|
7
81
|
# Roadmap
|
|
8
82
|
|
|
@@ -14,7 +88,7 @@ Problems to fix:
|
|
|
14
88
|
- https://gitlab.com/honeyryderchuck/http-2-next
|
|
15
89
|
- Open an issue there, ask what's the difference between the two gems?
|
|
16
90
|
|
|
17
|
-
## 0.
|
|
91
|
+
## 0.38
|
|
18
92
|
|
|
19
93
|
- Add more poly CLI commands and options:
|
|
20
94
|
|
|
@@ -26,7 +100,7 @@ Problems to fix:
|
|
|
26
100
|
- set port to bind to
|
|
27
101
|
- set forking process count
|
|
28
102
|
|
|
29
|
-
## 0.
|
|
103
|
+
## 0.39 Working Sinatra application
|
|
30
104
|
|
|
31
105
|
- app with database access (postgresql)
|
|
32
106
|
- benchmarks!
|
data/df/sample_agent.rb
CHANGED
|
@@ -13,7 +13,7 @@ class SampleAgent < DigitalFabric::Agent
|
|
|
13
13
|
HTML_SSE = IO.read(File.join(__dir__, 'sse_page.html'))
|
|
14
14
|
|
|
15
15
|
def http_request(req)
|
|
16
|
-
path = req
|
|
16
|
+
path = req.headers[':path']
|
|
17
17
|
case path
|
|
18
18
|
when '/agent'
|
|
19
19
|
send_df_message(Protocol.http_response(
|
data/df/server.rb
CHANGED
|
@@ -6,12 +6,17 @@ require 'tipi/digital_fabric'
|
|
|
6
6
|
require 'tipi/digital_fabric/executive'
|
|
7
7
|
require 'json'
|
|
8
8
|
require 'fileutils'
|
|
9
|
+
require 'localhost/authority'
|
|
10
|
+
|
|
9
11
|
FileUtils.cd(__dir__)
|
|
10
12
|
|
|
11
13
|
service = DigitalFabric::Service.new(token: 'foobar')
|
|
12
14
|
executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
GC.disable
|
|
17
|
+
Thread.current.backend.idle_gc_period = 60
|
|
18
|
+
|
|
19
|
+
# spin_loop(interval: 60) { GC.start }
|
|
15
20
|
|
|
16
21
|
class Polyphony::BaseException
|
|
17
22
|
attr_reader :caller_backtrace
|
|
@@ -19,13 +24,13 @@ end
|
|
|
19
24
|
|
|
20
25
|
puts "pid: #{Process.pid}"
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
http_listener = spin do
|
|
23
28
|
opts = {
|
|
24
29
|
reuse_addr: true,
|
|
25
30
|
dont_linger: true,
|
|
26
31
|
}
|
|
27
|
-
puts 'Listening on localhost:
|
|
28
|
-
server = Polyphony::Net.tcp_listen('0.0.0.0',
|
|
32
|
+
puts 'Listening for HTTP on localhost:10080'
|
|
33
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)
|
|
29
34
|
server.accept_loop do |client|
|
|
30
35
|
spin do
|
|
31
36
|
service.incr_connection_count
|
|
@@ -33,11 +38,59 @@ tcp_listener = spin do
|
|
|
33
38
|
ensure
|
|
34
39
|
service.decr_connection_count
|
|
35
40
|
end
|
|
41
|
+
rescue Exception => e
|
|
42
|
+
puts "HTTP accept_loop error: #{e.inspect}"
|
|
43
|
+
puts e.backtrace.join("\n")
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
|
48
|
+
|
|
49
|
+
https_listener = spin do
|
|
50
|
+
private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
|
|
51
|
+
c = IO.read('../../reality/ssl/cacert.pem')
|
|
52
|
+
certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
|
53
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
54
|
+
cert = certificates.shift
|
|
55
|
+
puts "Certificate expires: #{cert.not_after.inspect}"
|
|
56
|
+
ctx.add_certificate(cert, private_key, certificates)
|
|
57
|
+
ctx.ciphers = 'ECDH+aRSA'
|
|
58
|
+
|
|
59
|
+
# TODO: further limit ciphers
|
|
60
|
+
# ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
|
|
61
|
+
# ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/tls.rb
|
|
62
|
+
|
|
63
|
+
opts = {
|
|
64
|
+
reuse_addr: true,
|
|
65
|
+
dont_linger: true,
|
|
66
|
+
secure_context: ctx,
|
|
67
|
+
alpn_protocols: Tipi::ALPN_PROTOCOLS
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
puts 'Listening for HTTPS on localhost:10443'
|
|
71
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
|
|
72
|
+
loop do
|
|
73
|
+
client = server.accept
|
|
74
|
+
spin do
|
|
75
|
+
service.incr_connection_count
|
|
76
|
+
Tipi.client_loop(client, opts) { |req| service.http_request(req) }
|
|
77
|
+
rescue Exception => e
|
|
78
|
+
puts "Exception: #{e.inspect}"
|
|
79
|
+
puts e.backtrace.join("\n")
|
|
80
|
+
ensure
|
|
81
|
+
service.decr_connection_count
|
|
82
|
+
end
|
|
83
|
+
rescue Polyphony::BaseException
|
|
84
|
+
raise
|
|
85
|
+
rescue OpenSSL::SSL::SSLError, SystemCallError => e
|
|
86
|
+
puts "HTTPS accept error: #{e.inspect}"
|
|
87
|
+
rescue Exception => e
|
|
88
|
+
puts "HTTPS accept error: #{e.inspect}"
|
|
89
|
+
puts e.backtrace.join("\n")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
40
92
|
|
|
93
|
+
UNIX_SOCKET_PATH = '/tmp/df.sock'
|
|
41
94
|
unix_listener = spin do
|
|
42
95
|
puts "Listening on #{UNIX_SOCKET_PATH}"
|
|
43
96
|
FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
|
|
@@ -46,9 +99,13 @@ unix_listener = spin do
|
|
|
46
99
|
end
|
|
47
100
|
|
|
48
101
|
begin
|
|
49
|
-
Fiber.await(
|
|
102
|
+
Fiber.await(http_listener, https_listener, unix_listener)
|
|
50
103
|
rescue Interrupt
|
|
51
104
|
puts "Got SIGINT, shutting down gracefully"
|
|
52
105
|
service.graceful_shutdown
|
|
53
106
|
puts "post graceful shutdown"
|
|
107
|
+
rescue Exception => e
|
|
108
|
+
puts '*' * 40
|
|
109
|
+
p e
|
|
110
|
+
puts e.backtrace.join("\n")
|
|
54
111
|
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'tipi'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'acme-client'
|
|
7
|
+
|
|
8
|
+
# ::Exception.__disable_sanitized_backtrace__ = true
|
|
9
|
+
|
|
10
|
+
class CertificateManager
|
|
11
|
+
def initialize(store:, challenge_handler:)
|
|
12
|
+
@store = store
|
|
13
|
+
@challenge_handler = challenge_handler
|
|
14
|
+
@workers = {}
|
|
15
|
+
@contexts = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](name)
|
|
19
|
+
worker = worker_for_name(name)
|
|
20
|
+
p worker: worker
|
|
21
|
+
|
|
22
|
+
worker << Fiber.current
|
|
23
|
+
# cancel_after(30) { receive }
|
|
24
|
+
receive.tap { |ctx| p got_ctx: ctx }
|
|
25
|
+
rescue Exception => e
|
|
26
|
+
p e
|
|
27
|
+
puts e.backtrace.join("\n")
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def worker_for_name(name)
|
|
32
|
+
@workers[name] ||= spin { worker_loop(name) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def worker_loop(name)
|
|
36
|
+
while (client = receive)
|
|
37
|
+
puts "get request for #{name} from #{client.inspect}"
|
|
38
|
+
ctx = get_context(name)
|
|
39
|
+
client << ctx rescue nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_context(name)
|
|
44
|
+
@contexts[name] ||= setup_context(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
|
48
|
+
|
|
49
|
+
def setup_context(name)
|
|
50
|
+
certificate = get_certificate(name)
|
|
51
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
52
|
+
chain = certificate.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
|
53
|
+
cert = chain.shift
|
|
54
|
+
puts "Certificate expires: #{cert.not_after.inspect}"
|
|
55
|
+
ctx.add_certificate(cert, private_key, chain)
|
|
56
|
+
Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
|
|
57
|
+
ctx
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_certificate(name)
|
|
61
|
+
@store[name] ||= provision_certificate(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def private_key
|
|
65
|
+
@private_key ||= OpenSSL::PKey::RSA.new(4096)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
ACME_DIRECTORY = 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
|
69
|
+
|
|
70
|
+
def acme_client
|
|
71
|
+
@acme_client ||= setup_acme_client
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def setup_acme_client
|
|
75
|
+
client = Acme::Client.new(
|
|
76
|
+
private_key: private_key,
|
|
77
|
+
directory: ACME_DIRECTORY
|
|
78
|
+
)
|
|
79
|
+
p client: client
|
|
80
|
+
account = client.new_account(
|
|
81
|
+
contact: 'mailto:info@noteflakes.com',
|
|
82
|
+
terms_of_service_agreed: true
|
|
83
|
+
)
|
|
84
|
+
p account: account.kid
|
|
85
|
+
client
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def provision_certificate(name)
|
|
89
|
+
order = acme_client.new_order(identifiers: [name])
|
|
90
|
+
p order: true
|
|
91
|
+
authorization = order.authorizations.first
|
|
92
|
+
p authorization: authorization
|
|
93
|
+
challenge = authorization.http
|
|
94
|
+
p challenge: challenge
|
|
95
|
+
|
|
96
|
+
@challenge_handler.add(challenge)
|
|
97
|
+
challenge.request_validation
|
|
98
|
+
p challenge_status: challenge.status
|
|
99
|
+
while challenge.status == 'pending'
|
|
100
|
+
sleep(1)
|
|
101
|
+
challenge.reload
|
|
102
|
+
p challenge_status: challenge.status
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
csr = Acme::Client::CertificateRequest.new(private_key: @private_key, subject: { common_name: name })
|
|
106
|
+
p csr: csr
|
|
107
|
+
order.finalize(csr: csr)
|
|
108
|
+
p order_status: order.status
|
|
109
|
+
while order.status == 'processing'
|
|
110
|
+
sleep(1)
|
|
111
|
+
order.reload
|
|
112
|
+
p order_status: order.status
|
|
113
|
+
end
|
|
114
|
+
order.certificate.tap { |c| p certificate: c } # => PEM-formatted certificate
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class AcmeHTTPChallengeHandler
|
|
119
|
+
def initialize
|
|
120
|
+
@challenges = {}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add(challenge)
|
|
124
|
+
path = "/.well-known/acme-challenge/#{challenge.token}"
|
|
125
|
+
@challenges[path] = challenge
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def call(req)
|
|
129
|
+
# handle incoming request
|
|
130
|
+
challenge = @challenges[req.path]
|
|
131
|
+
return req.respond(nil, ':status' => 400) unless challenge
|
|
132
|
+
|
|
133
|
+
req.respond(challenge.file_content, 'content-type' => challenge.content_type)
|
|
134
|
+
@challenges.delete(req.path)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
challenge_handler = AcmeHTTPChallengeHandler.new
|
|
139
|
+
certificate_manager = CertificateManager.new(
|
|
140
|
+
store: {},
|
|
141
|
+
challenge_handler: challenge_handler
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
http_handler = Tipi.route do |r|
|
|
145
|
+
r.on('/.well-known/acme-challenge') { challenge_handler.call(r) }
|
|
146
|
+
r.default { r.redirect "https://#{r.host}#{r.path}" }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
https_handler = ->(r) { r.respond('Hello, world!') }
|
|
150
|
+
|
|
151
|
+
http_listener = spin do
|
|
152
|
+
opts = {
|
|
153
|
+
reuse_addr: true,
|
|
154
|
+
dont_linger: true,
|
|
155
|
+
}
|
|
156
|
+
puts 'Listening for HTTP on localhost:10080'
|
|
157
|
+
Tipi.serve('0.0.0.0', 10080, opts, &http_handler)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
https_listener = spin do
|
|
161
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
162
|
+
ctx.servername_cb = proc { |_, name| p name: name; certificate_manager[name] }
|
|
163
|
+
opts = {
|
|
164
|
+
reuse_addr: true,
|
|
165
|
+
dont_linger: true,
|
|
166
|
+
secure_context: ctx,
|
|
167
|
+
alpn_protocols: Tipi::ALPN_PROTOCOLS
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
puts 'Listening for HTTPS on localhost:10443'
|
|
171
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
|
|
172
|
+
server.accept_loop do |client|
|
|
173
|
+
spin do
|
|
174
|
+
service.incr_connection_count
|
|
175
|
+
Tipi.client_loop(client, opts) { |req| service.http_request(req) }
|
|
176
|
+
ensure
|
|
177
|
+
service.decr_connection_count
|
|
178
|
+
end
|
|
179
|
+
rescue Exception => e
|
|
180
|
+
puts "HTTPS accept_loop error: #{e.inspect}"
|
|
181
|
+
puts e.backtrace.join("\n")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
Fiber.await(http_listener, https_listener)
|
|
187
|
+
rescue Interrupt
|
|
188
|
+
puts "Got SIGINT, terminating"
|
|
189
|
+
rescue Exception => e
|
|
190
|
+
puts '*' * 40
|
|
191
|
+
p e
|
|
192
|
+
puts e.backtrace.join("\n")
|
|
193
|
+
end
|