Make 'import' a protected method

This commit is contained in:
asonix 2020-07-11 16:28:49 -05:00
parent de3e04a411
commit fd809e4a0b
6 changed files with 101 additions and 14 deletions

2
Cargo.lock generated
View File

@ -1463,7 +1463,7 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pict-rs" name = "pict-rs"
version = "0.2.0-alpha.2" version = "0.2.0-alpha.3"
dependencies = [ dependencies = [
"actix-form-data", "actix-form-data",
"actix-fs", "actix-fs",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "pict-rs" name = "pict-rs"
description = "A simple image hosting service" description = "A simple image hosting service"
version = "0.2.0-alpha.2" version = "0.2.0-alpha.3"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0" license = "AGPL-3.0"
readme = "README.md" readme = "README.md"

View File

@ -4,7 +4,7 @@ _a simple image hosting service_
## Usage ## Usage
### Running ### Running
``` ```
pict-rs 0.2.0-alpha.2 pict-rs 0.2.0-alpha.3
USAGE: USAGE:
pict-rs [FLAGS] [OPTIONS] --path <path> pict-rs [FLAGS] [OPTIONS] --path <path>
@ -17,6 +17,8 @@ FLAGS:
OPTIONS: OPTIONS:
-a, --addr <addr> The address and port the server binds to. [env: PICTRS_ADDR=] [default: -a, --addr <addr> The address and port the server binds to. [env: PICTRS_ADDR=] [default:
0.0.0.0:8080] 0.0.0.0:8080]
--api-key <api-key> An optional string to be checked on requests to privileged endpoints [env:
PICTRS_API_KEY=]
-f, --format <format> An optional image format to convert all uploaded files into, supports 'jpg', -f, --format <format> An optional image format to convert all uploaded files into, supports 'jpg',
'png', and 'webp' [env: PICTRS_FORMAT=] 'png', and 'webp' [env: PICTRS_FORMAT=]
-m, --max-file-size <max-file-size> Specify the maximum allowed uploaded file size (in Megabytes) [env: -m, --max-file-size <max-file-size> Specify the maximum allowed uploaded file size (in Megabytes) [env:
@ -99,9 +101,6 @@ pict-rs offers four endpoints:
"msg": "ok" "msg": "ok"
} }
``` ```
- `POST /import` for uploading an image while preserving the filename. This should not be exposed to
the public internet, as it can cause naming conflicts with saved files. The upload format and
response format are the same as the `POST /image` endpoint.
- `GET /image/download?url=...` Download an image from a remote server, returning the same JSON - `GET /image/download?url=...` Download an image from a remote server, returning the same JSON
payload as the `POST` endpoint payload as the `POST` endpoint
- `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the - `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the
@ -119,8 +118,17 @@ pict-rs offers four endpoints:
GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0 GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0
``` ```
which would create a 256x256px JPEG thumbnail and blur it which would create a 256x256px JPEG thumbnail and blur it
- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to delete a file, - `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
where `delete_token` and `file` are from the `/image` endpoint's JSON delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON
- `POST /internal/import` for uploading an image while preserving the filename. This should not be
exposed to the public internet, as it can cause naming conflicts with saved files. The upload
format and response format are the same as the `POST /image` endpoint.
This endpoint also requires authentication via the `X-Api-Token` header, and is disabled unless
the `--api-key` option is passed to the binary or the PICTRS_API_KEY environment variable is
set.
A secure API key can be generated by any password generator.
## Contributing ## Contributing
Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3. Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3.

View File

@ -50,6 +50,13 @@ pub(crate) struct Config {
default_value = "40" default_value = "40"
)] )]
max_file_size: usize, max_file_size: usize,
#[structopt(
long,
env = "PICTRS_API_KEY",
help = "An optional string to be checked on requests to privileged endpoints"
)]
api_key: Option<String>,
} }
impl Config { impl Config {
@ -78,6 +85,10 @@ impl Config {
pub(crate) fn max_file_size(&self) -> usize { pub(crate) fn max_file_size(&self) -> usize {
self.max_file_size self.max_file_size
} }
pub(crate) fn api_key(&self) -> Option<&str> {
self.api_key.as_ref().map(|s| s.as_str())
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View File

@ -24,7 +24,7 @@ mod validate;
use self::{ use self::{
config::Config, config::Config,
error::UploadError, error::UploadError,
middleware::Tracing, middleware::{Internal, Tracing},
processor::process_image, processor::process_image,
upload_manager::UploadManager, upload_manager::UploadManager,
validate::{image_webp, video_mp4}, validate::{image_webp, video_mp4},
@ -501,10 +501,14 @@ async fn main() -> Result<(), anyhow::Error> {
.service(web::resource("/original/{filename}").route(web::get().to(serve))) .service(web::resource("/original/{filename}").route(web::get().to(serve)))
.service(web::resource("/process.{ext}").route(web::get().to(process))), .service(web::resource("/process.{ext}").route(web::get().to(process))),
) )
.service(
web::scope("/internal")
.wrap(Internal(CONFIG.api_key().map(|s| s.to_owned())))
.service( .service(
web::resource("/import") web::resource("/import")
.wrap(import_form.clone()) .wrap(import_form.clone())
.route(web::post().to(upload)), .route(web::post().to(upload)),
),
) )
}) })
.bind(CONFIG.bind_address())? .bind(CONFIG.bind_address())?

View File

@ -1,5 +1,9 @@
use actix_web::dev::{Service, Transform}; use actix_web::{
use futures::future::{ok, Ready}; dev::{Service, ServiceRequest, Transform},
http::StatusCode,
HttpResponse, ResponseError,
};
use futures::future::{ok, LocalBoxFuture, Ready};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use tracing_futures::{Instrument, Instrumented}; use tracing_futures::{Instrument, Instrumented};
use uuid::Uuid; use uuid::Uuid;
@ -10,6 +14,22 @@ pub(crate) struct TracingMiddleware<S> {
inner: S, inner: S,
} }
pub(crate) struct Internal(pub(crate) Option<String>);
pub(crate) struct InternalMiddleware<S>(Option<String>, S);
#[derive(Clone, Debug, thiserror::Error)]
#[error("Invalid API Key")]
struct ApiError;
impl ResponseError for ApiError {
fn status_code(&self) -> StatusCode {
StatusCode::UNAUTHORIZED
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(serde_json::json!({ "msg": self.to_string() }))
}
}
impl<S> Transform<S> for Tracing impl<S> Transform<S> for Tracing
where where
S: Service, S: Service,
@ -49,3 +69,47 @@ where
.instrument(tracing::info_span!("request", ?uuid)) .instrument(tracing::info_span!("request", ?uuid))
} }
} }
impl<S> Transform<S> for Internal
where
S: Service<Request = ServiceRequest, Error = actix_web::Error>,
S::Future: 'static,
{
type Request = S::Request;
type Response = S::Response;
type Error = S::Error;
type InitError = ();
type Transform = InternalMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(InternalMiddleware(self.0.clone(), service))
}
}
impl<S> Service for InternalMiddleware<S>
where
S: Service<Request = ServiceRequest, Error = actix_web::Error>,
S::Future: 'static,
{
type Request = S::Request;
type Response = S::Response;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<S::Response, S::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.1.poll_ready(cx)
}
fn call(&mut self, req: S::Request) -> Self::Future {
if let Some(value) = req.headers().get("x-api-token") {
if value.to_str().is_ok() && value.to_str().ok() == self.0.as_ref().map(|s| s.as_str())
{
let fut = self.1.call(req);
return Box::pin(async move { fut.await });
}
}
Box::pin(async move { Err(ApiError.into()) })
}
}