itsi-server 0.2.22-aarch64-linux

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