aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkhai96_ <hvksmr1996@gmail.com>2019-04-26 06:04:16 +0000
committerkhai96_ <hvksmr1996@gmail.com>2019-04-26 06:04:16 +0000
commit838d86655fb39b5cdf63b2d3823ce047e63afaf4 (patch)
treecf5306f1050108f872d6d7d020606f5dc5c90206
parentUse rstest_parametrize for unit tests (diff)
parentUse rstest test fixtures to cut down on code duplication in integration tests (diff)
downloadminiserve-838d86655fb39b5cdf63b2d3823ce047e63afaf4.tar.gz
miniserve-838d86655fb39b5cdf63b2d3823ce047e63afaf4.zip
Merge remote-tracking branch 'mainrepo/master' into pullrequest.hashed-password
-rw-r--r--Cargo.lock41
-rw-r--r--Cargo.toml2
-rw-r--r--src/archive.rs135
-rw-r--r--src/args.rs30
-rw-r--r--src/auth.rs33
-rw-r--r--src/errors.rs154
-rw-r--r--src/file_upload.rs62
-rw-r--r--src/listing.rs4
-rw-r--r--src/main.rs87
-rw-r--r--src/renderer.rs9
-rw-r--r--tests/cli.rs44
11 files changed, 348 insertions, 253 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5068d5e..48ac09c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -120,7 +120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -521,7 +521,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -668,7 +668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
"synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1154,7 +1154,7 @@ dependencies = [
"port_check 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
- "rstest 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rstest 0.2.2 (git+https://github.com/la10736/rstest.git)",
"select 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1755,11 +1755,13 @@ dependencies = [
[[package]]
name = "rstest"
version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+source = "git+https://github.com/la10736/rstest.git#949e34ac50dbde94bbcac3a2c0db51bce3ba5510"
dependencies = [
+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1875,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2023,7 +2025,7 @@ dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2039,7 +2041,7 @@ dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2054,17 +2056,7 @@ dependencies = [
[[package]]
name = "syn"
-version = "0.14.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "syn"
-version = "0.15.32"
+version = "0.15.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2087,7 +2079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2577,7 +2569,7 @@ dependencies = [
"nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2900,7 +2892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum reqwest 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "943b9f85622f53bcf71721e0996f23688e3942e51fc33766c2e24a959316767b"
"checksum resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b263b4aa1b5de9ffc0054a2386f96992058bb6870aab516f8cdeb8a667d56dcb"
"checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a"
-"checksum rstest 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "17060b44b74f0aed4e7ee6c970e57b5e51adbd3aecd814e1ab38a27e00901d67"
+"checksum rstest 0.2.2 (git+https://github.com/la10736/rstest.git)" = "<none>"
"checksum rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc78bfd5acd7bf3e89cffcf899e5cb1a52d6fafa8dec2739ad70c9577a57288"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
@@ -2938,8 +2930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1c33039533f051704951680f1adfd468fd37ac46816ded0d9ee068e60f05f"
"checksum strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "47cd23f5c7dee395a00fa20135e2ec0fffcdfa151c56182966d7a3261343432e"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
-"checksum syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)" = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741"
-"checksum syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)" = "846620ec526c1599c070eff393bfeeeb88a93afa2513fc3b49f1fea84cf7b0ed"
+"checksum syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)" = "ec52cd796e5f01d0067225a5392e70084acc4c0013fa71d55166d38a8b307836"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
"checksum tar 0.4.23 (registry+https://github.com/rust-lang/crates.io-index)" = "8acf894d8bd30d060f3a8e457463f341ccabe475329e0670896de56e29e11b49"
diff --git a/Cargo.toml b/Cargo.toml
index 9629c35..d7b621f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,5 +54,5 @@ reqwest = "0.9"
assert_fs = "0.11"
select = "0.4"
port_check = "0.1"
-rstest = "0.2"
+rstest = { git = "https://github.com/la10736/rstest.git" }
pretty_assertions = "0.6"
diff --git a/src/archive.rs b/src/archive.rs
index 4703c0d..b5788f5 100644
--- a/src/archive.rs
+++ b/src/archive.rs
@@ -1,6 +1,5 @@
use actix_web::http::ContentEncoding;
use bytes::Bytes;
-use failure::ResultExt;
use libflate::gzip::Encoder;
use serde::Deserialize;
use std::io;
@@ -8,7 +7,7 @@ use std::path::PathBuf;
use strum_macros::{Display, EnumIter, EnumString};
use tar::Builder;
-use crate::errors;
+use crate::errors::{ContextualError, ContextualErrorKind};
/// Available compression methods
#[derive(Deserialize, Clone, EnumIter, EnumString, Display)]
@@ -47,49 +46,50 @@ pub fn create_archive(
method: &CompressionMethod,
dir: &PathBuf,
skip_symlinks: bool,
-) -> Result<(String, Bytes), errors::CompressionError> {
+) -> Result<(String, Bytes), ContextualError> {
match method {
CompressionMethod::TarGz => tgz_compress(&dir, skip_symlinks),
}
}
/// Compresses a given folder in .tar.gz format, and returns the result as a stream of bytes
-fn tgz_compress(
- dir: &PathBuf,
- skip_symlinks: bool,
-) -> Result<(String, Bytes), errors::CompressionError> {
+fn tgz_compress(dir: &PathBuf, skip_symlinks: bool) -> Result<(String, Bytes), ContextualError> {
let src_dir = dir.display().to_string();
- let inner_folder = match dir.file_name() {
- Some(directory_name) => match directory_name.to_str() {
- Some(directory) => directory,
- None => {
- return Err(errors::CompressionError::new(
- errors::CompressionErrorKind::InvalidUTF8DirectoryName,
+ if let Some(inner_folder) = dir.file_name() {
+ if let Some(directory) = inner_folder.to_str() {
+ let dst_filename = format!("{}.tar", directory);
+ let dst_tgz_filename = format!("{}.gz", dst_filename);
+ let mut tgz_data = Bytes::new();
+
+ let tar_data = tar(src_dir, directory.to_string(), skip_symlinks).map_err(|e| {
+ ContextualError::new(ContextualErrorKind::ArchiveCreationError(
+ "tarball".to_string(),
+ Box::new(e),
))
- }
- },
- None => {
- return Err(errors::CompressionError::new(
- errors::CompressionErrorKind::InvalidDirectoryName,
- ))
- }
- };
- let dst_filename = format!("{}.tar", inner_folder);
- let dst_tgz_filename = format!("{}.gz", dst_filename);
-
- let tar_content = tar(src_dir, inner_folder.to_string(), skip_symlinks).context(
- errors::CompressionErrorKind::TarBuildingError {
- message: "an error occured while writing the TAR archive".to_string(),
- },
- )?;
- let gz_data = gzip(&tar_content).context(errors::CompressionErrorKind::GZipBuildingError {
- message: "an error occured while writing the GZIP archive".to_string(),
- })?;
+ })?;
- let mut data = Bytes::new();
- data.extend_from_slice(&gz_data);
+ let gz_data = gzip(&tar_data).map_err(|e| {
+ ContextualError::new(ContextualErrorKind::ArchiveCreationError(
+ "GZIP archive".to_string(),
+ Box::new(e),
+ ))
+ })?;
- Ok((dst_tgz_filename, data))
+ tgz_data.extend_from_slice(&gz_data);
+
+ Ok((dst_tgz_filename, tgz_data))
+ } else {
+ // https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str
+ Err(ContextualError::new(ContextualErrorKind::InvalidPathError(
+ "Directory name contains invalid UTF-8 characters".to_string(),
+ )))
+ }
+ } else {
+ // https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name
+ Err(ContextualError::new(ContextualErrorKind::InvalidPathError(
+ "Directory name terminates in \"..\"".to_string(),
+ )))
+ }
}
/// Creates a TAR archive of a folder, and returns it as a stream of bytes
@@ -97,44 +97,53 @@ fn tar(
src_dir: String,
inner_folder: String,
skip_symlinks: bool,
-) -> Result<Vec<u8>, errors::CompressionError> {
+) -> Result<Vec<u8>, ContextualError> {
let mut tar_builder = Builder::new(Vec::new());
tar_builder.follow_symlinks(!skip_symlinks);
// Recursively adds the content of src_dir into the archive stream
- tar_builder.append_dir_all(inner_folder, &src_dir).context(
- errors::CompressionErrorKind::TarBuildingError {
- message: format!(
- "failed to append the content of {} to the TAR archive",
- &src_dir
- ),
- },
- )?;
-
- let tar_content =
- tar_builder
- .into_inner()
- .context(errors::CompressionErrorKind::TarBuildingError {
- message: "failed to finish writing the TAR archive".to_string(),
- })?;
+ tar_builder
+ .append_dir_all(inner_folder, &src_dir)
+ .map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ format!(
+ "Failed to append the content of {} to the TAR archive",
+ &src_dir
+ ),
+ e,
+ ))
+ })?;
+
+ let tar_content = tar_builder.into_inner().map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to finish writing the TAR archive".to_string(),
+ e,
+ ))
+ })?;
Ok(tar_content)
}
/// Compresses a stream of bytes using the GZIP algorithm, and returns the resulting stream
-fn gzip(mut data: &[u8]) -> Result<Vec<u8>, errors::CompressionError> {
- let mut encoder =
- Encoder::new(Vec::new()).context(errors::CompressionErrorKind::GZipBuildingError {
- message: "failed to create GZIP encoder".to_string(),
- })?;
- io::copy(&mut data, &mut encoder).context(errors::CompressionErrorKind::GZipBuildingError {
- message: "failed to write GZIP data".to_string(),
+fn gzip(mut data: &[u8]) -> Result<Vec<u8>, ContextualError> {
+ let mut encoder = Encoder::new(Vec::new()).map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to create GZIP encoder".to_string(),
+ e,
+ ))
+ })?;
+ io::copy(&mut data, &mut encoder).map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to write GZIP data".to_string(),
+ e,
+ ))
+ })?;
+ let data = encoder.finish().into_result().map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to write GZIP trailer".to_string(),
+ e,
+ ))
})?;
- let data = encoder.finish().into_result().context(
- errors::CompressionErrorKind::GZipBuildingError {
- message: "failed to write GZIP trailer".to_string(),
- },
- )?;
Ok(data)
}
diff --git a/src/args.rs b/src/args.rs
index 496d697..8f15ea4 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use structopt::StructOpt;
use crate::auth;
+use crate::errors::{ContextualError, ContextualErrorKind};
use crate::themes;
/// Possible characters for random routes
@@ -76,38 +77,44 @@ fn parse_interface(src: &str) -> Result<IpAddr, std::net::AddrParseError> {
}
/// Checks wether the auth string is valid, i.e. it follows the syntax username:password
-fn parse_auth(src: &str) -> Result<auth::RequiredAuth, String> {
+fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
let mut split = src.splitn(3, ':');
- let errmsg = "Invalid credentials string, expected format is username:password".to_owned();
+ let invalid_auth_format = Err(
+ ContextualError::new(ContextualErrorKind::InvalidAuthFormat)
+ );
let username = match split.next() {
Some(username) => username,
- None => return Err(errmsg),
+ None => return invalid_auth_format,
};
let second_part = match split.next() {
// This allows empty passwords, as the spec does not forbid it
Some(password) => password,
- None => return Err(errmsg),
+ None => return invalid_auth_format,
};
let password = if let Some(hash_hex) = split.next() {
let hash_bin = match hex::decode(hash_hex) {
Ok(hash_bin) => hash_bin,
- _ => return Err("Hash string is not a valid hex code".to_owned()),
+ _ => return Err(ContextualError::new(ContextualErrorKind::InvalidPasswordHash)),
};
match second_part {
"sha256" => auth::RequiredAuthPassword::Sha256(hash_bin.to_owned()),
"sha512" => auth::RequiredAuthPassword::Sha512(hash_bin.to_owned()),
- _ => return Err("Invalid hash method, only accept either sha256 or sha512".to_owned()),
+ _ => {
+ return Err(ContextualError::new(
+ ContextualErrorKind::InvalidHashMethod(second_part.to_owned())
+ ))
+ },
}
} else {
// To make it Windows-compatible, the password needs to be shorter than 255 characters.
// After 255 characters, Windows will truncate the value.
// As for the username, the spec does not mention a limit in length
if second_part.len() > 255 {
- return Err("Password length cannot exceed 255 characters".to_owned());
+ return Err(ContextualError::new(ContextualErrorKind::PasswordTooLongError));
}
auth::RequiredAuthPassword::Plain(second_part.to_owned())
@@ -194,18 +201,19 @@ mod tests {
auth_string, err_msg,
case(
"foo",
- "Invalid credentials string, expected format is username:password"
+ "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash"
),
case(
"username:blahblah:abcd",
- "Invalid hash method, only accept either sha256 or sha512"
+ "blahblah is not a valid hashing method. Expected sha256 or sha512"
),
case(
"username:sha256:invalid",
- "Hash string is not a valid hex code"
+ "Invalid format for password hash. Expected hex code"
),
)]
fn parse_auth_invalid(auth_string: &str, err_msg: &str) {
- assert_eq!(parse_auth(auth_string).unwrap_err(), err_msg.to_owned(),);
+ let err = parse_auth(auth_string).unwrap_err();
+ assert_eq!(format!("{}", err), err_msg.to_owned());
}
}
diff --git a/src/auth.rs b/src/auth.rs
index 2db422d..432f6ce 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -3,12 +3,9 @@ use actix_web::middleware::{Middleware, Response};
use actix_web::{HttpRequest, HttpResponse, Result};
use sha2::{Digest, Sha256, Sha512};
-pub struct Auth;
+use crate::errors::{ContextualError, ContextualErrorKind};
-/// HTTP Basic authentication errors
-pub enum BasicAuthError {
- Base64DecodeError,
-}
+pub struct Auth;
#[derive(Clone, Debug)]
/// HTTP Basic authentication parameters
@@ -34,9 +31,17 @@ pub struct RequiredAuth {
/// Decode a HTTP basic auth string into a tuple of username and password.
pub fn parse_basic_auth(
authorization_header: &header::HeaderValue,
-) -> Result<BasicAuthParams, BasicAuthError> {
- let basic_removed = authorization_header.to_str().unwrap().replace("Basic ", "");
- let decoded = base64::decode(&basic_removed).map_err(|_| BasicAuthError::Base64DecodeError)?;
+) -> Result<BasicAuthParams, ContextualError> {
+ let basic_removed = authorization_header
+ .to_str()
+ .map_err(|e| {
+ ContextualError::new(ContextualErrorKind::ParseError(
+ "HTTP authentication header".to_string(),
+ e.to_string(),
+ ))
+ })?
+ .replace("Basic ", "");
+ let decoded = base64::decode(&basic_removed).map_err(ContextualErrorKind::Base64DecodeError)?;
let decoded_str = String::from_utf8_lossy(&decoded);
let credentials: Vec<&str> = decoded_str.splitn(2, ':').collect();
@@ -87,11 +92,13 @@ impl Middleware<crate::MiniserveConfig> for Auth {
if let Some(auth_headers) = req.headers().get(header::AUTHORIZATION) {
let auth_req = match parse_basic_auth(auth_headers) {
Ok(auth_req) => auth_req,
- Err(BasicAuthError::Base64DecodeError) => {
- return Ok(Response::Done(HttpResponse::BadRequest().body(format!(
- "Error decoding basic auth base64: '{}'",
- auth_headers.to_str().unwrap()
- ))));
+ Err(err) => {
+ let auth_err = ContextualError::new(
+ ContextualErrorKind::HTTPAuthenticationError(Box::new(err)),
+ );
+ return Ok(Response::Done(
+ HttpResponse::BadRequest().body(auth_err.to_string()),
+ ));
}
};
if !match_auth(auth_req, required_auth) {
diff --git a/src/errors.rs b/src/errors.rs
index 21d9e07..833e9c4 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,85 +1,92 @@
use failure::{Backtrace, Context, Fail};
use std::fmt::{self, Debug, Display};
-/// Kinds of errors which might happen during file upload
#[derive(Debug, Fail)]
-pub enum FileUploadErrorKind {
- /// This error will occur when file overriding is off and a file with same name already exists
- #[fail(display = "File with this name already exists")]
- FileExist,
- /// This error will occur when the server fails to process the HTTP header during file upload
- #[fail(display = "Failed to parse incoming request")]
- ParseError,
- /// This error will occur when we fail to process the multipart request
- #[fail(display = "Failed to process multipart request")]
+pub enum ContextualErrorKind {
+ /// Fully customized errors, not inheriting from any error
+ #[fail(display = "{}", _0)]
+ CustomError(String),
+
+ /// Any kind of IO errors
+ #[fail(display = "{}\ncaused by: {}", _0, _1)]
+ IOError(String, std::io::Error),
+
+ /// MultipartError, which might occur during file upload, when processing the multipart request fails
+ #[fail(display = "Failed to process multipart request\ncaused by: {}", _0)]
MultipartError(actix_web::error::MultipartError),
- /// This error may occur when trying to write the incoming file to disk
- #[fail(display = "Failed to create or write to file")]
- IOError(std::io::Error),
- /// This error will occur when we he have insuffictent permissions to create new file
- #[fail(display = "Insufficient permissions to create file")]
- InsufficientPermissions,
-}
-/// Kinds of errors which might happen during the generation of an archive
-#[derive(Debug, Fail)]
-pub enum CompressionErrorKind {
- /// This error will occur if the directory name could not be retrieved from the path
- /// See https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name
- #[fail(display = "Invalid path: directory name terminates in \"..\"")]
- InvalidDirectoryName,
- /// This error will occur when trying to convert an OSString into a String, if the path
- /// contains invalid UTF-8 characters
- /// See https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str
- #[fail(display = "Invalid path: directory name contains invalid UTF-8 characters")]
- InvalidUTF8DirectoryName,
- /// This error might occur while building a TAR archive, or while writing the termination sections
- /// See https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.append_dir_all
- /// and https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.into_inner
- #[fail(display = "Failed to create the TAR archive: {}", message)]
- TarBuildingError { message: String },
- /// This error might occur while building a GZIP archive, or while writing the GZIP trailer
- /// See https://docs.rs/libflate/0.1.21/libflate/gzip/struct.Encoder.html#method.finish
- #[fail(display = "Failed to create the GZIP archive: {}", message)]
- GZipBuildingError { message: String },
-}
+ /// This error might occur when decoding the HTTP authentication header.
+ #[fail(
+ display = "Failed to decode HTTP authentication header\ncaused by: {}",
+ _0
+ )]
+ Base64DecodeError(base64::DecodeError),
-/// Prints the full chain of error, up to the root cause.
-/// If RUST_BACKTRACE is set to 1, also prints the backtrace for each error
-pub fn print_error_chain(err: CompressionError) {
- log::error!("{}", &err);
- print_backtrace(&err);
- for cause in Fail::iter_causes(&err) {
- log::error!("caused by: {}", cause);
- print_backtrace(cause);
- }
+ /// Any error related to an invalid path (failed to retrieve entry name, unexpected entry type, etc)
+ #[fail(display = "Invalid path\ncaused by: {}", _0)]
+ InvalidPathError(String),
+
+ /// This error might occur if the HTTP credential string does not respect the expected format
+ #[fail(
+ display = "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash"
+ )]
+ InvalidAuthFormat,
+
+ /// This error might occure if the hash method is neither sha256 nor sha512
+ #[fail(display = "{} is not a valid hashing method. Expected sha256 or sha512", _0)]
+ InvalidHashMethod(String),
+
+ /// This error might occur if the HTTP auth hash password is not a valid hex code
+ #[fail(display = "Invalid format for password hash. Expected hex code")]
+ InvalidPasswordHash,
+
+ /// This error might occur if the HTTP auth password exceeds 255 characters
+ #[fail(display = "HTTP password length exceeds 255 characters")]
+ PasswordTooLongError,
+
+ /// This error might occur if the user has unsufficient permissions to create an entry in a given directory
+ #[fail(display = "Insufficient permissions to create file in {}", _0)]
+ InsufficientPermissionsError(String),
+
+ /// Any error related to parsing.
+ #[fail(display = "Failed to parse {}\ncaused by: {}", _0, _1)]
+ ParseError(String, String),
+
+ /// This error might occur when the creation of an archive fails
+ #[fail(
+ display = "An error occured while creating the {}\ncaused by: {}",
+ _0, _1
+ )]
+ ArchiveCreationError(String, Box<ContextualError>),
+
+ /// This error might occur when the HTTP authentication fails
+ #[fail(
+ display = "An error occured during HTTP authentication\ncaused by: {}",
+ _0
+ )]
+ HTTPAuthenticationError(Box<ContextualError>),
}
-/// Prints the backtrace of an error
-/// RUST_BACKTRACE needs to be set to 1 to display the backtrace
-fn print_backtrace(err: &dyn Fail) {
- if let Some(backtrace) = err.backtrace() {
- let backtrace = backtrace.to_string();
- if backtrace != "" {
- log::error!("{}", backtrace);
- }
+pub fn log_error_chain(description: String) {
+ for cause in description.lines() {
+ log::error!("{}", cause);
}
}
/// Based on https://boats.gitlab.io/failure/error-errorkind.html
-pub struct CompressionError {
- inner: Context<CompressionErrorKind>,
+pub struct ContextualError {
+ inner: Context<ContextualErrorKind>,
}
-impl CompressionError {
- pub fn new(kind: CompressionErrorKind) -> CompressionError {
- CompressionError {
+impl ContextualError {
+ pub fn new(kind: ContextualErrorKind) -> ContextualError {
+ ContextualError {
inner: Context::new(kind),
}
}
}
-impl Fail for CompressionError {
+impl Fail for ContextualError {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}
@@ -89,28 +96,35 @@ impl Fail for CompressionError {
}
}
-impl Display for CompressionError {
+impl Display for ContextualError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.inner, f)
}
}
-impl Debug for CompressionError {
+impl Debug for ContextualError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(&self.inner, f)
}
}
-impl From<Context<CompressionErrorKind>> for CompressionError {
- fn from(inner: Context<CompressionErrorKind>) -> CompressionError {
- CompressionError { inner }
+impl From<Context<ContextualErrorKind>> for ContextualError {
+ fn from(inner: Context<ContextualErrorKind>) -> ContextualError {
+ ContextualError { inner }
}
}
-impl From<CompressionErrorKind> for CompressionError {
- fn from(kind: CompressionErrorKind) -> CompressionError {
- CompressionError {
+impl From<ContextualErrorKind> for ContextualError {
+ fn from(kind: ContextualErrorKind) -> ContextualError {
+ ContextualError {
inner: Context::new(kind),
}
}
}
+
+/// This allows to create CustomErrors more simply
+impl From<String> for ContextualError {
+ fn from(msg: String) -> ContextualError {
+ ContextualError::new(ContextualErrorKind::CustomError(msg))
+ }
+}
diff --git a/src/file_upload.rs b/src/file_upload.rs
index 88f8a7c..1618617 100644
--- a/src/file_upload.rs
+++ b/src/file_upload.rs
@@ -1,5 +1,3 @@
-use crate::errors::FileUploadErrorKind;
-use crate::renderer::file_upload_error;
use actix_web::{
dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest,
HttpResponse, Query,
@@ -12,6 +10,9 @@ use std::{
path::{Component, PathBuf},
};
+use crate::errors::{self, ContextualErrorKind};
+use crate::renderer;
+
/// Query parameters
#[derive(Debug, Deserialize)]
struct QueryParameters {
@@ -23,24 +24,32 @@ fn save_file(
field: multipart::Field<dev::Payload>,
file_path: PathBuf,
overwrite_files: bool,
-) -> Box<Future<Item = i64, Error = FileUploadErrorKind>> {
+) -> Box<Future<Item = i64, Error = ContextualErrorKind>> {
if !overwrite_files && file_path.exists() {
- return Box::new(future::err(FileUploadErrorKind::FileExist));
+ return Box::new(future::err(ContextualErrorKind::CustomError(
+ "File already exists, and the overwrite_files option has not been set".to_string(),
+ )));
}
- let mut file = match std::fs::File::create(file_path) {
+
+ let mut file = match std::fs::File::create(&file_path) {
Ok(file) => file,
Err(e) => {
- return Box::new(future::err(FileUploadErrorKind::IOError(e)));
+ return Box::new(future::err(ContextualErrorKind::IOError(
+ format!("Failed to create file in {}", file_path.display()),
+ e,
+ )));
}
};
Box::new(
field
- .map_err(FileUploadErrorKind::MultipartError)
+ .map_err(ContextualErrorKind::MultipartError)
.fold(0i64, move |acc, bytes| {
let rt = file
.write_all(bytes.as_ref())
.map(|_| acc + bytes.len() as i64)
- .map_err(FileUploadErrorKind::IOError);
+ .map_err(|e| {
+ ContextualErrorKind::IOError("Failed to write to file".to_string(), e)
+ });
future::result(rt)
}),
)
@@ -51,44 +60,56 @@ fn handle_multipart(
item: multipart::MultipartItem<dev::Payload>,
mut file_path: PathBuf,
overwrite_files: bool,
-) -> Box<Stream<Item = i64, Error = FileUploadErrorKind>> {
+) -> Box<Stream<Item = i64, Error = ContextualErrorKind>> {
match item {
multipart::MultipartItem::Field(field) => {
let filename = field
.headers()
.get(header::CONTENT_DISPOSITION)
- .ok_or(FileUploadErrorKind::ParseError)
+ .ok_or(ContextualErrorKind::ParseError)
.and_then(|cd| {
header::ContentDisposition::from_raw(cd)
- .map_err(|_| FileUploadErrorKind::ParseError)
+ .map_err(|_| ContextualErrorKind::ParseError)
})
.and_then(|content_disposition| {
content_disposition
.get_filename()
- .ok_or(FileUploadErrorKind::ParseError)
+ .ok_or(ContextualErrorKind::ParseError)
.map(String::from)
});
- let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream());
+ let err = |e: ContextualErrorKind| Box::new(future::err(e).into_stream());
match filename {
Ok(f) => {
match fs::metadata(&file_path) {
Ok(metadata) => {
- if !metadata.is_dir() || metadata.permissions().readonly() {
- return err(FileUploadErrorKind::InsufficientPermissions);
+ if !metadata.is_dir() {
+ return err(ContextualErrorKind::InvalidPathError(format!(
+ "cannot upload file to {}, since it's not a directory",
+ &file_path.display()
+ )));
+ } else if metadata.permissions().readonly() {
+ return err(ContextualErrorKind::InsufficientPermissionsError(
+ file_path.display().to_string(),
+ ));
}
}
Err(_) => {
- return err(FileUploadErrorKind::InsufficientPermissions);
+ return err(ContextualErrorKind::InsufficientPermissionsError(
+ file_path.display().to_string(),
+ ));
}
}
file_path = file_path.join(f);
Box::new(save_file(field, file_path, overwrite_files).into_stream())
}
- Err(e) => err(e),
+ Err(e) => err(e(
+ "HTTP header".to_string(),
+ "Failed to retrieve the name of the file to upload".to_string(),
+ )),
}
}
multipart::MultipartItem::Nested(mp) => Box::new(
- mp.map_err(FileUploadErrorKind::MultipartError)
+ mp.map_err(ContextualErrorKind::MultipartError)
.map(move |item| handle_multipart(item, file_path.clone(), overwrite_files))
.flatten(),
),
@@ -135,7 +156,7 @@ pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse<
let overwrite_files = req.state().overwrite_files;
Box::new(
req.multipart()
- .map_err(FileUploadErrorKind::MultipartError)
+ .map_err(ContextualErrorKind::MultipartError)
.map(move |item| handle_multipart(item, target_dir.clone(), overwrite_files))
.flatten()
.collect()
@@ -155,9 +176,10 @@ fn create_error_response(
description: &str,
return_path: &str,
) -> FutureResult<HttpResponse, actix_web::error::Error> {
+ errors::log_error_chain(description.to_string());
future::ok(
HttpResponse::BadRequest()
.content_type("text/html; charset=utf-8")
- .body(file_upload_error(description, return_path).into_string()),
+ .body(renderer::render_error(description, return_path).into_string()),
)
}
diff --git a/src/listing.rs b/src/listing.rs
index 6aa1eac..a030feb 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -263,10 +263,10 @@ pub fn directory_listing<S>(
.body(Body::Streaming(Box::new(once(Ok(content))))))
}
Err(err) => {
- errors::print_error_chain(err);
+ errors::log_error_chain(err.to_string());
Ok(HttpResponse::Ok()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
- .body(""))
+ .body(renderer::render_error(&err.to_string(), serve_path).into_string()))
}
}
} else {
diff --git a/src/main.rs b/src/main.rs
index e63b505..bc8f3f0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,6 +19,8 @@ mod listing;
mod renderer;
mod themes;
+use crate::errors::{ContextualError, ContextualErrorKind};
+
#[derive(Clone)]
/// Configuration of the Miniserve application
pub struct MiniserveConfig {
@@ -57,10 +59,18 @@ pub struct MiniserveConfig {
}
fn main() {
+ match run() {
+ Ok(()) => (),
+ Err(e) => errors::log_error_chain(e.to_string()),
+ }
+}
+
+fn run() -> Result<(), ContextualError> {
if cfg!(windows) && !Paint::enable_windows_ascii() {
Paint::disable();
}
+ let sys = actix::System::new("miniserve");
let miniserve_config = args::parse_args();
let _ = if miniserve_config.verbose {
@@ -73,16 +83,20 @@ fn main() {
&& miniserve_config
.path
.symlink_metadata()
- .expect("Can't get file metadata")
+ .map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to retrieve symlink's metadata".to_string(),
+ e,
+ ))
+ })?
.file_type()
.is_symlink()
{
- log::error!("The no-symlinks option cannot be used with a symlink path");
- return;
+ return Err(ContextualError::from(
+ "The no-symlinks option cannot be used with a symlink path".to_string(),
+ ));
}
- let sys = actix::System::new("miniserve");
-
let inside_config = miniserve_config.clone();
let interfaces = miniserve_config
@@ -102,7 +116,12 @@ fn main() {
})
.collect::<Vec<String>>();
- let canon_path = miniserve_config.path.canonicalize().unwrap();
+ let canon_path = miniserve_config.path.canonicalize().map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to resolve path to be served".to_string(),
+ e,
+ ))
+ })?;
let path_string = canon_path.to_string_lossy();
println!(
@@ -116,10 +135,20 @@ fn main() {
" Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice."
);
print!("Starting server in ");
- io::stdout().flush().unwrap();
+ io::stdout().flush().map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to write data".to_string(),
+ e,
+ ))
+ })?;
for c in "3… 2… 1… \n".chars() {
print!("{}", c);
- io::stdout().flush().unwrap();
+ io::stdout().flush().map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to write data".to_string(),
+ e,
+ ))
+ })?;
thread::sleep(Duration::from_millis(500));
}
}
@@ -148,12 +177,6 @@ fn main() {
));
}
}
- println!(
- "Serving path {path} at {addresses}",
- path = Color::Yellow.paint(path_string).bold(),
- addresses = addresses,
- );
- println!("\nQuit by pressing CTRL-C");
let socket_addresses = interfaces
.iter()
@@ -167,10 +190,18 @@ fn main() {
})
.collect::<Result<Vec<SocketAddr>, _>>();
- // Note that this should never fail, since CLI parsing succeeded
- // This means the format of each IP address is valid, and so is the port
- // Valid IpAddr + valid port == valid SocketAddr
- let socket_addresses = socket_addresses.expect("Failed to parse string as socket address");
+ let socket_addresses = match socket_addresses {
+ Ok(addresses) => addresses,
+ Err(e) => {
+ // Note that this should never fail, since CLI parsing succeeded
+ // This means the format of each IP address is valid, and so is the port
+ // Valid IpAddr + valid port == valid SocketAddr
+ return Err(ContextualError::new(ContextualErrorKind::ParseError(
+ "string as socket address".to_string(),
+ e.to_string(),
+ )));
+ }
+ };
server::new(move || {
App::with_state(inside_config.clone())
@@ -179,10 +210,26 @@ fn main() {
.configure(configure_app)
})
.bind(socket_addresses.as_slice())
- .expect("Couldn't bind server")
+ .map_err(|e| {
+ ContextualError::new(ContextualErrorKind::IOError(
+ "Failed to bind server".to_string(),
+ e,
+ ))
+ })?
.shutdown_timeout(0)
.start();
+
+ println!(
+ "Serving path {path} at {addresses}",
+ path = Color::Yellow.paint(path_string).bold(),
+ addresses = addresses,
+ );
+
+ println!("\nQuit by pressing CTRL-C");
+
let _ = sys.run();
+
+ Ok(())
}
/// Configures the Actix application
@@ -205,7 +252,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> {
let u_r = upload_route.clone();
Some(
fs::StaticFiles::new(path)
- .expect("Couldn't create path")
+ .expect("Failed to setup static file handler")
.show_files_listing()
.files_listing_renderer(move |dir, req| {
listing::directory_listing(
diff --git a/src/renderer.rs b/src/renderer.rs
index c1dcdb2..b292e70 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -47,7 +47,7 @@ pub fn page(
form id="file_submit" action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" {
p { "Select a file to upload or drag it anywhere into the window" }
div {
- input#file-input type="file" name="file_to_upload" {}
+ input#file-input type="file" name="file_to_upload" required="" {}
button type="submit" { "Upload file" }
}
}
@@ -760,11 +760,10 @@ fn humanize_systemtime(src_time: Option<SystemTime>) -> Option<String> {
.map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past))
}
-/// Renders error page when file uploading fails
-pub fn file_upload_error(error_description: &str, return_address: &str) -> Markup {
+/// Renders an error on the webpage
+pub fn render_error(error_description: &str, return_address: &str) -> Markup {
html! {
- h1 { "File uploading failed" }
- p { (error_description) }
+ pre { (error_description) }
a href=(return_address) {
"Go back to file listing"
}
diff --git a/tests/cli.rs b/tests/cli.rs
index 6a41764..2390c3b 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -3,9 +3,9 @@ use assert_fs::fixture::TempDir;
use assert_fs::prelude::*;
use clap::{crate_name, crate_version};
use port_check::free_local_port;
-use pretty_assertions::assert_eq;
use reqwest;
use reqwest::multipart;
+use rstest::rstest;
use select::document::Document;
use select::predicate::{Attr, Text};
use std::process::{Command, Stdio};
@@ -17,17 +17,24 @@ type Error = Box<std::error::Error>;
static FILES: &[&str] = &["test.txt", "test.html", "test.mkv"];
/// Test fixture which creates a temporary directory with a few files inside.
-pub fn tmpdir() -> Result<TempDir, Error> {
- let tmpdir = assert_fs::TempDir::new()?;
+pub fn tmpdir() -> TempDir {
+ let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
for &file in FILES {
- tmpdir.child(file).write_str("Test Hello Yes")?;
+ tmpdir
+ .child(file)
+ .write_str("Test Hello Yes")
+ .expect("Couldn't write to file");
}
- Ok(tmpdir)
+ tmpdir
}
-#[test]
-fn serves_requests_with_no_options() -> Result<(), Error> {
- let tmpdir = tmpdir()?;
+/// Get a free port.
+pub fn port() -> u16 {
+ free_local_port().expect("Couldn't find a free local port")
+}
+
+#[rstest]
+fn serves_requests_with_no_options(tmpdir: TempDir) -> Result<(), Error> {
let mut child = Command::cargo_bin("miniserve")?
.arg(tmpdir.path())
.stdout(Stdio::null())
@@ -46,11 +53,8 @@ fn serves_requests_with_no_options() -> Result<(), Error> {
Ok(())
}
-#[test]
-fn serves_requests_with_non_default_port() -> Result<(), Error> {
- let tmpdir = tmpdir()?;
-
- let port = free_local_port().unwrap();
+#[rstest]
+fn serves_requests_with_non_default_port(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let mut child = Command::cargo_bin("miniserve")?
.arg(tmpdir.path())
.arg("-p")
@@ -71,11 +75,8 @@ fn serves_requests_with_non_default_port() -> Result<(), Error> {
Ok(())
}
-#[test]
-fn auth_works() -> Result<(), Error> {
- let tmpdir = tmpdir()?;
-
- let port = free_local_port().unwrap();
+#[rstest]
+fn auth_works(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let mut child = Command::cargo_bin("miniserve")?
.arg(tmpdir.path())
.arg("-p")
@@ -103,13 +104,10 @@ fn auth_works() -> Result<(), Error> {
Ok(())
}
-#[test]
-fn uploading_files_works() -> Result<(), Error> {
- let tmpdir = tmpdir()?;
-
+#[rstest]
+fn uploading_files_works(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let test_file_name = "uploaded test file.txt";
- let port = free_local_port().unwrap();
let mut child = Command::cargo_bin("miniserve")?
.arg(tmpdir.path())
.arg("-p")