tipi 0.37 → 0.40
Sign up to get free protection for your applications and to get access to all the features.
- 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
|