//! Actix route handlers use crate::setup::{self, Config}; use actix_identity::Identity; use actix_web::{error::BlockingError, web, Error, HttpRequest, HttpResponse, Responder}; use base64; use chrono::{DateTime, NaiveDateTime, Utc}; use diesel; use serde::Serialize; use std::convert::Infallible; #[cfg(feature = "dev")] use crate::get_env; #[cfg(feature = "dev")] use std::{fs, path::PathBuf}; /// Parses an ID fn parse_id(id: &str) -> Result { match i32::from_str_radix(id, 36) { Ok(id) => Ok(id), Err(_) => Err(HttpResponse::BadRequest().body("Invalid ID")), } } async fn auth( identity: Identity, request: HttpRequest, password_hash: &[u8], ) -> Result<(), HttpResponse> { if identity.identity().is_some() { return Ok(()); } if password_hash == setup::hash("").as_slice() { identity.remember("guest".into()); return Ok(()); } let header = match request.headers().get("Authorization") { Some(h) => match h.to_str() { Ok(h) => h, Err(_) => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")), }, None => { return Err(HttpResponse::Unauthorized() .header("WWW-Authenticate", "Basic realm=\"filite\"") .body("Unauthorized")) } }; let connection_string = header.replace("Basic ", ""); let (user, password) = match base64::decode(&connection_string) { Ok(c) => { let credentials: Vec> = c .splitn(2, |b| b == &b':') .map(|s| s.to_vec()) .collect::>>(); match credentials.len() { 2 => (credentials[0].clone(), credentials[1].clone()), _ => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")), } } Err(_) => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")), }; let infallible_hash = move || -> Result, Infallible> { Ok(setup::hash(password)) }; if web::block(infallible_hash).await.unwrap().as_slice() == password_hash { match String::from_utf8(user.to_vec()) { Ok(u) => { identity.remember(u); Ok(()) } Err(_) => Err(HttpResponse::BadRequest().body("Invalid Authorization header")), } } else { Err(HttpResponse::Unauthorized() .header("WWW-Authenticate", "Basic realm=\"filite\"") .body("Unauthorized")) } } /// Match result from REPLACE queries #[inline(always)] fn match_replace_result( result: Result>, ) -> Result { match result { Ok(x) => Ok(HttpResponse::Created().json(x)), Err(_) => Err(HttpResponse::InternalServerError() .body("Internal server error") .into()), } } /// Handles error from single GET queries using find #[inline(always)] fn match_find_error(error: BlockingError) -> Result { match error { BlockingError::Error(e) => match e { diesel::result::Error::NotFound => { Err(HttpResponse::NotFound().body("Not found").into()) } _ => Err(HttpResponse::InternalServerError() .body("Internal server error") .into()), }, BlockingError::Canceled => Err(HttpResponse::InternalServerError() .body("Internal server error") .into()), } } /// Formats a timestamp to the "Last-Modified" header format fn timestamp_to_last_modified(timestamp: i32) -> String { let datetime = DateTime::::from_utc(NaiveDateTime::from_timestamp(i64::from(timestamp), 0), Utc); datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string() } /// Escapes text to be inserted in a HTML element fn escape_html(text: &str) -> String { text.replace("&", "&") .replace("<", "<") .replace(">", ">") } /// GET multiple entries macro_rules! select { ($m:ident) => { pub async fn gets( request: HttpRequest, query: actix_web::web::Query, pool: actix_web::web::Data, identity: actix_identity::Identity, password_hash: actix_web::web::Data>, ) -> Result { crate::routes::auth(identity, request, &password_hash).await?; let filters = crate::queries::SelectFilters::from(query.into_inner()); match actix_web::web::block(move || crate::queries::$m::select(filters, pool)).await { Ok(x) => Ok(actix_web::HttpResponse::Ok().json(x)), Err(_) => Err(actix_web::HttpResponse::InternalServerError() .body("Internal server error") .into()), } } }; } /// DELETE an entry macro_rules! delete { ($m:ident) => { pub async fn delete( request: HttpRequest, path: actix_web::web::Path, pool: actix_web::web::Data, identity: actix_identity::Identity, password_hash: actix_web::web::Data>, ) -> Result { crate::routes::auth(identity, request, &password_hash).await?; let id = crate::routes::parse_id(&path)?; match actix_web::web::block(move || crate::queries::$m::delete(id, pool)).await { Ok(()) => Ok(actix_web::HttpResponse::NoContent().body("Deleted")), Err(e) => crate::routes::match_find_error(e), } } }; } #[cfg(feature = "dev")] lazy_static! { static ref RESOURCES_DIR: PathBuf = { let mut ressources_dir = PathBuf::new(); ressources_dir.push(get_env!("CARGO_MANIFEST_DIR")); ressources_dir.push("resources"); ressources_dir }; static ref HTML_PATH: PathBuf = { let mut html_path = RESOURCES_DIR.clone(); html_path.push("index.html"); html_path }; static ref JS_PATH: PathBuf = { let mut js_path = RESOURCES_DIR.clone(); js_path.push("script.js"); js_path }; static ref CSS_PATH: PathBuf = { let mut css_path = RESOURCES_DIR.clone(); css_path.push("style.css"); css_path }; } #[cfg(not(feature = "dev"))] lazy_static! { static ref INDEX_CONTENTS: String = { let html = include_str!("../resources/index.html"); let js = include_str!("../resources/script.js"); let css = include_str!("../resources/style.css"); html.replace("{{ js }}", js).replace("{{ css }}", css) }; } static HIGHLIGHT_CONTENTS: &str = include_str!("../resources/highlight.html"); const HIGHLIGHT_LANGUAGE: &str = r#""#; /// Index page letting users upload via a UI pub async fn index( request: HttpRequest, identity: Identity, password_hash: web::Data>, ) -> impl Responder { if let Err(response) = auth(identity, request, &password_hash).await { return response; } let contents = { #[cfg(feature = "dev")] { let html = fs::read_to_string(&*HTML_PATH).expect("Can't read index.html"); let js = fs::read_to_string(&*JS_PATH).expect("Can't read script.js"); let css = fs::read_to_string(&*CSS_PATH).expect("Can't read style.css"); html.replace("{{ js }}", &js).replace("{{ css }}", &css) } #[cfg(not(feature = "dev"))] { (&*INDEX_CONTENTS).clone() } }; HttpResponse::Ok() .header("Content-Type", "text/html") .body(contents) } /// GET the config info pub async fn get_config( request: HttpRequest, config: web::Data, identity: Identity, password_hash: web::Data>, ) -> impl Responder { match auth(identity, request, &password_hash).await { Ok(_) => HttpResponse::Ok().json(config.get_ref()), Err(response) => response, } } /// Logout route pub async fn logout(identity: Identity) -> impl Responder { if identity.identity().is_some() { identity.forget(); HttpResponse::Ok().body("Logged out") } else { HttpResponse::Unauthorized() .header("WWW-Authenticate", "Basic realm=\"filite\"") .body("Unauthorized") } } pub mod files { use crate::{ queries::{self, SelectQuery}, routes::{auth, match_find_error, parse_id}, setup::Config, Pool, }; use actix_files::NamedFile; use actix_identity::Identity; use actix_web::{error::BlockingError, http, web, Error, HttpRequest, HttpResponse}; use chrono::Utc; use std::{fs, path::PathBuf}; select!(files); /// GET a file entry and statically serve it pub async fn get( path: web::Path, pool: web::Data, config: web::Data, ) -> Result { let id = parse_id(&path)?; match web::block(move || queries::files::find(id, pool)).await { Ok(file) => { let mut path = config.files_dir.clone(); path.push(file.filepath); match NamedFile::open(&path) { Ok(nf) => Ok(nf), Err(_) => Err(HttpResponse::NotFound().body("Not found").into()), } } Err(e) => match_find_error(e), } } /// Request body when PUTting files #[derive(Deserialize)] pub struct PutFile { pub base64: String, pub filename: String, } /// PUT a new file entry pub async fn put( request: HttpRequest, path: web::Path, body: web::Json, pool: web::Data, config: web::Data, identity: Identity, password_hash: web::Data>, ) -> Result { auth(identity, request, &password_hash).await?; let id = parse_id(&path)?; let result = web::block(move || { let mut path = config.files_dir.clone(); let mut relative_path = PathBuf::new(); if fs::create_dir_all(&path).is_err() { return Err(http::StatusCode::from_u16(500).unwrap()); } let mut filename = body.filename.clone(); filename = format!("{:x}.{}", Utc::now().timestamp(), filename); path.push(&filename); relative_path.push(&filename); let relative_path = match relative_path.to_str() { Some(rp) => rp, None => return Err(http::StatusCode::from_u16(500).unwrap()), }; let contents = match base64::decode(&body.base64) { Ok(contents) => contents, Err(_) => return Err(http::StatusCode::from_u16(400).unwrap()), }; if fs::write(&path, contents).is_err() { return Err(http::StatusCode::from_u16(500).unwrap()); } match queries::files::replace(id, relative_path, pool) { Ok(file) => Ok(file), Err(_) => Err(http::StatusCode::from_u16(500).unwrap()), } }) .await; match result { Ok(file) => Ok(HttpResponse::Created().json(file)), Err(e) => match e { BlockingError::Error(sc) => Err(HttpResponse::new(sc).into()), BlockingError::Canceled => Err(HttpResponse::InternalServerError() .body("Internal server error") .into()), }, } } delete!(files); } pub mod links { use crate::{ queries::{self, SelectQuery}, routes::{ auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified, }, Pool, }; use actix_identity::Identity; use actix_web::{web, Error, HttpRequest, HttpResponse}; select!(links); /// GET a link entry and redirect to it pub async fn get( path: web::Path, pool: web::Data, ) -> Result { let id = parse_id(&path)?; match web::block(move || queries::links::find(id, pool)).await { Ok(link) => Ok(HttpResponse::Found() .header("Location", link.forward) .header("Last-Modified", timestamp_to_last_modified(link.created)) .finish()), Err(e) => match_find_error(e), } } /// Request body when PUTting links #[derive(Deserialize)] pub struct PutLink { pub forward: String, } /// PUT a new link entry pub async fn put( request: HttpRequest, path: web::Path, body: web::Json, pool: web::Data, identity: Identity, password_hash: web::Data>, ) -> Result { auth(identity, request, &password_hash).await?; let id = parse_id(&path)?; match_replace_result( web::block(move || queries::links::replace(id, &body.forward, pool)).await, ) } delete!(links); } pub mod texts { use crate::routes::escape_html; use crate::{ queries::{self, SelectQuery}, routes::{ auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified, }, Pool, }; use crate::{ routes::{HIGHLIGHT_CONTENTS, HIGHLIGHT_LANGUAGE}, setup::Config, }; use actix_identity::Identity; use actix_web::{web, Error, HttpRequest, HttpResponse}; select!(texts); /// GET a text entry and display it pub async fn get( config: web::Data, path: web::Path, pool: web::Data, ) -> Result { let id = parse_id(&path)?; match web::block(move || queries::texts::find(id, pool)).await { Ok(text) => { let last_modified = timestamp_to_last_modified(text.created); if text.highlight { let languages: Vec = config .highlight .languages .iter() .map(|l| HIGHLIGHT_LANGUAGE.replace("{{ language }}", l)) .collect(); let languages = languages.join("\n"); let contents = HIGHLIGHT_CONTENTS .replace("{{ title }}", &path) .replace("{{ theme }}", &config.highlight.theme) .replace("{{ contents }}", &escape_html(&text.contents)) .replace("{{ languages }}", &languages); Ok(HttpResponse::Ok() .header("Last-Modified", last_modified) .header("Content-Type", "text/html") .body(contents)) } else { Ok(HttpResponse::Ok() .header("Last-Modified", last_modified) .body(text.contents)) } } Err(e) => match_find_error(e), } } /// Request body when PUTting texts #[derive(Deserialize)] pub struct PutText { pub contents: String, pub highlight: bool, } /// PUT a new text entry pub async fn put( request: HttpRequest, path: web::Path, body: web::Json, pool: web::Data, identity: Identity, password_hash: web::Data>, ) -> Result { auth(identity, request, &password_hash).await?; let id = parse_id(&path)?; match_replace_result( web::block(move || queries::texts::replace(id, &body.contents, body.highlight, pool)) .await, ) } delete!(texts); }