tipi 0.39 → 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/CHANGELOG.md +7 -0
- data/Gemfile.lock +23 -5
- data/df/server.rb +20 -5
- data/examples/automatic_certificate.rb +193 -0
- data/examples/http_server_forked.rb +3 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +38 -0
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +6 -0
- data/lib/tipi/http1_adapter.rb +36 -1
- data/lib/tipi/http2_adapter.rb +2 -0
- data/lib/tipi/http2_stream.rb +15 -1
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- 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/CHANGELOG.md
CHANGED
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/df/server.rb
CHANGED
@@ -13,7 +13,10 @@ FileUtils.cd(__dir__)
|
|
13
13
|
service = DigitalFabric::Service.new(token: 'foobar')
|
14
14
|
executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
|
15
15
|
|
16
|
-
|
16
|
+
GC.disable
|
17
|
+
Thread.current.backend.idle_gc_period = 60
|
18
|
+
|
19
|
+
# spin_loop(interval: 60) { GC.start }
|
17
20
|
|
18
21
|
class Polyphony::BaseException
|
19
22
|
attr_reader :caller_backtrace
|
@@ -51,7 +54,12 @@ https_listener = spin do
|
|
51
54
|
cert = certificates.shift
|
52
55
|
puts "Certificate expires: #{cert.not_after.inspect}"
|
53
56
|
ctx.add_certificate(cert, private_key, certificates)
|
54
|
-
|
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
|
+
|
55
63
|
opts = {
|
56
64
|
reuse_addr: true,
|
57
65
|
dont_linger: true,
|
@@ -61,21 +69,28 @@ https_listener = spin do
|
|
61
69
|
|
62
70
|
puts 'Listening for HTTPS on localhost:10443'
|
63
71
|
server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
|
64
|
-
|
72
|
+
loop do
|
73
|
+
client = server.accept
|
65
74
|
spin do
|
66
75
|
service.incr_connection_count
|
67
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")
|
68
80
|
ensure
|
69
81
|
service.decr_connection_count
|
70
82
|
end
|
83
|
+
rescue Polyphony::BaseException
|
84
|
+
raise
|
85
|
+
rescue OpenSSL::SSL::SSLError, SystemCallError => e
|
86
|
+
puts "HTTPS accept error: #{e.inspect}"
|
71
87
|
rescue Exception => e
|
72
|
-
puts "HTTPS
|
88
|
+
puts "HTTPS accept error: #{e.inspect}"
|
73
89
|
puts e.backtrace.join("\n")
|
74
90
|
end
|
75
91
|
end
|
76
92
|
|
77
93
|
UNIX_SOCKET_PATH = '/tmp/df.sock'
|
78
|
-
|
79
94
|
unix_listener = spin do
|
80
95
|
puts "Listening on #{UNIX_SOCKET_PATH}"
|
81
96
|
FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
|
@@ -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
|
@@ -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,38 @@
|
|
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
|
+
app = Tipi.route do |req|
|
20
|
+
req.on('normal') do
|
21
|
+
path = File.join(root_path, req.route_relative_path)
|
22
|
+
if File.file?(path)
|
23
|
+
req.serve_file(path)
|
24
|
+
else
|
25
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
req.on('spliced') do
|
29
|
+
path = File.join(root_path, req.route_relative_path)
|
30
|
+
if File.file?(path)
|
31
|
+
req.serve_file(path, respond_from_io: true)
|
32
|
+
else
|
33
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Tipi.serve('0.0.0.0', 4411, opts, &app)
|
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
@@ -4,6 +4,12 @@ require 'polyphony'
|
|
4
4
|
require_relative './tipi/http1_adapter'
|
5
5
|
require_relative './tipi/http2_adapter'
|
6
6
|
require_relative './tipi/configuration'
|
7
|
+
require_relative './tipi/response_extensions'
|
8
|
+
require 'qeweney/request'
|
9
|
+
|
10
|
+
class Qeweney::Request
|
11
|
+
include Tipi::ResponseExtensions
|
12
|
+
end
|
7
13
|
|
8
14
|
module Tipi
|
9
15
|
ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
|
data/lib/tipi/http1_adapter.rb
CHANGED
@@ -50,6 +50,8 @@ module Tipi
|
|
50
50
|
# release references to various objects
|
51
51
|
@requests_head = @requests_tail = nil
|
52
52
|
@parser = nil
|
53
|
+
@splicing_pipe = nil
|
54
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
53
55
|
@conn.close
|
54
56
|
end
|
55
57
|
|
@@ -217,7 +219,25 @@ module Tipi
|
|
217
219
|
@conn.write(formatted_headers)
|
218
220
|
end
|
219
221
|
end
|
222
|
+
|
223
|
+
def respond_from_io(request, io, headers, chunk_size = 2**14)
|
224
|
+
consume_request(request) if @parsing
|
225
|
+
|
226
|
+
formatted_headers = format_headers(headers, true, true)
|
227
|
+
request.tx_incr(formatted_headers.bytesize)
|
220
228
|
|
229
|
+
# assume chunked encoding
|
230
|
+
Thread.current.backend.splice_chunks(
|
231
|
+
io,
|
232
|
+
@conn,
|
233
|
+
formatted_headers,
|
234
|
+
"0\r\n\r\n",
|
235
|
+
->(len) { "#{len.to_s(16)}\r\n" },
|
236
|
+
"\r\n",
|
237
|
+
16384
|
238
|
+
)
|
239
|
+
end
|
240
|
+
|
221
241
|
# Sends response headers. If empty_response is truthy, the response status
|
222
242
|
# code will default to 204, otherwise to 200.
|
223
243
|
# @param request [Qeweney::Request] HTTP request
|
@@ -248,6 +268,20 @@ module Tipi
|
|
248
268
|
@conn.write(data)
|
249
269
|
end
|
250
270
|
|
271
|
+
def send_chunk_from_io(request, io, r, w, chunk_size)
|
272
|
+
len = w.splice(io, chunk_size)
|
273
|
+
if len > 0
|
274
|
+
Thread.current.backend.chain(
|
275
|
+
[:write, @conn, "#{len.to_s(16)}\r\n"],
|
276
|
+
[:splice, r, @conn, len],
|
277
|
+
[:write, @conn, "\r\n"]
|
278
|
+
)
|
279
|
+
else
|
280
|
+
@conn.write("0\r\n\r\n")
|
281
|
+
end
|
282
|
+
len
|
283
|
+
end
|
284
|
+
|
251
285
|
# Finishes the response to the current request. If no headers were sent,
|
252
286
|
# default headers are sent using #send_headers.
|
253
287
|
# @return [void]
|
@@ -257,6 +291,7 @@ module Tipi
|
|
257
291
|
end
|
258
292
|
|
259
293
|
def close
|
294
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
260
295
|
@conn.close
|
261
296
|
end
|
262
297
|
|
@@ -303,7 +338,7 @@ module Tipi
|
|
303
338
|
if chunked
|
304
339
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
305
340
|
else
|
306
|
-
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
341
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
|
307
342
|
end
|
308
343
|
end
|
309
344
|
|
data/lib/tipi/http2_adapter.rb
CHANGED
@@ -87,10 +87,12 @@ module Tipi
|
|
87
87
|
def finalize_client_loop
|
88
88
|
@interface = nil
|
89
89
|
@streams.each_key(&:stop)
|
90
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
90
91
|
@conn.close
|
91
92
|
end
|
92
93
|
|
93
94
|
def close
|
95
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
94
96
|
@conn.close
|
95
97
|
end
|
96
98
|
|
data/lib/tipi/http2_stream.rb
CHANGED
@@ -123,9 +123,23 @@ module Tipi
|
|
123
123
|
headers[':status'] = headers[':status'].to_s
|
124
124
|
with_transfer_count(request) do
|
125
125
|
@stream.headers(transform_headers(headers))
|
126
|
+
@headers_sent = true
|
126
127
|
@stream.data(chunk || '')
|
127
128
|
end
|
128
|
-
|
129
|
+
rescue HTTP2::Error::StreamClosed
|
130
|
+
# ignore
|
131
|
+
end
|
132
|
+
|
133
|
+
def respond_from_io(request, io, headers, chunk_size = 2**16)
|
134
|
+
headers[':status'] ||= Qeweney::Status::OK
|
135
|
+
headers[':status'] = headers[':status'].to_s
|
136
|
+
with_transfer_count(request) do
|
137
|
+
@stream.headers(transform_headers(headers))
|
138
|
+
@headers_sent = true
|
139
|
+
while (chunk = io.read(chunk_size))
|
140
|
+
@stream.data(chunk)
|
141
|
+
end
|
142
|
+
end
|
129
143
|
rescue HTTP2::Error::StreamClosed
|
130
144
|
# ignore
|
131
145
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'qeweney/request'
|
4
|
+
|
5
|
+
module Tipi
|
6
|
+
module ResponseExtensions
|
7
|
+
SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
|
8
|
+
|
9
|
+
def serve_io(io, opts)
|
10
|
+
if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
|
11
|
+
@adapter.respond_from_io(self, io, opts[:headers])
|
12
|
+
else
|
13
|
+
respond(io.read, opts[:headers] || {})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/tipi/version.rb
CHANGED
data/tipi.gemspec
CHANGED
@@ -19,13 +19,14 @@ Gem::Specification.new do |s|
|
|
19
19
|
|
20
20
|
s.executables = ['tipi']
|
21
21
|
|
22
|
-
s.add_runtime_dependency 'polyphony', '~>0.
|
23
|
-
s.add_runtime_dependency 'qeweney', '~>0.
|
22
|
+
s.add_runtime_dependency 'polyphony', '~>0.57.0'
|
23
|
+
s.add_runtime_dependency 'qeweney', '~>0.10.0'
|
24
24
|
|
25
25
|
s.add_runtime_dependency 'http_parser.rb', '~>0.6.0'
|
26
26
|
s.add_runtime_dependency 'http-2', '~>0.10.0'
|
27
27
|
s.add_runtime_dependency 'rack', '>=2.0.8', '<2.3.0'
|
28
28
|
s.add_runtime_dependency 'websocket', '~>1.2.8'
|
29
|
+
s.add_runtime_dependency 'acme-client', '~>2.0.8'
|
29
30
|
|
30
31
|
# for digital fabric
|
31
32
|
s.add_runtime_dependency 'msgpack', '~>1.4.2'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tipi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.40'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-06-
|
11
|
+
date: 2021-06-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: polyphony
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.57.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.57.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: qeweney
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.10.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.10.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: http_parser.rb
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,6 +100,20 @@ dependencies:
|
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: 1.2.8
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: acme-client
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 2.0.8
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 2.0.8
|
103
117
|
- !ruby/object:Gem::Dependency
|
104
118
|
name: msgpack
|
105
119
|
requirement: !ruby/object:Gem::Requirement
|
@@ -215,6 +229,7 @@ files:
|
|
215
229
|
- docs/README.md
|
216
230
|
- docs/tipi-logo.png
|
217
231
|
- e
|
232
|
+
- examples/automatic_certificate.rb
|
218
233
|
- examples/cuba.ru
|
219
234
|
- examples/hanami-api.ru
|
220
235
|
- examples/hello.ru
|
@@ -224,7 +239,9 @@ files:
|
|
224
239
|
- examples/http_server_forked.rb
|
225
240
|
- examples/http_server_form.rb
|
226
241
|
- examples/http_server_graceful.rb
|
242
|
+
- examples/http_server_routes.rb
|
227
243
|
- examples/http_server_simple.rb
|
244
|
+
- examples/http_server_static.rb
|
228
245
|
- examples/http_server_throttled.rb
|
229
246
|
- examples/http_server_throttled_accept.rb
|
230
247
|
- examples/http_server_timeout.rb
|
@@ -259,6 +276,7 @@ files:
|
|
259
276
|
- lib/tipi/http2_adapter.rb
|
260
277
|
- lib/tipi/http2_stream.rb
|
261
278
|
- lib/tipi/rack_adapter.rb
|
279
|
+
- lib/tipi/response_extensions.rb
|
262
280
|
- lib/tipi/version.rb
|
263
281
|
- lib/tipi/websocket.rb
|
264
282
|
- test/coverage.rb
|