itsi-server 0.1.11 → 0.1.12

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +1536 -45
  5. data/README.md +4 -0
  6. data/_index.md +6 -0
  7. data/exe/itsi +33 -74
  8. data/ext/itsi_error/src/lib.rs +9 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  11. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  12. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  23. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  24. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  25. data/ext/itsi_rb_helpers/src/lib.rs +34 -7
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  28. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  29. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  36. data/ext/itsi_server/Cargo.toml +69 -30
  37. data/ext/itsi_server/src/lib.rs +79 -147
  38. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  39. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  40. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  41. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  42. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  43. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +101 -117
  44. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  45. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  47. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  48. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  49. data/ext/itsi_server/src/server/bind.rs +13 -5
  50. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  51. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  52. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  53. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  54. data/ext/itsi_server/src/server/listener.rs +102 -2
  55. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  56. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  80. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  81. data/ext/itsi_server/src/server/mod.rs +8 -1
  82. data/ext/itsi_server/src/server/process_worker.rs +38 -12
  83. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  84. data/ext/itsi_server/src/server/request_job.rs +11 -0
  85. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  86. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  87. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  88. data/ext/itsi_server/src/server/signal.rs +19 -0
  89. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  90. data/ext/itsi_server/src/server/thread_worker.rs +139 -94
  91. data/ext/itsi_server/src/server/types.rs +43 -0
  92. data/ext/itsi_tracing/Cargo.toml +1 -0
  93. data/ext/itsi_tracing/src/lib.rs +216 -45
  94. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  95. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  96. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  98. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  99. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  100. data/lib/itsi/{request.rb → http_request.rb} +29 -5
  101. data/lib/itsi/http_response.rb +39 -0
  102. data/lib/itsi/server/Itsi.rb +11 -19
  103. data/lib/itsi/server/config/dsl.rb +506 -0
  104. data/lib/itsi/server/config.rb +103 -8
  105. data/lib/itsi/server/default_app/default_app.rb +38 -0
  106. data/lib/itsi/server/grpc_interface.rb +213 -0
  107. data/lib/itsi/server/rack/handler/itsi.rb +8 -17
  108. data/lib/itsi/server/rack_interface.rb +23 -4
  109. data/lib/itsi/server/scheduler_interface.rb +1 -1
  110. data/lib/itsi/server/scheduler_mode.rb +4 -0
  111. data/lib/itsi/server/signal_trap.rb +7 -1
  112. data/lib/itsi/server/version.rb +1 -1
  113. data/lib/itsi/server.rb +74 -63
  114. data/lib/itsi/standard_headers.rb +86 -0
  115. metadata +84 -15
  116. data/ext/itsi_scheduler/extconf.rb +0 -6
  117. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  118. data/ext/itsi_server/src/request/mod.rs +0 -1
  119. data/ext/itsi_server/src/response/mod.rs +0 -1
  120. data/ext/itsi_server/src/server/itsi_server.rs +0 -288
  121. data/lib/itsi/server/options_dsl.rb +0 -401
  122. data/lib/itsi/stream_io.rb +0 -38
  123. /data/lib/itsi/{index.html → server/default_app/index.html} +0 -0
