zachery.lol/crates/server/bin/main.rs

145 lines
4.7 KiB
Rust

use axum::{
Json, Router,
http::StatusCode,
response::Html,
routing::{get, post},
};
use message_tools::BoxedMessage;
use serde::Serialize;
use std::{
fs::OpenOptions,
io::{BufRead, Read},
path::{Path, PathBuf},
};
use tokio::io::AsyncWriteExt;
use tower_http::services::ServeDir;
const PUBKEY_PATH: &str = "./pubkeys/public_keys.json";
const MESSAGE_STORAGE: &str = "./messages";
lazy_static::lazy_static! {
static ref PUBLIC_KEYS: Vec<Vec<u8>> = load_public_keys(PUBKEY_PATH).expect("static path");
}
type Error = Box<dyn std::error::Error>;
type Result<T> = std::result::Result<T, Error>;
fn load_public_keys(path: impl AsRef<Path>) -> std::io::Result<Vec<Vec<u8>>> {
// Open the file
let f = OpenOptions::new().read(true).write(false).open(path)?;
// Wrap it in a BufReader for efficient line-by-line reading
let reader = std::io::BufReader::new(f);
let mut public_keys = Vec::new();
// Iterate over each line in the file
for line_result in reader.lines() {
let line = line_result?; // Handle potential IO errors during reading
// Skip empty lines or lines with only whitespace
let trimmed_line = line.trim();
if trimmed_line.is_empty() {
continue;
}
// Deserialize the line (which is a single JSON array) into a Vec<u8>
let key: Vec<u8> = serde_json::from_str(trimmed_line).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse JSON line: {}", e),
)
})?;
public_keys.push(key);
}
Ok(public_keys)
}
#[tokio::main]
async fn main() -> Result<()> {
// build our application with a single route
let app = Router::new()
.route("/", get(serve_path("./routes/root/index.html")?))
.route("/who", get(serve_path("./routes/who/index.html")?))
.route("/api/pubkey", get(async || select_key(&PUBLIC_KEYS)))
.route("/contact", get(serve_path("./routes/contact/index.html")?))
.route("/contact/message", get(serve_path("./routes/contact/message/index.html")?))
.route("/api/publish", post(publish_message))
.nest_service("/gossip-dashboard", ServeDir::new("./routes/gossip-dashboard"))
.nest_service("/routes", ServeDir::new("./routes"));
let addr = "0.0.0.0:3000";
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("Server running at http://{addr}");
axum::serve(listener, app).await.unwrap();
Ok(())
}
// Define a struct for the response
#[derive(Serialize)]
struct UserCreated {
tag: String,
}
// FIXME: sanitation of boxed messages
async fn publish_message(Json(msg): Json<BoxedMessage>) -> (StatusCode, Json<UserCreated>) {
let tag = msg.tag(); // Capture tag early
// 1. Define the full *file* path: e.g., "MESSAGE_STORAGE/some_tag/.json"
let file_path = PathBuf::from(MESSAGE_STORAGE)
.join(format!("{tag}.json")); // The file inside that directory
// 3. Create all necessary parent directories recursively (async operation)
if let Err(e) = tokio::fs::create_dir_all(&MESSAGE_STORAGE).await {
eprintln!(
"Failed to create directory {}: {}",
MESSAGE_STORAGE,
e
);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag }));
}
// 4. Open the file asynchronously
let mut f = match tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(&file_path)
.await
{
Ok(file) => file,
Err(e) => {
eprintln!("Failed to open file {}: {}", file_path.display(), e);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag }));
}
};
// 5. Serialize and write the data asynchronously
// We append a newline character for standard text file formatting
let s = serde_json::to_string(&msg).expect("failed to serialize boxed message");
if let Err(e) = f.write_all(format!("{}\n", s).as_bytes()).await {
eprintln!("Failed to write message to file: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag }));
}
(StatusCode::CREATED, Json(UserCreated { tag }))
}
fn select_key(pks: &Vec<Vec<u8>>) -> String {
let idx = getrandom::u64().expect("random integer") as usize % pks.len();
serde_json::to_string(&pks[idx]).expect("serialize vec of bytes")
}
fn serve_path(path: impl AsRef<std::path::Path>) -> Result<Html<String>> {
let mut s = String::new();
OpenOptions::new()
.read(true)
.write(false)
.open(path)?
.read_to_string(&mut s)?;
Ok(Html(s))
}