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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/Cargo.lock +29 -30
  4. data/README.md +1 -0
  5. data/crates/itsi_scheduler/Cargo.toml +1 -1
  6. data/crates/itsi_server/Cargo.toml +1 -1
  7. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
  8. data/docs/content/_index.md +1 -1
  9. data/docs/content/contact/_index.md +1 -1
  10. data/docs/content/directory_listing.jpg +0 -0
  11. data/docs/content/error_page.jpg +0 -0
  12. data/docs/content/getting_started/local_development.md +9 -2
  13. data/docs/content/getting_started/logging.md +1 -2
  14. data/docs/content/getting_started/signals.md +0 -1
  15. data/docs/hugo.yaml +3 -0
  16. data/gems/scheduler/Cargo.lock +74 -17
  17. data/gems/scheduler/itsi-scheduler.gemspec +2 -2
  18. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  19. data/gems/server/Cargo.lock +28 -29
  20. data/gems/server/itsi-server.gemspec +2 -2
  21. data/gems/server/lib/itsi/http_request.rb +31 -34
  22. data/gems/server/lib/itsi/http_response.rb +10 -8
  23. data/gems/server/lib/itsi/passfile.rb +6 -6
  24. data/gems/server/lib/itsi/server/config/config_helpers.rb +33 -33
  25. data/gems/server/lib/itsi/server/config/dsl.rb +14 -19
  26. data/gems/server/lib/itsi/server/config/known_paths.rb +11 -7
  27. data/gems/server/lib/itsi/server/config/middleware/error_response.md +13 -0
  28. data/gems/server/lib/itsi/server/config/middleware/static_assets.md +40 -0
  29. data/gems/server/lib/itsi/server/config/option.rb +0 -1
  30. data/gems/server/lib/itsi/server/config/options/nodelay.md +2 -2
  31. data/gems/server/lib/itsi/server/config/options/reuse_address.md +1 -1
  32. data/gems/server/lib/itsi/server/config/typed_struct.rb +32 -35
  33. data/gems/server/lib/itsi/server/config.rb +107 -92
  34. data/gems/server/lib/itsi/server/default_app/default_app.rb +1 -1
  35. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +4 -5
  36. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +6 -7
  37. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +0 -1
  38. data/gems/server/lib/itsi/server/rack_interface.rb +0 -1
  39. data/gems/server/lib/itsi/server/route_tester.rb +25 -23
  40. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
  41. data/gems/server/lib/itsi/server/version.rb +1 -1
  42. data/gems/server/lib/itsi/server.rb +21 -21
  43. data/gems/server/lib/itsi/standard_headers.rb +80 -80
  44. data/gems/server/test/helpers/test_helper.rb +17 -16
  45. data/gems/server/test/middleware/test_log_requests.rb +54 -2
  46. data/gems/server/test/options/test_workers.rb +12 -5
  47. data/lib/itsi/version.rb +1 -1
  48. metadata +9 -13
  49. data/examples/static_assets_example.rb +0 -83
  50. data/grpc_test/Itsi.rb +0 -11
  51. data/grpc_test/echo.proto +0 -14
  52. data/grpc_test/echo_pb.rb +0 -16
  53. data/grpc_test/echo_service_impl.rb +0 -8
  54. 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['app_proc'].inspect.split(' ')[1][0...-1]})"
12
+ "\e[33mapp\e[0m(#{mw_args["app_proc"].inspect.split(" ")[1][0...-1]})"
13
13
  when "log_requests"
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]}...)"
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['algorithms'].join(' ')}, #{mw_args['mime_types']})"
22
+ "\e[33mcompress\e[0m(#{mw_args["algorithms"].join(" ")}, #{mw_args["mime_types"]})"
23
23
  when "cors"
24
- "\e[33mcors\e[0m(#{mw_args['allow_origins'].join(' ')}, #{mw_args['allow_methods'].join(' ')})"
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['type']}/#{mw_args['algorithm']}, #{mw_args['handle_if_none_match'] ? 'if_none_match' : ''})"
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['max_age']}, #{mw_args.select{|_,v| v == true }.keys.join(", ")})"
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['to']}, type: #{mw_args['type']})"
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['root_dir']})"
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['valid_keys'].keys}#{mw_args['credentials_file'] ? ", credentials_file: #{mw_args['credentials_file']}" : ""})"
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['realm']}#{mw_args['credentials_file'] ? ", credentials_file: #{mw_args['credentials_file']}" : ""})"
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['verifiers'].keys.join(",")})"
40
+ "\e[33mjwt_auth\e[0m(#{mw_args["verifiers"].keys.join(",")})"
39
41
  when "rate_limit"
40
- key = mw_args['key'].kind_of?(Hash) ? mw_args['key']["parameter"] : mw_args['key']
41
- "\e[33mrate_limit\e[0m(rps: #{mw_args['requests']}/#{mw_args['seconds']}, key: #{key})"
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['allowed_patterns'].join(", ")})"
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['denied_patterns'].join(", ")})"
47
+ "\e[33mdeny_list\e[0m(patterns: #{mw_args["denied_patterns"].join(", ")})"
46
48
  when "csp"
47
- "\e[33mcsp\e[0m(#{mw_args['policy'].map{|k,v| "#{k}: #{v.join(",")}"}.join(", ")})"
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['banned_url_patterns']&.length}, banned_header_patterns: #{mw_args['banned_header_patterns']&.keys&.join(", ")}, #{mw_args['banned_time_seconds']}s)"
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 'prism'
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 'Source Extraction Failed' unless intermediate.success?
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
- [ proc.parameters.select{|p| p == :req }&.length || 0, {}]
52
+ rescue StandardError
53
+ [proc.parameters.select { |p| p == :req }&.length || 0, {}]
52
54
  end
53
55
  end
54
56
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.3"
6
6
  end
7
7
  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!(cli_params = {})
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 == 'echo'
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, 'sha256')
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 'add', 'echo'
146
+ when "add", "echo"
148
147
  Passfile.send(subcmd, filename, algorithm)
149
- when 'remove', 'list'
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 = $1.to_i
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 = "=== HMAC #{bits}-bit Secret (base64) ===\n#{pem}\n"
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
- STDERR.puts "Unsupported algorithm: #{alg}"
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, Itsi::Server::Config.config_file_path(cli_params[:config_file_path]))
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 == 'https' ? "tls://#{socket_path}" : "unix://#{socket_path}"
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
- RequestContext.new(uri, self).instance_exec(uri, &blk)
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 'net/http'
64
- require 'net_http_unix'
65
- require 'uri'
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 == 'unix'
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 == 'https',
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 == 'unix'
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