tipi 0.43 → 0.45

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +1 -3
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +14 -7
  7. data/README.md +184 -8
  8. data/Rakefile +1 -7
  9. data/benchmarks/bm_http1_parser.rb +1 -1
  10. data/bin/benchmark +0 -0
  11. data/bin/h1pd +0 -0
  12. data/bm.png +0 -0
  13. data/df/agent.rb +1 -1
  14. data/df/sample_agent.rb +2 -2
  15. data/df/server_utils.rb +1 -1
  16. data/examples/hello.rb +5 -0
  17. data/examples/http_server.js +1 -1
  18. data/examples/http_server_graceful.rb +1 -1
  19. data/examples/https_server.rb +41 -18
  20. data/examples/rack_server_forked.rb +26 -0
  21. data/examples/rack_server_https_forked.rb +1 -1
  22. data/examples/websocket_demo.rb +1 -1
  23. data/lib/tipi/acme.rb +46 -39
  24. data/lib/tipi/cli.rb +79 -16
  25. data/lib/tipi/config_dsl.rb +13 -13
  26. data/lib/tipi/configuration.rb +2 -2
  27. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  28. data/lib/tipi/controller/bare_stock.rb +10 -0
  29. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  30. data/lib/tipi/controller/web_polyphony.rb +351 -0
  31. data/lib/tipi/controller/web_stock.rb +631 -0
  32. data/lib/tipi/controller.rb +12 -0
  33. data/lib/tipi/digital_fabric/agent.rb +3 -3
  34. data/lib/tipi/digital_fabric/agent_proxy.rb +11 -5
  35. data/lib/tipi/digital_fabric/executive.rb +1 -1
  36. data/lib/tipi/digital_fabric/protocol.rb +1 -1
  37. data/lib/tipi/digital_fabric/service.rb +8 -8
  38. data/lib/tipi/handler.rb +2 -2
  39. data/lib/tipi/http1_adapter.rb +32 -27
  40. data/lib/tipi/http2_adapter.rb +10 -10
  41. data/lib/tipi/http2_stream.rb +14 -14
  42. data/lib/tipi/rack_adapter.rb +2 -2
  43. data/lib/tipi/response_extensions.rb +1 -1
  44. data/lib/tipi/supervisor.rb +75 -0
  45. data/lib/tipi/version.rb +1 -1
  46. data/lib/tipi/websocket.rb +3 -3
  47. data/lib/tipi.rb +4 -83
  48. data/test/coverage.rb +2 -2
  49. data/test/test_http_server.rb +14 -14
  50. data/tipi.gemspec +3 -2
  51. metadata +30 -5
data/lib/tipi/acme.rb CHANGED
@@ -35,32 +35,41 @@ module Tipi
35
35
  IP_REGEXP = /^\d+\.\d+\.\d+\.\d+$/
36
36
 
37
37
  def setup_sni_callback
38
- @master_ctx.servername_cb = proc do |_socket, name|
39
- p servername_cb: name
40
- state = { ctx: nil }
41
-
42
- if name =~ IP_REGEXP
43
- @master_ctx
44
- else
45
- @requests << [name, state]
46
- wait_for_ctx(state)
47
- p name: name, error: state if state[:error]
48
- # Eventually we might want to return an error returned in
49
- # state[:error]. For the time being we handle errors by returning the
50
- # master context
51
- state[:ctx] || @master_ctx
52
- end
53
- end
38
+ @master_ctx.servername_cb = proc { |_socket, name| get_ctx(name) }
54
39
  end
55
-
40
+
41
+ def get_ctx(name)
42
+ state = { ctx: nil }
43
+
44
+ ready_ctx = @contexts[name]
45
+ return ready_ctx if ready_ctx
46
+ return @master_ctx if name =~ IP_REGEXP
47
+
48
+ @requests << [name, state]
49
+ wait_for_ctx(state)
50
+ # Eventually we might want to return an error returned in
51
+ # state[:error]. For the time being we handle errors by returning the
52
+ # master context
53
+ state[:ctx] || @master_ctx
54
+ rescue => e
55
+ @master_ctx
56
+ end
57
+
58
+ MAX_WAIT_FOR_CTX_DURATION = 30
59
+
56
60
  def wait_for_ctx(state)
61
+ t0 = Time.now
57
62
  period = 0.00001
58
63
  while !state[:ctx] && !state[:error]
59
64
  orig_sleep period
60
- period *= 2 if period < 0.1
65
+ if period < 0.1
66
+ period *= 2
67
+ elsif Time.now - t0 > MAX_WAIT_FOR_CTX_DURATION
68
+ raise "Timeout waiting for certificate provisioning"
69
+ end
61
70
  end
62
71
  end
63
-
72
+
64
73
  def run
65
74
  loop do
66
75
  name, state = @requests.shift
@@ -69,13 +78,13 @@ module Tipi
69
78
  state[:error] = e if state
70
79
  end
71
80
  end
72
-
81
+
73
82
  LOCALHOST_REGEXP = /\.?localhost$/.freeze
74
83
 
75
84
  def get_context(name)
76
85
  @contexts[name] = setup_context(name)
77
86
  end
78
-
87
+
79
88
  def setup_context(name)
80
89
  ctx = provision_context(name)
81
90
  transfer_ctx_settings(ctx)
@@ -100,7 +109,7 @@ module Tipi
100
109
  end
101
110
 
102
111
  CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
103
-
112
+
104
113
  def parse_certificate(certificate)
105
114
  certificate
106
115
  .scan(CERTIFICATE_REGEXP)
@@ -126,17 +135,17 @@ module Tipi
126
135
  @localhost_authority ||= Localhost::Authority.fetch
127
136
  @localhost_authority.server_context
128
137
  end
129
-
138
+
130
139
  def private_key
131
140
  @private_key ||= OpenSSL::PKey::RSA.new(4096)
132
141
  end
133
-
142
+
134
143
  ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
135
-
144
+
136
145
  def acme_client
137
146
  @acme_client ||= setup_acme_client
138
147
  end
139
-
148
+
140
149
  def setup_acme_client
141
150
  client = Acme::Client.new(
142
151
  private_key: private_key,
@@ -148,13 +157,12 @@ module Tipi
148
157
  )
149
158
  client
150
159
  end
151
-
160
+
152
161
  def provision_certificate(name)
153
- p provision_certificate: name
154
162
  order = acme_client.new_order(identifiers: [name])
155
163
  authorization = order.authorizations.first
156
164
  challenge = authorization.http
157
-
165
+
158
166
  @challenge_handler.add(challenge)
159
167
  challenge.request_validation
160
168
  while challenge.status == 'pending'
@@ -162,8 +170,7 @@ module Tipi
162
170
  challenge.reload
163
171
  end
164
172
  raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid'
165
-
166
- p challenge_status: challenge.status
173
+
167
174
  private_key = OpenSSL::PKey::RSA.new(4096)
168
175
  csr = Acme::Client::CertificateRequest.new(
169
176
  private_key: private_key,
@@ -189,33 +196,33 @@ module Tipi
189
196
  }
190
197
  end
191
198
  end
192
-
199
+
193
200
  class HTTPChallengeHandler
194
201
  def initialize
195
202
  @challenges = {}
196
203
  end
197
-
204
+
198
205
  def add(challenge)
199
206
  path = "/.well-known/acme-challenge/#{challenge.token}"
200
207
  @challenges[path] = challenge
201
208
  end
202
-
209
+
203
210
  def remove(challenge)
204
211
  path = "/.well-known/acme-challenge/#{challenge.token}"
205
212
  @challenges.delete(path)
206
213
  end
207
-
214
+
208
215
  def call(req)
209
216
  challenge = @challenges[req.path]
210
-
217
+
211
218
  # handle incoming request
212
219
  challenge = @challenges[req.path]
213
220
  return req.respond(nil, ':status' => 400) unless challenge
