tipi 0.39 → 0.43

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/.gitignore +5 -1
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +62 -25
  7. data/Rakefile +7 -3
  8. data/benchmarks/bm_http1_parser.rb +85 -0
  9. data/bin/benchmark +37 -0
  10. data/bin/h1pd +6 -0
  11. data/bin/tipi +3 -21
  12. data/df/server.rb +16 -87
  13. data/df/server_utils.rb +175 -0
  14. data/examples/full_service.rb +13 -0
  15. data/examples/http1_parser.rb +55 -0
  16. data/examples/http_server.rb +15 -3
  17. data/examples/http_server_forked.rb +3 -1
  18. data/examples/http_server_routes.rb +29 -0
  19. data/examples/http_server_static.rb +26 -0
  20. data/examples/https_server.rb +3 -0
  21. data/examples/servername_cb.rb +37 -0
  22. data/examples/websocket_demo.rb +2 -8
  23. data/examples/ws_page.html +2 -2
  24. data/lib/tipi.rb +89 -1
  25. data/lib/tipi/acme.rb +308 -0
  26. data/lib/tipi/cli.rb +30 -0
  27. data/lib/tipi/digital_fabric/agent.rb +7 -5
  28. data/lib/tipi/digital_fabric/agent_proxy.rb +16 -8
  29. data/lib/tipi/digital_fabric/executive.rb +6 -2
  30. data/lib/tipi/digital_fabric/protocol.rb +18 -3
  31. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  32. data/lib/tipi/digital_fabric/service.rb +77 -49
  33. data/lib/tipi/http1_adapter.rb +91 -100
  34. data/lib/tipi/http2_adapter.rb +21 -6
  35. data/lib/tipi/http2_stream.rb +54 -44
  36. data/lib/tipi/rack_adapter.rb +2 -53
  37. data/lib/tipi/response_extensions.rb +17 -0
  38. data/lib/tipi/version.rb +1 -1
  39. data/test/helper.rb +60 -12
  40. data/test/test_http_server.rb +0 -27
  41. data/test/test_request.rb +2 -29
  42. data/tipi.gemspec +11 -7
  43. metadata +79 -26
  44. data/e +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02c5ce4798e1961ca59798b75bafe158570e777661f6fc8667169553df3cd3f4
4
- data.tar.gz: 405bfa3526efaad394d34840764dc131230a366574cfc52b63344a6d5cdfe6dd
3
+ metadata.gz: c7a42c93f436a24b74aef2fc9de54d7f01b587ec42b2897d0b0c24a9090776d8
4
+ data.tar.gz: 1fa532504e9c79d620306b4ddfbd01bf1d1dc39b563ec2a2c3cfec9cb00a1b64
5
5
  SHA512:
6
- metadata.gz: 754bd35136e4e004e6bd782eb2060d933b2439c437a9c2a966529e0465c42096b97fa08b1dae04b7a3c8aa0c77c367fd5395d215cca73fd7cf1aa80c297b3178
7
- data.tar.gz: 24425d798c076ad43b4fa45b641a90a5e801dfe35acd6217e8a8fc97ea546f18457fc9a8653e47f7bd5f696da885ef86db8db36385dcaa79ccbcf9d42d64081c
6
+ metadata.gz: 93282cde73eca46289cf212e76c90e7390dcd9e48e8594bd756d5f59ae3bc5fed41e61d216c85cca9c9d2c8fca1061ee0159f8cc0ca575fe0b7149a21b1edc24
7
+ data.tar.gz: 6fd1dc4de05351d0b7da91a7344798145f31659f4663f451355222ce63bb8504fb123c562199820b4f217bc14d064bb529fcc0c9ea495863856bd79f073a491b
@@ -23,5 +23,9 @@ jobs:
23
23
  run: |
24
24
  gem install bundler
