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
@@ -0,0 +1,28 @@
1
+ require_relative 'test_service_services_pb' # generated by grpc_tools_ruby_protoc
2
+
3
+ class TestServiceImpl < Test::TestService::Service
4
+ # Unary: echo back the message [oai_citation_attribution:2‡Gustavo Caso](https://gustavocaso.dev/posts/grpc-tutorial-with-ruby/)
5
+ def unary_echo(req, _unused_call)
6
+ Test::EchoResponse.new(message: req.message)
7
+ end
8
+
9
+ # Client‑streaming: collect all incoming messages into one response [oai_citation_attribution:3‡Medium](https://alessiobussolari.medium.com/integrating-grpc-with-ruby-on-rails-2b2a203107d5?utm_source=chatgpt.com)
10
+ def client_stream(stream, _call)
11
+ msgs = stream.map(&:message)
12
+ Test::StreamResponse.new(messages: msgs)
13
+ end
14
+
15
+ # Server‑streaming: send back one StreamResponse per incoming request, with each char [oai_citation_attribution:4‡Medium](https://alessiobussolari.medium.com/integrating-grpc-with-ruby-on-rails-2b2a203107d5?utm_source=chatgpt.com)
16
+ def server_stream(req, _call)
17
+ Enumerator.new do |y|
18
+ req.message.each_char { |c| y << Test::StreamResponse.new(messages: [c]) }
19
+ end
20
+ end
21
+
22
+ # Bidirectional: for each incoming EchoRequest, immediately echo it back [oai_citation_attribution:5‡Medium](https://alessiobussolari.medium.com/integrating-grpc-with-ruby-on-rails-2b2a203107d5?utm_source=chatgpt.com)
23
+ def bidi_stream(stream, _call)
24
+ Enumerator.new do |y|
25
+ stream.each { |req| y << Test::EchoResponse.new(message: req.message.upcase) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: test_service.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n\x12test_service.proto\x12\x04test\"\x1e\n\x0b\x45\x63hoRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x1f\n\x0c\x45\x63hoResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\" \n\rStreamRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\"\n\x0eStreamResponse\x12\x10\n\x08messages\x18\x01 \x03(\t2\xfa\x01\n\x0bTestService\x12\x34\n\tUnaryEcho\x12\x11.test.EchoRequest\x1a\x12.test.EchoResponse\"\x00\x12=\n\x0c\x43lientStream\x12\x13.test.StreamRequest\x1a\x14.test.StreamResponse\"\x00(\x01\x12;\n\x0cServerStream\x12\x11.test.EchoRequest\x1a\x14.test.StreamResponse\"\x00\x30\x01\x12\x39\n\nBidiStream\x12\x11.test.EchoRequest\x1a\x12.test.EchoResponse\"\x00(\x01\x30\x01\x62\x06proto3"
9
+
10
+ pool = Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Test
14
+ EchoRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.EchoRequest").msgclass
15
+ EchoResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.EchoResponse").msgclass
16
+ StreamRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.StreamRequest").msgclass
17
+ StreamResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.StreamResponse").msgclass
18
+ end
@@ -0,0 +1,30 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: test_service.proto for package 'test'
3
+
4
+ require 'grpc'
5
+ require_relative 'test_service_pb'
6
+
7
+ module Test
8
+ module TestService
9
+ # A service with one of each RPC type [oai_citation_attribution:0‡Википедия — свободная энциклопедия](https://ru.wikipedia.org/wiki/GRPC?utm_source=chatgpt.com)
10
+ class Service
11
+
12
+ include ::GRPC::GenericService
13
+
14
+ self.marshal_class_method = :encode
15
+ self.unmarshal_class_method = :decode
16
+ self.service_name = 'test.TestService'
17
+
18
+ # Unary RPC
19
+ rpc :UnaryEcho, ::Test::EchoRequest, ::Test::EchoResponse
20
+ # Client‑streaming RPC
21
+ rpc :ClientStream, stream(::Test::StreamRequest), ::Test::StreamResponse
22
+ # Server‑streaming RPC
23
+ rpc :ServerStream, ::Test::EchoRequest, stream(::Test::StreamResponse)
24
+ # Bidirectional streaming RPC
25
+ rpc :BidiStream, stream(::Test::EchoRequest), stream(::Test::EchoResponse)
26
+ end
27
+
28
+ Stub = Service.rpc_stub_class
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ class TestHeaderInterpolation < Minitest::Test
2
+ def test_request_header_interpolation_and_response_header_interpolation
3
+ server(
4
+ itsi_rb: lambda do
5
+ # 1) Echo back incoming header "X-Auth-User" in a new request header "X-User"
6
+ request_headers \
7
+ additions: { "X-User" => ["{X-Auth-User}"] },
8
+ removals: []
9
+
10
+ # 2) After the handler runs, take the request‑echoed "X-User" and also echo it into "X-User-Echo" in the response
11
+ response_headers \
12
+ additions: { "X-User-Echo" => ["{X-User}"] },
13
+ removals: []
14
+
15
+ get("/echo") do |r|
16
+ # Return the value of the new X-User header in the body for verification
17
+ r.ok("Hello #{r.header("X-User").first}", headers: {"X-User" => [r.header("X-User").first]})
18
+ end
19
+ end
20
+ ) do
21
+ # Simulate a client sending X-Auth-User: alice
22
+ req_headers = { "X-Auth-User" => "alice" }
23
+
24
+ res = get_resp("/echo", req_headers)
25
+ assert_equal "200", res.code
26
+
27
+ # The request middleware should have created X-User == "alice",
28
+ # and the handler returned it in the body:
29
+ assert_equal "Hello alice", res.body
30
+
31
+ # The response middleware should have added X-User-Echo == "alice"
32
+ assert_equal "alice", res["X-User-Echo"]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,259 @@
1
+ require_relative "../helpers/test_helper"
2
+ require "redis"
3
+
4
+ class TestIntrusionProtection < Minitest::Test
5
+
6
+ # 1. Banned URL pattern causes immediate ban + 403
7
+ def test_banned_url_pattern
8
+ server(
9
+ itsi_rb: lambda do
10
+ location "/secret" do
11
+ intrusion_protection \
12
+ banned_url_patterns: ["/secret"],
13
+ banned_time_seconds: 0.1,
14
+ store_config: { redis: { connection_url: "redis://localhost:6379/14" } }
15
+ get { |r| r.ok "never" }
16
+ end
17
+ location "/public" do
18
+ get { |r| r.ok "public" }
19
+ end
20
+ end
21
+ ) do
22
+
23
+ # First request: matched → banned + forbidden
24
+ res1 = get_resp("/secret")
25
+ assert_equal "403", res1.code
26
+
27
+ # Immediately banned: second request also forbidden
28
+ res2 = get_resp("/secret")
29
+ assert_equal "403", res2.code
30
+
31
+ # After ban TTL expires
32
+ sleep 0.11
33
+
34
+ res3 = get_resp("/public")
35
+ assert_equal "200", res3.code
36
+ res4 = get_resp("/secret")
37
+ assert_equal "403", res4.code
38
+ end
39
+ end
40
+
41
+ # 2. Banned header pattern causes ban + 403
42
+ def test_banned_header_pattern
43
+ server(
44
+ itsi_rb: lambda do
45
+ intrusion_protection \
46
+ banned_header_patterns: { "User-Agent" => ["BadBot"] },
47
+ banned_time_seconds: 0.1,
48
+ store_config: "in_memory"
49
+ get("/hi") { |r| r.ok "hi" }
50
+ end
51
+ ) do
52
+ # First: header matches → banned
53
+ res1 = get_resp("/hi", { "User-Agent" => "MyBadBot/1.0" })
54
+ assert_equal "403", res1.code
55
+
56
+ # Still banned until TTL
57
+ res2 = get_resp("/hi", { "User-Agent" => "MyBadBot/1.0" })
58
+ assert_equal "403", res2.code
59
+
60
+ sleep 0.11
61
+ # After TTL, banned set clears; header still matches → ban again
62
+ res3 = get_resp("/hi", { "User-Agent" => "MyBadBot/1.0" })
63
+ assert_equal "403", res3.code
64
+ end
65
+ end
66
+
67
+ # 3. Clean traffic passes through
68
+ def test_clean_traffic
69
+ server(
70
+ itsi_rb: lambda do
71
+ intrusion_protection \
72
+ banned_url_patterns: [".*\\.php$"],
73
+ banned_header_patterns: { "X-Test" => ["evil"] },
74
+ banned_time_seconds: 0.1,
75
+ store_config: { redis: { connection_url: "redis://localhost:6379/13" } }
76
+ get("/hello") { |r| r.ok "world" }
77
+ end
78
+ ) do
79
+ # Non‑matching URL & header → allowed
80
+ res = get_resp("/hello", { "User-Agent" => "Mozilla/5.0" })
81
+ assert_equal "200", res.code
82
+ assert_equal "world", res.body
83
+ end
84
+ end
85
+
86
+ # 4. Custom error_response
87
+ def test_custom_error_response
88
+ server(
89
+ itsi_rb: lambda do
90
+ intrusion_protection \
91
+ banned_url_patterns: ["/bad"],
92
+ banned_header_patterns: {},
93
+ banned_time_seconds: 5,
94
+ store_config: "in_memory",
95
+ error_response: {
96
+ code: 401,
97
+ plaintext: { inline: "Halt!" },
98
+ default: "plaintext"
99
+ }
100
+ get("/bad") { |r| r.ok "never" }
101
+ end
102
+ ) do
103
+ res = get_resp("/bad")
104
+ assert_equal "401", res.code
105
+ assert_equal "Halt!", res.body
106
+ end
107
+ end
108
+
109
+ # 5. Intrusion protection middleware stacks (nested: parent + child)
110
+ def test_nested_intrusion_protection_stacking
111
+ server(
112
+ itsi_rb: lambda do
113
+ location "/protected" do
114
+ intrusion_protection \
115
+ banned_url_patterns: ["/nested"],
116
+ banned_time_seconds: 0.1,
117
+ store_config: "in_memory"
118
+
119
+ location "/nested" do
120
+ intrusion_protection \
121
+ banned_header_patterns: { "X-Evil" => ["1"] },
122
+ banned_time_seconds: 0.1,
123
+ store_config: "in_memory"
124
+ get { |r| r.ok "should not see this" }
125
+ end
126
+
127
+ get { |r| r.ok "Should also not see this" }
128
+ end
129
+ location "/public" do
130
+ get { |r| r.ok "safe" }
131
+ end
132
+ end
133
+ ) do
134
+ # 1. Triggers child (header) rule → ban
135
+ res1 = get_resp("protected/nested", { "X-Evil" => "1" })
136
+ assert_equal "403", res1.code
137
+
138
+ sleep 0.11
139
+
140
+ # 3. Triggers parent (path) rule → ban
141
+ res3 = get_resp("protected/nested")
142
+ assert_equal "403", res3.code
143
+
144
+ sleep 0.11
145
+
146
+ # 4. Confirm public route works
147
+ res4 = get_resp("/public")
148
+ assert_equal "200", res4.code
149
+ end
150
+ end
151
+
152
+ # 6. Sibling intrusion protection rules stack independently
153
+ def test_sibling_intrusion_protection_stacking
154
+ server(
155
+ itsi_rb: lambda do
156
+ location "/one" do
157
+ intrusion_protection \
158
+ banned_url_patterns: ["/one"],
159
+ banned_time_seconds: 0.1,
160
+ store_config: "in_memory"
161
+ get { |r| r.ok "never" }
162
+ end
163
+
164
+ location "/two" do
165
+ intrusion_protection \
166
+ banned_header_patterns: { "X-Bot" => ["true"] },
167
+ banned_time_seconds: 0.1,
168
+ store_config: "in_memory"
169
+ get { |r| r.ok "never" }
170
+ end
171
+
172
+ location "/ok" do
173
+ get { |r| r.ok "ok" }
174
+ end
175
+ end
176
+ ) do
177
+ # Route `/one` banned by path
178
+ res1 = get_resp("/one")
179
+ assert_equal "403", res1.code
180
+
181
+ # Route `/two` banned by header
182
+ res2 = get_resp("/two", { "X-Bot" => "true" })
183
+ assert_equal "403", res2.code
184
+
185
+ # Route `/ok` untouched
186
+ res3 = get_resp("/ok")
187
+ assert_equal "200", res3.code
188
+
189
+ # Wait for TTL to expire, confirm unban
190
+ sleep 0.11
191
+
192
+ # `/one` still banned due to path match → re-ban
193
+ res4 = get_resp("/one")
194
+ assert_equal "403", res4.code
195
+
196
+ # `/two` still banned by header → re-ban
197
+ res5 = get_resp("/two", { "X-Bot" => "true" })
198
+ assert_equal "403", res5.code
199
+ end
200
+ end
201
+
202
+ # 7. Trusted proxy: bans applied based on forwarded IP
203
+ def test_trusted_proxy_bans_based_on_forwarded_ip
204
+ server(
205
+ itsi_rb: lambda do
206
+ intrusion_protection \
207
+ banned_url_patterns: ["/flagged"],
208
+ banned_time_seconds: 0.1,
209
+ trusted_proxies: {
210
+ "127.0.0.1" => { header: { name: "X-Forwarded-For" } }
211
+ },
212
+ store_config: "in_memory"
213
+ get("/flagged") { |r| r.ok "should not see" }
214
+ get("/okay") { |r| r.ok "ok" }
215
+ end
216
+ ) do
217
+ # Request with client IP 203.0.113.42 via trusted proxy → triggers ban
218
+ res1 = get_resp("/flagged", { "X-Forwarded-For" => "203.0.113.42" })
219
+ assert_equal "403", res1.code
220
+
221
+ # Second request (same IP) still banned
222
+ res2 = get_resp("/flagged", { "X-Forwarded-For" => "203.0.113.42" })
223
+ assert_equal "403", res2.code
224
+
225
+ # Different client IP → not banned
226
+ res3 = get_resp("/flagged", { "X-Forwarded-For" => "203.0.113.99" })
227
+ assert_equal "403", res3.code # path still matches; gets banned
228
+
229
+ # Wait for first ban to expire
230
+ sleep 0.11
231
+ res4 = get_resp("/okay", { "X-Forwarded-For" => "203.0.113.42" })
232
+ assert_equal "200", res4.code
233
+ end
234
+ end
235
+
236
+ # 8. Untrusted proxy: forwarded IP ignored; ban keyed by socket IP
237
+ def test_untrusted_proxy_ignores_forwarded_ip
238
+ server(
239
+ itsi_rb: lambda do
240
+ intrusion_protection \
241
+ banned_url_patterns: ["/banned"],
242
+ banned_time_seconds: 0.1,
243
+ trusted_proxies: {
244
+ "10.0.0.1" => { header: { name: "X-Forwarded-For" } }
245
+ },
246
+ store_config: "in_memory"
247
+ get("/banned") { |r| r.ok "nope" }
248
+ end
249
+ ) do
250
+ # This header is ignored (sender IP is not trusted)
251
+ res1 = get_resp("/banned", { "X-Forwarded-For" => "198.51.100.7" })
252
+ assert_equal "403", res1.code
253
+
254
+ # Banned again based on socket IP, not spoofed one
255
+ res2 = get_resp("/banned", { "X-Forwarded-For" => "198.51.100.99" })
256
+ assert_equal "403", res2.code
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,220 @@
1
+ require_relative "../helpers/test_helper"
2
+
3
+ class TestNestedLocation < Minitest::Test
4
+ # Tests that a nested block takes precedence over its parent block
5
+ def test_basic_nested_route
6
+ server(
7
+ itsi_rb: lambda do
8
+ # The outer location "/" applies to all routes.
9
+ location "/" do
10
+ # A nested location matching "/foo" with its own GET handler.
11
+ location "/foo" do
12
+ get("/bar") { |r| r.respond("From nested /foo/bar") }
13
+ end
14
+ # This GET handler is only hit if no nested location applies.
15
+ get("/bar") { |r| r.respond("From root /bar") }
16
+ end
17
+ end
18
+ ) do
19
+ res_nested = get_resp("/foo/bar")
20
+ assert_equal "From nested /foo/bar", res_nested.body
21
+
22
+ res_root = get_resp("/bar")
23
+ assert_equal "From root /bar", res_root.body
24
+ end
25
+ end
26
+
27
+ # Tests deep nesting where dynamic segments are used
28
+ def test_deeply_nested_route
29
+ server(
30
+ itsi_rb: lambda do
31
+ location "/v1" do
32
+ location "/users" do
33
+ # A nested capture for user id.
34
+ location "/:user_id" do
35
+ get("/profile") { |r, params| r.respond("User profile for v1/users/#{params[:user_id]}") }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ ) do
41
+ res = get_resp("/v1/users/123/profile")
42
+ assert_equal "User profile for v1/users/123", res.body
43
+ end
44
+ end
45
+
46
+ # Tests that ordering of nested location blocks is honored. Only the first matching block should execute.
47
+ def test_nested_route_ordering
48
+ server(
49
+ itsi_rb: lambda do
50
+ location "/" do
51
+ location "/alpha" do
52
+ # This is the first nested match for "/alpha/beta"
53
+ location "/beta" do
54
+ get("/gamma") { |r| r.respond("Response from first nested /alpha/beta/gamma") }
55
+ end
56
+ # Though this block also matches "/alpha/beta", it should never be reached.
57
+ location "/beta" do
58
+ get("/gamma") { |r| r.respond("Response from second nested /alpha/beta/gamma") }
59
+ end
60
+ end
61
+ # This handler is defined on the outer level and is a fallback.
62
+ get("/alpha/beta/gamma") { |r| r.respond("Response from outer /alpha/beta/gamma") }
63
+ end
64
+ end
65
+ ) do
66
+ res = get_resp("/alpha/beta/gamma")
67
+ # According to the recursive matching rules, the first matching child (/alpha then /beta) is used.
68
+ assert_equal "Response from first nested /alpha/beta/gamma", res.body
69
+ end
70
+ end
71
+
72
+ # Tests multiple nested levels under a common outer location.
73
+ def test_multiple_nested_options
74
+ server(
75
+ itsi_rb: lambda do
76
+ location "/api" do
77
+ get("/status") { |r| r.respond("API Status from /api") }
78
+ location "/users" do
79
+ get("/list") { |r| r.respond("User List from /api/users") }
80
+ location "/:user_id" do
81
+ get("/details") { |r| r.respond("User Details from /api/users/:user_id") }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ ) do
87
+ res_status = get_resp("/api/status")
88
+ assert_equal "API Status from /api", res_status.body
89
+
90
+ res_list = get_resp("/api/users/list")
91
+ assert_equal "User List from /api/users", res_list.body
92
+
93
+ res_details = get_resp("/api/users/42/details")
94
+ assert_equal "User Details from /api/users/:user_id", res_details.body
95
+ end
96
+ end
97
+
98
+ def test_dynamic_vs_wildcard_precedence
99
+ server(
100
+ itsi_rb: lambda do
101
+ location "/products" do
102
+ # This nested block should match when a numeric id is provided.
103
+ location "/:id([0-9]+)" do
104
+ get("/details") { |r| r.respond("Product details for numeric id") }
105
+ end
106
+ # Fallback handler for when the numeric match does not occur.
107
+ get("/details") { |r| r.respond("General product details") }
108
+ end
109
+ end
110
+ ) do
111
+ res_dynamic = get_resp("/products/123/details")
112
+ assert_equal "Product details for numeric id", res_dynamic.body
113
+
114
+ res_fallback = get_resp("/products/details")
115
+ assert_equal "General product details", res_fallback.body
116
+ end
117
+ end
118
+
119
+ # Test deep nesting with multiple dynamic segments (year, month, slug) versus a fallback archive route.
120
+ def test_multiple_dynamic_segments
121
+ server(
122
+ itsi_rb: lambda do
123
+ location "/blog" do
124
+ # Nested dynamic segments for a blog post.
125
+ location "/:year([0-9]{4})" do
126
+ location "/:month([0-9]{1,2})" do
127
+ location "/:slug" do
128
+ get { |r, params| r.respond("Blog post: #{params[:year]}/#{params[:month]}/#{params[:slug]}") }
129
+ end
130
+ end
131
+ end
132
+ # Fallback route for blog archive.
133
+ get("/archive") { |r| r.respond("Blog archive") }
134
+ end
135
+ end
136
+ ) do
137
+ res_post = get_resp("/blog/2021/9/challenge-post")
138
+ assert_equal "Blog post: 2021/9/challenge-post", res_post.body
139
+
140
+ res_archive = get_resp("/blog/archive")
141
+ assert_equal "Blog archive", res_archive.body
142
+ end
143
+ end
144
+
145
+ # Test mixed static and dynamic segments. The static route should override the dynamic one for an exact match.
146
+ def test_static_vs_dynamic_precedence
147
+ server(
148
+ itsi_rb: lambda do
149
+ location "/dashboard" do
150
+ # Static route for settings.
151
+ location "/settings" do
152
+ get { |r| r.respond("Dashboard settings") }
153
+ end
154
+ # Dynamic route for other sections.
155
+ location "/:section" do
156
+ get { |r, params| r.respond("Dashboard section: #{params[:section]}") }
157
+ end
158
+ end
159
+ end
160
+ ) do
161
+ res_static = get_resp("/dashboard/settings")
162
+ assert_equal "Dashboard settings", res_static.body
163
+
164
+ res_dynamic = get_resp("/dashboard/stats")
165
+ assert_equal "Dashboard section: stats", res_dynamic.body
166
+ end
167
+ end
168
+
169
+ # Test overlapping regex-based routing versus a fallback dynamic match.
170
+ def test_overlapping_regex_and_fallback
171
+ server(
172
+ itsi_rb: lambda do
173
+ location "/files" do
174
+ # This nested location will match when the filename ends with .txt.
175
+ location "/:filename([a-z]+\.txt)" do
176
+ get { |r, params| r.respond("Text file: #{params[:filename]}") }
177
+ end
178
+ # Fallback for any file that doesn't match the above regex.
179
+ location "/:filename" do
180
+ get { |r, params| r.respond("Other file: #{params[:filename]}") }
181
+ end
182
+ end
183
+ end
184
+ ) do
185
+ res_txt = get_resp("/files/readme.txt")
186
+ assert_equal "Text file: readme.txt", res_txt.body
187
+
188
+ res_other = get_resp("/files/readme.pdf")
189
+ assert_equal "Other file: readme.pdf", res_other.body
190
+ end
191
+ end
192
+
193
+ # Test a deeper nested structure that mimics a versioned API with sub-resources.
194
+ def test_complex_nested_structure
195
+ server(
196
+ itsi_rb: lambda do
197
+ location "/api" do
198
+ location "/v1" do
199
+ location "/users" do
200
+ location "/:user_id" do
201
+ location "/profile" do
202
+ get("/") { |r, params| r.respond("User Profile for #{r.params[:user_id]}") }
203
+ end
204
+ location "/orders" do
205
+ get("/") { |r, params| r.respond("User Orders for #{r.params[:user_id]}") }
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ ) do
213
+ res_profile = get_resp("/api/v1/users/42/profile")
214
+ assert_equal "User Profile for 42", res_profile.body
215
+
216
+ res_orders = get_resp("/api/v1/users/42/orders")
217
+ assert_equal "User Orders for 42", res_orders.body
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "../helpers/test_helper"
2
+
3
+ class TestMaxBody < Minitest::Test
4
+ def test_max_body_enforced
5
+ server(
6
+ itsi_rb: lambda do
7
+ max_body limit_bytes: 20
8
+ post("/") { |r|
9
+ r.ok "OK"
10
+ }
11
+ end
12
+ ) do
13
+ small = "a" * 10
14
+ large = "b" * 100
15
+
16
+ assert_equal "200", post("/", small).code
17
+ assert_equal "413", post("/", large).code
18
+ end
19
+ end
20
+ end