itsi-server 0.1.1 → 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 (184) 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 +3937 -0
  5. data/Cargo.toml +7 -0
  6. data/README.md +4 -0
  7. data/Rakefile +8 -1
  8. data/_index.md +6 -0
  9. data/exe/itsi +141 -46
  10. data/ext/itsi_error/Cargo.toml +3 -0
  11. data/ext/itsi_error/src/lib.rs +98 -24
  12. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  13. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  14. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  15. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  23. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  24. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  25. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  26. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  27. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  28. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  29. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  30. data/ext/itsi_rb_helpers/src/lib.rs +140 -10
  31. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  32. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  33. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  34. 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
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  36. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  37. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  38. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  39. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  40. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  41. data/ext/itsi_scheduler/Cargo.toml +24 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  43. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  44. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  45. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  46. data/ext/itsi_scheduler/src/lib.rs +38 -0
  47. data/ext/itsi_server/Cargo.lock +2956 -0
  48. data/ext/itsi_server/Cargo.toml +72 -14
  49. data/ext/itsi_server/extconf.rb +1 -1
  50. data/ext/itsi_server/src/default_responses/html/401.html +68 -0
  51. data/ext/itsi_server/src/default_responses/html/403.html +68 -0
  52. data/ext/itsi_server/src/default_responses/html/404.html +68 -0
  53. data/ext/itsi_server/src/default_responses/html/413.html +71 -0
  54. data/ext/itsi_server/src/default_responses/html/429.html +68 -0
  55. data/ext/itsi_server/src/default_responses/html/500.html +71 -0
  56. data/ext/itsi_server/src/default_responses/html/502.html +71 -0
  57. data/ext/itsi_server/src/default_responses/html/503.html +68 -0
  58. data/ext/itsi_server/src/default_responses/html/504.html +69 -0
  59. data/ext/itsi_server/src/default_responses/html/index.html +238 -0
  60. data/ext/itsi_server/src/default_responses/json/401.json +6 -0
  61. data/ext/itsi_server/src/default_responses/json/403.json +6 -0
  62. data/ext/itsi_server/src/default_responses/json/404.json +6 -0
  63. data/ext/itsi_server/src/default_responses/json/413.json +6 -0
  64. data/ext/itsi_server/src/default_responses/json/429.json +6 -0
  65. data/ext/itsi_server/src/default_responses/json/500.json +6 -0
  66. data/ext/itsi_server/src/default_responses/json/502.json +6 -0
  67. data/ext/itsi_server/src/default_responses/json/503.json +6 -0
  68. data/ext/itsi_server/src/default_responses/json/504.json +6 -0
  69. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  70. data/ext/itsi_server/src/env.rs +43 -0
  71. data/ext/itsi_server/src/lib.rs +132 -40
  72. data/ext/itsi_server/src/prelude.rs +2 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  74. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
  75. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  76. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  77. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  78. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
  79. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  80. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  81. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  82. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  83. data/ext/itsi_server/src/server/binds/bind.rs +201 -0
  84. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  85. data/ext/itsi_server/src/server/binds/listener.rs +432 -0
  86. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  87. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  88. data/ext/itsi_server/src/server/binds/tls.rs +270 -0
  89. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  90. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  91. data/ext/itsi_server/src/server/io_stream.rs +105 -0
  92. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  112. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  113. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  114. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  115. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  116. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  117. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  118. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  119. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  120. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  121. data/ext/itsi_server/src/server/mod.rs +12 -5
  122. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  123. data/ext/itsi_server/src/server/request_job.rs +11 -0
  124. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
  125. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  126. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
  127. data/ext/itsi_server/src/server/signal.rs +76 -0
  128. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  129. data/ext/itsi_server/src/server/thread_worker.rs +475 -0
  130. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  131. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  132. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  133. data/ext/itsi_server/src/services/mod.rs +6 -0
  134. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  135. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  136. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  137. data/ext/itsi_tracing/Cargo.toml +5 -0
  138. data/ext/itsi_tracing/src/lib.rs +315 -7
  139. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  140. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  141. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  142. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  143. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  144. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  145. data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  146. data/lib/itsi/http_request.rb +186 -0
  147. data/lib/itsi/http_response.rb +41 -0
  148. data/lib/itsi/passfile.rb +109 -0
  149. data/lib/itsi/server/config/dsl.rb +565 -0
  150. data/lib/itsi/server/config.rb +166 -0
  151. data/lib/itsi/server/default_app/default_app.rb +34 -0
  152. data/lib/itsi/server/default_app/index.html +115 -0
  153. data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
  154. data/lib/itsi/server/default_config/Itsi.rb +107 -0
  155. data/lib/itsi/server/grpc/grpc_call.rb +246 -0
  156. data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  157. data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  158. data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  159. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  160. data/lib/itsi/server/rack_interface.rb +94 -0
  161. data/lib/itsi/server/route_tester.rb +107 -0
  162. data/lib/itsi/server/scheduler_interface.rb +21 -0
  163. data/lib/itsi/server/scheduler_mode.rb +10 -0
  164. data/lib/itsi/server/signal_trap.rb +29 -0
  165. data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  166. data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  167. data/lib/itsi/server/typed_handlers.rb +17 -0
  168. data/lib/itsi/server/version.rb +1 -1
  169. data/lib/itsi/server.rb +160 -9
  170. data/lib/itsi/standard_headers.rb +86 -0
  171. data/lib/ruby_lsp/itsi/addon.rb +111 -0
  172. data/lib/shell_completions/completions.rb +26 -0
  173. metadata +182 -25
  174. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  175. data/ext/itsi_server/src/request/mod.rs +0 -1
  176. data/ext/itsi_server/src/server/bind.rs +0 -138
  177. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  178. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  179. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  180. data/ext/itsi_server/src/server/listener.rs +0 -218
  181. data/ext/itsi_server/src/server/tls.rs +0 -138
  182. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  183. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  184. data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,109 @@
