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