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]]
name = "pict-rs"
version = "0.2.0-alpha.2"
version = "0.2.0-alpha.3"
dependencies = [
"actix-form-data",
"actix-fs",

View File

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

View File

@ -4,7 +4,7 @@ _a simple image hosting service_
## Usage
### Running
```
pict-rs 0.2.0-alpha.2
pict-rs 0.2.0-alpha.3
USAGE:
pict-rs [FLAGS] [OPTIONS] --path <path>
@ -17,6 +17,8 @@ FLAGS:
OPTIONS:
-a, --addr <addr> The address and port the server binds to. [env: PICTRS_ADDR=] [default:
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',
'png', and 'webp' [env: PICTRS_FORMAT=]
-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"
}
```
- `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
payload as the `POST` endpoint
- `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
```
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,
where `delete_token` and `file` are from the `/image` endpoint's JSON
- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
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
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"
)]
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 {
@ -78,6 +85,10 @@ impl Config {
pub(crate) fn max_file_size(&self) -> usize {
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)]

View File

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

View File

@ -1,5 +1,9 @@
use actix_web::dev::{Service, Transform};
use futures::future::{ok, Ready};
use actix_web::{
dev::{Service, ServiceRequest, Transform},
http::StatusCode,
HttpResponse, ResponseError,
};
use futures::future::{ok, LocalBoxFuture, Ready};
use std::task::{Context, Poll};
use tracing_futures::{Instrument, Instrumented};
use uuid::Uuid;
@ -10,6 +14,22 @@ pub(crate) struct TracingMiddleware<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
where
S: Service,
@ -49,3 +69,47 @@ where
.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()) })
}
}