itsi-server 0.2.2 → 0.2.4
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/Cargo.lock +28 -29
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +1 -1
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +28 -11
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +14 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +86 -41
- data/ext/itsi_server/src/services/itsi_http_service.rs +46 -35
- data/ext/itsi_server/src/services/static_file_server.rs +31 -3
- data/lib/itsi/http_request.rb +31 -34
- data/lib/itsi/http_response.rb +10 -8
- data/lib/itsi/passfile.rb +6 -6
- data/lib/itsi/server/config/config_helpers.rb +33 -33
- data/lib/itsi/server/config/dsl.rb +16 -21
- data/lib/itsi/server/config/known_paths.rb +11 -7
- data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +0 -4
- data/lib/itsi/server/config/middleware/error_response.md +13 -0
- data/lib/itsi/server/config/middleware/location.rb +25 -21
- data/lib/itsi/server/config/middleware/proxy.rb +15 -14
- data/lib/itsi/server/config/middleware/rackup_file.rb +7 -10
- data/lib/itsi/server/config/middleware/static_assets.md +40 -0
- data/lib/itsi/server/config/middleware/static_assets.rb +8 -4
- data/lib/itsi/server/config/middleware/string_rewrite.md +14 -0
- data/lib/itsi/server/config/option.rb +0 -1
- data/lib/itsi/server/config/options/include.rb +1 -1
- data/lib/itsi/server/config/options/nodelay.md +2 -2
- data/lib/itsi/server/config/options/reuse_address.md +1 -1
- data/lib/itsi/server/config/typed_struct.rb +32 -35
- data/lib/itsi/server/config.rb +107 -92
- data/lib/itsi/server/default_app/default_app.rb +1 -1
- data/lib/itsi/server/grpc/grpc_call.rb +4 -5
- data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
- data/lib/itsi/server/rack/handler/itsi.rb +0 -1
- data/lib/itsi/server/rack_interface.rb +1 -2
- data/lib/itsi/server/route_tester.rb +26 -24
- data/lib/itsi/server/typed_handlers/param_parser.rb +25 -0
- data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +22 -22
- data/lib/itsi/standard_headers.rb +80 -80
- metadata +3 -3
data/lib/itsi/server/config.rb
CHANGED
@@ -41,15 +41,18 @@ module Itsi
|
|
41
41
|
elsif args[:static]
|
42
42
|
DSL.evaluate do
|
43
43
|
location "/" do
|
44
|
-
rate_limit key:
|
45
|
-
etag type:
|
46
|
-
compress min_size: 1024 * 1024, level:
|
47
|
-
|
44
|
+
rate_limit key: "address", store_config: "in_memory", requests: 2, seconds: 5
|
45
|
+
etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
|
46
|
+
compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
|
47
|
+
mime_types: %w[all], compress_streams: true
|
48
|
+
log_requests before: { level: "INFO", format: "[{request_id}] {method} {path_and_query} - {addr} " },
|
49
|
+
after: { level: "INFO",
|
50
|
+
format: "[{request_id}] └─ {status} in {response_time}" }
|
48
51
|
static_assets \
|
49
52
|
relative_path: true,
|
50
53
|
allowed_extensions: [],
|
51
|
-
root_dir:
|
52
|
-
not_found_behavior: {error:
|
54
|
+
root_dir: ".",
|
55
|
+
not_found_behavior: { error: "not_found" },
|
53
56
|
auto_index: true,
|
54
57
|
try_html_extension: true,
|
55
58
|
max_file_size_in_memory: 1024 * 1024, # 1MB
|
@@ -57,7 +60,7 @@ module Itsi
|
|
57
60
|
file_check_interval: 1,
|
58
61
|
serve_hidden_files: false,
|
59
62
|
headers: {
|
60
|
-
|
63
|
+
"X-Content-Type-Options" => "nosniff"
|
61
64
|
}
|
62
65
|
end
|
63
66
|
end
|
@@ -69,7 +72,7 @@ module Itsi
|
|
69
72
|
rackup_file args.fetch(:rackup_file, "./config.ru")
|
70
73
|
end
|
71
74
|
else
|
72
|
-
DSL.evaluate{}
|
75
|
+
DSL.evaluate {}
|
73
76
|
end
|
74
77
|
|
75
78
|
itsifile_config.transform_keys!(&:to_sym)
|
@@ -85,7 +88,7 @@ module Itsi
|
|
85
88
|
Itsi.log_debug("Preloading middleware and default rack app")
|
86
89
|
preloaded_middleware = middleware_loader.call
|
87
90
|
middleware_loader = -> { preloaded_middleware }
|
88
|
-
rescue
|
91
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
89
92
|
errors << [e, e.backtrace[0]]
|
90
93
|
end
|
91
94
|
# If we're just preloading a specific gem group, we'll do that here too
|
@@ -107,7 +110,13 @@ module Itsi
|
|
107
110
|
worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
|
108
111
|
silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
|
109
112
|
shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
|
110
|
-
hooks: args[:hooks] && itsifile_config[:hooks]
|
113
|
+
hooks: if args[:hooks] && itsifile_config[:hooks]
|
114
|
+
args[:hooks].merge(itsifile_config[:hooks])
|
115
|
+
else
|
116
|
+
itsifile_config.fetch(
|
117
|
+
:hooks, args[:hooks]
|
118
|
+
)
|
119
|
+
end,
|
111
120
|
preload: !!preload,
|
112
121
|
request_timeout: itsifile_config.fetch(:request_timeout, nil),
|
113
122
|
header_read_timeout: args.fetch(:header_read_timeout) { itsifile_config.fetch(:header_read_timeout, nil) },
|
@@ -132,53 +141,23 @@ module Itsi
|
|
132
141
|
listeners: args.fetch(:listeners) { nil },
|
133
142
|
reuse_address: itsifile_config.fetch(:reuse_address, true),
|
134
143
|
reuse_port: itsifile_config.fetch(:reuse_port, true),
|
135
|
-
listen_backlog: itsifile_config.fetch(:listen_backlog, 1024
|
144
|
+
listen_backlog: itsifile_config.fetch(:listen_backlog, 1024),
|
136
145
|
nodelay: itsifile_config.fetch(:nodelay, true),
|
137
146
|
recv_buffer_size: itsifile_config.fetch(:recv_buffer_size, 262_144)
|
138
147
|
}.transform_keys(&:to_s)
|
139
148
|
|
140
|
-
|
141
|
-
|
142
|
-
file, lineno = location.split(":")
|
143
|
-
lineno = lineno.to_i
|
144
|
-
err_message = error.kind_of?(NoMethodError) ? error.detailed_message : error.message
|
145
|
-
file_lines = IO.readlines(file)
|
146
|
-
info_lines = if error.kind_of?(SyntaxError)
|
147
|
-
[]
|
148
|
-
else
|
149
|
-
([lineno-2, 0].max...[file_lines.length, lineno.succ.succ].min).map do |currline|
|
150
|
-
if currline == lineno-1
|
151
|
-
line = file_lines[currline][0...-1]
|
152
|
-
padding = line[/^\s+/]&.length || 0
|
153
|
-
|
154
|
-
[
|
155
|
-
" \e[31m#{currline.succ.to_s.rjust(3)} | #{line}\e[0m",
|
156
|
-
" | #{' ' * padding}\e[33m^^^\e[0m "
|
157
|
-
]
|
158
|
-
else
|
159
|
-
" #{currline.succ.to_s.rjust(3)} | #{file_lines[currline][0...-1]}"
|
160
|
-
end
|
161
|
-
end.flatten
|
162
|
-
end
|
163
|
-
[
|
164
|
-
err_message,
|
165
|
-
" --> #{File.expand_path(file)}:#{lineno}",
|
166
|
-
*info_lines
|
167
|
-
]
|
168
|
-
end
|
169
|
-
|
170
|
-
return srv_config, error_lines
|
171
|
-
rescue
|
149
|
+
[srv_config, errors_to_error_lines(errors)]
|
150
|
+
rescue StandardError
|
172
151
|
Itsi.log_error e.message
|
173
152
|
puts e.backtrace
|
174
153
|
end
|
175
154
|
|
176
155
|
def self.test!(cli_params)
|
177
|
-
|
156
|
+
config, errors = build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file]))
|
178
157
|
unless errors.any?
|
179
158
|
begin
|
180
|
-
|
181
|
-
rescue Exception => e
|
159
|
+
config["middleware_loader"][]
|
160
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
182
161
|
errors = [e]
|
183
162
|
end
|
184
163
|
end
|
@@ -191,6 +170,42 @@ module Itsi
|
|
191
170
|
end
|
192
171
|
end
|
193
172
|
|
173
|
+
def self.errors_to_error_lines(errors)
|
174
|
+
return unless errors
|
175
|
+
|
176
|
+
errors.flat_map do |(error, message)|
|
177
|
+
location = message[/(.*?):in/, 1]
|
178
|
+
file, lineno = location.split(":")
|
179
|
+
lineno = lineno.to_i
|
180
|
+
err_message = error.is_a?(NoMethodError) ? error.detailed_message : error.message
|
181
|
+
file_lines = IO.readlines(file)
|
182
|
+
info_lines = \
|
183
|
+
if error.is_a?(SyntaxError)
|
184
|
+
[]
|
185
|
+
else
|
186
|
+
|
187
|
+
([lineno - 2, 0].max...[file_lines.length, lineno.succ.succ].min).map do |currline|
|
188
|
+
if currline == lineno - 1
|
189
|
+
line = file_lines[currline][0...-1]
|
190
|
+
padding = line[/^\s+/]&.length || 0
|
191
|
+
|
192
|
+
[
|
193
|
+
" \e[31m#{currline.succ.to_s.rjust(3)} | #{line}\e[0m",
|
194
|
+
" | #{" " * padding}\e[33m^^^\e[0m "
|
195
|
+
]
|
196
|
+
else
|
197
|
+
" #{currline.succ.to_s.rjust(3)} | #{file_lines[currline][0...-1]}"
|
198
|
+
end
|
199
|
+
end.flatten
|
200
|
+
end
|
201
|
+
[
|
202
|
+
err_message,
|
203
|
+
" --> #{File.expand_path(file)}:#{lineno}",
|
204
|
+
*info_lines
|
205
|
+
]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
194
209
|
# Reloads the entire process
|
195
210
|
# using exec, passing in any active file descriptors
|
196
211
|
# and previous invocation arguments
|
@@ -233,56 +248,56 @@ module Itsi
|
|
233
248
|
|
234
249
|
default_config = IO.read("#{__dir__}/default_config/Itsi.rb")
|
235
250
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
251
|
+
default_config << \
|
252
|
+
if File.exist?("./config.ru")
|
253
|
+
<<~RUBY
|
254
|
+
# You can mount several Ruby apps as either
|
255
|
+
# 1. rackup files
|
256
|
+
# 2. inline rack apps
|
257
|
+
# 3. inline Ruby endpoints
|
258
|
+
#
|
259
|
+
# 1. rackup_file
|
260
|
+
rackup_file "./config.ru"
|
261
|
+
#
|
262
|
+
# 2. inline rack app
|
263
|
+
# require 'rack'
|
264
|
+
# run(Rack::Builder.app do
|
265
|
+
# use Rack::CommonLogger
|
266
|
+
# run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
|
267
|
+
# end)
|
268
|
+
#
|
269
|
+
# 3. Endpoints
|
270
|
+
# endpoint "/" do |req|
|
271
|
+
# req.ok "Hello from Itsi"
|
272
|
+
# end
|
273
|
+
RUBY
|
274
|
+
else
|
275
|
+
<<~RUBY
|
276
|
+
# You can mount several Ruby apps as either
|
277
|
+
# 1. rackup files
|
278
|
+
# 2. inline rack apps
|
279
|
+
# 3. inline Ruby endpoints
|
280
|
+
#
|
281
|
+
# 1. rackup_file
|
282
|
+
# Use `rackup_file` to specify the Rack app file name.
|
283
|
+
#
|
284
|
+
# 2. inline rack app
|
285
|
+
# require 'rack'
|
286
|
+
# run(Rack::Builder.app do
|
287
|
+
# use Rack::CommonLogger
|
288
|
+
# run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
|
289
|
+
# end)
|
290
|
+
#
|
291
|
+
# 3. Endpoint
|
292
|
+
endpoint "/" do |req|
|
293
|
+
req.ok "Hello from Itsi"
|
294
|
+
end
|
295
|
+
RUBY
|
296
|
+
end
|
281
297
|
|
282
298
|
File.open(ITSI_DEFAULT_CONFIG_FILE, "w") do |file|
|
283
299
|
file.write(default_config)
|
284
300
|
end
|
285
|
-
|
286
301
|
end
|
287
302
|
end
|
288
303
|
end
|
@@ -7,11 +7,11 @@ module Itsi
|
|
7
7
|
attr_accessor :rpc_desc
|
8
8
|
|
9
9
|
def input_stream?
|
10
|
-
@input_stream ||= @rpc_desc&.input
|
10
|
+
@input_stream ||= @rpc_desc&.input.is_a?(GRPC::RpcDesc::Stream) || false
|
11
11
|
end
|
12
12
|
|
13
13
|
def output_stream?
|
14
|
-
@output_stream ||= @rpc_desc&.output
|
14
|
+
@output_stream ||= @rpc_desc&.output.is_a?(GRPC::RpcDesc::Stream) || false
|
15
15
|
end
|
16
16
|
|
17
17
|
def input_type
|
@@ -64,7 +64,7 @@ module Itsi
|
|
64
64
|
return nil if first_char.nil?
|
65
65
|
|
66
66
|
# Step 2: Process objects until we hit the end of the JSON stream or array.
|
67
|
-
loop do
|
67
|
+
loop do # rubocop:disable Lint/UnreachableLoop,Metrics/BlockLength
|
68
68
|
# Skip any whitespace or commas preceding an object.
|
69
69
|
char = nil
|
70
70
|
loop do
|
@@ -89,7 +89,6 @@ module Itsi
|
|
89
89
|
|
90
90
|
buffer << ch
|
91
91
|
|
92
|
-
|
93
92
|
if in_string
|
94
93
|
if escape
|
95
94
|
escape = false
|
@@ -120,7 +119,7 @@ module Itsi
|
|
120
119
|
def remote_read
|
121
120
|
if content_type == "application/json"
|
122
121
|
if input_stream?
|
123
|
-
if next_item = parse_from_json_stream(reader)
|
122
|
+
if (next_item = parse_from_json_stream(reader))
|
124
123
|
input_type.decode_json(next_item)
|
125
124
|
end
|
126
125
|
else
|
@@ -29,7 +29,6 @@ module Itsi
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def handle_request(active_call)
|
32
|
-
puts "Handling active call"
|
33
32
|
unless (active_call.rpc_desc = @rpc_descs[active_call.method_name])
|
34
33
|
active_call.stream.write("\n")
|
35
34
|
active_call.send_status(13, "Method not found")
|
@@ -89,18 +88,18 @@ module Itsi
|
|
89
88
|
result = service.send(active_call.method_name, message, active_call) do |response|
|
90
89
|
active_call.remote_send(response)
|
91
90
|
end
|
92
|
-
|
93
|
-
|
94
|
-
|
91
|
+
return unless result.is_a?(Enumerator)
|
92
|
+
|
93
|
+
result.each { |response| active_call.remote_send(response) }
|
95
94
|
end
|
96
95
|
|
97
96
|
def handle_bidi_streaming(active_call)
|
98
97
|
result = service.send(active_call.method_name, active_call.each_remote_read, active_call) do |response|
|
99
98
|
active_call.remote_send(response)
|
100
99
|
end
|
101
|
-
|
102
|
-
|
103
|
-
|
100
|
+
return unless result.is_a?(Enumerator)
|
101
|
+
|
102
|
+
result.each { |response| active_call.remote_send(response) }
|
104
103
|
end
|
105
104
|
end
|
106
105
|
end
|
@@ -1,14 +1,13 @@
|
|
1
1
|
module Itsi
|
2
2
|
class Server
|
3
3
|
module RackInterface
|
4
|
-
|
5
4
|
# Builds a handler proc that is compatible with Rack applications.
|
6
5
|
def self.for(app)
|
7
6
|
require "rack"
|
8
7
|
if app.is_a?(String)
|
9
8
|
dir = File.expand_path(File.dirname(app))
|
10
9
|
Dir.chdir(dir) do
|
11
|
-
loaded_app = ::Rack::Builder.parse_file(app)
|
10
|
+
loaded_app = ::Rack::Builder.parse_file(File.basename(app))
|
12
11
|
app = loaded_app.is_a?(Array) ? loaded_app.first : loaded_app
|
13
12
|
end
|
14
13
|
end
|
@@ -1,53 +1,55 @@
|
|
1
1
|
module Itsi
|
2
2
|
class Server
|
3
|
+
# Utility module for printing Itsi route information
|
3
4
|
module RouteTester
|
4
|
-
|
5
5
|
require "set"
|
6
6
|
require "strscan"
|
7
|
-
|
7
|
+
|
8
8
|
def format_mw(mw)
|
9
9
|
mw_name, mw_args = mw
|
10
10
|
case mw_name
|
11
11
|
when "app"
|
12
|
-
"\e[33mapp\e[0m(#{mw_args[
|
12
|
+
"\e[33mapp\e[0m(#{mw_args["app_proc"].inspect.split(" ")[1][0...-1]})"
|
13
13
|
when "log_requests"
|
14
|
-
if mw_args[
|
15
|
-
"\e[33mlog_requests\e[0m(before: #{mw_args[
|
16
|
-
elsif mw_args[
|
17
|
-
"\e[33mlog_requests\e[0m(before: #{mw_args[
|
18
|
-
elsif mw_args[
|
19
|
-
"\e[33mlog_requests\e[0m(before: nil, after: #{mw_args[
|
14
|
+
if mw_args["before"] && mw_args["after"]
|
15
|
+
"\e[33mlog_requests\e[0m(before: #{mw_args["before"]["format"][0..6]}..., after: #{mw_args["after"]["format"][0..6]}...)"
|
16
|
+
elsif mw_args["before"]
|
17
|
+
"\e[33mlog_requests\e[0m(before: #{mw_args["before"]["format"][0..6]}...)"
|
18
|
+
elsif mw_args["after"]
|
19
|
+
"\e[33mlog_requests\e[0m(before: nil, after: #{mw_args["after"]["format"][0..6]}...)"
|
20
20
|
end
|
21
21
|
when "compress"
|
22
|
-
"\e[33mcompress\e[0m(#{mw_args[
|
22
|
+
"\e[33mcompress\e[0m(#{mw_args["algorithms"].join(" ")}, #{mw_args["mime_types"]})"
|
23
23
|
when "cors"
|
24
|
-
"\e[33mcors\e[0m(#{mw_args[
|
24
|
+
"\e[33mcors\e[0m(#{mw_args["allow_origins"].join(" ")}, #{mw_args["allow_methods"].join(" ")})"
|
25
25
|
when "etag"
|
26
|
-
"\e[33metag\e[0m(#{mw_args[
|
26
|
+
"\e[33metag\e[0m(#{mw_args["type"]}/#{mw_args["algorithm"]}, #{mw_args["handle_if_none_match"] ? "if_none_match" : ""})"
|
27
27
|
when "cache_control"
|
28
|
-
"\e[33mcache_control\e[0m(max_age: #{mw_args[
|
28
|
+
"\e[33mcache_control\e[0m(max_age: #{mw_args["max_age"]}, #{mw_args.select do |_, v|
|
29
|
+
v == true
|
30
|
+
end.keys.join(", ")})"
|
29
31
|
when "redirect"
|
30
|
-
"\e[33mredirect\e[0m(to: #{mw_args[
|
32
|
+
"\e[33mredirect\e[0m(to: #{mw_args["to"]}, type: #{mw_args["type"]})"
|
31
33
|
when "static_assets"
|
32
|
-
"\e[33mstatic_assets\e[0m(path: #{mw_args[
|
34
|
+
"\e[33mstatic_assets\e[0m(path: #{mw_args["root_dir"]})"
|
33
35
|
when "auth_api_key"
|
34
|
-
"\e[33mauth_api_key\e[0m(keys: #{mw_args[
|
36
|
+
"\e[33mauth_api_key\e[0m(keys: #{mw_args["valid_keys"].keys}#{mw_args["credentials_file"] ? ", credentials_file: #{mw_args["credentials_file"]}" : ""})"
|
35
37
|
when "auth_basic"
|
36
|
-
"\e[33mbasic_auth\e[0m(keys: #{mw_args[
|
38
|
+
"\e[33mbasic_auth\e[0m(keys: #{mw_args["realm"]}#{mw_args["credentials_file"] ? ", credentials_file: #{mw_args["credentials_file"]}" : ""})"
|
37
39
|
when "auth_jwt"
|
38
|
-
"\e[33mjwt_auth\e[0m(#{mw_args[
|
40
|
+
"\e[33mjwt_auth\e[0m(#{mw_args["verifiers"].keys.join(",")})"
|
39
41
|
when "rate_limit"
|
40
|
-
key = mw_args[
|
41
|
-
"\e[33mrate_limit\e[0m(rps: #{mw_args[
|
42
|
+
key = mw_args["key"].is_a?(Hash) ? mw_args["key"]["parameter"] : mw_args["key"]
|
43
|
+
"\e[33mrate_limit\e[0m(rps: #{mw_args["requests"]}/#{mw_args["seconds"]}, key: #{key})"
|
42
44
|
when "allow_list"
|
43
|
-
"\e[33mallow_list\e[0m(patterns: #{mw_args[
|
45
|
+
"\e[33mallow_list\e[0m(patterns: #{mw_args["allowed_patterns"].join(", ")})"
|
44
46
|
when "deny_list"
|
45
|
-
"\e[33mdeny_list\e[0m(patterns: #{mw_args[
|
47
|
+
"\e[33mdeny_list\e[0m(patterns: #{mw_args["denied_patterns"].join(", ")})"
|
46
48
|
when "csp"
|
47
|
-
"\e[33mcsp\e[0m(#{mw_args[
|
49
|
+
"\e[33mcsp\e[0m(#{mw_args["policy"].map { |k, v| "#{k}: #{v.join(",")}" }.join(", ")})"
|
48
50
|
when "intrusion_protection"
|
49
51
|
[mw_args].flatten.map do |mw_args|
|
50
|
-
"\e[33mintrusion_protection\e[0m(banned_url_patterns: #{mw_args[
|
52
|
+
"\e[33mintrusion_protection\e[0m(banned_url_patterns: #{mw_args["banned_url_patterns"]&.length}, banned_header_patterns: #{mw_args["banned_header_patterns"]&.keys&.join(", ")}, #{mw_args["banned_time_seconds"]}s)"
|
51
53
|
end.join("\n")
|
52
54
|
when "request_headers"
|
53
55
|
[mw_args].flatten.map do |mw_args|
|
@@ -143,6 +143,31 @@ module Itsi
|
|
143
143
|
# Fixed keys are converted to symbols, and regex-matched keys remain as strings.
|
144
144
|
# The current location in the params is tracked as an array of path segments.
|
145
145
|
def apply_schema!(params, schema, path = [])
|
146
|
+
# Support top-level array schema: homogeneous arrays.
|
147
|
+
if schema.is_a?(Array)
|
148
|
+
# Only allow homogeneous array types
|
149
|
+
unless schema.size == 1
|
150
|
+
raise ValidationError.new(["Schema Array must contain exactly one type. Got #{schema.size}"])
|
151
|
+
end
|
152
|
+
expected_type = schema.first
|
153
|
+
# Expect params to be an Array
|
154
|
+
unless params.is_a?(Array)
|
155
|
+
raise ValidationError.new(["Expected Array at #{format_path(path)}, got #{params.class}"])
|
156
|
+
end
|
157
|
+
errors = []
|
158
|
+
params.each_with_index do |_, idx|
|
159
|
+
err = cast_value!(params, idx, expected_type, path + [idx])
|
160
|
+
errors << err if err
|
161
|
+
end
|
162
|
+
raise ValidationError.new(errors) unless errors.empty?
|
163
|
+
return params
|
164
|
+
end
|
165
|
+
|
166
|
+
# Ensure schema is a Hash
|
167
|
+
unless schema.is_a?(Hash)
|
168
|
+
raise ValidationError.new(["Unsupported schema type: #{schema.class} at #{format_path(path)}"])
|
169
|
+
end
|
170
|
+
|
146
171
|
errors = []
|
147
172
|
processed = processed_schema(schema)
|
148
173
|
fixed_schema = processed[0]
|
@@ -2,9 +2,10 @@ module Itsi
|
|
2
2
|
class Server
|
3
3
|
module TypedHandlers
|
4
4
|
module SourceParser
|
5
|
-
require
|
6
|
-
|
5
|
+
require "prism"
|
7
6
|
|
7
|
+
# Source Parser interprets endpoint handlers
|
8
|
+
# and extracts arity and schema information.
|
8
9
|
def self.extract_expr_from_source_location(proc)
|
9
10
|
source_location = proc.source_location
|
10
11
|
source_lines = IO.readlines(source_location.first)
|
@@ -23,17 +24,19 @@ module Itsi
|
|
23
24
|
|
24
25
|
lines[1..-1].each do |line|
|
25
26
|
break if intermediate.success?
|
27
|
+
|
26
28
|
token_count = 0
|
27
29
|
line.split(/(?=\s|;|\)|\})/).each do |token|
|
28
30
|
src_str << token
|
29
31
|
token_count += 1
|
30
32
|
intermediate = Prism.parse(src_str)
|
31
33
|
next unless intermediate.success? && token_count > 1
|
34
|
+
|
32
35
|
break
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
36
|
-
raise
|
39
|
+
raise "Source Extraction Failed" unless intermediate.success?
|
37
40
|
|
38
41
|
src = intermediate.value.statements.body.first.yield_self do |s|
|
39
42
|
s.type == :call_node ? s.block : s
|
@@ -41,14 +44,13 @@ module Itsi
|
|
41
44
|
params = src.parameters
|
42
45
|
params = params.parameters if params.respond_to?(:parameters)
|
43
46
|
requireds = (params&.requireds || []).map(&:name)
|
44
|
-
optionals = params&.optionals || []
|
45
47
|
keywords = (params&.keywords || []).map do |kw|
|
46
|
-
[kw.name, kw.value.slice.gsub(/^_\./,
|
48
|
+
[kw.name, kw.value.slice.gsub(/^_\./, "$.")]
|
47
49
|
end.to_h
|
48
50
|
|
49
51
|
[requireds.length, keywords]
|
50
|
-
rescue
|
51
|
-
[
|
52
|
+
rescue StandardError
|
53
|
+
[proc.parameters.select { |p| p == :req }&.length || 0, {}]
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|