tipi 0.38 → 0.42

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +5 -1
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +58 -16
  7. data/Rakefile +7 -3
  8. data/TODO.md +77 -1
  9. data/benchmarks/bm_http1_parser.rb +61 -0
  10. data/bin/benchmark +37 -0
  11. data/bin/h1pd +6 -0
  12. data/bin/tipi +3 -21
  13. data/df/sample_agent.rb +1 -1
  14. data/df/server.rb +16 -47
  15. data/df/server_utils.rb +178 -0
  16. data/examples/full_service.rb +13 -0
  17. data/examples/http1_parser.rb +55 -0
  18. data/examples/http_server.rb +15 -3
  19. data/examples/http_server_forked.rb +5 -1
  20. data/examples/http_server_routes.rb +29 -0
  21. data/examples/http_server_static.rb +26 -0
  22. data/examples/http_server_throttled.rb +3 -2
  23. data/examples/https_server.rb +6 -4
  24. data/examples/https_wss_server.rb +2 -1
  25. data/examples/rack_server.rb +5 -0
  26. data/examples/rack_server_https.rb +1 -1
  27. data/examples/rack_server_https_forked.rb +4 -3
  28. data/examples/routing_server.rb +5 -4
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +2 -8
  31. data/examples/ws_page.html +2 -2
  32. data/ext/tipi/extconf.rb +13 -0
  33. data/ext/tipi/http1_parser.c +823 -0
  34. data/ext/tipi/http1_parser.h +18 -0
  35. data/ext/tipi/tipi_ext.c +5 -0
  36. data/lib/tipi.rb +89 -1
  37. data/lib/tipi/acme.rb +308 -0
  38. data/lib/tipi/cli.rb +30 -0
  39. data/lib/tipi/digital_fabric/agent.rb +22 -17
  40. data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
  41. data/lib/tipi/digital_fabric/executive.rb +6 -2
  42. data/lib/tipi/digital_fabric/protocol.rb +87 -15
  43. data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
  44. data/lib/tipi/digital_fabric/service.rb +77 -51
  45. data/lib/tipi/http1_adapter.rb +116 -117
  46. data/lib/tipi/http2_adapter.rb +56 -10
  47. data/lib/tipi/http2_stream.rb +106 -53
  48. data/lib/tipi/rack_adapter.rb +2 -53
  49. data/lib/tipi/response_extensions.rb +17 -0
  50. data/lib/tipi/version.rb +1 -1
  51. data/security/http1.rb +12 -0
  52. data/test/helper.rb +60 -11
  53. data/test/test_http1_parser.rb +586 -0
  54. data/test/test_http_server.rb +0 -27
  55. data/test/test_request.rb +1 -28
  56. data/tipi.gemspec +11 -5
  57. metadata +96 -22
  58. data/e +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a8c075ce0b769f014a20587dd061f17e32361f211aad5741f655dafe6687e37
4
- data.tar.gz: d14c0fe026a7db1aa46edded54e5ee6f1a0f8598f626dd1aff342ad07b5cec39
3
+ metadata.gz: 82155f4b86223d3fb32dc8c314abfeb75fda951747e63832ed5c15bc3b1f43cc
4
+ data.tar.gz: 721b98fb8d330d1bc38f8f236f82ddd909235d31f5875c1b4f89895e4e3926da
5
5
  SHA512:
6
- metadata.gz: e306783fc83d9d7ebd0678c4d68a13f6f9bd77e4b9d83fc84d745e5cc3f53eb27b69286fb1ba9f1ca54fe57358009a4c11dc9374c8a8d9046dc4330e359dc988
7
- data.tar.gz: 789c3b4e1374cc57e04e3bce4aa8d12022cfd7e31c9cba4f5777302f57943da0a967f17f8cb2f91551357deac1bbd07fe514c2cb2e002331f525d341c6b7f0e9
6
+ metadata.gz: 10389779975234d90c968626b4f9d6b168491f9a32ecf1be71683c37cf11f58f1bac52a36478691e72328236a16d309a8462104bda556772e25cebe52b2b0d53
7
+ data.tar.gz: 99db2c15278cdfc286ffad05f266f5d62eec6a3bb1363f8dfa4c302549a308a691496ee60ec085ede59e3b594533e8e196bbca144878e5a6dccf2ecfc962151c
@@ -22,6 +22,10 @@ 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
+ - name: Show Linux kernel version
27
+ run: uname -r
28
+ - name: Compile C-extension
29
+ run: bundle exec rake compile
26
30
  - name: Run tests
27
31
  run: bundle exec rake test
