tipi 0.38 → 0.42

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.
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