parent
9c2e9d1503
commit
c3ac2a21c9
6 changed files with 56 additions and 11 deletions
|
@ -120,6 +120,7 @@ pub struct Args {
|
||||||
pub addrs: Vec<IpAddr>,
|
pub addrs: Vec<IpAddr>,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub path_is_file: bool,
|
||||||
pub path_prefix: String,
|
pub path_prefix: String,
|
||||||
pub uri_prefix: String,
|
pub uri_prefix: String,
|
||||||
pub auth: AccessControl,
|
pub auth: AccessControl,
|
||||||
|
@ -146,6 +147,7 @@ impl Args {
|
||||||
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
|
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
|
||||||
let addrs: Vec<IpAddr> = Args::parse_addrs(&addrs)?;
|
let addrs: Vec<IpAddr> = Args::parse_addrs(&addrs)?;
|
||||||
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
|
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
|
||||||
|
let path_is_file = path.metadata()?.is_file();
|
||||||
let path_prefix = matches
|
let path_prefix = matches
|
||||||
.value_of("path-prefix")
|
.value_of("path-prefix")
|
||||||
.map(|v| v.trim_matches('/').to_owned())
|
.map(|v| v.trim_matches('/').to_owned())
|
||||||
|
@ -180,6 +182,7 @@ impl Args {
|
||||||
addrs,
|
addrs,
|
||||||
port,
|
port,
|
||||||
path,
|
path,
|
||||||
|
path_is_file,
|
||||||
path_prefix,
|
path_prefix,
|
||||||
uri_prefix,
|
uri_prefix,
|
||||||
auth,
|
auth,
|
||||||
|
|
|
@ -90,19 +90,26 @@ impl Server {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
let authorization = headers.get(AUTHORIZATION);
|
|
||||||
let guard_type = self.args.auth.guard(req_path, &method, authorization);
|
|
||||||
|
|
||||||
if req_path == "/favicon.ico" && method == Method::GET {
|
if req_path == "/favicon.ico" && method == Method::GET {
|
||||||
self.handle_send_favicon(headers, &mut res).await?;
|
self.handle_send_favicon(headers, &mut res).await?;
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let authorization = headers.get(AUTHORIZATION);
|
||||||
|
let guard_type = self.args.auth.guard(req_path, &method, authorization);
|
||||||
if guard_type.is_reject() {
|
if guard_type.is_reject() {
|
||||||
self.auth_reject(&mut res);
|
self.auth_reject(&mut res);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let head_only = method == Method::HEAD;
|
||||||
|
|
||||||
|
if self.args.path_is_file {
|
||||||
|
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
|
||||||
|
.await?;
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
let path = match self.extract_path(req_path) {
|
let path = match self.extract_path(req_path) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
|
@ -133,7 +140,6 @@ impl Server {
|
||||||
|
|
||||||
match method {
|
match method {
|
||||||
Method::GET | Method::HEAD => {
|
Method::GET | Method::HEAD => {
|
||||||
let head_only = method == Method::HEAD;
|
|
||||||
if is_dir {
|
if is_dir {
|
||||||
if render_try_index && query == "zip" {
|
if render_try_index && query == "zip" {
|
||||||
self.handle_zip_dir(path, head_only, &mut res).await?;
|
self.handle_zip_dir(path, head_only, &mut res).await?;
|
||||||
|
@ -340,10 +346,7 @@ impl Server {
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> BoxResult<()> {
|
) -> BoxResult<()> {
|
||||||
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
|
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
|
||||||
let filename = path
|
let filename = get_file_name(path)?;
|
||||||
.file_name()
|
|
||||||
.and_then(|v| v.to_str())
|
|
||||||
.ok_or_else(|| format!("Failed to get name of `{}`", path.display()))?;
|
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
CONTENT_DISPOSITION,
|
CONTENT_DISPOSITION,
|
||||||
HeaderValue::from_str(&format!(
|
HeaderValue::from_str(&format!(
|
||||||
|
@ -482,6 +485,13 @@ impl Server {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filename = get_file_name(path)?;
|
||||||
|
res.headers_mut().insert(
|
||||||
|
CONTENT_DISPOSITION,
|
||||||
|
HeaderValue::from_str(&format!("inline; filename=\"{}\"", encode_uri(filename),))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
res.headers_mut().typed_insert(AcceptRanges::bytes());
|
res.headers_mut().typed_insert(AcceptRanges::bytes());
|
||||||
|
|
||||||
let size = meta.len();
|
let size = meta.len();
|
||||||
|
@ -1022,6 +1032,12 @@ fn status_no_content(res: &mut Response) {
|
||||||
*res.status_mut() = StatusCode::NO_CONTENT;
|
*res.status_mut() = StatusCode::NO_CONTENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_file_name(path: &Path) -> BoxResult<&str> {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|v| v.to_str())
|
||||||
|
.ok_or_else(|| format!("Failed to get file name of `{}`", path.display()).into())
|
||||||
|
}
|
||||||
|
|
||||||
fn set_webdav_headers(res: &mut Response) {
|
fn set_webdav_headers(res: &mut Response) {
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
"Allow",
|
"Allow",
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
mod fixtures;
|
mod fixtures;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use fixtures::{server, Error, TestServer};
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_fs::fixture::TempDir;
|
||||||
|
use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
|
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
|
||||||
|
@ -28,3 +31,23 @@ fn path_prefix_propfind(
|
||||||
assert!(text.contains("<D:href>/xyz/</D:href>"));
|
assert!(text.contains("<D:href>/xyz/</D:href>"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("index.html")]
|
||||||
|
fn serve_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
|
||||||
|
let mut child = Command::cargo_bin("duf")?
|
||||||
|
.env("RUST_LOG", "false")
|
||||||
|
.arg(tmpdir.path().join(file))
|
||||||
|
.arg("-p")
|
||||||
|
.arg(port.to_string())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
wait_for_port(port);
|
||||||
|
|
||||||
|
let resp = reqwest::blocking::get(format!("http://localhost:{}/index.html", port))?;
|
||||||
|
assert_eq!(resp.text()?, "This is index.html");
|
||||||
|
|
||||||
|
child.kill()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
mod fixtures;
|
mod fixtures;
|
||||||
|
|
||||||
use fixtures::{port, server, tmpdir, Error, TestServer};
|
use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer};
|
||||||
|
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
use assert_fs::fixture::TempDir;
|
use assert_fs::fixture::TempDir;
|
||||||
|
@ -59,6 +59,8 @@ fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> R
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
|
wait_for_port(port);
|
||||||
|
|
||||||
// WARN assumes urls list is terminated by an empty line
|
// WARN assumes urls list is terminated by an empty line
|
||||||
let url_lines = BufReader::new(child.stdout.take().unwrap())
|
let url_lines = BufReader::new(child.stdout.take().unwrap())
|
||||||
.lines()
|
.lines()
|
||||||
|
|
|
@ -142,7 +142,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait a max of 1s for the port to become available.
|
/// Wait a max of 1s for the port to become available.
|
||||||
fn wait_for_port(port: u16) {
|
pub fn wait_for_port(port: u16) {
|
||||||
let start_wait = Instant::now();
|
let start_wait = Instant::now();
|
||||||
|
|
||||||
while !port_check::is_port_reachable(format!("localhost:{}", port)) {
|
while !port_check::is_port_reachable(format!("localhost:{}", port)) {
|
||||||
|
|
|
@ -105,6 +105,7 @@ fn head_file(server: TestServer) -> Result<(), Error> {
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
|
assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
|
||||||
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
|
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
|
||||||
|
assert!(resp.headers().contains_key("content-disposition"));
|
||||||
assert!(resp.headers().contains_key("etag"));
|
assert!(resp.headers().contains_key("etag"));
|
||||||
assert!(resp.headers().contains_key("last-modified"));
|
assert!(resp.headers().contains_key("last-modified"));
|
||||||
assert!(resp.headers().contains_key("content-length"));
|
assert!(resp.headers().contains_key("content-length"));
|
||||||
|
|
Loading…
Reference in a new issue