itsi-server 0.1.1 → 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 (457) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +4487 -0
  3. data/Cargo.toml +7 -0
  4. data/README.md +6 -0
  5. data/Rakefile +7 -4
  6. data/exe/itsi +152 -46
  7. data/ext/itsi_acme/Cargo.toml +86 -0
  8. data/ext/itsi_acme/examples/high_level.rs +63 -0
  9. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  10. data/ext/itsi_acme/examples/low_level.rs +87 -0
  11. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  12. data/ext/itsi_acme/src/acceptor.rs +81 -0
  13. data/ext/itsi_acme/src/acme.rs +354 -0
  14. data/ext/itsi_acme/src/axum.rs +86 -0
  15. data/ext/itsi_acme/src/cache.rs +39 -0
  16. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  17. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  18. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  19. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  20. data/ext/itsi_acme/src/caches/no.rs +78 -0
  21. data/ext/itsi_acme/src/caches/test.rs +136 -0
  22. data/ext/itsi_acme/src/config.rs +172 -0
  23. data/ext/itsi_acme/src/https_helper.rs +69 -0
  24. data/ext/itsi_acme/src/incoming.rs +142 -0
  25. data/ext/itsi_acme/src/jose.rs +161 -0
  26. data/ext/itsi_acme/src/lib.rs +142 -0
  27. data/ext/itsi_acme/src/resolver.rs +59 -0
  28. data/ext/itsi_acme/src/state.rs +424 -0
  29. data/ext/itsi_error/Cargo.toml +3 -0
  30. data/ext/itsi_error/src/lib.rs +98 -24
  31. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  32. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  33. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  34. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  35. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  36. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  37. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  38. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  39. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  40. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  41. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  42. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  43. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  44. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  45. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  46. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  47. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  48. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  49. data/ext/itsi_rb_helpers/src/lib.rs +141 -10
  50. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  51. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  52. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  53. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  54. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  55. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  56. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  57. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  58. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  59. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  60. data/ext/itsi_scheduler/Cargo.toml +24 -0
  61. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  62. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  63. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  64. data/ext/itsi_scheduler/src/itsi_scheduler.rs +314 -0
  65. data/ext/itsi_scheduler/src/lib.rs +39 -0
  66. data/ext/itsi_server/Cargo.lock +2956 -0
  67. data/ext/itsi_server/Cargo.toml +75 -14
  68. data/ext/itsi_server/extconf.rb +1 -1
  69. data/ext/itsi_server/src/default_responses/html/401.html +68 -0
  70. data/ext/itsi_server/src/default_responses/html/403.html +68 -0
  71. data/ext/itsi_server/src/default_responses/html/404.html +68 -0
  72. data/ext/itsi_server/src/default_responses/html/413.html +71 -0
  73. data/ext/itsi_server/src/default_responses/html/429.html +68 -0
  74. data/ext/itsi_server/src/default_responses/html/500.html +71 -0
  75. data/ext/itsi_server/src/default_responses/html/502.html +71 -0
  76. data/ext/itsi_server/src/default_responses/html/503.html +68 -0
  77. data/ext/itsi_server/src/default_responses/html/504.html +69 -0
  78. data/ext/itsi_server/src/default_responses/html/index.html +238 -0
  79. data/ext/itsi_server/src/default_responses/json/401.json +6 -0
  80. data/ext/itsi_server/src/default_responses/json/403.json +6 -0
  81. data/ext/itsi_server/src/default_responses/json/404.json +6 -0
  82. data/ext/itsi_server/src/default_responses/json/413.json +6 -0
  83. data/ext/itsi_server/src/default_responses/json/429.json +6 -0
  84. data/ext/itsi_server/src/default_responses/json/500.json +6 -0
  85. data/ext/itsi_server/src/default_responses/json/502.json +6 -0
  86. data/ext/itsi_server/src/default_responses/json/503.json +6 -0
  87. data/ext/itsi_server/src/default_responses/json/504.json +6 -0
  88. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  89. data/ext/itsi_server/src/env.rs +43 -0
  90. data/ext/itsi_server/src/lib.rs +133 -40
  91. data/ext/itsi_server/src/prelude.rs +2 -0
  92. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  93. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
  94. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  95. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  96. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
  97. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
  98. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
  99. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
  100. data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
  101. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  102. data/ext/itsi_server/src/server/binds/bind.rs +204 -0
  103. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  104. data/ext/itsi_server/src/server/binds/listener.rs +444 -0
  105. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  106. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  107. data/ext/itsi_server/src/server/binds/tls.rs +278 -0
  108. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  109. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  110. data/ext/itsi_server/src/server/io_stream.rs +105 -0
  111. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  112. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  113. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  114. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  115. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
  116. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  117. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  118. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
  119. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
  120. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  121. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  122. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
  123. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
  124. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
  125. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  126. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  127. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  128. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  129. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
  130. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
  131. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
  132. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
  133. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  134. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  135. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  136. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
  137. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  138. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
  139. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  140. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  141. data/ext/itsi_server/src/server/mod.rs +13 -5
  142. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  143. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  144. data/ext/itsi_server/src/server/request_job.rs +11 -0
  145. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +354 -0
  146. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  147. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +481 -0
  148. data/ext/itsi_server/src/server/signal.rs +77 -0
  149. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  150. data/ext/itsi_server/src/server/thread_worker.rs +479 -0
  151. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  152. data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
  153. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  154. data/ext/itsi_server/src/services/mod.rs +6 -0
  155. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  156. data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
  157. data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
  158. data/ext/itsi_tracing/Cargo.toml +5 -0
  159. data/ext/itsi_tracing/src/lib.rs +366 -7
  160. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  161. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  162. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  163. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  164. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  165. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  166. data/lib/itsi/http_request/response_status_shortcodes.rb +76 -0
  167. data/lib/itsi/http_request.rb +218 -0
  168. data/lib/itsi/http_response.rb +42 -0
  169. data/lib/itsi/passfile.rb +108 -0
  170. data/lib/itsi/server/config/config_helpers.rb +105 -0
  171. data/lib/itsi/server/config/dsl.rb +211 -0
  172. data/lib/itsi/server/config/known_paths/KitchensinkDirectories.txt +2346 -0
  173. data/lib/itsi/server/config/known_paths/Randomfiles.txt +24 -0
  174. data/lib/itsi/server/config/known_paths/UnixDotfiles.txt +52 -0
  175. data/lib/itsi/server/config/known_paths/backdoors/ASP_CommonBackdoors.txt +29 -0
  176. data/lib/itsi/server/config/known_paths/backdoors/bot_control_panels.txt +1668 -0
  177. data/lib/itsi/server/config/known_paths/backdoors/shells.txt +1167 -0
  178. data/lib/itsi/server/config/known_paths/cgi/CGI_HTTP_POST.txt +7 -0
  179. data/lib/itsi/server/config/known_paths/cgi/CGI_HTTP_POST_Windows.txt +6 -0
  180. data/lib/itsi/server/config/known_paths/cgi/CGI_Microsoft.txt +79 -0
  181. data/lib/itsi/server/config/known_paths/cgi/CGI_XPlatform.txt +3948 -0
  182. data/lib/itsi/server/config/known_paths/cms/README.md +5 -0
  183. data/lib/itsi/server/config/known_paths/cms/drupal_plugins.txt +6320 -0
  184. data/lib/itsi/server/config/known_paths/cms/drupal_themes.txt +828 -0
  185. data/lib/itsi/server/config/known_paths/cms/joomla_plugins.txt +224 -0
  186. data/lib/itsi/server/config/known_paths/cms/joomla_themes.txt +30 -0
  187. data/lib/itsi/server/config/known_paths/cms/php-nuke.txt +2142 -0
  188. data/lib/itsi/server/config/known_paths/cms/wordpress.txt +1566 -0
  189. data/lib/itsi/server/config/known_paths/cms/wp_common_theme_files.txt +46 -0
  190. data/lib/itsi/server/config/known_paths/cms/wp_plugins.txt +13366 -0
  191. data/lib/itsi/server/config/known_paths/cms/wp_plugins_full.txt +68662 -0
  192. data/lib/itsi/server/config/known_paths/cms/wp_plugins_top225.txt +225 -0
  193. data/lib/itsi/server/config/known_paths/cms/wp_themes.readme +12 -0
  194. data/lib/itsi/server/config/known_paths/cms/wp_themes.txt +7336 -0
  195. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/3CharExtBrute.txt +17576 -0
  196. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/CommonWebExtensions.txt +80 -0
  197. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Backup.txt +14 -0
  198. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Common.txt +865 -0
  199. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Compressed.txt +186 -0
  200. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Mostcommon.txt +30 -0
  201. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Skipfish.txt +93 -0
  202. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/WordlistSkipfish.txt +1918 -0
  203. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/copy_of.txt +8 -0
  204. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-directories-lowercase.txt +56180 -0
  205. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-directories.txt +62290 -0
  206. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-extensions-lowercase.txt +2367 -0
  207. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-extensions.txt +2450 -0
  208. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-files-lowercase.txt +35323 -0
  209. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-files.txt +37037 -0
  210. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-words-lowercase.txt +107982 -0
  211. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-words.txt +119600 -0
  212. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-directories-lowercase.txt +26593 -0
  213. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-directories.txt +30009 -0
  214. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-extensions-lowercase.txt +1233 -0
  215. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-extensions.txt +1289 -0
  216. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-files-lowercase.txt +16243 -0
  217. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-files.txt +17128 -0
  218. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-words-lowercase.txt +56293 -0
  219. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-words.txt +63087 -0
  220. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-directories-lowercase.txt +17776 -0
  221. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-directories.txt +20122 -0
  222. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-extensions-lowercase.txt +914 -0
  223. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-extensions.txt +963 -0
  224. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-files-lowercase.txt +10848 -0
  225. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-files.txt +11424 -0
  226. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-words-lowercase.txt +38267 -0
  227. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-words.txt +43003 -0
  228. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/spanish.txt +445 -0
  229. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/test_demo.txt +36 -0
  230. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/upload_variants.txt +44 -0
  231. data/lib/itsi/server/config/known_paths/login-file-locations/Logins.txt +71 -0
  232. data/lib/itsi/server/config/known_paths/login-file-locations/cfm.txt +294 -0
  233. data/lib/itsi/server/config/known_paths/login-file-locations/html.txt +295 -0
  234. data/lib/itsi/server/config/known_paths/login-file-locations/jsp.txt +294 -0
  235. data/lib/itsi/server/config/known_paths/login-file-locations/php.txt +294 -0
  236. data/lib/itsi/server/config/known_paths/login-file-locations/windows-asp.txt +294 -0
  237. data/lib/itsi/server/config/known_paths/login-file-locations/windows-aspx.txt +294 -0
  238. data/lib/itsi/server/config/known_paths/password-file-locations/Passwords.txt +47 -0
  239. data/lib/itsi/server/config/known_paths/php/PHP.txt +30 -0
  240. data/lib/itsi/server/config/known_paths/php/PHP_CommonBackdoors.txt +5 -0
  241. data/lib/itsi/server/config/known_paths/proxy-conf.txt +31 -0
  242. data/lib/itsi/server/config/known_paths/tftp.txt +79 -0
  243. data/lib/itsi/server/config/known_paths/webservers-appservers/ADFS.txt +86 -0
  244. data/lib/itsi/server/config/known_paths/webservers-appservers/AdobeXML.txt +16 -0
  245. data/lib/itsi/server/config/known_paths/webservers-appservers/Apache.txt +101 -0
  246. data/lib/itsi/server/config/known_paths/webservers-appservers/ApacheTomcat.txt +47 -0
  247. data/lib/itsi/server/config/known_paths/webservers-appservers/Apache_Axis.txt +16 -0
  248. data/lib/itsi/server/config/known_paths/webservers-appservers/ColdFusion.txt +111 -0
  249. data/lib/itsi/server/config/known_paths/webservers-appservers/FatwireCMS.txt +390 -0
  250. data/lib/itsi/server/config/known_paths/webservers-appservers/Frontpage.txt +38 -0
  251. data/lib/itsi/server/config/known_paths/webservers-appservers/HP_System_Mgmt_Homepage.txt +239 -0
  252. data/lib/itsi/server/config/known_paths/webservers-appservers/HTTP_POST_Microsoft.txt +2 -0
  253. data/lib/itsi/server/config/known_paths/webservers-appservers/Hyperion.txt +578 -0
  254. data/lib/itsi/server/config/known_paths/webservers-appservers/IIS.txt +187 -0
  255. data/lib/itsi/server/config/known_paths/webservers-appservers/JBoss.txt +5 -0
  256. data/lib/itsi/server/config/known_paths/webservers-appservers/JRun.txt +13 -0
  257. data/lib/itsi/server/config/known_paths/webservers-appservers/JavaServlets_Common.txt +3 -0
  258. data/lib/itsi/server/config/known_paths/webservers-appservers/Joomla_exploitable.txt +1937 -0
  259. data/lib/itsi/server/config/known_paths/webservers-appservers/LotusNotes.txt +206 -0
  260. data/lib/itsi/server/config/known_paths/webservers-appservers/Netware.txt +18 -0
  261. data/lib/itsi/server/config/known_paths/webservers-appservers/Oracle9i.txt +60 -0
  262. data/lib/itsi/server/config/known_paths/webservers-appservers/OracleAppServer.txt +192 -0
  263. data/lib/itsi/server/config/known_paths/webservers-appservers/README.md +6 -0
  264. data/lib/itsi/server/config/known_paths/webservers-appservers/Ruby_Rails.txt +121 -0
  265. data/lib/itsi/server/config/known_paths/webservers-appservers/SAP.txt +463 -0
  266. data/lib/itsi/server/config/known_paths/webservers-appservers/Sharepoint.txt +1707 -0
  267. data/lib/itsi/server/config/known_paths/webservers-appservers/SiteMinder.txt +19 -0
  268. data/lib/itsi/server/config/known_paths/webservers-appservers/SunAppServerGlassfish.txt +51 -0
  269. data/lib/itsi/server/config/known_paths/webservers-appservers/SuniPlanet.txt +35 -0
  270. data/lib/itsi/server/config/known_paths/webservers-appservers/Vignette.txt +73 -0
  271. data/lib/itsi/server/config/known_paths/webservers-appservers/Weblogic.txt +160 -0
  272. data/lib/itsi/server/config/known_paths/webservers-appservers/Websphere.txt +366 -0
  273. data/lib/itsi/server/config/known_paths/wellknown-rfc5785.txt +30 -0
  274. data/lib/itsi/server/config/known_paths.rb +20 -0
  275. data/lib/itsi/server/config/middleware/_index.md +56 -0
  276. data/lib/itsi/server/config/middleware/allow_list.md +46 -0
  277. data/lib/itsi/server/config/middleware/allow_list.rb +42 -0
  278. data/lib/itsi/server/config/middleware/auth_api_key.md +90 -0
  279. data/lib/itsi/server/config/middleware/auth_api_key.rb +51 -0
  280. data/lib/itsi/server/config/middleware/auth_basic.md +45 -0
  281. data/lib/itsi/server/config/middleware/auth_basic.rb +44 -0
  282. data/lib/itsi/server/config/middleware/auth_jwt.md +82 -0
  283. data/lib/itsi/server/config/middleware/auth_jwt.rb +38 -0
  284. data/lib/itsi/server/config/middleware/cache_control.md +78 -0
  285. data/lib/itsi/server/config/middleware/cache_control.rb +45 -0
  286. data/lib/itsi/server/config/middleware/cidr_to_regex.rb +50 -0
  287. data/lib/itsi/server/config/middleware/compression.md +50 -0
  288. data/lib/itsi/server/config/middleware/compression.rb +37 -0
  289. data/lib/itsi/server/config/middleware/cors.md +93 -0
  290. data/lib/itsi/server/config/middleware/cors.rb +32 -0
  291. data/lib/itsi/server/config/middleware/csp.md +37 -0
  292. data/lib/itsi/server/config/middleware/csp.rb +44 -0
  293. data/lib/itsi/server/config/middleware/deny_list.md +45 -0
  294. data/lib/itsi/server/config/middleware/deny_list.rb +42 -0
  295. data/lib/itsi/server/config/middleware/endpoint/_index.md +159 -0
  296. data/lib/itsi/server/config/middleware/endpoint/controller.md +186 -0
  297. data/lib/itsi/server/config/middleware/endpoint/controller.rb +33 -0
  298. data/lib/itsi/server/config/middleware/endpoint/delete.md +12 -0
  299. data/lib/itsi/server/config/middleware/endpoint/delete.rb +42 -0
  300. data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +99 -0
  301. data/lib/itsi/server/config/middleware/endpoint/get.md +12 -0
  302. data/lib/itsi/server/config/middleware/endpoint/get.rb +42 -0
  303. data/lib/itsi/server/config/middleware/endpoint/http_request.md +44 -0
  304. data/lib/itsi/server/config/middleware/endpoint/http_response.md +39 -0
  305. data/lib/itsi/server/config/middleware/endpoint/patch.md +12 -0
  306. data/lib/itsi/server/config/middleware/endpoint/patch.rb +42 -0
  307. data/lib/itsi/server/config/middleware/endpoint/post.md +12 -0
  308. data/lib/itsi/server/config/middleware/endpoint/post.rb +42 -0
  309. data/lib/itsi/server/config/middleware/endpoint/put.md +12 -0
  310. data/lib/itsi/server/config/middleware/endpoint/put.rb +42 -0
  311. data/lib/itsi/server/config/middleware/endpoint/schemas.md +122 -0
  312. data/lib/itsi/server/config/middleware/error_response.md +61 -0
  313. data/lib/itsi/server/config/middleware/error_response.rb +36 -0
  314. data/lib/itsi/server/config/middleware/etag.md +59 -0
  315. data/lib/itsi/server/config/middleware/etag.rb +27 -0
  316. data/lib/itsi/server/config/middleware/grpc.md +172 -0
  317. data/lib/itsi/server/config/middleware/grpc.rb +54 -0
  318. data/lib/itsi/server/config/middleware/intrusion_protection.md +124 -0
  319. data/lib/itsi/server/config/middleware/intrusion_protection.rb +61 -0
  320. data/lib/itsi/server/config/middleware/location.md +107 -0
  321. data/lib/itsi/server/config/middleware/location.rb +99 -0
  322. data/lib/itsi/server/config/middleware/log_requests.md +65 -0
  323. data/lib/itsi/server/config/middleware/log_requests.rb +31 -0
  324. data/lib/itsi/server/config/middleware/max_body.md +18 -0
  325. data/lib/itsi/server/config/middleware/max_body.rb +21 -0
  326. data/lib/itsi/server/config/middleware/proxy.md +62 -0
  327. data/lib/itsi/server/config/middleware/proxy.rb +41 -0
  328. data/lib/itsi/server/config/middleware/rackup_file.md +54 -0
  329. data/lib/itsi/server/config/middleware/rackup_file.rb +44 -0
  330. data/lib/itsi/server/config/middleware/rate_limit.md +126 -0
  331. data/lib/itsi/server/config/middleware/rate_limit.rb +34 -0
  332. data/lib/itsi/server/config/middleware/rate_limit_store.rb +25 -0
  333. data/lib/itsi/server/config/middleware/redirect.md +55 -0
  334. data/lib/itsi/server/config/middleware/redirect.rb +25 -0
  335. data/lib/itsi/server/config/middleware/request_headers.md +34 -0
  336. data/lib/itsi/server/config/middleware/request_headers.rb +24 -0
  337. data/lib/itsi/server/config/middleware/response_headers.md +33 -0
  338. data/lib/itsi/server/config/middleware/response_headers.rb +25 -0
  339. data/lib/itsi/server/config/middleware/run.md +60 -0
  340. data/lib/itsi/server/config/middleware/run.rb +43 -0
  341. data/lib/itsi/server/config/middleware/static_assets.md +73 -0
  342. data/lib/itsi/server/config/middleware/static_assets.rb +87 -0
  343. data/lib/itsi/server/config/middleware/static_response.md +44 -0
  344. data/lib/itsi/server/config/middleware/static_response.rb +29 -0
  345. data/lib/itsi/server/config/middleware/string_rewrite.md +67 -0
  346. data/lib/itsi/server/config/middleware/token_source.rb +32 -0
  347. data/lib/itsi/server/config/middleware.rb +13 -0
  348. data/lib/itsi/server/config/option.rb +14 -0
  349. data/lib/itsi/server/config/options/_index.md +37 -0
  350. data/lib/itsi/server/config/options/auto_reload_config.md +13 -0
  351. data/lib/itsi/server/config/options/auto_reload_config.rb +41 -0
  352. data/lib/itsi/server/config/options/bind.md +71 -0
  353. data/lib/itsi/server/config/options/bind.rb +26 -0
  354. data/lib/itsi/server/config/options/certificates.md +65 -0
  355. data/lib/itsi/server/config/options/daemonize.md +14 -0
  356. data/lib/itsi/server/config/options/daemonize.rb +19 -0
  357. data/lib/itsi/server/config/options/fiber_scheduler.md +34 -0
  358. data/lib/itsi/server/config/options/fiber_scheduler.rb +21 -0
  359. data/lib/itsi/server/config/options/header_read_timeout.md +17 -0
  360. data/lib/itsi/server/config/options/header_read_timeout.rb +19 -0
  361. data/lib/itsi/server/config/options/hooks/_index.md +11 -0
  362. data/lib/itsi/server/config/options/hooks/after_fork.md +13 -0
  363. data/lib/itsi/server/config/options/hooks/after_fork.rb +28 -0
  364. data/lib/itsi/server/config/options/hooks/after_memory_limit_reached.md +14 -0
  365. data/lib/itsi/server/config/options/hooks/after_memory_limit_reached.rb +28 -0
  366. data/lib/itsi/server/config/options/hooks/after_start.md +12 -0
  367. data/lib/itsi/server/config/options/hooks/after_start.rb +28 -0
  368. data/lib/itsi/server/config/options/hooks/before_fork.md +13 -0
  369. data/lib/itsi/server/config/options/hooks/before_fork.rb +28 -0
  370. data/lib/itsi/server/config/options/hooks/before_restart.md +12 -0
  371. data/lib/itsi/server/config/options/hooks/before_restart.rb +28 -0
  372. data/lib/itsi/server/config/options/hooks/before_shutdown.md +12 -0
  373. data/lib/itsi/server/config/options/hooks/before_shutdown.rb +28 -0
  374. data/lib/itsi/server/config/options/include.md +20 -0
  375. data/lib/itsi/server/config/options/include.rb +36 -0
  376. data/lib/itsi/server/config/options/listen_backlog.md +11 -0
  377. data/lib/itsi/server/config/options/listen_backlog.rb +19 -0
  378. data/lib/itsi/server/config/options/log_format.md +18 -0
  379. data/lib/itsi/server/config/options/log_format.rb +19 -0
  380. data/lib/itsi/server/config/options/log_level.md +34 -0
  381. data/lib/itsi/server/config/options/log_level.rb +20 -0
  382. data/lib/itsi/server/config/options/log_target.md +38 -0
  383. data/lib/itsi/server/config/options/log_target.rb +19 -0
  384. data/lib/itsi/server/config/options/log_target_filters.md +17 -0
  385. data/lib/itsi/server/config/options/log_target_filters.rb +19 -0
  386. data/lib/itsi/server/config/options/multithreaded_reactor.md +27 -0
  387. data/lib/itsi/server/config/options/multithreaded_reactor.rb +24 -0
  388. data/lib/itsi/server/config/options/nodelay.md +16 -0
  389. data/lib/itsi/server/config/options/nodelay.rb +19 -0
  390. data/lib/itsi/server/config/options/oob_gc_responses_threshold.md +19 -0
  391. data/lib/itsi/server/config/options/oob_gc_responses_threshold.rb +18 -0
  392. data/lib/itsi/server/config/options/pin_worker_cores.md +17 -0
  393. data/lib/itsi/server/config/options/pin_worker_cores.rb +19 -0
  394. data/lib/itsi/server/config/options/preload.md +21 -0
  395. data/lib/itsi/server/config/options/preload.rb +18 -0
  396. data/lib/itsi/server/config/options/recv_buffer_size.md +15 -0
  397. data/lib/itsi/server/config/options/recv_buffer_size.rb +19 -0
  398. data/lib/itsi/server/config/options/redirect_http_to_https.md +21 -0
  399. data/lib/itsi/server/config/options/redirect_http_to_https.rb +30 -0
  400. data/lib/itsi/server/config/options/request_timeout.md +23 -0
  401. data/lib/itsi/server/config/options/request_timeout.rb +19 -0
  402. data/lib/itsi/server/config/options/reuse_address.md +16 -0
  403. data/lib/itsi/server/config/options/reuse_address.rb +19 -0
  404. data/lib/itsi/server/config/options/reuse_port.md +16 -0
  405. data/lib/itsi/server/config/options/reuse_port.rb +19 -0
  406. data/lib/itsi/server/config/options/scheduler_threads.md +34 -0
  407. data/lib/itsi/server/config/options/scheduler_threads.rb +17 -0
  408. data/lib/itsi/server/config/options/shutdown_timeout.md +17 -0
  409. data/lib/itsi/server/config/options/shutdown_timeout.rb +19 -0
  410. data/lib/itsi/server/config/options/stream_body.md +32 -0
  411. data/lib/itsi/server/config/options/stream_body.rb +18 -0
  412. data/lib/itsi/server/config/options/threads.md +44 -0
  413. data/lib/itsi/server/config/options/threads.rb +17 -0
  414. data/lib/itsi/server/config/options/watch.md +16 -0
  415. data/lib/itsi/server/config/options/watch.rb +28 -0
  416. data/lib/itsi/server/config/options/worker_memory_limit.md +22 -0
  417. data/lib/itsi/server/config/options/worker_memory_limit.rb +18 -0
  418. data/lib/itsi/server/config/options/workers.md +42 -0
  419. data/lib/itsi/server/config/options/workers.rb +17 -0
  420. data/lib/itsi/server/config/typed_struct.rb +242 -0
  421. data/lib/itsi/server/config.rb +289 -0
  422. data/lib/itsi/server/default_app/default_app.rb +34 -0
  423. data/lib/itsi/server/default_app/index.html +115 -0
  424. data/lib/itsi/server/default_config/Itsi.rb +107 -0
  425. data/lib/itsi/server/grpc/grpc_call.rb +246 -0
  426. data/lib/itsi/server/grpc/grpc_interface.rb +107 -0
  427. data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  428. data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  429. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  430. data/lib/itsi/server/rack_interface.rb +94 -0
  431. data/lib/itsi/server/route_tester.rb +157 -0
  432. data/lib/itsi/server/scheduler_interface.rb +21 -0
  433. data/lib/itsi/server/scheduler_mode.rb +10 -0
  434. data/lib/itsi/server/signal_trap.rb +33 -0
  435. data/lib/itsi/server/typed_handlers/param_parser.rb +196 -0
  436. data/lib/itsi/server/typed_handlers/source_parser.rb +56 -0
  437. data/lib/itsi/server/typed_handlers.rb +25 -0
  438. data/lib/itsi/server/version.rb +1 -1
  439. data/lib/itsi/server.rb +265 -9
  440. data/lib/itsi/standard_headers.rb +86 -0
  441. data/lib/ruby_lsp/itsi/addon.rb +129 -0
  442. data/lib/shell_completions/completions.rb +26 -0
  443. metadata +454 -28
  444. data/CHANGELOG.md +0 -5
  445. data/CODE_OF_CONDUCT.md +0 -132
  446. data/LICENSE.txt +0 -21
  447. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  448. data/ext/itsi_server/src/request/mod.rs +0 -1
  449. data/ext/itsi_server/src/server/bind.rs +0 -138
  450. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  451. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  452. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  453. data/ext/itsi_server/src/server/listener.rs +0 -218
  454. data/ext/itsi_server/src/server/tls.rs +0 -138
  455. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  456. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  457. data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,1340 @@