214
-
221
+
215
222
  req.respond(challenge.file_content, 'content-type' => challenge.content_type)
216
223
  end
217
- end
218
-
224
+ end
225
+
219
226
  class CertificateStore
220
227
  def set(name, private_key:, certificate:, expired_stamp:)
221
228
  raise NotImplementedError
data/lib/tipi/cli.rb CHANGED
@@ -2,29 +2,92 @@
2
2
 
3
3
  require 'tipi'
4
4
  require 'fileutils'
5
+ require 'tipi/supervisor'
6
+ require 'optparse'
5
7
 
6
8
  module Tipi
9
+ DEFAULT_OPTS = {
10
+ app_type: :web,
11
+ mode: :polyphony,
12
+ workers: 1,
13
+ threads: 1,
14
+ listen: ['http', 'localhost', 1234],
15
+ path: '.',
16
+ }
17
+
18
+ def self.opts_from_argv(argv)
19
+ opts = DEFAULT_OPTS.dup
20
+ parser = OptionParser.new do |o|
21
+ o.banner = "Usage: tipi [options] path"
22
+ o.on('-h', '--help', 'Show this help') { puts o; exit }
23
+ o.on('-wNUM', '--workers NUM', 'Number of worker processes (default: 1)') do |v|
24
+ opts[:workers] = v
25
+ end
26
+ o.on('-tNUM', '--threads NUM', 'Number of worker threads (default: 1)') do |v|
27
+ opts[:threads] = v
28
+ opts[:mode] = :stock
29
+ end
30
+ o.on('-c', '--compatibility', 'Use compatibility mode') do
31
+ opts[:mode] = :stock
32
+ end
33
+ o.on('-lSPEC', '--listen SPEC', 'Setup HTTP listener') do |v|
34
+ opts[:listen] = parse_listen_spec('http', v)
35
+ end
36
+ o.on('-sSPEC', '--secure SPEC', 'Setup HTTPS listener (for localhost)') do |v|
37
+ opts[:listen] = parse_listen_spec('https', v)
38
+ end
39
+ o.on('-fSPEC', '--full-service SPEC', 'Setup HTTP/HTTPS listeners (with automatic certificates)') do |v|
40
+ opts[:listen] = parse_listen_spec('full', v)
41
+ end
42
+ o.on('-v', '--verbose', 'Verbose output') do
43
+ opts[:verbose] = true
44
+ end
45
+ end.parse!(argv)
46
+ opts[:path] = argv.shift unless argv.empty?
47
+ verify_path(opts[:path])
48
+ opts
49
+ end
50
+
51
+ def self.parse_listen_spec(type, spec)
52
+ [type, *spec.split(':').map { |s| str_to_native_type(s) }]
53
+ end
54
+
55
+ def self.str_to_native_type(str)
56
+ case str
57
+ when /^\d+$/
58
+ str.to_i
59
+ else
60
+ str
61
+ end
62
+ end
63
+
64
+ def self.verify_path(path)
65
+ return if File.file?(path) || File.directory?(path)
66
+
67
+ puts "Invalid path specified #{opts[:path]}"
68
+ exit!
69
+ end
70
+
7
71
  module CLI
8
- BANNER = "
9
- ooo
10
- oo
11
- o
12
- \\|/ Tipi - a better web server for a better world
13
- / \\
14
- / \\ https://github.com/digital-fabric/tipi
15
- ⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
16
- "
17
-
18
- def self.start
19
- display_banner
20
- require File.expand_path(ARGV[0] || 'app.rb', FileUtils.pwd)
72
+ BANNER =
73
+ "\n" +
74
+ " ooo\n" +
75
+ " oo\n" +
76
+ " o\n" +
77
+ " \\|/ Tipi - a better web server for a better world\n" +
78
+ " / \\ \n" +
79
+ " / \\ https://github.com/digital-fabric/tipi\n" +
80
+ "⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺\n"
81
+
82
+ def self.start(argv = ARGV.dup)
83
+ opts = Tipi.opts_from_argv(argv)
84
+ display_banner if STDOUT.tty? && !opts[:silent]
85
+
86
+ Tipi::Supervisor.run(opts)
21
87
  end
