itsi 0.1.14 → 0.1.18

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +124 -109
  3. data/Cargo.toml +6 -0
  4. data/crates/itsi_error/Cargo.toml +1 -0
  5. data/crates/itsi_error/src/lib.rs +100 -10
  6. data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  7. data/crates/itsi_server/Cargo.toml +8 -10
  8. data/crates/itsi_server/src/default_responses/html/401.html +68 -0
  9. data/crates/itsi_server/src/default_responses/html/403.html +68 -0
  10. data/crates/itsi_server/src/default_responses/html/404.html +68 -0
  11. data/crates/itsi_server/src/default_responses/html/413.html +71 -0
  12. data/crates/itsi_server/src/default_responses/html/429.html +68 -0
  13. data/crates/itsi_server/src/default_responses/html/500.html +71 -0
  14. data/crates/itsi_server/src/default_responses/html/502.html +71 -0
  15. data/crates/itsi_server/src/default_responses/html/503.html +68 -0
  16. data/crates/itsi_server/src/default_responses/html/504.html +69 -0
  17. data/crates/itsi_server/src/default_responses/html/index.html +238 -0
  18. data/crates/itsi_server/src/default_responses/json/401.json +6 -0
  19. data/crates/itsi_server/src/default_responses/json/403.json +6 -0
  20. data/crates/itsi_server/src/default_responses/json/404.json +6 -0
  21. data/crates/itsi_server/src/default_responses/json/413.json +6 -0
  22. data/crates/itsi_server/src/default_responses/json/429.json +6 -0
  23. data/crates/itsi_server/src/default_responses/json/500.json +6 -0
  24. data/crates/itsi_server/src/default_responses/json/502.json +6 -0
  25. data/crates/itsi_server/src/default_responses/json/503.json +6 -0
  26. data/crates/itsi_server/src/default_responses/json/504.json +6 -0
  27. data/crates/itsi_server/src/default_responses/mod.rs +11 -0
  28. data/crates/itsi_server/src/lib.rs +58 -26
  29. data/crates/itsi_server/src/prelude.rs +2 -0
  30. data/crates/itsi_server/src/ruby_types/README.md +21 -0
  31. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
  32. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  33. data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
  34. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
  35. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
  36. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
  37. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
  38. data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
  39. data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
  40. data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
  41. data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
  42. data/crates/itsi_server/src/server/binds/mod.rs +4 -0
  43. data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
  44. data/crates/itsi_server/src/server/http_message_types.rs +97 -0
  45. data/crates/itsi_server/src/server/io_stream.rs +2 -1
  46. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
  70. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  71. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
  72. data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
  73. data/crates/itsi_server/src/server/mod.rs +3 -9
  74. data/crates/itsi_server/src/server/process_worker.rs +21 -3
  75. data/crates/itsi_server/src/server/request_job.rs +2 -2
  76. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
  77. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
  78. data/crates/itsi_server/src/server/signal.rs +24 -41
  79. data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
  80. data/crates/itsi_server/src/server/thread_worker.rs +59 -28
  81. data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
  82. data/crates/itsi_server/src/services/mime_types.rs +1416 -0
  83. data/crates/itsi_server/src/services/mod.rs +6 -0
  84. data/crates/itsi_server/src/services/password_hasher.rs +83 -0
  85. data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
  86. data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
  87. data/crates/itsi_tracing/src/lib.rs +145 -55
  88. data/{Itsi.rb → foo/Itsi.rb} +6 -9
  89. data/gems/scheduler/Cargo.lock +7 -0
  90. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  91. data/gems/scheduler/test/helpers/test_helper.rb +0 -1
  92. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  93. data/gems/scheduler/test/test_network_io.rb +1 -1
  94. data/gems/scheduler/test/test_process_wait.rb +0 -1
  95. data/gems/server/Cargo.lock +124 -109
  96. data/gems/server/exe/itsi +65 -19
  97. data/gems/server/itsi-server.gemspec +4 -3
  98. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  99. data/gems/server/lib/itsi/http_request.rb +116 -17
  100. data/gems/server/lib/itsi/http_response.rb +2 -0
  101. data/gems/server/lib/itsi/passfile.rb +109 -0
  102. data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
  103. data/gems/server/lib/itsi/server/config.rb +58 -23
  104. data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
  105. data/gems/server/lib/itsi/server/default_app/index.html +113 -89
  106. data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
  107. data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
  108. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
  109. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  110. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  111. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  112. data/gems/server/lib/itsi/server/route_tester.rb +107 -0
  113. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  114. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  115. data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
  116. data/gems/server/lib/itsi/server/version.rb +1 -1
  117. data/gems/server/lib/itsi/server.rb +82 -12
  118. data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
  119. data/gems/server/lib/shell_completions/completions.rb +26 -0
  120. data/gems/server/test/helpers/test_helper.rb +2 -1
  121. data/lib/itsi/version.rb +1 -1
  122. data/sandbox/README.md +5 -0
  123. data/sandbox/itsi_file/Gemfile +4 -2
  124. data/sandbox/itsi_file/Gemfile.lock +48 -6
  125. data/sandbox/itsi_file/Itsi.rb +326 -129
  126. data/sandbox/itsi_file/call.json +1 -0
  127. data/sandbox/itsi_file/echo_client/Gemfile +10 -0
  128. data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
  129. data/sandbox/itsi_file/echo_client/README.md +95 -0
  130. data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
  131. data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
  132. data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
  133. data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
  134. data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
  135. data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
  136. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
  137. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
  138. data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
  139. data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
  140. data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
  141. data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
  142. data/sandbox/itsi_sandbox_async/config.ru +0 -1
  143. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  144. data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
  145. data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
  146. data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
  147. data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
  148. data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
  149. data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
  150. data/sandbox/itsi_sinatra/app.rb +0 -1
  151. data/sandbox/static_files/.env +1 -0
  152. data/sandbox/static_files/404.html +25 -0
  153. data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
  154. data/sandbox/static_files/about.html +68 -0
  155. data/sandbox/static_files/tiny.html +1 -0
  156. data/sandbox/static_files/writebook.zip +0 -0
  157. data/tasks.txt +28 -33
  158. metadata +87 -26
  159. data/crates/itsi_error/src/from.rs +0 -68
  160. data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
  161. data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
  162. data/crates/itsi_server/src/server/itsi_service.rs +0 -172
  163. data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
  164. data/crates/itsi_server/src/server/types.rs +0 -43
  165. data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
  166. data/sandbox/itsi_file/public/assets/index.html +0 -1
  167. /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
  168. /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
  169. /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -5,10 +5,10 @@ module Itsi
