tipi 0.38 → 0.39
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 +9 -0
- data/Gemfile.lock +5 -5
- data/TODO.md +77 -1
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +46 -4
- data/examples/http_server.rb +1 -1
- data/examples/http_server_forked.rb +2 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +3 -4
- 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/lib/tipi/digital_fabric/agent.rb +16 -13
- data/lib/tipi/digital_fabric/agent_proxy.rb +79 -32
- 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 +9 -7
- data/lib/tipi/http1_adapter.rb +35 -26
- data/lib/tipi/http2_adapter.rb +35 -4
- data/lib/tipi/http2_stream.rb +63 -20
- data/lib/tipi/version.rb +1 -1
- data/tipi.gemspec +2 -2
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02c5ce4798e1961ca59798b75bafe158570e777661f6fc8667169553df3cd3f4
|
4
|
+
data.tar.gz: 405bfa3526efaad394d34840764dc131230a366574cfc52b63344a6d5cdfe6dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 754bd35136e4e004e6bd782eb2060d933b2439c437a9c2a966529e0465c42096b97fa08b1dae04b7a3c8aa0c77c367fd5395d215cca73fd7cf1aa80c297b3178
|
7
|
+
data.tar.gz: 24425d798c076ad43b4fa45b641a90a5e801dfe35acd6217e8a8fc97ea546f18457fc9a8653e47f7bd5f696da885ef86db8db36385dcaa79ccbcf9d42d64081c
|
data/.github/workflows/test.yml
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## 0.39 2021-06-20
|
2
|
+
|
3
|
+
- More work on DF server
|
4
|
+
- Fix HTTP2StreamHandler#send_headers
|
5
|
+
- Various fixes to HTTP/2 adapter
|
6
|
+
- Fix host detection for HTTP/2 connections
|
7
|
+
- Fix HTTP/1 adapter #respond with nil body
|
8
|
+
- Fix HTTP1Adapter#send_headers
|
9
|
+
|
1
10
|
## 0.38 2021-03-09
|
2
11
|
|
3
12
|
- Don't use chunked transfer encoding for non-streaming responses
|
data/Gemfile.lock
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tipi (0.
|
4
|
+
tipi (0.39)
|
5
5
|
http-2 (~> 0.10.0)
|
6
6
|
http_parser.rb (~> 0.6.0)
|
7
7
|
msgpack (~> 1.4.2)
|
8
|
-
polyphony (~> 0.
|
9
|
-
qeweney (~> 0.
|
8
|
+
polyphony (~> 0.55.0)
|
9
|
+
qeweney (~> 0.9.1)
|
10
10
|
rack (>= 2.0.8, < 2.3.0)
|
11
11
|
websocket (~> 1.2.8)
|
12
12
|
|
@@ -28,8 +28,8 @@ GEM
|
|
28
28
|
minitest (>= 5.0)
|
29
29
|
ruby-progressbar
|
30
30
|
msgpack (1.4.2)
|
31
|
-
polyphony (0.
|
32
|
-
qeweney (0.
|
31
|
+
polyphony (0.55.0)
|
32
|
+
qeweney (0.9.1)
|
33
33
|
escape_utils (~> 1.2.1)
|
34
34
|
rack (2.2.3)
|
35
35
|
rake (12.3.3)
|
data/TODO.md
CHANGED
@@ -1,4 +1,80 @@
|
|
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
79
|
|
4
80
|
|
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,6 +6,8 @@ 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')
|
@@ -19,13 +21,46 @@ end
|
|
19
21
|
|
20
22
|
puts "pid: #{Process.pid}"
|
21
23
|
|
22
|
-
|
24
|
+
http_listener = spin do
|
23
25
|
opts = {
|
24
26
|
reuse_addr: true,
|
25
27
|
dont_linger: true,
|
26
28
|
}
|
27
|
-
puts 'Listening on localhost:
|
28
|
-
server = Polyphony::Net.tcp_listen('0.0.0.0',
|
29
|
+
puts 'Listening for HTTP on localhost:10080'
|
30
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)
|
31
|
+
server.accept_loop do |client|
|
32
|
+
spin do
|
33
|
+
service.incr_connection_count
|
34
|
+
Tipi.client_loop(client, opts) { |req| service.http_request(req) }
|
35
|
+
ensure
|
36
|
+
service.decr_connection_count
|
37
|
+
end
|
38
|
+
rescue Exception => e
|
39
|
+
puts "HTTP accept_loop error: #{e.inspect}"
|
40
|
+
puts e.backtrace.join("\n")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
|
45
|
+
|
46
|
+
https_listener = spin do
|
47
|
+
private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
|
48
|
+
c = IO.read('../../reality/ssl/cacert.pem')
|
49
|
+
certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
|
50
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
51
|
+
cert = certificates.shift
|
52
|
+
puts "Certificate expires: #{cert.not_after.inspect}"
|
53
|
+
ctx.add_certificate(cert, private_key, certificates)
|
54
|
+
# ctx = Localhost::Authority.fetch.server_context
|
55
|
+
opts = {
|
56
|
+
reuse_addr: true,
|
57
|
+
dont_linger: true,
|
58
|
+
secure_context: ctx,
|
59
|
+
alpn_protocols: Tipi::ALPN_PROTOCOLS
|
60
|
+
}
|
61
|
+
|
62
|
+
puts 'Listening for HTTPS on localhost:10443'
|
63
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
|
29
64
|
server.accept_loop do |client|
|
30
65
|
spin do
|
31
66
|
service.incr_connection_count
|
@@ -33,6 +68,9 @@ tcp_listener = spin do
|
|
33
68
|
ensure
|
34
69
|
service.decr_connection_count
|
35
70
|
end
|
71
|
+
rescue Exception => e
|
72
|
+
puts "HTTPS accept_loop error: #{e.inspect}"
|
73
|
+
puts e.backtrace.join("\n")
|
36
74
|
end
|
37
75
|
end
|
38
76
|
|
@@ -46,9 +84,13 @@ unix_listener = spin do
|
|
46
84
|
end
|
47
85
|
|
48
86
|
begin
|
49
|
-
Fiber.await(
|
87
|
+
Fiber.await(http_listener, https_listener, unix_listener)
|
50
88
|
rescue Interrupt
|
51
89
|
puts "Got SIGINT, shutting down gracefully"
|
52
90
|
service.graceful_shutdown
|
53
91
|
puts "post graceful shutdown"
|
92
|
+
rescue Exception => e
|
93
|
+
puts '*' * 40
|
94
|
+
p e
|
95
|
+
puts e.backtrace.join("\n")
|
54
96
|
end
|
data/examples/http_server.rb
CHANGED
@@ -13,7 +13,6 @@ puts 'Listening on port 4411...'
|
|
13
13
|
|
14
14
|
spin do
|
15
15
|
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
16
|
-
p path: req.path
|
17
16
|
if req.path == '/stream'
|
18
17
|
req.send_headers('Foo' => 'Bar')
|
19
18
|
sleep 1
|
@@ -24,6 +23,7 @@ spin do
|
|
24
23
|
else
|
25
24
|
req.respond("Hello world!\n")
|
26
25
|
end
|
26
|
+
p req.transfer_counts
|
27
27
|
end
|
28
28
|
p 'done...'
|
29
29
|
end.await
|
@@ -3,9 +3,9 @@
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'tipi'
|
5
5
|
|
6
|
-
$throttler =
|
6
|
+
$throttler = Polyphony::Throttler.new(1000)
|
7
7
|
opts = { reuse_addr: true, dont_linger: true }
|
8
|
-
spin do
|
8
|
+
server = spin do
|
9
9
|
Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
10
10
|
$throttler.call { req.respond("Hello world!\n") }
|
11
11
|
end
|
@@ -13,3 +13,4 @@ end
|
|
13
13
|
|
14
14
|
puts "pid: #{Process.pid}"
|
15
15
|
puts 'Listening on port 1234...'
|
16
|
+
server.await
|
data/examples/https_server.rb
CHANGED
@@ -19,11 +19,10 @@ Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
|
19
19
|
p path: req.path
|
20
20
|
if req.path == '/stream'
|
21
21
|
req.send_headers('Foo' => 'Bar')
|
22
|
-
sleep
|
22
|
+
sleep 0.5
|
23
23
|
req.send_chunk("foo\n")
|
24
|
-
sleep
|
25
|
-
req.send_chunk("bar\n")
|
26
|
-
req.finish
|
24
|
+
sleep 0.5
|
25
|
+
req.send_chunk("bar\n", done: true)
|
27
26
|
else
|
28
27
|
req.respond("Hello world!\n")
|
29
28
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'tipi'
|
5
|
+
require 'tipi/websocket'
|
5
6
|
require 'localhost/authority'
|
6
7
|
|
7
8
|
def ws_handler(conn)
|
@@ -26,7 +27,7 @@ opts = {
|
|
26
27
|
dont_linger: true,
|
27
28
|
secure_context: authority.server_context,
|
28
29
|
upgrade: {
|
29
|
-
websocket:
|
30
|
+
websocket: Tipi::Websocket.handler(&method(:ws_handler))
|
30
31
|
}
|
31
32
|
}
|
32
33
|
|
data/examples/rack_server.rb
CHANGED
@@ -4,6 +4,11 @@ require 'bundler/setup'
|
|
4
4
|
require 'tipi'
|
5
5
|
|
6
6
|
app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
|
7
|
+
unless File.file?(app_path)
|
8
|
+
STDERR.puts "Please provide rack config file (there are some in the examples directory.)"
|
9
|
+
exit!
|
10
|
+
end
|
11
|
+
|
7
12
|
app = Tipi::RackAdapter.load(app_path)
|
8
13
|
opts = { reuse_addr: true, dont_linger: true }
|
9
14
|
|
@@ -5,7 +5,7 @@ require 'tipi'
|
|
5
5
|
require 'localhost/authority'
|
6
6
|
|
7
7
|
app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
|
8
|
-
app =
|
8
|
+
app = Tipi::RackAdapter.load(app_path)
|
9
9
|
|
10
10
|
authority = Localhost::Authority.fetch
|
11
11
|
opts = {
|
@@ -5,15 +5,16 @@ require 'tipi'
|
|
5
5
|
require 'localhost/authority'
|
6
6
|
|
7
7
|
app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
|
8
|
-
app =
|
8
|
+
app = Tipi::RackAdapter.load(app_path)
|
9
9
|
|
10
10
|
authority = Localhost::Authority.fetch
|
11
11
|
opts = {
|
12
12
|
reuse_addr: true,
|
13
|
+
reuse_port: true,
|
13
14
|
dont_linger: true,
|
14
15
|
secure_context: authority.server_context
|
15
16
|
}
|
16
|
-
server =
|
17
|
+
server = Tipi.listen('0.0.0.0', 1234, opts)
|
17
18
|
puts 'Listening on port 1234'
|
18
19
|
|
19
20
|
child_pids = []
|
@@ -24,4 +25,4 @@ child_pids = []
|
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
|
-
child_pids.each { |pid|
|
28
|
+
child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
|
data/examples/routing_server.rb
CHANGED
@@ -12,17 +12,18 @@ puts "pid: #{Process.pid}"
|
|
12
12
|
puts 'Listening on port 4411...'
|
13
13
|
|
14
14
|
app = Tipi.route do |r|
|
15
|
-
|
15
|
+
|
16
|
+
r.on_root do
|
16
17
|
r.redirect '/hello'
|
17
18
|
end
|
18
19
|
r.on 'hello' do
|
19
|
-
r.
|
20
|
+
r.on_get 'world' do
|
20
21
|
r.respond 'Hello world'
|
21
22
|
end
|
22
|
-
r.
|
23
|
+
r.on_get do
|
23
24
|
r.respond 'Hello'
|
24
25
|
end
|
25
|
-
r.
|
26
|
+
r.on_post do
|
26
27
|
puts 'Someone said Hello'
|
27
28
|
r.redirect '/'
|
28
29
|
end
|
@@ -119,7 +119,7 @@ module DigitalFabric
|
|
119
119
|
|
120
120
|
def recv_df_message(msg)
|
121
121
|
@last_recv = Time.now
|
122
|
-
case msg[
|
122
|
+
case msg[Protocol::Attribute::KIND]
|
123
123
|
when Protocol::SHUTDOWN
|
124
124
|
recv_shutdown
|
125
125
|
when Protocol::HTTP_REQUEST
|
@@ -130,7 +130,7 @@ module DigitalFabric
|
|
130
130
|
recv_ws_request(msg)
|
131
131
|
when Protocol::CONN_DATA, Protocol::CONN_CLOSE,
|
132
132
|
Protocol::WS_DATA, Protocol::WS_CLOSE
|
133
|
-
fiber = @requests[msg[
|
133
|
+
fiber = @requests[msg[Protocol::Attribute::ID]]
|
134
134
|
fiber << msg if fiber
|
135
135
|
end
|
136
136
|
end
|
@@ -140,7 +140,7 @@ module DigitalFabric
|
|
140
140
|
# messages. This is so we can correctly stop long-running requests
|
141
141
|
# upon graceful shutdown
|
142
142
|
if is_long_running_request_response?(msg)
|
143
|
-
id = msg[
|
143
|
+
id = msg[Protocol::Attribute::ID]
|
144
144
|
@long_running_requests[id] = @requests[id]
|
145
145
|
end
|
146
146
|
@last_send = Time.now
|
@@ -148,11 +148,11 @@ module DigitalFabric
|
|
148
148
|
end
|
149
149
|
|
150
150
|
def is_long_running_request_response?(msg)
|
151
|
-
case msg[
|
151
|
+
case msg[Protocol::Attribute::KIND]
|
152
152
|
when Protocol::HTTP_UPGRADE
|
153
153
|
true
|
154
154
|
when Protocol::HTTP_RESPONSE
|
155
|
-
|
155
|
+
!msg[Protocol::Attribute::HttpResponse::COMPLETE]
|
156
156
|
end
|
157
157
|
end
|
158
158
|
|
@@ -165,7 +165,7 @@ module DigitalFabric
|
|
165
165
|
|
166
166
|
def recv_http_request(msg)
|
167
167
|
req = prepare_http_request(msg)
|
168
|
-
id = msg[
|
168
|
+
id = msg[Protocol::Attribute::ID]
|
169
169
|
@requests[id] = spin do
|
170
170
|
http_request(req)
|
171
171
|
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
@@ -180,17 +180,20 @@ module DigitalFabric
|
|
180
180
|
end
|
181
181
|
|
182
182
|
def prepare_http_request(msg)
|
183
|
-
|
184
|
-
|
185
|
-
|
183
|
+
headers = msg[Protocol::Attribute::HttpRequest::HEADERS]
|
184
|
+
body_chunk = msg[Protocol::Attribute::HttpRequest::BODY_CHUNK]
|
185
|
+
complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]
|
186
|
+
req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))
|
187
|
+
req.buffer_body_chunk(body_chunk) if body_chunk
|
188
|
+
req.complete! if complete
|
186
189
|
req
|
187
190
|
end
|
188
191
|
|
189
192
|
def recv_http_request_body(msg)
|
190
|
-
fiber = @requests[msg[
|
193
|
+
fiber = @requests[msg[Protocol::Attribute::ID]]
|
191
194
|
return unless fiber
|
192
195
|
|
193
|
-
fiber << msg[
|
196
|
+
fiber << msg[Protocol::Attribute::HttpRequestBody::BODY]
|
194
197
|
end
|
195
198
|
|
196
199
|
def get_http_request_body(id, limit)
|
@@ -199,8 +202,8 @@ module DigitalFabric
|
|
199
202
|
end
|
200
203
|
|
201
204
|
def recv_ws_request(msg)
|
202
|
-
req = Qeweney::Request.new(msg[
|
203
|
-
id = msg[
|
205
|
+
req = Qeweney::Request.new(msg[Protocol::Attribute::WS::HEADERS], RequestAdapter.new(self, msg))
|
206
|
+
id = msg[Protocol::Attribute::ID]
|
204
207
|
@requests[id] = @long_running_requests[id] = spin do
|
205
208
|
ws_request(req)
|
206
209
|
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|