itsi 0.1.14 → 0.1.19

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 +126 -272
  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 +12 -11
  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 +126 -272
  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 +117 -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 +171 -99
  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 +327 -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,83 +84,118 @@ 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=nil, 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
+ app = { preloader: -> { app_proc }, nonblocking: nonblocking }
150
+ @middleware[:app] = app
151
+ location("*") do
152
+ @middleware[:app] = app
153
+ end
125
154
  end
126
155
  end
127
156
 
128
- def grpc(handler, **)
157
+ def grpc(*handlers, reflection: true, nonblocking: false, **, &blk)
129
158
  if @middleware[:app] && @middleware[:app][:request_type].to_s != "grpc"
130
159
  raise "App has already been set. You can use only one of `run` and `rackup_file` or `grpc` per location"
131
160
  end
132
161
 
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
162
+ grpc_reflection(handlers) if reflection
140
163
 
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"
164
+ handlers.each do |handler|
165
+ location(Regexp.new("#{Regexp.escape(handler.class.service_name)}/(?:#{handler.class.rpc_descs.keys.map(&:to_s).join("|")})")) do
166
+ @middleware[:app] = { preloader: -> { Itsi::Server::GrpcInterface.for(handler) }, request_type: "grpc", nonblocking: nonblocking }
167
+ instance_exec(&blk)
168
+ end
144
169
  end
170
+ end
145
171
 
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 }
172
+ def grpc_reflection(handlers)
173
+ @grpc_reflected_services ||= []
174
+ @grpc_reflected_services.concat(handlers)
175
+
176
+ location(["grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
177
+ "grpc.reflection.v1.ServerReflection/ServerReflectionInfo"]) do
178
+ @middleware[:app] = { preloader: lambda {
179
+ Itsi::Server::GrpcInterface.reflection_for(handlers)
180
+ }, request_type: "grpc" }
150
181
  end
151
182
  end
152
183
 
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"
184
+ def run(app, sendfile: true, nonblocking: false, path_info: "/")
185
+ app_args = { preloader: -> { Itsi::Server::RackInterface.for(app) }, sendfile: sendfile, base_path: "^(?<base_path>#{paths_from_parent.gsub(/\.\*\)$/, ')')}).*$", path_info: path_info, nonblocking: nonblocking }
186
+ base_path = "^(?<base_path>#{paths_from_parent.gsub(/\.\*\)$/, ')')}).*$"
187
+ @middleware[:app] = app_args
188
+ location("*") do
189
+ @middleware[:app] = app_args
156
190
  end
191
+ end
157
192
 
193
+ def rackup_file(rackup_file, nonblocking: false, sendfile: true, path_info: "/")
158
194
  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) }
195
+ app_args = { preloader: -> { Itsi::Server::RackInterface.for(rackup_file) }, sendfile: sendfile, base_path: "^(?<base_path>#{paths_from_parent.gsub(/\.\*\)$/, ')')}).*$", path_info: path_info, nonblocking: nonblocking }
196
+ @middleware[:app] = app_args
197
+ location("*") do
198
+ @middleware[:app] = app_args
164
199
  end
165
200
  end
166
201
 
@@ -209,6 +244,20 @@ module Itsi
209
244
  @options[:multithreaded_reactor] = !!multithreaded
210
245
  end
211
246
 
247
+ def pin_worker_cores(pin_worker_cores)
248
+ raise "Pin worker cores must be set at the root" unless @parent.nil?
249
+
250
+ @options[:pin_worker_cores] = !!pin_worker_cores
251
+ end
252
+
253
+ def auto_reload_config!
254
+ if ENV["BUNDLE_BIN_PATH"]
255
+ watch "Itsi.rb", [%w[bundle exec itsi restart]]
256
+ else
257
+ watch "Itsi.rb", [%w[itsi restart]]
258
+ end
259
+ end
260
+
212
261
  def watch(path, commands)
213
262
  raise "Watch be set at the root" unless @parent.nil?
214
263
 
@@ -223,6 +272,18 @@ module Itsi
223
272
  @options[:scheduler_class] = klass_name if klass_name
224
273
  end
225
274
 