5
5
  attr_reader :parent, :children, :middleware, :controller_class, :routes, :methods, :protocols,
6
6
  :hosts, :ports, :extensions, :content_types, :accepts, :options
7
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)
8
+ def self.evaluate(config = Itsi::Server::Config.config_file_path, &blk)
9
+ new(routes: ["/"]) do
10
+ if blk
11
+ instance_exec(&blk)
12
12
  else
13
13
  code = IO.read(config)
14
14
  instance_eval(code, config.to_s, 1)
@@ -35,7 +35,6 @@ module Itsi
35
35
  @controller_class = nil
36
36
 
37
37
  @controller = controller
38
- # We'll store our array of route specs (strings or a single Regexp).
39
38
  @routes = Array(routes).flatten
40
39
  @methods = methods.map { |s| s.is_a?(Regexp) ? s : s.to_s }
41
40
  @protocols = protocols.map { |s| s.is_a?(Regexp) ? s : s.to_s }
@@ -49,6 +48,8 @@ module Itsi
49
48
  middleware_loaders: [],
50
49
  middleware_loader: lambda do
51
50
  @options[:middleware_loaders].each(&:call)
51
+ @middleware[:app] ||= {}
52
+ @middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
52
53
  flatten_routes
53
54
  end
54
55
  }
@@ -56,7 +57,6 @@ module Itsi
56
57
  instance_exec(&block)
57
58
  end
58
59
 
59
-
60
60
  def workers(workers)
61
61
  raise "Workers must be set at the root" unless @parent.nil?
62
62
 
@@ -84,84 +84,106 @@ module Itsi
84
84
  def log_format(format)
