pict-rs-proxy/src/main.rs
asonix b68ee94f38
All checks were successful
continuous-integration/drone/push Build is passing
Remove dependency on once_cell
2024-02-01 17:22:19 -06:00

1096 lines
30 KiB
Rust

// need this for ructe
#![allow(clippy::needless_borrow)]
use actix_web::{
body::BodyStream,
http::{
header::{CacheControl, CacheDirective, ContentType, LastModified, LOCATION},
StatusCode,
},
middleware::NormalizePath,
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer, ResponseError,
};
use anyhow::Context;
use awc::{Client, Connector};
use clap::Parser;
use console_subscriber::ConsoleLayer;
use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{propagation::TraceContextPropagator, Resource};
use rustls::{
sign::CertifiedKey, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore,
ServerConfig,
};
use std::{
io::Cursor,
net::SocketAddr,
path::PathBuf,
sync::{Arc, OnceLock},
time::{Duration, SystemTime},
};
use tracing_actix_web::TracingLogger;
use tracing_awc::Tracing;
use tracing_error::{ErrorLayer, SpanTrace};
use tracing_log::LogTracer;
use tracing_subscriber::{
filter::Targets, layer::SubscriberExt, registry::LookupSpan, Layer, Registry,
};
use url::Url;
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
const HOURS: u32 = 60 * 60;
const DAYS: u32 = 24 * HOURS;
/// Simple proxy service to demonstrate pict-rs's functionality
#[derive(Clone, Debug, Parser)]
#[command(author, version, about, long_about = None)]
struct Config {
#[arg(
short,
long,
env = "PICTRS_PROXY_ADDR",
default_value = "[::]:8081",
help = "The address and port the server binds to"
)]
addr: SocketAddr,
#[arg(
short,
long,
env = "PICTRS_PROXY_UPSTREAM",
default_value = "http://localhost:8080",
help = "The url of the upstream pict-rs server"
)]
upstream: Url,
#[arg(
short,
long,
env = "PICTRS_PROXY_DOMAIN",
default_value = "http://localhost:8081",
help = "The scheme, domain, and optional port of the pict-rs proxy server"
)]
domain: Url,
#[arg(
long,
env = "PICTRS_PROXY_CONSOLE_BUFFER_SIZE",
help = "Number of events to buffer for the console subscriber"
)]
console_event_buffer_size: Option<usize>,
#[arg(
long,
env = "PICTRS_PROXY_CONSOLE",
help = "Enable the tokio-console at the specified address"
)]
console_addr: Option<SocketAddr>,
#[arg(
short,
long,
env = "PICTRS_PROXY_OPENTELEMETRY_URL",
help = "URL of OpenTelemetry Collector"
)]
opentelemetry_url: Option<Url>,
#[arg(
short,
long,
env = "PICTRS_PROXY_CERTIFICATE",
help = "Path to the certificate file to connect to pict-rs over TLS"
)]
certificate: Option<PathBuf>,
#[arg(
long,
env = "PICTRS_PROXY_SERVER_CERTIFICATE",
help = "Path to the certificate file to serve pict-rs-proxy over TLS"
)]
server_certificate: Option<PathBuf>,
#[arg(
long,
env = "PICTRS_PROXY_SERVER_PRIVATE_KEY",
help = "Path to the private key file to serve pict-rs-proxy over TLS"
)]
server_private_key: Option<PathBuf>,
}
impl Config {
fn domain(&self) -> Option<&str> {
config().domain.domain()
}
fn upstream_upload_url(&self) -> String {
let mut url = self.upstream.clone();
url.set_path("image/backgrounded");
url.to_string()
}
fn upstream_claim_url(&self, upload_id: &str) -> String {
let mut url = self.upstream.clone();
url.set_path("image/backgrounded/claim");
url.set_query(Some(&format!("upload_id={}", upload_id)));
url.to_string()
}
fn upstream_details_url(&self, name: &str) -> String {
let mut url = self.upstream.clone();
url.set_path(&format!("image/details/original/{}", name));
url.to_string()
}
fn upstream_image_url(&self, name: &str) -> String {
let mut url = self.upstream.clone();
url.set_path(&format!("image/original/{}", name));
url.to_string()
}
fn upstream_thumbnail_url(&self, size: u64, name: &str, filetype: FileType) -> String {
let mut url = self.upstream.clone();
url.set_path(&format!("image/process.{}", filetype.as_str()));
url.set_query(Some(&format!("src={}&thumbnail={}", name, size)));
url.to_string()
}
fn upstream_delete_url(&self, token: &str, name: &str) -> String {
let mut url = self.upstream.clone();
url.set_path(&format!("image/delete/{}/{}", token, name));
url.to_string()
}
fn image_url(&self, name: &str) -> String {
let mut url = self.domain.clone();
url.set_path(&format!("image/{}", name));
url.to_string()
}
fn thumbnail_url(&self, size: u64, name: &str, filetype: FileType) -> String {
let mut url = self.domain.clone();
url.set_path(&format!("thumb/{}/{}/{}", size, filetype.as_str(), name));
url.to_string()
}
fn view_url(&self, size: Option<u64>, name: &str) -> String {
let mut url = self.domain.clone();
if let Some(size) = size {
url.set_path(&format!("view/{}/{}", size, name));
} else {
url.set_path(&format!("view/{}", name));
}
url.to_string()
}
fn thumbnails_url(&self, name: &str) -> String {
let mut url = self.domain.clone();
url.set_path("/thumbnails");
url.set_query(Some(&format!("image={}", name)));
url.to_string()
}
fn delete_url(&self, token: &str, name: &str) -> String {
let mut url = self.domain.clone();
url.set_path("delete");
url.set_query(Some(&format!("file={}&token={}", name, token)));
url.to_string()
}
fn confirm_delete_url(&self, token: &str, name: &str) -> String {
let mut url = self.domain.clone();
url.set_path("delete");
url.set_query(Some(&format!("file={}&token={}&confirm=true", name, token)));
url.to_string()
}
}
static CONFIG: OnceLock<Config> = OnceLock::new();
fn config() -> &'static Config {
CONFIG.get_or_init(Config::parse)
}
pub enum UploadResult<'a> {
Image(Image),
UploadId(&'a str),
Error(Error),
}
#[derive(Debug, serde::Deserialize)]
enum FileType {
#[serde(rename = "avif")]
Avif,
#[serde(rename = "jpg")]
Jpg,
#[serde(rename = "webp")]
Webp,
}
impl FileType {
fn as_str(&self) -> &'static str {
match self {
Self::Avif => "avif",
Self::Jpg => "jpg",
Self::Webp => "webp",
}
}
}
#[derive(Debug, serde::Deserialize)]
pub struct Images {
msg: String,
files: Option<Vec<Image>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct Upload {
// This is technically a UUID, but we don't care in this program
upload_id: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct Uploads {
msg: String,
uploads: Option<Vec<Upload>>,
}
#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct UploadQuery {
#[serde(default)]
uploads: Vec<String>,
#[serde(default)]
files: Vec<(String, String)>,
}
impl UploadQuery {
fn try_from_uploads(uploads: Uploads) -> Result<Self, String> {
if let Some(uploads) = uploads.uploads {
Ok(UploadQuery {
uploads: uploads.into_iter().map(|u| u.upload_id).collect(),
files: vec![],
})
} else {
Err(uploads.msg)
}
}
}
impl Images {
fn files(&self) -> Option<&[Image]> {
self.files.as_ref().map(|v| v.as_ref())
}
fn msg(&self) -> &str {
&self.msg
}
fn is_ok(&self) -> bool {
self.files().is_some()
}
fn message(&self) -> &'static str {
if self.is_ok() {
"Images Uploaded"
} else {
"Image Upload Failed"
}
}
}
#[derive(Debug, serde::Deserialize)]
pub struct Details {
height: usize,
width: usize,
content_type: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct Image {
file: String,
delete_token: String,
details: Details,
}
impl Image {
fn filename(&self) -> &str {
&self.file
}
fn is_video(&self) -> bool {
self.details.content_type.starts_with("video")
}
fn width(&self) -> usize {
self.details.width
}
fn height(&self) -> usize {
self.details.height
}
fn mime(&self) -> &str {
&self.details.content_type
}
fn link(&self) -> String {
config().image_url(&self.file)
}
fn thumbnails(&self) -> String {
config().thumbnails_url(&self.file)
}
fn view(&self, size: Option<u64>) -> String {
config().view_url(size, &self.file)
}
fn thumb(&self, size: u64, filetype: FileType) -> String {
config().thumbnail_url(size, &self.file, filetype)
}
fn delete(&self) -> String {
config().delete_url(&self.delete_token, &self.file)
}
fn confirm_delete(&self) -> String {
config().confirm_delete_url(&self.delete_token, &self.file)
}
}
fn statics(file: &str) -> String {
format!("/static/{}", file)
}
#[derive(Debug)]
pub struct Error {
context: SpanTrace,
kind: ErrorKind,
}
impl Error {
pub(crate) fn upstream_error(&self) -> Option<&str> {
match self.kind {
ErrorKind::UploadFailed(ref msg) => Some(msg),
_ => None,
}
}
}
impl<T> From<T> for Error
where
ErrorKind: From<T>,
{
fn from(error: T) -> Self {
Error {
context: SpanTrace::capture(),
kind: error.into(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut source: Option<&dyn std::error::Error> = Some(&self.kind);
while let Some(error) = source {
writeln!(f, "{}", error)?;
source = std::error::Error::source(error);
}
std::fmt::Display::fmt(&self.context, f)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.kind.source()
}
}
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
match render(HttpResponse::build(self.status_code()), |cursor| {
self::templates::error_html(cursor, &self.kind.to_string())
}) {
Ok(res) => res,
Err(_) => HttpResponse::build(self.status_code())
.content_type(mime::TEXT_PLAIN.essence_str())
.body(self.kind.to_string()),
}
}
}
#[derive(Debug, thiserror::Error)]
enum ErrorKind {
#[error("Upload failed: {0}")]
UploadFailed(String),
#[error("Error transfering data")]
Io(#[from] std::io::Error),
#[error("Upstream request failed")]
SendRequest(#[from] awc::error::SendRequestError),
#[error("Could not parse response")]
JsonPayload(#[from] awc::error::JsonPayloadError),
#[error("Invalid query string")]
Query(#[from] serde_qs::Error),
#[error("Operation panicked")]
Canceled(#[from] actix_rt::task::JoinError),
}
#[tracing::instrument(name = "Upload Page")]
async fn index() -> Result<HttpResponse, Error> {
render(HttpResponse::Ok(), |cursor| {
self::templates::index_html(cursor, "/upload", "images[]")
})
}
#[tracing::instrument(name = "List Uploads", skip(client))]
async fn list_uploads(
query: serde_qs::actix::QsQuery<UploadQuery>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let query = query.into_inner();
let mut upload_handles = Vec::new();
let mut details_handles = Vec::new();
for upload_id in &query.uploads {
let claim_url = config().upstream_claim_url(upload_id.as_str());
let client = client.clone();
upload_handles.push(actix_rt::spawn(async move {
let mut res = client.get(claim_url).send().await?;
if res.status() == 204 {
return Ok(None) as Result<Option<Image>, Error>;
}
let result = res.json::<Images>().await?;
match result.files {
Some(files) if files.len() == 1 => Ok(files.into_iter().next()),
Some(_) => Err(ErrorKind::UploadFailed("Bad response".into()).into()),
None => Err(ErrorKind::UploadFailed(result.msg).into()),
}
}));
}
for (file, delete_token) in &query.files {
let details_url = config().upstream_details_url(file);
let file = file.clone();
let delete_token = delete_token.clone();
let client = client.clone();
details_handles.push(actix_rt::spawn(async move {
let mut res = client.get(details_url).send().await?;
let details = res.json::<Details>().await?;
Ok(Image {
file,
delete_token,
details,
})
}))
}
let mut results = Vec::new();
let mut any_incomplete = false;
for handle in details_handles {
let res = handle.await.map_err(Error::from).and_then(|res| res);
match res {
Ok(image) => {
results.push(UploadResult::Image(image));
}
Err(e) => {
results.push(UploadResult::Error(e));
}
}
}
for (handle, upload_id) in upload_handles.into_iter().zip(&query.uploads) {
let res = handle.await.map_err(Error::from).and_then(|res| res);
match res {
Ok(Some(image)) => {
results.push(UploadResult::Image(image));
}
Ok(None) => {
any_incomplete = true;
results.push(UploadResult::UploadId(upload_id));
}
Err(e) => {
results.push(UploadResult::Error(e));
}
}
}
if any_incomplete {
let new_query =
results
.into_iter()
.fold(UploadQuery::default(), |mut query, res| match res {
UploadResult::Image(img) => {
query.files.push((img.file, img.delete_token));
query
}
UploadResult::UploadId(id) => {
query.uploads.push(id.to_owned());
query
}
_ => query,
});
if new_query != query {
let query = serde_qs::to_string(&new_query)?;
return Ok(HttpResponse::SeeOther()
.insert_header((LOCATION, format!("/upload?{}", query)))
.finish());
}
return render(HttpResponse::Ok(), |cursor| {
self::templates::uploads_html(cursor)
});
}
render(HttpResponse::Ok(), |cursor| {
self::templates::finished_uploads_html(cursor, results)
})
}
#[tracing::instrument(name = "Upload", skip(req, body, client))]
async fn upload(
req: HttpRequest,
body: web::Payload,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let client_request = client.request_from(config().upstream_upload_url(), req.head());
let client_request = if let Some(addr) = req.head().peer_addr {
client_request.append_header(("X-Forwarded-For", addr.to_string()))
} else {
client_request
};
let mut res = client_request.send_stream(body).await?;
let uploads = res.json::<Uploads>().await?;
let query = UploadQuery::try_from_uploads(uploads).map_err(ErrorKind::UploadFailed)?;
let query = serde_qs::to_string(&query)?;
Ok(HttpResponse::SeeOther()
.insert_header((LOCATION, format!("/upload?{}", query)))
.finish())
}
const THUMBNAIL_SIZES: &[u64] = &[40, 50, 80, 100, 200, 400, 800, 1200];
#[derive(Debug, serde::Deserialize)]
struct ThumbnailQuery {
image: String,
}
#[tracing::instrument(name = "Thumbs", skip(client))]
async fn thumbs(
query: web::Query<ThumbnailQuery>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let file = query.into_inner().image;
let url = config().upstream_details_url(&file);
let mut res = client.get(url).send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
}
let details: Details = res.json().await?;
let image = Image {
file,
delete_token: String::new(),
details,
};
render(HttpResponse::Ok(), |cursor| {
self::templates::thumbnails_html(cursor, image, THUMBNAIL_SIZES)
})
}
#[tracing::instrument(name = "Image", skip(req, client))]
async fn image(
url: String,
req: HttpRequest,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let client_request = client.request_from(url, req.head());
let client_request = if let Some(addr) = req.head().peer_addr {
client_request.insert_header(("X-Forwarded-For", addr.to_string()))
} else {
client_request
};
let res = client_request.no_decompress().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
}
let mut client_res = HttpResponse::build(res.status());
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
client_res.insert_header((name.clone(), value.clone()));
}
Ok(client_res.body(BodyStream::new(res)))
}
#[tracing::instrument(name = "View original", skip(client))]
async fn view_original(
file: web::Path<String>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let file = file.into_inner();
let url = config().upstream_details_url(&file);
let mut res = client.get(url).send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
}
let details: Details = res.json().await?;
let image = Image {
file,
delete_token: String::new(),
details,
};
render(HttpResponse::Ok(), |cursor| {
self::templates::view_html(cursor, image, None, THUMBNAIL_SIZES.last())
})
}
#[tracing::instrument(name = "View", skip(client))]
async fn view(
parts: web::Path<(u64, String)>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let (size, file) = parts.into_inner();
if !valid_thumbnail_size(size) {
return Ok(to_404());
}
let url = config().upstream_details_url(&file);
let mut res = client.get(url).send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
}
let details: Details = res.json().await?;
let image = Image {
file,
delete_token: String::new(),
details,
};
render(HttpResponse::Ok(), |cursor| {
self::templates::view_html(cursor, image, Some(size), THUMBNAIL_SIZES.last())
})
}
#[tracing::instrument(name = "Thumbnail", skip(req, client))]
async fn thumbnail(
parts: web::Path<(u64, FileType, String)>,
req: HttpRequest,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let (size, filetype, file) = parts.into_inner();
if valid_thumbnail_size(size) {
let url = config().upstream_thumbnail_url(size, &file, filetype);
return image(url, req, client).await;
}
Ok(to_404())
}
fn valid_thumbnail_size(size: u64) -> bool {
THUMBNAIL_SIZES.contains(&size)
}
#[tracing::instrument(name = "Full resolution", skip(req, client))]
async fn full_res(
filename: web::Path<String>,
req: HttpRequest,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let url = config().upstream_image_url(&filename.into_inner());
image(url, req, client).await
}
#[allow(clippy::async_yields_async)]
#[tracing::instrument(name = "Static files")]
async fn static_files(filename: web::Path<String>) -> HttpResponse {
let filename = filename.into_inner();
if let Some(data) = self::templates::statics::StaticFile::get(&filename) {
return HttpResponse::Ok()
.insert_header(LastModified(SystemTime::now().into()))
.insert_header(CacheControl(vec![
CacheDirective::Public,
CacheDirective::MaxAge(365 * DAYS),
CacheDirective::Extension("immutable".to_owned(), None),
]))
.insert_header(ContentType(data.mime.clone()))
.body(data.content);
}
to_404()
}
#[derive(Debug, serde::Deserialize)]
struct DeleteQuery {
token: String,
file: String,
#[serde(default)]
confirm: bool,
}
#[tracing::instrument(name = "Delete", skip(client))]
async fn delete(
query: web::Query<DeleteQuery>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let DeleteQuery {
token,
file,
confirm,
} = query.into_inner();
let url = config().upstream_details_url(&file);
let mut res = client.get(url).send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
}
if confirm {
let url = config().upstream_delete_url(&token, &file);
client.delete(url).send().await?;
render(HttpResponse::Ok(), |cursor| {
self::templates::deleted_html(cursor, &file)
})
} else {
let details: Details = res.json().await?;
render(HttpResponse::Ok(), move |cursor| {
self::templates::confirm_delete_html(
cursor,
&Image {
file,
delete_token: token,
details,
},
)
})
}
}
fn to_404() -> HttpResponse {
HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, "/404"))
.finish()
}
#[tracing::instrument(name = "Not Found")]
async fn not_found() -> Result<HttpResponse, Error> {
render(HttpResponse::NotFound(), |cursor| {
self::templates::not_found_html(cursor)
})
}
async fn go_home() -> HttpResponse {
HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, "/"))
.finish()
}
#[tracing::instrument(name = "Render", skip(builder, f))]
fn render(
mut builder: HttpResponseBuilder,
f: impl FnOnce(&mut Cursor<&mut Vec<u8>>) -> Result<(), std::io::Error>,
) -> Result<HttpResponse, Error> {
let min = {
let mut bytes = vec![];
(f)(&mut Cursor::new(&mut bytes))?;
minify_html::minify(&bytes, &minify_html::Cfg::spec_compliant())
};
Ok(builder
.content_type(mime::TEXT_HTML.essence_str())
.body(min))
}
fn init_tracing(
service_name: &'static str,
opentelemetry_url: Option<&Url>,
console_addr: Option<SocketAddr>,
console_event_buffer_size: Option<usize>,
) -> Result<(), anyhow::Error> {
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
LogTracer::init()?;
let targets: Targets = std::env::var("RUST_LOG")
.unwrap_or_else(|_| "info".into())
.parse()?;
let format_layer = tracing_subscriber::fmt::layer().with_filter(targets.clone());
let subscriber = Registry::default()
.with(format_layer)
.with(ErrorLayer::default());
if let Some(console_addr) = console_addr {
let console_builder = ConsoleLayer::builder()
.with_default_env()
.server_addr(console_addr);
let console_layer = if let Some(buffer_size) = console_event_buffer_size {
console_builder.event_buffer_capacity(buffer_size).spawn()
} else {
console_builder.spawn()
};
let subscriber = subscriber.with(console_layer);
init_subscriber(subscriber, targets, opentelemetry_url, service_name)?;
tracing::info!("Serving tokio-console endpoint on {console_addr}");
} else {
init_subscriber(subscriber, targets, opentelemetry_url, service_name)?;
}
Ok(())
}
fn init_subscriber<S>(
subscriber: S,
targets: Targets,
opentelemetry_url: Option<&Url>,
service_name: &'static str,
) -> anyhow::Result<()>
where
S: SubscriberExt + Send + Sync,
for<'a> S: LookupSpan<'a>,
{
if let Some(url) = opentelemetry_url {
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_trace_config(
opentelemetry_sdk::trace::config().with_resource(Resource::new(vec![
KeyValue::new("service.name", service_name),
])),
)
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(url.as_str()),
)
.install_batch(opentelemetry_sdk::runtime::Tokio)?;
let otel_layer = tracing_opentelemetry::layer()
.with_tracer(tracer)
.with_filter(targets);
let subscriber = subscriber.with(otel_layer);
tracing::subscriber::set_global_default(subscriber)?;
} else {
tracing::subscriber::set_global_default(subscriber)?;
}
Ok(())
}
async fn rustls_client_config() -> anyhow::Result<ClientConfig> {
let mut cert_store = RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS
.iter()
.map(|anchor| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
anchor.subject.to_vec(),
anchor.subject_public_key_info.to_vec(),
anchor.name_constraints.as_ref().map(|d| d.to_vec()),
)
})
.collect(),
};
if let Some(cert) = config().certificate.as_ref() {
let cert_bytes = tokio::fs::read(&cert).await?;
for res in rustls_pemfile::certs(&mut cert_bytes.as_slice()) {
cert_store.add(&Certificate(res?.to_vec()))?;
}
};
Ok(ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(cert_store)
.with_no_client_auth())
}
async fn rustls_server_key() -> anyhow::Result<Option<CertifiedKey>> {
let certificate_path = if let Some(c) = &config().server_certificate {
c
} else {
tracing::info!("No server certificate");
return Ok(None);
};
let private_key_path = if let Some(p) = &config().server_private_key {
p
} else {
tracing::info!("No server private_key");
return Ok(None);
};
let cert_bytes = tokio::fs::read(certificate_path).await?;
let certs = rustls_pemfile::certs(&mut cert_bytes.as_slice())
.map(|res| res.map(|c| Certificate(c.to_vec())))
.collect::<Result<Vec<_>, _>>()?;
let key_bytes = tokio::fs::read(private_key_path).await?;
let key =
rustls_pemfile::private_key(&mut key_bytes.as_slice())?.context("No key in keyfile")?;
let signing_key = rustls::sign::any_supported_type(&PrivateKey(Vec::from(key.secret_der())))?;
Ok(Some(CertifiedKey::new(certs, signing_key)))
}
#[actix_rt::main]
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
init_tracing(
"pict-rs-proxy",
config().opentelemetry_url.as_ref(),
config().console_addr,
config().console_event_buffer_size,
)?;
let client_config = rustls_client_config().await?;
let server = HttpServer::new(move || {
let client = Client::builder()
.wrap(Tracing)
.add_default_header(("User-Agent", "pict-rs-frontend, v0.5.0"))
.timeout(Duration::from_secs(30))
.disable_redirects()
.connector(Connector::new().rustls_021(Arc::new(client_config.clone())))
.finish();
App::new()
.app_data(web::Data::new(client))
.wrap(NormalizePath::trim())
.wrap(TracingLogger::default())
.service(web::resource("/").route(web::get().to(index)))
.service(
web::resource("/upload")
.route(web::post().to(upload))
.route(web::get().to(list_uploads)),
)
.service(web::resource("/image/{filename}").route(web::get().to(full_res)))
.service(web::resource("/thumbnails").route(web::get().to(thumbs)))
.service(web::resource("/view/{size}/{filename}").route(web::get().to(view)))
.service(web::resource("/view/{filename}").route(web::get().to(view_original)))
.service(
web::resource("/thumb/{size}/{filetype}/{filename}")
.route(web::get().to(thumbnail)),
)
.service(web::resource("/static/{filename}").route(web::get().to(static_files)))
.service(web::resource("/delete").route(web::get().to(delete)))
.service(web::resource("/404").route(web::get().to(not_found)))
.default_service(web::get().to(go_home))
});
if let Some(key) = rustls_server_key().await? {
tracing::info!("Serving pict-rs-proxy over TLS on {}", config().addr);
let (tx, rx) = rustls_channel_resolver::channel::<32>(key);
let handle = actix_rt::spawn(async move {
let mut interval = actix_rt::time::interval(Duration::from_secs(30));
interval.tick().await;
loop {
interval.tick().await;
match rustls_server_key().await {
Ok(Some(key)) => tx.update(key),
Ok(None) => tracing::warn!("Missing server certificates"),
Err(e) => tracing::error!("Failed to read server certificates {e}"),
}
}
});
let server_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(rx);
server
.bind_rustls_021(config().addr, server_config)?
.run()
.await?;
handle.abort();
let _ = handle.await;
} else {
tracing::info!("Serving pict-rs-proxy on {}", config().addr);
server.bind(config().addr)?.run().await?;
}
Ok(())
}