itsi 0.1.20 → 0.2.2

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 (319) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -8
  3. data/Cargo.lock +2 -2
  4. data/LICENSE.txt +698 -0
  5. data/README.md +15 -4
  6. data/Rakefile +9 -5
  7. data/crates/itsi_acme/.gitignore +4 -0
  8. data/crates/itsi_acme/Cargo.toml +86 -0
  9. data/crates/itsi_acme/LICENSE-APACHE +201 -0
  10. data/crates/itsi_acme/LICENSE-MIT +23 -0
  11. data/crates/itsi_acme/README.md +9 -0
  12. data/crates/itsi_acme/examples/high_level.rs +63 -0
  13. data/crates/itsi_acme/examples/high_level_warp.rs +52 -0
  14. data/crates/itsi_acme/examples/low_level.rs +87 -0
  15. data/crates/itsi_acme/examples/low_level_axum.rs +66 -0
  16. data/crates/itsi_acme/src/acceptor.rs +81 -0
  17. data/crates/itsi_acme/src/acme.rs +354 -0
  18. data/crates/itsi_acme/src/axum.rs +86 -0
  19. data/crates/itsi_acme/src/cache.rs +39 -0
  20. data/crates/itsi_acme/src/caches/boxed.rs +80 -0
  21. data/crates/itsi_acme/src/caches/composite.rs +69 -0
  22. data/crates/itsi_acme/src/caches/dir.rs +106 -0
  23. data/crates/itsi_acme/src/caches/mod.rs +11 -0
  24. data/crates/itsi_acme/src/caches/no.rs +78 -0
  25. data/crates/itsi_acme/src/caches/test.rs +136 -0
  26. data/crates/itsi_acme/src/config.rs +172 -0
  27. data/crates/itsi_acme/src/https_helper.rs +69 -0
  28. data/crates/itsi_acme/src/incoming.rs +142 -0
  29. data/crates/itsi_acme/src/jose.rs +161 -0
  30. data/crates/itsi_acme/src/lib.rs +142 -0
  31. data/crates/itsi_acme/src/resolver.rs +59 -0
  32. data/crates/itsi_acme/src/state.rs +424 -0
  33. data/crates/itsi_rb_helpers/src/lib.rs +4 -3
  34. data/crates/itsi_scheduler/Cargo.toml +1 -1
  35. data/crates/itsi_scheduler/src/itsi_scheduler.rs +8 -2
  36. data/crates/itsi_scheduler/src/lib.rs +1 -0
  37. data/crates/itsi_server/Cargo.toml +1 -1
  38. data/crates/itsi_server/src/lib.rs +2 -1
  39. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +18 -1
  40. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +11 -3
  41. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +122 -63
  42. data/crates/itsi_server/src/ruby_types/itsi_server.rs +2 -0
  43. data/crates/itsi_server/src/server/binds/bind.rs +3 -0
  44. data/crates/itsi_server/src/server/binds/listener.rs +12 -5
  45. data/crates/itsi_server/src/server/binds/tls.rs +13 -5
  46. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +12 -5
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +8 -1
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +9 -1
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +48 -43
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +11 -2
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +39 -12
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +36 -27
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/csp.rs +25 -11
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +12 -3
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +74 -72
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +15 -1
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +11 -8
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +19 -11
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +5 -5
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +2 -2
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +11 -5
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +17 -20
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +19 -8
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +16 -37
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +22 -12
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +26 -11
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +7 -1
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +14 -4
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +19 -0
  70. data/crates/itsi_server/src/server/middleware_stack/mod.rs +49 -13
  71. data/crates/itsi_server/src/server/mod.rs +1 -0
  72. data/crates/itsi_server/src/server/redirect_type.rs +26 -0
  73. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +22 -16
  74. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +49 -12
  75. data/crates/itsi_server/src/server/signal.rs +1 -0
  76. data/crates/itsi_server/src/server/size_limited_incoming.rs +6 -0
  77. data/crates/itsi_server/src/server/thread_worker.rs +5 -1
  78. data/crates/itsi_server/src/services/itsi_http_service.rs +20 -2
  79. data/crates/itsi_server/src/services/rate_limiter.rs +15 -4
  80. data/crates/itsi_server/src/services/static_file_server.rs +33 -19
  81. data/crates/itsi_tracing/src/lib.rs +42 -22
  82. data/docs/content/_index.md +1 -2
  83. data/docs/content/acknowledgements/_index.md +5 -2
  84. data/docs/content/configuration/_index.md +8 -5
  85. data/docs/content/contact/_index.md +8 -1
  86. data/docs/content/faqs/_index.md +5 -3
  87. data/docs/content/features/_index.md +56 -50
  88. data/docs/content/getting_started/_index.md +8 -5
  89. data/docs/content/getting_started/local_development.md +68 -8
  90. data/docs/content/getting_started/logging.md +16 -9
  91. data/docs/content/getting_started/running_itsi_in_production.md +5 -3
  92. data/docs/content/getting_started/signals.md +38 -0
  93. data/docs/content/itsi_scheduler/_index.md +8 -7
  94. data/docs/content/utilities/_index.md +13 -0
  95. data/docs/content/utilities/config_file_testing.md +17 -0
  96. data/docs/content/utilities/passfile_generator.md +41 -0
  97. data/docs/content/utilities/route_testing.md +27 -0
  98. data/docs/content/utilities/secrets_management.md +30 -0
  99. data/docs/hugo.yaml +1 -1
  100. data/fairytale.txt +3 -4
  101. data/gems/scheduler/Cargo.lock +1 -1
  102. data/gems/scheduler/README.md +4 -5
  103. data/gems/scheduler/Rakefile +0 -4
  104. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  105. data/gems/scheduler/lib/itsi/scheduler.rb +9 -4
  106. data/gems/scheduler/test/test_active_record.rb +12 -7
  107. data/gems/server/Cargo.lock +1 -1
  108. data/gems/server/Rakefile +0 -4
  109. data/gems/server/exe/itsi +13 -2
  110. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +2 -0
  111. data/gems/server/lib/itsi/http_request.rb +40 -9
  112. data/gems/server/lib/itsi/http_response.rb +2 -1
  113. data/gems/server/lib/itsi/passfile.rb +0 -1
  114. data/gems/server/lib/itsi/server/config/config_helpers.rb +20 -8
  115. data/gems/server/lib/itsi/server/config/dsl.rb +20 -435
  116. data/gems/server/lib/itsi/server/config/known_paths.rb +4 -1
  117. data/gems/server/lib/itsi/server/config/middleware/_index.md +6 -4
  118. data/gems/server/lib/itsi/server/config/middleware/allow_list.md +46 -0
  119. data/gems/server/lib/itsi/server/config/middleware/allow_list.rb +42 -0
  120. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.md +90 -0
  121. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.rb +51 -0
  122. data/gems/server/lib/itsi/server/config/middleware/auth_basic.md +45 -0
  123. data/gems/server/lib/itsi/server/config/middleware/auth_basic.rb +44 -0
  124. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.md +82 -0
  125. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.rb +38 -0
  126. data/gems/server/lib/itsi/server/config/middleware/cache_control.md +78 -0
  127. data/gems/server/lib/itsi/server/config/middleware/cache_control.rb +45 -0
  128. data/gems/server/lib/itsi/server/config/middleware/cidr_to_regex.rb +50 -0
  129. data/gems/server/lib/itsi/server/config/middleware/compression.md +50 -0
  130. data/gems/server/lib/itsi/server/config/middleware/compression.rb +37 -0
  131. data/gems/server/lib/itsi/server/config/middleware/cors.md +93 -0
  132. data/gems/server/lib/itsi/server/config/middleware/cors.rb +32 -0
  133. data/gems/server/lib/itsi/server/config/middleware/csp.md +37 -0
  134. data/gems/server/lib/itsi/server/config/middleware/csp.rb +44 -0
  135. data/gems/server/lib/itsi/server/config/middleware/deny_list.md +45 -0
  136. data/gems/server/lib/itsi/server/config/middleware/deny_list.rb +42 -0
  137. data/gems/server/lib/itsi/server/config/middleware/endpoint/_index.md +159 -0
  138. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.md +186 -0
  139. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.rb +33 -0
  140. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.md +12 -0
  141. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.rb +42 -0
  142. data/gems/server/lib/itsi/server/config/middleware/endpoint/endpoint.rb +99 -0
  143. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.md +12 -0
  144. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.rb +42 -0
  145. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_request.md +44 -0
  146. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_response.md +39 -0
  147. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.md +12 -0
  148. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.rb +42 -0
  149. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.md +12 -0
  150. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.rb +42 -0
  151. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.md +12 -0
  152. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.rb +42 -0
  153. data/gems/server/lib/itsi/server/config/middleware/endpoint/schemas.md +122 -0
  154. data/gems/server/lib/itsi/server/config/middleware/error_response.md +61 -0
  155. data/gems/server/lib/itsi/server/config/middleware/error_response.rb +36 -0
  156. data/gems/server/lib/itsi/server/config/middleware/etag.md +59 -0
  157. data/gems/server/lib/itsi/server/config/middleware/etag.rb +27 -0
  158. data/gems/server/lib/itsi/server/config/middleware/grpc.md +172 -0
  159. data/gems/server/lib/itsi/server/config/middleware/grpc.rb +54 -0
  160. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.md +124 -0
  161. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.rb +61 -0
  162. data/gems/server/lib/itsi/server/config/middleware/location.md +107 -0
  163. data/gems/server/lib/itsi/server/config/middleware/location.rb +99 -0
  164. data/gems/server/lib/itsi/server/config/middleware/log_requests.md +13 -11
  165. data/gems/server/lib/itsi/server/config/middleware/log_requests.rb +1 -3
  166. data/gems/server/lib/itsi/server/config/middleware/max_body.md +18 -0
  167. data/gems/server/lib/itsi/server/config/middleware/max_body.rb +21 -0
  168. data/gems/server/lib/itsi/server/config/middleware/proxy.md +62 -0
  169. data/gems/server/lib/itsi/server/config/middleware/proxy.rb +41 -0
  170. data/gems/server/lib/itsi/server/config/middleware/rackup_file.md +54 -0
  171. data/gems/server/lib/itsi/server/config/middleware/rackup_file.rb +44 -0
  172. data/gems/server/lib/itsi/server/config/middleware/rate_limit.md +126 -0
  173. data/gems/server/lib/itsi/server/config/middleware/rate_limit.rb +34 -0
  174. data/gems/server/lib/itsi/server/config/middleware/rate_limit_store.rb +25 -0
  175. data/gems/server/lib/itsi/server/config/middleware/redirect.md +55 -0
  176. data/gems/server/lib/itsi/server/config/middleware/redirect.rb +25 -0
  177. data/gems/server/lib/itsi/server/config/middleware/request_headers.md +34 -0
  178. data/gems/server/lib/itsi/server/config/middleware/request_headers.rb +24 -0
  179. data/gems/server/lib/itsi/server/config/middleware/response_headers.md +33 -0
  180. data/gems/server/lib/itsi/server/config/middleware/response_headers.rb +25 -0
  181. data/gems/server/lib/itsi/server/config/middleware/run.md +60 -0
  182. data/gems/server/lib/itsi/server/config/middleware/run.rb +43 -0
  183. data/gems/server/lib/itsi/server/config/middleware/static_assets.md +73 -0
  184. data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +87 -0
  185. data/gems/server/lib/itsi/server/config/middleware/static_response.md +44 -0
  186. data/gems/server/lib/itsi/server/config/middleware/static_response.rb +29 -0
  187. data/gems/server/lib/itsi/server/config/middleware/string_rewrite.md +67 -0
  188. data/gems/server/lib/itsi/server/config/middleware/token_source.rb +32 -0
  189. data/gems/server/lib/itsi/server/config/middleware.rb +4 -0
  190. data/gems/server/lib/itsi/server/config/option.rb +5 -0
  191. data/gems/server/lib/itsi/server/config/options/_index.md +3 -2
  192. data/gems/server/lib/itsi/server/config/options/auto_reload_config.md +13 -0
  193. data/gems/server/lib/itsi/server/config/options/auto_reload_config.rb +41 -0
  194. data/gems/server/lib/itsi/server/config/options/bind.md +71 -0
  195. data/gems/server/lib/itsi/server/config/options/bind.rb +26 -0
  196. data/gems/server/lib/itsi/server/config/options/certificates.md +65 -0
  197. data/gems/server/lib/itsi/server/config/options/daemonize.md +14 -0
  198. data/gems/server/lib/itsi/server/config/options/daemonize.rb +19 -0
  199. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.md +1 -2
  200. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.rb +6 -3
  201. data/gems/server/lib/itsi/server/config/options/header_read_timeout.md +17 -0
  202. data/gems/server/lib/itsi/server/config/options/header_read_timeout.rb +19 -0
  203. data/gems/server/lib/itsi/server/config/options/hooks/_index.md +11 -0
  204. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.md +13 -0
  205. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.rb +28 -0
  206. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.md +14 -0
  207. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.rb +28 -0
  208. data/gems/server/lib/itsi/server/config/options/hooks/after_start.md +12 -0
  209. data/gems/server/lib/itsi/server/config/options/hooks/after_start.rb +28 -0
  210. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.md +13 -0
  211. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.rb +28 -0
  212. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.md +12 -0
  213. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.rb +28 -0
  214. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.md +12 -0
  215. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.rb +28 -0
  216. data/gems/server/lib/itsi/server/config/options/include.md +20 -0
  217. data/gems/server/lib/itsi/server/config/options/include.rb +36 -0
  218. data/gems/server/lib/itsi/server/config/options/listen_backlog.md +11 -0
  219. data/gems/server/lib/itsi/server/config/options/listen_backlog.rb +19 -0
  220. data/gems/server/lib/itsi/server/config/options/log_format.md +18 -0
  221. data/gems/server/lib/itsi/server/config/options/log_format.rb +19 -0
  222. data/gems/server/lib/itsi/server/config/options/log_level.md +34 -0
  223. data/gems/server/lib/itsi/server/config/options/log_level.rb +20 -0
  224. data/gems/server/lib/itsi/server/config/options/log_target.md +38 -0
  225. data/gems/server/lib/itsi/server/config/options/log_target.rb +19 -0
  226. data/gems/server/lib/itsi/server/config/options/log_target_filters.md +17 -0
  227. data/gems/server/lib/itsi/server/config/options/log_target_filters.rb +19 -0
  228. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.md +27 -0
  229. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.rb +24 -0
  230. data/gems/server/lib/itsi/server/config/options/nodelay.md +16 -0
  231. data/gems/server/lib/itsi/server/config/options/nodelay.rb +19 -0
  232. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.md +19 -0
  233. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.rb +18 -0
  234. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.md +17 -0
  235. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.rb +19 -0
  236. data/gems/server/lib/itsi/server/config/options/preload.md +21 -0
  237. data/gems/server/lib/itsi/server/config/options/preload.rb +18 -0
  238. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.md +15 -0
  239. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.rb +19 -0
  240. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.md +21 -0
  241. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.rb +30 -0
  242. data/gems/server/lib/itsi/server/config/options/request_timeout.md +23 -0
  243. data/gems/server/lib/itsi/server/config/options/request_timeout.rb +19 -0
  244. data/gems/server/lib/itsi/server/config/options/reuse_address.md +16 -0
  245. data/gems/server/lib/itsi/server/config/options/reuse_address.rb +19 -0
  246. data/gems/server/lib/itsi/server/config/options/reuse_port.md +16 -0
  247. data/gems/server/lib/itsi/server/config/options/reuse_port.rb +19 -0
  248. data/gems/server/lib/itsi/server/config/options/scheduler_threads.md +34 -0
  249. data/gems/server/lib/itsi/server/config/options/scheduler_threads.rb +17 -0
  250. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.md +17 -0
  251. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.rb +19 -0
  252. data/gems/server/lib/itsi/server/config/options/stream_body.md +32 -0
  253. data/gems/server/lib/itsi/server/config/options/stream_body.rb +18 -0
  254. data/gems/server/lib/itsi/server/config/options/threads.md +7 -2
  255. data/gems/server/lib/itsi/server/config/options/threads.rb +1 -1
  256. data/gems/server/lib/itsi/server/config/options/watch.md +16 -0
  257. data/gems/server/lib/itsi/server/config/options/watch.rb +28 -0
  258. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.md +22 -0
  259. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.rb +18 -0
  260. data/gems/server/lib/itsi/server/config/options/workers.md +1 -2
  261. data/gems/server/lib/itsi/server/config/options/workers.rb +1 -1
  262. data/gems/server/lib/itsi/server/config/typed_struct.rb +59 -20
  263. data/gems/server/lib/itsi/server/config.rb +77 -48
  264. data/gems/server/lib/itsi/server/default_config/Itsi.rb +3 -3
  265. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +1 -1
  266. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +11 -4
  267. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +3 -3
  268. data/gems/server/lib/itsi/server/route_tester.rb +58 -8
  269. data/gems/server/lib/itsi/server/signal_trap.rb +1 -1
  270. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +14 -18
  271. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +5 -4
  272. data/gems/server/lib/itsi/server/typed_handlers.rb +12 -4
  273. data/gems/server/lib/itsi/server/version.rb +1 -1
  274. data/gems/server/lib/itsi/server.rb +98 -14
  275. data/gems/server/lib/ruby_lsp/itsi/addon.rb +20 -18
  276. data/gems/server/test/helpers/test_helper.rb +89 -29
  277. data/gems/server/test/middleware/allow_list.rb +128 -0
  278. data/gems/server/test/middleware/auth_api_key.rb +141 -0
  279. data/gems/server/test/middleware/auth_basic.rb +91 -0
  280. data/gems/server/test/middleware/auth_jwt.rb +214 -0
  281. data/gems/server/test/middleware/cache_control.rb +82 -0
  282. data/gems/server/test/middleware/cidr_to_regex.rb +46 -0
  283. data/gems/server/test/middleware/compression.rb +89 -0
  284. data/gems/server/test/middleware/cors.rb +113 -0
  285. data/gems/server/test/middleware/csp.rb +62 -0
  286. data/gems/server/test/middleware/deny_list.rb +131 -0
  287. data/gems/server/test/middleware/endpoint.rb +300 -0
  288. data/gems/server/test/middleware/etag.rb +75 -0
  289. data/gems/server/test/middleware/grpc/grpc.rb +158 -0
  290. data/gems/server/test/middleware/grpc/test_service.proto +32 -0
  291. data/gems/server/test/middleware/grpc/test_service_impl.rb +28 -0
  292. data/gems/server/test/middleware/grpc/test_service_pb.rb +18 -0
  293. data/gems/server/test/middleware/grpc/test_service_services_pb.rb +30 -0
  294. data/gems/server/test/middleware/header_interpolation.rb +35 -0
  295. data/gems/server/test/middleware/intrusion_protection.rb +259 -0
  296. data/gems/server/test/middleware/location.rb +220 -0
  297. data/gems/server/test/middleware/max_body.rb +20 -0
  298. data/gems/server/test/middleware/proxy.rb +415 -0
  299. data/gems/server/test/middleware/rate_limit.rb +211 -0
  300. data/gems/server/test/middleware/redirect.rb +85 -0
  301. data/gems/server/test/middleware/request_headers.rb +50 -0
  302. data/gems/server/test/middleware/response_headers.rb +50 -0
  303. data/gems/server/test/middleware/static_assets.rb +374 -0
  304. data/gems/server/test/middleware/static_response.rb +56 -0
  305. data/gems/server/test/middleware/string_rewrite.rb +112 -0
  306. data/gems/server/test/options/bind.rb +47 -0
  307. data/gems/server/test/options/header_read_timeout.rb +23 -0
  308. data/gems/server/test/options/test_request_timeout.rb +16 -0
  309. data/gems/server/test/options/test_workers.rb +2 -4
  310. data/gems/server/test/{test_itsi_server.rb → rack/test_rack_server.rb} +2 -2
  311. data/grpc_test/Itsi.rb +11 -0
  312. data/grpc_test/echo.proto +14 -0
  313. data/grpc_test/echo_pb.rb +16 -0
  314. data/grpc_test/echo_service_impl.rb +8 -0
  315. data/grpc_test/echo_services_pb.rb +22 -0
  316. data/lib/itsi/version.rb +1 -1
  317. data/tasks.txt +15 -72
  318. metadata +210 -7
  319. data/gems/server/lib/itsi/server/default_config/Itsi-rackup.rb +0 -119
