itsi 0.1.11 → 0.1.12

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 (293) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1535 -45
  3. data/{sandbox/itsi_itsi_file/Itsi.rb → Itsi.rb} +19 -13
  4. data/Rakefile +8 -7
  5. data/crates/itsi_error/src/lib.rs +9 -0
  6. data/crates/itsi_rb_helpers/Cargo.toml +1 -0
  7. data/crates/itsi_rb_helpers/src/heap_value.rs +18 -0
  8. data/crates/itsi_rb_helpers/src/lib.rs +34 -7
  9. data/crates/itsi_server/Cargo.toml +69 -30
  10. data/crates/itsi_server/src/lib.rs +79 -147
  11. data/crates/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  12. data/crates/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  13. data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  14. data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  15. data/crates/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  16. data/{gems/server/ext/itsi_server/src/request/itsi_request.rs → crates/itsi_server/src/ruby_types/itsi_http_request.rs} +101 -117
  17. data/crates/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  18. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  19. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  20. data/crates/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  21. data/crates/itsi_server/src/ruby_types/mod.rs +55 -0
  22. data/crates/itsi_server/src/server/bind.rs +13 -5
  23. data/crates/itsi_server/src/server/byte_frame.rs +32 -0
  24. data/crates/itsi_server/src/server/cache_store.rs +74 -0
  25. data/crates/itsi_server/src/server/itsi_service.rs +172 -0
  26. data/crates/itsi_server/src/server/lifecycle_event.rs +3 -0
  27. data/crates/itsi_server/src/server/listener.rs +102 -2
  28. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  29. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  30. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  31. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  32. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  33. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  34. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  35. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  36. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  37. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  38. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  39. data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  40. data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  41. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  42. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  43. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  44. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  45. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  46. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  53. data/crates/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  54. data/crates/itsi_server/src/server/mod.rs +8 -1
  55. data/crates/itsi_server/src/server/process_worker.rs +38 -12
  56. data/crates/itsi_server/src/server/rate_limiter.rs +565 -0
  57. data/crates/itsi_server/src/server/request_job.rs +11 -0
  58. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  59. data/crates/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  60. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  61. data/crates/itsi_server/src/server/signal.rs +19 -0
  62. data/crates/itsi_server/src/server/static_file_server.rs +984 -0
  63. data/crates/itsi_server/src/server/thread_worker.rs +139 -94
  64. data/crates/itsi_server/src/server/types.rs +43 -0
  65. data/crates/itsi_server/test.md +14 -0
  66. data/crates/itsi_tracing/Cargo.toml +1 -0
  67. data/crates/itsi_tracing/src/lib.rs +216 -45
  68. data/docs/.gitignore +7 -0
  69. data/docs/.gitpod.yml +15 -0
  70. data/docs/Itsi.rb +17 -0
  71. data/docs/content/_index.md +17 -0
  72. data/docs/content/about.md +6 -0
  73. data/docs/content/docs/_index.md +18 -0
  74. data/docs/content/docs/first-page.md +9 -0
  75. data/docs/content/docs/folder/_index.md +10 -0
  76. data/docs/content/docs/folder/leaf.md +7 -0
  77. data/docs/go.mod +5 -0
  78. data/docs/go.sum +2 -0
  79. data/docs/hugo.yaml +77 -0
  80. data/examples/static_assets_example.rb +83 -0
  81. data/gems/_index.md +18 -0
  82. data/gems/scheduler/CODE_OF_CONDUCT.md +7 -0
  83. data/gems/scheduler/Cargo.lock +75 -14
  84. data/gems/scheduler/README.md +5 -0
  85. data/gems/scheduler/_index.md +7 -0
  86. data/gems/scheduler/itsi-scheduler.gemspec +4 -1
  87. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  88. data/gems/scheduler/lib/itsi/scheduler.rb +2 -2
  89. data/gems/scheduler/test/test_file_io.rb +0 -1
  90. data/gems/scheduler/test/test_itsi_scheduler.rb +1 -1
  91. data/gems/server/CHANGELOG.md +5 -0
  92. data/gems/server/CODE_OF_CONDUCT.md +7 -0
  93. data/gems/server/Cargo.lock +1536 -45
  94. data/gems/server/README.md +4 -0
  95. data/gems/server/_index.md +6 -0
  96. data/gems/server/exe/itsi +33 -74
  97. data/gems/server/itsi-server.gemspec +3 -2
  98. data/gems/server/lib/itsi/{request.rb → http_request.rb} +29 -5
  99. data/gems/server/lib/itsi/http_response.rb +39 -0
  100. data/gems/server/lib/itsi/server/Itsi.rb +11 -19
  101. data/gems/server/lib/itsi/server/config/dsl.rb +506 -0
  102. data/gems/server/lib/itsi/server/config.rb +103 -8
  103. data/gems/server/lib/itsi/server/default_app/default_app.rb +38 -0
  104. data/gems/server/lib/itsi/server/grpc_interface.rb +213 -0
  105. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +8 -17
  106. data/gems/server/lib/itsi/server/rack_interface.rb +23 -4
  107. data/gems/server/lib/itsi/server/scheduler_interface.rb +1 -1
  108. data/gems/server/lib/itsi/server/scheduler_mode.rb +4 -0
  109. data/gems/server/lib/itsi/server/signal_trap.rb +7 -1
  110. data/gems/server/lib/itsi/server/version.rb +1 -1
  111. data/gems/server/lib/itsi/server.rb +74 -63
  112. data/gems/server/lib/itsi/standard_headers.rb +86 -0
  113. data/gems/server/test/helpers/test_helper.rb +12 -12
  114. data/gems/server/test/test_itsi_server.rb +2 -2
  115. data/lib/itsi/version.rb +1 -1
  116. data/sandbox/itsi_file/Gemfile +11 -0
  117. data/sandbox/itsi_file/Gemfile.lock +69 -0
  118. data/sandbox/itsi_file/Itsi.rb +276 -0
  119. data/sandbox/itsi_file/error.html +2 -0
  120. data/sandbox/itsi_file/organisations_controller.rb +20 -0
  121. data/sandbox/itsi_file/public/assets/image.png +0 -0
  122. data/sandbox/itsi_file/public/assets/index.html +1 -0
  123. data/sandbox/itsi_sandbox_hanami/Gemfile.lock +2 -2
  124. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  125. data/sandbox/itsi_sandbox_rack/config.ru +2 -15
  126. data/sandbox/itsi_sandbox_rails/.dockerignore +2 -5
  127. data/sandbox/itsi_sandbox_rails/.github/workflows/ci.yml +1 -1
  128. data/sandbox/itsi_sandbox_rails/.gitignore +2 -1
  129. data/sandbox/itsi_sandbox_rails/Dockerfile +6 -9
  130. data/sandbox/itsi_sandbox_rails/Gemfile +16 -22
  131. data/sandbox/itsi_sandbox_rails/Gemfile.lock +100 -225
  132. data/sandbox/itsi_sandbox_rails/app/assets/config/manifest.js +4 -0
  133. data/sandbox/itsi_sandbox_rails/app/assets/stylesheets/application.css +11 -6
  134. data/sandbox/itsi_sandbox_rails/app/channels/application_cable/channel.rb +4 -0
  135. data/sandbox/itsi_sandbox_rails/app/channels/application_cable/connection.rb +4 -0
  136. data/sandbox/itsi_sandbox_rails/app/controllers/live_controller.rb +7 -8
  137. data/sandbox/itsi_sandbox_rails/app/controllers/uploads_controller.rb +0 -3
  138. data/sandbox/itsi_sandbox_rails/app/views/layouts/application.html.erb +2 -7
  139. data/sandbox/itsi_sandbox_rails/bin/docker-entrypoint +3 -4
  140. data/sandbox/itsi_sandbox_rails/bin/setup +8 -5
  141. data/sandbox/itsi_sandbox_rails/config/application.rb +1 -35
  142. data/sandbox/itsi_sandbox_rails/config/cable.yml +3 -10
  143. data/sandbox/itsi_sandbox_rails/config/credentials.yml.enc +1 -1
  144. data/sandbox/itsi_sandbox_rails/config/database.yml +9 -19
  145. data/sandbox/itsi_sandbox_rails/config/environment.rb +1 -1
  146. data/sandbox/itsi_sandbox_rails/config/environments/development.rb +21 -12
  147. data/sandbox/itsi_sandbox_rails/config/environments/production.rb +49 -34
  148. data/sandbox/itsi_sandbox_rails/config/environments/test.rb +19 -5
  149. data/sandbox/itsi_sandbox_rails/config/initializers/assets.rb +5 -0
  150. data/sandbox/itsi_sandbox_rails/config/initializers/filter_parameter_logging.rb +1 -1
  151. data/sandbox/itsi_sandbox_rails/config/initializers/permissions_policy.rb +13 -0
  152. data/sandbox/itsi_sandbox_rails/config/puma.rb +2 -9
  153. data/sandbox/itsi_sandbox_rails/config.ru +0 -1
  154. data/sandbox/itsi_sandbox_rails/db/migrate/20250301041554_create_posts.rb +1 -1
  155. data/sandbox/itsi_sandbox_rails/db/schema.rb +2 -2
  156. data/sandbox/itsi_sandbox_rails/lib/assets/.keep +0 -0
  157. data/sandbox/itsi_sandbox_rails/public/404.html +66 -113
  158. data/sandbox/itsi_sandbox_rails/public/406-unsupported-browser.html +65 -113
  159. data/sandbox/itsi_sandbox_rails/public/422.html +66 -113
  160. data/sandbox/itsi_sandbox_rails/public/500.html +65 -113
  161. data/sandbox/itsi_sandbox_rails/public/icon.png +0 -0
  162. data/sandbox/itsi_sandbox_rails/public/icon.svg +2 -2
  163. data/sandbox/itsi_sandbox_rails/test/channels/application_cable/connection_test.rb +13 -0
  164. data/sandbox/itsi_sandbox_roda/Gemfile.lock +3 -10
  165. data/tasks.txt +72 -35
  166. metadata +89 -139
  167. data/crates/itsi_server/src/body_proxy/mod.rs +0 -2
  168. data/crates/itsi_server/src/request/itsi_request.rs +0 -298
  169. data/crates/itsi_server/src/request/mod.rs +0 -1
  170. data/crates/itsi_server/src/response/mod.rs +0 -1
  171. data/crates/itsi_server/src/server/itsi_server.rs +0 -288
  172. data/gems/scheduler/ext/itsi_error/Cargo.lock +0 -368
  173. data/gems/scheduler/ext/itsi_error/Cargo.toml +0 -11
  174. data/gems/scheduler/ext/itsi_error/src/from.rs +0 -68
  175. data/gems/scheduler/ext/itsi_error/src/lib.rs +0 -24
  176. data/gems/scheduler/ext/itsi_instrument_entry/Cargo.toml +0 -15
  177. data/gems/scheduler/ext/itsi_instrument_entry/src/lib.rs +0 -31
  178. data/gems/scheduler/ext/itsi_rb_helpers/Cargo.lock +0 -355
  179. data/gems/scheduler/ext/itsi_rb_helpers/Cargo.toml +0 -10
  180. data/gems/scheduler/ext/itsi_rb_helpers/src/heap_value.rs +0 -121
  181. data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +0 -201
  182. data/gems/scheduler/ext/itsi_scheduler/Cargo.toml +0 -24
  183. data/gems/scheduler/ext/itsi_scheduler/extconf.rb +0 -6
  184. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +0 -56
  185. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +0 -44
  186. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +0 -44
  187. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler.rs +0 -308
  188. data/gems/scheduler/ext/itsi_scheduler/src/lib.rs +0 -38
  189. data/gems/scheduler/ext/itsi_server/Cargo.lock +0 -2956
  190. data/gems/scheduler/ext/itsi_server/Cargo.toml +0 -50
  191. data/gems/scheduler/ext/itsi_server/extconf.rb +0 -6
  192. data/gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs +0 -104
  193. data/gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +0 -122
  194. data/gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  195. data/gems/scheduler/ext/itsi_server/src/env.rs +0 -43
  196. data/gems/scheduler/ext/itsi_server/src/lib.rs +0 -180
  197. data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +0 -298
  198. data/gems/scheduler/ext/itsi_server/src/request/mod.rs +0 -1
  199. data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +0 -357
  200. data/gems/scheduler/ext/itsi_server/src/response/mod.rs +0 -1
  201. data/gems/scheduler/ext/itsi_server/src/server/bind.rs +0 -174
  202. data/gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs +0 -37
  203. data/gems/scheduler/ext/itsi_server/src/server/io_stream.rs +0 -104
  204. data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +0 -288
  205. data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +0 -9
  206. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +0 -318
  207. data/gems/scheduler/ext/itsi_server/src/server/mod.rs +0 -11
  208. data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +0 -203
  209. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +0 -260
  210. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/mod.rs +0 -27
  211. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +0 -276
  212. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +0 -74
  213. data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +0 -399
  214. data/gems/scheduler/ext/itsi_server/src/server/tls/locked_dir_cache.rs +0 -132
  215. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +0 -265
  216. data/gems/scheduler/ext/itsi_tracing/Cargo.lock +0 -274
  217. data/gems/scheduler/ext/itsi_tracing/Cargo.toml +0 -16
  218. data/gems/scheduler/ext/itsi_tracing/src/lib.rs +0 -58
  219. data/gems/server/ext/itsi_error/Cargo.lock +0 -368
  220. data/gems/server/ext/itsi_error/Cargo.toml +0 -11
  221. data/gems/server/ext/itsi_error/src/from.rs +0 -68
  222. data/gems/server/ext/itsi_error/src/lib.rs +0 -24
  223. data/gems/server/ext/itsi_instrument_entry/Cargo.toml +0 -15
  224. data/gems/server/ext/itsi_instrument_entry/src/lib.rs +0 -31
  225. data/gems/server/ext/itsi_rb_helpers/Cargo.lock +0 -355
  226. data/gems/server/ext/itsi_rb_helpers/Cargo.toml +0 -10
  227. data/gems/server/ext/itsi_rb_helpers/src/heap_value.rs +0 -121
  228. data/gems/server/ext/itsi_rb_helpers/src/lib.rs +0 -201
  229. data/gems/server/ext/itsi_scheduler/Cargo.toml +0 -24
  230. data/gems/server/ext/itsi_scheduler/extconf.rb +0 -6
  231. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +0 -56
  232. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +0 -44
  233. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +0 -44
  234. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler.rs +0 -308
  235. data/gems/server/ext/itsi_scheduler/src/lib.rs +0 -38
  236. data/gems/server/ext/itsi_server/Cargo.lock +0 -2956
  237. data/gems/server/ext/itsi_server/Cargo.toml +0 -50
  238. data/gems/server/ext/itsi_server/extconf.rb +0 -6
  239. data/gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs +0 -104
  240. data/gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +0 -122
  241. data/gems/server/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  242. data/gems/server/ext/itsi_server/src/env.rs +0 -43
  243. data/gems/server/ext/itsi_server/src/lib.rs +0 -180
  244. data/gems/server/ext/itsi_server/src/request/mod.rs +0 -1
  245. data/gems/server/ext/itsi_server/src/response/itsi_response.rs +0 -357
  246. data/gems/server/ext/itsi_server/src/response/mod.rs +0 -1
  247. data/gems/server/ext/itsi_server/src/server/bind.rs +0 -174
  248. data/gems/server/ext/itsi_server/src/server/bind_protocol.rs +0 -37
  249. data/gems/server/ext/itsi_server/src/server/io_stream.rs +0 -104
  250. data/gems/server/ext/itsi_server/src/server/itsi_server.rs +0 -288
  251. data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +0 -9
  252. data/gems/server/ext/itsi_server/src/server/listener.rs +0 -318
  253. data/gems/server/ext/itsi_server/src/server/mod.rs +0 -11
  254. data/gems/server/ext/itsi_server/src/server/process_worker.rs +0 -203
  255. data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +0 -260
  256. data/gems/server/ext/itsi_server/src/server/serve_strategy/mod.rs +0 -27
  257. data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +0 -276
  258. data/gems/server/ext/itsi_server/src/server/signal.rs +0 -74
  259. data/gems/server/ext/itsi_server/src/server/thread_worker.rs +0 -399
  260. data/gems/server/ext/itsi_server/src/server/tls/locked_dir_cache.rs +0 -132
  261. data/gems/server/ext/itsi_server/src/server/tls.rs +0 -265
  262. data/gems/server/ext/itsi_tracing/Cargo.lock +0 -274
  263. data/gems/server/ext/itsi_tracing/Cargo.toml +0 -16
  264. data/gems/server/ext/itsi_tracing/src/lib.rs +0 -58
  265. data/gems/server/lib/itsi/server/options_dsl.rb +0 -401
  266. data/gems/server/lib/itsi/stream_io.rb +0 -38
  267. data/location_dsl.rb +0 -381
  268. data/sandbox/itsi_sandbox_rails/.kamal/hooks/docker-setup.sample +0 -3
  269. data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-app-boot.sample +0 -3
  270. data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-deploy.sample +0 -14
  271. data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-proxy-reboot.sample +0 -3
  272. data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-app-boot.sample +0 -3
  273. data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-build.sample +0 -51
  274. data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-connect.sample +0 -47
  275. data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-deploy.sample +0 -109
  276. data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-proxy-reboot.sample +0 -3
  277. data/sandbox/itsi_sandbox_rails/.kamal/secrets +0 -17
  278. data/sandbox/itsi_sandbox_rails/bin/dev +0 -2
  279. data/sandbox/itsi_sandbox_rails/bin/jobs +0 -6
  280. data/sandbox/itsi_sandbox_rails/bin/kamal +0 -27
  281. data/sandbox/itsi_sandbox_rails/bin/thrust +0 -5
  282. data/sandbox/itsi_sandbox_rails/config/cache.yml +0 -16
  283. data/sandbox/itsi_sandbox_rails/config/deploy.yml +0 -116
  284. data/sandbox/itsi_sandbox_rails/config/queue.yml +0 -18
  285. data/sandbox/itsi_sandbox_rails/config/recurring.yml +0 -10
  286. data/sandbox/itsi_sandbox_rails/db/cable_schema.rb +0 -11
  287. data/sandbox/itsi_sandbox_rails/db/cache_schema.rb +0 -14
  288. data/sandbox/itsi_sandbox_rails/db/queue_schema.rb +0 -129
  289. data/sandbox/itsi_sandbox_rails/public/400.html +0 -114
  290. data/sandbox/itsi_sandbox_rails/test/fixtures/posts.yml +0 -9
  291. data/sandbox/itsi_sandbox_rails/test/models/post_test.rb +0 -7
  292. /data/{sandbox/itsi_sandbox_rails/script/.keep → crates/_index.md} +0 -0
  293. /data/gems/server/lib/itsi/{index.html → server/default_app/index.html} +0 -0