85
85
  raise "Log format must be set at the root" unless @parent.nil?
86
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
87
+ @options[:log_format] = format.to_s
88
+ end
89
+
90
+ def log_target(target)
91
+ raise "Log target must be set at the root" unless @parent.nil?
92
+
93
+ @options[:log_target] = target.to_s
93
94
  end
94
95
 
95
- def get(route, app_proc = nil, &blk)
96
- endpoint(route, :get, app_proc, &blk)
96
+ def get(route, app_proc = nil, nonblocking: false, &blk)
97
+ endpoint(route, [:get], app_proc, nonblocking: nonblocking, &blk)
97
98
  end
98
99
 
99
- def post(route, app_proc = nil, &blk)
100
- endpoint(route, :post, app_proc, &blk)
100
+ def post(route, app_proc = nil, nonblocking: false, &blk)
101
+ endpoint(route, [:post], app_proc, nonblocking: nonblocking, &blk)
101
102
  end
102
103
 
103
- def put(route, app_proc = nil, &blk)
104
- endpoint(route, :put, app_proc, &blk)
104
+ def put(route, app_proc = nil, nonblocking: false, &blk)
105
+ endpoint(route, [:put], app_proc, nonblocking: nonblocking, &blk)
105
106
  end
106
107
 
107
- def delete(route, app_proc = nil, &blk)
108
- endpoint(route, :delete, app_proc, &blk)
108
+ def delete(route, app_proc = nil, nonblocking: false, &blk)
109
+ endpoint(route, [:delete], app_proc, nonblocking: nonblocking, &blk)
109
110
  end
110
111
 
111
- def patch(route, app_proc = nil, &blk)
112
- endpoint(route, :patch, app_proc, &blk)
112
+ def patch(route, app_proc = nil, nonblocking: false, &blk)
113
+ endpoint(route, [:patch], app_proc, nonblocking: nonblocking, &blk)
113
114
  end
114
115
 
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
116
+ def endpoint(route, methods=[], app_proc = nil, nonblocking: false, &blk)
117
117
  raise "You must provide either a block or an explicit handler for the endpoint" if app_proc.nil? && blk.nil?
118
118
 
119
119
  app_proc = @controller.method(app_proc).to_proc if app_proc.is_a?(Symbol)
120
120
 
121
121
  app_proc ||= blk
122
+ num_required, keywords = Itsi::Server::TypedHandlers::SourceParser.extract_expr_from_source_location(app_proc)
123
+ params_schema = keywords[:params]
124
+
125
+ if params_schema && num_required > 1
126
+ raise "Cannot accept multiple required parameters in a single endpoint. A single typed or untyped params argument is supported"
127
+ end
128
+ if num_required > 2
129
+ raise "Cannot accept more than two required parameters in a single endpoint. An can either accept a single request argument, or a request and a params argument (which may be typed or untyped)"
130
+ end
131
+ if num_required == 0
132
+ raise "Cannot accept zero required parameters in a single endpoint. Endpoint must accept a request parameter"
133
+ end
134
+
135
+ accepts_params = !params_schema.nil? || num_required > 1
122
136
 
123
- location(route, methods: [method]) do
124
- @middleware[:app] = { app_proc: app_proc }
137
+ if accepts_params
138
+ app_proc = Itsi::Server::TypedHandlers.handler_for(app_proc, params_schema)
139
+ end
140
+
141
+
142
+ if route && methods.any?
143
+ # For endpoints, it's usually assumed trailing slash and non-trailing slash behaviour is the same
144
+ routes = route == "/" ? ["", "/"] : [route]
145
+ location(*routes, methods: methods) do
146
+ @middleware[:app] = { preloader: -> { app_proc }, nonblocking: nonblocking }
147
+ end
148
+ else
149
+ @middleware[:app] = { preloader: -> { app_proc }, nonblocking: nonblocking }
125
150
  end
126
151
  end
127
152
 
128
- def grpc(handler, **)
153
+ def grpc(*handlers, reflection: true, nonblocking: false, **, &blk)
129
154
  if @middleware[:app] && @middleware[:app][:request_type].to_s != "grpc"
