itsi 0.2.2 → 0.2.3
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 +1 -1
- data/Cargo.lock +29 -30
- data/README.md +1 -0
- data/crates/itsi_scheduler/Cargo.toml +1 -1
- data/crates/itsi_server/Cargo.toml +1 -1
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
- data/docs/content/_index.md +1 -1
- data/docs/content/contact/_index.md +1 -1
- data/docs/content/directory_listing.jpg +0 -0
- data/docs/content/error_page.jpg +0 -0
- data/docs/content/getting_started/local_development.md +9 -2
- data/docs/content/getting_started/logging.md +1 -2
- data/docs/content/getting_started/signals.md +0 -1
- data/docs/hugo.yaml +3 -0
- data/gems/scheduler/Cargo.lock +74 -17
- data/gems/scheduler/itsi-scheduler.gemspec +2 -2
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/server/Cargo.lock +28 -29
- data/gems/server/itsi-server.gemspec +2 -2
- data/gems/server/lib/itsi/http_request.rb +31 -34
- data/gems/server/lib/itsi/http_response.rb +10 -8
- data/gems/server/lib/itsi/passfile.rb +6 -6
- data/gems/server/lib/itsi/server/config/config_helpers.rb +33 -33
- data/gems/server/lib/itsi/server/config/dsl.rb +14 -19
- data/gems/server/lib/itsi/server/config/known_paths.rb +11 -7
- data/gems/server/lib/itsi/server/config/middleware/error_response.md +13 -0
- data/gems/server/lib/itsi/server/config/middleware/static_assets.md +40 -0
- data/gems/server/lib/itsi/server/config/option.rb +0 -1
- data/gems/server/lib/itsi/server/config/options/nodelay.md +2 -2
- data/gems/server/lib/itsi/server/config/options/reuse_address.md +1 -1
- data/gems/server/lib/itsi/server/config/typed_struct.rb +32 -35
- data/gems/server/lib/itsi/server/config.rb +107 -92
- data/gems/server/lib/itsi/server/default_app/default_app.rb +1 -1
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +4 -5
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +6 -7
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +0 -1
- data/gems/server/lib/itsi/server/rack_interface.rb +0 -1
- data/gems/server/lib/itsi/server/route_tester.rb +25 -23
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +21 -21
- data/gems/server/lib/itsi/standard_headers.rb +80 -80
- data/gems/server/test/helpers/test_helper.rb +17 -16
- data/gems/server/test/middleware/test_log_requests.rb +54 -2
- data/gems/server/test/options/test_workers.rb +12 -5
- data/lib/itsi/version.rb +1 -1
- metadata +9 -13
- data/examples/static_assets_example.rb +0 -83
- data/grpc_test/Itsi.rb +0 -11
- data/grpc_test/echo.proto +0 -14
- data/grpc_test/echo_pb.rb +0 -16
- data/grpc_test/echo_service_impl.rb +0 -8
- data/grpc_test/echo_services_pb.rb +0 -22
@@ -1,7 +1,7 @@
|
|
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
|
require "debug"
|
@@ -9,45 +9,47 @@ module Itsi
|
|
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|
|
@@ -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
|
@@ -27,7 +27,6 @@ module Itsi
|
|
27
27
|
extend RouteTester
|
28
28
|
|
29
29
|
class << self
|
30
|
-
|
31
30
|
def running?
|
32
31
|
@running && !@running.empty?
|
33
32
|
end
|
@@ -62,9 +61,8 @@ module Itsi
|
|
62
61
|
server
|
63
62
|
end
|
64
63
|
background ? [server, Thread.new(&run)] : run[]
|
65
|
-
rescue Exception => e
|
64
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
66
65
|
Itsi.log_error e.message
|
67
|
-
raise e
|
68
66
|
end
|
69
67
|
|
70
68
|
def static(cli_params)
|
@@ -72,7 +70,8 @@ module Itsi
|
|
72
70
|
end
|
73
71
|
|
74
72
|
def stop
|
75
|
-
return unless pid = get_pid
|
73
|
+
return unless (pid = get_pid)
|
74
|
+
|
76
75
|
Process.kill(:INT, pid)
|
77
76
|
i = 0
|
78
77
|
while i < 10
|
@@ -96,7 +95,7 @@ module Itsi
|
|
96
95
|
File.write(Itsi::Server::Config.pid_file_path, Process.pid)
|
97
96
|
end
|
98
97
|
|
99
|
-
def get_pid(warn=true)
|
98
|
+
def get_pid(warn = true)
|
100
99
|
pid = File.read(Itsi::Server::Config.pid_file_path).to_i
|
101
100
|
if Process.kill(0, pid)
|
102
101
|
pid
|
@@ -110,7 +109,7 @@ module Itsi
|
|
110
109
|
end
|
111
110
|
|
112
111
|
def test
|
113
|
-
Itsi::Server::Config.test!(
|
112
|
+
Itsi::Server::Config.test!({})
|
114
113
|
end
|
115
114
|
|
116
115
|
def init
|
@@ -118,25 +117,25 @@ module Itsi
|
|
118
117
|
end
|
119
118
|
|
120
119
|
def reload
|
121
|
-
return unless pid = get_pid
|
120
|
+
return unless (pid = get_pid)
|
122
121
|
|
123
122
|
Process.kill(:HUP, pid)
|
124
123
|
end
|
125
124
|
|
126
125
|
def restart
|
127
|
-
return unless pid = get_pid
|
126
|
+
return unless (pid = get_pid)
|
128
127
|
|
129
128
|
Process.kill(:USR1, pid)
|
130
129
|
end
|
131
130
|
|
132
131
|
def passfile(options, subcmd)
|
133
132
|
filename = options[:passfile]
|
134
|
-
unless filename || subcmd ==
|
133
|
+
unless filename || subcmd == "echo"
|
135
134
|
puts "Error: passfile not set. Use --passfile option to provide a path to a file containing hashed credentials."
|
136
135
|
puts "This file contains hashed credentials and should not be included in source control without additional protection."
|
137
136
|
exit(1)
|
138
137
|
end
|
139
|
-
algorithm = options.fetch(:algorithm,
|
138
|
+
algorithm = options.fetch(:algorithm, "sha256")
|
140
139
|
|
141
140
|
unless %w[sha256 sha512 bcrypt argon2 none].include?(algorithm)
|
142
141
|
puts "Invalid algorithm"
|
@@ -144,9 +143,9 @@ module Itsi
|
|
144
143
|
end
|
145
144
|
|
146
145
|
case subcmd
|
147
|
-
when
|
146
|
+
when "add", "echo"
|
148
147
|
Passfile.send(subcmd, filename, algorithm)
|
149
|
-
when
|
148
|
+
when "remove", "list"
|
150
149
|
Passfile.send(subcmd, filename)
|
151
150
|
else
|
152
151
|
puts "Valid subcommands are: add | remove | list"
|
@@ -165,6 +164,7 @@ module Itsi
|
|
165
164
|
new_name = "#{base}_#{i}#{ext}"
|
166
165
|
candidate = File.join(dir, new_name)
|
167
166
|
return candidate unless File.exist?(candidate)
|
167
|
+
|
168
168
|
i += 1
|
169
169
|
end
|
170
170
|
end
|
@@ -189,11 +189,11 @@ module Itsi
|
|
189
189
|
|
190
190
|
case alg
|
191
191
|
when /^HS(\d+)$/
|
192
|
-
bits =
|
192
|
+
bits = ::Regexp.last_match(1).to_i
|
193
193
|
bytes = bits / 8
|
194
194
|
key = SecureRandom.random_bytes(bytes)
|
195
195
|
pem = Base64.strict_encode64(key)
|
196
|
-
content =
|
196
|
+
content = "=== HMAC #{bits}-bit Secret (base64) ===\n#{pem}\n"
|
197
197
|
save_or_print("hmac_#{bits}_secret.txt", content, options)
|
198
198
|
|
199
199
|
when /^RS/, /^PS/
|
@@ -215,32 +215,33 @@ module Itsi
|
|
215
215
|
save_or_print("ecdsa_public.pem", "=== ECDSA Public Key ===\n#{pub}", options)
|
216
216
|
|
217
217
|
else
|
218
|
-
|
218
|
+
warn "Unsupported algorithm: #{alg}"
|
219
219
|
exit 1
|
220
220
|
end
|
221
221
|
end
|
222
222
|
|
223
|
-
|
224
223
|
def add_worker
|
225
|
-
return unless pid = get_pid
|
224
|
+
return unless (pid = get_pid)
|
226
225
|
|
227
226
|
Process.kill(:TTIN, pid)
|
228
227
|
end
|
229
228
|
|
230
229
|
def remove_worker
|
231
|
-
return unless pid = get_pid
|
230
|
+
return unless (pid = get_pid)
|
232
231
|
|
233
232
|
Process.kill(:TTOU, pid)
|
234
233
|
end
|
235
234
|
|
236
235
|
def status
|
237
|
-
return unless pid = get_pid
|
236
|
+
return unless (pid = get_pid)
|
237
|
+
|
238
238
|
Itsi.log_info("Itsi running on #{pid}")
|
239
239
|
Process.kill(:USR2, pid)
|
240
240
|
end
|
241
241
|
|
242
242
|
def load_route_middleware_stack(cli_params)
|
243
|
-
middleware, errors = Config.build_config(cli_params,
|
243
|
+
middleware, errors = Config.build_config(cli_params,
|
244
|
+
Itsi::Server::Config.config_file_path(cli_params[:config_file_path]))
|
244
245
|
if errors.any?
|
245
246
|
puts errors
|
246
247
|
[]
|
@@ -271,7 +272,6 @@ module Itsi
|
|
271
272
|
end
|
272
273
|
|
273
274
|
alias serve start
|
274
|
-
|
275
275
|
end
|
276
276
|
end
|
277
277
|
end
|
@@ -1,86 +1,86 @@
|
|
1
1
|
module Itsi
|
2
2
|
module StandardHeaders
|
3
3
|
ALL = [
|
4
|
-
ACCEPT = "accept",
|
5
|
-
ACCEPT_CHARSET = "accept-charset",
|
6
|
-
ACCEPT_ENCODING = "accept-encoding",
|
7
|
-
ACCEPT_LANGUAGE = "accept-language",
|
8
|
-
ACCEPT_RANGES = "accept-ranges",
|
9
|
-
ACCESS_CONTROL_ALLOW_CREDENTIALS = "access-control-allow-credentials",
|
10
|
-
ACCESS_CONTROL_ALLOW_HEADERS = "access-control-allow-headers",
|
11
|
-
ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods",
|
12
|
-
ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin",
|
13
|
-
ACCESS_CONTROL_EXPOSE_HEADERS = "access-control-expose-headers",
|
14
|
-
ACCESS_CONTROL_MAX_AGE = "access-control-max-age",
|
15
|
-
ACCESS_CONTROL_REQUEST_HEADERS = "access-control-request-headers",
|
16
|
-
ACCESS_CONTROL_REQUEST_METHOD = "access-control-request-method",
|
17
|
-
AGE = "age",
|
18
|
-
ALLOW = "allow",
|
19
|
-
ALT_SVC = "alt-svc",
|
20
|
-
AUTHORIZATION = "authorization",
|
21
|
-
CACHE_CONTROL = "cache-control",
|
22
|
-
CACHE_STATUS = "cache-status",
|
23
|
-
CDN_CACHE_CONTROL = "cdn-cache-control",
|
24
|
-
CONNECTION = "connection",
|
25
|
-
CONTENT_DISPOSITION = "content-disposition",
|
26
|
-
CONTENT_ENCODING = "content-encoding",
|
27
|
-
CONTENT_LANGUAGE = "content-language",
|
28
|
-
CONTENT_LENGTH = "content-length",
|
29
|
-
CONTENT_LOCATION = "content-location",
|
30
|
-
CONTENT_RANGE = "content-range",
|
31
|
-
CONTENT_SECURITY_POLICY_REPORT_ONLY = "content-security-policy-report-only",
|
32
|
-
CONTENT_TYPE = "content-type",
|
33
|
-
COOKIE = "cookie",
|
34
|
-
DNT = "dnt",
|
35
|
-
DATE = "date",
|
36
|
-
ETAG = "etag",
|
37
|
-
EXPECT = "expect",
|
38
|
-
EXPIRES = "expires",
|
39
|
-
FORWARDED = "forwarded",
|
40
|
-
FROM = "from",
|
41
|
-
HOST = "host",
|
42
|
-
IF_MATCH = "if-match",
|
43
|
-
IF_MODIFIED_SINCE = "if-modified-since",
|
44
|
-
IF_NONE_MATCH = "if-none-match",
|
45
|
-
IF_RANGE = "if-range",
|
46
|
-
IF_UNMODIFIED_SINCE = "if-unmodified-since",
|
47
|
-
LAST_MODIFIED = "last-modified",
|
48
|
-
LINK = "link",
|
49
|
-
LOCATION = "location",
|
50
|
-
MAX_FORWARDS = "max-forwards",
|
51
|
-
ORIGIN = "origin",
|
52
|
-
PRAGMA = "pragma",
|
53
|
-
PROXY_AUTHENTICATE = "proxy-authenticate",
|
54
|
-
PROXY_AUTHORIZATION = "proxy-authorization",
|
55
|
-
PUBLIC_KEY_PINS = "public-key-pins",
|
56
|
-
PUBLIC_KEY_PINS_REPORT_ONLY = "public-key-pins-report-only",
|
57
|
-
RANGE = "range",
|
58
|
-
REFERER = "referer",
|
59
|
-
REFERRER_POLICY = "referrer-policy",
|
60
|
-
REFRESH = "refresh",
|
61
|
-
RETRY_AFTER = "retry-after",
|
62
|
-
SEC_WEBSOCKET_ACCEPT = "sec-websocket-accept",
|
63
|
-
SEC_WEBSOCKET_EXTENSIONS = "sec-websocket-extensions",
|
64
|
-
SEC_WEBSOCKET_KEY = "sec-websocket-key",
|
65
|
-
SEC_WEBSOCKET_PROTOCOL = "sec-websocket-protocol",
|
66
|
-
SEC_WEBSOCKET_VERSION = "sec-websocket-version",
|
67
|
-
SERVER = "server",
|
68
|
-
SET_COOKIE = "set-cookie",
|
69
|
-
STRICT_TRANSPORT_SECURITY = "strict-transport-security",
|
70
|
-
TE = "te",
|
71
|
-
TRAILER = "trailer",
|
72
|
-
TRANSFER_ENCODING = "transfer-encoding",
|
73
|
-
USER_AGENT = "user-agent",
|
74
|
-
UPGRADE = "upgrade",
|
75
|
-
UPGRADE_INSECURE_REQUESTS = "upgrade-insecure-requests",
|
76
|
-
VARY = "vary",
|
77
|
-
VIA = "via",
|
78
|
-
WARNING = "warning",
|
79
|
-
WWW_AUTHENTICATE = "www-authenticate",
|
80
|
-
X_CONTENT_TYPE_OPTIONS = "x-content-type-options",
|
81
|
-
X_DNS_PREFETCH_CONTROL = "x-dns-prefetch-control",
|
82
|
-
X_FRAME_OPTIONS = "x-frame-options",
|
83
|
-
X_XSS_PROTECTION = "x-xss-protection"
|
4
|
+
ACCEPT = "accept".freeze,
|
5
|
+
ACCEPT_CHARSET = "accept-charset".freeze,
|
6
|
+
ACCEPT_ENCODING = "accept-encoding".freeze,
|
7
|
+
ACCEPT_LANGUAGE = "accept-language".freeze,
|
8
|
+
ACCEPT_RANGES = "accept-ranges".freeze,
|
9
|
+
ACCESS_CONTROL_ALLOW_CREDENTIALS = "access-control-allow-credentials".freeze,
|
10
|
+
ACCESS_CONTROL_ALLOW_HEADERS = "access-control-allow-headers".freeze,
|
11
|
+
ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods".freeze,
|
12
|
+
ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin".freeze,
|
13
|
+
ACCESS_CONTROL_EXPOSE_HEADERS = "access-control-expose-headers".freeze,
|
14
|
+
ACCESS_CONTROL_MAX_AGE = "access-control-max-age".freeze,
|
15
|
+
ACCESS_CONTROL_REQUEST_HEADERS = "access-control-request-headers".freeze,
|
16
|
+
ACCESS_CONTROL_REQUEST_METHOD = "access-control-request-method".freeze,
|
17
|
+
AGE = "age".freeze,
|
18
|
+
ALLOW = "allow".freeze,
|
19
|
+
ALT_SVC = "alt-svc".freeze,
|
20
|
+
AUTHORIZATION = "authorization".freeze,
|
21
|
+
CACHE_CONTROL = "cache-control".freeze,
|
22
|
+
CACHE_STATUS = "cache-status".freeze,
|
23
|
+
CDN_CACHE_CONTROL = "cdn-cache-control".freeze,
|
24
|
+
CONNECTION = "connection".freeze,
|
25
|
+
CONTENT_DISPOSITION = "content-disposition".freeze,
|
26
|
+
CONTENT_ENCODING = "content-encoding".freeze,
|
27
|
+
CONTENT_LANGUAGE = "content-language".freeze,
|
28
|
+
CONTENT_LENGTH = "content-length".freeze,
|
29
|
+
CONTENT_LOCATION = "content-location".freeze,
|
30
|
+
CONTENT_RANGE = "content-range".freeze,
|
31
|
+
CONTENT_SECURITY_POLICY_REPORT_ONLY = "content-security-policy-report-only".freeze,
|
32
|
+
CONTENT_TYPE = "content-type".freeze,
|
33
|
+
COOKIE = "cookie".freeze,
|
34
|
+
DNT = "dnt".freeze,
|
35
|
+
DATE = "date".freeze,
|
36
|
+
ETAG = "etag".freeze,
|
37
|
+
EXPECT = "expect".freeze,
|
38
|
+
EXPIRES = "expires".freeze,
|
39
|
+
FORWARDED = "forwarded".freeze,
|
40
|
+
FROM = "from".freeze,
|
41
|
+
HOST = "host".freeze,
|
42
|
+
IF_MATCH = "if-match".freeze,
|
43
|
+
IF_MODIFIED_SINCE = "if-modified-since".freeze,
|
44
|
+
IF_NONE_MATCH = "if-none-match".freeze,
|
45
|
+
IF_RANGE = "if-range".freeze,
|
46
|
+
IF_UNMODIFIED_SINCE = "if-unmodified-since".freeze,
|
47
|
+
LAST_MODIFIED = "last-modified".freeze,
|
48
|
+
LINK = "link".freeze,
|
49
|
+
LOCATION = "location".freeze,
|
50
|
+
MAX_FORWARDS = "max-forwards".freeze,
|
51
|
+
ORIGIN = "origin".freeze,
|
52
|
+
PRAGMA = "pragma".freeze,
|
53
|
+
PROXY_AUTHENTICATE = "proxy-authenticate".freeze,
|
54
|
+
PROXY_AUTHORIZATION = "proxy-authorization".freeze,
|
55
|
+
PUBLIC_KEY_PINS = "public-key-pins".freeze,
|
56
|
+
PUBLIC_KEY_PINS_REPORT_ONLY = "public-key-pins-report-only".freeze,
|
57
|
+
RANGE = "range".freeze,
|
58
|
+
REFERER = "referer".freeze,
|
59
|
+
REFERRER_POLICY = "referrer-policy".freeze,
|
60
|
+
REFRESH = "refresh".freeze,
|
61
|
+
RETRY_AFTER = "retry-after".freeze,
|
62
|
+
SEC_WEBSOCKET_ACCEPT = "sec-websocket-accept".freeze,
|
63
|
+
SEC_WEBSOCKET_EXTENSIONS = "sec-websocket-extensions".freeze,
|
64
|
+
SEC_WEBSOCKET_KEY = "sec-websocket-key".freeze,
|
65
|
+
SEC_WEBSOCKET_PROTOCOL = "sec-websocket-protocol".freeze,
|
66
|
+
SEC_WEBSOCKET_VERSION = "sec-websocket-version".freeze,
|
67
|
+
SERVER = "server".freeze,
|
68
|
+
SET_COOKIE = "set-cookie".freeze,
|
69
|
+
STRICT_TRANSPORT_SECURITY = "strict-transport-security".freeze,
|
70
|
+
TE = "te".freeze,
|
71
|
+
TRAILER = "trailer".freeze,
|
72
|
+
TRANSFER_ENCODING = "transfer-encoding".freeze,
|
73
|
+
USER_AGENT = "user-agent".freeze,
|
74
|
+
UPGRADE = "upgrade".freeze,
|
75
|
+
UPGRADE_INSECURE_REQUESTS = "upgrade-insecure-requests".freeze,
|
76
|
+
VARY = "vary".freeze,
|
77
|
+
VIA = "via".freeze,
|
78
|
+
WARNING = "warning".freeze,
|
79
|
+
WWW_AUTHENTICATE = "www-authenticate".freeze,
|
80
|
+
X_CONTENT_TYPE_OPTIONS = "x-content-type-options".freeze,
|
81
|
+
X_DNS_PREFETCH_CONTROL = "x-dns-prefetch-control".freeze,
|
82
|
+
X_FRAME_OPTIONS = "x-frame-options".freeze,
|
83
|
+
X_XSS_PROTECTION = "x-xss-protection".freeze
|
84
84
|
]
|
85
85
|
end
|
86
86
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
ENV["ITSI_LOG"] = "off"
|
3
4
|
|
4
5
|
require "minitest/reporters"
|
@@ -8,14 +9,13 @@ require "socket"
|
|
8
9
|
require "net/http"
|
9
10
|
require "minitest/autorun"
|
10
11
|
|
11
|
-
|
12
12
|
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
13
13
|
|
14
|
-
def free_bind(protocol="http", unix_socket: false)
|
14
|
+
def free_bind(protocol = "http", unix_socket: false)
|
15
15
|
if unix_socket
|
16
16
|
socket_path = "/tmp/itsi_socket_#{Process.pid}_#{rand(1000)}.sock"
|
17
17
|
UNIXServer.new(socket_path).close
|
18
|
-
protocol ==
|
18
|
+
protocol == "https" ? "tls://#{socket_path}" : "unix://#{socket_path}"
|
19
19
|
else
|
20
20
|
server = TCPServer.new("0.0.0.0", 0)
|
21
21
|
port = server.addr[1]
|
@@ -24,7 +24,6 @@ def free_bind(protocol="http", unix_socket: false)
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
27
|
def server(app: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, cleanup: true, timeout: 5, &blk)
|
29
28
|
itsi_rb ||= lambda do
|
30
29
|
# Inline Itsi.rb
|
@@ -49,7 +48,7 @@ def server(app: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil,
|
|
49
48
|
sync.pop
|
50
49
|
uri = URI(bind)
|
51
50
|
# Timeout.timeout(timeout) do
|
52
|
-
|
51
|
+
RequestContext.new(uri, self).instance_exec(uri, &blk)
|
53
52
|
# end
|
54
53
|
rescue StandardError => e
|
55
54
|
puts e
|
@@ -60,9 +59,9 @@ ensure
|
|
60
59
|
Itsi::Server.stop_background_threads if cleanup
|
61
60
|
end
|
62
61
|
|
63
|
-
require
|
64
|
-
require
|
65
|
-
require
|
62
|
+
require "net/http"
|
63
|
+
require "net_http_unix"
|
64
|
+
require "uri"
|
66
65
|
|
67
66
|
class RequestContext
|
68
67
|
def initialize(uri, binding)
|
@@ -74,7 +73,7 @@ class RequestContext
|
|
74
73
|
@binding.send(method_name, *args, &block)
|
75
74
|
end
|
76
75
|
|
77
|
-
def post(path, data="", headers = {})
|
76
|
+
def post(path, data = "", headers = {})
|
78
77
|
client.post(uri_for(path), data, headers)
|
79
78
|
end
|
80
79
|
|
@@ -101,7 +100,7 @@ class RequestContext
|
|
101
100
|
client.request(request)
|
102
101
|
end
|
103
102
|
|
104
|
-
def put(path, data="", headers = {})
|
103
|
+
def put(path, data = "", headers = {})
|
105
104
|
request = Net::HTTP::Put.new(uri_for(path))
|
106
105
|
request.body = data
|
107
106
|
headers.each { |k, v| request[k] = v }
|
@@ -114,7 +113,7 @@ class RequestContext
|
|
114
113
|
client.request(request)
|
115
114
|
end
|
116
115
|
|
117
|
-
def patch(path, data="", headers = {})
|
116
|
+
def patch(path, data = "", headers = {})
|
118
117
|
request = Net::HTTP::Patch.new(uri_for(path))
|
119
118
|
request.body = data
|
120
119
|
client.request(request)
|
@@ -127,21 +126,23 @@ class RequestContext
|
|
127
126
|
read_timeout: 1,
|
128
127
|
open_timeout: 1
|
129
128
|
}
|
130
|
-
if @uri.scheme ==
|
129
|
+
if @uri.scheme == "unix"
|
131
130
|
NetX::HTTPUnix.new(
|
132
131
|
@uri.to_s,
|
133
|
-
**opts
|
132
|
+
**opts
|
133
|
+
)
|
134
134
|
else
|
135
135
|
Net::HTTP.start(
|
136
136
|
@uri.host,
|
137
137
|
@uri.port,
|
138
|
-
use_ssl: @uri.scheme ==
|
139
|
-
**opts
|
138
|
+
use_ssl: @uri.scheme == "https",
|
139
|
+
**opts
|
140
|
+
)
|
140
141
|
end
|
141
142
|
end
|
142
143
|
|
143
144
|
def uri_for(path)
|
144
|
-
if @uri.scheme ==
|
145
|
+
if @uri.scheme == "unix"
|
145
146
|
URI::HTTP.build(path: path, host: "localhost")
|
146
147
|
else
|
147
148
|
URI.join(@uri.to_s, path)
|
@@ -1,17 +1,69 @@
|
|
1
1
|
require_relative "../helpers/test_helper"
|
2
2
|
|
3
3
|
class TestLogRequests < Minitest::Test
|
4
|
-
|
4
|
+
# 1. before‑only logging
|
5
5
|
def test_it_supports_logging_before_requests
|
6
|
+
stdout, = capture_subprocess_io do
|
7
|
+
server(
|
8
|
+
itsi_rb: lambda do
|
9
|
+
log_level :info
|
10
|
+
log_requests \
|
11
|
+
before: {
|
12
|
+
level: "INFO",
|
13
|
+
format: "[{request_id}] BEFORE {method} {path_and_query}"
|
14
|
+
}
|
15
|
+
get("/foo?bar=baz") { |r| r.ok "ok" }
|
16
|
+
end
|
17
|
+
) do
|
18
|
+
get_resp("/foo?bar=baz")
|
19
|
+
end
|
20
|
+
end
|
6
21
|
|
22
|
+
# should emit something like "[a1b2c3] BEFORE GET /foo?bar=baz"
|
23
|
+
assert_match(%r{\[[0-9a-f]+\] BEFORE GET /foo\?bar=baz}, stdout)
|
7
24
|
end
|
8
25
|
|
26
|
+
# 2. after‑only logging
|
9
27
|
def test_it_supports_logging_after_requests
|
28
|
+
stdout, = capture_subprocess_io do
|
29
|
+
server(
|
30
|
+
itsi_rb: lambda do
|
31
|
+
log_level :info
|
32
|
+
log_requests \
|
33
|
+
after: {
|
34
|
+
level: "INFO",
|
35
|
+
format: "[{request_id}] AFTER {status} in {response_time}"
|
36
|
+
}
|
37
|
+
get("/foo") { |r| r.ok "ok" }
|
38
|
+
end
|
39
|
+
) do
|
40
|
+
get_resp("/foo")
|
41
|
+
end
|
42
|
+
end
|
10
43
|
|
44
|
+
# should emit something like "[d4e5f6] AFTER 200 in 1.234ms"
|
45
|
+
assert_match(/\[[0-9a-f]+\] AFTER 200 in \d+\.\d+ms/, stdout)
|
11
46
|
end
|
12
47
|
|
48
|
+
# 3. custom log‑level is honored
|
13
49
|
def test_it_supports_configuring_log_level
|
50
|
+
stdout, = capture_subprocess_io do
|
51
|
+
server(
|
52
|
+
itsi_rb: lambda do
|
53
|
+
log_level :error
|
54
|
+
log_requests \
|
55
|
+
before: {
|
56
|
+
level: "ERROR",
|
57
|
+
format: "X"
|
58
|
+
}
|
59
|
+
get("/foo") { |r| r.ok "ok" }
|
60
|
+
end
|
61
|
+
) do
|
62
|
+
get_resp("/foo")
|
63
|
+
end
|
64
|
+
end
|
14
65
|
|
66
|
+
# our line should begin with "ERROR" and then our literal "X"
|
67
|
+
assert_match(/ERROR.*X/, stdout)
|
15
68
|
end
|
16
|
-
|
17
69
|
end
|