@@ -0,0 +1,47 @@
1
+ use super::{ErrorResponse, FromValue, MiddlewareLayer};
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ types::{HttpRequest, HttpResponse},
5
+ };
6
+ use async_trait::async_trait;
7
+ use either::Either;
8
+ use itsi_error::ItsiError;
9
+ use magnus::error::Result;
10
+ use regex::RegexSet;
11
+ use serde::Deserialize;
12
+ use std::sync::OnceLock;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct AllowList {
16
+ #[serde(skip_deserializing)]
17
+ pub allowed_ips: OnceLock<RegexSet>,
18
+ pub allowed_patterns: Vec<String>,
19
+ pub error_response: ErrorResponse,
20
+ }
21
+
22
+ #[async_trait]
23
+ impl MiddlewareLayer for AllowList {
24
+ async fn initialize(&self) -> Result<()> {
25
+ let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::default)?;
26
+ self.allowed_ips
27
+ .set(allowed_ips)
28
+ .map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
29
+ Ok(())
30
+ }
31
+
32
+ async fn before(
33
+ &self,
34
+ req: HttpRequest,
35
+ context: &mut RequestContext,
36
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
37
+ if let Some(allowed_ips) = self.allowed_ips.get() {
38
+ if !allowed_ips.is_match(&context.addr) {
39
+ return Ok(Either::Right(
40
+ self.error_response.to_http_response(&req).await,
41
+ ));
42
+ }
43
+ }
44
+ Ok(Either::Left(req))
45
+ }
46
+ }
47
+ impl FromValue for AllowList {}
@@ -0,0 +1,58 @@
1
+ use crate::server::{
2
+ itsi_service::RequestContext,
3
+ types::{HttpRequest, HttpResponse, RequestExt},
4
+ };
5
+
6
+ use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
7
+
8
+ use async_trait::async_trait;
9
+ use either::Either;
10
+ use magnus::error::Result;
11
+ use serde::Deserialize;
12
+
13
+ /// A simple API key filter.
14
+ /// The API key can be given inside the header or a query string
15
+ /// Keys are validated against a list of allowed key values (Changing these requires a restart)
16
+ ///
17
+ #[derive(Debug, Clone, Deserialize)]
18
+ pub struct AuthAPIKey {
19
+ pub valid_keys: Vec<String>,
20
+ pub token_source: TokenSource,
21
+ pub error_response: ErrorResponse,
22
+ }
23
+
24
+ #[async_trait]
25
+ impl MiddlewareLayer for AuthAPIKey {
26
+ async fn before(
27
+ &self,
28
+ req: HttpRequest,
29
+ _context: &mut RequestContext,
30
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
31
+ let submitted_value = match &self.token_source {
32
+ TokenSource::Header { name, prefix } => {
33
+ if let Some(header) = req.header(name) {
34
+ if let Some(prefix) = prefix {
35
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
36
+ } else {
37
+ Some(header.trim_ascii())
38
+ }
39
+ } else {
40
+ None
41
+ }
42
+ }
43
+ TokenSource::Query(query_name) => req.query_param(query_name),
44
+ };
45
+ if !self
46
+ .valid_keys
47
+ .iter()
48
+ .any(|key| submitted_value.is_some_and(|sv| sv == key))
49
+ {
50
+ Ok(Either::Right(
51
+ self.error_response.to_http_response(&req).await,
52
+ ))
53
+ } else {
54
+ Ok(Either::Left(req))
55
+ }
56
+ }
57
+ }
58
+ impl FromValue for AuthAPIKey {}
@@ -0,0 +1,82 @@
1
+ use async_trait::async_trait;
2
+ use base64::{engine::general_purpose, Engine};
3
+ use bytes::Bytes;
4
+ use either::Either;
5
+ use http::{Response, StatusCode};
6
+ use http_body_util::{combinators::BoxBody, Full};
7
+ use magnus::error::Result;
8
+ use serde::{Deserialize, Serialize};
9
+ use std::collections::HashMap;
10
+ use std::str;
11
+
12
+ use crate::server::{
13
+ itsi_service::RequestContext,
14
+ types::{HttpRequest, HttpResponse, RequestExt},
15
+ };
16
+
17
+ use super::{FromValue, MiddlewareLayer};
18
+
19
+ #[derive(Debug, Clone, Serialize, Deserialize)]
20
+ pub struct AuthBasic {
21
+ pub realm: String,
22
+ /// Maps usernames to passwords.
23
+ pub credential_pairs: HashMap<String, String>,
24
+ }
25
+
26
+ impl AuthBasic {
27
+ fn basic_auth_failed_response(&self) -> HttpResponse {
28
+ Response::builder()
29
+ .status(StatusCode::UNAUTHORIZED)
30
+ .header(
31
+ "WWW-Authenticate",
32
+ format!("Basic realm=\"{}\"", self.realm),
33
+ )
34
+ .body(BoxBody::new(Full::new(Bytes::from("Unauthorized"))))
35
+ .unwrap()
36
+ }
37
+ }
38
+ #[async_trait]
39
+ impl MiddlewareLayer for AuthBasic {
40
+ async fn before(
41
+ &self,
42
+ req: HttpRequest,
43
+ _context: &mut RequestContext,
44
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
45
+ // Retrieve the Authorization header.
46
+ let auth_header = req.header("Authorization");
47
+
48
+ if !auth_header.is_some_and(|header| header.starts_with("Basic ")) {
49
+ return Ok(Either::Right(self.basic_auth_failed_response()));
50
+ }
51
+
52
+ let auth_header = auth_header.unwrap();
53
+
54
+ let encoded_credentials = &auth_header["Basic ".len()..];
55
+ let decoded = match general_purpose::STANDARD.decode(encoded_credentials) {
56
+ Ok(bytes) => bytes,
57
+ Err(_) => {
58
+ return Ok(Either::Right(self.basic_auth_failed_response()));
59
+ }
60
+ };
61
+
62
+ let decoded_str = match str::from_utf8(&decoded) {
63
+ Ok(s) => s,
64
+ Err(_) => {
65
+ return Ok(Either::Right(self.basic_auth_failed_response()));
66
+ }
67
+ };
68
+
69
+ let mut parts = decoded_str.splitn(2, ':');
70
+ let username = parts.next().unwrap_or("");
71
+ let password = parts.next().unwrap_or("");
72
+
73
+ match self.credential_pairs.get(username) {
74
+ Some(expected_password) if expected_password == password => Ok(Either::Left(req)),
75
+ _ => {
76
+ return Ok(Either::Right(self.basic_auth_failed_response()));
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ impl FromValue for AuthBasic {}
@@ -0,0 +1,321 @@
1
+ use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ types::{HttpRequest, HttpResponse, RequestExt},
5
+ };
6
+ use async_trait::async_trait;
7
+ use base64::{engine::general_purpose, Engine};
8
+ use either::Either;
9
+ use itsi_error::ItsiError;
10
+ use jwt_simple::{
11
+ claims::{self, JWTClaims, NoCustomClaims},
12
+ prelude::{
13
+ ECDSAP256PublicKeyLike, ECDSAP384PublicKeyLike, ES256PublicKey, ES384PublicKey, HS256Key,
14
+ HS384Key, HS512Key, MACLike, PS256PublicKey, PS384PublicKey, PS512PublicKey,
15
+ RS256PublicKey, RS384PublicKey, RS512PublicKey, RSAPublicKeyLike,
16
+ },
17
+ token::Token,
18
+ };
19
+ use magnus::error::Result;
20
+ use serde::Deserialize;
21
+ use std::str;
22
+ use std::{
23
+ collections::{HashMap, HashSet},
24
+ sync::OnceLock,
25
+ };
26
+
27
+ #[derive(Debug, Clone, Deserialize)]
28
+ pub struct AuthJwt {
29
+ pub token_source: TokenSource,
30
+ pub verifiers: HashMap<JwtAlgorithm, Vec<String>>,
31
+ #[serde(skip_deserializing)]
32
+ pub keys: OnceLock<HashMap<JwtAlgorithm, Vec<JwtKey>>>,
33
+ pub audiences: Option<HashSet<String>>,
34
+ pub subjects: Option<HashSet<String>>,
35
+ pub issuers: Option<HashSet<String>>,
36
+ pub leeway: Option<u64>,
37
+ pub error_response: ErrorResponse,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
41
+ pub enum JwtAlgorithm {
42
+ #[serde(rename(deserialize = "hs256"))]
43
+ Hs256,
44
+ #[serde(rename(deserialize = "hs384"))]
45
+ Hs384,
46
+ #[serde(rename(deserialize = "hs512"))]
47
+ Hs512,
48
+ #[serde(rename(deserialize = "rs256"))]
49
+ Rs256,
50
+ #[serde(rename(deserialize = "rs384"))]
51
+ Rs384,
52
+ #[serde(rename(deserialize = "rs512"))]
53
+ Rs512,
54
+ #[serde(rename(deserialize = "es256"))]
55
+ Es256,
56
+ #[serde(rename(deserialize = "es384"))]
57
+ Es384,
58
+ #[serde(rename(deserialize = "ps256"))]
59
+ Ps256,
60
+ #[serde(rename(deserialize = "ps384"))]
61
+ Ps384,
62
+ #[serde(rename(deserialize = "ps512"))]
63
+ Ps512,
64
+ }
65
+
66
+ impl JwtAlgorithm {
67
+ pub fn key_from(&self, base64: &str) -> Result<JwtKey> {
68
+ let bytes = general_purpose::STANDARD
69
+ .decode(base64)
70
+ .map_err(ItsiError::default)?;
71
+
72
+ match self {
73
+ JwtAlgorithm::Hs256 => Ok(JwtKey::Hs256(HS256Key::from_bytes(&bytes))),
74
+ JwtAlgorithm::Hs384 => Ok(JwtKey::Hs384(HS384Key::from_bytes(&bytes))),
75
+ JwtAlgorithm::Hs512 => Ok(JwtKey::Hs512(HS512Key::from_bytes(&bytes))),
76
+ JwtAlgorithm::Rs256 => Ok(RS256PublicKey::from_der(&bytes)
77
+ .or_else(|_| {
78
+ RS256PublicKey::from_pem(
79
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
80
+ )
81
+ })
82
+ .map(JwtKey::Rs256)
83
+ .map_err(ItsiError::default)?),
84
+ JwtAlgorithm::Rs384 => Ok(RS384PublicKey::from_der(&bytes)
85
+ .or_else(|_| {
86
+ RS384PublicKey::from_pem(
87
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
88
+ )
89
+ })
90
+ .map(JwtKey::Rs384)
91
+ .map_err(ItsiError::default)?),
92
+ JwtAlgorithm::Rs512 => Ok(RS512PublicKey::from_der(&bytes)
93
+ .or_else(|_| {
94
+ RS512PublicKey::from_pem(
95
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
96
+ )
97
+ })
98
+ .map(JwtKey::Rs512)
99
+ .map_err(ItsiError::default)?),
100
+ JwtAlgorithm::Es256 => Ok(ES256PublicKey::from_der(&bytes)
101
+ .or_else(|_| {
102
+ ES256PublicKey::from_pem(
103
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
104
+ )
105
+ })
106
+ .map(JwtKey::Es256)
107
+ .map_err(ItsiError::default)?),
108
+ JwtAlgorithm::Es384 => Ok(ES384PublicKey::from_der(&bytes)
109
+ .or_else(|_| {
110
+ ES384PublicKey::from_pem(
111
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
112
+ )
113
+ })
114
+ .map(JwtKey::Es384)
115
+ .map_err(ItsiError::default)?),
116
+ JwtAlgorithm::Ps256 => Ok(PS256PublicKey::from_der(&bytes)
117
+ .or_else(|_| {
118
+ PS256PublicKey::from_pem(
119
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
120
+ )
121
+ })
122
+ .map(JwtKey::Ps256)
123
+ .map_err(ItsiError::default)?),
124
+ JwtAlgorithm::Ps384 => Ok(PS384PublicKey::from_der(&bytes)
125
+ .or_else(|_| {
126
+ PS384PublicKey::from_pem(
127
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
128
+ )
129
+ })
130
+ .map(JwtKey::Ps384)
131
+ .map_err(ItsiError::default)?),
132
+ JwtAlgorithm::Ps512 => Ok(PS512PublicKey::from_der(&bytes)
133
+ .or_else(|_| {
134
+ PS512PublicKey::from_pem(
135
+ &String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
136
+ )
137
+ })
138
+ .map(JwtKey::Ps512)
139
+ .map_err(ItsiError::default)?),
140
+ }
141
+ }
142
+ }
143
+
144
+ #[derive(Debug, Clone)]
145
+ pub enum JwtKey {
146
+ Hs256(HS256Key),
147
+ Hs384(HS384Key),
148
+ Hs512(HS512Key),
149
+ Rs256(RS256PublicKey),
150
+ Rs384(RS384PublicKey),
151
+ Rs512(RS512PublicKey),
152
+ Es256(ES256PublicKey),
153
+ Es384(ES384PublicKey),
154
+ Ps256(PS256PublicKey),
155
+ Ps384(PS384PublicKey),
156
+ Ps512(PS512PublicKey),
157
+ }
158
+
159
+ impl TryFrom<&str> for JwtAlgorithm {
160
+ type Error = itsi_error::ItsiError;
161
+
162
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
163
+ match value.to_ascii_lowercase().as_str() {
164
+ "hs256" => Ok(JwtAlgorithm::Hs256),
165
+ "hs384" => Ok(JwtAlgorithm::Hs384),
166
+ "hs512" => Ok(JwtAlgorithm::Hs512),
167
+ "rs256" => Ok(JwtAlgorithm::Rs256),
168
+ "rs384" => Ok(JwtAlgorithm::Rs384),
169
+ "rs512" => Ok(JwtAlgorithm::Rs512),
170
+ "es256" => Ok(JwtAlgorithm::Es256),
171
+ "es384" => Ok(JwtAlgorithm::Es384),
172
+ "ps256" => Ok(JwtAlgorithm::Ps256),
173
+ "ps384" => Ok(JwtAlgorithm::Ps384),
174
+ "ps512" => Ok(JwtAlgorithm::Ps512),
175
+ _ => Err(itsi_error::ItsiError::UnsupportedProtocol(
176
+ "Unsupported JWT Algorithm".to_string(),
177
+ )),
178
+ }
179
+ }
180
+ }
181
+
182
+ impl JwtKey {
183
+ pub fn verify(
184
+ &self,
185
+ token: &str,
186
+ ) -> std::result::Result<JWTClaims<claims::NoCustomClaims>, jwt_simple::Error> {
187
+ match self {
188
+ JwtKey::Hs256(key) => key.verify_token::<NoCustomClaims>(token, None),
189
+ JwtKey::Hs384(key) => key.verify_token::<NoCustomClaims>(token, None),
190
+ JwtKey::Hs512(key) => key.verify_token::<NoCustomClaims>(token, None),
191
+ JwtKey::Rs256(key) => key.verify_token::<NoCustomClaims>(token, None),
192
+ JwtKey::Rs384(key) => key.verify_token::<NoCustomClaims>(token, None),
193
+ JwtKey::Rs512(key) => key.verify_token::<NoCustomClaims>(token, None),
194
+ JwtKey::Es256(key) => key.verify_token::<NoCustomClaims>(token, None),
195
+ JwtKey::Es384(key) => key.verify_token::<NoCustomClaims>(token, None),
196
+ JwtKey::Ps256(key) => key.verify_token::<NoCustomClaims>(token, None),
197
+ JwtKey::Ps384(key) => key.verify_token::<NoCustomClaims>(token, None),
198
+ JwtKey::Ps512(key) => key.verify_token::<NoCustomClaims>(token, None),
199
+ }
200
+ }
201
+ }
202
+
203
+ #[async_trait]
204
+ impl MiddlewareLayer for AuthJwt {
205
+ async fn initialize(&self) -> Result<()> {
206
+ let keys: HashMap<JwtAlgorithm, Vec<JwtKey>> = self
207
+ .verifiers
208
+ .iter()
209
+ .map(|(algorithm, key_strings)| {
210
+ let algo = algorithm.clone();
211
+ let keys: Result<Vec<JwtKey>> = key_strings
212
+ .iter()
213
+ .map(|key_string| algorithm.key_from(key_string))
214
+ .collect();
215
+ keys.map(|keys| (algo, keys))
216
+ })
217
+ .collect::<Result<HashMap<JwtAlgorithm, Vec<JwtKey>>>>()?;
218
+ self.keys
219
+ .set(keys)
220
+ .map_err(|e| ItsiError::default(format!("Failed to set keys: {:?}", e)))?;
221
+ Ok(())
222
+ }
223
+
224
+ async fn before(
225
+ &self,
226
+ req: HttpRequest,
227
+ _context: &mut RequestContext,
228
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
229
+ let token_str = match &self.token_source {
230
+ TokenSource::Header { name, prefix } => {
231
+ if let Some(header) = req.header(name) {
232
+ if let Some(prefix) = prefix {
233
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
234
+ } else {
235
+ Some(header.trim_ascii())
236
+ }
237
+ } else {
238
+ None
239
+ }
240
+ }
241
+ TokenSource::Query(query_name) => req.query_param(query_name),
242
+ };
243
+
244
+ if token_str.is_none() {
245
+ return Ok(Either::Right(
246
+ self.error_response.to_http_response(&req).await,
247
+ ));
248
+ }
249
+
250
+ let token_str = token_str.unwrap();
251
+ let token_meta = Token::decode_metadata(token_str);
252
+
253
+ if token_meta.is_err() {
254
+ return Ok(Either::Right(
255
+ self.error_response.to_http_response(&req).await,
256
+ ));
257
+ }
258
+ let token_meta: std::result::Result<JwtAlgorithm, ItsiError> =
259
+ token_meta.unwrap().algorithm().try_into();
260
+ if token_meta.is_err() {
261
+ return Ok(Either::Right(
262
+ self.error_response.to_http_response(&req).await,
263
+ ));
264
+ }
265
+ let algorithm = token_meta.unwrap();
266
+
267
+ if !self.verifiers.contains_key(&algorithm) {
268
+ return Ok(Either::Right(
269
+ self.error_response.to_http_response(&req).await,
270
+ ));
271
+ }
272
+
273
+ let keys = self.keys.get().unwrap().get(&algorithm).unwrap();
274
+
275
+ let verified_claims = keys.iter().find_map(|key| key.verify(token_str).ok());
276
+ if verified_claims.is_none() {
277
+ return Ok(Either::Right(
278
+ self.error_response.to_http_response(&req).await,
279
+ ));
280
+ }
281
+
282
+ let claims = verified_claims.unwrap();
283
+
284
+ if let Some(expected_audiences) = &self.audiences {
285
+ // The aud claim may be a string or an array.
286
+ if let Some(audiences) = &claims.audiences {
287
+ if !audiences.contains(expected_audiences) {
288
+ return Ok(Either::Right(
289
+ self.error_response.to_http_response(&req).await,
290
+ ));
291
+ }
292
+ }
293
+ }
294
+
295
+ if let Some(expected_subjects) = &self.subjects {
296
+ // The aud claim may be a string or an array.
297
+ if let Some(subject) = &claims.subject {
298
+ if !expected_subjects.contains(subject) {
299
+ return Ok(Either::Right(
300
+ self.error_response.to_http_response(&req).await,
301
+ ));
302
+ }
303
+ }
304
+ }
305
+
306
+ if let Some(expected_issuers) = &self.issuers {
307
+ // The aud claim may be a string or an array.
308
+ if let Some(issuer) = &claims.issuer {
309
+ if !expected_issuers.contains(issuer) {
310
+ return Ok(Either::Right(
311
+ self.error_response.to_http_response(&req).await,
312
+ ));
313
+ }
314
+ }
315
+ }
316
+
317
+ Ok(Either::Left(req))
318
+ }
319
+ }
320
+
321
+ impl FromValue for AuthJwt {}
@@ -0,0 +1,139 @@
1
+ use super::{FromValue, MiddlewareLayer};
2
+ use crate::server::{itsi_service::RequestContext, types::HttpResponse};
3
+ use async_trait::async_trait;
4
+ use http::{HeaderName, HeaderValue};
5
+ use magnus::error::Result;
6
+ use serde::Deserialize;
7
+ use std::{collections::HashMap, sync::OnceLock};
8
+
9
+ #[derive(Debug, Clone, Deserialize)]
10
+ pub struct CacheControl {
11
+ #[serde(default)]
12
+ pub max_age: Option<u64>,
13
+ #[serde(default)]
14
+ pub s_max_age: Option<u64>,
15
+ #[serde(default)]
16
+ pub stale_while_revalidate: Option<u64>,
17
+ #[serde(default)]
18
+ pub stale_if_error: Option<u64>,
19
+ #[serde(default)]
20
+ pub public: bool,
21
+ #[serde(default)]
22
+ pub private: bool,
23
+ #[serde(default)]
24
+ pub no_cache: bool,
25
+ #[serde(default)]
26
+ pub no_store: bool,
27
+ #[serde(default)]
28
+ pub must_revalidate: bool,
29
+ #[serde(default)]
30
+ pub proxy_revalidate: bool,
31
+ #[serde(default)]
32
+ pub immutable: bool,
33
+ #[serde(default)]
34
+ pub vary: Vec<String>,
35
+ #[serde(default)]
36
+ pub additional_headers: HashMap<String, String>,
37
+ #[serde(skip_deserializing)]
38
+ pub cache_control_str: OnceLock<String>,
39
+ }
40
+
41
+ #[async_trait]
42
+ impl MiddlewareLayer for CacheControl {
43
+ async fn initialize(&self) -> Result<()> {
44
+ let mut directives = Vec::new();
45
+
46
+ if self.public && !self.private {
47
+ directives.push("public".to_owned());
48
+ } else if self.private && !self.public {
49
+ directives.push("private".to_owned());
50
+ }
51
+ if self.no_cache {
52
+ directives.push("no-cache".to_owned());
53
+ }
54
+ if self.no_store {
55
+ directives.push("no-store".to_owned());
56
+ }
57
+ if self.must_revalidate {
58
+ directives.push("must-revalidate".to_owned());
59
+ }
60
+ if self.proxy_revalidate {
61
+ directives.push("proxy-revalidate".to_owned());
62
+ }
63
+ if self.immutable {
64
+ directives.push("immutable".to_owned());
65
+ }
66
+
67
+ // Add age parameters
68
+ if let Some(max_age) = self.max_age {
69
+ directives.push(format!("max-age={}", max_age));
70
+ }
71
+
72
+ if let Some(s_max_age) = self.s_max_age {
73
+ directives.push(format!("s-maxage={}", s_max_age));
74
+ }
75
+
76
+ if let Some(stale_while_revalidate) = self.stale_while_revalidate {
77
+ directives.push(format!("stale-while-revalidate={}", stale_while_revalidate));
78
+ }
79
+
80
+ if let Some(stale_if_error) = self.stale_if_error {
81
+ directives.push(format!("stale-if-error={}", stale_if_error));
82
+ }
83
+
84
+ // Set the Cache-Control header if we have directives
85
+ if !directives.is_empty() {
86
+ let cache_control_value = directives.join(", ");
87
+ self.cache_control_str.set(cache_control_value).unwrap();
88
+ }
89
+
90
+ Ok(())
91
+ }
92
+
93
+ async fn after(&self, mut resp: HttpResponse, _: &mut RequestContext) -> HttpResponse {
94
+ // Skip for statuses where caching doesn't make sense
95
+ let status = resp.status().as_u16();
96
+ if matches!(status, 401 | 403 | 500..=599) {
97
+ return resp;
98
+ }
99
+
100
+ // Set the Cache-Control header if we have directives
101
+ if let Some(cache_control_value) = self.cache_control_str.get() {
102
+ if let Ok(value) = HeaderValue::from_str(cache_control_value) {
103
+ resp.headers_mut().insert("Cache-Control", value);
104
+ }
105
+ }
106
+
107
+ // Set Expires header based on max-age if present
108
+ if let Some(max_age) = self.max_age {
109
+ // Set the Expires header based on max-age
110
+ // Use a helper to format the HTTP date correctly
111
+ let expires = chrono::Utc::now() + chrono::Duration::seconds(max_age as i64);
112
+ let expires_str = expires.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
113
+ if let Ok(value) = HeaderValue::from_str(&expires_str) {
114
+ resp.headers_mut().insert("Expires", value);
115
+ }
116
+ }
117
+
118
+ // Set Vary header
119
+ if !self.vary.is_empty() {
120
+ let vary_value = self.vary.join(", ");
121
+ if let Ok(value) = HeaderValue::from_str(&vary_value) {
122
+ resp.headers_mut().insert("Vary", value);
123
+ }
124
+ }
125
+
126
+ // Set additional custom headers
127
+ for (name, value) in &self.additional_headers {
128
+ if let Ok(header_value) = HeaderValue::from_str(value) {
129
+ if let Ok(header_name) = name.parse::<HeaderName>() {
130
+ resp.headers_mut().insert(header_name, header_value);
131
+ }
132
+ }
133
+ }
134
+
135
+ resp
136
+ }
137
+ }
138
+
139
+ impl FromValue for CacheControl {}