mod config; mod db; mod deleter; mod download; mod mime_relations; mod multipart; mod rate_limit; mod template; mod upload; use crate::rate_limit::ForwardedPeerIpKeyExtractor; use actix_files::Files; use actix_governor::{Governor, GovernorConfigBuilder}; use actix_web::{ http::header::{ HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION, }, middleware::{self, Condition, DefaultHeaders, Logger}, web::{self, Data}, App, Error, HttpResponse, HttpServer, }; use env_logger::Env; use sqlx::postgres::PgPool; use std::env; use tokio::sync::mpsc::channel; const DEFAULT_CONTENT_SECURITY_POLICY: (HeaderName, &str) = ( CONTENT_SECURITY_POLICY, "default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self';" ); #[allow(clippy::declare_interior_mutable_const)] const DEFAULT_PERMISSIONS: (HeaderName, &str) = ( PERMISSIONS_POLICY, "accelerometer=(), ambient-light-sensor=(), battery=(), camera=(), display-capture=(), document-domain=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(), usb=(), web-share=()" ); const DEFAULT_CONTENT_TYPE_OPTIONS: (HeaderName, &str) = (X_CONTENT_TYPE_OPTIONS, "nosniff"); const DEFAULT_FRAME_OPTIONS: (HeaderName, &str) = (X_FRAME_OPTIONS, "deny"); const DEFAULT_XSS_PROTECTION: (HeaderName, &str) = (X_XSS_PROTECTION, "1; mode=block"); const DEFAULT_REFERRER_POLICY: (HeaderName, &str) = (REFERRER_POLICY, "no-referrer"); async fn not_found() -> Result { Ok(HttpResponse::NotFound() .content_type("text/plain") .body("not found")) } #[tokio::main] async fn main() -> std::io::Result<()> { env_logger::Builder::from_env(Env::default().default_filter_or("info,sqlx=warn")).init(); let pool: PgPool = db::setup().await; let config = config::from_env().await; let (sender, receiver) = channel(8); log::info!("omnomnom"); let db = web::Data::new(pool.clone()); let expiry_watch_sender = web::Data::new(sender); let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:8000".to_owned()); let deleter = tokio::spawn(deleter::delete_old_files( receiver, pool, config.files_dir.clone(), )); template::write_prefillable_templates(&config).await; let config = Data::new(config); let governor_conf = GovernorConfigBuilder::default() .per_second(config.rate_limit_replenish_seconds) .burst_size(config.rate_limit_burst) .key_extractor(ForwardedPeerIpKeyExtractor { proxied: config.proxied, }) .use_headers() .finish() .unwrap(); let http_server = HttpServer::new({ move || { App::new() .wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#)) .wrap( DefaultHeaders::new() .add(DEFAULT_CONTENT_SECURITY_POLICY) .add(DEFAULT_PERMISSIONS) .add(DEFAULT_CONTENT_TYPE_OPTIONS) .add(DEFAULT_FRAME_OPTIONS) .add(DEFAULT_XSS_PROTECTION) .add(DEFAULT_REFERRER_POLICY), ) .wrap(middleware::Compress::default()) .wrap(middleware::NormalizePath::trim()) .app_data(db.clone()) .app_data(expiry_watch_sender.clone()) .app_data(config.clone()) .service( web::resource("/") .route(web::get().to(upload::index)) .route(web::head().to(upload::index)), ) .service(web::resource("/upload").route(web::post().to(upload::upload))) .service( web::resource(["/upload/{id}", "/upload/{id}/{name}"]) .route(web::get().to(upload::uploaded)) .route(web::head().to(upload::uploaded)), ) .service(Files::new("/static", "static").disable_content_disposition()) .default_service(web::route().to(not_found)) .service( web::resource(["/{id:[a-z0-9]{5}}", "/{id:[a-z0-9]{5}}/{name}"]) .wrap(Condition::new( config.enable_rate_limit, Governor::new(&governor_conf), )) .route(web::get().to(download::download)) .route(web::head().to(download::download)), ) } }) .bind(bind_address)? .run(); // exit when http_server exits OR when deleter errors tokio::select! { result = http_server => result, result = deleter => { result?.map(|_| unreachable!("deletion runs infinitely")).expect("deletion may not fail") }, } }