tipi 0.38 → 0.39
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 +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
|