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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a8c075ce0b769f014a20587dd061f17e32361f211aad5741f655dafe6687e37
4
- data.tar.gz: d14c0fe026a7db1aa46edded54e5ee6f1a0f8598f626dd1aff342ad07b5cec39
3
+ metadata.gz: 02c5ce4798e1961ca59798b75bafe158570e777661f6fc8667169553df3cd3f4
4
+ data.tar.gz: 405bfa3526efaad394d34840764dc131230a366574cfc52b63344a6d5cdfe6dd
5
5
  SHA512:
6
- metadata.gz: e306783fc83d9d7ebd0678c4d68a13f6f9bd77e4b9d83fc84d745e5cc3f53eb27b69286fb1ba9f1ca54fe57358009a4c11dc9374c8a8d9046dc4330e359dc988
7
- data.tar.gz: 789c3b4e1374cc57e04e3bce4aa8d12022cfd7e31c9cba4f5777302f57943da0a967f17f8cb2f91551357deac1bbd07fe514c2cb2e002331f525d341c6b7f0e9
6
+ metadata.gz: 754bd35136e4e004e6bd782eb2060d933b2439c437a9c2a966529e0465c42096b97fa08b1dae04b7a3c8aa0c77c367fd5395d215cca73fd7cf1aa80c297b3178
7
+ data.tar.gz: 24425d798c076ad43b4fa45b641a90a5e801dfe35acd6217e8a8fc97ea546f18457fc9a8653e47f7bd5f696da885ef86db8db36385dcaa79ccbcf9d42d64081c
@@ -22,6 +22,6 @@ jobs:
22
22
  - name: Install dependencies
23
23
  run: |
24
24
  gem install bundler
25
- bundle install
25
+ POLYPHONY_USE_LIBEV=1 bundle install
26
26
  - name: Run tests
27
27
  run: bundle exec rake test
data/.gitignore CHANGED
@@ -54,3 +54,4 @@ build-iPhoneSimulator/
54
54
 
55
55
  # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
56
  # .rubocop-https?--*
57
+ log
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.38)
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.52.0)
9
- qeweney (~> 0.6)
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.52.0)
32
- qeweney (0.7.5)
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
- For immediate execution:
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['headers'][':path']
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
- tcp_listener = spin do
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:4411'
28
- server = Polyphony::Net.tcp_listen('0.0.0.0', 4411, opts)
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(tcp_listener, unix_listener)
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
@@ -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
@@ -25,4 +25,6 @@ end
25
25
 
26
26
  puts 'Listening on port 1234'
27
27
 
28
+ trap('SIGINT') { exit! }
29
+
28
30
  child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -3,9 +3,9 @@
3
3
  require 'bundler/setup'
4
4
  require 'tipi'
5
5
 
6
- $throttler = throttle(1000)
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
@@ -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 1
22
+ sleep 0.5
23
23
  req.send_chunk("foo\n")
24
- sleep 1
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: Polyphony::Websocket.handler(&method(:ws_handler))
30
+ websocket: Tipi::Websocket.handler(&method(:ws_handler))
30
31
  }
31
32
  }
32
33
 
@@ -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 = Polyphony::HTTP::Server::RackAdapter.load(app_path)
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 = Polyphony::HTTP::Server::RackAdapter.load(app_path)
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 = Polyphony::HTTP::Server.listen('0.0.0.0', 1234, opts)
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| EV::Child.new(pid).await }
28
+ child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -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
- r.root do
15
+
16
+ r.on_root do
16
17
  r.redirect '/hello'
17
18
  end
18
19
  r.on 'hello' do
19
- r.get 'world' do
20
+ r.on_get 'world' do
20
21
  r.respond 'Hello world'
21
22
  end
22
- r.get do
23
+ r.on_get do
23
24
  r.respond 'Hello'
24
25
  end
25
- r.post do
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['kind']
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['id']]
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[:id]
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[:kind]
151
+ case msg[Protocol::Attribute::KIND]
152
152
  when Protocol::HTTP_UPGRADE
153
153
  true
154
154
  when Protocol::HTTP_RESPONSE
155
- msg[:body] && !msg[:complete]
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['id']
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
- req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
184
- req.buffer_body_chunk(msg['body']) if msg['body']
185
- req.complete! if msg['complete']
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['id']]
193
+ fiber = @requests[msg[Protocol::Attribute::ID]]
191
194
  return unless fiber
192
195
 
193
- fiber << msg['body']
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['headers'], RequestAdapter.new(self, msg))
203
- id = msg['id']
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