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,107 @@
1
+ module Itsi
2
+ class Server
3
+ module RouteTester
4
+
5
+ require "set"
6
+ require "strscan"
7
+
8
+ def format_mw(mw)
9
+ case mw["type"]
10
+ when "app"
11
+ "app #{mw["parameters"]["app_proc"].inspect.split(" ")[1]}"
12
+ else
13
+ mw["type"]
14
+ end
15
+ end
16
+
17
+ def print_route(route_str, stack)
18
+ filters = %w[methods ports protocols extensions].map do |key|
19
+ val = stack[key]
20
+ val ? "#{key}: #{Array(val).join(",")}" : nil
21
+ end.compact
22
+ filter_str = filters.any? ? filters.join(", ") : "(none)"
23
+
24
+ middlewares = stack["middleware"]
25
+
26
+ puts "─" * 76
27
+ puts "Route: #{route_str}"
28
+ puts "Conditions: #{filter_str}"
29
+ puts "Middleware: • #{format_mw(middlewares.first)}"
30
+ middlewares[1..].each do |mw|
31
+ puts " • #{format_mw(mw)}"
32
+ end
33
+ end
34
+
35
+ def explode_route_pattern(pattern)
36
+ pattern = pattern.gsub(/^\^|\$$/, "")
37
+ pattern = pattern.gsub("\\", "")
38
+ tokens = parse_expression(StringScanner.new(pattern))
39
+ expand_tokens(tokens)
40
+ end
41
+
42
+ # Parses the expression into a nested tree of tokens
43
+ def parse_expression(scanner)
44
+ tokens = []
45
+ buffer = ""
46
+
47
+ until scanner.eos?
48
+ if scanner.scan(/\(\?:/)
49
+ tokens << buffer unless buffer.empty?
50
+ buffer = ""
51
+ tokens << parse_alternation(scanner)
52
+ elsif scanner.peek(1) == ")"
53
+ scanner.getch # consume ')'
54
+ break
55
+ else
56
+ buffer << scanner.getch
57
+ end
58
+ end
59
+
60
+ tokens << buffer unless buffer.empty?
61
+ tokens
62
+ end
63
+
64
+ # Parses inside a non-capturing group (?:A|B|C)
65
+ def parse_alternation(scanner)
66
+ options = []
67
+ current = []
68
+
69
+ until scanner.eos?
70
+ if scanner.scan(/\(\?:/)
71
+ current << parse_alternation(scanner)
72
+ elsif scanner.peek(1) == ")"
73
+ scanner.getch # consume ')'
74
+ break
75
+ elsif scanner.peek(1) == "|"
76
+ scanner.getch # consume '|'
77
+ options << current
78
+ current = []
79
+ else
80
+ current << scanner.getch
81
+ end
82
+ end
83
+
84
+ options << current
85
+ { alt: options }
86
+ end
87
+
88
+ def expand_tokens(tokens)
89
+ parts = tokens.map do |token|
90
+ if token.is_a?(String)
91
+ [token]
92
+ elsif token.is_a?(Hash) && token[:alt]
93
+ # Recurse into each branch of the alternation
94
+ token[:alt].map { |branch| expand_tokens(branch) }.flatten
95
+ else
96
+ raise "Unexpected token: #{token.inspect}"
97
+ end
98
+ end
99
+
100
+ # Cartesian product of all parts
101
+ parts.inject([""]) do |acc, part|
102
+ acc.product(part).map { |a, b| a + b }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,21 @@
1
+ module Itsi
2
+ class Server
3
+ module SchedulerInterface
4
+ # Simple wrapper to instantiate a scheduler, start it,
5
+ # and immediate have it invoke a scheduler proc
6
+ def start_scheduler_loop(scheduler_class, scheduler_task)
7
+ scheduler = scheduler_class.new
8
+ Fiber.set_scheduler(scheduler)
9
+ [scheduler, Fiber.schedule(&scheduler_task)]
10
+ end
11
+
12
+ # When running in scheduler mode,
13
+ # each request is wrapped in a Fiber.
14
+ def schedule(app, request)
15
+ Fiber.schedule do
16
+ app.call(request)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # Running with a Fiber scheduler enabled but with an ActiveSupport isolation_level set to Thread
2
+ # can be dangerous. A thread isolation level means that all fibers sharing a thread can content
3
+ # for the same resources, which can lead to race conditions.
4
+ # This hook should *only* be disabled if you know there are no such shared resources.
5
+ if defined?(ActiveSupport::IsolatedExecutionState) && !ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
6
+ Itsi.log_info \
7
+ "ActiveSupport Isolated Execution state detected. Automatically switching to :fiber mode. "\
8
+ "Set ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL to disable this behavior"
9
+ ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
10
+ end
@@ -0,0 +1,29 @@
1
+ module Itsi
2
+ # This trap is necessary for debuggers and similar which intercept certain signals
3
+ # then attempt to restore these to the previous signal when finished.
4
+ # If the previous signal handler was registered in native code, this restoration doesn't
5
+ # work as expected and the native signal handler is lost.
6
+ # We intercept restored signals here and reinstate the Itsi server signal handlers
7
+ # (if the server is still running).
8
+ module SignalTrap
9
+ DEFAULT_SIGNALS = ["DEFAULT", "", nil].freeze
10
+ INTERCEPTED_SIGNALS = ["INT"].freeze
11
+
12
+ def trap(signal, *args, &block)
13
+ unless INTERCEPTED_SIGNALS.include?(signal.to_s) && block.nil? && Itsi::Server.running?
14
+ return super(signal, *args, &block)
15
+ end
16
+
17
+ Itsi::Server.reset_signal_handlers
18
+ nil
19
+ end
20
+ end
21
+ end
22
+
23
+ [Kernel, Signal].each do |receiver|
24
+ receiver.singleton_class.prepend(Itsi::SignalTrap)
25
+ end
26
+
27
+ [Object].each do |receiver|
28
+ receiver.include(Itsi::SignalTrap)
29
+ end
@@ -0,0 +1,200 @@
1
+ module Itsi
2
+ class Server
3
+ module TypedHandlers
4
+ module ParamParser
5
+ require 'date'
6
+
7
+ class ValidationError < StandardError
8
+ attr_reader :errors
9
+ def initialize(errors)
10
+ @errors = errors
11
+ super("Validation failed: #{errors.join('; ')}")
12
+ end
13
+ end
14
+
15
+ # Conversion map for primitive/base type conversions.
16
+ CONVERSION_MAP = {
17
+ String => ->(v){ v.to_s },
18
+ Symbol => ->(v){ v.to_sym },
19
+ Integer => ->(v){ Integer(v) },
20
+ Float => ->(v){ Float(v) },
21
+ :Number => ->(v){ Float(v) },
22
+ TrueClass => ->(v){
23
+ case v
24
+ when true, 'true', '1', 1 then true
25
+ when false, 'false', '0', 0 then false
26
+ else raise "Cannot cast #{v.inspect} to Boolean"
27
+ end
28
+ },
29
+ FalseClass => ->(v){
30
+ case v
31
+ when true, 'true', '1', 1 then true
32
+ when false, 'false', '0', 0 then false
33
+ else raise "Cannot cast #{v.inspect} to Boolean"
34
+ end
35
+ },
36
+ :Boolean => ->(v){
37
+ case v
38
+ when true, 'true', '1', 1 then true
39
+ when false, 'false', '0', 0 then false
40
+ else raise "Cannot cast #{v.inspect} to Boolean"
41
+ end
42
+ },
43
+ Date => ->(v){ Date.parse(v.to_s) },
44
+ Time => ->(v){ Time.parse(v.to_s) },
45
+ DateTime => ->(v){ DateTime.parse(v.to_s) }
46
+ }.compare_by_identity
47
+
48
+ # Extracts the expected type and required flag from a schema definition.
49
+ def extract_schema(schema_def)
50
+ if schema_def.is_a?(Hash) && schema_def.key?(:_type)
51
+ expected_type = schema_def[:_type]
52
+ required = schema_def.fetch(:required, true)
53
+ [expected_type, required]
54
+ else
55
+ [schema_def, true]
56
+ end
57
+ end
58
+
59
+ # Preprocess the schema into fixed keys (as symbols) and regex keys.
60
+ # Memoizes the result based on the schema.
61
+ def processed_schema(schema)
62
+ @@schema_cache ||= {}
63
+ @@schema_cache[schema] ||= begin
64
+ fixed = {}
65
+ regex = []
66
+ schema.each do |k, schema_def|
67
+ expected_type, required = extract_schema(schema_def)
68
+ if k.is_a?(Regexp)
69
+ regex << [k, [expected_type, required]]
70
+ else
71
+ fixed[k.to_sym] = [expected_type, required]
72
+ end
73
+ end
74
+ [fixed, regex]
75
+ end
76
+ end
77
+
78
+ # Helper that converts an array of path segments into a string.
79
+ # For example, [:user, "addresses", 0, :street] becomes "user.addresses[0].street".
80
+ def format_path(path)
81
+ result = "".dup
82
+ path.each do |seg|
83
+ if seg.is_a?(Integer)
84
+ result << "[#{seg}]"
85
+ else
86
+ result << (result.empty? ? seg.to_s : ".#{seg}")
87
+ end
88
+ end
89
+ result
90
+ end
91
+
92
+ # In-place casts the value at container[key] according to expected_type.
93
+ # On success, updates container[key] and returns nil.
94
+ # On failure, returns an error message string that uses the formatted path.
95
+ def cast_value!(container, key, expected_type, path)
96
+ if expected_type.is_a?(Array)
97
+ # Only allow homogeneous array types.
98
+ return "Only homogeneous array types are supported at #{format_path(path)}" if expected_type.size != 1
99
+
100
+ # Expect container[key] to be an Array; process each element in place.
101
+ unless container[key].is_a?(Array)
102
+ return "Expected an Array at #{format_path(path)}, got #{container[key].class}"
103
+ end
104
+ container[key].each_with_index do |_, idx|
105
+ err = cast_value!(container[key], idx, expected_type.first, path + [idx])
106
+ return err if err
107
+ end
108
+ return nil
109
+
110
+ elsif expected_type.is_a?(Hash)
111
+ # Nested schema: expect container[key] to be a Hash; process it in place.
112
+ unless container[key].is_a?(Hash)
113
+ return "Expected a Hash at #{format_path(path)}, got #{container[key].class}"
114
+ end
115
+ begin
116
+ apply_schema!(container[key], expected_type, path)
117
+ return nil
118
+ rescue ValidationError => ve
119
+ return ve.errors.join('; ')
120
+ end
121
+
122
+ else
123
+ converter = CONVERSION_MAP[expected_type]
124
+ if converter
125
+ begin
126
+ container[key] = converter.call(container[key])
127
+ return nil
128
+ rescue => e
129
+ return "Invalid value for #{expected_type} at #{format_path(path)}: #{container[key].inspect} (#{e.message})"
130
+ end
131
+ end
132
+
133
+ # Fallbacks.
134
+ if expected_type == Array
135
+ unless container[key].is_a?(Array)
136
+ return "Expected Array at #{format_path(path)}, got #{container[key].class}"
137
+ end
138
+ return nil
139
+ elsif expected_type == Hash
140
+ unless container[key].is_a?(Hash)
141
+ return "Expected Hash at #{format_path(path)}, got #{container[key].class}"
142
+ end
143
+ return nil
144
+ elsif expected_type == File && container[key].is_a?(Hash) && container[key][:tempfile].is_a?(Tempfile)
145
+ return nil
146
+ else
147
+ return "Unsupported type: #{expected_type.inspect} at #{format_path(path)}"
148
+ end
149
+ end
150
+ end
151
+
152
+ # Applies the schema in place to the given params hash.
153
+ # Fixed keys are converted to symbols, and regex-matched keys remain as strings.
154
+ # The current location in the params is tracked as an array of path segments.
155
+ def apply_schema!(params, schema, path = [])
156
+ errors = []
157
+ processed = processed_schema(schema)
158
+ fixed_schema = processed[0]
159
+ regex_schema = processed[1]
160
+
161
+ # Process fixed keys.
162
+ fixed_schema.each do |fixed_key, (expected_type, required)|
163
+ new_path = path + [fixed_key]
164
+ if params.key?(fixed_key)
165
+ # Symbol key present.
166
+ elsif params.key?(fixed_key.to_s)
167
+ params[fixed_key] = params.delete(fixed_key.to_s)
168
+ else
169
+ if required
170
+ errors << "Missing required key: #{format_path(new_path)}"
171
+ else
172
+ params[fixed_key] = nil
173
+ end
174
+ next
175
+ end
176
+
177
+ err = cast_value!(params, fixed_key, expected_type, new_path)
178
+ errors << err if err
179
+ end
180
+
181
+ # Process regex keys (only string keys not already handled as fixed keys).
182
+ params.keys.select { |k| k.is_a?(String) }.each do |key|
183
+ next if fixed_schema.has_key?(key.to_sym) || fixed_schema.has_key?(key)
184
+ regex_schema.each do |regex, (expected_type, _required)|
185
+ if regex.match(key)
186
+ new_path = path + [key]
187
+ err = cast_value!(params, key, expected_type, new_path)
188
+ errors << err if err
189
+ break # only use the first matching regex
190
+ end
191
+ end
192
+ end
193
+
194
+ raise ValidationError.new(errors) unless errors.empty?
195
+ params
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,55 @@
1
+ module Itsi
2
+ class Server
3
+ module TypedHandlers
4
+ module SourceParser
5
+ require 'prism'
6
+
7
+
8
+ def self.extract_expr_from_source_location(proc)
9
+ source_lines = IO.readlines(proc.source_location.first)
10
+
11
+ proc_line = source_location.last - 1
12
+ first_line = source_lines[proc_line]
13
+ binding.b
14
+ until first_line =~ /(?:lambda|proc|->|def|.*?do\s*\|)/ || proc_line.zero?
15
+ proc_line -= 1
16
+ first_line = source_lines[proc_line]
17
+ end
18
+ lines = source_lines[proc_line..]
19
+ lines[0] = lines[0][/(?:lambda|proc|->|def|.*?do\s*\|).*/]
20
+ src_str = lines.first << "\n"
21
+ intermediate = Prism.parse(src_str)
22
+
23
+ lines[1..-1].each do |line|
24
+ break if intermediate.success?
25
+ token_count = 0
26
+ line.split(/(?=\s|;|\)|\})/).each do |token|
27
+ src_str << token
28
+ token_count += 1
29
+ intermediate = Prism.parse(src_str)
30
+ next unless intermediate.success? && token_count > 1
31
+ break
32
+ end
33
+ end
34
+
35
+ raise 'Source Extraction Failed' unless intermediate.success?
36
+
37
+ src = intermediate.value.statements.body.first.yield_self do |s|
38
+ s.type == :call_node ? s.block : s
39
+ end
40
+ params = src.parameters
41
+ params = params.parameters if params.respond_to?(:parameters)
42
+ requireds = (params&.requireds || []).map(&:name)
43
+ optionals = params&.optionals || []
44
+ keywords = (params&.keywords || []).map do |kw|
45
+ [kw.name, kw.value.slice.gsub(/^_\./, '$.')]
46
+ end.to_h
47
+
48
+ [requireds.length, keywords]
49
+ rescue
50
+ [ proc.parameters.first&.length || 0, {}]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "typed_handlers/source_parser"
2
+ require_relative "typed_handlers/param_parser"
3
+
4
+ module Itsi
5
+ class Server
6
+ module TypedHandlers
7
+ def self.handler_for(proc, input_schema)
8
+ input_schema = proc.binding.eval(input_schema) if input_schema
9
+ lambda do |req|
10
+ req.params(input_schema) do |params|
11
+ proc.call(req, params: params)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.18"
6
6
  end
