tipi 0.43 → 0.45

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