tipi 0.38 → 0.39

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