7
7
  end
data/lib/itsi/server.rb CHANGED
@@ -2,20 +2,171 @@
2
2
 
3
3
  require_relative "server/version"
4
4
  require_relative "server/itsi_server"
5
+ require_relative "server/rack_interface"
6
+ require_relative "server/grpc/grpc_interface"
7
+ require_relative "server/grpc/grpc_call"
8
+ require_relative "server/scheduler_interface"
9
+ require_relative "server/signal_trap"
10
+ require_relative "server/route_tester"
11
+ require_relative "server/rack/handler/itsi"
12
+ require_relative "server/config"
13
+ require_relative "server/typed_handlers"
14
+ require_relative "standard_headers"
15
+ require_relative "http_request"
16
+ require_relative "http_response"
17
+ require_relative "passfile"
18
+ require_relative "../shell_completions/completions"
5
19
 
6
20
  module Itsi
7
21
  class Server
8
- # Call our Rack app with our request ENV.
9
- def self.call(app, request)
10
- app.call(request.to_env)
11
- end
22
+ extend RackInterface
23
+ extend SchedulerInterface
24
+ extend RouteTester
25
+
26
+ class << self
27
+
28
+ def running?
29
+ !!@running
30
+ end
31
+
32
+ def start_in_background_thread(cli_params = {}, &blk)
33
+ @background_thread = start(cli_params, background: true, &blk)
34
+ end
35
+
36
+ def start(cli_params, background: false, &blk)
37
+ itsi_file = Itsi::Server::Config.config_file_path(cli_params[:config_file])
38
+ server = new(cli_params, itsi_file, blk)
39
+ previous_handler = Signal.trap(:INT, :DEFAULT)
40
+ run = lambda do
41
+ write_pid
42
+ @running = server
43
+ server.start
44
+ @running = false
45
+ Signal.trap(:INT, previous_handler)
46
+ server
47
+ end
48
+ background ? Thread.new(&run) : run[]
49
+ end
50
+
51
+ def static(cli_params)
52
+ start(cli_params.merge(static: true))
53
+ end
54
+
55
+ def stop
56
+ return unless pid = get_pid
57
+ Process.kill(:INT, pid)
58
+ end
12
59
 
