fix: message sanitize, CORS policy (#5)
This commit is contained in:
parent
d2e9f37e6e
commit
4200ca77df
3 changed files with 70 additions and 22 deletions
|
|
@ -10,7 +10,7 @@ path = "bin/main.rs"
|
|||
[dependencies]
|
||||
axum = "0.8.7"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs", "cors", "limit"] }
|
||||
serde_json = "1.0.145"
|
||||
|
||||
message-tools = { path = "../message-tools" }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use axum::{
|
||||
Json, Router,
|
||||
http::StatusCode,
|
||||
http::{Method, StatusCode},
|
||||
response::Html,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
|
@ -10,17 +10,22 @@ use std::{
|
|||
fs::OpenOptions,
|
||||
io::{BufRead, Read},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer, 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");
|
||||
static ref PUBLISH_LIMITER: std::sync::Mutex<(std::time::Instant, u32)> =
|
||||
std::sync::Mutex::new((std::time::Instant::now(), 0));
|
||||
}
|
||||
|
||||
const MAX_PUBLISHES_PER_MINUTE: u32 = 10;
|
||||
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
|
@ -59,16 +64,31 @@ fn load_public_keys(path: impl AsRef<Path>) -> std::io::Result<Vec<Vec<u8>>> {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// build our application with a single route
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin([
|
||||
"https://zachery.lol"
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.unwrap(),
|
||||
"https://www.zachery.lol"
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.unwrap(),
|
||||
])
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
.allow_headers([axum::http::header::CONTENT_TYPE]);
|
||||
|
||||
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))
|
||||
.route(
|
||||
"/api/publish",
|
||||
post(publish_message).layer(RequestBodyLimitLayer::new(64 * 1024)),
|
||||
)
|
||||
.nest_service("/gossip-dashboard", ServeDir::new("./routes/gossip-dashboard"))
|
||||
.nest_service("/routes", ServeDir::new("./routes"));
|
||||
.nest_service("/routes", ServeDir::new("./routes"))
|
||||
.layer(cors);
|
||||
|
||||
let addr = "0.0.0.0:3000";
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
|
|
@ -84,13 +104,40 @@ 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
|
||||
/// Strip everything except ASCII alphanumerics, dashes, and underscores
|
||||
/// so a tag can never escape the storage directory.
|
||||
fn sanitize_tag(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 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
|
||||
async fn publish_message(Json(msg): Json<BoxedMessage>) -> (StatusCode, Json<UserCreated>) {
|
||||
// Rate limit: sliding window of MAX_PUBLISHES_PER_MINUTE
|
||||
{
|
||||
let mut limiter = PUBLISH_LIMITER.lock().unwrap();
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(limiter.0) > Duration::from_secs(60) {
|
||||
*limiter = (now, 0);
|
||||
}
|
||||
if limiter.1 >= MAX_PUBLISHES_PER_MINUTE {
|
||||
return (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
Json(UserCreated {
|
||||
tag: String::new(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
limiter.1 += 1;
|
||||
}
|
||||
|
||||
let tag = sanitize_tag(&msg.tag());
|
||||
|
||||
if tag.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(UserCreated { tag }));
|
||||
}
|
||||
|
||||
let file_path = PathBuf::from(MESSAGE_STORAGE).join(format!("{tag}.json"));
|
||||
|
||||
// 3. Create all necessary parent directories recursively (async operation)
|
||||
if let Err(e) = tokio::fs::create_dir_all(&MESSAGE_STORAGE).await {
|
||||
|
|
@ -128,9 +175,18 @@ async fn publish_message(Json(msg): Json<BoxedMessage>) -> (StatusCode, Json<Use
|
|||
(StatusCode::CREATED, Json(UserCreated { tag }))
|
||||
}
|
||||
|
||||
fn select_key(pks: &Vec<Vec<u8>>) -> String {
|
||||
fn select_key(pks: &[Vec<u8>]) -> (StatusCode, String) {
|
||||
if pks.is_empty() {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"no keys available".to_string(),
|
||||
);
|
||||
}
|
||||
let idx = getrandom::u64().expect("random integer") as usize % pks.len();
|
||||
serde_json::to_string(&pks[idx]).expect("serialize vec of bytes")
|
||||
(
|
||||
StatusCode::OK,
|
||||
serde_json::to_string(&pks[idx]).expect("serialize vec of bytes"),
|
||||
)
|
||||
}
|
||||
|
||||
fn serve_path(path: impl AsRef<std::path::Path>) -> Result<Html<String>> {
|
||||
|
|
|
|||
|
|
@ -122,8 +122,6 @@
|
|||
await wasm_bindgen();
|
||||
await fetchPublicKey();
|
||||
|
||||
console.log(`Server pk: ${serverPublicKey}`);
|
||||
|
||||
const form = document.getElementById("message-form");
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -148,7 +146,6 @@
|
|||
}
|
||||
|
||||
async function submit() {
|
||||
console.log("begin");
|
||||
const user = document.getElementById("name").value;
|
||||
const password = document.getElementById("password").value;
|
||||
const message = document.getElementById("message").value;
|
||||
|
|
@ -162,7 +159,6 @@
|
|||
};
|
||||
|
||||
const keyBytes = make_key_bytes(JSON.stringify(makeKeyBytesArgs));
|
||||
console.log(keyBytes);
|
||||
|
||||
const boxMessageArgs = {
|
||||
user: String(user),
|
||||
|
|
@ -171,10 +167,8 @@
|
|||
remote_pk: [...serverPublicKey],
|
||||
plaintext: String(message),
|
||||
};
|
||||
console.log("Boxed preflight: ", JSON.stringify(boxMessageArgs));
|
||||
const boxed = box_message(JSON.stringify(boxMessageArgs));
|
||||
const boxedObj = JSON.parse(boxed);
|
||||
console.log(boxedObj);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/publish", {
|
||||
|
|
@ -190,7 +184,6 @@
|
|||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log("Success:", result);
|
||||
alert("Sent!");
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
|
|
@ -198,7 +191,6 @@
|
|||
alert("Failed to send message: " + error.message);
|
||||
}
|
||||
|
||||
console.log("End");
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
|
|||
Loading…
Reference in a new issue