130
155
  raise "App has already been set. You can use only one of `run` and `rackup_file` or `grpc` per location"
131
156
  end
132
157
 
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
158
+ grpc_reflection(handlers) if reflection
140
159
 
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"
160
+ handlers.each do |handler|
161
+ location(Regexp.new("#{Regexp.escape(handler.class.service_name)}/(?:#{handler.class.rpc_descs.keys.map(&:to_s).join("|")})")) do
162
+ @middleware[:app] = { preloader: -> { Itsi::Server::GrpcInterface.for(handler) }, request_type: "grpc", nonblocking: nonblocking }
163
+ instance_exec(&blk)
164
+ end
144
165
  end
166
+ end
145
167
 
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 }
168
+ def grpc_reflection(handlers)
169
+ @grpc_reflected_services ||= []
170
+ @grpc_reflected_services.concat(handlers)
171
+
172
+ location(["grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
173
+ "grpc.reflection.v1.ServerReflection/ServerReflectionInfo"]) do
174
+ @middleware[:app] = { preloader: lambda {
175
+ Itsi::Server::GrpcInterface.reflection_for(handlers)
176
+ }, request_type: "grpc" }
150
177
  end
151
178
  end
152
179
 
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
180
+ def run(app, sendfile: true, nonblocking: false, path_info: "/")
181
+ @middleware[:app] = { preloader: -> { Itsi::Server::RackInterface.for(app) }, sendfile: sendfile, base_path: "^(?<base_path>#{paths_from_parent.gsub(/\.\*\)$/, ')')}).*$", path_info: path_info, nonblocking: nonblocking }
182
+ end
157
183
 
184
+ def rackup_file(rackup_file, nonblocking: false, sendfile: true, path_info: "/")
158
185
  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
186
+ @middleware[:app] = { preloader: -> { Itsi::Server::RackInterface.for(rackup_file) }, sendfile: sendfile, base_path: "^(?<base_path>#{paths_from_parent.gsub(/\.\*\)$/, ')')}).*$", path_info: path_info, nonblocking: nonblocking }
165
187
  end
166
188
 
167
189
  def include(path)
@@ -209,6 +231,20 @@ module Itsi
209
231
  @options[:multithreaded_reactor] = !!multithreaded
210
232
  end
211
233
 
234
+ def pin_worker_cores(pin_worker_cores)
235
+ raise "Pin worker cores must be set at the root" unless @parent.nil?
236
+
237
+ @options[:pin_worker_cores] = !!pin_worker_cores
238
+ end
239
+
240
+ def auto_reload_config!
241
+ if ENV["BUNDLE_BIN_PATH"]
242
+ watch "Itsi.rb", [%w[bundle exec itsi restart]]
243
+ else
244
+ watch "Itsi.rb", [%w[itsi restart]]
245
+ end
246
+ end
247
+
212
248
  def watch(path, commands)
213
249
  raise "Watch be set at the root" unless @parent.nil?
214
250
 
@@ -223,6 +259,18 @@ module Itsi
223
259
  @options[:scheduler_class] = klass_name if klass_name
224
260
  end
225
261
 
262
+ def scheduler_threads(threads = 1)
263
+ raise "Scheduler threads must be set at the root" unless @parent.nil?
264
+
265
+ @options[:scheduler_threads] = threads
266
+ end
267
+
268
+ def request_timeout(request_timeout)
269
+ raise "Request timeout must be set at the root" unless @parent.nil?
270
+
271
+ @options[:request_timeout] = request_timeout
272
+ end
273
+
226
274
  def preload(preload)
227
275
  raise "Preload must be set at the root" unless @parent.nil?
228
276
 
@@ -235,11 +283,6 @@ module Itsi
235
283
  @options[:shutdown_timeout] = shutdown_timeout.to_f
236
284
  end
237
285
 
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
286
 
244
287
  def stream_body(stream_body)
245
288
  raise "Stream body must be set at the root" unless @parent.nil?
@@ -298,80 +341,85 @@ module Itsi
298
341
  end
299
342
 
300
343
  def controller(controller)
301
- raise "`controller` must be set inside a location block" if @parent.nil?
302
-
303
344
  @controller = controller
