diff --git a/assets/index.html b/assets/index.html
index d814aa0..75bd2a3 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -4,6 +4,9 @@
+
diff --git a/src/main.rs b/src/main.rs
index 6a7d160..f57381f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@ mod auth;
mod http_logger;
mod http_utils;
mod logger;
+mod noscript;
mod server;
mod utils;
diff --git a/src/noscript.rs b/src/noscript.rs
new file mode 100644
index 0000000..d40a0f7
--- /dev/null
+++ b/src/noscript.rs
@@ -0,0 +1,100 @@
+use crate::{
+ server::{IndexData, PathItem, PathType, MAX_SUBPATHS_COUNT},
+ utils::encode_uri,
+};
+
+use anyhow::Result;
+use chrono::{DateTime, Utc};
+use xml::escape::escape_str_pcdata;
+
+pub fn detect_noscript(user_agent: &str) -> bool {
+ [
+ "lynx/", "w3m/", "links ", "elinks/", "curl/", "wget/", "httpie/", "aria2/",
+ ]
+ .iter()
+ .any(|v| user_agent.starts_with(v))
+}
+
+pub fn generate_noscript_html(data: &IndexData) -> Result {
+ let mut html = String::new();
+
+ let title = format!("Index of {}", escape_str_pcdata(&data.href));
+
+ html.push_str("\n");
+ html.push_str("\n");
+ html.push_str(&format!("{title}\n"));
+ html.push_str(
+ r#"
+"#,
+ );
+ html.push_str("\n");
+ html.push_str("\n");
+ html.push_str(&format!("{title}
\n"));
+ html.push_str("\n");
+ html.push_str(" \n");
+ html.push_str(&format!(" {}\n", render_parent()));
+
+ for path in &data.paths {
+ html.push_str(&format!(" {}\n", render_path_item(path)));
+ }
+
+ html.push_str(" \n");
+ html.push_str("
\n");
+ html.push_str("\n");
+
+ Ok(html)
+}
+
+fn render_parent() -> String {
+ let value = "../";
+ format!("{value} | | |
")
+}
+
+fn render_path_item(path: &PathItem) -> String {
+ let href = encode_uri(&path.name);
+ let suffix = if path.path_type.is_dir() { "/" } else { "" };
+ let name = escape_str_pcdata(&path.name);
+ let mtime = format_mtime(path.mtime).unwrap_or_default();
+ let size = format_size(path.size, path.path_type);
+
+ format!("{name}{suffix} | {mtime} | {size} |
")
+}
+
+fn format_mtime(mtime: u64) -> Option {
+ let datetime = DateTime::::from_timestamp_millis(mtime as _)?;
+ Some(datetime.format("%Y-%m-%dT%H:%M:%S.%3fZ").to_string())
+}
+
+fn format_size(size: u64, path_type: PathType) -> String {
+ if path_type.is_dir() {
+ let unit = if size == 1 { "item" } else { "items" };
+ let num = match size >= MAX_SUBPATHS_COUNT {
+ true => format!(">{}", MAX_SUBPATHS_COUNT - 1),
+ false => size.to_string(),
+ };
+ format!("{num} {unit}")
+ } else {
+ if size == 0 {
+ return "0 B".to_string();
+ }
+ const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
+ let i = (size as f64).log2() / 10.0;
+ let i = i.floor() as usize;
+
+ if i >= UNITS.len() {
+ // Handle extremely large numbers beyond Terabytes
+ return format!("{:.2} PB", size as f64 / 1024.0f64.powi(5));
+ }
+
+ let size = size as f64 / 1024.0f64.powi(i as i32);
+ format!("{:.2} {}", size, UNITS[i])
+ }
+}
diff --git a/src/server.rs b/src/server.rs
index c7165cd..a100006 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -2,6 +2,7 @@
use crate::auth::{www_authenticate, AccessPaths, AccessPerm};
use crate::http_utils::{body_full, IncomingStream, LengthLimitedStream};
+use crate::noscript::{detect_noscript, generate_noscript_html};
use crate::utils::{
decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, parse_range,
try_get_file_name,
@@ -63,7 +64,7 @@ const BUF_SIZE: usize = 65536;
const EDITABLE_TEXT_MAX_SIZE: u64 = 4194304; // 4M
const RESUMABLE_UPLOAD_MIN_SIZE: u64 = 20971520; // 20M
const HEALTH_CHECK_PATH: &str = "__dufs__/health";
-const MAX_SUBPATHS_COUNT: u64 = 1000;
+pub const MAX_SUBPATHS_COUNT: u64 = 1000;
pub struct Server {
args: Args,
@@ -110,18 +111,12 @@ impl Server {
let uri = req.uri().clone();
let assets_prefix = &self.assets_prefix;
let enable_cors = self.args.enable_cors;
- let is_microsoft_webdav = req
- .headers()
- .get("user-agent")
- .and_then(|v| v.to_str().ok())
- .map(|v| v.starts_with("Microsoft-WebDAV-MiniRedir/"))
- .unwrap_or_default();
let mut http_log_data = self.args.http_logger.data(&req);
if let Some(addr) = addr {
http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
}
- let mut res = match self.clone().handle(req, is_microsoft_webdav).await {
+ let mut res = match self.clone().handle(req).await {
Ok(res) => {
http_log_data.insert("status".to_string(), res.status().as_u16().to_string());
if !uri.path().starts_with(assets_prefix) {
@@ -141,28 +136,33 @@ impl Server {
}
};
- if is_microsoft_webdav {
- // microsoft webdav requires this.
- res.headers_mut()
- .insert(CONNECTION, HeaderValue::from_static("close"));
- }
if enable_cors {
add_cors(&mut res);
}
Ok(res)
}
- pub async fn handle(
- self: Arc,
- req: Request,
- is_microsoft_webdav: bool,
- ) -> Result {
+ pub async fn handle(self: Arc, req: Request) -> Result {
let mut res = Response::default();
let req_path = req.uri().path();
let headers = req.headers();
let method = req.method().clone();
+ let user_agent = headers
+ .get("user-agent")
+ .and_then(|v| v.to_str().ok())
+ .map(|v| v.to_lowercase())
+ .unwrap_or_default();
+
+ let is_microsoft_webdav = user_agent.starts_with("microsoft-webdav-miniredir/");
+
+ if is_microsoft_webdav {
+ // microsoft webdav requires this.
+ res.headers_mut()
+ .insert(CONNECTION, HeaderValue::from_static("close"));
+ }
+
let relative_path = match self.resolve_path(req_path) {
Some(v) => v,
None => {
@@ -198,10 +198,14 @@ impl Server {
};
let query = req.uri().query().unwrap_or_default();
- let query_params: HashMap = form_urlencoded::parse(query.as_bytes())
+ let mut query_params: HashMap = form_urlencoded::parse(query.as_bytes())
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
+ if detect_noscript(&user_agent) {
+ query_params.insert("noscript".to_string(), String::new());
+ }
+
if method.as_str() == "CHECKAUTH" {
*res.body_mut() = body_full(user.clone().unwrap_or_default());
return Ok(res);
@@ -1203,6 +1207,10 @@ impl Server {
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
serde_json::to_string_pretty(&data)?
+ } else if has_query_flag(query_params, "noscript") {
+ res.headers_mut()
+ .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
+ generate_noscript_html(&data)?
} else {
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
@@ -1417,45 +1425,33 @@ impl Server {
}
#[derive(Debug, Serialize, PartialEq)]
-enum DataKind {
+pub enum DataKind {
Index,
Edit,
View,
}
#[derive(Debug, Serialize)]
-struct IndexData {
- href: String,
- kind: DataKind,
- uri_prefix: String,
- allow_upload: bool,
- allow_delete: bool,
- allow_search: bool,
- allow_archive: bool,
- dir_exists: bool,
- auth: bool,
- user: Option,
- paths: Vec,
-}
-
-#[derive(Debug, Serialize)]
-struct EditData {
- href: String,
- kind: DataKind,
- uri_prefix: String,
- allow_upload: bool,
- allow_delete: bool,
- auth: bool,
- user: Option,
- editable: bool,
+pub struct IndexData {
+ pub href: String,
+ pub kind: DataKind,
+ pub uri_prefix: String,
+ pub allow_upload: bool,
+ pub allow_delete: bool,
+ pub allow_search: bool,
+ pub allow_archive: bool,
+ pub dir_exists: bool,
+ pub auth: bool,
+ pub user: Option,
+ pub paths: Vec,
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
-struct PathItem {
- path_type: PathType,
- name: String,
- mtime: u64,
- size: u64,
+pub struct PathItem {
+ pub path_type: PathType,
+ pub name: String,
+ pub mtime: u64,
+ pub size: u64,
}
impl PathItem {
@@ -1533,14 +1529,20 @@ impl PathItem {
}
}
-#[derive(Debug, Serialize, Eq, PartialEq)]
-enum PathType {
+#[derive(Debug, Serialize, Clone, Copy, Eq, PartialEq)]
+pub enum PathType {
Dir,
SymlinkDir,
File,
SymlinkFile,
}
+impl PathType {
+ pub fn is_dir(&self) -> bool {
+ matches!(self, Self::Dir | Self::SymlinkDir)
+ }
+}
+
impl Ord for PathType {
fn cmp(&self, other: &Self) -> Ordering {
let to_value = |t: &Self| -> u8 {
@@ -1559,6 +1561,18 @@ impl PartialOrd for PathType {
}
}
+#[derive(Debug, Serialize)]
+struct EditData {
+ href: String,
+ kind: DataKind,
+ uri_prefix: String,
+ allow_upload: bool,
+ allow_delete: bool,
+ auth: bool,
+ user: Option,
+ editable: bool,
+}
+
fn to_timestamp(time: &SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
diff --git a/tests/http.rs b/tests/http.rs
index 28c9d79..4627acc 100644
--- a/tests/http.rs
+++ b/tests/http.rs
@@ -82,6 +82,19 @@ fn get_dir_simple(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
Ok(())
}
+#[rstest]
+fn get_dir_noscript(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}?noscript", server.url()))?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "text/html; charset=utf-8"
+ );
+ let text = resp.text().unwrap();
+ assert!(text.contains(r#"index.html | "#));
+ Ok(())
+}
+
#[rstest]
fn head_dir_zip(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}?zip", server.url())).send()?;