1
+ module Itsi
2
+ class Server
3
+
4
+ module Passfile
5
+ require 'io/console'
6
+
7
+ module_function
8
+
9
+ def load(filename)
10
+ if filename.nil? || filename.strip.empty?
11
+ puts "Error: a valid filename is required."
12
+ return nil
13
+ end
14
+
15
+ creds = {}
16
+ if File.exist?(filename)
17
+ File.foreach(filename) do |line|
18
+ line.chomp!
19
+ next if line.empty?
20
+
21
+ user, pass = line.split(':', 2)
22
+ creds[user] = pass
23
+ end
24
+ end
25
+ creds
26
+ end
27
+
28
+ def save(creds, filename)
29
+ File.open(filename, 'w', 0o600) do |f|
30
+ creds.each do |u, p|
31
+ f.puts "#{u}:#{p}"
32
+ end
33
+ end
34
+ end
35
+
36
+ def echo(filename, algorithm)
37
+ return unless (creds = load(filename))
38
+ print "Enter username: "
39
+ username = $stdin.gets.chomp
40
+
41
+ print "Enter password: "
42
+
43
+ password = $stdin.noecho(&:gets).chomp
44
+ puts
45
+
46
+ print "Confirm password: "
47
+ password_confirm = $stdin.noecho(&:gets).chomp
48
+ puts
49
+
50
+ if password != password_confirm
51
+ puts "Error: Passwords do not match!"
52
+ exit(1)
53
+ end
54
+
55
+ puts "#{username}:#{Itsi.create_password_hash(password, algorithm)}"
56
+ end
57
+
58
+ def add(filename, algorithm)
59
+ return unless (creds = load(filename))
60
+ print "Enter username: "
61
+ username = $stdin.gets.chomp
62
+
63
+ print "Enter password: "
64
+
65
+ password = $stdin.noecho(&:gets).chomp
66
+ puts
67
+
68
+ print "Confirm password: "
69
+ password_confirm = $stdin.noecho(&:gets).chomp
70
+ puts
71
+
72
+ if password != password_confirm
73
+ puts "Error: Passwords do not match!"
74
+ exit(1)
75
+ end
76
+
77
+ creds[username] = Itsi.create_password_hash(password, algorithm)
78
+
79
+ save(creds, filename)
80
+
81
+ puts "User '#{username}' added."
82
+ end
83
+
84
+ def remove(filename)
85
+ return unless (creds = load(filename))
86
+
87
+ print "Enter username to remove: "
88
+ username = $stdin.gets.chomp
89
+
90
+ if creds.key?(username)
91
+ creds.delete(username)
92
+ save(creds, filename)
93
+ puts "User '#{username}' removed."
94
+ else
95
+ puts "Warning: User '#{username}' not found."
96
+ end
97
+ end
98
+
99
+ def list(filename)
100
+ puts "Current credentials in '#{filename}':"
101
+ return unless (creds = load(filename))
102
+ creds.each do |u, p|
103
+ puts "#{u}:#{p}"
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,565 @@
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, &blk)
9
+ new(routes: ["/"]) do
10
+ if blk
11
+ instance_exec(&blk)
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
+ @routes = Array(routes).flatten
39
+ @methods = methods.map { |s| s.is_a?(Regexp) ? s : s.to_s }
40
+ @protocols = protocols.map { |s| s.is_a?(Regexp) ? s : s.to_s }
41
+ @hosts = hosts.map { |s| s.is_a?(Regexp) ? s : s.to_s }
42
+ @ports = ports.map { |s| s.is_a?(Regexp) ? s : s.to_s }
43
+ @extensions = extensions.map { |s| s.is_a?(Regexp) ? s : s.to_s }
44
+ @content_types = content_types.map { |s| s.is_a?(Regexp) ? s : s.to_s }
45
+ @accepts = accepts.map { |s| s.is_a?(Regexp) ? s : s.to_s }
46
+
47
+ @options = {
48
+ middleware_loaders: [],
49
+ middleware_loader: lambda do
50
+ @options[:middleware_loaders].each(&:call)
51
+ @middleware[:app] ||= {}
52
+ @middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
53
+ flatten_routes
54
+ end
55
+ }
56
+
57
+ instance_exec(&block)
58
+ end
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
+ @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
94
+ end
95
+
96
+ def get(route, app_proc = nil, nonblocking: false, &blk)
97
+ endpoint(route, [:get], app_proc, nonblocking: nonblocking, &blk)
98
+ end
99
+
100
+ def post(route, app_proc = nil, nonblocking: false, &blk)
101
+ endpoint(route, [:post], app_proc, nonblocking: nonblocking, &blk)
102
+ end
103
+
104
+ def put(route, app_proc = nil, nonblocking: false, &blk)
105
+ endpoint(route, [:put], app_proc, nonblocking: nonblocking, &blk)
106
+ end
107
+
108
+ def delete(route, app_proc = nil, nonblocking: false, &blk)
109
+ endpoint(route, [:delete], app_proc, nonblocking: nonblocking, &blk)
110
+ end
111
+
112
+ def patch(route, app_proc = nil, nonblocking: false, &blk)
113
+ endpoint(route, [:patch], app_proc, nonblocking: nonblocking, &blk)
114
+ end
115
+
116
+ def endpoint(route, methods=[], app_proc = nil, nonblocking: false, &blk)
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
+ 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
136
+
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 }
150
+ end
151
+ end
152
+
153
+ def grpc(*handlers, reflection: true, nonblocking: false, **, &blk)
154
+ if @middleware[:app] && @middleware[:app][:request_type].to_s != "grpc"
155
+ raise "App has already been set. You can use only one of `run` and `rackup_file` or `grpc` per location"
156
+ end
157
+
158
+ grpc_reflection(handlers) if reflection
159
+
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
165
+ end
166
+ end
167
+
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" }
177
+ end
178
+ end
179
+
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
183
+
184
+ def rackup_file(rackup_file, nonblocking: false, sendfile: true, path_info: "/")
185
+ raise "Rackup file #{rackup_file} doesn't exist" unless File.exist?(rackup_file)
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 }
187
+ end
188
+
189
+ def include(path)
190
+ code = IO.read("#{path}.rb")
191
+ instance_eval(code, "#{path}.rb", 1)
192
+ end
193
+
194
+ def bind(bind_str)
195
+ raise "Bind must be set at the root" unless @parent.nil?
196
+
197
+ @options[:binds] ||= []
198
+ @options[:binds] << bind_str.to_s
199
+ end
200
+
201
+ def after_fork(&block)
202
+ raise "After fork must be set at the root" unless @parent.nil?
203
+
204
+ @options[:hooks] ||= {}
205
+ @options[:hooks][:after_fork] = block
206
+ end
207
+
208
+ def before_fork(&block)
209
+ raise "Before fork must be set at the root" unless @parent.nil?
210
+
211
+ @options[:hooks] ||= {}
212
+ @options[:hooks][:before_fork] = block
213
+ end
214
+
215
+ def after_memory_threshold_reached(&block)
216
+ raise "Before fork must be set at the root" unless @parent.nil?
217
+
218
+ @options[:hooks] ||= {}
219
+ @options[:hooks][:after_memory_threshold_reached] = block
220
+ end
221
+
222
+ def worker_memory_limit(memory_limit)
223
+ raise "Worker memory limit must be set at the root" unless @parent.nil?
224
+
225
+ @options[:worker_memory_limit] = memory_limit
226
+ end
227
+
228
+ def multithreaded_reactor(multithreaded)
229
+ raise "Multithreaded reactor must be set at the root" unless @parent.nil?
230
+
231
+ @options[:multithreaded_reactor] = !!multithreaded
232
+ end
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
+
248
+ def watch(path, commands)
249
+ raise "Watch be set at the root" unless @parent.nil?
250
+
251
+ @options[:notify_watchers] ||= []
252
+ @options[:notify_watchers] << [path, commands]
253
+ end
254
+
255
+ def fiber_scheduler(klass_name = true)
256
+ raise "Fiber scheduler must be set at the root" unless @parent.nil?
257
+
258
+ klass_name = "Itsi::Scheduler" if klass_name == true
259
+ @options[:scheduler_class] = klass_name if klass_name
260
+ end
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
+
274
+ def preload(preload)
275
+ raise "Preload must be set at the root" unless @parent.nil?
276
+
277
+ @options[:preload] = preload
278
+ end
279
+
280
+ def shutdown_timeout(shutdown_timeout)
281
+ raise "Shutdown timeout must be set at the root" unless @parent.nil?
282
+
283
+ @options[:shutdown_timeout] = shutdown_timeout.to_f
284
+ end
285
+
286
+
287
+ def stream_body(stream_body)
288
+ raise "Stream body must be set at the root" unless @parent.nil?
289
+
290
+ @options[:stream_body] = !!stream_body
291
+ end
292
+
293
+ def location(*routes, methods: [], protocols: [], hosts: [], ports: [], extensions: [], content_types: [],
294
+ accepts: [], &block)
295
+ build_child = lambda {
296
+ @children << DSL.new(
297
+ self,
298
+ routes: routes,
299
+ methods: Array(methods) | self.methods,
300
+ protocols: Array(protocols) | self.protocols,
301
+ hosts: Array(hosts) | self.hosts,
302
+ ports: Array(ports) | self.ports,
303
+ extensions: Array(extensions) | self.extensions,
304
+ content_types: Array(content_types) | self.content_types,
305
+ accepts: Array(accepts) | self.accepts,
306
+ controller: @controller,
307
+ &block
308
+ )
309
+ }
310
+ if @parent.nil?
311
+ @options[:middleware_loaders] << build_child
312
+ else
313
+ build_child[]
314
+ end
315
+ end
316
+
317
+ def log_requests(**args)
318
+ @middleware[:log_requests] = args
319
+ end
320
+
321
+ def allow_list(**args)
322
+ args[:allowed_patterns] = Array(args[:allowed_patterns]).map do |pattern|
323
+ if pattern.is_a?(Regexp)
324
+ pattern.source
325
+ else
326
+ pattern
327
+ end
328
+ end
329
+ @middleware[:allow_list] = args
330
+ end
331
+
332
+ def deny_list(**args)
333
+ args[:denied_patterns] = Array(args[:denied_patterns]).map do |pattern|
334
+ if pattern.is_a?(Regexp)
335
+ pattern.source
336
+ else
337
+ pattern
338
+ end
339
+ end
340
+ @middleware[:deny_list] = args
341
+ end
342
+
343
+ def controller(controller)
344
+ @controller = controller
345
+ end
346
+
347
+ def auth_basic(**args)
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
356
+
357
+ @middleware[:auth_basic] = args
358
+ end
359
+
360
+ def redirect(**args)
361
+ @middleware[:redirect] = args
362
+ end
363
+
364
+ def proxy(**args)
365
+ @middleware[:proxy] = args
366
+ end
367
+
368
+ def static_response(**args)
369
+ args[:body] = args[:body].bytes
370
+ @middleware[:static_response] = args
371
+ end
372
+
373
+ def auth_jwt(**args)
374
+ @middleware[:auth_jwt] = args
375
+ end
376
+
377
+ def auth_api_key(**args)
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
390
+
391
+ @middleware[:auth_api_key] = args
392
+ end
393
+
394
+ def compress(**args)
395
+ @middleware[:compression] = args
396
+ end
397
+
398
+ def request_headers(**args)
399
+ @middleware[:request_headers] = args
400
+ end
401
+
402
+ def max_body(**args)
403
+ @middleware[:max_body] = args
404
+ end
405
+
406
+ def response_headers(**args)
407
+ @middleware[:response_headers] = args
408
+ end
409
+
410
+ def rate_limit(**args)
411
+ @middleware[:rate_limit] = args
412
+ end
413
+
414
+ def cache_control(**args)
415
+ @middleware[:cache_control] = args
416
+ end
417
+
418
+ def etag(**args)
419
+ @middleware[:etag] = args
420
+ end
421
+
422
+ def intrusion_protection(**args)
423
+ args[:banned_url_patterns] = Array(args[:banned_url_patterns]).map do |pattern|
424
+ if pattern.is_a?(Regexp)
425
+ pattern.source
426
+ else
427
+ pattern
428
+ end
429
+ end
430
+ @middleware[:intrusion_protection] = args
431
+ end
432
+
433
+ def cors(**args)
434
+ @middleware[:cors] = args
435
+ end
436
+
437
+ def static_assets(**args)
438
+ root_dir = args[:root_dir] || "."
439
+
440
+ if !File.exist?(root_dir)
441
+ warn "Warning: static_assets root_dir '#{root_dir}' does not exist!"
442
+ elsif !File.directory?(root_dir)
443
+ warn "Warning: static_assets root_dir '#{root_dir}' is not a directory!"
444
+ end
445
+
446
+ args[:relative_path] = true unless args.key?(:relative_path)
447
+
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
457
+ @middleware[:static_assets] = args
458
+ end
459
+ end
460
+
461
+ def file_server(**args)
462
+ # Forward to static_assets for implementation
463
+ puts "Note: file_server is an alias for static_assets"
464
+ static_assets(**args)
465
+ end
466
+
467
+ def flatten_routes
468
+ result = []
469
+ result.concat(@children.flat_map(&:flatten_routes))
470
+ route_options = paths_from_parent
471
+ if route_options
472
+ result << deep_stringify_keys(
473
+ {
474
+ route: Regexp.new("^#{route_options}/?$"),
475
+ methods: @methods.any? ? @methods : nil,
476
+ protocols: @protocols.any? ? @protocols : nil,
477
+ hosts: @hosts.any? ? @hosts : nil,
478
+ ports: @ports.any? ? @ports : nil,
479
+ extensions: @extensions.any? ? @extensions : nil,
480
+ content_types: @content_types.any? ? @content_types : nil,
481
+ accepts: @accepts.any? ? @accepts : nil,
482
+ middleware: effective_middleware
483
+ }
484
+ )
485
+ end
486
+ result
487
+ end
488
+
489
+ def paths_from_parent
490
+ return nil unless @routes.any?
491
+
492
+ route_or_str = @routes.map do |seg|
493
+ case seg
494
+ when Regexp
495
+ seg.source
496
+ else
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("/")
514
+ end
515
+ end.join("|")
516
+ if parent && parent.paths_from_parent && parent.paths_from_parent != "(?:/)"
517
+ "#{parent.paths_from_parent}#{route_or_str != "" ? "(?:#{route_or_str})" : ""}"
518
+ else
519
+ route_or_str = "/#{route_or_str}" unless route_or_str.start_with?("/")
520
+ "(?:#{route_or_str})"
521
+ end
522
+ end
523
+
524
+ def effective_middleware
525
+ merged = merge_ancestor_middleware
526
+ merged.map { |k, v| { type: k.to_s, parameters: v } }
527
+ end
528
+
529
+ def merge_ancestor_middleware
530
+ chain = []
531
+ node = self
532
+ while node
533
+ if node.middleware[:app]&.[](:preloader)
534
+ node.middleware[:app][:app_proc] = node.middleware[:app].delete(:preloader).call
535
+ end
536
+ chain << node
537
+ node = node.parent
538
+ end
539
+ chain.reverse!
540
+
541
+ merged = {}
542
+
543
+ chain.each do |n|
544
+ n.middleware.each do |k, v|
545
+ merged[k] = v
546
+ end
547
+ end
548
+ merged
549
+ end
550
+
551
+ def deep_stringify_keys(obj)
552
+ case obj
553
+ when Hash
554
+ obj.transform_keys!(&:to_s)
555
+ obj.transform_values! { |v| deep_stringify_keys(v) }
556
+ when Array
557
+ obj.map { |v| deep_stringify_keys(v) }
558
+ else
559
+ obj
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end
565
+ end