diff --git a/README.md b/README.md index 351baeb..05f32c0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ _a simple image hosting service_ ## Usage ### Running ``` -$ ./pict-rs --help pict-rs 0.1.0 USAGE: @@ -15,9 +14,11 @@ FLAGS: -V, --version Prints version information OPTIONS: - -a, --addr The address and port the server binds to, e.g. 127.0.0.1:80 - -f, --format An image format to convert all uploaded files into, supports 'jpg' and 'png' - -p, --path The path to the data directory, e.g. data/ + -a, --addr The address and port the server binds to, e.g. 127.0.0.1:80 + -f, --format An image format to convert all uploaded files into, supports 'jpg' and 'png' + -p, --path The path to the data directory, e.g. data/ + -w, --whitelist ... An optional list of filters to whitelist, supports 'identity', 'thumbnail', and + 'blur' ``` #### Example: @@ -29,6 +30,10 @@ Running locally, port 9000, storing data in data/, and converting all uploads to ``` $ ./pict-rs -a 127.0.0.1:9000 -p data/ -f png ``` +Running locally, port 8080, storing data in data/, and only allowing the `thumbnail` and `identity` filters +``` +$ ./pict-rs -a 127.0.0.1:8080 -p data/ -w thumbnail identity +``` ### API pict-rs offers four endpoints: @@ -63,10 +68,10 @@ pict-rs offers four endpoints: existing transformations include - `identity`: apply no changes - `blur{float}`: apply a gaussian blur to the file - - `{int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square + - `thumbnail{int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square An example of usage could be ``` - GET /image/256/blur3.0/asdf.png + GET /image/thumbnail256/blur3.0/asdf.png ``` which would create a 256x256px thumbnail and blur it diff --git a/src/config.rs b/src/config.rs index dd8c78a..8ea873a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ -use std::{net::SocketAddr, path::PathBuf}; +use std::{collections::HashSet, net::SocketAddr, path::PathBuf}; -#[derive(structopt::StructOpt)] +#[derive(Clone, Debug, structopt::StructOpt)] pub(crate) struct Config { #[structopt( short, @@ -25,6 +25,13 @@ pub(crate) struct Config { help = "An image format to convert all uploaded files into, supports 'jpg' and 'png'" )] format: Option, + + #[structopt( + short, + long, + help = "An optional list of filters to whitelist, supports 'identity', 'thumbnail', and 'blur'" + )] + whitelist: Option>, } impl Config { @@ -39,6 +46,12 @@ impl Config { pub(crate) fn format(&self) -> Option { self.format.clone() } + + pub(crate) fn filter_whitelist(&self) -> Option> { + self.whitelist + .as_ref() + .map(|wl| wl.iter().cloned().collect()) + } } #[derive(Debug, thiserror::Error)] diff --git a/src/main.rs b/src/main.rs index 575e1ea..81e2c57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use actix_web::{ }; use futures::stream::{Stream, TryStreamExt}; use log::{error, info}; -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; use structopt::StructOpt; mod config; @@ -154,8 +154,9 @@ async fn delete( /// Serve files async fn serve( - manager: web::Data, segments: web::Path, + manager: web::Data, + whitelist: web::Data>>, ) -> Result { let mut segments: Vec = segments .into_inner() @@ -164,7 +165,7 @@ async fn serve( .collect(); let alias = segments.pop().ok_or(UploadError::MissingFilename)?; - let chain = self::processor::build_chain(&segments); + let chain = self::processor::build_chain(&segments, whitelist.as_ref().as_ref()); let name = manager.from_alias(alias).await?; let base = manager.image_dir(); @@ -280,16 +281,20 @@ async fn main() -> Result<(), anyhow::Error> { })), ); + let config2 = config.clone(); HttpServer::new(move || { let client = Client::build() .header("User-Agent", "pict-rs v0.1.0-master") .finish(); + let config = config2.clone(); + App::new() .wrap(Logger::default()) .wrap(Compress::default()) .data(manager.clone()) .data(client) + .data(config.filter_whitelist()) .service( web::scope("/image") .service( diff --git a/src/processor.rs b/src/processor.rs index 02339e8..d94b64c 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,17 +1,59 @@ use crate::error::UploadError; use actix_web::web; use image::{DynamicImage, GenericImageView}; -use log::warn; -use std::path::PathBuf; +use log::debug; +use std::{collections::HashSet, path::PathBuf}; pub(crate) trait Processor { + fn name() -> &'static str + where + Self: Sized; + + fn is_processor(s: &str) -> bool + where + Self: Sized; + + fn parse(s: &str) -> Option> + where + Self: Sized; + fn path(&self, path: PathBuf) -> PathBuf; fn process(&self, img: DynamicImage) -> Result; + + fn is_whitelisted(whitelist: Option<&HashSet>) -> bool + where + Self: Sized, + { + whitelist + .map(|wl| wl.contains(Self::name())) + .unwrap_or(true) + } } pub(crate) struct Identity; impl Processor for Identity { + fn name() -> &'static str + where + Self: Sized, + { + "identity" + } + + fn is_processor(s: &str) -> bool + where + Self: Sized, + { + s == Self::name() + } + + fn parse(_: &str) -> Option> + where + Self: Sized, + { + Some(Box::new(Identity)) + } + fn path(&self, path: PathBuf) -> PathBuf { path } @@ -24,8 +66,30 @@ impl Processor for Identity { pub(crate) struct Thumbnail(u32); impl Processor for Thumbnail { + fn name() -> &'static str + where + Self: Sized, + { + "thumbnail" + } + + fn is_processor(s: &str) -> bool + where + Self: Sized, + { + s.starts_with(Self::name()) + } + + fn parse(s: &str) -> Option> + where + Self: Sized, + { + let size = s.trim_start_matches(Self::name()).parse().ok()?; + Some(Box::new(Thumbnail(size))) + } + fn path(&self, mut path: PathBuf) -> PathBuf { - path.push("thumbnail"); + path.push(Self::name()); path.push(self.0.to_string()); path } @@ -42,8 +106,24 @@ impl Processor for Thumbnail { pub(crate) struct Blur(f32); impl Processor for Blur { + fn name() -> &'static str + where + Self: Sized, + { + "blur" + } + + fn is_processor(s: &str) -> bool { + s.starts_with(Self::name()) + } + + fn parse(s: &str) -> Option> { + let sigma = s.trim_start_matches(Self::name()).parse().ok()?; + Some(Box::new(Blur(sigma))) + } + fn path(&self, mut path: PathBuf) -> PathBuf { - path.push("blur"); + path.push(Self::name()); path.push(self.0.to_string()); path } @@ -53,25 +133,29 @@ impl Processor for Blur { } } -pub(crate) fn build_chain(args: &[String]) -> Vec> { - args.into_iter().fold(Vec::new(), |mut acc, arg| { - match arg.to_lowercase().as_str() { - "identity" => acc.push(Box::new(Identity)), - other if other.starts_with("blur") => { - if let Ok(sigma) = other.trim_start_matches("blur").parse() { - acc.push(Box::new(Blur(sigma))); - } - } - other => { - if let Ok(size) = other.parse() { - acc.push(Box::new(Thumbnail(size))); - } else { - warn!("Unknown processor {}", other); - } - } - }; - acc - }) +macro_rules! parse { + ($x:ident, $y:expr, $z:expr) => {{ + if $x::is_processor($y) && $x::is_whitelisted($z) { + return $x::parse($y); + } + }}; +} + +pub(crate) fn build_chain( + args: &[String], + whitelist: Option<&HashSet>, +) -> Vec> { + args.into_iter() + .filter_map(|arg| { + parse!(Identity, arg.as_str(), whitelist); + parse!(Thumbnail, arg.as_str(), whitelist); + parse!(Blur, arg.as_str(), whitelist); + + debug!("Skipping {}, invalid or whitelisted", arg); + + None + }) + .collect() } pub(crate) fn build_path(