@@ -0,0 +1,506 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class DSL
5
+ attr_reader :parent, :children, :middleware, :controller_class, :routes, :methods, :protocols,
6
+ :hosts, :ports, :extensions, :content_types, :accepts, :options
7
+
8
+ def self.evaluate(config = Itsi::Server::Config.config_file_path)
9
+ new do
10
+ if config.is_a?(Proc)
11
+ instance_exec(&config)
12
+ else
13
+ code = IO.read(config)
14
+ instance_eval(code, config.to_s, 1)
15
+ end
16
+ end.options
17
+ end
18
+
19
+ def initialize(
20
+ parent = nil,
21
+ routes: [],
22
+ methods: [],
23
+ protocols: [],
24
+ hosts: [],
25
+ ports: [],
26
+ extensions: [],
27
+ content_types: [],
28
+ accepts: [],
29
+ controller: self,
30
+ &block
31
+ )
32
+ @parent = parent
33
+ @children = []
34
+ @middleware = {}
35
+ @controller_class = nil
36
+
37
+ @controller = controller
38
+ # We'll store our array of route specs (strings or a single Regexp).
39
+ @routes = Array(routes).flatten
40
+ @methods = methods.map { |s| s.is_a?(Regexp) ? s : s.to_s }
41
+ @protocols = protocols.map { |s| s.is_a?(Regexp) ? s : s.to_s }
42
+ @hosts = hosts.map { |s| s.is_a?(Regexp) ? s : s.to_s }
43
+ @ports = ports.map { |s| s.is_a?(Regexp) ? s : s.to_s }
44
+ @extensions = extensions.map { |s| s.is_a?(Regexp) ? s : s.to_s }
45
+ @content_types = content_types.map { |s| s.is_a?(Regexp) ? s : s.to_s }
46
+ @accepts = accepts.map { |s| s.is_a?(Regexp) ? s : s.to_s }
47
+
48
+ @options = {
49
+ middleware_loaders: [],
50
+ middleware_loader: lambda do
51
+ @options[:middleware_loaders].each(&:call)
52
+ flatten_routes
53
+ end
54
+ }
55
+
56
+ instance_exec(&block)
57
+ end
58
+
59
+
60
+ def workers(workers)
61
+ raise "Workers must be set at the root" unless @parent.nil?
62
+
63
+ @options[:workers] = [workers.to_i, 1].max
64
+ end
65
+
66
+ def threads(threads)
67
+ raise "Threads must be set at the root" unless @parent.nil?
68
+
69
+ @options[:threads] = [threads.to_i, 1].max
70
+ end
71
+
72
+ def oob_gc_responses_threshold(threshold)
73
+ raise "OOB GC responses threshold must be set at the root" unless @parent.nil?
74
+
75
+ @options[:oob_gc_responses_threshold] = threshold.to_i
76
+ end
77
+
78
+ def log_level(level)
79
+ raise "Log level must be set at the root" unless @parent.nil?
80
+
81
+ @options[:log_level] = level.to_s
82
+ end
83
+
84
+ def log_format(format)
85
+ raise "Log format must be set at the root" unless @parent.nil?
86
+
87
+ case format.to_s
88
+ when "auto" then nil
89
+ when "ansi" then ENV["ITSI_LOG_ANSI"] = "true"
90
+ when "json", "plain" then ENV["ITSI_LOG_PLAIN"] = "true"
91
+ else raise "Invalid log format '#{format}'"
92
+ end
93
+ end
94
+
95
+ def get(route, app_proc = nil, &blk)
96
+ endpoint(route, :get, app_proc, &blk)
97
+ end
98
+
99
+ def post(route, app_proc = nil, &blk)
100
+ endpoint(route, :post, app_proc, &blk)
101
+ end
102
+
103
+ def put(route, app_proc = nil, &blk)
104
+ endpoint(route, :put, app_proc, &blk)
105
+ end
106
+
107
+ def delete(route, app_proc = nil, &blk)
108
+ endpoint(route, :delete, app_proc, &blk)
109
+ end
110
+
111
+ def patch(route, app_proc = nil, &blk)
112
+ endpoint(route, :patch, app_proc, &blk)
113
+ end
114
+
115
+ def endpoint(route, method, app_proc = nil, &blk)
116
+ raise "You can't use both a block and an explicit handler in the same endpoint" if blk && app_proc
117
+ raise "You must provide either a block or an explicit handler for the endpoint" if app_proc.nil? && blk.nil?
118
+
119
+ app_proc = @controller.method(app_proc).to_proc if app_proc.is_a?(Symbol)
120
+
121
+ app_proc ||= blk
122
+
123
+ location(route, methods: [method]) do
124
+ @middleware[:app] = { app_proc: app_proc }
125
+ end
126
+ end
127
+
128
+ def grpc(handler, **)
129
+ if @middleware[:app] && @middleware[:app][:request_type].to_s != "grpc"
130
+ raise "App has already been set. You can use only one of `run` and `rackup_file` or `grpc` per location"
131
+ end
132
+
133
+ @middleware[:app] ||= {
134
+ request_type: "grpc",
135
+ impls: []
136
+ }
137
+ @middleware[:app][:impls] << handler
138
+ @middleware[:app][:app_proc] = Itsi::Server::GrpcInterface.for(@middleware[:app][:impls])
139
+ end
140
+
141
+ def run(app, sendfile: true)
142
+ if @options[:app_loader]
143
+ raise "App has already been set. You can use only one of `run` and `rackup_file` per location"
144
+ end
145
+
146
+ if @parent.nil?
147
+ @options[:app_loader] = -> { { "app_proc" => Itsi::Server::RackInterface.for(app) } }
148
+ else
149
+ @middleware[:app] = { app_proc: Itsi::Server::RackInterface.for(app), sendfile: sendfile }
150
+ end
151
+ end
152
+
153
+ def rackup_file(rackup_file)
154
+ if @options[:app_loader]
155
+ raise "App has already been set. You can use only one of `run` and `rackup_file` per location"
156
+ end
157
+
158
+ raise "Rackup file #{rackup_file} doesn't exist" unless File.exist?(rackup_file)
159
+
160
+ if @parent.nil?
161
+ @options[:app_loader] = -> { { "app_proc" => Itsi::Server::RackInterface.for(rackup_file) } }
162
+ else
163
+ @middleware[:app] = { app_proc: Itsi::Server::RackInterface.for(rackup_file) }
164
+ end
165
+ end
166
+
167
+ def include(path)
168
+ code = IO.read("#{path}.rb")
169
+ instance_eval(code, "#{path}.rb", 1)
170
+ end
171
+
172
+ def bind(bind_str)
173
+ raise "Bind must be set at the root" unless @parent.nil?
174
+
175
+ @options[:binds] ||= []
176
+ @options[:binds] << bind_str.to_s
177
+ end
178
+
179
+ def after_fork(&block)
180
+ raise "After fork must be set at the root" unless @parent.nil?
181
+
182
+ @options[:hooks] ||= {}
183
+ @options[:hooks][:after_fork] = block
184
+ end
185
+
186
+ def before_fork(&block)
187
+ raise "Before fork must be set at the root" unless @parent.nil?
188
+
189
+ @options[:hooks] ||= {}
190
+ @options[:hooks][:before_fork] = block
191
+ end
192
+
193
+ def after_memory_threshold_reached(&block)
194
+ raise "Before fork must be set at the root" unless @parent.nil?
195
+
196
+ @options[:hooks] ||= {}
197
+ @options[:hooks][:after_memory_threshold_reached] = block
198
+ end
199
+
200
+ def worker_memory_limit(memory_limit)
201
+ raise "Worker memory limit must be set at the root" unless @parent.nil?
202
+
203
+ @options[:worker_memory_limit] = memory_limit
204
+ end
205
+
206
+ def multithreaded_reactor(multithreaded)
207
+ raise "Multithreaded reactor must be set at the root" unless @parent.nil?
208
+
209
+ @options[:multithreaded_reactor] = !!multithreaded
210
+ end
211
+
212
+ def watch(path, commands)
213
+ raise "Watch be set at the root" unless @parent.nil?
214
+
215
+ @options[:notify_watchers] ||= []
216
+ @options[:notify_watchers] << [path, commands]
217
+ end
218
+
219
+ def fiber_scheduler(klass_name = true)
220
+ raise "Fiber scheduler must be set at the root" unless @parent.nil?
221
+
222
+ klass_name = "Itsi::Scheduler" if klass_name == true
223
+ @options[:scheduler_class] = klass_name if klass_name
224
+ end
225
+
226
+ def preload(preload)
227
+ raise "Preload must be set at the root" unless @parent.nil?
228
+
229
+ @options[:preload] = preload
230
+ end
231
+
232
+ def shutdown_timeout(shutdown_timeout)
233
+ raise "Shutdown timeout must be set at the root" unless @parent.nil?
234
+
235
+ @options[:shutdown_timeout] = shutdown_timeout.to_f
236
+ end
237
+
238
+ def script_name(script_name)
239
+ raise "Script name must be set at the root" unless @parent.nil?
240
+
241
+ @options[:script_name] = script_name.to_s
242
+ end
243
+
244
+ def stream_body(stream_body)
245
+ raise "Stream body must be set at the root" unless @parent.nil?
246
+
247
+ @options[:stream_body] = !!stream_body
248
+ end
249
+
250
+ def location(*routes, methods: [], protocols: [], hosts: [], ports: [], extensions: [], content_types: [],
251
+ accepts: [], &block)
252
+ build_child = lambda {
253
+ @children << DSL.new(
254
+ self,
255
+ routes: routes,
256
+ methods: Array(methods) | self.methods,
257
+ protocols: Array(protocols) | self.protocols,
258
+ hosts: Array(hosts) | self.hosts,
259
+ ports: Array(ports) | self.ports,
260
+ extensions: Array(extensions) | self.extensions,
261
+ content_types: Array(content_types) | self.content_types,
262
+ accepts: Array(accepts) | self.accepts,
263
+ controller: @controller,
264
+ &block
265
+ )
266
+ }
267
+ if @parent.nil?
268
+ @options[:middleware_loaders] << build_child
269
+ else
270
+ build_child[]
271
+ end
272
+ end
273
+
274
+ def log_requests(**args)
275
+ @middleware[:log_requests] = args
276
+ end
277
+
278
+ def allow_list(**args)
279
+ args[:allowed_patterns] = Array(args[:allowed_patterns]).map do |pattern|
280
+ if pattern.is_a?(Regexp)
281
+ pattern.source
282
+ else
283
+ pattern
284
+ end
285
+ end
286
+ @middleware[:allow_list] = args
287
+ end
288
+
289
+ def deny_list(**args)
290
+ args[:denied_patterns] = Array(args[:denied_patterns]).map do |pattern|
291
+ if pattern.is_a?(Regexp)
292
+ pattern.source
293
+ else
294
+ pattern
295
+ end
296
+ end
297
+ @middleware[:deny_list] = args
298
+ end
299
+
300
+ def controller(controller)
301
+ raise "`controller` must be set inside a location block" if @parent.nil?
302
+
303
+ @controller = controller
304
+ end
305
+
306
+ def auth_basic(**args)
307
+ raise "`auth_basic` must be set inside a location block" if @parent.nil?
308
+
309
+ @middleware[:auth_basic] = args
310
+ end
311
+
312
+ def redirect(**args)
313
+ raise "`redirect` must be set inside a location block" if @parent.nil?
314
+
315
+ @middleware[:redirect] = args
316
+ end
317
+
318
+ def proxy(**args)
319
+ raise "`proxy` must be set inside a location block" if @parent.nil?
320
+
321
+ @middleware[:proxy] = args
322
+ end
323
+
324
+ def auth_jwt(**args)
325
+ raise "`auth_jwt` must be set inside a location block" if @parent.nil?
326
+
327
+ @middleware[:auth_jwt] = args
328
+ end
329
+
330
+ def auth_api_key(**args)
331
+ raise "`auth_api_key` must be set inside a location block" if @parent.nil?
332
+
333
+ @middleware[:auth_api_key] = args
334
+ end
335
+
336
+ def compress(**args)
337
+ raise "`compress` must be set inside a location block" if @parent.nil?
338
+
339
+ @middleware[:compression] = args
340
+ end
341
+
342
+ def request_headers(**args)
343
+ raise "`request_headers` must be set inside a location block" if @parent.nil?
344
+
345
+ @middleware[:request_headers] = args
346
+ end
347
+
348
+ def response_headers(**args)
349
+ raise "`response_headers` must be set inside a location block" if @parent.nil?
350
+
351
+ @middleware[:response_headers] = args
352
+ end
353
+
354
+ def rate_limit(**args)
355
+ raise "`rate_limit` must be set inside a location block" if @parent.nil?
356
+
357
+ @middleware[:rate_limit] = args
358
+ end
359
+
360
+ def cache_control(**args)
361
+ raise "`cache_control` must be set inside a location block" if @parent.nil?
362
+
363
+ @middleware[:cache_control] = args
364
+ end
365
+
366
+ def etag(**args)
367
+ raise "`etag` must be set inside a location block" if @parent.nil?
368
+
369
+ @middleware[:etag] = args
370
+ end
371
+
372
+ def intrusion_protection(**args)
373
+ raise "`intrusion_protection` must be set inside a location block" if @parent.nil?
374
+
375
+ args[:banned_url_patterns] = Array(args[:banned_url_patterns]).map do |pattern|
376
+ if pattern.is_a?(Regexp)
377
+ pattern.source
378
+ else
379
+ pattern
380
+ end
381
+ end
382
+ @middleware[:intrusion_protection] = args
383
+ end
384
+
385
+ def cors(**args)
386
+ raise "`cors` must be set inside a location block" if @parent.nil?
387
+
388
+ @middleware[:cors] = args
389
+ end
390
+
391
+ def static_assets(**args)
392
+ raise "`static_assets` must be set inside a location block" if @parent.nil?
393
+
394
+ root_dir = args[:root_dir] || "."
395
+
396
+ if !File.exist?(root_dir)
397
+ warn "Warning: static_assets root_dir '#{root_dir}' does not exist!"
398
+ elsif !File.directory?(root_dir)
399
+ warn "Warning: static_assets root_dir '#{root_dir}' is not a directory!"
400
+ end
401
+
402
+ args[:relative_path] = true unless args.key?(:relative_path)
403
+
404
+ location(/(?<path_suffix>.*)/, extensions: args[:allowed_extensions] || []) do
405
+ @middleware[:static_assets] = args
406
+ end
407
+ end
408
+
409
+ def file_server(**args)
410
+ raise "`file_server` must be set inside a location block" if @parent.nil?
411
+
412
+ # Forward to static_assets for implementation
413
+ puts "Note: file_server is an alias for static_assets"
414
+ static_assets(**args)
415
+ end
416
+
417
+ def flatten_routes
418
+ result = []
419
+ result.concat(@children.flat_map(&:flatten_routes))
420
+ route_options = paths_from_parent
421
+ if route_options
422
+ result << deep_stringify_keys(
423
+ {
424
+ route: Regexp.new("^#{route_options}$"),
425
+ methods: @methods.any? ? @methods : nil,
426
+ protocols: @protocols.any? ? @protocols : nil,
427
+ hosts: @hosts.any? ? @hosts : nil,
428
+ ports: @ports.any? ? @ports : nil,
429
+ extensions: @extensions.any? ? @extensions : nil,
430
+ content_types: @content_types.any? ? @content_types : nil,
431
+ accepts: @accepts.any? ? @accepts : nil,
432
+ middleware: effective_middleware
433
+ }
434
+ )
435
+ end
436
+ result
437
+ end
438
+
439
+ def paths_from_parent
440
+ return nil unless @routes.any?
441
+
442
+ route_or_str = @routes.map do |seg|
443
+ case seg
444
+ when Regexp
445
+ seg.source
446
+ when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
447
+ param_name = Regexp.last_match(1)
448
+ custom = Regexp.last_match(2)
449
+ if custom && !custom.empty?
450
+ "(?<#{param_name}>#{custom})"
451
+ else
452
+ "(?<#{param_name}>[^/]+)"
453
+ end
454
+ when /\*/
455
+ seg.gsub(/\*/, ".*")
456
+ else
457
+ Regexp.escape(seg).gsub(%r{/$}, ".*")
458
+ end
459
+ end.join("|")
460
+
461
+ if parent.paths_from_parent && parent.paths_from_parent != "(?:/.*)"
462
+ "#{parent.paths_from_parent}#{route_or_str != "" ? "(?:#{route_or_str})" : ""}"
463
+ else
464
+ route_or_str = "/#{route_or_str}" unless route_or_str.start_with?("/")
465
+ "(?:#{route_or_str})"
466
+ end
467
+ end
468
+
469
+ def effective_middleware
470
+ merged = merge_ancestor_middleware
471
+ merged.map { |k, v| { type: k.to_s, parameters: v } }
472
+ end
473
+
474
+ def merge_ancestor_middleware
475
+ chain = []
476
+ node = self
477
+ while node
478
+ chain << node
479
+ node = node.parent
480
+ end
481
+ chain.reverse!
482
+
483
+ merged = {}
484
+ chain.each do |n|
485
+ n.middleware.each do |k, v|
486
+ merged[k] = v
487
+ end
488
+ end
489
+ merged
490
+ end
491
+
492
+ def deep_stringify_keys(obj)
493
+ case obj
494
+ when Hash
495
+ obj.transform_keys!(&:to_s)
496
+ obj.transform_values! { |v| deep_stringify_keys(v) }
497
+ when Array
498
+ obj.map { |v| deep_stringify_keys(v) }
499
+ else
500
+ obj
501
+ end
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end
@@ -1,26 +1,121 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Itsi
2
4
  class Server
3
5
  module Config
4
- module_function
6
+ require_relative "config/dsl"
7
+ require_relative "default_app/default_app"
8
+ require "debug"
5
9
 
6
10
  ITSI_DEFAULT_CONFIG_FILE = "Itsi.rb"
7
11
 
8
- def load(options)
9
- options[:config_file] ||= \
12
+ def self.save_argv!
13
+ @argv ||= ARGV[0...ARGV.index("--listeners")]
14
+ end
15
+
16
+ # The configuration used when launching the Itsi server are evaluated in the following precedence:
17
+ # 1. CLI Args.
18
+ # 2. Itsi.rb file.
19
+ # 3. Default values.
20
+ def self.build_config(args, config_file_path, builder_proc)
21
+ itsifile_config = \
22
+ if builder_proc
23
+ DSL.evaluate(builder_proc)
24
+ elsif File.exist?(config_file_path.to_s)
25
+ DSL.evaluate(config_file_path)
26
+ else
27
+ {}
28
+ end
29
+ args.transform_keys!(&:to_sym)
30
+ itsifile_config.transform_keys!(&:to_sym)
31
+
32
+ # We'll preload while we load config, if enabled.
33
+ middleware_loader = itsifile_config.fetch(:middleware_loader, -> {})
34
+ default_app_loader = itsifile_config.fetch(:app_loader) do
35
+ rackup_file_path = args.fetch(:rackup_file, "./config.ru")
36
+ if File.exist?(rackup_file_path)
37
+ lambda {
38
+ { "app_proc" => Itsi::Server::RackInterface.for(rackup_file_path) }
39
+ }
40
+ else
41
+ DEFAULT_APP
42
+ end
43
+ end
44
+ preload = args.fetch(:preload) { itsifile_config.fetch(:preload, false) }
45
+
46
+ case preload
47
+ # If we preload everything, then we'll load middleware and default rack app ahead of time
48
+ when true
49
+ preloaded_middleware = middleware_loader.call
50
+ preloaded_app = default_app_loader.call
51
+ middleware_loader = -> { preloaded_middleware }
52
+ default_app_loader = -> { preloaded_app }
53
+ # If we're just preloading a specific gem group, we'll do that here too
54
+ when Symbol
55
+ Bundler.require(preload)
56
+ end
57
+
58
+ {
59
+ workers: args.fetch(:workers) { itsifile_config.fetch(:workers, Etc.nprocessors) },
60
+ worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
61
+ silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
62
+ shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
63
+ hooks: itsifile_config.fetch(:hooks, nil),
64
+ preload: !!preload,
65
+ notify_watchers: itsifile_config.fetch(:notify_watchers, nil),
66
+ threads: args.fetch(:threads) { itsifile_config.fetch(:threads, 1) },
67
+ script_name: args.fetch(:script_name) { itsifile_config.fetch(:script_name, "") },
68
+ streamable_body: args.fetch(:streamable_body) { itsifile_config.fetch(:streamable_body, false) },
69
+ multithreaded_reactor: args.fetch(:multithreaded_reactor) do
70
+ itsifile_config.fetch(:multithreaded_reactor, true)
71
+ end,
72
+ scheduler_class: args.fetch(:scheduler_class) { itsifile_config.fetch(:scheduler_class, nil) },
73
+ oob_gc_responses_threshold: args.fetch(:oob_gc_responses_threshold) do
74
+ itsifile_config.fetch(:oob_gc_responses_threshold, nil)
75
+ end,
76
+ log_level: args.fetch(:log_level) { itsifile_config.fetch(:log_level, nil) },
77
+ binds: args.fetch(:binds) { itsifile_config.fetch(:binds, ["http://0.0.0.0:3000"]) },
78
+ middleware_loader: middleware_loader,
79
+ default_app_loader: default_app_loader,
80
+ listeners: args.fetch(:listeners) { nil }
81
+ }.transform_keys(&:to_s)
82
+ end
83
+
84
+ # Reloads the entire process
85
+ # using exec, passing in any active file descriptors
86
+ # and previous invocation arguments
87
+ def self.reload_exec(listener_info)
88
+ if ENV["BUNDLE_BIN_PATH"]
89
+ exec "bundle", "exec", $PROGRAM_NAME, *@argv, "--listeners", listener_info
90
+ else
91
+ exec $PROGRAM_NAME, *@argv, "--listeners", listener_info
92
+ end
93
+ end
94
+
95
+ # Find config file path, if it exists.
96
+ def self.config_file_path(config_file_path = nil)
97
+ config_file_path ||= \
10
98
  if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
11
99
  ITSI_DEFAULT_CONFIG_FILE
12
100
  elsif File.exist?("config/#{ITSI_DEFAULT_CONFIG_FILE}")
13
101
  "config/#{ITSI_DEFAULT_CONFIG_FILE}"
14
102
  end
15
-
16
103
  # Options simply pass through unless we've specified a config file
17
- return options unless options[:config_file]
104
+ return unless File.exist?(config_file_path.to_s)
105
+
106
+ config_file_path
107
+ end
18
108
 
19
- require_relative "options_dsl"
20
- OptionsDSL.evaluate(options[:config_file]).merge(options)
109
+ def self.pid_file_path
110
+ if Dir.exist?("tmp")
111
+ File.join("tmp", "itsi.pid")
112
+ else
113
+ ".itsi.pid"
114
+ end
21
115
  end
22
116
 
23
- def write_default
117
+ # Write a default config file, if one doesn't exist.
118
+ def self.write_default
24
119
  if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
25
120
  puts "#{ITSI_DEFAULT_CONFIG_FILE} already exists."
26
121
  return
@@ -0,0 +1,38 @@
1
+ # When you Run Itsi without a Rack app,
2
+ # we start a tiny little echo server, just so you can see it in action.
3
+ DEFAULT_INDEX = IO.read("#{__dir__}/index.html").freeze
4
+ DEFAULT_BINDS = ["http://0.0.0.0:3000"].freeze
5
+ DEFAULT_APP = lambda {
6
+ require "json"
7
+ require "itsi/scheduler"
8
+ Itsi.log_warn "No config.ru or Itsi.rb app detected. Running default app."
9
+ {
10
+ "app_proc" => Itsi::Server::RackInterface.for(lambda do |env|
11
+ headers, body = \
12
+ if env["itsi.response"].json?
13
+ [
14
+ { "Content-Type" => "application/json" },
15
+ [{ "message" => "You're running on Itsi!", "rack_env" => env,
16
+ "version" => Itsi::Server::VERSION }.to_json]
17
+ ]
18
+ else
19
+ [
20
+ { "Content-Type" => "text/html" },
21
+ [
22
+ format(
23
+ DEFAULT_INDEX,
24
+ REQUEST_METHOD: env["REQUEST_METHOD"],
25
+ PATH_INFO: env["PATH_INFO"],
26
+ SERVER_NAME: env["SERVER_NAME"],
27
+ SERVER_PORT: env["SERVER_PORT"],
28
+ REMOTE_ADDR: env["REMOTE_ADDR"],
29
+ HTTP_USER_AGENT: env["HTTP_USER_AGENT"]
30
+ )
31
+ ]
32
+ ]
33
+ end
34
+ [200, headers, body]
35
+ end)
36
+ }
37
+
38
+ }