data/.gitignore CHANGED
@@ -54,3 +54,8 @@ build-iPhoneSimulator/
54
54
 
55
55
  # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
56
  # .rubocop-https?--*
57
+ log
58
+ log.*
59
+
60
+ lib/tipi_ext*
61
+ examples/certificate_store.db
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## 0.42 2021-08-16
2
+
3
+ - HTTP/1 parser: disable UTF-8 parsing for all but header values
4
+ - Add support for parsing HTTP/1 from callable source
5
+ - Introduce full_service API for automatic HTTPS
6
+ - Introduce automatic SSL certificate provisioning
7
+ - Improve handling of exceptions
8
+ - Various fixes to DF service and agent pxoy
9
+ - Fix upgrading to HTTP2 with a request body
10
+ - Switch to new HTTP/1 parser
11
+
12
+ ## 0.41 2021-07-26
13
+
14
+ - Fix Rack adapter (#11)
15
+ - Introduce experimental HTTP/1 parser
16
+ - More work on DF server
17
+ - Allow setting chunk size in `#respond_from_io`
18
+
19
+ ## 0.40 2021-06-24
20
+
21
+ - Implement serving static files using splice_chunks (nice performance boost for
22
+ files bigger than 1M)
23
+ - Call shutdown before closing socket
24
+ - Fix examples (thanks @timhatch!)
25
+
26
+ ## 0.39 2021-06-20
27
+
28
+ - More work on DF server
29
+ - Fix HTTP2StreamHandler#send_headers
30
+ - Various fixes to HTTP/2 adapter
31
+ - Fix host detection for HTTP/2 connections
32
+ - Fix HTTP/1 adapter #respond with nil body
33
+ - Fix HTTP1Adapter#send_headers
34
+
1
35
  ## 0.38 2021-03-09
2
36
 
3
37
  - Don't use chunked transfer encoding for non-streaming responses
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec
3
+ gemspec
4
+ %w{polyphony qeweney}.each do |dep|
5
+ dir = "../#{dep}"
6
+ gem(dep, path: dir) if File.directory?(dir)
7
+ end
data/Gemfile.lock CHANGED
@@ -1,39 +1,77 @@
1
+ PATH
2
+ remote: ../polyphony
3
+ specs:
4
+ polyphony (0.69)
5
+
6
+ PATH
7
+ remote: ../qeweney
8
+ specs:
9
+ qeweney (0.14)
10
+ escape_utils (~> 1.2.1)
11
+
1
12
  PATH
2
13
  remote: .
3
14
  specs:
4
- tipi (0.38)
5
- http-2 (~> 0.10.0)
6
- http_parser.rb (~> 0.6.0)
15
+ tipi (0.42)
16
+ acme-client (~> 2.0.8)
17
+ extralite (~> 1.2)
18
+ http-2 (~> 0.11)
19
+ localhost (~> 1.1.4)
7
20
  msgpack (~> 1.4.2)
8
- polyphony (~> 0.52.0)
9
- qeweney (~> 0.6)
21
+ polyphony (~> 0.69)
22
+ qeweney (~> 0.14)
10
23
  rack (>= 2.0.8, < 2.3.0)
11
24
  websocket (~> 1.2.8)
12
25
 
13
26
  GEM
14
27
  remote: https://rubygems.org/
15
28
  specs:
29
+ acme-client (2.0.8)
30
+ faraday (>= 0.17, < 2.0.0)
16
31
  ansi (1.5.0)
17
32
  builder (3.2.4)
18
- docile (1.3.2)
33
+ cuba (3.9.3)
34
+ rack (>= 1.6.0)
35
+ docile (1.4.0)
19
36
  escape_utils (1.2.1)
20
- http-2 (0.10.2)
21
- http_parser.rb (0.6.0)
22
- json (2.3.1)
23
- localhost (1.1.4)
37
+ extralite (1.2)
38
+ faraday (1.7.0)
39
+ faraday-em_http (~> 1.0)
40
+ faraday-em_synchrony (~> 1.0)
41
+ faraday-excon (~> 1.1)
42
+ faraday-httpclient (~> 1.0.1)
43
+ faraday-net_http (~> 1.0)
44
+ faraday-net_http_persistent (~> 1.1)
45
+ faraday-patron (~> 1.0)
46
+ faraday-rack (~> 1.0)
47
+ multipart-post (>= 1.2, < 3)
48
+ ruby2_keywords (>= 0.0.4)
49
+ faraday-em_http (1.0.0)
50
+ faraday-em_synchrony (1.0.0)
51
+ faraday-excon (1.1.0)
52
+ faraday-httpclient (1.0.1)
53
+ faraday-net_http (1.0.1)
54
+ faraday-net_http_persistent (1.2.0)
55
+ faraday-patron (1.0.0)
56
+ faraday-rack (1.0.0)
57
+ http-2 (0.11.0)
58
+ http_parser.rb (0.7.0)
59
+ json (2.5.1)
60
+ localhost (1.1.8)
24
61
  minitest (5.11.3)
25
- minitest-reporters (1.4.2)
62
+ minitest-reporters (1.4.3)
26
63
  ansi
27
64
  builder
28
65
  minitest (>= 5.0)
29
66
  ruby-progressbar
30
67
  msgpack (1.4.2)
31
- polyphony (0.52.0)
32
- qeweney (0.7.5)
33
- escape_utils (~> 1.2.1)
68
+ multipart-post (2.1.1)
34
69
  rack (2.2.3)
35
70
  rake (12.3.3)
36
- ruby-progressbar (1.10.1)
71
+ rake-compiler (1.1.1)
72
+ rake
73
+ ruby-progressbar (1.11.0)
74
+ ruby2_keywords (0.0.5)
37
75
  simplecov (0.17.1)
38
76
  docile (~> 1.1)
39
77
  json (>= 1.8, < 3)
@@ -45,10 +83,14 @@ PLATFORMS
45
83
  ruby
46
84
 
47
85
  DEPENDENCIES
48
- localhost (~> 1.1.4)
86
+ cuba (~> 3.9.3)
87
+ http_parser.rb (= 0.7.0)
49
88
  minitest (~> 5.11.3)
50
89
  minitest-reporters (~> 1.4.2)
90
+ polyphony!
91
+ qeweney!
51
92
  rake (~> 12.3.3)
93
+ rake-compiler (= 1.1.1)
52
94
  simplecov (~> 0.17.1)
53
95
  tipi!
54
96
 
data/Rakefile CHANGED
@@ -3,10 +3,14 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/clean"
5
5
 
6
- # frozen_string_literal: true
6
+ require "rake/extensiontask"
7
+ Rake::ExtensionTask.new("tipi_ext") do |ext|
8
+ ext.ext_dir = "ext/tipi"
9
+ end
10
+
11
+ task :recompile => [:clean, :compile]
12
+ task :default => [:compile, :test]
7
13
 
8
- task :default => [:test]
9
14
  task :test do
10
15
  exec 'ruby test/run.rb'
11
16
  end
12
-
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
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ HTTP_REQUEST = "GET /foo HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"
6
+
7
+ def benchmark_other_http1_parser(iterations)
8
+ STDOUT << "http_parser.rb: "
9
+ require 'http_parser.rb'
10
+
11
+ i, o = IO.pipe
12
+ parser = Http::Parser.new
13
+ done = false
14
+ headers = nil
15
+ parser.on_headers_complete = proc do |h|
16
+ headers = h
17
+ headers[':method'] = parser.http_method
18
+ headers[':path'] = parser.request_url
19
+ headers[':protocol'] = parser.http_version
20
+ end
21
+ parser.on_message_complete = proc { done = true }
22
+
23
+ t0 = Time.now
24
+ iterations.times do
25
+ o << HTTP_REQUEST
26
+ done = false
27
+ while !done
28
+ msg = i.readpartial(4096)
29
+ parser << msg
30
+ end
31
+ end
32
+ t1 = Time.now
33
+ puts "#{iterations / (t1 - t0)} ips"
34
+ end
35
+
36
+ def benchmark_tipi_http1_parser(iterations)
37
+ STDOUT << "tipi parser: "
38
+ require_relative '../lib/tipi_ext'
39
+ i, o = IO.pipe
40
+ reader = proc { |len| i.readpartial(len) }
41
+ parser = Tipi::HTTP1Parser.new(reader)
42
+
43
+ t0 = Time.now
44
+ iterations.times do
45
+ o << HTTP_REQUEST
46
+ headers = parser.parse_headers
47
+ end
48
+ t1 = Time.now
49
+ puts "#{iterations / (t1 - t0)} ips"
50
+ end
51
+
52
+ def fork_benchmark(method, iterations)
53
+ pid = fork { send(method, iterations) }
54
+ Process.wait(pid)
55
+ end
56
+
57
+ x = 500000
58
+ fork_benchmark(:benchmark_other_http1_parser, x)
59
+ fork_benchmark(:benchmark_tipi_http1_parser, x)
60
+
61
+ # benchmark_tipi_http1_parser(x)
data/bin/benchmark ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ def parse_latency(latency)
7
+ m = latency.match(/^([\d\.]+)(us|ms|s)$/)
8
+ return nil unless m
9
+
10
+ value = m[1].to_f
11
+ case m[2]
12
+ when 's' then value
13
+ when 'ms' then value / 1000
14
+ when 'us' then value / 1000000
15
+ end
16
+ end
17
+
18
+ def parse_wrk_results(results)
19
+ lines = results.lines
20
+ latencies = lines[3].strip.split(/\s+/)
21
+ throughput = lines[6].strip.split(/\s+/)
22
+
23
+ {
24
+ latency_avg: parse_latency(latencies[1]),
25
+ latency_max: parse_latency(latencies[3]),
26
+ rate: throughput[1].to_f
27
+ }
28
+ end
29
+
30
+ def run_wrk(duration: 10, threads: 2, connections: 10, url: )
31
+ `wrk -d#{duration} -t#{threads} -c#{connections} #{url}`
32
+ end
33
+
34
+ [8, 64, 256, 512].each do |c|
35
+ puts "connections: #{c}"
36
+ p parse_wrk_results(run_wrk(duration: 10, threads: 4, connections: c, url: "http://localhost:10080/"))
37
+ end
data/bin/h1pd ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+ rake compile
5
+ ruby test/test_http1_parser.rb
6
+ ruby benchmarks/bm_http1_parser.rb
data/bin/tipi CHANGED
@@ -1,26 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'polyphony'
5
- require File.expand_path('../lib/tipi/configuration', __dir__)
4
+ require 'tipi/cli'
6
5
 
7
- config = {}
8
- #config[:forked] = 4
6
+ trap('SIGINT') { exit }
9
7
 
10
- puts DATA.read
11
- puts
12
-
13
- configuration_manager = spin { Tipi::Configuration.supervise_config }
14
-
15
- configuration_manager << config
16
- configuration_manager.await
17
-
18
- __END__
19
-
20
- ooo
21
- oo
22
- o
23
- \|/
24
- / \ Tipi - A better web server for a better world
25
- /___\
26
-
8
+ Tipi::CLI.start
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
@@ -1,54 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/setup'
4
- require 'tipi'
5
- require 'tipi/digital_fabric'
6
- require 'tipi/digital_fabric/executive'
7
- require 'json'
8
- require 'fileutils'
9
- FileUtils.cd(__dir__)
3
+ require_relative 'server_utils'
10
4
 
11
- service = DigitalFabric::Service.new(token: 'foobar')
12
- executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
13
-
14
- spin_loop(interval: 60) { GC.start }
15
-
16
- class Polyphony::BaseException
17
- attr_reader :caller_backtrace
18
- end
19
-
20
- puts "pid: #{Process.pid}"
21
-
22
- tcp_listener = spin do
23
- opts = {
24
- reuse_addr: true,
25
- dont_linger: true,
26
- }
27
- puts 'Listening on localhost:4411'
28
- server = Polyphony::Net.tcp_listen('0.0.0.0', 4411, opts)
29
- server.accept_loop do |client|
30
- spin do
31
- service.incr_connection_count
32
- Tipi.client_loop(client, opts) { |req| service.http_request(req) }
33
- ensure
34
- service.decr_connection_count
35
- end
36
- end
37
- end
38
-
39
- UNIX_SOCKET_PATH = '/tmp/df.sock'
40
-
41
- unix_listener = spin do
42
- puts "Listening on #{UNIX_SOCKET_PATH}"
43
- FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
44
- socket = UNIXServer.new(UNIX_SOCKET_PATH)
45
- Tipi.accept_loop(socket, {}) { |req| service.http_request(req) }
46
- end
5
+ listeners = [
6
+ listen_http,
7
+ listen_https,
8
+ listen_unix
9
+ ]
47
10
 
48
11
  begin
49
- Fiber.await(tcp_listener, unix_listener)
12
+ log('Starting DF server')
13
+ Fiber.await(*listeners)
50
14
  rescue Interrupt
51
- puts "Got SIGINT, shutting down gracefully"
52
- service.graceful_shutdown
53
- puts "post graceful shutdown"
15
+ log('Got SIGINT, shutting down gracefully')
16
+ @service.graceful_shutdown
17
+ rescue SystemExit
18
+ # ignore
19
+ rescue Exception => e
20
+ log("Uncaught exception", error: e, source: e.source_fiber, raising: e.raising_fiber, backtrace: e.backtrace)
21
+ ensure
22
+ log('DF server stopped')
54
23
  end