304
345
  end
305
346
 
306
347
  def auth_basic(**args)
307
- raise "`auth_basic` must be set inside a location block" if @parent.nil?
348
+
349
+ if File.exist?(".itsi-credentials") && !args[:credential_file]
350
+ args[:credential_file] = ".itsi-credentials"
351
+ end
352
+
353
+ if args[:credential_file] && File.exist?(args[:credential_file])
354
+ args[:credential_pairs] = Passfile.load(args[:credential_file])
355
+ end
308
356
 
309
357
  @middleware[:auth_basic] = args
310
358
  end
311
359
 
312
360
  def redirect(**args)
313
- raise "`redirect` must be set inside a location block" if @parent.nil?
314
-
315
361
  @middleware[:redirect] = args
316
362
  end
317
363
 
318
364
  def proxy(**args)
319
- raise "`proxy` must be set inside a location block" if @parent.nil?
320
-
321
365
  @middleware[:proxy] = args
322
366
  end
323
367
 
324
- def auth_jwt(**args)
325
- raise "`auth_jwt` must be set inside a location block" if @parent.nil?
368
+ def static_response(**args)
369
+ args[:body] = args[:body].bytes
370
+ @middleware[:static_response] = args
371
+ end
326
372
 
373
+ def auth_jwt(**args)
327
374
  @middleware[:auth_jwt] = args
328
375
  end
329
376
 
330
377
  def auth_api_key(**args)
331
- raise "`auth_api_key` must be set inside a location block" if @parent.nil?
378
+ if args[:valid_keys] && args[:valid_keys].is_a?(Array)
379
+ args[:valid_keys] = args[:valid_keys].each_with_index.map { |key, index| [index, key] }.to_h
380
+ args[:key_id_source] = nil
381
+ end
382
+
383
+ if File.exist?(".itsi-credentials") && !args[:credential_file]
384
+ args[:credential_file] = ".itsi-credentials"
385
+ end
386
+
387
+ if args[:credential_file] && File.exist?(args[:credential_file])
388
+ args[:valid_keys] = Passfile.load(args[:credential_file])
389
+ end
332
390
 
333
391
  @middleware[:auth_api_key] = args
334
392
  end
335
393
 
336
394
  def compress(**args)
337
- raise "`compress` must be set inside a location block" if @parent.nil?
338
-
339
395
  @middleware[:compression] = args
340
396
  end
341
397
 
342
398
  def request_headers(**args)
343
- raise "`request_headers` must be set inside a location block" if @parent.nil?
344
-
345
399
  @middleware[:request_headers] = args
346
400
  end
347
401
 
348
- def response_headers(**args)
349
- raise "`response_headers` must be set inside a location block" if @parent.nil?
402
+ def max_body(**args)
403
+ @middleware[:max_body] = args
404
+ end
350
405
 
406
+ def response_headers(**args)
351
407
  @middleware[:response_headers] = args
352
408
  end
353
409
 
354
410
  def rate_limit(**args)
355
- raise "`rate_limit` must be set inside a location block" if @parent.nil?
356
-
357
411
  @middleware[:rate_limit] = args
358
412
  end
359
413
 
360
414
  def cache_control(**args)
361
- raise "`cache_control` must be set inside a location block" if @parent.nil?
362
-
363
415
  @middleware[:cache_control] = args
364
416
  end
365
417
 
366
418
  def etag(**args)
367
- raise "`etag` must be set inside a location block" if @parent.nil?
368
-
369
419
  @middleware[:etag] = args
370
420
  end
371
421
 
372
422
  def intrusion_protection(**args)
373
- raise "`intrusion_protection` must be set inside a location block" if @parent.nil?
374
-
375
423
  args[:banned_url_patterns] = Array(args[:banned_url_patterns]).map do |pattern|
376
424
  if pattern.is_a?(Regexp)
377
425
  pattern.source
@@ -383,14 +431,10 @@ module Itsi
383
431
  end
384
432
 
385
433
  def cors(**args)
386
- raise "`cors` must be set inside a location block" if @parent.nil?
387
-
388
434
  @middleware[:cors] = args
389
435
  end
390
436
 
391
437
  def static_assets(**args)
