Add support for config file in image directory

This config file will be loaded together with the images and defines the
passwords for login and some other parameters.
This commit is contained in:
2023-04-07 00:37:54 +02:00
parent 68f50b5292
commit 8f004e5e65
6 changed files with 160 additions and 18 deletions

View File

@ -1,8 +1,8 @@
use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::{OsStr, OsString}, fs::File, io::{BufReader, Seek, Cursor, BufRead}, env::args_os, os::unix::prelude::OsStrExt, cmp::Reverse, sync::{Arc, RwLock}, collections::HashMap};
use anyhow::{Context, Result, anyhow};
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
use axum_sessions::{async_session::MemoryStore, SessionLayer, extractors::{ReadableSession, WritableSession}};
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect, Response}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
use axum_sessions::{async_session::{MemoryStore, SessionStore}, SessionLayer, extractors::{ReadableSession, WritableSession}};
use axum_template::{RenderHtml, engine::Engine};
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
use chrono::{DateTime, Utc};
@ -15,6 +15,7 @@ use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer};
use tracing::Level;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
mod config;
mod error;
#[global_allocator]
@ -22,7 +23,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
// TODO
// * Polish the css
// * Add configuration file
// * Brute-force protection / rate-limiting
// * Support for headlines
// * Image cache cleanup
@ -31,9 +32,11 @@ type ImageDir = PathBuf;
type SecretKey = [u8; 64];
type ImageListCache = Arc<RwLock<Vec<ImageInfo>>>;
type ImageCache = Arc<RwLock<HashMap<OsString, (DateTime<Utc>, Vec<u8>)>>>;
type Config = Arc<RwLock<config::Config>>;
#[derive(Clone, extract::FromRef)]
struct ApplicationState {
config: Config,
engine: TemplateEngine,
image_cache: ImageCache,
image_dir: ImageDir,
@ -60,15 +63,19 @@ async fn main() {
let sessions = MemoryStore::new();
let image_dir = PathBuf::from(image_path);
let config = Config::default();
let image_cache = ImageCache::default();
let image_list_cache = ImageListCache::default();
tokio::spawn(update_image_list_cache_job(
tokio::spawn(update_config_and_image_list_cache_job(
image_dir.clone(),
image_list_cache.clone()
config.clone(),
image_list_cache.clone(),
sessions.clone(),
));
let mut jinja = minijinja::Environment::new();
jinja.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
jinja.add_template("base", include_str!("../templates/base.html")).unwrap();
jinja.add_template("index", include_str!("../templates/index.html")).unwrap();
jinja.add_template("login", include_str!("../templates/login.html")).unwrap();
@ -79,6 +86,7 @@ async fn main() {
let app = Router::new()
.route("/", get(index.layer(CompressionLayer::new())))
.route("/config.yaml", get(example_config))
.route("/authenticate", post(authenticate))
.route("/image/:encrypted_filename_hex", get(converted_image))
.layer(TraceLayer::new_for_http()
@ -87,6 +95,7 @@ async fn main() {
)
.layer(session_layer)
.with_state(ApplicationState {
config,
engine: Engine::from(jinja),
image_cache,
image_dir,
@ -102,13 +111,50 @@ async fn main() {
.unwrap();
}
async fn update_image_list_cache_job(image_dir: ImageDir, image_cache: ImageListCache) {
async fn update_config_and_image_list_cache_job(
image_dir: ImageDir,
config: Config,
image_cache: ImageListCache,
sessions: MemoryStore,
) {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
// Read config
let config_file = image_dir.join("config.yaml");
let config = config.clone();
let invalidate_sessions = if config_file.exists() {
task::spawn_blocking(move || {
match config::read_from_file(&config_file) {
Ok(new_config) => {
let mut config = config.write().unwrap();
if new_config != *config {
tracing::debug!("New config: {:?}", new_config);
*config = new_config;
true
} else {
false
}
},
Err(error) => {
tracing::error!("Could not read config, using default: {:#}", error);
*config.write().unwrap() = config::Config::default();
true
}
}
}).await.unwrap()
} else {
*config.write().unwrap() = config::Config::default();
true
};
if invalidate_sessions {
sessions.clear_store().await.ok();
}
// Update image list
let image_dir = image_dir.clone();
let images = task::spawn_blocking(move || {
// TODO: Only update images with changed modification times
@ -140,19 +186,28 @@ pub struct ImageInfo {
}
#[derive(Debug, Serialize)]
pub struct IndexTempalte {
pub struct LoginTemplate {
title: String,
}
#[derive(Debug, Serialize)]
pub struct IndexTemplate {
title: String,
top_message: Option<String>,
images: Vec<ImageInfo>,
}
async fn index(
engine: TemplateEngine,
State(config): State<Config>,
State(image_list_cache): State<ImageListCache>,
State(secret_key): State<SecretKey>,
session: ReadableSession,
) -> Result<impl IntoResponse, AppError> {
) -> Result<Response, AppError> {
let logged_in = session.get::<()>("logged_in").is_some();
let config = config.read().unwrap();
if logged_in {
let images = image_list_cache.read().unwrap();
@ -171,18 +226,22 @@ async fn index(
images.sort_by_key(|entry| Reverse(entry.created));
Ok(RenderHtml("index", engine, IndexTempalte {
title: "Some pictures".into(),
Ok(RenderHtml("index", engine, IndexTemplate {
title: config.title.clone(),
top_message: config.top_message.clone(),
images,
}))
}).into_response())
} else {
Ok(RenderHtml("login", engine, IndexTempalte {
title: "Some pictures".into(),
images: vec![],
}))
Ok(RenderHtml("login", engine, LoginTemplate {
title: config.title.clone(),
}).into_response())
}
}
async fn example_config() -> &'static str {
include_str!("../config.yaml.example")
}
#[derive(Deserialize)]
struct AuthenticateForm {
password: String,
@ -190,9 +249,10 @@ struct AuthenticateForm {
async fn authenticate(
mut session: WritableSession,
Form(form): Form<AuthenticateForm>
State(config): State<Config>,
Form(form): Form<AuthenticateForm>,
) -> Redirect {
if form.password == "testle" {
if config.read().unwrap().passwords.contains(&form.password) {
session.insert("logged_in", ()).ok();
}
Redirect::to("/")