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/lib/tipi/acme.rb
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'acme-client'
|
|
5
|
+
require 'localhost/authority'
|
|
6
|
+
|
|
7
|
+
module Tipi
|
|
8
|
+
module ACME
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CertificateManager
|
|
13
|
+
def initialize(master_ctx:, store:, challenge_handler:)
|
|
14
|
+
@master_ctx = master_ctx
|
|
15
|
+
@store = store
|
|
16
|
+
@challenge_handler = challenge_handler
|
|
17
|
+
@contexts = {}
|
|
18
|
+
@requests = Polyphony::Queue.new
|
|
19
|
+
@worker = spin { run }
|
|
20
|
+
setup_sni_callback
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ACME_CHALLENGE_PATH_REGEXP = /\/\.well\-known\/acme\-challenge/.freeze
|
|
24
|
+
|
|
25
|
+
def challenge_routing_app(app)
|
|
26
|
+
->(req) do
|
|
27
|
+
(req.path =~ ACME_CHALLENGE_PATH_REGEXP ? @challenge_handler : app)
|
|
28
|
+
.(req)
|
|
29
|
+
rescue => e
|
|
30
|
+
puts "Error while handling request: #{e.inspect} (headers: #{req.headers})"
|
|
31
|
+
req.respond(nil, ':status' => Qeweney::Status::BAD_REQUEST)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
IP_REGEXP = /^\d+\.\d+\.\d+\.\d+$/
|
|
36
|
+
|
|
37
|
+
def setup_sni_callback
|
|
38
|
+
@master_ctx.servername_cb = proc do |_socket, name|
|
|
39
|
+
p servername_cb: name
|
|
40
|
+
state = { ctx: nil }
|
|
41
|
+
|
|
42
|
+
if name =~ IP_REGEXP
|
|
43
|
+
@master_ctx
|
|
44
|
+
else
|
|
45
|
+
@requests << [name, state]
|
|
46
|
+
wait_for_ctx(state)
|
|
47
|
+
p name: name, error: state if state[:error]
|
|
48
|
+
# Eventually we might want to return an error returned in
|
|
49
|
+
# state[:error]. For the time being we handle errors by returning the
|
|
50
|
+
# master context
|
|
51
|
+
state[:ctx] || @master_ctx
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wait_for_ctx(state)
|
|
57
|
+
period = 0.00001
|
|
58
|
+
while !state[:ctx] && !state[:error]
|
|
59
|
+
orig_sleep period
|
|
60
|
+
period *= 2 if period < 0.1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run
|
|
65
|
+
loop do
|
|
66
|
+
name, state = @requests.shift
|
|
67
|
+
state[:ctx] = get_context(name)
|
|
68
|
+
rescue => e
|
|
69
|
+
state[:error] = e if state
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
LOCALHOST_REGEXP = /\.?localhost$/.freeze
|
|
74
|
+
|
|
75
|
+
def get_context(name)
|
|
76
|
+
@contexts[name] = setup_context(name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def setup_context(name)
|
|
80
|
+
ctx = provision_context(name)
|
|
81
|
+
transfer_ctx_settings(ctx)
|
|
82
|
+
ctx
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def provision_context(name)
|
|
86
|
+
return localhost_context if name =~ LOCALHOST_REGEXP
|
|
87
|
+
|
|
88
|
+
info = get_certificate(name)
|
|
89
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
90
|
+
chain = parse_certificate(info[:certificate])
|
|
91
|
+
cert = chain.shift
|
|
92
|
+
ctx.add_certificate(cert, info[:private_key], chain)
|
|
93
|
+
ctx
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def transfer_ctx_settings(ctx)
|
|
97
|
+
ctx.alpn_protocols = @master_ctx.alpn_protocols
|
|
98
|
+
ctx.alpn_select_cb = @master_ctx.alpn_select_cb
|
|
99
|
+
ctx.ciphers = @master_ctx.ciphers
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
|
103
|
+
|
|
104
|
+
def parse_certificate(certificate)
|
|
105
|
+
certificate
|
|
106
|
+
.scan(CERTIFICATE_REGEXP)
|
|
107
|
+
.map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_expired_stamp(certificate)
|
|
111
|
+
chain = parse_certificate(certificate)
|
|
112
|
+
cert = chain.shift
|
|
113
|
+
cert.not_after
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def get_certificate(name)
|
|
117
|
+
entry = @store.get(name)
|
|
118
|
+
return entry if entry
|
|
119
|
+
|
|
120
|
+
provision_certificate(name).tap do |entry|
|
|
121
|
+
@store.set(name, **entry)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def localhost_context
|
|
126
|
+
@localhost_authority ||= Localhost::Authority.fetch
|
|
127
|
+
@localhost_authority.server_context
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def private_key
|
|
131
|
+
@private_key ||= OpenSSL::PKey::RSA.new(4096)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
|
|
135
|
+
|
|
136
|
+
def acme_client
|
|
137
|
+
@acme_client ||= setup_acme_client
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def setup_acme_client
|
|
141
|
+
client = Acme::Client.new(
|
|
142
|
+
private_key: private_key,
|
|
143
|
+
directory: ACME_DIRECTORY
|
|
144
|
+
)
|
|
145
|
+
account = client.new_account(
|
|
146
|
+
contact: 'mailto:info@noteflakes.com',
|
|
147
|
+
terms_of_service_agreed: true
|
|
148
|
+
)
|
|
149
|
+
client
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def provision_certificate(name)
|
|
153
|
+
p provision_certificate: name
|
|
154
|
+
order = acme_client.new_order(identifiers: [name])
|
|
155
|
+
authorization = order.authorizations.first
|
|
156
|
+
challenge = authorization.http
|
|
157
|
+
|
|
158
|
+
@challenge_handler.add(challenge)
|
|
159
|
+
challenge.request_validation
|
|
160
|
+
while challenge.status == 'pending'
|
|
161
|
+
sleep(0.25)
|
|
162
|
+
challenge.reload
|
|
163
|
+
end
|
|
164
|
+
raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid'
|
|
165
|
+
|
|
166
|
+
p challenge_status: challenge.status
|
|
167
|
+
private_key = OpenSSL::PKey::RSA.new(4096)
|
|
168
|
+
csr = Acme::Client::CertificateRequest.new(
|
|
169
|
+
private_key: private_key,
|
|
170
|
+
subject: { common_name: name }
|
|
171
|
+
)
|
|
172
|
+
order.finalize(csr: csr)
|
|
173
|
+
while order.status == 'processing'
|
|
174
|
+
sleep(0.25)
|
|
175
|
+
order.reload
|
|
176
|
+
end
|
|
177
|
+
certificate = begin
|
|
178
|
+
order.certificate(force_chain: 'DST Root CA X3')
|
|
179
|
+
rescue Acme::Client::Error::ForcedChainNotFound
|
|
180
|
+
order.certificate
|
|
181
|
+
end
|
|
182
|
+
expired_stamp = get_expired_stamp(certificate)
|
|
183
|
+
puts "Certificate for #{name} expires: #{expired_stamp.inspect}"
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
private_key: private_key,
|
|
187
|
+
certificate: certificate,
|
|
188
|
+
expired_stamp: expired_stamp
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class HTTPChallengeHandler
|
|
194
|
+
def initialize
|
|
195
|
+
@challenges = {}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def add(challenge)
|
|
199
|
+
path = "/.well-known/acme-challenge/#{challenge.token}"
|
|
200
|
+
@challenges[path] = challenge
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def remove(challenge)
|
|
204
|
+
path = "/.well-known/acme-challenge/#{challenge.token}"
|
|
205
|
+
@challenges.delete(path)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def call(req)
|
|
209
|
+
challenge = @challenges[req.path]
|
|
210
|
+
|
|
211
|
+
# handle incoming request
|
|
212
|
+
challenge = @challenges[req.path]
|
|
213
|
+
return req.respond(nil, ':status' => 400) unless challenge
|
|
214
|
+
|
|
215
|
+
req.respond(challenge.file_content, 'content-type' => challenge.content_type)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
class CertificateStore
|
|
220
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
|
221
|
+
raise NotImplementedError
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def get(name)
|
|
225
|
+
raise NotImplementedError
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
class InMemoryCertificateStore
|
|
230
|
+
def initialize
|
|
231
|
+
@store = {}
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
|
235
|
+
@store[name] = {
|
|
236
|
+
private_key: private_key,
|
|
237
|
+
certificate: certificate,
|
|
238
|
+
expired_stamp: expired_stamp
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def get(name)
|
|
243
|
+
entry = @store[name]
|
|
244
|
+
return nil unless entry
|
|
245
|
+
if Time.now >= entry[:expired_stamp]
|
|
246
|
+
@store.delete(name)
|
|
247
|
+
return nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
entry
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class SQLiteCertificateStore
|
|
255
|
+
attr_reader :db
|
|
256
|
+
|
|
257
|
+
def initialize(path)
|
|
258
|
+
require 'extralite'
|
|
259
|
+
|
|
260
|
+
@db = Extralite::Database.new(path)
|
|
261
|
+
@db.query("
|
|
262
|
+
create table if not exists certificates (
|
|
263
|
+
name primary key not null,
|
|
264
|
+
private_key not null,
|
|
265
|
+
certificate not null,
|
|
266
|
+
expired_stamp not null
|
|
267
|
+
);"
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def set(name, private_key:, certificate:, expired_stamp:)
|
|
272
|
+
@db.query("
|
|
273
|
+
insert into certificates values (?, ?, ?, ?)
|
|
274
|
+
", name, private_key.to_s, certificate, expired_stamp.to_i)
|
|
275
|
+
rescue Extralite::Error => e
|
|
276
|
+
p error_in_set: e
|
|
277
|
+
raise e
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def get(name)
|
|
281
|
+
remove_expired_certificates
|
|
282
|
+
|
|
283
|
+
entry = @db.query_single_row("
|
|
284
|
+
select name, private_key, certificate, expired_stamp
|
|
285
|
+
from certificates
|
|
286
|
+
where name = ?
|
|
287
|
+
", name)
|
|
288
|
+
return nil unless entry
|
|
289
|
+
entry[:expired_stamp] = Time.at(entry[:expired_stamp])
|
|
290
|
+
entry[:private_key] = OpenSSL::PKey::RSA.new(entry[:private_key])
|
|
291
|
+
entry
|
|
292
|
+
rescue Extralite::Error => e
|
|
293
|
+
p error_in_get: e
|
|
294
|
+
raise e
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def remove_expired_certificates
|
|
298
|
+
@db.query("
|
|
299
|
+
delete from certificates
|
|
300
|
+
where expired_stamp < ?
|
|
301
|
+
", Time.now.to_i)
|
|
302
|
+
rescue Extralite::Error => e
|
|
303
|
+
p error_in_remove_expired_certificates: e
|
|
304
|
+
raise e
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
data/lib/tipi/cli.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tipi'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Tipi
|
|
7
|
+
module CLI
|
|
8
|
+
BANNER = "
|
|
9
|
+
ooo
|
|
10
|
+
oo
|
|
11
|
+
o
|
|
12
|
+
\\|/ Tipi - a better web server for a better world
|
|
13
|
+
/ \\
|
|
14
|
+
/ \\ https://github.com/digital-fabric/tipi
|
|
15
|
+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
|
|
16
|
+
"
|
|
17
|
+
|
|
18
|
+
def self.start
|
|
19
|
+
display_banner
|
|
20
|
+
require File.expand_path(ARGV[0] || 'app.rb', FileUtils.pwd)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.display_banner
|
|
24
|
+
puts BANNER
|
|
25
|
+
puts
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
__END__
|
|
@@ -24,9 +24,11 @@ module DigitalFabric
|
|
|
24
24
|
class GracefulShutdown < RuntimeError
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
@@id = 0
|
|
28
|
+
|
|
27
29
|
def run
|
|
28
30
|
@fiber = Fiber.current
|
|
29
|
-
@keep_alive_timer = spin_loop(interval: 5) { keep_alive }
|
|
31
|
+
@keep_alive_timer = spin_loop("#{@fiber.tag}-keep_alive", interval: 5) { keep_alive }
|
|
30
32
|
while true
|
|
31
33
|
connect_and_process_incoming_requests
|
|
32
34
|
return if @shutdown
|
|
@@ -166,12 +168,13 @@ module DigitalFabric
|
|
|
166
168
|
def recv_http_request(msg)
|
|
167
169
|
req = prepare_http_request(msg)
|
|
168
170
|
id = msg[Protocol::Attribute::ID]
|
|
169
|
-
@requests[id] = spin do
|
|
171
|
+
@requests[id] = spin("#{Fiber.current.tag}.#{id}") do
|
|
170
172
|
http_request(req)
|
|
171
173
|
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
|
172
174
|
# ignore
|
|
173
|
-
rescue Polyphony::Terminate
|
|
175
|
+
rescue Polyphony::Terminate => e
|
|
174
176
|
req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
|
|
177
|
+
raise e
|
|
175
178
|
ensure
|
|
176
179
|
@requests.delete(id)
|
|
177
180
|
@long_running_requests.delete(id)
|
|
@@ -185,7 +188,6 @@ module DigitalFabric
|
|
|
185
188
|
complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]
|
|
186
189
|
req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))
|
|
187
190
|
req.buffer_body_chunk(body_chunk) if body_chunk
|
|
188
|
-
req.complete! if complete
|
|
189
191
|
req
|
|
190
192
|
end
|
|
191
193
|
|
|
@@ -204,7 +206,7 @@ module DigitalFabric
|
|
|
204
206
|
def recv_ws_request(msg)
|
|
205
207
|
req = Qeweney::Request.new(msg[Protocol::Attribute::WS::HEADERS], RequestAdapter.new(self, msg))
|
|
206
208
|
id = msg[Protocol::Attribute::ID]
|
|
207
|
-
@requests[id] = @long_running_requests[id] = spin do
|
|
209
|
+
@requests[id] = @long_running_requests[id] = spin("#{Fiber.current.tag}.#{id}-ws") do
|
|
208
210
|
ws_request(req)
|
|
209
211
|
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
|
210
212
|
# ignore
|
|
@@ -32,13 +32,13 @@ module DigitalFabric
|
|
|
32
32
|
@fiber = Fiber.current
|
|
33
33
|
@service.mount(route, self)
|
|
34
34
|
@mounted = true
|
|
35
|
-
keep_alive_timer = spin_loop(interval: 5) { keep_alive }
|
|
35
|
+
# keep_alive_timer = spin_loop("#{@fiber.tag}-keep_alive", interval: 5) { keep_alive }
|
|
36
36
|
process_incoming_messages(false)
|
|
37
37
|
rescue GracefulShutdown
|
|
38
38
|
puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
|
|
39
|
-
process_incoming_messages(true)
|
|
39
|
+
move_on_after(15) { process_incoming_messages(true) }
|
|
40
40
|
ensure
|
|
41
|
-
keep_alive_timer&.stop
|
|
41
|
+
# keep_alive_timer&.stop
|
|
42
42
|
unmount
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -60,7 +60,7 @@ module DigitalFabric
|
|
|
60
60
|
@mounted = nil
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def send_shutdown
|
|
64
64
|
send_df_message(Protocol.shutdown)
|
|
65
65
|
@fiber.raise GracefulShutdown.new
|
|
66
66
|
end
|
|
@@ -98,6 +98,8 @@ module DigitalFabric
|
|
|
98
98
|
return
|
|
99
99
|
when Protocol::UNMOUNT
|
|
100
100
|
return unmount
|
|
101
|
+
when Protocol::STATS_REQUEST
|
|
102
|
+
return handle_stats_request(message[Protocol::Attribute::ID])
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
handler = @requests[message[Protocol::Attribute::ID]]
|
|
@@ -142,11 +144,12 @@ module DigitalFabric
|
|
|
142
144
|
t0 = Time.now
|
|
143
145
|
t1 = nil
|
|
144
146
|
with_request do |id|
|
|
145
|
-
|
|
147
|
+
msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
|
|
148
|
+
send_df_message(msg)
|
|
146
149
|
while (message = receive)
|
|
147
150
|
unless t1
|
|
148
151
|
t1 = Time.now
|
|
149
|
-
@service.record_latency_measurement(t1 - t0)
|
|
152
|
+
@service.record_latency_measurement(t1 - t0, req)
|
|
150
153
|
end
|
|
151
154
|
kind = message[Protocol::Attribute::KIND]
|
|
152
155
|
attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
|
|
@@ -187,6 +190,11 @@ module DigitalFabric
|
|
|
187
190
|
send_df_message(Protocol.transfer_count(key, rx, tx))
|
|
188
191
|
end
|
|
189
192
|
|
|
193
|
+
def handle_stats_request(id)
|
|
194
|
+
stats = @service.get_stats
|
|
195
|
+
send_df_message(Protocol.stats_response(id, stats))
|
|
196
|
+
end
|
|
197
|
+
|
|
190
198
|
HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
|
|
191
199
|
|
|
192
200
|
def http_custom_upgrade(id, req, headers)
|
|
@@ -197,7 +205,7 @@ module DigitalFabric
|
|
|
197
205
|
req.send_headers(upgrade_headers, true)
|
|
198
206
|
|
|
199
207
|
conn = req.adapter.conn
|
|
200
|
-
reader = spin do
|
|
208
|
+
reader = spin("#{Fiber.current.tag}.#{id}") do
|
|
201
209
|
conn.recv_loop do |data|
|
|
202
210
|
send_df_message(Protocol.conn_data(id, data))
|
|
203
211
|
end
|
|
@@ -294,7 +302,7 @@ module DigitalFabric
|
|
|
294
302
|
end
|
|
295
303
|
|
|
296
304
|
def run_websocket_connection(id, websocket)
|
|
297
|
-
reader = spin do
|
|
305
|
+
reader = spin("#{Fiber.current}.#{id}-ws") do
|
|
298
306
|
websocket.recv_loop do |data|
|
|
299
307
|
send_df_message(Protocol.ws_data(id, data))
|
|
300
308
|
end
|