@@ -17,6 +17,9 @@ require_relative "http_response"
17
17
  require_relative "passfile"
18
18
  require_relative "../shell_completions/completions"
19
19
 
20
+ require "securerandom"
21
+ require "fileutils"
22
+
20
23
  module Itsi
21
24
  class Server
22
25
  extend RackInterface
@@ -26,19 +29,24 @@ module Itsi
26
29
  class << self
27
30
 
28
31
  def running?
29
- !!@running
32
+ @running && !@running.empty?
30
33
  end
31
34
 
32
35
  def start_in_background_thread(cli_params = {}, &blk)
33
- @background_thread = start(cli_params, background: true, &blk)
36
+ @background_threads ||= []
37
+ server, background_thread = start(cli_params, background: true, &blk)
38
+ @background_threads << background_thread
39
+ server
34
40
  end
35
41
 
36
42
  def start(cli_params, background: false, &blk)
37
43
  itsi_file = Itsi::Server::Config.config_file_path(cli_params[:config_file])
44
+ Itsi.log_debug "Constructing server #{cli_params}: #{itsi_file}"
38
45
  server = new(cli_params, itsi_file, blk)
39
46
  previous_handler = Signal.trap(:INT, :DEFAULT)
40
47
  run = lambda do
41
- @running = server
48
+ @running ||= []
49
+ @running << server
42
50
 
43
51
  if cli_params[:daemonize]
44
52
  Itsi.log_info("Itsi is running in the background. Writing pid to #{Itsi::Server::Config.pid_file_path}")
@@ -47,13 +55,16 @@ module Itsi
47
55
  end
48
56
  write_pid
49
57
 
58
+ Itsi.log_info "Starting Itsi..."
50
59
  server.start
51
- @running = nil
60
+ @running.delete(server)
52
61
  Signal.trap(:INT, previous_handler)
53
62
  server
54
63
  end
55
- background ? Thread.new(&run) : run[]
56
- rescue
64
+ background ? [server, Thread.new(&run)] : run[]
65
+ rescue Exception => e
66
+ Itsi.log_error e.message
67
+ raise e
57
68
  end
58
69
 
59
70
  def static(cli_params)
@@ -74,9 +85,11 @@ module Itsi
74
85
  end
75
86
  end
76
87
 
77
- def stop_background_thread
78
- @running&.stop
79
- @background_thread&.join
88
+ def stop_background_threads
89
+ @running && @running.each(&:stop)
90
+ @background_threads&.each(&:join)
91
+ @background_threads = []
92
+ @running = []
80
93
  end
81
94
 
82
95
  def write_pid
@@ -118,7 +131,7 @@ module Itsi
118
131
 
