Compare commits

...

4 Commits

Author SHA1 Message Date
minish a315baa258
config restructure + motd option 2023-12-07 13:31:27 -05:00
minish 2aa97e05b4
fix typo in ViewError::NotFound doc comment 2023-12-07 13:21:13 -05:00
minish 5f8adf023f
lower path traversal warning to an info
the new default log level is warning
so i don't want it to be possible to spam server logs on default config
2023-12-02 15:58:00 -05:00
minish d9f560677a
lol oops 2023-11-10 19:36:43 -05:00
11 changed files with 189 additions and 143 deletions

151
Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
@ -114,9 +129,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.1"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08b108ad2665fa3f6e6a517c3d80ec3e77d224c47d605167aefaa5d7ef97fa48"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
@ -141,16 +156,15 @@ dependencies = [
"sync_wrapper",
"tokio",
"tower",
"tower-http",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.0"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92"
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
@ -165,14 +179,29 @@ dependencies = [
[[package]]
name = "axum-macros"
version = "0.3.0"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4df0fc33ada14a338b799002f7e8657711422b25d4e16afb032708d6b185621"
checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 1.0.104",
"syn 2.0.38",
]
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
@ -471,10 +500,16 @@ dependencies = [
]
[[package]]
name = "h2"
version = "0.3.15"
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "h2"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
dependencies = [
"bytes",
"fnv",
@ -482,7 +517,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 1.9.2",
"indexmap 2.0.2",
"slab",
"tokio",
"tokio-util",
@ -527,9 +562,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.8"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [
"bytes",
"fnv",
@ -547,12 +582,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
[[package]]
name = "httparse"
version = "1.8.0"
@ -567,9 +596,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.23"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
@ -642,9 +671,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.4"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
@ -663,9 +692,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.137"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "lock_api"
@ -705,15 +734,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio"
version = "0.8.5"
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.42.0",
"windows-sys 0.48.0",
]
[[package]]
@ -745,6 +782,15 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.16.0"
@ -887,6 +933,12 @@ dependencies = [
"bitflags",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustversion"
version = "1.0.9"
@ -1039,9 +1091,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.7"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
@ -1122,14 +1174,14 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.22.0"
version = "1.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
dependencies = [
"autocfg",
"backtrace",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"parking_lot",
@ -1137,18 +1189,18 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"winapi",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "1.8.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.104",
"syn 2.0.38",
]
[[package]]
@ -1230,25 +1282,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"

View File

@ -15,7 +15,7 @@ I wrote breeze with the intention of running it in a container, but it runs just
Either way, you need to start off by cloning the Git repository.
```bash
git clone https://git.min.rip/minish/breeze.git
git clone https://git.min.rip/min/breeze.git
```
To run it in Docker, I recommend using Docker Compose. An example `docker-compose.yaml` configuration is below.

7
archived/Cargo.lock generated
View File

@ -8,6 +8,7 @@ version = "0.2.0"
dependencies = [
"bytes",
"once_cell",
"rustc-hash",
]
[[package]]
@ -21,3 +22,9 @@ name = "once_cell"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"

View File

@ -29,7 +29,11 @@ impl Archive {
}
} */
pub fn with_full_scan(full_scan_frequency: Duration, entry_lifetime: Duration, capacity: usize) -> Self {
pub fn with_full_scan(
full_scan_frequency: Duration,
entry_lifetime: Duration,
capacity: usize,
) -> Self {
Self {
cache_table: HashMap::with_capacity(256),
full_scan_frequency: Some(full_scan_frequency),
@ -67,11 +71,7 @@ impl Archive {
.map(|cache_entry| &cache_entry.value)
}
pub fn get_or_insert<F>(
&mut self,
key: String,
factory: F,
) -> &Bytes
pub fn get_or_insert<F>(&mut self, key: String, factory: F) -> &Bytes
where
F: Fn() -> Bytes,
{
@ -87,15 +87,15 @@ impl Archive {
&occupied.into_mut().value
}
Entry::Vacant(vacant) => &vacant.insert(CacheEntry::new(factory(), self.entry_lifetime)).value,
Entry::Vacant(vacant) => {
&vacant
.insert(CacheEntry::new(factory(), self.entry_lifetime))
.value
}
}
}
pub fn insert(
&mut self,
key: String,
value: Bytes,
) -> Option<Bytes> {
pub fn insert(&mut self, key: String, value: Bytes) -> Option<Bytes> {
let now = SystemTime::now();
self.try_full_scan_expired_items(now);
@ -144,7 +144,7 @@ impl Archive {
Some(())
}
None => None
None => None,
}
}

View File

@ -7,7 +7,7 @@ use tracing_subscriber::filter::LevelFilter;
#[derive(Deserialize)]
pub struct Config {
pub engine: EngineConfig,
pub cache: CacheConfig,
pub http: HttpConfig,
pub logger: LoggerConfig,
}
@ -22,7 +22,18 @@ pub struct EngineConfig {
pub save_path: PathBuf,
/// Authentication key for new uploads, will be required if this is specified. (optional)
pub upload_key: Option<String>,
#[serde(default)]
pub upload_key: String,
/// Configuration for cache system
pub cache: CacheConfig,
/// Motd displayed when the server's index page is visited.
///
/// This isn't explicitly engine-related but the engine is what gets passed to routes,
/// so it is here for now.
#[serde(default = "default_motd")]
pub motd: String,
}
#[serde_as]
@ -45,11 +56,26 @@ pub struct CacheConfig {
pub mem_capacity: usize,
}
fn default_motd() -> String {
"breeze file server (v%version%) - currently hosting %uplcount% files".to_string()
}
#[derive(Deserialize)]
pub struct HttpConfig {
pub listen_on: String,
}
fn default_level_filter() -> LevelFilter {
LevelFilter::WARN
}
#[serde_as]
#[derive(Deserialize)]
pub struct LoggerConfig {
/// Minimum level a log must be for it to be shown.
/// This defaults to "warn" if not specified.
#[serde_as(as = "Option<DisplayFromStr>")]
pub level: Option<LevelFilter>,
#[serde_as(as = "DisplayFromStr")]
#[serde(default = "default_level_filter")]
// yes... kind of a hack but serde doesn't have anything better
pub level: LevelFilter,
}

View File

@ -2,7 +2,6 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use archived::Archive;
@ -21,66 +20,47 @@ use tokio_stream::StreamExt;
use tracing::{debug, error, info};
use walkdir::WalkDir;
use crate::view::{ViewError, ViewSuccess};
use crate::{
config,
view::{ViewError, ViewSuccess},
};
/// breeze engine! this is the core of everything
pub struct Engine {
// ------ STATE ------ //
/// The in-memory cache that cached uploads are stored in.
cache: RwLock<Archive>,
/// Cached count of uploaded files.
pub upl_count: AtomicUsize,
// ------ CONFIG ------ //
/// The base URL that the server will be accessed from.
/// It is only used for formatting returned upload URLs.
pub base_url: String,
/// The path on disk that uploads are saved to.
save_path: PathBuf,
/// The authorisation key required for uploading new files.
/// If it is empty, no key will be required.
pub upload_key: String,
/// The maximum size for an upload to be stored in cache.
/// Anything bigger skips cache and is read/written to
/// directly from disk.
cache_max_length: usize,
/// Engine configuration
pub cfg: config::EngineConfig,
}
impl Engine {
/// Creates a new instance of the breeze engine.
pub fn new(
base_url: String,
save_path: PathBuf,
upload_key: String,
cache_max_length: usize,
cache_lifetime: Duration,
cache_full_scan_freq: Duration, // how often the cache will be scanned for expired items
cache_mem_capacity: usize,
) -> Self {
pub fn new(cfg: config::EngineConfig) -> Self {
Self {
cache: RwLock::new(Archive::with_full_scan(
cache_full_scan_freq,
cache_lifetime,
cache_mem_capacity,
cfg.cache.scan_freq,
cfg.cache.upload_lifetime,
cfg.cache.mem_capacity,
)),
upl_count: AtomicUsize::new(WalkDir::new(&save_path).min_depth(1).into_iter().count()), // count the amount of files in the save path and initialise our cached count with it
upl_count: AtomicUsize::new(
WalkDir::new(&cfg.save_path)
.min_depth(1)
.into_iter()
.count(),
), // count the amount of files in the save path and initialise our cached count with it
base_url,
save_path,
upload_key,
cache_max_length,
cfg,
}
}
/// Returns if an upload would be able to be cached
#[inline(always)]
fn will_use_cache(&self, length: usize) -> bool {
length <= self.cache_max_length
length <= self.cfg.cache.max_length
}
/// Check if an upload exists in cache or on disk
@ -128,7 +108,7 @@ impl Engine {
.to_string();
// path on disk
let mut path = self.save_path.clone();
let mut path = self.cfg.save_path.clone();
path.push(&id);
path.set_extension(original_extension);
@ -238,7 +218,7 @@ impl Engine {
.to_string();
// path on disk
let mut path = self.save_path.clone();
let mut path = self.cfg.save_path.clone();
path.push(&name);
// check if the upload exists, if not then 404

View File

@ -6,7 +6,11 @@ use axum::extract::State;
pub async fn index(State(engine): State<Arc<crate::engine::Engine>>) -> String {
let count = engine.upl_count.load(Ordering::Relaxed);
format!("minish's image host, currently hosting {} files", count)
let motd = engine.cfg.motd.clone();
motd
.replace("%version%", env!("CARGO_PKG_VERSION"))
.replace("%uplcount%", &count.to_string())
}
pub async fn robots_txt() -> &'static str {

View File

@ -11,7 +11,6 @@ use axum::{
};
use tokio::{fs, signal};
use tracing::{info, warn};
use tracing_subscriber::filter::LevelFilter;
mod config;
mod engine;
@ -35,30 +34,22 @@ async fn main() {
.await
.expect("failed to read config file! make sure it exists and you have read permissions");
let c: config::Config = toml::from_str(&config_str).expect("invalid config! check that you have included all required options and structured it properly (no config options expecting a number getting a string, etc.)");
let cfg: config::Config = toml::from_str(&config_str).expect("invalid config! check that you have included all required options and structured it properly (no config options expecting a number getting a string, etc.)");
tracing_subscriber::fmt()
.with_max_level(c.logger.level.unwrap_or(LevelFilter::WARN))
.with_max_level(cfg.logger.level)
.init();
if !c.engine.save_path.exists() || !c.engine.save_path.is_dir() {
if !cfg.engine.save_path.exists() || !cfg.engine.save_path.is_dir() {
panic!("the save path does not exist or is not a directory! this is invalid");
}
if c.engine.upload_key.is_none() {
if cfg.engine.upload_key.is_empty() {
warn!("engine upload_key is empty! no key will be required for uploading new files");
}
// create engine
let engine = Engine::new(
c.engine.base_url,
c.engine.save_path,
c.engine.upload_key.unwrap_or_default(),
c.cache.max_length,
c.cache.upload_lifetime,
c.cache.scan_freq,
c.cache.mem_capacity,
);
let engine = Engine::new(cfg.engine);
// build main router
let app = Router::new()
@ -69,11 +60,16 @@ async fn main() {
.with_state(Arc::new(engine));
// start web server
axum::Server::bind(&"0.0.0.0:8000".parse().unwrap())
axum::Server::bind(
&cfg.http
.listen_on
.parse()
.expect("failed to parse listen_on address"),
)
.serve(app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
.expect("failed to start server");
}
async fn shutdown_signal() {

View File

@ -20,7 +20,7 @@ pub async fn new(
const EMPTY_STRING: &String = &String::new();
// check upload key, if i need to
if !engine.upload_key.is_empty() && key.unwrap_or(EMPTY_STRING) != &engine.upload_key {
if !engine.cfg.upload_key.is_empty() && key.unwrap_or(EMPTY_STRING) != &engine.cfg.upload_key {
return Err(StatusCode::FORBIDDEN);
}
@ -40,7 +40,7 @@ pub async fn new(
.unwrap_or_default()
.to_string();
let url = format!("{}/p/{}", engine.base_url, name);
let url = format!("{}/p/{}", engine.cfg.base_url, name);
// read and parse content-length, and if it fails just assume it's really high so it doesn't cache
let content_length = headers

View File

@ -13,7 +13,7 @@ use bytes::Bytes;
use hyper::{http::HeaderValue, StatusCode};
use tokio::{fs::File, runtime::Handle};
use tokio_util::io::ReaderStream;
use tracing::{error, debug, warn};
use tracing::{error, debug, info};
/// Responses for a successful view operation
pub enum ViewSuccess {
@ -38,7 +38,7 @@ pub enum ViewSuccess {
/// Responses for a failed view operation
pub enum ViewError {
/// Will send status code 404 witha plaintext "not found" message.
/// Will send status code 404 with a plaintext "not found" message.
NotFound,
/// Will send status code 500 with a plaintext "internal server error" message.
@ -134,7 +134,7 @@ pub async fn view(
.components()
.any(|x| !matches!(x, Component::Normal(_)))
{
warn!("a request attempted path traversal");
info!("a request attempted path traversal");
return Err(ViewError::NotFound);
}