itsi-server 0.1.1 → 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.

Potentially problematic release.


This version of itsi-server might be problematic. Click here for more details.

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 +3776 -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 +75 -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 +187 -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 +578 -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,578 @@
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=nil, 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
+ app = { preloader: -> { app_proc }, nonblocking: nonblocking }
150
+ @middleware[:app] = app
151
+ location("*") do
152
+ @middleware[:app] = app
153
+ end
154
+ end
155
+ end
156
+
157
+ def grpc(*handlers, reflection: true, nonblocking: false, **, &blk)
158
+ if @middleware[:app] && @middleware[:app][:request_type].to_s != "grpc"
159
+ raise "App has already been set. You can use only one of `run` and `rackup_file` or `grpc` per location"
160
+ end
161
+
162
+ grpc_reflection(handlers) if reflection
163
+
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
169
+ end
170
+ end
171
+
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" }
181
+ end
182
+ end
183
+
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
190
+ end
191
+ end
192
+
193
+ def rackup_file(rackup_file, nonblocking: false, sendfile: true, path_info: "/")
194
+ raise "Rackup file #{rackup_file} doesn't exist" unless File.exist?(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
199
+ end
200
+ end
201
+
202
+ def include(path)
203
+ code = IO.read("#{path}.rb")
204
+ instance_eval(code, "#{path}.rb", 1)
205
+ end
206
+
207
+ def bind(bind_str)
208
+ raise "Bind must be set at the root" unless @parent.nil?
209
+
210
+ @options[:binds] ||= []
211
+ @options[:binds] << bind_str.to_s
212
+ end
213
+
214
+ def after_fork(&block)
215
+ raise "After fork must be set at the root" unless @parent.nil?
216
+
217
+ @options[:hooks] ||= {}
218
+ @options[:hooks][:after_fork] = block
219
+ end
220
+
221
+ def before_fork(&block)
222
+ raise "Before fork must be set at the root" unless @parent.nil?
223
+
224
+ @options[:hooks] ||= {}
225
+ @options[:hooks][:before_fork] = block
226
+ end
227
+
228
+ def after_memory_threshold_reached(&block)
229
+ raise "Before fork must be set at the root" unless @parent.nil?
230
+
231
+ @options[:hooks] ||= {}
232
+ @options[:hooks][:after_memory_threshold_reached] = block
233
+ end
234
+
235
+ def worker_memory_limit(memory_limit)
236
+ raise "Worker memory limit must be set at the root" unless @parent.nil?
237
+
238
+ @options[:worker_memory_limit] = memory_limit
239
+ end
240
+
241
+ def multithreaded_reactor(multithreaded)
242
+ raise "Multithreaded reactor must be set at the root" unless @parent.nil?
243
+
244
+ @options[:multithreaded_reactor] = !!multithreaded
245
+ end
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
+
261
+ def watch(path, commands)
262
+ raise "Watch be set at the root" unless @parent.nil?
263
+
264
+ @options[:notify_watchers] ||= []
265
+ @options[:notify_watchers] << [path, commands]
266
+ end
267
+
268
+ def fiber_scheduler(klass_name = true)
269
+ raise "Fiber scheduler must be set at the root" unless @parent.nil?
270
+
271
+ klass_name = "Itsi::Scheduler" if klass_name == true
272
+ @options[:scheduler_class] = klass_name if klass_name
273
+ end
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
+
287
+ def preload(preload)
288
+ raise "Preload must be set at the root" unless @parent.nil?
289
+
290
+ @options[:preload] = preload
291
+ end
292
+
293
+ def shutdown_timeout(shutdown_timeout)
294
+ raise "Shutdown timeout must be set at the root" unless @parent.nil?
295
+
296
+ @options[:shutdown_timeout] = shutdown_timeout.to_f
297
+ end
298
+
299
+
300
+ def stream_body(stream_body)
301
+ raise "Stream body must be set at the root" unless @parent.nil?
302
+
303
+ @options[:stream_body] = !!stream_body
304
+ end
305
+
306
+ def location(*routes, methods: [], protocols: [], hosts: [], ports: [], extensions: [], content_types: [],
307
+ accepts: [], &block)
308
+ build_child = lambda {
309
+ @children << DSL.new(
310
+ self,
311
+ routes: routes,
312
+ methods: Array(methods) | self.methods,
313
+ protocols: Array(protocols) | self.protocols,
314
+ hosts: Array(hosts) | self.hosts,
315
+ ports: Array(ports) | self.ports,
316
+ extensions: Array(extensions) | self.extensions,
317
+ content_types: Array(content_types) | self.content_types,
318
+ accepts: Array(accepts) | self.accepts,
319
+ controller: @controller,
320
+ &block
321
+ )
322
+ }
323
+ if @parent.nil?
324
+ @options[:middleware_loaders] << build_child
325
+ else
326
+ build_child[]
327
+ end
328
+ end
329
+
330
+ def log_requests(**args)
331
+ @middleware[:log_requests] = args
332
+ end
333
+
334
+ def allow_list(**args)
335
+ args[:allowed_patterns] = Array(args[:allowed_patterns]).map do |pattern|
336
+ if pattern.is_a?(Regexp)
337
+ pattern.source
338
+ else
339
+ pattern
340
+ end
341
+ end
342
+ @middleware[:allow_list] = args
343
+ end
344
+
345
+ def deny_list(**args)
346
+ args[:denied_patterns] = Array(args[:denied_patterns]).map do |pattern|
347
+ if pattern.is_a?(Regexp)
348
+ pattern.source
349
+ else
350
+ pattern
351
+ end
352
+ end
353
+ @middleware[:deny_list] = args
354
+ end
355
+
356
+ def controller(controller)
357
+ @controller = controller
358
+ end
359
+
360
+ def auth_basic(**args)
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
369
+
370
+ @middleware[:auth_basic] = args
371
+ end
372
+
373
+ def redirect(**args)
374
+ @middleware[:redirect] = args
375
+ end
376
+
377
+ def proxy(**args)
378
+ @middleware[:proxy] = args
379
+ end
380
+
381
+ def static_response(**args)
382
+ args[:body] = args[:body].bytes
383
+ @middleware[:static_response] = args
384
+ end
385
+
386
+ def auth_jwt(**args)
387
+ @middleware[:auth_jwt] = args
388
+ end
389
+
390
+ def auth_api_key(**args)
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
403
+
404
+ @middleware[:auth_api_key] = args
405
+ end
406
+
407
+ def compress(**args)
408
+ @middleware[:compression] = args
409
+ end
410
+
411
+ def request_headers(**args)
412
+ @middleware[:request_headers] = args
413
+ end
414
+
415
+ def max_body(**args)
416
+ @middleware[:max_body] = args
417
+ end
418
+
419
+ def response_headers(**args)
420
+ @middleware[:response_headers] = args
421
+ end
422
+
423
+ def rate_limit(**args)
424
+ @middleware[:rate_limit] = args
425
+ end
426
+
427
+ def cache_control(**args)
428
+ @middleware[:cache_control] = args
429
+ end
430
+
431
+ def etag(**args)
432
+ @middleware[:etag] = args
433
+ end
434
+
435
+ def intrusion_protection(**args)
436
+ args[:banned_url_patterns] = Array(args[:banned_url_patterns]).map do |pattern|
437
+ if pattern.is_a?(Regexp)
438
+ pattern.source
439
+ else
440
+ pattern
441
+ end
442
+ end
443
+ @middleware[:intrusion_protection] = args
444
+ end
445
+
446
+ def cors(**args)
447
+ @middleware[:cors] = args
448
+ end
449
+
450
+ def static_assets(**args)
451
+ root_dir = args[:root_dir] || "."
452
+
453
+ if !File.exist?(root_dir)
454
+ warn "Warning: static_assets root_dir '#{root_dir}' does not exist!"
455
+ elsif !File.directory?(root_dir)
456
+ warn "Warning: static_assets root_dir '#{root_dir}' is not a directory!"
457
+ end
458
+
459
+ args[:relative_path] = true unless args.key?(:relative_path)
460
+
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
470
+ @middleware[:static_assets] = args
471
+ end
472
+ end
473
+
474
+ def file_server(**args)
475
+ # Forward to static_assets for implementation
476
+ puts "Note: file_server is an alias for static_assets"
477
+ static_assets(**args)
478
+ end
479
+
480
+ def flatten_routes
481
+ result = []
482
+ result.concat(@children.flat_map(&:flatten_routes))
483
+ route_options = paths_from_parent
484
+ if route_options
485
+ result << deep_stringify_keys(
486
+ {
487
+ route: Regexp.new("^#{route_options}/?$"),
488
+ methods: @methods.any? ? @methods : nil,
489
+ protocols: @protocols.any? ? @protocols : nil,
490
+ hosts: @hosts.any? ? @hosts : nil,
491
+ ports: @ports.any? ? @ports : nil,
492
+ extensions: @extensions.any? ? @extensions : nil,
493
+ content_types: @content_types.any? ? @content_types : nil,
494
+ accepts: @accepts.any? ? @accepts : nil,
495
+ middleware: effective_middleware
496
+ }
497
+ )
498
+ end
499
+ result
500
+ end
501
+
502
+ def paths_from_parent
503
+ return nil unless @routes.any?
504
+
505
+ route_or_str = @routes.map do |seg|
506
+ case seg
507
+ when Regexp
508
+ seg.source
509
+ else
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("/")
527
+ end
528
+ end.join("|")
529
+ if parent && parent.paths_from_parent && parent.paths_from_parent != "(?:/)"
530
+ "#{parent.paths_from_parent}#{route_or_str != "" ? "(?:#{route_or_str})" : ""}"
531
+ else
532
+ route_or_str = "/#{route_or_str}" unless route_or_str.start_with?("/")
533
+ "(?:#{route_or_str})"
534
+ end
535
+ end
536
+
537
+ def effective_middleware
538
+ merged = merge_ancestor_middleware
539
+ merged.map { |k, v| { type: k.to_s, parameters: v } }
540
+ end
541
+
542
+ def merge_ancestor_middleware
543
+ chain = []
544
+ node = self
545
+ while node
546
+ if node.middleware[:app]&.[](:preloader)
547
+ node.middleware[:app][:app_proc] = node.middleware[:app].delete(:preloader).call
548
+ end
549
+ chain << node
550
+ node = node.parent
551
+ end
552
+ chain.reverse!
553
+
554
+ merged = {}
555
+
556
+ chain.each do |n|
557
+ n.middleware.each do |k, v|
558
+ merged[k] = v
559
+ end
560
+ end
561
+ merged
562
+ end
563
+
564
+ def deep_stringify_keys(obj)
565
+ case obj
566
+ when Hash
567
+ obj.transform_keys!(&:to_s)
568
+ obj.transform_values! { |v| deep_stringify_keys(v) }
569
+ when Array
570
+ obj.map { |v| deep_stringify_keys(v) }
571
+ else
572
+ obj
573
+ end
574
+ end
575
+ end
576
+ end
577
+ end
578
+ end