diff options
author | khai96_ <hvksmr1996@gmail.com> | 2019-04-26 06:04:16 +0000 |
---|---|---|
committer | khai96_ <hvksmr1996@gmail.com> | 2019-04-26 06:04:16 +0000 |
commit | 838d86655fb39b5cdf63b2d3823ce047e63afaf4 (patch) | |
tree | cf5306f1050108f872d6d7d020606f5dc5c90206 /src | |
parent | Use rstest_parametrize for unit tests (diff) | |
parent | Use rstest test fixtures to cut down on code duplication in integration tests (diff) | |
download | miniserve-838d86655fb39b5cdf63b2d3823ce047e63afaf4.tar.gz miniserve-838d86655fb39b5cdf63b2d3823ce047e63afaf4.zip |
Merge remote-tracking branch 'mainrepo/master' into pullrequest.hashed-password
Diffstat (limited to 'src')
-rw-r--r-- | src/archive.rs | 135 | ||||
-rw-r--r-- | src/args.rs | 30 | ||||
-rw-r--r-- | src/auth.rs | 33 | ||||
-rw-r--r-- | src/errors.rs | 154 | ||||
-rw-r--r-- | src/file_upload.rs | 62 | ||||
-rw-r--r-- | src/listing.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 87 | ||||
-rw-r--r-- | src/renderer.rs | 9 |
8 files changed, 310 insertions, 204 deletions
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" } |