Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor errors #1331

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased] - ReleaseDate
- Add `-C/--compress-response` to enable response compression [1315](https://github.com/svenstaro/miniserve/pull/1315) (thanks @zuisong)
- Refactor errors [#1331](https://github.com/svenstaro/miniserve/pull/1331) (thanks @cyqsimon)

## [0.26.0] - 2024-01-13
- Properly handle read-only errors on Windows [#1310](https://github.com/svenstaro/miniserve/pull/1310) (thanks @ViRb3)
Expand Down
71 changes: 34 additions & 37 deletions src/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use strum::{Display, EnumIter, EnumString};
use tar::Builder;
use zip::{write, ZipWriter};

use crate::errors::ContextualError;
use crate::errors::RuntimeError;

/// Available archive methods
#[derive(Deserialize, Clone, Copy, EnumIter, EnumString, Display)]
Expand Down Expand Up @@ -62,7 +62,7 @@ impl ArchiveMethod {
dir: T,
skip_symlinks: bool,
out: W,
) -> Result<(), ContextualError>
) -> Result<(), RuntimeError>
where
T: AsRef<Path>,
W: std::io::Write,
Expand All @@ -77,17 +77,17 @@ impl ArchiveMethod {
}

/// Write a gzipped tarball of `dir` in `out`.
fn tar_gz<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError>
fn tar_gz<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>
where
W: std::io::Write,
{
let mut out = Encoder::new(out).map_err(|e| ContextualError::IoError("GZIP".to_string(), e))?;
let mut out = Encoder::new(out).map_err(|e| RuntimeError::IoError("GZIP".to_string(), e))?;

tar_dir(dir, skip_symlinks, &mut out)?;

out.finish()
.into_result()
.map_err(|e| ContextualError::IoError("GZIP finish".to_string(), e))?;
.map_err(|e| RuntimeError::IoError("GZIP finish".to_string(), e))?;

Ok(())
}
Expand Down Expand Up @@ -115,22 +115,22 @@ where
/// ├── f
/// └── g
/// ```
fn tar_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError>
fn tar_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>
where
W: std::io::Write,
{
let inner_folder = dir.file_name().ok_or_else(|| {
ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string())
RuntimeError::InvalidPathError("Directory name terminates in \"..\"".to_string())
})?;

let directory = inner_folder.to_str().ok_or_else(|| {
ContextualError::InvalidPathError(
RuntimeError::InvalidPathError(
"Directory name contains invalid UTF-8 characters".to_string(),
)
})?;

tar(dir, directory.to_string(), skip_symlinks, out)
.map_err(|e| ContextualError::ArchiveCreationError("tarball".to_string(), Box::new(e)))
.map_err(|e| RuntimeError::ArchiveCreationError("tarball".to_string(), Box::new(e)))
}

/// Writes a tarball of `dir` in `out`.
Expand All @@ -141,7 +141,7 @@ fn tar<W>(
inner_folder: String,
skip_symlinks: bool,
out: W,
) -> Result<(), ContextualError>
) -> Result<(), RuntimeError>
where
W: std::io::Write,
{
Expand All @@ -153,7 +153,7 @@ where
tar_builder
.append_dir_all(inner_folder, src_dir)
.map_err(|e| {
ContextualError::IoError(
RuntimeError::IoError(
format!(
"Failed to append the content of {} to the TAR archive",
src_dir.to_str().unwrap_or("file")
Expand All @@ -164,7 +164,7 @@ where

// Finish the archive
tar_builder.into_inner().map_err(|e| {
ContextualError::IoError("Failed to finish writing the TAR archive".to_string(), e)
RuntimeError::IoError("Failed to finish writing the TAR archive".to_string(), e)
})?;

Ok(())
Expand Down Expand Up @@ -197,28 +197,28 @@ fn create_zip_from_directory<W>(
out: W,
directory: &Path,
skip_symlinks: bool,
) -> Result<(), ContextualError>
) -> Result<(), RuntimeError>
where
W: std::io::Write + std::io::Seek,
{
let options = write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
let mut paths_queue: Vec<PathBuf> = vec![directory.to_path_buf()];
let zip_root_folder_name = directory.file_name().ok_or_else(|| {
ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string())
RuntimeError::InvalidPathError("Directory name terminates in \"..\"".to_string())
})?;

let mut zip_writer = ZipWriter::new(out);
let mut buffer = Vec::new();
while !paths_queue.is_empty() {
let next = paths_queue.pop().ok_or_else(|| {
ContextualError::ArchiveCreationDetailError("Could not get path from queue".to_string())
RuntimeError::ArchiveCreationDetailError("Could not get path from queue".to_string())
})?;
let current_dir = next.as_path();
let directory_entry_iterator = std::fs::read_dir(current_dir)
.map_err(|e| ContextualError::IoError("Could not read directory".to_string(), e))?;
.map_err(|e| RuntimeError::IoError("Could not read directory".to_string(), e))?;
let zip_directory = Path::new(zip_root_folder_name).join(
current_dir.strip_prefix(directory).map_err(|_| {
ContextualError::ArchiveCreationDetailError(
RuntimeError::ArchiveCreationDetailError(
"Could not append base directory".to_string(),
)
})?,
Expand All @@ -228,37 +228,36 @@ where
let entry_path = entry
.ok()
.ok_or_else(|| {
ContextualError::InvalidPathError(
RuntimeError::InvalidPathError(
"Directory name terminates in \"..\"".to_string(),
)
})?
.path();
let entry_metadata = std::fs::metadata(entry_path.clone()).map_err(|e| {
ContextualError::IoError("Could not get file metadata".to_string(), e)
})?;
let entry_metadata = std::fs::metadata(entry_path.clone())
.map_err(|e| RuntimeError::IoError("Could not get file metadata".to_string(), e))?;

if entry_metadata.file_type().is_symlink() && skip_symlinks {
continue;
}
let current_entry_name = entry_path.file_name().ok_or_else(|| {
ContextualError::InvalidPathError("Invalid file or directory name".to_string())
RuntimeError::InvalidPathError("Invalid file or directory name".to_string())
})?;
if entry_metadata.is_file() {
let mut f = File::open(&entry_path)
.map_err(|e| ContextualError::IoError("Could not open file".to_string(), e))?;
.map_err(|e| RuntimeError::IoError("Could not open file".to_string(), e))?;
f.read_to_end(&mut buffer).map_err(|e| {
ContextualError::IoError("Could not read from file".to_string(), e)
RuntimeError::IoError("Could not read from file".to_string(), e)
})?;
let relative_path = zip_directory.join(current_entry_name).into_os_string();
zip_writer
.start_file(relative_path.to_string_lossy(), options)
.map_err(|_| {
ContextualError::ArchiveCreationDetailError(
RuntimeError::ArchiveCreationDetailError(
"Could not add file path to ZIP".to_string(),
)
})?;
zip_writer.write(buffer.as_ref()).map_err(|_| {
ContextualError::ArchiveCreationDetailError(
RuntimeError::ArchiveCreationDetailError(
"Could not write file to ZIP".to_string(),
)
})?;
Expand All @@ -268,7 +267,7 @@ where
zip_writer
.add_directory(relative_path.to_string_lossy(), options)
.map_err(|_| {
ContextualError::ArchiveCreationDetailError(
RuntimeError::ArchiveCreationDetailError(
"Could not add directory path to ZIP".to_string(),
)
})?;
Expand All @@ -278,49 +277,47 @@ where
}

zip_writer.finish().map_err(|_| {
ContextualError::ArchiveCreationDetailError(
"Could not finish writing ZIP archive".to_string(),
)
RuntimeError::ArchiveCreationDetailError("Could not finish writing ZIP archive".to_string())
})?;
Ok(())
}

/// Writes a zip of `dir` in `out`.
///
/// The content of `src_dir` will be saved in the archive as the folder named .
fn zip_data<W>(src_dir: &Path, skip_symlinks: bool, mut out: W) -> Result<(), ContextualError>
fn zip_data<W>(src_dir: &Path, skip_symlinks: bool, mut out: W) -> Result<(), RuntimeError>
where
W: std::io::Write,
{
let mut data = Vec::new();
let memory_file = Cursor::new(&mut data);
create_zip_from_directory(memory_file, src_dir, skip_symlinks).map_err(|e| {
ContextualError::ArchiveCreationError(
RuntimeError::ArchiveCreationError(
"Failed to create the ZIP archive".to_string(),
Box::new(e),
)
})?;

out.write_all(data.as_mut_slice())
.map_err(|e| ContextualError::IoError("Failed to write the ZIP archive".to_string(), e))?;
.map_err(|e| RuntimeError::IoError("Failed to write the ZIP archive".to_string(), e))?;

Ok(())
}

fn zip_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError>
fn zip_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>
where
W: std::io::Write,
{
let inner_folder = dir.file_name().ok_or_else(|| {
ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string())
RuntimeError::InvalidPathError("Directory name terminates in \"..\"".to_string())
})?;

inner_folder.to_str().ok_or_else(|| {
ContextualError::InvalidPathError(
RuntimeError::InvalidPathError(
"Directory name contains invalid UTF-8 characters".to_string(),
)
})?;

zip_data(dir, skip_symlinks, out)
.map_err(|e| ContextualError::ArchiveCreationError("zip".to_string(), Box::new(e)))
.map_err(|e| RuntimeError::ArchiveCreationError("zip".to_string(), Box::new(e)))
}
32 changes: 26 additions & 6 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use clap::{Parser, ValueEnum, ValueHint};
use http::header::{HeaderMap, HeaderName, HeaderValue};

use crate::auth;
use crate::errors::ContextualError;
use crate::listing::{SortingMethod, SortingOrder};
use crate::renderer::ThemeSlug;

Expand Down Expand Up @@ -301,10 +300,31 @@ fn parse_interface(src: &str) -> Result<IpAddr, std::net::AddrParseError> {
src.parse::<IpAddr>()
}

#[derive(Clone, Debug, thiserror::Error)]
pub enum AuthParseError {
/// Might occur if the HTTP credential string does not respect the expected format
#[error("Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash")]
InvalidAuthFormat,

/// Might occur if the hash method is neither sha256 nor sha512
#[error("{0} is not a valid hashing method. Expected sha256 or sha512")]
InvalidHashMethod(String),

/// Might occur if the HTTP auth hash password is not a valid hex code
#[error("Invalid format for password hash. Expected hex code")]
InvalidPasswordHash,

/// Might occur if the HTTP auth password exceeds 255 characters
#[error("HTTP password length exceeds 255 characters")]
PasswordTooLong,
}

/// Parse authentication requirement
pub fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
pub fn parse_auth(src: &str) -> Result<auth::RequiredAuth, AuthParseError> {
use AuthParseError as E;

let mut split = src.splitn(3, ':');
let invalid_auth_format = Err(ContextualError::InvalidAuthFormat);
let invalid_auth_format = Err(E::InvalidAuthFormat);

let username = match split.next() {
Some(username) => username,
Expand All @@ -319,19 +339,19 @@ pub fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
};

let password = if let Some(hash_hex) = split.next() {
let hash_bin = hex::decode(hash_hex).map_err(|_| ContextualError::InvalidPasswordHash)?;
let hash_bin = hex::decode(hash_hex).map_err(|_| E::InvalidPasswordHash)?;

match second_part {
"sha256" => auth::RequiredAuthPassword::Sha256(hash_bin),
"sha512" => auth::RequiredAuthPassword::Sha512(hash_bin),
_ => return Err(ContextualError::InvalidHashMethod(second_part.to_owned())),
_ => return Err(E::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(ContextualError::PasswordTooLongError);
return Err(E::PasswordTooLong);
}

auth::RequiredAuthPassword::Plain(second_part.to_owned())
Expand Down
4 changes: 2 additions & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use actix_web::{dev::ServiceRequest, HttpMessage};
use actix_web_httpauth::extractors::basic::BasicAuth;
use sha2::{Digest, Sha256, Sha512};

use crate::errors::ContextualError;
use crate::errors::RuntimeError;

#[derive(Clone, Debug)]
/// HTTP Basic authentication parameters
Expand Down Expand Up @@ -86,7 +86,7 @@ pub async fn handle_auth(
if match_auth(&cred.into(), required_auth) {
Ok(req)
} else {
Err((ContextualError::InvalidHttpCredentials.into(), req))
Err((RuntimeError::InvalidHttpCredentials.into(), req))
}
}

Expand Down
Loading
Loading