1
+ use crate::{
2
+ default_responses::NOT_FOUND_RESPONSE,
3
+ prelude::*,
4
+ server::{
5
+ http_message_types::{HttpRequest, HttpResponse, RequestExt, ResponseFormat},
6
+ middleware_stack::ErrorResponse,
7
+ redirect_type::RedirectType,
8
+ },
9
+ };
10
+ use base64::{engine::general_purpose, Engine};
11
+ use bytes::Bytes;
12
+ use chrono::{DateTime, Utc};
13
+ use http::{
14
+ header::{
15
+ self, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, LAST_MODIFIED,
16
+ },
17
+ HeaderValue, Response, StatusCode,
18
+ };
19
+ use http_body_util::{combinators::BoxBody, Full};
20
+ use itsi_error::Result;
21
+ use moka::sync::Cache;
22
+ use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
23
+ use serde::Deserialize;
24
+ use serde_json::json;
25
+ use sha2::{Digest, Sha256};
26
+ use std::{
27
+ borrow::Cow,
28
+ cmp::Ordering,
29
+ collections::HashMap,
30
+ convert::Infallible,
31
+ fs::Metadata,
32
+ ops::Deref,
33
+ path::{Path, PathBuf},
34
+ sync::{Arc, LazyLock},
35
+ time::{Duration, Instant, SystemTime},
36
+ };
37
+ use tokio::sync::Mutex;
38
+ use tokio::{fs::File, io::AsyncReadExt};
39
+
40
+ use super::mime_types::get_mime_type;
41
+
42
+ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|| {
43
+ StaticFileServer::new(StaticFileServerConfig {
44
+ root_dir: Path::new("./").to_path_buf(),
45
+ max_file_size: 4096,
46
+ max_entries: 1024 * 1024 * 10,
47
+ recheck_interval: Duration::from_secs(1),
48
+ try_html_extension: true,
49
+ auto_index: true,
50
+ headers: None,
51
+ not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
52
+ serve_hidden_files: false,
53
+ allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
54
+ })
55
+ .unwrap()
56
+ });
57
+
58
+ #[derive(Debug, Clone, Deserialize)]
59
+ pub struct Redirect {
60
+ pub to: String,
61
+ pub r#type: RedirectType,
62
+ }
63
+
64
+ #[derive(Debug, Clone, Deserialize)]
65
+ pub enum NotFoundBehavior {
66
+ #[serde(rename = "error")]
67
+ Error(ErrorResponse),
68
+ #[serde(rename = "fallthrough")]
69
+ FallThrough,
70
+ #[serde(rename = "index")]
71
+ IndexFile(PathBuf),
72
+ #[serde(rename = "redirect")]
73
+ Redirect(Redirect),
74
+ }
75
+
76
+ #[derive(Debug, Clone)]
77
+ pub struct StaticFileServerConfig {
78
+ pub root_dir: PathBuf,
79
+ pub max_file_size: u64,
80
+ pub max_entries: u64,
81
+ pub recheck_interval: Duration,
82
+ pub try_html_extension: bool,
83
+ pub auto_index: bool,
84
+ pub not_found_behavior: NotFoundBehavior,
85
+ pub headers: Option<HashMap<String, String>>,
86
+ pub serve_hidden_files: bool,
87
+ pub allowed_extensions: Vec<String>,
88
+ }
89
+
90
+ #[derive(Debug, Clone)]
91
+ pub struct StaticFileServer {
92
+ config: Arc<StaticFileServerConfig>,
93
+ key_to_path: Arc<Mutex<HashMap<String, PathBuf>>>,
94
+ cache: Cache<PathBuf, CacheEntry>,
95
+ }
96
+
97
+ impl Deref for StaticFileServer {
98
+ type Target = StaticFileServerConfig;
99
+
100
+ fn deref(&self) -> &Self::Target {
101
+ &self.config
102
+ }
103
+ }
104
+
105
+ #[derive(Clone, Debug)]
106
+ struct CacheEntry {
107
+ content: Arc<Bytes>,
108
+ br_encoded: Option<Arc<Bytes>>,
109
+ zstd_encoded: Option<Arc<Bytes>>,
110
+ gzip_encoded: Option<Arc<Bytes>>,
111
+ deflate_encoded: Option<Arc<Bytes>>,
112
+ etag: String,
113
+ last_modified: SystemTime,
114
+ last_checked: Instant,
115
+ }
116
+
117
+ impl CacheEntry {
118
+ pub fn suggest_content_for(
119
+ &self,
120
+ supported_encodings: &[HeaderValue],
121
+ ) -> (Arc<Bytes>, Option<&str>) {
122
+ for encoding_header in supported_encodings {
123
+ if let Ok(header_value) = encoding_header.to_str() {
124
+ for header_value in header_value.split(",").map(|hv| hv.trim()) {
125
+ for algo in header_value.split(";").map(|hv| hv.trim()) {
126
+ match algo {
127
+ "zstd" if self.zstd_encoded.is_some() => {
128
+ return (self.zstd_encoded.clone().unwrap(), Some("zstd"))
129
+ }
130
+ "gzip" if self.gzip_encoded.is_some() => {
131
+ return (self.gzip_encoded.clone().unwrap(), Some("gzip"))
132
+ }
133
+ "br" if self.br_encoded.is_some() => {
134
+ return (self.br_encoded.clone().unwrap(), Some("br"))
135
+ }
136
+ "deflate" if self.deflate_encoded.is_some() => {
137
+ return (self.deflate_encoded.clone().unwrap(), Some("deflate"))
138
+ }
139
+ _ => {}
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ (self.content.clone(), None)
146
+ }
147
+ }
148
+
149
+ #[derive(Debug, Clone)]
150
+ pub enum ServeRange {
151
+ Range(u64, u64),
152
+ Full,
153
+ }
154
+
155
+ impl CacheEntry {
156
+ async fn new(path: PathBuf) -> Result<Self> {
157
+ let (bytes, last_modified) = read_entire_file(&path).await?;
158
+ let etag = {
159
+ let mut hasher = Sha256::new();
160
+ hasher.update(&bytes);
161
+ let result = hasher.finalize();
162
+ general_purpose::STANDARD.encode(result)
163
+ };
164
+ Ok(CacheEntry {
165
+ content: Arc::new(bytes),
166
+ gzip_encoded: read_variant(&path, "gz").await.map(Arc::new),
167
+ br_encoded: read_variant(&path, "br").await.map(Arc::new),
168
+ zstd_encoded: read_variant(&path, "zstd").await.map(Arc::new),
169
+ deflate_encoded: read_variant(&path, "deflate").await.map(Arc::new),
170
+ last_modified,
171
+ etag,
172
+ last_checked: Instant::now(),
173
+ })
174
+ }
175
+
176
+ async fn new_virtual_listing(
177
+ path: PathBuf,
178
+ config: &StaticFileServerConfig,
179
+ accept: ResponseFormat,
180
+ ) -> Self {
181
+ let directory_listing: Bytes =
182
+ generate_directory_listing(path.parent().unwrap(), config, accept)
183
+ .await
184
+ .unwrap_or("".to_owned())
185
+ .into();
186
+ let etag = {
187
+ let mut hasher = Sha256::new();
188
+ hasher.update(&directory_listing);
189
+ let result = hasher.finalize();
190
+ general_purpose::STANDARD.encode(result)
191
+ };
192
+ CacheEntry {
193
+ content: Arc::new(directory_listing),
194
+ gzip_encoded: None,
195
+ br_encoded: None,
196
+ zstd_encoded: None,
197
+ deflate_encoded: None,
198
+ last_modified: SystemTime::now(),
199
+ etag,
200
+ last_checked: Instant::now(),
201
+ }
202
+ }
203
+ }
204
+
205
+ struct ServeStreamArgs(PathBuf, Metadata, u64, u64, bool, Option<SystemTime>, bool);
206
+ struct ServeCacheArgs<'a>(
207
+ &'a CacheEntry,
208
+ u64,
209
+ u64,
210
+ bool,
211
+ Option<SystemTime>,
212
+ bool,
213
+ &'a Path,
214
+ &'a [HeaderValue],
215
+ );
216
+
217
+ impl StaticFileServer {
218
+ pub fn new(config: StaticFileServerConfig) -> Result<Self> {
219
+ let cache = Cache::builder().max_capacity(config.max_entries).build();
220
+ if !config.root_dir.exists() {
221
+ return Err(ItsiError::InternalError(format!(
222
+ "Root directory {} for static file server doesn't exist",
223
+ config.root_dir.display()
224
+ )));
225
+ }
226
+
227
+ if std::fs::read_dir(&config.root_dir).is_err() {
228
+ return Err(ItsiError::InternalError(format!(
229
+ "Root directory {} for static file server is not readable",
230
+ config.root_dir.display()
231
+ )));
232
+ }
233
+
234
+ Ok(StaticFileServer {
235
+ config: Arc::new(config),
236
+ cache,
237
+ key_to_path: Arc::new(Mutex::new(HashMap::new())),
238
+ })
239
+ }
240
+
241
+ #[allow(clippy::too_many_arguments)]
242
+ pub async fn serve(
243
+ &self,
244
+ request: &HttpRequest,
245
+ path: &str,
246
+ abs_path: &str,
247
+ serve_range: ServeRange,
248
+ if_modified_since: Option<SystemTime>,
249
+ is_head_request: bool,
250
+ supported_encodings: &[HeaderValue],
251
+ ) -> Option<HttpResponse> {
252
+ let accept: ResponseFormat = request.accept().into();
253
+ let resolved = self.resolve(path, abs_path, accept.clone()).await;
254
+
255
+ Some(match resolved {
256
+ Ok(ResolvedAsset {
257
+ path,
258
+ cache_entry,
259
+ metadata,
260
+ redirect_to: None,
261
+ }) => {
262
+ let (start, end) = match serve_range {
263
+ ServeRange::Full => (0, u64::MAX),
264
+ ServeRange::Range(start, end) => (start, end),
265
+ };
266
+ let is_range_request = matches!(serve_range, ServeRange::Range { .. });
267
+
268
+ if let Some(cache_entry) = cache_entry {
269
+ self.serve_cached_content(ServeCacheArgs(
270
+ &cache_entry,
271
+ start,
272
+ end,
273
+ is_range_request,
274
+ if_modified_since,
275
+ is_head_request,
276
+ &path,
277
+ supported_encodings,
278
+ ))
279
+ } else {
280
+ self.serve_stream_content(ServeStreamArgs(
281
+ path,
282
+ metadata.unwrap(),
283
+ start,
284
+ end,
285
+ is_range_request,
286
+ if_modified_since,
287
+ is_head_request,
288
+ ))
289
+ .await
290
+ }
291
+ }
292
+ Ok(ResolvedAsset {
293
+ redirect_to: Some(redirect_to),
294
+ ..
295
+ }) => Response::builder()
296
+ .status(StatusCode::MOVED_PERMANENTLY)
297
+ .header(header::LOCATION, redirect_to)
298
+ .body(BoxBody::new(Full::new(Bytes::new())))
299
+ .unwrap(),
300
+ Err(not_found_behavior) => match not_found_behavior {
301
+ NotFoundBehavior::Error(error_response) => {
302
+ error_response
303
+ .to_http_response(request.accept().into())
304
+ .await
305
+ }
306
+ NotFoundBehavior::FallThrough => return None,
307
+ NotFoundBehavior::IndexFile(index_file) => {
308
+ self.serve_single(index_file.to_str().unwrap(), accept, supported_encodings)
309
+ .await
310
+ }
311
+ NotFoundBehavior::Redirect(redirect) => Response::builder()
312
+ .status(redirect.r#type.status_code())
313
+ .header(header::LOCATION, redirect.to)
314
+ .body(BoxBody::new(Full::new(Bytes::new())))
315
+ .unwrap(),
316
+ },
317
+ })
318
+ }
319
+
320
+ pub async fn serve_single_abs(
321
+ &self,
322
+ path: &str,
323
+ accept: ResponseFormat,
324
+ supported_encodings: &[HeaderValue],
325
+ ) -> HttpResponse {
326
+ if let (Ok(root), Ok(path_buf)) = (
327
+ self.root_dir.canonicalize(),
328
+ PathBuf::from(path).canonicalize(),
329
+ ) {
330
+ // Check that the path is under root.
331
+ if let Ok(stripped) = path_buf.strip_prefix(root) {
332
+ if let Some(stripped_str) = stripped.to_str() {
333
+ return self
334
+ .serve_single(stripped_str, accept, supported_encodings)
335
+ .await;
336
+ }
337
+ }
338
+ }
339
+ NOT_FOUND_RESPONSE.to_http_response(accept).await
340
+ }
341
+
342
+ pub async fn serve_single(
343
+ &self,
344
+ path: &str,
345
+ accept: ResponseFormat,
346
+ supported_encodings: &[HeaderValue],
347
+ ) -> HttpResponse {
348
+ let resolved = self.resolve(path, path, accept).await;
349
+ if let Ok(ResolvedAsset {
350
+ path,
351
+ cache_entry: Some(cache_entry),
352
+ ..
353
+ }) = resolved
354
+ {
355
+ return self.serve_cached_content(ServeCacheArgs(
356
+ &cache_entry,
357
+ 0,
358
+ u64::MAX,
359
+ false,
360
+ None,
361
+ false,
362
+ &path,
363
+ supported_encodings,
364
+ ));
365
+ } else if let Ok(ResolvedAsset { path, metadata, .. }) = resolved {
366
+ return self
367
+ .serve_stream_content(ServeStreamArgs(
368
+ path,
369
+ metadata.unwrap(),
370
+ 0,
371
+ u64::MAX,
372
+ false,
373
+ None,
374
+ false,
375
+ ))
376
+ .await;
377
+ }
378
+
379
+ Response::builder()
380
+ .status(StatusCode::NOT_FOUND)
381
+ .body(BoxBody::new(Full::new(Bytes::new())))
382
+ .unwrap()
383
+ }
384
+
385
+ /// Resolves a request key to an actual file path and determines if it needs to be cached
386
+ async fn resolve(
387
+ &self,
388
+ key: &str,
389
+ abs_path: &str,
390
+ accept: ResponseFormat,
391
+ ) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
392
+ // First check if we have a cached mapping for this key
393
+ if let Some(path) = self.key_to_path.lock().await.get(key) {
394
+ // Check if the cached entry is still valid
395
+ if let Some(entry) = self.cache.get(path) {
396
+ let last_check_elapsed = entry.last_checked.elapsed();
397
+ if last_check_elapsed < self.config.recheck_interval {
398
+ // Entry is still fresh, use it
399
+ return Ok(ResolvedAsset {
400
+ path: path.clone(),
401
+ cache_entry: Some(entry.clone()),
402
+ metadata: None,
403
+ redirect_to: None,
404
+ });
405
+ }
406
+
407
+ // Entry is stale, check if file has changed
408
+ if let Ok(metadata) = tokio::fs::metadata(path).await {
409
+ if metadata
410
+ .modified()
411
+ .is_ok_and(|modified| modified == entry.last_modified)
412
+ {
413
+ // File hasn't changed, just update last_checked
414
+ let mut entry = entry;
415
+ entry.last_checked = Instant::now();
416
+ self.cache.insert(path.clone(), entry.clone());
417
+ return Ok(ResolvedAsset {
418
+ path: path.clone(),
419
+ cache_entry: Some(entry.clone()),
420
+ metadata: None,
421
+ redirect_to: None,
422
+ });
423
+ }
424
+
425
+ // File has changed, check if it's still cacheable
426
+ if metadata.len() > self.config.max_file_size {
427
+ // File is now too large, remove from cache
428
+ self.cache.invalidate(path);
429
+ self.key_to_path.lock().await.remove(key);
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ // No valid cached entry, resolve the key to a file path
436
+ let decoded_key = percent_decode_str(key).decode_utf8_lossy();
437
+ let normalized_path = normalize_path(decoded_key)
438
+ .ok_or(NotFoundBehavior::Error(NOT_FOUND_RESPONSE.clone()))?;
439
+
440
+ if !self.config.serve_hidden_files
441
+ && normalized_path
442
+ .file_name()
443
+ .and_then(|f| f.to_str())
444
+ .unwrap_or("")
445
+ .starts_with('.')
446
+ {
447
+ return Err(self.config.not_found_behavior.clone());
448
+ }
449
+
450
+ let mut full_path = self.config.root_dir.clone();
451
+ full_path.push(normalized_path);
452
+ debug!("Resolving path {:?}", full_path);
453
+ // Check if path exists and is a file
454
+ match tokio::fs::metadata(&full_path).await {
455
+ Ok(metadata) => {
456
+ if metadata.is_file() {
457
+ let cache_entry = if metadata.len() <= self.config.max_file_size {
458
+ self.key_to_path
459
+ .lock()
460
+ .await
461
+ .insert(key.to_string(), full_path.clone());
462
+ let cache_entry = CacheEntry::new(full_path.clone()).await.unwrap();
463
+ self.cache.insert(full_path.clone(), cache_entry.clone());
464
+ Some(cache_entry)
465
+ } else {
466
+ None
467
+ };
468
+ return Ok(ResolvedAsset {
469
+ path: full_path,
470
+ cache_entry,
471
+ metadata: Some(metadata),
472
+ redirect_to: None,
473
+ });
474
+ } else if metadata.is_dir() {
475
+ if !abs_path.ends_with("/") {
476
+ return Ok(ResolvedAsset {
477
+ path: full_path,
478
+ cache_entry: None,
479
+ metadata: Some(metadata),
480
+ redirect_to: Some(format!("{}/", abs_path)),
481
+ });
482
+ }
483
+ let mut index_file = None;
484
+
485
+ let index_path = full_path.join("index.html");
486
+ if let Ok(idx_meta) = tokio::fs::metadata(&index_path).await {
487
+ if idx_meta.is_file() {
488
+ index_file = Some(index_path);
489
+ }
490
+ }
491
+
492
+ if index_file.is_none() {
493
+ // Check for case insensitive index.html
494
+ let entries = match tokio::fs::read_dir(&full_path).await {
495
+ Ok(entries) => entries,
496
+ Err(_) => {
497
+ return Err(NotFoundBehavior::Error(NOT_FOUND_RESPONSE.clone()))
498
+ }
499
+ };
500
+
501
+ tokio::pin!(entries);
502
+ while let Some(entry) = entries.next_entry().await.unwrap_or(None) {
503
+ if let Ok(metadata) = entry.metadata().await {
504
+ if entry
505
+ .file_name()
506
+ .to_str()
507
+ .is_some_and(|name| name.eq_ignore_ascii_case("index.html"))
508
+ && metadata.is_file()
509
+ {
510
+ index_file = Some(entry.path());
511
+ break;
512
+ }
513
+ } else {
514
+ error!("Failed to retrieve metadata for entry: {:?}", entry.path());
515
+ return Err(self.config.not_found_behavior.clone());
516
+ }
517
+ }
518
+ }
519
+ if index_file.is_some() {
520
+ let index_path = index_file.unwrap();
521
+ self.key_to_path
522
+ .lock()
523
+ .await
524
+ .insert(key.to_string(), index_path.clone());
525
+ let cache_entry = CacheEntry::new(index_path.clone()).await.unwrap();
526
+ self.cache.insert(index_path.clone(), cache_entry.clone());
527
+ return Ok(ResolvedAsset {
528
+ path: index_path,
529
+ cache_entry: Some(cache_entry),
530
+ metadata: None,
531
+ redirect_to: None,
532
+ });
533
+ }
534
+
535
+ if self.config.auto_index {
536
+ let virtual_path = if matches!(accept, ResponseFormat::JSON) {
537
+ full_path.join(".directory_listing.dir_list_json")
538
+ } else {
539
+ full_path.join(".directory_listing.dir_list")
540
+ };
541
+
542
+ let cache_entry = CacheEntry::new_virtual_listing(
543
+ virtual_path.clone(),
544
+ &self.config,
545
+ accept,
546
+ )
547
+ .await;
548
+ self.key_to_path
549
+ .lock()
550
+ .await
551
+ .insert(key.to_string(), virtual_path.clone());
552
+ self.cache.insert(virtual_path.clone(), cache_entry.clone());
553
+ return Ok(ResolvedAsset {
554
+ path: virtual_path.clone(),
555
+ cache_entry: Some(cache_entry.clone()),
556
+ metadata: None,
557
+ redirect_to: None,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ Err(_) => {
563
+ // Path doesn't exist, try with .html extension if configured
564
+ debug!("Path doesn't exist");
565
+ if self.config.try_html_extension {
566
+ let mut html_path = full_path.clone();
567
+ html_path.set_extension("html");
568
+
569
+ if let Ok(html_meta) = tokio::fs::metadata(&html_path).await {
570
+ if html_meta.is_file() {
571
+ self.key_to_path
572
+ .lock()
573
+ .await
574
+ .insert(key.to_string(), html_path.clone());
575
+ let cache_entry = if html_meta.len() <= self.config.max_file_size {
576
+ let cache_entry = CacheEntry::new(html_path.clone()).await.unwrap();
577
+ self.cache.insert(html_path.clone(), cache_entry.clone());
578
+ Some(cache_entry)
579
+ } else {
580
+ None
581
+ };
582
+ return Ok(ResolvedAsset {
583
+ path: html_path,
584
+ cache_entry,
585
+ metadata: Some(html_meta),
586
+ redirect_to: None,
587
+ });
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+
594
+ // If we get here, we couldn't resolve the key to a file
595
+ Err(self.config.not_found_behavior.clone())
596
+ }
597
+
598
+ async fn stream_file_range(
599
+ &self,
600
+ path: PathBuf,
601
+ start: u64,
602
+ end: u64,
603
+ ) -> Option<BoxBody<Bytes, Infallible>> {
604
+ use futures::TryStreamExt;
605
+ use http_body_util::StreamBody;
606
+ use hyper::body::Frame;
607
+ use tokio::io::AsyncSeekExt;
608
+ use tokio_util::io::ReaderStream;
609
+
610
+ let mut file = match File::open(&path).await {
611
+ Ok(f) => f,
612
+ Err(e) => {
613
+ warn!(
614
+ "Failed to open file for streaming: {}: {}",
615
+ path.display(),
616
+ e
617
+ );
618
+ return None;
619
+ }
620
+ };
621
+
622
+ // Seek to the start position
623
+ if let Err(e) = file.seek(std::io::SeekFrom::Start(start)).await {
624
+ warn!(
625
+ "Failed to seek to position {} in file {}: {}",
626
+ start,
627
+ path.display(),
628
+ e
629
+ );
630
+ return None;
631
+ }
632
+
633
+ // Create a limited reader that will only read up to range_length bytes
634
+ let range_length = end - start + 1;
635
+ let limited_reader = tokio::io::AsyncReadExt::take(file, range_length);
636
+ let path_clone = path.clone();
637
+ let stream = ReaderStream::new(limited_reader)
638
+ .map_ok(Frame::data)
639
+ .map_err(move |e| {
640
+ warn!("Error streaming file {}: {}", path_clone.display(), e);
641
+ unreachable!("We handle IO errors above")
642
+ });
643
+
644
+ Some(BoxBody::new(StreamBody::new(stream)))
645
+ }
646
+
647
+ async fn stream_file(&self, path: PathBuf) -> Option<BoxBody<Bytes, Infallible>> {
648
+ use futures::TryStreamExt;
649
+ use http_body_util::StreamBody;
650
+ use hyper::body::Frame;
651
+ use tokio_util::io::ReaderStream;
652
+
653
+ match File::open(&path).await {
654
+ Ok(file) => {
655
+ let path_clone = path.clone();
656
+ let stream = ReaderStream::new(file)
657
+ .map_ok(Frame::data)
658
+ .map_err(move |e| {
659
+ warn!("Error streaming file {}: {}", path_clone.display(), e);
660
+ unreachable!("We handle IO errors above")
661
+ });
662
+ Some(BoxBody::new(StreamBody::new(stream)))
663
+ }
664
+ Err(e) => {
665
+ warn!(
666
+ "Failed to open file for streaming: {}: {}",
667
+ path.display(),
668
+ e
669
+ );
670
+ None
671
+ }
672
+ }
673
+ }
674
+
675
+ async fn serve_stream_content(&self, stream_args: ServeStreamArgs) -> HttpResponse {
676
+ let ServeStreamArgs(
677
+ file,
678
+ metadata,
679
+ start,
680
+ end,
681
+ is_range_request,
682
+ if_modified_since,
683
+ is_head_request,
684
+ ) = stream_args;
685
+
686
+ let content_length = metadata.len();
687
+ let last_modified = metadata.modified().unwrap();
688
+
689
+ // Handle If-Modified-Since header
690
+ if is_not_modified(last_modified, if_modified_since) {
691
+ return build_not_modified_response();
692
+ }
693
+
694
+ // For range requests, validate the range bounds
695
+ if is_range_request && start >= content_length {
696
+ return Response::builder()
697
+ .status(StatusCode::RANGE_NOT_SATISFIABLE)
698
+ .header("Content-Range", format!("bytes */{}", content_length))
699
+ .body(BoxBody::new(Full::new(Bytes::new())))
700
+ .unwrap();
701
+ }
702
+
703
+ // Adjust end bound for open-ended ranges or to not exceed file size
704
+ let adjusted_end = if end == u64::MAX {
705
+ content_length - 1
706
+ } else {
707
+ std::cmp::min(end, content_length - 1)
708
+ };
709
+
710
+ // Create response based on request type
711
+ let status = if is_range_request {
712
+ StatusCode::PARTIAL_CONTENT
713
+ } else {
714
+ StatusCode::OK
715
+ };
716
+
717
+ let content_range = if is_range_request {
718
+ Some(format!(
719
+ "bytes {}-{}/{}",
720
+ start, adjusted_end, content_length
721
+ ))
722
+ } else {
723
+ None
724
+ };
725
+
726
+ // For HEAD requests, return just the headers
727
+ if is_head_request {
728
+ let mut builder = Response::builder()
729
+ .status(status)
730
+ .header("Content-Type", get_mime_type(&file))
731
+ .header(
732
+ "Content-Length",
733
+ if is_range_request {
734
+ (adjusted_end - start + 1).to_string()
735
+ } else {
736
+ content_length.to_string()
737
+ },
738
+ )
739
+ .header("Last-Modified", format_http_date(last_modified));
740
+
741
+ if let Some(range) = content_range {
742
+ builder = builder.header("Content-Range", range);
743
+ }
744
+
745
+ return builder.body(BoxBody::new(Full::new(Bytes::new()))).unwrap();
746
+ }
747
+
748
+ // For GET requests, prepare the actual content
749
+ if is_range_request {
750
+ // Extract the requested range from the cached content
751
+ let end_idx = std::cmp::min((adjusted_end + 1) as u64, content_length);
752
+
753
+ build_file_response(
754
+ status,
755
+ None,
756
+ None,
757
+ get_mime_type(&file),
758
+ (end_idx - start) as usize,
759
+ last_modified,
760
+ content_range,
761
+ &self.headers,
762
+ self.stream_file_range(file, start, end_idx).await.unwrap(),
763
+ )
764
+ } else {
765
+ build_file_response(
766
+ status,
767
+ None,
768
+ None,
769
+ get_mime_type(&file),
770
+ content_length as usize,
771
+ last_modified,
772
+ content_range,
773
+ &self.headers,
774
+ self.stream_file(file).await.unwrap(),
775
+ )
776
+ }
777
+ }
778
+
779
+ fn serve_cached_content(
780
+ &self,
781
+ serve_cache_args: ServeCacheArgs,
782
+ ) -> http::Response<BoxBody<Bytes, Infallible>> {
783
+ let ServeCacheArgs(
784
+ cache_entry,
785
+ start,
786
+ end,
787
+ is_range_request,
788
+ if_modified_since,
789
+ is_head_request,
790
+ path,
791
+ supported_encodings,
792
+ ) = serve_cache_args;
793
+
794
+ let content_length = cache_entry.content.len() as u64;
795
+
796
+ if is_not_modified(cache_entry.last_modified, if_modified_since) {
797
+ return build_not_modified_response();
798
+ }
799
+
800
+ // For range requests, validate the range bounds
801
+ if is_range_request && start >= content_length {
802
+ return Response::builder()
803
+ .status(StatusCode::RANGE_NOT_SATISFIABLE)
804
+ .header("Content-Range", format!("bytes */{}", content_length))
805
+ .body(BoxBody::new(Full::new(Bytes::new())))
806
+ .unwrap();
807
+ }
808
+
809
+ // Adjust end bound for open-ended ranges or to not exceed file size
810
+ let adjusted_end = if end == u64::MAX {
811
+ content_length.saturating_sub(1)
812
+ } else {
813
+ std::cmp::min(end, content_length.saturating_sub(1))
814
+ };
815
+
816
+ // Create response based on request type
817
+ let status = if is_range_request {
818
+ StatusCode::PARTIAL_CONTENT
819
+ } else {
820
+ StatusCode::OK
821
+ };
822
+
823
+ let content_range = if is_range_request {
824
+ Some(format!(
825
+ "bytes {}-{}/{}",
826
+ start, adjusted_end, content_length
827
+ ))
828
+ } else {
829
+ None
830
+ };
831
+
832
+ // For HEAD requests, return just the headers
833
+ if is_head_request {
834
+ let mut builder = Response::builder()
835
+ .status(status)
836
+ .header("Content-Type", get_mime_type(path))
837
+ .header(
838
+ "Content-Length",
839
+ if is_range_request {
840
+ (adjusted_end - start + 1).to_string()
841
+ } else {
842
+ content_length.to_string()
843
+ },
844
+ )
845
+ .header("Last-Modified", format_http_date(cache_entry.last_modified));
846
+
847
+ if let Some(range) = content_range {
848
+ builder = builder.header("Content-Range", range);
849
+ }
850
+
851
+ return builder.body(BoxBody::new(Full::new(Bytes::new()))).unwrap();
852
+ }
853
+
854
+ if is_range_request {
855
+ let start_idx = start as usize;
856
+ let end_idx = std::cmp::min((adjusted_end + 1) as usize, cache_entry.content.len());
857
+ let range_bytes = cache_entry.content.slice(start_idx..end_idx);
858
+ let etag = {
859
+ let mut hasher = Sha256::new();
860
+ hasher.update(&range_bytes);
861
+ let result = hasher.finalize();
862
+ general_purpose::STANDARD.encode(result)
863
+ };
864
+ build_file_response(
865
+ status,
866
+ None,
867
+ Some(&etag),
868
+ get_mime_type(path),
869
+ range_bytes.len(),
870
+ cache_entry.last_modified,
871
+ content_range,
872
+ &self.headers,
873
+ BoxBody::new(Full::new(range_bytes)),
874
+ )
875
+ } else {
876
+ // Return the full content
877
+ let (content, encoding) = cache_entry.suggest_content_for(supported_encodings);
878
+ let body = build_ok_body(content);
879
+ build_file_response(
880
+ status,
881
+ encoding,
882
+ Some(&cache_entry.etag),
883
+ get_mime_type(path),
884
+ content_length as usize,
885
+ cache_entry.last_modified,
886
+ content_range,
887
+ &self.headers,
888
+ body,
889
+ )
890
+ }
891
+ }
892
+
893
+ pub async fn invalidate_cache(&self, path: &Path) {
894
+ if let Ok(path_buf) = path.to_path_buf().canonicalize() {
895
+ self.cache.invalidate(&path_buf);
896
+ }
897
+ }
898
+ }
899
+
900
+ fn format_http_date(last_modified: SystemTime) -> String {
901
+ let datetime = DateTime::<Utc>::from(last_modified);
902
+ datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
903
+ }
904
+
905
+ async fn read_entire_file(path: &Path) -> std::io::Result<(Bytes, SystemTime)> {
906
+ let metadata = tokio::fs::metadata(path).await?;
907
+ let last_modified = metadata.modified()?;
908
+ let mut file = File::open(path).await?;
909
+ let mut buf = Vec::with_capacity(metadata.len().try_into().unwrap_or(4096));
910
+ file.read_to_end(&mut buf).await?;
911
+ Ok((Bytes::from(buf), last_modified))
912
+ }
913
+
914
+ fn with_added_extension(path: &Path, ext: &str) -> PathBuf {
915
+ let mut new_path = path.to_path_buf();
916
+ if new_path.file_name().is_some() {
917
+ // Append the dot and extension in place.
918
+ new_path.as_mut_os_string().push(".");
919
+ new_path.as_mut_os_string().push(ext);
920
+ }
921
+ new_path
922
+ }
923
+
924
+ async fn read_variant(path: &Path, ext: &str) -> Option<Bytes> {
925
+ let variant = with_added_extension(path, ext);
926
+ if let Ok(metadata) = tokio::fs::metadata(&variant).await {
927
+ if let Ok(mut file) = File::open(&variant).await {
928
+ let mut buf = Vec::with_capacity(metadata.len().try_into().unwrap_or(4096));
929
+ if file.read_to_end(&mut buf).await.is_ok() {
930
+ return Some(Bytes::from(buf));
931
+ }
932
+ }
933
+ }
934
+ None
935
+ }
936
+
937
+ fn build_ok_body(bytes: Arc<Bytes>) -> BoxBody<Bytes, Infallible> {
938
+ BoxBody::new(Full::new(bytes.as_ref().clone()))
939
+ }
940
+
941
+ // Helper function to handle not modified responses
942
+ fn build_not_modified_response() -> http::Response<BoxBody<Bytes, Infallible>> {
943
+ Response::builder()
944
+ .status(StatusCode::NOT_MODIFIED)
945
+ .body(BoxBody::new(Full::new(Bytes::new())))
946
+ .unwrap()
947
+ }
948
+
949
+ #[allow(clippy::too_many_arguments)]
950
+ fn build_file_response(
951
+ status: StatusCode,
952
+ content_encoding: Option<&str>,
953
+ etag: Option<&str>,
954
+ content_type: &str,
955
+ content_length: usize,
956
+ last_modified: SystemTime,
957
+ range_header: Option<String>,
958
+ headers: &Option<HashMap<String, String>>,
959
+ body: BoxBody<Bytes, Infallible>,
960
+ ) -> http::Response<BoxBody<Bytes, Infallible>> {
961
+ let mut builder = Response::builder()
962
+ .status(status)
963
+ .header(CONTENT_TYPE, content_type)
964
+ .header(CONTENT_LENGTH, content_length)
965
+ .header(LAST_MODIFIED, format_http_date(last_modified));
966
+
967
+ if let Some(etag) = etag {
968
+ builder = builder.header(ETAG, etag);
969
+ }
970
+
971
+ if let Some(content_encoding) = content_encoding {
972
+ builder = builder.header(CONTENT_ENCODING, content_encoding);
973
+ }
974
+
975
+ if let Some(range) = range_header {
976
+ builder = builder.header(CONTENT_RANGE, range);
977
+ }
978
+ if let Some(headers) = headers {
979
+ for (key, value) in headers {
980
+ builder = builder.header(key, value);
981
+ }
982
+ }
983
+
984
+ builder.body(body).unwrap()
985
+ }
986
+
987
+ // Helper function to check if a file is too old based on If-Modified-Since
988
+ fn is_not_modified(last_modified: SystemTime, if_modified_since: Option<SystemTime>) -> bool {
989
+ if let Some(ims) = if_modified_since {
990
+ if ims >= last_modified {
991
+ return true;
992
+ }
993
+ }
994
+ false
995
+ }
996
+
997
+ fn normalize_path(path: Cow<'_, str>) -> Option<PathBuf> {
998
+ let mut normalized = PathBuf::new();
999
+ let path = path.trim_start_matches('/');
1000
+
1001
+ for segment in path.split('/') {
1002
+ if segment.is_empty() || segment == "." {
1003
+ continue;
1004
+ }
1005
+
1006
+ if segment == ".." {
1007
+ return None;
1008
+ }
1009
+
1010
+ // Reject Windows-style backslash separators just in case
1011
+ if segment.contains('\\') {
1012
+ return None;
1013
+ }
1014
+
1015
+ normalized.push(segment);
1016
+ }
1017
+
1018
+ Some(normalized)
1019
+ }
1020
+
1021
+ #[derive(Debug)]
1022
+ struct ResolvedAsset {
1023
+ path: PathBuf,
1024
+ cache_entry: Option<CacheEntry>,
1025
+ metadata: Option<Metadata>,
1026
+ redirect_to: Option<String>,
1027
+ }
1028
+
1029
+ impl std::fmt::Display for StaticFileServer {
1030
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1031
+ write!(f, "StaticFileServer(root_dir: {:?})", self.config.root_dir)
1032
+ }
1033
+ }
1034
+
1035
+ async fn generate_directory_listing(
1036
+ dir_path: &Path,
1037
+ config: &StaticFileServerConfig,
1038
+ accept: ResponseFormat,
1039
+ ) -> std::io::Result<String> {
1040
+ match accept {
1041
+ ResponseFormat::JSON => {
1042
+ let directory_display = {
1043
+ let display = dir_path
1044
+ .strip_prefix(&config.root_dir)
1045
+ .unwrap_or(Path::new(""))
1046
+ .to_string_lossy();
1047
+ if display.is_empty() {
1048
+ Cow::Borrowed(".")
1049
+ } else {
1050
+ display
1051
+ }
1052
+ };
1053
+
1054
+ let mut items = Vec::new();
1055
+
1056
+ // Add a parent directory entry if not at the root.
1057
+ if dir_path != config.root_dir {
1058
+ items.push(json!({
1059
+ "name": "..",
1060
+ "path": "..",
1061
+ "is_dir": true,
1062
+ "size": null,
1063
+ "modified": null,
1064
+ }));
1065
+ }
1066
+
1067
+ // Read directory entries.
1068
+ let mut entries = tokio::fs::read_dir(dir_path).await?;
1069
+ let mut dirs = Vec::new();
1070
+ let mut files = Vec::new();
1071
+
1072
+ while let Some(entry) = entries.next_entry().await? {
1073
+ let entry_path = entry.path();
1074
+ let metadata = entry.metadata().await?;
1075
+ let name = entry_path
1076
+ .file_name()
1077
+ .unwrap()
1078
+ .to_string_lossy()
1079
+ .into_owned();
1080
+
1081
+ if !config.serve_hidden_files && name.starts_with('.') {
1082
+ continue;
1083
+ }
1084
+
1085
+ let ext = entry_path
1086
+ .extension()
1087
+ .and_then(|s| s.to_str())
1088
+ .unwrap_or("");
1089
+
1090
+ if metadata.is_dir() {
1091
+ dirs.push((name, metadata));
1092
+ } else if config.allowed_extensions.is_empty()
1093
+ || config.allowed_extensions.iter().any(|e| e == ext)
1094
+ {
1095
+ files.push((name, metadata));
1096
+ }
1097
+ }
1098
+
1099
+ // Sort directories alphabetically with dot directories pushed to the bottom.
1100
+ dirs.sort_by(|(name_a, _), (name_b, _)| {
1101
+ let a_is_dot = name_a.starts_with('.');
1102
+ let b_is_dot = name_b.starts_with('.');
1103
+ if a_is_dot != b_is_dot {
1104
+ if a_is_dot {
1105
+ Ordering::Greater
1106
+ } else {
1107
+ Ordering::Less
1108
+ }
1109
+ } else {
1110
+ name_a.cmp(name_b)
1111
+ }
1112
+ });
1113
+
1114
+ // Sort files so that dot files appear last.
1115
+ files.sort_by(|(name_a, _), (name_b, _)| {
1116
+ let a_is_dot = name_a.starts_with('.');
1117
+ let b_is_dot = name_b.starts_with('.');
1118
+ if a_is_dot != b_is_dot {
1119
+ if a_is_dot {
1120
+ Ordering::Greater
1121
+ } else {
1122
+ Ordering::Less
1123
+ }
1124
+ } else {
1125
+ name_a.cmp(name_b)
1126
+ }
1127
+ });
1128
+
1129
+ // Generate JSON entries for directories.
1130
+ for (name, metadata) in dirs {
1131
+ let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1132
+ let modified = metadata
1133
+ .modified()
1134
+ .ok()
1135
+ .map(|m| {
1136
+ DateTime::<Utc>::from(m)
1137
+ .format("%Y-%m-%d %H:%M:%S")
1138
+ .to_string()
1139
+ })
1140
+ .unwrap_or_else(|| "-".to_string());
1141
+
1142
+ items.push(json!({
1143
+ "name": format!("{}/", name),
1144
+ "path": format!("{}/", encoded),
1145
+ "is_dir": true,
1146
+ "size": null,
1147
+ "modified": modified,
1148
+ }));
1149
+ }
1150
+
1151
+ // Generate JSON entries for files.
1152
+ for (name, metadata) in files {
1153
+ let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1154
+ let file_size = metadata.len();
1155
+ let formatted_size = if file_size < 1024 {
1156
+ format!("{} B", file_size)
1157
+ } else if file_size < 1024 * 1024 {
1158
+ format!("{:.1} KB", file_size as f64 / 1024.0)
1159
+ } else if file_size < 1024 * 1024 * 1024 {
1160
+ format!("{:.1} MB", file_size as f64 / (1024.0 * 1024.0))
1161
+ } else {
1162
+ format!("{:.1} GB", file_size as f64 / (1024.0 * 1024.0 * 1024.0))
1163
+ };
1164
+
1165
+ let modified_str = metadata
1166
+ .modified()
1167
+ .ok()
1168
+ .map(|m| {
1169
+ DateTime::<Utc>::from(m)
1170
+ .format("%Y-%m-%d %H:%M:%S")
1171
+ .to_string()
1172
+ })
1173
+ .unwrap_or_else(|| "-".to_string());
1174
+
1175
+ items.push(json!({
1176
+ "name": name,
1177
+ "path": encoded,
1178
+ "is_dir": false,
1179
+ "size": formatted_size,
1180
+ "modified": modified_str,
1181
+ }));
1182
+ }
1183
+
1184
+ // Build the final JSON object.
1185
+ let json_obj = json!({
1186
+ "title": format!("Directory listing for {}", directory_display),
1187
+ "directory": directory_display,
1188
+ "items": items,
1189
+ });
1190
+
1191
+ // Serialize the JSON object to a pretty-printed string.
1192
+ let json_string = serde_json::to_string_pretty(&json_obj)
1193
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
1194
+
1195
+ Ok(json_string)
1196
+ }
1197
+ ResponseFormat::HTML | ResponseFormat::TEXT | ResponseFormat::UNKNOWN => {
1198
+ let template = include_str!("../default_responses/html/index.html");
1199
+
1200
+ let directory_display = {
1201
+ let display = dir_path
1202
+ .strip_prefix(&config.root_dir)
1203
+ .unwrap_or(Path::new(""))
1204
+ .to_string_lossy();
1205
+ if display.is_empty() {
1206
+ Cow::Borrowed(".")
1207
+ } else {
1208
+ display
1209
+ }
1210
+ };
1211
+
1212
+ let mut rows = String::new();
1213
+ if dir_path != config.root_dir {
1214
+ rows.push_str(
1215
+ r#"<tr><td><a href="..">..</a></td><td class="size">-</td><td class="date">-</td></tr>"#,
1216
+ );
1217
+ rows.push('\n');
1218
+ }
1219
+
1220
+ // Read directory entries.
1221
+ let mut entries = tokio::fs::read_dir(dir_path).await?;
1222
+ let mut dirs = Vec::new();
1223
+ let mut files = Vec::new();
1224
+
1225
+ while let Some(entry) = entries.next_entry().await? {
1226
+ let entry_path = entry.path();
1227
+ let metadata = entry.metadata().await?;
1228
+ let name = entry_path
1229
+ .file_name()
1230
+ .unwrap()
1231
+ .to_string_lossy()
1232
+ .into_owned();
1233
+
1234
+ if !config.serve_hidden_files && name.starts_with('.') {
1235
+ continue;
1236
+ }
1237
+
1238
+ let ext = entry_path
1239
+ .extension()
1240
+ .and_then(|s| s.to_str())
1241
+ .unwrap_or("");
1242
+
1243
+ if metadata.is_dir() {
1244
+ dirs.push((name, metadata));
1245
+ } else if config.allowed_extensions.is_empty()
1246
+ || config.allowed_extensions.iter().any(|e| e == ext)
1247
+ {
1248
+ files.push((name, metadata));
1249
+ }
1250
+ }
1251
+
1252
+ // Sort directories and files alphabetically.
1253
+ dirs.sort_by(|(name_a, _), (name_b, _)| {
1254
+ let a_is_dot = name_a.starts_with('.');
1255
+ let b_is_dot = name_b.starts_with('.');
1256
+ if a_is_dot != b_is_dot {
1257
+ if a_is_dot {
1258
+ Ordering::Greater
1259
+ } else {
1260
+ Ordering::Less
1261
+ }
1262
+ } else {
1263
+ name_a.cmp(name_b)
1264
+ }
1265
+ });
1266
+
1267
+ // Sort files so that dot files are at the bottom.
1268
+ files.sort_by(|(name_a, _), (name_b, _)| {
1269
+ let a_is_dot = name_a.starts_with('.');
1270
+ let b_is_dot = name_b.starts_with('.');
1271
+ if a_is_dot != b_is_dot {
1272
+ if a_is_dot {
1273
+ Ordering::Greater
1274
+ } else {
1275
+ Ordering::Less
1276
+ }
1277
+ } else {
1278
+ name_a.cmp(name_b)
1279
+ }
1280
+ });
1281
+
1282
+ // Generate rows for directories.
1283
+ for (name, metadata) in dirs {
1284
+ let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1285
+
1286
+ rows.push_str(&format!(
1287
+ r#"<tr><td><a href="{0}/">{1}/</a></td><td class="size">-</td><td class="date">{2}</td></tr>"#,
1288
+ encoded,
1289
+ name,
1290
+ metadata.modified().ok().map(|m| DateTime::<Utc>::from(m).format("%Y-%m-%d %H:%M:%S").to_string())
1291
+ .unwrap_or_else(|| "-".to_string())
1292
+ ));
1293
+ rows.push('\n');
1294
+ }
1295
+
1296
+ // Generate rows for files.
1297
+ for (name, metadata) in files {
1298
+ let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1299
+
1300
+ let file_size = metadata.len();
1301
+ let formatted_size = if file_size < 1024 {
1302
+ format!("{} B", file_size)
1303
+ } else if file_size < 1024 * 1024 {
1304
+ format!("{:.1} KB", file_size as f64 / 1024.0)
1305
+ } else if file_size < 1024 * 1024 * 1024 {
1306
+ format!("{:.1} MB", file_size as f64 / (1024.0 * 1024.0))
1307
+ } else {
1308
+ format!("{:.1} GB", file_size as f64 / (1024.0 * 1024.0 * 1024.0))
1309
+ };
1310
+
1311
+ let modified_str = metadata
1312
+ .modified()
1313
+ .ok()
1314
+ .map(|m| {
1315
+ DateTime::<Utc>::from(m)
1316
+ .format("%Y-%m-%d %H:%M:%S")
1317
+ .to_string()
1318
+ })
1319
+ .unwrap_or_else(|| "-".to_string());
1320
+
1321
+ rows.push_str(&format!(
1322
+ r#"<tr><td><a href="{0}">{1}</a></td><td class="size">{2}</td><td class="date">{3}</td></tr>"#,
1323
+ encoded, name, formatted_size, modified_str
1324
+ ));
1325
+ rows.push('\n');
1326
+ }
1327
+
1328
+ // Replace the placeholders in our template.
1329
+ let html = template
1330
+ .replace(
1331
+ "{{title}}",
1332
+ &format!("Directory listing for {}", directory_display),
1333
+ )
1334
+ .replace("{{directory}}", &directory_display)
1335
+ .replace("{{rows}}", &rows);
1336
+
1337
+ Ok(html)
1338
+ }
1339
+ }
1340
+ }