392
- raise "`static_assets` must be set inside a location block" if @parent.nil?
393
-
394
438
  root_dir = args[:root_dir] || "."
395
439
 
396
440
  if !File.exist?(root_dir)
@@ -401,14 +445,20 @@ module Itsi
401
445
 
402
446
  args[:relative_path] = true unless args.key?(:relative_path)
403
447
 
404
- location(/(?<path_suffix>.*)/, extensions: args[:allowed_extensions] || []) do
448
+ args[:allowed_extensions] ||= []
449
+
450
+ if (args[:allowed_extensions].include?("html") || args[:allowed_extensions].include?(:html)) && args[:try_html_extension]
451
+ args[:allowed_extensions] << ""
452
+ end
453
+
454
+ args[:base_path] = "^(?<base_path>#{paths_from_parent}).*$"
455
+
456
+ location("*", extensions: args[:allowed_extensions]) do
405
457
  @middleware[:static_assets] = args
406
458
  end
407
459
  end
408
460
 
409
461
  def file_server(**args)
410
- raise "`file_server` must be set inside a location block" if @parent.nil?
411
-
412
462
  # Forward to static_assets for implementation
413
463
  puts "Note: file_server is an alias for static_assets"
414
464
  static_assets(**args)
@@ -421,7 +471,7 @@ module Itsi
421
471
  if route_options