13
- # If scheduler is enabled
14
- # Each request is wrapped in a Fiber.
15
- def self.schedule(app, request)
16
- Fiber.schedule do
17
- call(app, request)
60
+ def stop_background_thread
61
+ @running&.stop
62
+ @background_thread&.join
18
63
  end
64
+
65
+ def write_pid
66
+ File.write(Itsi::Server::Config.pid_file_path, Process.pid)
67
+ end
68
+
69
+ def get_pid
70
+ pid = File.read(Itsi::Server::Config.pid_file_path).to_i
71
+ if Process.kill(0, pid)
72
+ pid
73
+ else
74
+ puts "No server running"
75
+ nil
76
+ end
77
+ rescue StandardError
78
+ puts "No server running"
79
+ nil
80
+ end
81
+
82
+ def init
83
+ Config.write_default
84
+ end
85
+
86
+ def reload
87
+ return unless pid = get_pid
88
+
89
+ Process.kill(:HUP, pid)
90
+ end
91
+
92
+ def restart
93
+ return unless pid = get_pid
94
+
95
+ Process.kill(:USR1, pid)
96
+ end
97
+
98
+ def passfile(options, subcmd)
99
+ filename = options[:passfile]
100
+ unless filename
101
+ puts "Error: passfile not set. Use --passfile option to provide a path to a file containing hashed credentials."
102
+ puts "This file contains hashed credentials and should not be included in source control without additional protection."
103
+ exit(1)
104
+ end
105
+ algorithm = options.fetch(:algorithm, 'sha256')
106
+
107
+ unless %w[sha256 sha512 bcrypt argon2 none].include?(algorithm)
108
+ puts "Invalid algorithm"
109
+ exit(1)
110
+ end
111
+
112
+ case subcmd
113
+ when 'add', 'echo'
114
+ Passfile.send(subcmd, filename, algorithm)
115
+ when 'remove', 'list'
116
+ Passfile.send(subcmd, filename)
117
+ else
118
+ puts "Valid subcommands are: add | remove | list"
119
+ exit(0)
120
+ end
121
+ end
122
+
123
+ def add_worker
124
+ return unless pid = get_pid
125
+
126
+ Process.kill(:TTIN, pid)
127
+ end
128
+
129
+ def remove_worker
130
+ return unless pid = get_pid
131
+
132
+ Process.kill(:TTOU, pid)
133
+ end
134
+
135
+ def status
136
+ return unless pid = get_pid
137
+
138
+ Process.kill(:USR2, pid)
139
+ end
140
+
141
+ def load_route_middleware_stack(cli_params)
142
+ Config.build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file_path]))[
143
+ "middleware_loader"
144
+ ][]
145
+ end
146
+
147
+ def test_route(cli_params = {}, route_str)
148
+ matched_route = load_route_middleware_stack(cli_params).find do |route|
149
+ route["route"] =~ route_str
150
+ end
151
+ if matched_route
152
+ print_route(route_str, matched_route)
153
+ else
154
+ puts "No matching route found"
155
+ end
156
+ end
157
+
158
+ def routes(cli_params = {})
159
+ load_route_middleware_stack(cli_params).each do |stack|
160
+ routes = explode_route_pattern(stack["route"].source)
161
+ routes.each do |route|
162
+ print_route(route, stack)
163
+ end
164
+ end
165
+ puts "─" * 76
166
+ end
167
+
168
+ alias serve start
169
+
19
170
  end
20
171
  end
21
172
  end