25
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,4 +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
57
+ log
58
+ log.*
59
+
60
+ lib/tipi_ext*
61
+ examples/certificate_store.db
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.43 2021-08-20
2
+
3
+ - Extract HTTP/1 parser into a separate gem:
4
+ [H1P](https://github.com/digital-fabric/h1p)
5
+
6
+ ## 0.42 2021-08-16
7
+
8
+ - HTTP/1 parser: disable UTF-8 parsing for all but header values
9
+ - Add support for parsing HTTP/1 from callable source
10
+ - Introduce full_service API for automatic HTTPS
11
+ - Introduce automatic SSL certificate provisioning
12
+ - Improve handling of exceptions
13
+ - Various fixes to DF service and agent pxoy
14
+ - Fix upgrading to HTTP2 with a request body
15
+ - Switch to new HTTP/1 parser
16
+
17
+ ## 0.41 2021-07-26
18
+
19
+ - Fix Rack adapter (#11)
20
+ - Introduce experimental HTTP/1 parser
21
+ - More work on DF server
22
+ - Allow setting chunk size in `#respond_from_io`
23
+
24
+ ## 0.40 2021-06-24
25
+
26
+ - Implement serving static files using splice_chunks (nice performance boost for
27
+ files bigger than 1M)
28
+ - Call shutdown before closing socket
29
+ - Fix examples (thanks @timhatch!)
30
+
1
31
  ## 0.39 2021-06-20
2
32
 
3
33
  - More work on DF server
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec
3
+ gemspec
4
+ %w{polyphony qeweney h1p}.each do |dep|
5
+ dir = "../#{dep}"
6
+ gem(dep, path: dir) if File.directory?(dir)
7
+ end
data/Gemfile.lock CHANGED
@@ -1,39 +1,73 @@
1
+ PATH
2
+ remote: ../h1p
3
+ specs:
4
+ h1p (0.2)
5
+
6
+ PATH
7
+ remote: ../polyphony
8
+ specs:
9
+ polyphony (0.70)
10
+
11
+ PATH
12
+ remote: ../qeweney
13
+ specs:
14
+ qeweney (0.14)
15
+ escape_utils (~> 1.2.1)
16
+
1
17
  PATH
2
18
  remote: .
3
19
  specs:
4
- tipi (0.39)
5
- http-2 (~> 0.10.0)
6
- http_parser.rb (~> 0.6.0)
20
+ tipi (0.43)
21
+ acme-client (~> 2.0.8)
22
+ extralite (~> 1.2)
23
+ h1p (~> 0.2)
24
+ http-2 (~> 0.11)
25
+ localhost (~> 1.1.4)
7
26
  msgpack (~> 1.4.2)
8
- polyphony (~> 0.55.0)
9
- qeweney (~> 0.9.1)
27
+ polyphony (~> 0.69)
28
+ qeweney (~> 0.14)
10
29
  rack (>= 2.0.8, < 2.3.0)
11
30
  websocket (~> 1.2.8)
12
31
 
13
32
  GEM
14
33
  remote: https://rubygems.org/
15
34
  specs:
16
- ansi (1.5.0)
17
- builder (3.2.4)
18
- docile (1.3.2)
35
+ acme-client (2.0.8)
36
+ faraday (>= 0.17, < 2.0.0)
37
+ cuba (3.9.3)
38
+ rack (>= 1.6.0)
39
+ docile (1.4.0)
19
40
  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)
41
+ extralite (1.3)
42
+ faraday (1.7.0)
43
+ faraday-em_http (~> 1.0)
44
+ faraday-em_synchrony (~> 1.0)
45
+ faraday-excon (~> 1.1)
46
+ faraday-httpclient (~> 1.0.1)
47
+ faraday-net_http (~> 1.0)
48
+ faraday-net_http_persistent (~> 1.1)
49
+ faraday-patron (~> 1.0)
50
+ faraday-rack (~> 1.0)
51
+ multipart-post (>= 1.2, < 3)
52
+ ruby2_keywords (>= 0.0.4)
53
+ faraday-em_http (1.0.0)
54
+ faraday-em_synchrony (1.0.0)
55
+ faraday-excon (1.1.0)
56
+ faraday-httpclient (1.0.1)
57
+ faraday-net_http (1.0.1)
58
+ faraday-net_http_persistent (1.2.0)
59
+ faraday-patron (1.0.0)
60
+ faraday-rack (1.0.0)
61
+ http-2 (0.11.0)
62
+ json (2.5.1)
63
+ localhost (1.1.8)
64
+ memory_profiler (1.0.0)
24
65
  minitest (5.11.3)
25
- minitest-reporters (1.4.2)
26
- ansi
27
- builder
28
- minitest (>= 5.0)
29
- ruby-progressbar
30
66
  msgpack (1.4.2)
31
- polyphony (0.55.0)
32
- qeweney (0.9.1)
33
- escape_utils (~> 1.2.1)
67
+ multipart-post (2.1.1)
34
68
  rack (2.2.3)
35
- rake (12.3.3)
36
- ruby-progressbar (1.10.1)
69
+ rake (13.0.6)
70
+ ruby2_keywords (0.0.5)
37
71
  simplecov (0.17.1)
38
72
  docile (~> 1.1)
39
73
  json (>= 1.8, < 3)
@@ -45,10 +79,13 @@ PLATFORMS
45
79
  ruby
46
80
 
47
81
  DEPENDENCIES
48
- localhost (~> 1.1.4)
82
+ cuba (~> 3.9.3)
83
+ h1p!
84
+ memory_profiler (~> 1.0.0)
49
85
  minitest (~> 5.11.3)
50
- minitest-reporters (~> 1.4.2)
51
- rake (~> 12.3.3)
86
+ polyphony!
87
+ qeweney!
88
+ rake (~> 13.0.6)
52
89
  simplecov (~> 0.17.1)
53
90
  tipi!
54
91
 
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
-
@@ -0,0 +1,85 @@
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 measure_time_and_allocs
8
+ 4.times { GC.start }
9
+ GC.disable
10
+
11
+ t0 = Time.now
12
+ a0 = object_count
13
+ yield
14
+ t1 = Time.now
15
+ a1 = object_count
16
+ [t1 - t0, a1 - a0]
17
+ ensure
18
+ GC.enable
19
+ end
20
+
21
+ def object_count
22
+ count = ObjectSpace.count_objects
23
+ count[:TOTAL] - count[:FREE]
24
+ end
25
+
26
+ def benchmark_other_http1_parser(iterations)
27
+ STDOUT << "http_parser.rb: "
28
+ require 'http_parser.rb'
29
+
30
+ i, o = IO.pipe
31
+ parser = Http::Parser.new
32
+ done = false
33
+ headers = nil
34
+ parser.on_headers_complete = proc do |h|
35
+ headers = h
36
+ headers[':method'] = parser.http_method
37
+ headers[':path'] = parser.request_url
38
+ end
39
+ parser.on_message_complete = proc { done = true }
40
+
41
+ elapsed, allocated = measure_time_and_allocs do
42
+ iterations.times do
43
+ o << HTTP_REQUEST
44
+ done = false
45
+ while !done
46
+ msg = i.readpartial(4096)
47
+ parser << msg
48
+ end
49
+ end
50
+ end
51
+ puts(format('elapsed: %f, allocated: %d (%f/req), rate: %f ips', elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
52
+ end
53
+
54
+ def benchmark_tipi_http1_parser(iterations)
55
+ STDOUT << "tipi parser: "
56
+ require_relative '../lib/tipi_ext'
57
+ i, o = IO.pipe
58
+ reader = proc { |len| i.readpartial(len) }
59
+ parser = Tipi::HTTP1Parser.new(reader)
60
+
61
+ elapsed, allocated = measure_time_and_allocs do
62
+ iterations.times do
63
+ o << HTTP_REQUEST
64
+ headers = parser.parse_headers
65
+ end
66
+ end
67
+ puts(format('elapsed: %f, allocated: %d (%f/req), rate: %f ips', elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
68
+ end
69
+
70
+ def fork_benchmark(method, iterations)
71
+ pid = fork do
72
+ send(method, iterations)
73
+ rescue Exception => e
74
+ p e
75
+ p e.backtrace
76
+ exit!
77
+ end
78
+ Process.wait(pid)
79
+ end
80
+
81
+ x = 500000
82
+ # fork_benchmark(:benchmark_other_http1_parser, x)
83
+ # fork_benchmark(:benchmark_tipi_http1_parser, x)
84
+
85
+ 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/server.rb CHANGED
@@ -1,96 +1,25 @@
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
- require 'localhost/authority'
3
+ require_relative 'server_utils'
10
4
 
11
- FileUtils.cd(__dir__)
5
+ listeners = [
6
+ listen_http,
7
+ listen_https,
8
+ listen_unix
9
+ ]
12
10
 
13
- service = DigitalFabric::Service.new(token: 'foobar')
14
- executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
15
-
16
- spin_loop(interval: 60) { GC.start }
17
-
18
- class Polyphony::BaseException
19
- attr_reader :caller_backtrace
20
- end
21
-
22
- puts "pid: #{Process.pid}"
23
-
24
- http_listener = spin do
25
- opts = {
26
- reuse_addr: true,
27
- dont_linger: true,
28
- }
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)
64
- server.accept_loop do |client|
65
- spin do
66
- service.incr_connection_count
67
- Tipi.client_loop(client, opts) { |req| service.http_request(req) }
68
- ensure
69
- service.decr_connection_count
70
- end
71
- rescue Exception => e
72
- puts "HTTPS accept_loop error: #{e.inspect}"
73
- puts e.backtrace.join("\n")
74
- end
75
- end
76
-
77
- UNIX_SOCKET_PATH = '/tmp/df.sock'
78
-
79
- unix_listener = spin do
80
- puts "Listening on #{UNIX_SOCKET_PATH}"
81
- FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
82
- socket = UNIXServer.new(UNIX_SOCKET_PATH)
83
- Tipi.accept_loop(socket, {}) { |req| service.http_request(req) }
84
- end
11
+ spin_loop(interval: 60) { GC.compact } if GC.respond_to?(:compact)
85
12
 
86
13
  begin
87
- Fiber.await(http_listener, https_listener, unix_listener)
14
+ log('Starting DF server')
15
+ Fiber.await(*listeners)
88
16
  rescue Interrupt
89
- puts "Got SIGINT, shutting down gracefully"
90
- service.graceful_shutdown
91
- puts "post graceful shutdown"
17
+ log('Got SIGINT, shutting down gracefully')
18
+ @service.graceful_shutdown
19
+ rescue SystemExit
20
+ # ignore
92
21
  rescue Exception => e
93
- puts '*' * 40
94
- p e
95
- puts e.backtrace.join("\n")
22
+ log("Uncaught exception", error: e, source: e.source_fiber, raising: e.raising_fiber, backtrace: e.backtrace)
23
+ ensure
24
+ log('DF server stopped')
96
25
  end