]> OzVa Git service - ozva-cloud/commitdiff
feat: add basic auth and readonly mode
authorsigoden <sigoden@gmail.com>
Thu, 26 May 2022 10:06:52 +0000 (18:06 +0800)
committersigoden <sigoden@gmail.com>
Thu, 26 May 2022 10:42:35 +0000 (18:42 +0800)
Cargo.lock
Cargo.toml
src/args.rs
src/index.css
src/index.html
src/server.rs

index 40c1f3f7ec690b599411a693d65b2e871246c271..35cea9d35377b62312755be3569958353fe9335f 100644 (file)
@@ -8,6 +8,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -49,9 +55,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "duf2"
+name = "duf"
 version = "0.1.0"
 dependencies = [
+ "base64",
  "clap",
  "futures",
  "hyper",
index a3188499c1ff05ae23c027d36ad9839888c9f5f9..bdc7595cb05b09fb306d3601273899c150ef80a9 100644 (file)
@@ -1,5 +1,5 @@
 [package]
-name = "duf2"
+name = "duf"
 version = "0.1.0"
 edition = "2021"
 
@@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 tokio-util = { version = "0.7", features = ["codec", "io-util"] }
 futures = "0.3"
+base64 = "0.13.0"
 
 [dev-dependencies]
 tempfile = "3"
index bae28118322c25ec90f5b88f0923fcab9c470f04..4fdeba8f9b5200d8683676707d689b63dcdc0b59 100644 (file)
@@ -29,11 +29,24 @@ fn app() -> clap::Command<'static> {
         .allow_invalid_utf8(true)
         .help("Path to a directory for serving files");
 
+    let arg_readonly = Arg::new("readonly")
+        .short('r')
+        .long("readonly")
+        .help("Only serve static files, no operations like upload and delete");
+
+    let arg_auth = Arg::new("auth")
+        .short('a')
+        .long("auth")
+        .help("Authenticate with user and pass")
+        .value_name("user:pass");
+
     clap::command!()
         .about(ABOUT)
         .arg(arg_address)
         .arg(arg_port)
         .arg(arg_path)
+        .arg(arg_readonly)
+        .arg(arg_auth)
 }
 
 pub fn matches() -> ArgMatches {
@@ -45,6 +58,8 @@ pub struct Args {
     pub address: String,
     pub port: u16,
     pub path: PathBuf,
+    pub readonly: bool,
+    pub auth: Option<String>,
 }
 
 impl Args {
@@ -57,11 +72,15 @@ impl Args {
         let port = matches.value_of_t::<u16>("port")?;
         let path = matches.value_of_os("path").unwrap_or_default();
         let path = Args::parse_path(path)?;
+        let readonly = matches.is_present("readonly");
+        let auth = matches.value_of("auth").map(|v| v.to_owned());
 
         Ok(Args {
             address,
             port,
             path,
+            readonly,
+            auth,
         })
     }
 
index d92d9c681797313f59340146e20a4f4e8c911a7b..f59b195d3b38558a03caea346c6bae754ae7c7ab 100644 (file)
@@ -44,7 +44,7 @@ html {
   padding-left: 0.5em;
 }
 
-.action {
+.upload-control {
   cursor: pointer;
 }
 
index 2c67065f102931c2fdc459da44d56553b09d1f60..31748715c461fe472a84b52745603cd5b20f412a 100644 (file)
 <body>
   <div class="head">
     <div class="breadcrumb"></div>
-    <div class="action" title="Upload file">
-      <label for="file">
-        <svg viewBox="0 0 384.97 384.97" width="14" height="14"><path d="M372.939,264.641c-6.641,0-12.03,5.39-12.03,12.03v84.212H24.061v-84.212c0-6.641-5.39-12.03-12.03-12.03 S0,270.031,0,276.671v96.242c0,6.641,5.39,12.03,12.03,12.03h360.909c6.641,0,12.03-5.39,12.03-12.03v-96.242 C384.97,270.019,379.58,264.641,372.939,264.641z"></path><path d="M117.067,103.507l63.46-62.558v235.71c0,6.641,5.438,12.03,12.151,12.03c6.713,0,12.151-5.39,12.151-12.03V40.95 l63.46,62.558c4.74,4.704,12.439,4.704,17.179,0c4.74-4.704,4.752-12.319,0-17.011l-84.2-82.997 c-4.692-4.656-12.584-4.608-17.191,0L99.888,86.496c-4.752,4.704-4.74,12.319,0,17.011 C104.628,108.211,112.327,108.211,117.067,103.507z"></path></svg>
-      </label>
-      <input type="file" id="file" name="file" multiple>
-    </div>
   </div>
   <div class="main">
     <div class="uploaders">
   </div>
   <script>
 
+    const $head = document.querySelector(".head");
     const $tbody = document.querySelector(".main tbody");
     const $breadcrumb = document.querySelector(".breadcrumb");
-    const $fileInput = document.getElementById("file");
     const $uploaders = document.querySelector(".uploaders");
-    const { breadcrumb, paths } = __DATA__;
+    const $uploadControl = document.querySelector(".upload-control");
+    const { breadcrumb, paths, readonly } = __DATA__;
     let uploaderIdx = 0;
 
     class Uploader {
 `)
     }
 
+    function addUploadControl() {
+      $head.insertAdjacentHTML("beforeend", `
+    <div class="upload-control" title="Upload file">
+      <label for="file">
+        <svg viewBox="0 0 384.97 384.97" width="14" height="14"><path d="M372.939,264.641c-6.641,0-12.03,5.39-12.03,12.03v84.212H24.061v-84.212c0-6.641-5.39-12.03-12.03-12.03 S0,270.031,0,276.671v96.242c0,6.641,5.39,12.03,12.03,12.03h360.909c6.641,0,12.03-5.39,12.03-12.03v-96.242 C384.97,270.019,379.58,264.641,372.939,264.641z"></path><path d="M117.067,103.507l63.46-62.558v235.71c0,6.641,5.438,12.03,12.151,12.03c6.713,0,12.151-5.39,12.151-12.03V40.95 l63.46,62.558c4.74,4.704,12.439,4.704,17.179,0c4.74-4.704,4.752-12.319,0-17.011l-84.2-82.997 c-4.692-4.656-12.584-4.608-17.191,0L99.888,86.496c-4.752,4.704-4.74,12.319,0,17.011 C104.628,108.211,112.327,108.211,117.067,103.507z"></path></svg>
+      </label>
+      <input type="file" id="file" name="file" multiple>
+    </div>
+`);
+    }
+
     function getSvg(path_type) {
       switch (path_type) {
         case "Dir":
     document.addEventListener('DOMContentLoaded', () => {
       addBreadcrumb(breadcrumb);
       paths.forEach(file => addFile(file));
-      $fileInput.addEventListener("change", e => {
-        const files = e.target.files;
-        for (const file of files) {
-          uploaderIdx += 1;
-          const uploader = new Uploader(uploaderIdx, file);
-          uploader.upload();
-        }
-      });
+      if (readonly) {
+        addUploadControl();
+        document.getElementById("file").addEventListener("change", e => {
+          const files = e.target.files;
+          for (const file of files) {
+            uploaderIdx += 1;
+            const uploader = new Uploader(uploaderIdx, file);
+            uploader.upload();
+          }
+        });
+      }
     });
   </script>
 </body>
index 843320333d735cfa4e81da5a95ca2780dc71ba18..663a77e3cbf90728a9c7e0057d7b81abd5bd1152 100644 (file)
@@ -1,6 +1,7 @@
 use crate::{Args, BoxResult};
 
 use futures::TryStreamExt;
+use hyper::header::HeaderValue;
 use hyper::service::{make_service_fn, service_fn};
 use hyper::{Body, Method, StatusCode};
 use percent_encoding::percent_decode;
@@ -62,6 +63,9 @@ impl InnerService {
         let res = if req.method() == Method::GET {
             self.handle_static(req).await
         } else if req.method() == Method::PUT {
+            if self.args.readonly {
+                return Ok(status_code!(StatusCode::FORBIDDEN));
+            }
             self.handle_upload(req).await
         } else {
             return Ok(status_code!(StatusCode::NOT_FOUND));
@@ -70,6 +74,11 @@ impl InnerService {
     }
 
     async fn handle_static(&self, req: Request) -> BoxResult<Response> {
+        if !self.auth_guard(&req).unwrap_or_default() {
+            let mut res = status_code!(StatusCode::UNAUTHORIZED);
+            res.headers_mut().insert("WWW-Authenticate" , HeaderValue::from_static("Basic"));
+            return Ok(res)
+        }
         let path = match self.get_file_path(req.uri().path())? {
             Some(path) => path,
             None => return Ok(status_code!(StatusCode::FORBIDDEN)),
@@ -87,6 +96,11 @@ impl InnerService {
     }
 
     async fn handle_upload(&self, mut req: Request) -> BoxResult<Response> {
+        if !self.auth_guard(&req).unwrap_or_default() {
+            let mut res = status_code!(StatusCode::UNAUTHORIZED);
+            res.headers_mut().insert("WWW-Authenticate" , HeaderValue::from_static("Basic"));
+            return Ok(res)
+        }
         let path = match self.get_file_path(req.uri().path())? {
             Some(path) => path,
             None => return Ok(status_code!(StatusCode::FORBIDDEN)),
@@ -165,7 +179,7 @@ impl InnerService {
 
         paths.sort_unstable();
         let breadcrumb = self.get_breadcrumb(path);
-        let data = SendDirData { breadcrumb, paths };
+        let data = SendDirData { breadcrumb, paths, readonly: !self.args.readonly };
         let data = serde_json::to_string(&data).unwrap();
 
         let mut output =
@@ -182,6 +196,25 @@ impl InnerService {
         Ok(Response::new(body))
     }
 
+    fn auth_guard(&self, req: &Request) -> BoxResult<bool> {
+        if let Some(auth) = &self.args.auth {
+            if let Some(value) = req.headers().get("Authorization") {
+                let value = value.to_str()?;
+                let value = if value.contains("Basic ") {
+                    &value[6..]
+                } else {
+                    return Ok(false);
+                };
+                let value = base64::decode(value)?;
+                let value = std::str::from_utf8(&value)?;
+                return Ok(value == auth);
+            } else {
+                return Ok(false);
+            }
+        }
+        Ok(true)
+    }
+
     fn get_breadcrumb(&self, path: &Path) -> String {
         let path = match self.args.path.parent() {
             Some(p) => path.strip_prefix(p).unwrap(),
@@ -210,6 +243,7 @@ impl InnerService {
 struct SendDirData {
     breadcrumb: String,
     paths: Vec<PathItem>,
+    readonly: bool,
 }
 
 #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]