422
472
  result << deep_stringify_keys(
423
473
  {
424
- route: Regexp.new("^#{route_options}$"),
474
+ route: Regexp.new("^#{route_options}/?$"),
425
475
  methods: @methods.any? ? @methods : nil,
426
476
  protocols: @protocols.any? ? @protocols : nil,
427
477
  hosts: @hosts.any? ? @hosts : nil,
@@ -443,22 +493,27 @@ module Itsi
443
493
  case seg
444
494
  when Regexp
445
495
  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
496
  else
457
- Regexp.escape(seg).gsub(%r{/$}, ".*")
497
+ parts = seg.split('/')
498
+ parts.map do |part|
499
+ case part
500
+ when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
501
+ param_name = Regexp.last_match(1)
502
+ custom = Regexp.last_match(2)
503
+ if custom && !custom.empty?
504
+ "(?<#{param_name}>#{custom})"
505
+ else
506
+ "(?<#{param_name}>[^/]+)"
507
+ end
508
+ when /\*/
509
+ part.gsub(/\*/, ".*")
510
+ else
511
+ Regexp.escape(part)
512
+ end
513
+ end.join("/")
458
514
  end
459
515
  end.join("|")
460
-
461
- if parent.paths_from_parent && parent.paths_from_parent != "(?:/.*)"
516
+ if parent && parent.paths_from_parent && parent.paths_from_parent != "(?:/)"
462
517
  "#{parent.paths_from_parent}#{route_or_str != "" ? "(?:#{route_or_str})" : ""}"
463
518
  else
464
519
  route_or_str = "/#{route_or_str}" unless route_or_str.start_with?("/")
@@ -475,12 +530,16 @@ module Itsi
475
530
  chain = []
476
531
  node = self
477
532
  while node
533
+ if node.middleware[:app]&.[](:preloader)
534
+ node.middleware[:app][:app_proc] = node.middleware[:app].delete(:preloader).call
535
+ end
478
536
  chain << node
479
537
  node = node.parent
480
538
  end
481
539
  chain.reverse!
482
540
 
483
541
  merged = {}
542
+
484
543
  chain.each do |n|
485
544
  n.middleware.each do |k, v|
486
545
  merged[k] = v
@@ -5,78 +5,113 @@ module Itsi
5
5
  module Config
6
6
  require_relative "config/dsl"
7
7
  require_relative "default_app/default_app"
8
- require "debug"
9
8
 
10
9
  ITSI_DEFAULT_CONFIG_FILE = "Itsi.rb"
11
10
 
12
- def self.save_argv!
11
+ def self.prep_reexec!
13
12
  @argv ||= ARGV[0...ARGV.index("--listeners")]
13
+
14
+ auto_suppress_fork_darwin_fork_safety_warnings = [
15
+ ENV["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"].nil? || ENV["PGGSSENCMODE"].nil?,
16
+ RUBY_PLATFORM =~ /darwin/,
17
+ !ENV.key?("ITSI_DISABLE_AUTO_DISABLE_DARWIN_FORK_SAFETY_WARNINGS"),
18
+ $PROGRAM_NAME =~ /itsi$/
19
+ ].all?
20
+ return unless auto_suppress_fork_darwin_fork_safety_warnings
21
+
22
+ env = ENV.to_h.merge("OBJC_DISABLE_INITIALIZE_FORK_SAFETY" => "YES", "PGGSSENCMODE" => "disable")
23
+ if ENV["BUNDLE_BIN_PATH"]
24
+ exec env, "bundle", "exec", $PROGRAM_NAME, *@argv
25
+ else
26
+ exec env, $PROGRAM_NAME, *@argv
27
+ end
14
28
  end
15
29
 
16
30
  # The configuration used when launching the Itsi server are evaluated in the following precedence:
17
31
  # 1. CLI Args.
18
32
  # 2. Itsi.rb file.
19
33
  # 3. Default values.
20
- def self.build_config(args, config_file_path, builder_proc)
34
+ def self.build_config(args, config_file_path, builder_proc = nil)
35
+ args.transform_keys!(&:to_sym)
21
36
  itsifile_config = \
22
37
  if builder_proc
23
- DSL.evaluate(builder_proc)
38
+ DSL.evaluate(&builder_proc)
39
+ elsif args[:static]
40
+ DSL.evaluate do
41
+ location "/" do
42
+ allow_list allowed_patterns: ['127.0.0.1']
43
+ rate_limit key: 'address', store_config: 'in_memory', requests: 2, seconds: 5
44
+ etag type: 'strong', algorithm: 'md5', min_body_size: 1024 * 1024
45
+ compress min_size: 1024 * 1024, level: 'fastest', algorithms: %w[zstd gzip brotli deflate], mime_types: %w[all], compress_streams: true
46
+ log_requests before: { level: "INFO", format: "[{request_id}] {method} {path_and_query} - {addr} " }, after: { level: "INFO", format: "[{request_id}] └─ {status} in {response_time}" }
47
+ static_assets \
48
+ relative_path: true,
49
+ allowed_extensions: [],
50
+ root_dir: '.',
51
+ not_found_behavior: {error: 'not_found'},
52
+ auto_index: true,
53
+ try_html_extension: true,
54
+ max_file_size_in_memory: 1024 * 1024, # 1MB
55
+ max_files_in_memory: 1000,
56
+ file_check_interval: 1,
57
+ serve_hidden_files: false,
58
+ headers: {
59
+ 'X-Content-Type-Options' => 'nosniff'
60
+ }
61
+ end
62
+ end
24
63
  elsif File.exist?(config_file_path.to_s)
25
64
  DSL.evaluate(config_file_path)
65
+ elsif File.exist?("./config.ru")
66
+ DSL.evaluate do
67
+ preload true
68
+ rackup_file args.fetch(:rackup_file, "./config.ru")
69
+ end
26
70
  else
27
- {}
71
+ DSL.evaluate{}
28
72
  end
29
- args.transform_keys!(&:to_sym)
73
+
30
74
  itsifile_config.transform_keys!(&:to_sym)
31
75
 
32
76
  # We'll preload while we load config, if enabled.
33
77
  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
78
  preload = args.fetch(:preload) { itsifile_config.fetch(:preload, false) }
45
79
 
46
80
  case preload
47
81
  # If we preload everything, then we'll load middleware and default rack app ahead of time
48
82
  when true
49
83
  preloaded_middleware = middleware_loader.call
50
- preloaded_app = default_app_loader.call
51
84
  middleware_loader = -> { preloaded_middleware }
52
- default_app_loader = -> { preloaded_app }
53
85
  # If we're just preloading a specific gem group, we'll do that here too
54
86
  when Symbol
55
87
  Bundler.require(preload)
56
88
  end
57
89
 
58
90
  {
59
- workers: args.fetch(:workers) { itsifile_config.fetch(:workers, Etc.nprocessors) },
91
+ workers: args.fetch(:workers) { itsifile_config.fetch(:workers, 1) },
60
92
  worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
61
93
  silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
62
94
  shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
63
95
  hooks: itsifile_config.fetch(:hooks, nil),
64
96
  preload: !!preload,
97
+ request_timeout: itsifile_config.fetch(:request_timeout, nil),
65
98
  notify_watchers: itsifile_config.fetch(:notify_watchers, nil),
66
99
  threads: args.fetch(:threads) { itsifile_config.fetch(:threads, 1) },
67
- script_name: args.fetch(:script_name) { itsifile_config.fetch(:script_name, "") },
100
+ scheduler_threads: args.fetch(:scheduler_threads) { itsifile_config.fetch(:scheduler_threads, nil) },
68
101
  streamable_body: args.fetch(:streamable_body) { itsifile_config.fetch(:streamable_body, false) },
69
102
  multithreaded_reactor: args.fetch(:multithreaded_reactor) do
70
- itsifile_config.fetch(:multithreaded_reactor, true)
103
+ itsifile_config.fetch(:multithreaded_reactor, nil)
71
104
  end,
105
+ pin_worker_cores: args.fetch(:pin_worker_cores) { itsifile_config.fetch(:pin_worker_cores, true) },
72
106
  scheduler_class: args.fetch(:scheduler_class) { itsifile_config.fetch(:scheduler_class, nil) },
73
107
  oob_gc_responses_threshold: args.fetch(:oob_gc_responses_threshold) do
74
108
  itsifile_config.fetch(:oob_gc_responses_threshold, nil)
75
109
  end,
76
110
  log_level: args.fetch(:log_level) { itsifile_config.fetch(:log_level, nil) },
111
+ log_format: args.fetch(:log_format) { itsifile_config.fetch(:log_format, nil) },
112
+ log_target: args.fetch(:log_target) { itsifile_config.fetch(:log_target, nil) },
77
113
  binds: args.fetch(:binds) { itsifile_config.fetch(:binds, ["http://0.0.0.0:3000"]) },
78
114
  middleware_loader: middleware_loader,
79
- default_app_loader: default_app_loader,
80
115
  listeners: args.fetch(:listeners) { nil }
81
116
  }.transform_keys(&:to_s)
82
117
  end
@@ -123,7 +158,7 @@ module Itsi
123
158
 
124
159
  puts "Writing default configuration..."
125
160
  File.open(ITSI_DEFAULT_CONFIG_FILE, "w") do |file|
126
- file.write(IO.read("#{__dir__}/Itsi.rb"))
161
+ file.write(IO.read("#{__dir__}/default_config/Itsi.rb"))
127
162
  end
128
163
  end
129
164
  end
@@ -4,35 +4,31 @@ DEFAULT_INDEX = IO.read("#{__dir__}/index.html").freeze
4
4
  DEFAULT_BINDS = ["http://0.0.0.0:3000"].freeze
5
5
  DEFAULT_APP = lambda {
6
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
7
+
8
+ Itsi::Server::RackInterface.for(lambda do |env|
9
+ headers, body = \
10
+ if env["itsi.response"].json?
11
+ [
12
+ { "Content-Type" => "application/json" },
13
+ [{ "message" => "You're running on Itsi!", "rack_env" => env,
14
+ "version" => Itsi::Server::VERSION }.to_json]
15
+ ]
16
+ else
17
+ [
18
+ { "Content-Type" => "text/html" },
19
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
- ]
20
+ format(
21
+ DEFAULT_INDEX,
22
+ REQUEST_METHOD: env["REQUEST_METHOD"],
23
+ PATH_INFO: env["PATH_INFO"],
24
+ SERVER_NAME: env["SERVER_NAME"],
25
+ SERVER_PORT: env["SERVER_PORT"],
26
+ REMOTE_ADDR: env["REMOTE_ADDR"],
27
+ HTTP_USER_AGENT: env["HTTP_USER_AGENT"]
28
+ )
32
29
  ]
33
- end
34
- [200, headers, body]
35
- end)
36
- }
37
-
30
+ ]
31
+ end
32
+ [200, headers, body]
33
+ end)
38
34
  }