diff --git a/assets/index.css b/assets/index.css
index ae2efb4..7f89a7e 100644
--- a/assets/index.css
+++ b/assets/index.css
@@ -208,9 +208,12 @@ body {
outline: none;
}
-.save-btn {
+.toolbox2 {
margin-left: auto;
margin-right: 2em;
+}
+
+.save-btn {
cursor: pointer;
user-select: none;
}
diff --git a/assets/index.html b/assets/index.html
index 6694f35..fee290c 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -72,12 +72,27 @@
-
diff --git a/assets/index.js b/assets/index.js
index 4349c38..28ff54c 100644
--- a/assets/index.js
+++ b/assets/index.js
@@ -16,6 +16,8 @@
* @property {boolean} allow_delete
* @property {boolean} allow_search
* @property {boolean} allow_archive
+ * @property {boolean} auth
+ * @property {string} user
* @property {boolean} dir_exists
* @property {string} editable
*/
@@ -71,6 +73,10 @@ let $emptyFolder;
* @type Element
*/
let $editor;
+/**
+ * @type Element
+ */
+let $userBtn;
function ready() {
$pathsTable = document.querySelector(".paths-table")
@@ -79,6 +85,7 @@ function ready() {
$uploadersTable = document.querySelector(".uploaders-table");
$emptyFolder = document.querySelector(".empty-folder");
$editor = document.querySelector(".editor");
+ $userBtn = document.querySelector(".user-btn");
addBreadcrumb(DATA.href, DATA.uri_prefix);
@@ -86,6 +93,10 @@ function ready() {
document.title = `Index of ${DATA.href} - Dufs`;
document.querySelector(".index-page").classList.remove("hidden");
+ if (DATA.auth) {
+ setupAuth();
+ }
+
if (DATA.allow_search) {
setupSearch()
}
@@ -106,7 +117,6 @@ function ready() {
document.title = `Edit of ${DATA.href} - Dufs`;
document.querySelector(".editor-page").classList.remove("hidden");;
-
setupEditor();
}
}
@@ -203,16 +213,22 @@ Uploader.globalIdx = 0;
Uploader.runnings = 0;
+Uploader.auth = false;
+
/**
* @type Uploader[]
*/
Uploader.queues = [];
-Uploader.runQueue = () => {
+Uploader.runQueue = async () => {
if (Uploader.runnings > 2) return;
let uploader = Uploader.queues.shift();
if (!uploader) return;
+ if (!Uploader.auth) {
+ Uploader.auth = true;
+ await login();
+ }
uploader.ajax();
}
@@ -365,7 +381,7 @@ function addPath(file, index) {
${getPathSvg(file.path_type)}
- ${encodedName}
+ ${encodedName}
|
${formatMtime(file.mtime)} |
${formatSize(file.size).join(" ")} |
@@ -385,10 +401,11 @@ async function deletePath(index) {
if (!confirm(`Delete \`${file.name}\`?`)) return;
try {
+ await login();
const res = await fetch(newUrl(file.name), {
method: "DELETE",
});
- await assertFetch(res);
+ await assertResOK(res);
document.getElementById(`addPath${index}`).remove();
DATA.paths[index] = null;
if (!DATA.paths.find(v => !!v)) {
@@ -425,14 +442,15 @@ async function movePath(index) {
const newFileUrl = fileUrlObj.origin + prefix + newPath.split("/").map(encodeURIComponent).join("/");
try {
+ await login();
const res = await fetch(fileUrl, {
method: "MOVE",
headers: {
"Destination": newFileUrl,
}
});
- await assertFetch(res);
- location.href = newFileUrl.split("/").slice(0, -1).join("/")
+ await assertResOK(res);
+ location.href = newFileUrl.split("/").slice(0, -1).join("/");
} catch (err) {
alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
}
@@ -445,7 +463,7 @@ function dropzone() {
e.stopPropagation();
});
});
- document.addEventListener("drop", e => {
+ document.addEventListener("drop", async e => {
if (!e.dataTransfer.items[0].webkitGetAsEntry) {
const files = e.dataTransfer.files.filter(v => v.size > 0);
for (const file of files) {
@@ -462,6 +480,18 @@ function dropzone() {
});
}
+function setupAuth() {
+ if (DATA.user) {
+ $userBtn.classList.remove("hidden");
+ $userBtn.title = DATA.user;
+ } else {
+ const $loginBtn = document.querySelector(".login-btn");
+ $loginBtn.classList.remove("hidden");
+ $loginBtn.addEventListener("click", () => login(true));
+ }
+}
+
+
/**
* Setup searchbar
*/
@@ -491,7 +521,7 @@ function setupUpload() {
if (name) createFolder(name);
});
document.querySelector(".upload-file").classList.remove("hidden");
- document.getElementById("file").addEventListener("change", e => {
+ document.getElementById("file").addEventListener("change", async e => {
const files = e.target.files;
for (let file of files) {
new Uploader(file, []).upload();
@@ -527,7 +557,7 @@ async function setupEditor() {
$editor.classList.remove("hidden");
try {
const res = await fetch(baseUrl());
- await assertFetch(res);
+ await assertResOK(res);
const text = await res.text();
$editor.value = text;
} catch (err) {
@@ -549,6 +579,24 @@ async function saveChange() {
}
}
+async function login(alert = false) {
+ if (!DATA.auth) return;
+ try {
+ const res = await fetch(baseUrl() + "?auth");
+ await assertResOK(res);
+ document.querySelector(".login-btn").classList.add("hidden");
+ $userBtn.classList.remove("hidden");
+ $userBtn.title = "";
+ } catch (err) {
+ let message = `Cannot login, ${err.message}`;
+ if (alert) {
+ alert(message);
+ } else {
+ throw new Error(message);
+ }
+ }
+}
+
/**
* Create a folder
* @param {string} name
@@ -556,10 +604,11 @@ async function saveChange() {
async function createFolder(name) {
const url = newUrl(name);
try {
+ await login();
const res = await fetch(url, {
method: "MKCOL",
});
- await assertFetch(res);
+ await assertResOK(res);
location.href = url;
} catch (err) {
alert(`Cannot create folder \`${name}\`, ${err.message}`);
@@ -569,11 +618,12 @@ async function createFolder(name) {
async function createFile(name) {
const url = newUrl(name);
try {
+ await login();
const res = await fetch(url, {
method: "PUT",
body: "",
});
- await assertFetch(res);
+ await assertResOK(res);
location.href = url + "?edit";
} catch (err) {
alert(`Cannot create file \`${name}\`, ${err.message}`);
@@ -663,7 +713,7 @@ function encodedStr(rawStr) {
});
}
-async function assertFetch(res) {
+async function assertResOK(res) {
if (!(res.status >= 200 && res.status < 300)) {
throw new Error(await res.text())
}
diff --git a/src/auth.rs b/src/auth.rs
index e19bc16..cf51ce6 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -72,6 +72,10 @@ impl AccessControl {
Ok(Self { rules })
}
+ pub fn valid(&self) -> bool {
+ !self.rules.is_empty()
+ }
+
pub fn guard(
&self,
path: &str,
@@ -134,6 +138,9 @@ impl GuardType {
pub fn is_reject(&self) -> bool {
*self == GuardType::Reject
}
+ pub fn is_readwrite(&self) -> bool {
+ *self == GuardType::ReadWrite
+ }
}
fn sanitize_path(path: &str, uri_prefix: &str) -> String {
diff --git a/src/server.rs b/src/server.rs
index d5e7b09..fa37d75 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -144,6 +144,18 @@ impl Server {
return Ok(res);
}
+ let query = req.uri().query().unwrap_or_default();
+ let query_params: HashMap
= form_urlencoded::parse(query.as_bytes())
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+
+ if query_params.contains_key("auth") {
+ if !guard_type.is_readwrite() {
+ self.auth_reject(&mut res);
+ }
+ return Ok(res);
+ }
+
let head_only = method == Method::HEAD;
if self.args.path_is_file {
@@ -170,11 +182,6 @@ impl Server {
let path = path.as_path();
- let query = req.uri().query().unwrap_or_default();
- let query_params: HashMap = form_urlencoded::parse(query.as_bytes())
- .map(|(k, v)| (k.to_string(), v.to_string()))
- .collect();
-
let (is_miss, is_dir, is_file, size) = match fs::metadata(path).await.ok() {
Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()),
None => (true, false, false, 0),
@@ -204,21 +211,32 @@ impl Server {
}
self.handle_zip_dir(path, head_only, &mut res).await?;
} else if allow_search && query_params.contains_key("q") {
- self.handle_search_dir(path, &query_params, head_only, &mut res)
+ let user = self.retrieve_user(authorization);
+ self.handle_search_dir(path, &query_params, head_only, user, &mut res)
.await?;
} else {
+ let user = self.retrieve_user(authorization);
self.handle_render_index(
path,
&query_params,
headers,
head_only,
+ user,
&mut res,
)
.await?;
}
} else if render_index || render_spa {
- self.handle_render_index(path, &query_params, headers, head_only, &mut res)
- .await?;
+ let user = self.retrieve_user(authorization);
+ self.handle_render_index(
+ path,
+ &query_params,
+ headers,
+ head_only,
+ user,
+ &mut res,
+ )
+ .await?;
} else if query_params.contains_key("zip") {
if !allow_archive {
status_not_found(&mut res);
@@ -226,15 +244,19 @@ impl Server {
}
self.handle_zip_dir(path, head_only, &mut res).await?;
} else if allow_search && query_params.contains_key("q") {
- self.handle_search_dir(path, &query_params, head_only, &mut res)
+ let user = self.retrieve_user(authorization);
+ self.handle_search_dir(path, &query_params, head_only, user, &mut res)
.await?;
} else {
- self.handle_ls_dir(path, true, &query_params, head_only, &mut res)
+ let user = self.retrieve_user(authorization);
+ self.handle_ls_dir(path, true, &query_params, head_only, user, &mut res)
.await?;
}
} else if is_file {
if query_params.contains_key("edit") {
- self.handle_edit_file(path, head_only, &mut res).await?;
+ let user = self.retrieve_user(authorization);
+ self.handle_edit_file(path, head_only, user, &mut res)
+ .await?;
} else {
self.handle_send_file(path, headers, head_only, &mut res)
.await?;
@@ -243,7 +265,8 @@ impl Server {
self.handle_render_spa(path, headers, head_only, &mut res)
.await?;
} else if allow_upload && req_path.ends_with('/') {
- self.handle_ls_dir(path, false, &query_params, head_only, &mut res)
+ let user = self.retrieve_user(authorization);
+ self.handle_ls_dir(path, false, &query_params, head_only, user, &mut res)
.await?;
} else {
status_not_found(&mut res);
@@ -382,6 +405,7 @@ impl Server {
exist: bool,
query_params: &HashMap,
head_only: bool,
+ user: Option,
res: &mut Response,
) -> BoxResult<()> {
let mut paths = vec![];
@@ -394,7 +418,7 @@ impl Server {
}
}
};
- self.send_index(path, paths, exist, query_params, head_only, res)
+ self.send_index(path, paths, exist, query_params, head_only, user, res)
}
async fn handle_search_dir(
@@ -402,6 +426,7 @@ impl Server {
path: &Path,
query_params: &HashMap,
head_only: bool,
+ user: Option,
res: &mut Response,
) -> BoxResult<()> {
let mut paths: Vec = vec![];
@@ -452,7 +477,7 @@ impl Server {
}
}
}
- self.send_index(path, paths, true, query_params, head_only, res)
+ self.send_index(path, paths, true, query_params, head_only, user, res)
}
async fn handle_zip_dir(
@@ -495,6 +520,7 @@ impl Server {
query_params: &HashMap,
headers: &HeaderMap,
head_only: bool,
+ user: Option,
res: &mut Response,
) -> BoxResult<()> {
let index_path = path.join(INDEX_NAME);
@@ -507,7 +533,7 @@ impl Server {
self.handle_send_file(&index_path, headers, head_only, res)
.await?;
} else if self.args.render_try_index {
- self.handle_ls_dir(path, true, query_params, head_only, res)
+ self.handle_ls_dir(path, true, query_params, head_only, user, res)
.await?;
} else {
status_not_found(res)
@@ -682,6 +708,7 @@ impl Server {
&self,
path: &Path,
head_only: bool,
+ user: Option,
res: &mut Response,
) -> BoxResult<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
@@ -696,6 +723,8 @@ impl Server {
uri_prefix: self.args.uri_prefix.clone(),
allow_upload: self.args.allow_upload,
allow_delete: self.args.allow_delete,
+ auth: self.args.auth.valid(),
+ user,
editable,
};
res.headers_mut()
@@ -842,6 +871,7 @@ impl Server {
Ok(())
}
+ #[allow(clippy::too_many_arguments)]
fn send_index(
&self,
path: &Path,
@@ -849,6 +879,7 @@ impl Server {
exist: bool,
query_params: &HashMap,
head_only: bool,
+ user: Option,
res: &mut Response,
) -> BoxResult<()> {
if let Some(sort) = query_params.get("sort") {
@@ -903,6 +934,8 @@ impl Server {
allow_search: self.args.allow_search,
allow_archive: self.args.allow_archive,
dir_exists: exist,
+ auth: self.args.auth.valid(),
+ user,
paths,
};
let output = if query_params.contains_key("json") {
@@ -1057,6 +1090,10 @@ impl Server {
size,
}))
}
+
+ fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option {
+ self.args.auth_method.get_user(authorization?)
+ }
}
#[derive(Debug, Serialize)]
@@ -1075,6 +1112,8 @@ struct IndexData {
allow_search: bool,
allow_archive: bool,
dir_exists: bool,
+ auth: bool,
+ user: Option,
paths: Vec,
}
@@ -1085,6 +1124,8 @@ struct EditData {
uri_prefix: String,
allow_upload: bool,
allow_delete: bool,
+ auth: bool,
+ user: Option,
editable: bool,
}
diff --git a/tests/auth.rs b/tests/auth.rs
index 83b6434..39e5757 100644
--- a/tests/auth.rs
+++ b/tests/auth.rs
@@ -45,6 +45,20 @@ fn auth_skip_on_options_method(
Ok(())
}
+#[rstest]
+fn auth_check(
+ #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,
+) -> Result<(), Error> {
+ let url = format!("{}index.html?auth", server.url());
+ let resp = fetch!(b"GET", &url).send()?;
+ assert_eq!(resp.status(), 401);
+ let resp = fetch!(b"GET", &url).send_with_digest_auth("user2", "pass2")?;
+ assert_eq!(resp.status(), 401);
+ let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
#[rstest]
fn auth_readonly(
#[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,