119
132
  def passfile(options, subcmd)
120
133
  filename = options[:passfile]
121
- unless filename
134
+ unless filename || subcmd == 'echo'
122
135
  puts "Error: passfile not set. Use --passfile option to provide a path to a file containing hashed credentials."
123
136
  puts "This file contains hashed credentials and should not be included in source control without additional protection."
124
137
  exit(1)
@@ -141,6 +154,73 @@ module Itsi
141
154
  end
142
155
  end
143
156
 
157
+ def unique_path(dir, filename)
158
+ base = File.basename(filename, ".*")
159
+ ext = File.extname(filename)
160
+ candidate = File.join(dir, filename)
161
+ return candidate unless File.exist?(candidate)
162
+
163
+ i = 1
164
+ loop do
165
+ new_name = "#{base}_#{i}#{ext}"
166
+ candidate = File.join(dir, new_name)
167
+ return candidate unless File.exist?(candidate)
168
+ i += 1
169
+ end
170
+ end
171
+
172
+ def save_or_print(filename, content, options)
173
+ if options[:save_dir]
174
+ FileUtils.mkdir_p(options[:save_dir])
175
+ path = unique_path(options[:save_dir], filename)
176
+ File.write(path, content)
177
+ puts "Written to #{path}"
178
+ else
179
+ puts content
180
+ end
181
+ end
182
+
183
+ def secret(options)
184
+ require "openssl"
185
+ require "base64"
186
+
187
+ puts "Enter algorithm (one of: HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384):"
188
+ alg = $stdin.gets.chomp.upcase
189
+
190
+ case alg
191
+ when /^HS(\d+)$/
192
+ bits = $1.to_i
193
+ bytes = bits / 8
194
+ key = SecureRandom.random_bytes(bytes)
195
+ pem = Base64.strict_encode64(key)
196
+ content = "=== HMAC #{bits}-bit Secret (base64) ===\n#{pem}\n"
197
+ save_or_print("hmac_#{bits}_secret.txt", content, options)
198
+
199
+ when /^RS/, /^PS/
200
+ rsa = OpenSSL::PKey::RSA.new(2048)
201
+ priv = rsa.to_pem
202
+ pub = rsa.public_key.to_pem
203
+ save_or_print("rsa_private.pem", "=== RSA Private Key ===\n#{priv}", options)
204
+ save_or_print("rsa_public.pem", "=== RSA Public Key ===\n#{pub}", options)
205
+
206
+ when "ES256", "ES384"
207
+ curve = (alg == "ES256" ? "prime256v1" : "secp384r1")
208
+ ec = OpenSSL::PKey::EC.new(curve)
209
+ ec.generate_key
210
+ priv = ec.to_pem
211
+ pub_ec = ec.dup
212
+ pub_ec.private_key = nil
213
+ pub = pub_ec.to_pem
214
+ save_or_print("ecdsa_private.pem", "=== ECDSA Private Key ===\n#{priv}", options)
215
+ save_or_print("ecdsa_public.pem", "=== ECDSA Public Key ===\n#{pub}", options)
216
+
217
+ else
218
+ STDERR.puts "Unsupported algorithm: #{alg}"
219
+ exit 1
220
+ end
221
+ end
222
+
223
+
144
224
  def add_worker
