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]
|
[dependencies]
|
||||||
axum = "0.8.7"
|
axum = "0.8.7"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
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"
|
serde_json = "1.0.145"
|
||||||
|
|
||||||
message-tools = { path = "../message-tools" }
|
message-tools = { path = "../message-tools" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
http::StatusCode,
|
http::{Method, StatusCode},
|
||||||
response::Html,
|
response::Html,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
@ -10,17 +10,22 @@ use std::{
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufRead, Read},
|
io::{BufRead, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::io::AsyncWriteExt;
|
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 PUBKEY_PATH: &str = "./pubkeys/public_keys.json";
|
||||||
const MESSAGE_STORAGE: &str = "./messages";
|
const MESSAGE_STORAGE: &str = "./messages";
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref PUBLIC_KEYS: Vec<Vec<u8>> = load_public_keys(PUBKEY_PATH).expect("static path");
|
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 Error = Box<dyn std::error::Error>;
|
||||||
type Result<T> = std::result::Result<T, 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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
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()
|
let app = Router::new()
|
||||||
.route("/", get(serve_path("./routes/root/index.html")?))
|
.route("/", get(serve_path("./routes/root/index.html")?))
|
||||||
.route("/who", get(serve_path("./routes/who/index.html")?))
|
.route("/who", get(serve_path("./routes/who/index.html")?))
|
||||||
.route("/api/pubkey", get(async || select_key(&PUBLIC_KEYS)))
|
.route("/api/pubkey", get(async || select_key(&PUBLIC_KEYS)))
|
||||||
.route("/contact", get(serve_path("./routes/contact/index.html")?))
|
.route("/contact", get(serve_path("./routes/contact/index.html")?))
|
||||||
.route("/contact/message", get(serve_path("./routes/contact/message/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("/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 addr = "0.0.0.0:3000";
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
|
@ -84,13 +104,40 @@ struct UserCreated {
|
||||||
tag: String,
|
tag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: sanitation of boxed messages
|
/// Strip everything except ASCII alphanumerics, dashes, and underscores
|
||||||
async fn publish_message(Json(msg): Json<BoxedMessage>) -> (StatusCode, Json<UserCreated>) {
|
/// so a tag can never escape the storage directory.
|
||||||
let tag = msg.tag(); // Capture tag early
|
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"
|
async fn publish_message(Json(msg): Json<BoxedMessage>) -> (StatusCode, Json<UserCreated>) {
|
||||||
let file_path = PathBuf::from(MESSAGE_STORAGE)
|
// Rate limit: sliding window of MAX_PUBLISHES_PER_MINUTE
|
||||||
.join(format!("{tag}.json")); // The file inside that directory
|
{
|
||||||
|
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)
|
// 3. Create all necessary parent directories recursively (async operation)
|
||||||
if let Err(e) = tokio::fs::create_dir_all(&MESSAGE_STORAGE).await {
|
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 }))
|
(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();
|
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>> {
|
fn serve_path(path: impl AsRef<std::path::Path>) -> Result<Html<String>> {
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,6 @@
|
||||||
await wasm_bindgen();
|
await wasm_bindgen();
|
||||||
await fetchPublicKey();
|
await fetchPublicKey();
|
||||||
|
|
||||||
console.log(`Server pk: ${serverPublicKey}`);
|
|
||||||
|
|
||||||
const form = document.getElementById("message-form");
|
const form = document.getElementById("message-form");
|
||||||
form.addEventListener("submit", (event) => {
|
form.addEventListener("submit", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -148,7 +146,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
console.log("begin");
|
|
||||||
const user = document.getElementById("name").value;
|
const user = document.getElementById("name").value;
|
||||||
const password = document.getElementById("password").value;
|
const password = document.getElementById("password").value;
|
||||||
const message = document.getElementById("message").value;
|
const message = document.getElementById("message").value;
|
||||||
|
|
@ -162,7 +159,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyBytes = make_key_bytes(JSON.stringify(makeKeyBytesArgs));
|
const keyBytes = make_key_bytes(JSON.stringify(makeKeyBytesArgs));
|
||||||
console.log(keyBytes);
|
|
||||||
|
|
||||||
const boxMessageArgs = {
|
const boxMessageArgs = {
|
||||||
user: String(user),
|
user: String(user),
|
||||||
|
|
@ -171,10 +167,8 @@
|
||||||
remote_pk: [...serverPublicKey],
|
remote_pk: [...serverPublicKey],
|
||||||
plaintext: String(message),
|
plaintext: String(message),
|
||||||
};
|
};
|
||||||
console.log("Boxed preflight: ", JSON.stringify(boxMessageArgs));
|
|
||||||
const boxed = box_message(JSON.stringify(boxMessageArgs));
|
const boxed = box_message(JSON.stringify(boxMessageArgs));
|
||||||
const boxedObj = JSON.parse(boxed);
|
const boxedObj = JSON.parse(boxed);
|
||||||
console.log(boxedObj);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/publish", {
|
const response = await fetch("/api/publish", {
|
||||||
|
|
@ -190,7 +184,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Success:", result);
|
|
||||||
alert("Sent!");
|
alert("Sent!");
|
||||||
location.reload();
|
location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -198,7 +191,6 @@
|
||||||
alert("Failed to send message: " + error.message);
|
alert("Failed to send message: " + error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("End");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue