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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +3937 -0
- data/Cargo.toml +7 -0
- data/README.md +4 -0
- data/Rakefile +8 -1
- data/_index.md +6 -0
- data/exe/itsi +141 -46
- data/ext/itsi_error/Cargo.toml +3 -0
- data/ext/itsi_error/src/lib.rs +98 -24
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +3 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
- data/ext/itsi_rb_helpers/src/lib.rs +140 -10
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- 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
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +72 -14
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/default_responses/html/401.html +68 -0
- data/ext/itsi_server/src/default_responses/html/403.html +68 -0
- data/ext/itsi_server/src/default_responses/html/404.html +68 -0
- data/ext/itsi_server/src/default_responses/html/413.html +71 -0
- data/ext/itsi_server/src/default_responses/html/429.html +68 -0
- data/ext/itsi_server/src/default_responses/html/500.html +71 -0
- data/ext/itsi_server/src/default_responses/html/502.html +71 -0
- data/ext/itsi_server/src/default_responses/html/503.html +68 -0
- data/ext/itsi_server/src/default_responses/html/504.html +69 -0
- data/ext/itsi_server/src/default_responses/html/index.html +238 -0
- data/ext/itsi_server/src/default_responses/json/401.json +6 -0
- data/ext/itsi_server/src/default_responses/json/403.json +6 -0
- data/ext/itsi_server/src/default_responses/json/404.json +6 -0
- data/ext/itsi_server/src/default_responses/json/413.json +6 -0
- data/ext/itsi_server/src/default_responses/json/429.json +6 -0
- data/ext/itsi_server/src/default_responses/json/500.json +6 -0
- data/ext/itsi_server/src/default_responses/json/502.json +6 -0
- data/ext/itsi_server/src/default_responses/json/503.json +6 -0
- data/ext/itsi_server/src/default_responses/json/504.json +6 -0
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +132 -40
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/binds/bind.rs +201 -0
- data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/binds/listener.rs +432 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/binds/tls.rs +270 -0
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +105 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
- data/ext/itsi_server/src/server/mod.rs +12 -5
- data/ext/itsi_server/src/server/process_worker.rs +247 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
- data/ext/itsi_server/src/server/signal.rs +76 -0
- data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/ext/itsi_server/src/server/thread_worker.rs +475 -0
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
- data/ext/itsi_tracing/Cargo.toml +5 -0
- data/ext/itsi_tracing/src/lib.rs +315 -7
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/lib/itsi/http_request.rb +186 -0
- data/lib/itsi/http_response.rb +41 -0
- data/lib/itsi/passfile.rb +109 -0
- data/lib/itsi/server/config/dsl.rb +565 -0
- data/lib/itsi/server/config.rb +166 -0
- data/lib/itsi/server/default_app/default_app.rb +34 -0
- data/lib/itsi/server/default_app/index.html +115 -0
- data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
- data/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/lib/itsi/server/rack/handler/itsi.rb +27 -0
- data/lib/itsi/server/rack_interface.rb +94 -0
- data/lib/itsi/server/route_tester.rb +107 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/scheduler_mode.rb +10 -0
- data/lib/itsi/server/signal_trap.rb +29 -0
- data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/lib/itsi/server/typed_handlers.rb +17 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +160 -9
- data/lib/itsi/standard_headers.rb +86 -0
- data/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/lib/shell_completions/completions.rb +26 -0
- metadata +182 -25
- data/ext/itsi_server/src/request/itsi_request.rs +0 -143
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/server/bind.rs +0 -138
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/itsi_server.rs +0 -182
- data/ext/itsi_server/src/server/listener.rs +0 -218
- data/ext/itsi_server/src/server/tls.rs +0 -138
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
- 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
|
data/lib/itsi/server/version.rb
CHANGED
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|