22
88
 
23
89
  def self.display_banner
24
90
  puts BANNER
25
- puts
26
91
  end
27
92
  end
28
93
  end
29
-
30
- __END__
@@ -4,28 +4,28 @@ module Tipi
4
4
  module Configuration
5
5
  class Interpreter
6
6
  # make_blank_slate
7
-
7
+
8
8
  def initialize(assembler)
9
9
  @assembler = assembler
10
10
  end
11
-
11
+
12
12
  def gzip_response
13
13
  @assembler.emit 'req = Tipi::GZip.wrap(req)'
14
14
  end
15
-
15
+
16
16
  def log(out)
17
17
  @assembler.wrap_current_frame 'logger.log_request(req) do |req|'
18
18
  end
19
-
19
+
20
20
  def error(&block)
21
21
  assembler.emit_exception_handler &block
22
22
  end
23
-
23
+
24
24
  def match(pattern, &block)
25
25
  @assembler.emit_conditional "if req.path =~ #{pattern.inspect}", &block
26
26
  end
27
27
  end
28
-
28
+
29
29
  class Assembler
30
30
  def self.from_source(code)
31
31
  new.from_source code
@@ -36,7 +36,7 @@ module Tipi
36
36
  @app_procs = {}
37
37
  @interpreter = Interpreter.new self
38
38
  @interpreter.instance_eval code
39
-
39
+
40
40
  loop do
41
41
  frame = @stack.pop
42
42
  return assemble_app_proc(frame).join("\n") if @stack.empty?
@@ -51,7 +51,7 @@ module Tipi
51
51
  body: []
52
52
  }
53
53
  end
54
-
54
+
55
55
  def add_frame(&block)
56
56
  @stack.push new_frame
57
57
  yield
@@ -67,20 +67,20 @@ module Tipi
67
67
  @stack.push wrapper
68
68
  @stack.push frame
69
69
  end
70
-
70
+
71
71
  def emit(code)
72
72
  @stack.last[:body] << code
73
73
  end
74
-
74
+
75
75
  def emit_prelude(code)
76
76
  @stack.last[:prelude] << code
77
77
  end
78
-
78
+
79
79
  def emit_exception_handler(&block)
80
80
  proc_id = add_app_proc block
81
81
  @stack.last[:rescue_proc_id] = proc_id
82
82
  end
83
-
83
+
84
84
  def emit_block(conditional, &block)
85
85
  proc_id = add_app_proc block
86
86
  @stack.last[:branched] = true
@@ -93,7 +93,7 @@ module Tipi
93
93
  @app_procs[id] = proc
94
94
  id
95
95
  end
96
-
96
+
97
97
  def assemble_frame(frame)
98
98
  indent = 0
99
99
  lines = []
@@ -12,7 +12,7 @@ module Tipi
12
12
  old_runner&.stop
13
13
  end
14
14
  end
15
-
15
+
16
16
  def run(config)
17
17
  start_listeners(config)
18
18
  config[:forked] ? forked_supervise(config) : simple_supervise(config)
@@ -29,7 +29,7 @@ module Tipi
29
29
  suspend
30
30
  # supervise(restart: true)
31
31
  end
32
-
32
+
33
33
  def forked_supervise(config)
34
34
  config[:forked].times do
35
35
  spin { Polyphony.watch_process { simple_supervise(config) } }
File without changes
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tipi
4
+ module Apps
5
+ module Bare
6
+ def start(opts)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tipi/http1_adapter'
4
+
5
+ module Tipi
6
+ class StockHTTP1Adapter < HTTP1Adapter
7
+ def initialize(conn, opts)
8
+ super(conn, opts)
9
+
10
+ end
11
+
12
+ def each(&block)
13
+ end
14
+ end
15
+ end