275
+ def scheduler_threads(threads = 1)
276
+ raise "Scheduler threads must be set at the root" unless @parent.nil?
277
+
278
+ @options[:scheduler_threads] = threads
279
+ end
280
+
281
+ def request_timeout(request_timeout)
282
+ raise "Request timeout must be set at the root" unless @parent.nil?
283
+
284
+ @options[:request_timeout] = request_timeout
285
+ end
286
+
226
287
  def preload(preload)
227
288
  raise "Preload must be set at the root" unless @parent.nil?
228
289
 
@@ -235,11 +296,6 @@ module Itsi
235
296
  @options[:shutdown_timeout] = shutdown_timeout.to_f
236
297
  end
237
298
 
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
299
 
244
300
  def stream_body(stream_body)
245
301
  raise "Stream body must be set at the root" unless @parent.nil?
@@ -298,80 +354,85 @@ module Itsi
298
354
  end
299
355
 
300
356
  def controller(controller)
301
- raise "`controller` must be set inside a location block" if @parent.nil?
302
-
303
357
  @controller = controller
304
358
  end
305
359
 
306
360
  def auth_basic(**args)
307
- raise "`auth_basic` must be set inside a location block" if @parent.nil?
361
+
362
+ if File.exist?(".itsi-credentials") && !args[:credential_file]
363
+ args[:credential_file] = ".itsi-credentials"
364
+ end
365
+
366
+ if args[:credential_file] && File.exist?(args[:credential_file])
367
+ args[:credential_pairs] = Passfile.load(args[:credential_file])
368
+ end
308
369
 
309
370
  @middleware[:auth_basic] = args
310
371
  end
311
372
 
312
373
  def redirect(**args)
313
- raise "`redirect` must be set inside a location block" if @parent.nil?
314
-
315
374
  @middleware[:redirect] = args
316
375
  end
317
376
 
318
377
  def proxy(**args)
319
- raise "`proxy` must be set inside a location block" if @parent.nil?
320
-
321
378
  @middleware[:proxy] = args
322
379
  end
323
380
 
324
- def auth_jwt(**args)
325
- raise "`auth_jwt` must be set inside a location block" if @parent.nil?
381
+ def static_response(**args)
382
+ args[:body] = args[:body].bytes
383
+ @middleware[:static_response] = args
384
+ end
326
385
 
386
+ def auth_jwt(**args)
327
387
  @middleware[:auth_jwt] = args
328
388
  end
329
389
 
330
390
  def auth_api_key(**args)
331
- raise "`auth_api_key` must be set inside a location block" if @parent.nil?
391
+ if args[:valid_keys] && args[:valid_keys].is_a?(Array)
392
+ args[:valid_keys] = args[:valid_keys].each_with_index.map { |key, index| [index, key] }.to_h
393
+ args[:key_id_source] = nil
394
+ end
395
+
396
+ if File.exist?(".itsi-credentials") && !args[:credential_file]
397
+ args[:credential_file] = ".itsi-credentials"
398
+ end
399
+
400
+ if args[:credential_file] && File.exist?(args[:credential_file])
401
+ args[:valid_keys] = Passfile.load(args[:credential_file])
402
+ end
332
403
 
333
404
  @middleware[:auth_api_key] = args
334
405
  end
335
406
 
336
407
  def compress(**args)
337
- raise "`compress` must be set inside a location block" if @parent.nil?
338
-
339
408
  @middleware[:compression] = args
340
409
  end
341
410
 
342
411
  def request_headers(**args)
343
- raise "`request_headers` must be set inside a location block" if @parent.nil?
344
-
345
412
  @middleware[:request_headers] = args
346
413
  end
347
414
 
348
- def response_headers(**args)
349
- raise "`response_headers` must be set inside a location block" if @parent.nil?
415
+ def max_body(**args)
416
+ @middleware[:max_body] = args
417
+ end
350
418
 
419
+ def response_headers(**args)
351
420
  @middleware[:response_headers] = args
352
421
  end
353
422
 
354
423
  def rate_limit(**args)
355
- raise "`rate_limit` must be set inside a location block" if @parent.nil?
356
-
357
424
  @middleware[:rate_limit] = args
358
425
  end
359
426
 
360
427
  def cache_control(**args)
361
- raise "`cache_control` must be set inside a location block" if @parent.nil?
362
-
363
428
  @middleware[:cache_control] = args
364
429
  end
365
430
 
366
431
  def etag(**args)