145
225
  return unless pid = get_pid
146
226
 
@@ -160,12 +240,16 @@ module Itsi
160
240
  end
161
241
 
162
242
  def load_route_middleware_stack(cli_params)
163
- Config.build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file_path]))[
164
- "middleware_loader"
165
- ][]
243
+ middleware, errors = Config.build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file_path]))
244
+ if errors.any?
245
+ puts errors
246
+ []
247
+ else
248
+ middleware["middleware_loader"][]
249
+ end
166
250
  end
167
251
 
168
- def test_route(cli_params = {}, route_str)
252
+ def test_route(cli_params, route_str)
169
253
  matched_route = load_route_middleware_stack(cli_params).find do |route|
170
254
  route["route"] =~ route_str
171
255
  end
@@ -100,24 +100,26 @@ module RubyLsp
100
100
  end
101
101
 
102
102
  ::Itsi::Server::Config::Middleware.subclasses.each do |middleware|
103
- completion_item = Interface::CompletionItem.new(
104
- label: middleware.middleware_name,
105
- kind: Constant::CompletionItemKind::METHOD,
106
- label_details: Interface::CompletionItemLabelDetails.new(
107
- detail: middleware.detail,
108
- description: middleware.documentation
109
- ),
110
- documentation: Interface::MarkupContent.new(
111
- kind: Constant::MarkupKind::MARKDOWN,
112
- value: middleware.documentation
113
- ),
114
- insert_text: middleware.insert_text,
115
- insert_text_format: Constant::InsertTextFormat::SNIPPET,
116
- data: {
117
- delegateCompletion: true
118
- }
119
- )
120
- @response_builder << completion_item
103
+ Array(middleware.insert_text).zip(Array(middleware.detail)).each do |insert_text, detail|
104
+ completion_item = Interface::CompletionItem.new(
105
+ label: middleware.middleware_name,
106
+ kind: Constant::CompletionItemKind::METHOD,
107
+ label_details: Interface::CompletionItemLabelDetails.new(
108
+ detail: detail,
109
+ description: middleware.documentation
110
+ ),
111
+ documentation: Interface::MarkupContent.new(
112
+ kind: Constant::MarkupKind::MARKDOWN,
113
+ value: middleware.documentation
114
+ ),
115
+ insert_text: insert_text,
116
+ insert_text_format: Constant::InsertTextFormat::SNIPPET,
117
+ data: {
118
+ delegateCompletion: true
119
+ }
120
+ )
121
+ @response_builder << completion_item
122
+ end
121
123
  end
