itsi-server 0.1.1 → 0.1.20

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.

Potentially problematic release.


This version of itsi-server might be problematic. Click here for more details.

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