367
- raise "`etag` must be set inside a location block" if @parent.nil?
368
-
369
432
  @middleware[:etag] = args
370
433
  end
371
434
 
372
435
  def intrusion_protection(**args)
373
- raise "`intrusion_protection` must be set inside a location block" if @parent.nil?
374
-
375
436
  args[:banned_url_patterns] = Array(args[:banned_url_patterns]).map do |pattern|
376
437
  if pattern.is_a?(Regexp)
377
438
  pattern.source
@@ -383,14 +444,10 @@ module Itsi
383
444
  end
384
445
 
385
446
  def cors(**args)
386
- raise "`cors` must be set inside a location block" if @parent.nil?
387
-
388
447
  @middleware[:cors] = args
389
448
  end
390
449
 
391
450
  def static_assets(**args)
392
- raise "`static_assets` must be set inside a location block" if @parent.nil?
393
-
394
451
  root_dir = args[:root_dir] || "."
395
452
 
396
453
  if !File.exist?(root_dir)
@@ -401,14 +458,20 @@ module Itsi
401
458
 
402
459
  args[:relative_path] = true unless args.key?(:relative_path)
403
460
 
404
- location(/(?<path_suffix>.*)/, extensions: args[:allowed_extensions] || []) do
461
+ args[:allowed_extensions] ||= []
462
+
463
+ if (args[:allowed_extensions].include?("html") || args[:allowed_extensions].include?(:html)) && args[:try_html_extension]
464
+ args[:allowed_extensions] << ""
465
+ end
466
+
467
+ args[:base_path] = "^(?<base_path>#{paths_from_parent}).*$"
468
+
469
+ location("*", extensions: args[:allowed_extensions]) do
405
470
  @middleware[:static_assets] = args
406
471
  end
407
472
  end
408
473
 
409
474
  def file_server(**args)
410
- raise "`file_server` must be set inside a location block" if @parent.nil?
411
-
412
475
  # Forward to static_assets for implementation
413
476
  puts "Note: file_server is an alias for static_assets"
414
477
  static_assets(**args)
@@ -421,7 +484,7 @@ module Itsi
421
484
  if route_options
422
485
  result << deep_stringify_keys(
423
486
  {
424
- route: Regexp.new("^#{route_options}$"),
487
+ route: Regexp.new("^#{route_options}/?$"),
425
488
  methods: @methods.any? ? @methods : nil,
426
489
  protocols: @protocols.any? ? @protocols : nil,
427
490
  hosts: @hosts.any? ? @hosts : nil,
@@ -443,22 +506,27 @@ module Itsi
443
506
  case seg
444
507
  when Regexp
445
508
  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
509
  else
457
- Regexp.escape(seg).gsub(%r{/$}, ".*")
510
+ parts = seg.split('/')
511
+ parts.map do |part|
512
+ case part
513
+ when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
514
+ param_name = Regexp.last_match(1)
515
+ custom = Regexp.last_match(2)
516
+ if custom && !custom.empty?
517
+ "(?<#{param_name}>#{custom})"
518
+ else
519
+ "(?<#{param_name}>[^/]+)"
520
+ end
521
+ when /\*/
522
+ part.gsub(/\*/, ".*")
523
+ else
524
+ Regexp.escape(part)
525
+ end
526
+ end.join("/")
458
527
  end
459
528
  end.join("|")
460
-
461
- if parent.paths_from_parent && parent.paths_from_parent != "(?:/.*)"
529
+ if parent && parent.paths_from_parent && parent.paths_from_parent != "(?:/)"
462
530
  "#{parent.paths_from_parent}#{route_or_str != "" ? "(?:#{route_or_str})" : ""}"
463
531
  else
464
532
  route_or_str = "/#{route_or_str}" unless route_or_str.start_with?("/")
@@ -475,12 +543,16 @@ module Itsi
475
543
  chain = []
476
544
  node = self
477
545
  while node
546
+ if node.middleware[:app]&.[](:preloader)
547
+ node.middleware[:app][:app_proc] = node.middleware[:app].delete(:preloader).call
548
+ end
478
549
  chain << node
479
550
  node = node.parent
480
551
  end
481
552
  chain.reverse!
482
553
 
483
554
  merged = {}
555
+
484
556
  chain.each do |n|
485
557
  n.middleware.each do |k, v|
486
558
  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
  }