Add filter whitelist

This commit is contained in:
asonix 2020-06-07 14:12:19 -05:00
parent 35aca6a426
commit 4f55251310
4 changed files with 141 additions and 34 deletions

View file

@ -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 <addr> The address and port the server binds to, e.g. 127.0.0.1:80
-f, --format <format> An image format to convert all uploaded files into, supports 'jpg' and 'png'
-p, --path <path> The path to the data directory, e.g. data/
-a, --addr <addr> The address and port the server binds to, e.g. 127.0.0.1:80
-f, --format <format> An image format to convert all uploaded files into, supports 'jpg' and 'png'
-p, --path <path> The path to the data directory, e.g. data/
-w, --whitelist <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

View file

@ -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,
@ -18,6 +18,13 @@ pub(crate) struct Config {
help = "An image format to convert all uploaded files into, supports 'jpg' and 'png'"
)]
format: Option<Format>,
#[structopt(
short,
long,
help = "An optional list of filters to whitelist, supports 'identity', 'thumbnail', and 'blur'"
)]
whitelist: Option<Vec<String>>,
}
impl Config {
@ -32,6 +39,12 @@ impl Config {
pub(crate) fn format(&self) -> Option<Format> {
self.format.clone()
}
pub(crate) fn filter_whitelist(&self) -> Option<HashSet<String>> {
self.whitelist
.as_ref()
.map(|wl| wl.iter().cloned().collect())
}
}
#[derive(Debug, thiserror::Error)]

View file

@ -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<UploadManager>,
segments: web::Path<String>,
manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> {
let mut segments: Vec<String> = 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(

View file

@ -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<Box<dyn Processor + Send>>
where
Self: Sized;
fn path(&self, path: PathBuf) -> PathBuf;
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError>;
fn is_whitelisted(whitelist: Option<&HashSet<String>>) -> 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<Box<dyn Processor + Send>>
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<Box<dyn Processor + Send>>
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<Box<dyn Processor + Send>> {
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<Box<dyn Processor + Send>> {
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<String>>,
) -> Vec<Box<dyn Processor + Send>> {
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(