tipi 0.39 → 0.43

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