122
124
  end
123
125
 
@@ -11,14 +11,21 @@ require "minitest/autorun"
11
11
 
12
12
  Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
13
13
 
14
- def free_bind(protocol)
15
- server = TCPServer.new("0.0.0.0", 0)
16
- port = server.addr[1]
17
- server.close
18
- "#{protocol}://0.0.0.0:#{port}"
14
+ def free_bind(protocol="http", unix_socket: false)
15
+ if unix_socket
16
+ socket_path = "/tmp/itsi_socket_#{Process.pid}_#{rand(1000)}.sock"
17
+ UNIXServer.new(socket_path).close
18
+ protocol == 'https' ? "tls://#{socket_path}" : "unix://#{socket_path}"
19
+ else
20
+ server = TCPServer.new("0.0.0.0", 0)
21
+ port = server.addr[1]
22
+ server.close
23
+ "#{protocol}://0.0.0.0:#{port}"
24
+ end
19
25
  end
20
26
 
21
- def server(app: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, &blk)
27
+
28
+ def server(app: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, cleanup: true, timeout: 5, &blk)
22
29
  itsi_rb ||= lambda do
23
30
  # Inline Itsi.rb
24
31
  bind bind
@@ -31,17 +38,32 @@ def server(app: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil,
31
38
  cli_params = {}
32
39
  cli_params[:binds] = [bind] if bind
33
40
 
41
+ sync = Queue.new
42
+ cli_params[:hooks] ||= {}
43
+ cli_params[:hooks]["after_start"] = lambda do
44
+ sync.push(true)
45
+ end
46
+
34
47
  Itsi::Server.start_in_background_thread(cli_params, &itsi_rb)
48
+
49
+ sync.pop
35
50
  uri = URI(bind)
36
- RequestContext.new(uri, self).instance_exec(uri, &blk)
51
+ # Timeout.timeout(timeout) do
52
+ RequestContext.new(uri, self).instance_exec(uri, &blk)
53
+ # end
37
54
  rescue StandardError => e
38
55
  puts e
39
- puts e.message
40
- puts e.backtrace.join("\n")
56
+ # puts e.message
57
+ # puts e.backtrace.join("\n")
58
+ raise
41
59
  ensure
42
- Itsi::Server.stop_background_thread
60
+ Itsi::Server.stop_background_threads if cleanup
43
61
  end
44
62
 
63
+ require 'net/http'
64
+ require 'net_http_unix'
65
+ require 'uri'
66
+
45
67
  class RequestContext
46
68
  def initialize(uri, binding)
47
69
  @uri = uri
@@ -52,39 +74,77 @@ class RequestContext
52
74
  @binding.send(method_name, *args, &block)
53
75
  end
54
76
 
55
- def post(path, data)
56
- Net::HTTP.post(@uri+path, data)
77
+ def post(path, data="", headers = {})
78
+ client.post(uri_for(path), data, headers)
57
79
  end
58
80
 
59
- def get(path)
60
- Net::HTTP.get(@uri+path)
81
+ def get(path, headers = {})
82
+ request = Net::HTTP::Get.new(uri_for(path))
83
+ headers.each { |k, v| request[k] = v }
84
+ client.request(request).body
61
85
  end
62
86
 
63
- def get_resp(path)
64
- Net::HTTP.get_response(@uri+path)
87
+ def get_resp(path, headers = {})
88
+ request = Net::HTTP::Get.new(uri_for(path))
89
+ headers.each { |k, v| request[k] = v }
90
+ client.request(request)
65
91
  end
66
92
 
67
93
  def head(path)
68
- Net::HTTP.start(@uri.host, @uri.port) {|http|
69
- http.head(path)
70
- }
94
+ request = Net::HTTP::Head.new(uri_for(path))
95
+ client.request(request)
71
96
  end
72
97
 
73
- def options(path)
74
- Net::HTTP.start(@uri.host, @uri.port) {|http|
75
- http.options(path)
76
- }
98
+ def options(path, headers = {})
99
+ request = Net::HTTP::Options.new(uri_for(path))
100
+ headers.each { |k, v| request[k] = v }
101
+ client.request(request)
102
+ end
103
+
104
+ def put(path, data="", headers = {})
105
+ request = Net::HTTP::Put.new(uri_for(path))
106
+ request.body = data
107
+ headers.each { |k, v| request[k] = v }
108
+ client.request(request)
77
109
  end
78
110
 
79
- def put(path, data)
80
- Net::HTTP.put(@uri+path, data)
111
+ def delete(path, headers = {})
112
+ request = Net::HTTP::Delete.new(uri_for(path))
113
+ headers.each { |k, v| request[k] = v }
114
+ client.request(request)
81
115
  end
82
116
 
83
- def delete(path)
84
- Net::HTTP.delete(@uri+path)
117
+ def patch(path, data="", headers = {})
118
+ request = Net::HTTP::Patch.new(uri_for(path))
119
+ request.body = data
120
+ client.request(request)
121
+ end
122
+
123
+ private
124
+
125
+ def client
126
+ opts = {
127
+ read_timeout: 1,
128
+ open_timeout: 1
129
+ }
130
+ if @uri.scheme == 'unix'
131
+ NetX::HTTPUnix.new(
132
+ @uri.to_s,
133
+ **opts)
134
+ else
135
+ Net::HTTP.start(
136
+ @uri.host,
137
+ @uri.port,
138
+ use_ssl: @uri.scheme == 'https',
139
+ **opts)
140
+ end
85
141
  end
86
142
 
87
- def patch(path, data)
88
- Net::HTTP.patch(@uri+path, data)
143
+ def uri_for(path)
144
+ if @uri.scheme == 'unix'
145
+ URI::HTTP.build(path: path, host: "localhost")
146
+ else
147
+ URI.join(@uri.to_s, path)
148
+ end
89
149
  end
90
150
  end
@@ -0,0 +1,128 @@
1
+ # test_allow_list.rb
2
+ require_relative "../helpers/test_helper"
3
+
4
+ class TestAllowList < Minitest::Test
5
+ # 1. Single‐pattern: only localhost is allowed
6
+ def test_single_pattern_allows_and_denies
7
+ server(
8
+ itsi_rb: lambda do
9
+ allow_list allowed_patterns: ["^127\\.0\\.0\\.1$"]
10
+ get("/foo") { |r| r.ok "allowed" }
11
+ end
12
+ ) do
13
+ # Our test client always comes from 127.0.0.1
14
+ res1 = get_resp("/foo")
15
+ assert_equal "200", res1.code
16
+ assert_equal "allowed", res1.body
17
+
18
+ # If we change the pattern so it no longer matches, 127.0.0.1 is now forbidden
19
+ server(
20
+ itsi_rb: lambda do
21
+ allow_list allowed_patterns: ["^10\\.0\\."]
22
+ get("/foo") { |r| r.ok "never" }
23
+ end
24
+ ) do
25
+ res2 = get_resp("/foo")
26
+ assert_equal "403", res2.code
27
+ end
28
+ end
29
+ end
30
+
31
+ # 2. Multiple‐pattern: localhost or 192.168.x.x
32
+ def test_multiple_patterns
33
+ server(
34
+ itsi_rb: lambda do
35
+ allow_list allowed_patterns: ["^127\\.0\\.0\\.1$", "^192\\.168\\."]
36
+ get("/ping") { |r| r.ok "pong" }
37
+ end
38
+ ) do
39
+ # localhost matches first pattern
40
+ res1 = get_resp("/ping")
41
+ assert_equal "200", res1.code
42
+ assert_equal "pong", res1.body
43
+
44
+ # If we restrict to only 192.168.*, localhost becomes forbidden
45
+ server(
46
+ itsi_rb: lambda do
47
+ allow_list allowed_patterns: ["^192\\.168\\."]
48
+ get("/ping") { |r| r.ok "never" }
49
+ end
50
+ ) do
51
+ res2 = get_resp("/ping")
52
+ assert_equal "403", res2.code
53
+ end
54
+ end
55
+ end
56
+
57
+ # 3. Custom error_response
58
+ def test_custom_error_response
59
+ server(
60
+ itsi_rb: lambda do
61
+ allow_list \
62
+ allowed_patterns: ["^192\\.168\\."], # localhost no longer matches
63
+ error_response: {
64
+ code: 403,
65
+ plaintext: { inline: "No access" },
66
+ default: "plaintext"
67
+ }
68
+ get("/x") { |r| r.ok "never" }
69
+ end
70
+ ) do
71
+ res = get_resp("/x")
72
+ assert_equal "403", res.code
73
+ assert_equal "No access", res.body
74
+ end
75
+ end
76
+
77
+ # 4. Trusted proxies: extract client IP from header if proxy is trusted
78
+ def test_trusted_proxy_allows_based_on_header
79
+ server(
80
+ itsi_rb: lambda do
81
+ allow_list \
82
+ allowed_patterns: ["^203\\.0\\.113\\.7$"], # only allow this client IP
83
+ trusted_proxies: {
84
+ "127.0.0.1" => { header: { name: "X-Forwarded-For" } }
85
+ }
86
+ get("/trusted") { |r| r.ok "trusted" }
87
+ end
88
+ ) do
89
+ res = get_resp("/trusted", { "X-Forwarded-For" => "203.0.113.7" })
90
+ assert_equal "200", res.code
91
+ assert_equal "trusted", res.body
92
+ end
93
+ end
94
+
95
+ def test_trusted_proxy_denies_if_forwarded_ip_does_not_match
96
+ server(
97
+ itsi_rb: lambda do
98
+ allow_list \
99
+ allowed_patterns: ["^203\\.0\\.113\\.7$"], # only allow this
100
+ trusted_proxies: {
101
+ "127.0.0.1" => { header: { name: "X-Forwarded-For" } }
102
+ }
103
+ get("/trusted") { |r| r.ok "nope" }
104
+ end
105
+ ) do
106
+ # Send a forwarded IP that doesn't match the allow list
107
+ res = get_resp("/trusted", { "X-Forwarded-For" => "192.0.2.55" })
108
+ assert_equal "403", res.code
109
+ end
110
+ end
111
+
112
+ def test_untrusted_proxy_ignores_forwarded_ip
113
+ server(
114
+ itsi_rb: lambda do
115
+ allow_list \
116
+ allowed_patterns: ["^203\\.0\\.113\\.7$"], # client IP matches, but header is ignored
117
+ trusted_proxies: {
118
+ "10.0.0.1" => { header: { name: "X-Forwarded-For" } } # current proxy (127.0.0.1) is not trusted
119
+ }
120
+ get("/trusted") { |r| r.ok "never" }
121
+ end
122
+ ) do
123
+ res = get_resp("/trusted", { "X-Forwarded-For" => "203.0.113.7" })
124
+ # Since proxy is untrusted, header is ignored, and 127.0.0.1 is checked (not allowed)
125
+ assert_equal "403", res.code
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,141 @@
1
+ require_relative "../helpers/test_helper"
2
+
3
+ class TestAuthApiKey < Minitest::Test
4
+
5
+ # 1. Anonymous inline keys: valid secret in Authorization header
6
+ def test_anonymous_inline_success
7
+ server(
8
+ itsi_rb: lambda do
9
+ auth_api_key valid_keys: [
10
+ Itsi.create_password_hash("supersecret", "sha256")
11
+ ]
12
+ get("/foo") {|r| r.ok "ok" }
13
+ end
14
+ ) do
15
+ res = get_resp("/foo", { "Authorization" => "Bearer supersecret" })
16
+
17
+ assert_equal "200", res.code
18
+ assert_equal "ok", res.body
19
+ end
20
+ end
21
+
22
+ # 2. Anonymous inline: missing token → 401
23
+ def test_anonymous_inline_missing
24
+ server(
25
+ itsi_rb: lambda do
26
+ auth_api_key valid_keys: [Itsi.create_password_hash("supersecret", "sha256")]
27
+ get("/foo") {|r| r.ok "never" }
28
+ end
29
+ ) do
30
+ res = get_resp("/foo")
31
+ assert_equal "401", res.code
32
+ end
33
+ end
34
+
35
+ # 3. Identified inline keys: need both ID header and Bearer token
36
+ def test_identified_inline_success
37
+ key_id = "Key-1"
38
+ server(
39
+ itsi_rb: lambda do
40
+ auth_api_key valid_keys: { "#{key_id}" => Itsi.create_password_hash("supersecret", "sha256") }
41
+ get("/bar") {|r| r.ok "bar OK" }
42
+ end
43
+ ) do
44
+ headers = {
45
+ "X-Api-Key-Id" => key_id,
46
+ "Authorization" => "Bearer supersecret"
47
+ }
48
+ res = get_resp("/bar", headers)
49
+ assert_equal "200", res.code
50
+ assert_equal "bar OK", res.body
51
+ end
52
+ end
53
+
54
+ # 4. Identified inline: wrong ID → 401
55
+ def test_identified_inline_wrong_id
56
+ key_id = "Key-1"
57
+ server(
58
+ itsi_rb: lambda do
59
+ auth_api_key valid_keys: { "#{key_id}" => Itsi.create_password_hash("supersecret", "sha256") }
60
+ get("/bar") {|r| r.ok "never" }
61
+ end
62
+ ) do
63
+ headers = { "X-Api-Key-Id" => "bad", "Authorization" => "Bearer supersecret" }
64
+ res = get_resp("/bar", headers)
65
+ assert_equal "401", res.code
66
+ end
67
+ end
68
+
69
+ # 5. Custom token_source/query param
70
+ def test_custom_token_source_query
71
+ server(
72
+ itsi_rb: lambda do
73
+ auth_api_key \
74
+ valid_keys: [Itsi.create_password_hash("supersecret", "sha256")],
75
+ token_source: { query: "api_key" }
76
+ get("/q") {|r| r.ok "qok" }
77
+ end
78
+ ) do
79
+ res = get_resp("/q?api_key=supersecret")
80
+ assert_equal "200", res.code
81
+ assert_equal "qok", res.body
82
+ end
83
+ end
84
+
85
+ # 6. Custom key_id_source/query param
86
+ def test_custom_key_id_source_query
87
+ server(
88
+ itsi_rb: lambda do
89
+ auth_api_key \
90
+ valid_keys: { "#{@id1}" => Itsi.create_password_hash("supersecret", "sha256") },
91
+ key_id_source: { query: "kid" }
92
+ get("/q") {|r| r.ok "qok" }
93
+ end
94
+ ) do
95
+ res = get_resp("/q?kid=#{@id1}", { "Authorization" => "Bearer supersecret" })
96
+ assert_equal "200", res.code
97
+ assert_equal "qok", res.body
98
+ end
99
+ end
100
+
101
+ # 7. Custom error_response override
102
+ def test_custom_error_response
103
+ server(
104
+ itsi_rb: lambda do
105
+ auth_api_key \
106
+ valid_keys: [Itsi.create_password_hash("supersecret", "sha256") ],
107
+ error_response: { code: 403,
108
+ plaintext: { inline: "nope" },
109
+ default: "plaintext" }
110
+ get("/x") {|r| r.ok "never" }
111
+ end
112
+ ) do
113
+ res = get_resp("/x")
114
+ assert_equal "403", res.code
115
+ assert_equal "nope", res.body
116
+ end
117
+ end
118
+
119
+ # 8. Scoped to a location path
120
+ def test_scoped_to_location
121
+ server(
122
+ itsi_rb: lambda do
123
+ location "/admin/*" do
124
+ auth_api_key valid_keys: [Itsi.create_password_hash("supersecret", "sha256") ]
125
+ end
126
+ get("/admin/secret") {|r| r.ok "adm ok" }
127
+ get("/public") {|r| r.ok "pub ok" }
128
+ end
129
+ ) do
130
+ # Unprotected
131
+ r1 = get_resp("/public")
132
+ assert_equal "200", r1.code
133
+ # Protected
134
+ r2 = get_resp("/admin/secret")
135
+ assert_equal "401", r2.code
136
+ # Then with key
137
+ r3 = get_resp("/admin/secret", { "Authorization" => "Bearer supersecret" })
138
+ assert_equal "200", r3.code
139
+ end
140
+ end
141
+ end