Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,839 changes: 785 additions & 1,054 deletions Cargo.lock

Large diffs are not rendered by default.

48 changes: 21 additions & 27 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "rustic_server"
version = "0.4.4"
authors = ["the rustic-rs team"]
categories = ["command-line-utilities"]
edition = "2021"
edition = "2024"
homepage = "https://rustic.cli.rs/"
include = [
"src/**/*",
Expand All @@ -16,7 +16,7 @@ include = [
keywords = ["backup", "restic", "cli", "server"]
license = "AGPL-3.0-or-later"
repository = "https://github.com/rustic-rs/rustic_server"
rust-version = "1.74.0"
rust-version = "1.88.0"
description = """
rustic server - a REST server built in rust to use with rustic and restic.
"""
Expand All @@ -38,65 +38,59 @@ license = false
eula = false

[dependencies]
abscissa_tokio = "0.8.0"
abscissa_tokio = "0.9.0"
anyhow = "1"
async-trait = "0.1"
axum = { version = "0.7", features = ["tracing", "multipart", "http2", "macros"] }
axum-auth = "0.7"
axum-extra = { version = "0.9", features = ["typed-header", "query", "async-read-body", "typed-routing", "erased-json"] }
axum-macros = "0.4"
axum-range = "0.4"
axum-server = { version = "0.7", features = ["tls-rustls-no-provider"] }
chrono = { version = "0.4.38", features = ["serde"] }
axum = { version = "0.8", features = ["tracing", "multipart", "http2", "macros"] }
axum-auth = "0.8"
axum-extra = { version = "0.12", features = ["typed-header", "query", "async-read-body", "typed-routing", "erased-json"] }
axum-macros = "0.5"
axum-range = "1"
axum-server = { version = "0.8", features = ["tls-rustls-no-provider"] }
chrono = { version = "0.4.45", features = ["serde"] }
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
conflate = "0.3.3"
displaydoc = "0.2"
# enum_dispatch = "0.3.12"
futures = "0.3"
futures-util = "0.3"
htpasswd-verify = "0.3"
htauth = "0.1.2"
http-body-util = "0.1"
http-range = "0.1"
inquire = "0.7"
pin-project = "1"
rand = "0.8"
serde = { version = "1", default-features = false, features = ["derive"] }
serde_derive = "1"
strum = { version = "0.26", features = ["derive"] }
strum = { version = "0.28", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["io", "io-util"] }
toml = "0.8"
toml = "1.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.11.0", features = ["v4"] }
uuid = { version = "1.24.0", features = ["v4"] }
walkdir = "2"

[dependencies.abscissa_core]
version = "0.8.1"
version = "0.9.0"
# optional: use `gimli` to capture backtraces
# see https://github.com/rust-lang/backtrace-rs/issues/189
# features = ["gimli-backtrace"]

[dependencies.rustls]
version = "0.23.17"
version = "0.23.43"
features = ["logging", "std", "ring", "tls12"]
default-features = false

[dev-dependencies]
abscissa_core = { version = "0.8.1", features = ["testing"] }
abscissa_core = { version = "0.9.0", features = ["testing"] }
anyhow = "1"
assert_cmd = "2"
base64 = "0.22"
dircmp = "0.2"
base64 = "0.23"
insta = { version = "1", features = ["redactions", "toml"] }
once_cell = "1.20"
predicates = "3.1.2"
pretty_assertions = "1"
rstest = "0.23"
rstest = "0.26"
serde_json = "1"
# reqwest = "0.11.18"
serial_test = { version = "3.2.0", features = ["file_locks"] }
serial_test = { version = "4.0.1", features = ["file_locks"] }
tower = "0.5"

# see: https://nnethercote.github.io/perf-book/build-configuration.html
Expand Down Expand Up @@ -152,7 +146,7 @@ rust_2018_idioms = { level = "warn", priority = -1 }
trivial_casts = "warn"
unused_lifetimes = "warn"
unused_qualifications = "warn"
bad_style = "warn"
bad_style = { level = "warn", priority = -1 }
dead_code = "allow" # TODO: "warn"
improper_ctypes = "warn"
missing_copy_implementations = "warn"
Expand Down
7 changes: 4 additions & 3 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ using a `.htpasswd` file to specify users. By default, the server looks for this
file at the root of the persistence directory, but this can be changed using the
`--htpasswd-file` option. You can create such a file by executing the following
command (note that you need the `htpasswd` program from Apache's http-tools). In
order to append new user to the file, just omit the `-c` argument. Only bcrypt
and SHA encryption methods are supported, so use -B (very secure) or -s
(insecure by today's standards) when adding/changing passwords.
order to append new user to the file, just omit the `-c` argument. The supported
hashing methods are bcrypt (`-B`), SHA-256 crypt (`-2`), SHA-512 crypt (`-5`)
and APR1-MD5 (`-m`). Use `-B` unless you have a reason not to; the SHA1 (`-s`)
and `crypt(3)` (`-d`) methods are broken and are not accepted.

```sh
htpasswd -B -c .htpasswd username
Expand Down
5 changes: 2 additions & 3 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
# FIXME!: See https://github.com/RustCrypto/RSA/issues/19#issuecomment-1822995643.
# There is no workaround available yet.
"RUSTSEC-2023-0071",
# { id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
# "a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
# { crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
Expand All @@ -91,6 +88,8 @@ ignore = [
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
# Our own license
"AGPL-3.0-or-later",
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
Expand Down
7 changes: 1 addition & 6 deletions src/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ fn read_toml(file_path: &PathBuf) -> AppResult<BTreeMap<String, RepoAcl>> {
err, file_path
))
})?;
// make the contents static in memory
let s = Box::leak(s.into_boxed_str());

let mut repos: BTreeMap<String, RepoAcl> = toml::from_str(s)
let mut repos: BTreeMap<String, RepoAcl> = toml::from_str(&s)
.map_err(|err| ErrorKind::Config.context(format!("Could not parse TOML: {}", err)))?;

// copy key "default" into ""
Expand Down Expand Up @@ -254,8 +251,6 @@ mod tests {
use crate::testing::server_config;
use rstest::rstest;

use std::env;

#[rstest]
fn test_static_acl_access_passes() {
let acl = server_config().acl;
Expand Down
3 changes: 2 additions & 1 deletion src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use crate::{commands::EntryPoint, config::RusticServerConfig};
use abscissa_core::Config;
use abscissa_core::FrameworkErrorKind::IoError;
use abscissa_core::{
Application, FrameworkError, StandardPaths,
application::{self, AppCell},
config::{self, CfgCell},
path::AbsPathBuf,
trace, Application, FrameworkError, StandardPaths,
trace,
};
use abscissa_tokio::TokioComponent;
use std::path::Path;
Expand Down
49 changes: 43 additions & 6 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{borrow::Borrow, path::PathBuf};
use std::path::PathBuf;

use abscissa_core::SecretString;
use axum::{extract::FromRequestParts, http::request::Parts};
Expand Down Expand Up @@ -58,7 +58,23 @@ impl Auth {
let user = user.into();
let passwd = passwd.into();

self.users.as_ref().map_or(true, |users| matches!(users.get(&user), Some(passwd_data) if htpasswd_verify::Htpasswd::from(passwd_data.to_string().borrow()).check(user, passwd)))
let Some(users) = self.users.as_ref() else {
return true;
};

let Some(credential) = users.get(&user) else {
return false;
};

match htauth::verify_password(&passwd, credential.hash()) {
Ok(verified) => verified,
Err(err) => {
// An unsupported or malformed hash must never authenticate the
// user, but it is worth telling the operator about it.
tracing::warn!(%user, %err, "[AUTH] could not verify the stored hash");
false
}
}
}

pub const fn is_disabled(&self) -> bool {
Expand All @@ -72,7 +88,6 @@ pub struct BasicAuthFromRequest {
pub(crate) _password: SecretString,
}

#[async_trait::async_trait]
impl<S: Send + Sync> FromRequestParts<S> for BasicAuthFromRequest {
type Rejection = ApiErrorKind;

Expand All @@ -85,7 +100,7 @@ impl<S: Send + Sync> FromRequestParts<S> for BasicAuthFromRequest {

tracing::debug!(?auth_result, "[AUTH]");

return match auth_result {
match auth_result {
Ok(auth) => {
let AuthBasic((user, passw)) = auth;
let password = passw.unwrap_or_else(String::new);
Expand All @@ -108,22 +123,23 @@ impl<S: Send + Sync> FromRequestParts<S> for BasicAuthFromRequest {
}
Err(ApiErrorKind::AuthenticationHeaderError)
}
};
}
}
}

#[cfg(test)]
mod test {
use super::*;

use crate::htpasswd::Credential;
use crate::testing::{basic_auth_header_value, init_test_environment, server_config};

use anyhow::Result;
use axum::{
Router,
body::Body,
http::{Method, Request, StatusCode},
routing::get,
Router,
};
use http_body_util::BodyExt;
use rstest::{fixture, rstest};
Expand All @@ -143,6 +159,27 @@ mod test {
Ok(())
}

/// Hashes we cannot verify must be rejected, never accepted.
///
/// `{SHA}` and the DES based `crypt(3)` are no longer supported, and a
/// truncated hash cannot be verified either.
#[rstest]
#[case("{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=")]
#[case("aWmMhCH/tzWEg")]
#[case("$apr1$truncated")]
#[case("")]
fn test_unverifiable_hash_is_rejected(#[case] hash: &str) {
let mut credentials = CredentialMap::new();
let _ = credentials.insert(
"rustic".to_string(),
Credential::from_line(format!("rustic:{hash}")).unwrap(),
);

let auth = Auth::from(credentials);

assert!(!auth.verify("rustic", "password"));
}

#[rstest]
fn test_auth_from_file_passes(auth: Auth) {
init_auth(auth).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ use crate::{
config::RusticServerConfig,
};
use abscissa_core::{
config::Override, tracing::info, Command, Configurable, FrameworkError, Runnable,
Command, Configurable, FrameworkError, Runnable, config::Override, tracing::info,
};
use clap::builder::{
styling::{AnsiColor, Effects},
Styles,
styling::{AnsiColor, Effects},
};
use std::path::PathBuf;

Expand Down
6 changes: 3 additions & 3 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use std::path::PathBuf;

use abscissa_core::{status_err, Application, Command, Runnable, Shutdown};
use anyhow::{bail, Result};
use abscissa_core::{Application, Command, Runnable, Shutdown, status_err};
use anyhow::{Result, bail};
use clap::{Args, Parser, Subcommand};

use crate::{htpasswd::Htpasswd, prelude::RUSTIC_SERVER_APP};
Expand Down Expand Up @@ -132,7 +132,7 @@ fn check(path: &PathBuf) -> Result<()> {
{
bail!(
"Failed to create empty server configuration file: {} due to {}",
&path.to_string_lossy(),
path.to_string_lossy(),
err
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/commands/serve.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! `serve` subcommand

use abscissa_core::{
Application, Command, FrameworkError, Runnable, Shutdown,
config::Override,
status_err,
tracing::{debug, info},
Application, Command, FrameworkError, Runnable, Shutdown,
};
use anyhow::Result;
use clap::Parser;
Expand Down
8 changes: 5 additions & 3 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
acl::Acl,
auth::Auth,
config::{
default_data_dir, default_socket_address, AclSettings, HtpasswdSettings, LogSettings,
RusticServerConfig, TlsSettings,
AclSettings, HtpasswdSettings, LogSettings, RusticServerConfig, TlsSettings,
default_data_dir, default_socket_address,
},
error::{AppResult, ErrorKind},
storage::Storage,
Expand Down Expand Up @@ -119,7 +119,9 @@ where
fn auth(htpasswd_settings: HtpasswdSettings, data_dir: PathBuf) -> AppResult<Auth> {
let auth = if htpasswd_settings.is_disabled() {
info!("Authentication is disabled.");
warn!("This allows anyone to push to your repositories. This should be considered insecure and is not recommended for production use.");
warn!(
"This allows anyone to push to your repositories. This should be considered insecure and is not recommended for production use."
);
Auth::default()
} else {
info!(
Expand Down
10 changes: 6 additions & 4 deletions src/handlers/access_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ use tracing::debug;
use strum::VariantNames;

use crate::{
acl::{AccessType, AclChecker, ACL},
acl::{ACL, AccessType, AclChecker},
error::{ApiErrorKind, ApiResult},
typed_path::TpeKind,
};

pub fn check_auth_and_acl(
pub fn check_auth_and_acl<T: Into<Option<TpeKind>>>(
user: String,
tpe: impl Into<Option<TpeKind>>,
tpe: T,
path: &Path,
access_type: AccessType,
) -> ApiResult<impl IntoResponse> {
// `use<T>` keeps the response from capturing the lifetime of `path`, which
// edition 2024 would otherwise do implicitly.
) -> ApiResult<impl IntoResponse + use<T>> {
let tpe = tpe.into();

// don't allow paths that includes any of the defined types
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/file_config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};

use axum::{extract::Request, http::header, response::IntoResponse};
use axum_extra::{headers::Range, TypedHeader};
use axum_extra::{TypedHeader, headers::Range};
use axum_macros::debug_handler;
use axum_range::{KnownSize, Ranged};

Expand Down Expand Up @@ -139,9 +139,10 @@ mod test {
use std::{fs, path::PathBuf};

use axum::{
Router,
body::Body,
http::{Method, Request, StatusCode},
middleware, Router,
middleware,
};
use axum_extra::routing::RouterExt; // for `Router::typed_*`
use http_body_util::BodyExt;
Expand Down
Loading