fix: CI workflow, password reset flow, health/limits in services, session refactor, dead mixin stub cleanup
This commit is contained in:
parent
45dd592c40
commit
f4e15b45c9
95 changed files with 899 additions and 185 deletions
|
|
@ -10,9 +10,10 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
tower-http = { version = "0.7", features = ["trace"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
pub mod config;
|
||||
pub mod presence;
|
||||
|
||||
use axum::{Router, extract::DefaultBodyLimit, routing::{get, post}};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{get, post},
|
||||
};
|
||||
use common::internal::{InternalKey, require_internal_key};
|
||||
use config::Config;
|
||||
use presence::store::Store;
|
||||
use std::sync::Arc;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
/// Presence requests are tiny JSON (a ≤64-char payload and ≤40 uuids).
|
||||
const MAX_BODY_BYTES: usize = 8 * 1024;
|
||||
|
|
@ -22,6 +27,7 @@ pub fn build_app(cfg: &Config) -> (Router, Arc<Store>) {
|
|||
));
|
||||
let app = Router::new()
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.merge(api);
|
||||
.merge(api)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
(app, store)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
let cfg = chat_service::config::Config::from_env()?;
|
||||
cfg.validate()?;
|
||||
let (app, state) = chat_service::build_app(&cfg);
|
||||
|
|
|
|||
|
|
@ -63,15 +63,23 @@ pub async fn sync(
|
|||
}
|
||||
let now = Instant::now();
|
||||
if req.publish
|
||||
&& let Err(PublishError::Claimed) = store.publish(id.account_id, &server, req.mc, req.gui, now)
|
||||
&& let Err(PublishError::Claimed) =
|
||||
store.publish(id.account_id, &server, req.mc, req.gui, now)
|
||||
{
|
||||
return error(StatusCode::CONFLICT, "player uuid is bound to another account");
|
||||
return error(
|
||||
StatusCode::CONFLICT,
|
||||
"player uuid is bound to another account",
|
||||
);
|
||||
}
|
||||
let players: Vec<PlayerState> = store
|
||||
.lookup(&server, &req.want, now)
|
||||
.into_iter()
|
||||
.filter(|s| s.mc != req.mc)
|
||||
.map(|s| PlayerState { mc: s.mc, gui: s.gui, age: s.age_ms })
|
||||
.map(|s| PlayerState {
|
||||
mc: s.mc,
|
||||
gui: s.gui,
|
||||
age: s.age_ms,
|
||||
})
|
||||
.collect();
|
||||
Json(json!({ "players": players })).into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ pub mod handlers;
|
|||
pub mod payload;
|
||||
pub mod store;
|
||||
|
||||
use std::{sync::Arc, time::{Duration, Instant}};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use store::Store;
|
||||
|
||||
pub fn spawn_purger(store: Arc<Store>) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ pub const MAX_WANT: usize = 40;
|
|||
pub fn valid_payload(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.len() <= MAX_PAYLOAD_CHARS
|
||||
&& s.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
|
||||
&& s.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
|
||||
}
|
||||
|
||||
/// Normalised server key: trimmed, lower-case, no control characters, bounded.
|
||||
|
|
@ -38,7 +39,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn server_is_normalised() {
|
||||
assert_eq!(normalize_server(" Play.Example.COM ").as_deref(), Some("play.example.com"));
|
||||
assert_eq!(
|
||||
normalize_server(" Play.Example.COM ").as_deref(),
|
||||
Some("play.example.com")
|
||||
);
|
||||
assert_eq!(normalize_server(""), None);
|
||||
assert_eq!(normalize_server("a\nb"), None);
|
||||
assert_eq!(normalize_server(&"x".repeat(65)), None);
|
||||
|
|
|
|||
|
|
@ -97,7 +97,9 @@ impl Store {
|
|||
|
||||
pub fn purge(&self, now: Instant) {
|
||||
let mut inner = self.inner.lock().expect("presence lock");
|
||||
inner.entries.retain(|_, e| now.duration_since(e.updated) < ENTRY_TTL);
|
||||
inner
|
||||
.entries
|
||||
.retain(|_, e| now.duration_since(e.updated) < ENTRY_TTL);
|
||||
let expired: Vec<Uuid> = inner
|
||||
.claims
|
||||
.iter()
|
||||
|
|
@ -117,7 +119,12 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn ids() -> (Uuid, Uuid, Uuid, Uuid) {
|
||||
(Uuid::from_u128(1), Uuid::from_u128(2), Uuid::from_u128(10), Uuid::from_u128(20))
|
||||
(
|
||||
Uuid::from_u128(1),
|
||||
Uuid::from_u128(2),
|
||||
Uuid::from_u128(10),
|
||||
Uuid::from_u128(20),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -125,7 +132,9 @@ mod tests {
|
|||
let (acc, _, mc, _) = ids();
|
||||
let store = Store::default();
|
||||
let t0 = Instant::now();
|
||||
store.publish(acc, "srv", mc, Some("AAAA".into()), t0).unwrap();
|
||||
store
|
||||
.publish(acc, "srv", mc, Some("AAAA".into()), t0)
|
||||
.unwrap();
|
||||
let seen = store.lookup("srv", &[mc], t0 + Duration::from_millis(500));
|
||||
assert_eq!(seen.len(), 1);
|
||||
assert_eq!(seen[0].gui.as_deref(), Some("AAAA"));
|
||||
|
|
@ -149,7 +158,10 @@ mod tests {
|
|||
let store = Store::default();
|
||||
let t0 = Instant::now();
|
||||
store.publish(acc, "s", mc, None, t0).unwrap();
|
||||
assert_eq!(store.publish(other, "s", mc, None, t0 + Duration::from_secs(1)), Err(PublishError::Claimed));
|
||||
assert_eq!(
|
||||
store.publish(other, "s", mc, None, t0 + Duration::from_secs(1)),
|
||||
Err(PublishError::Claimed)
|
||||
);
|
||||
// after the claim goes idle it can be taken over
|
||||
assert!(store.publish(other, "s", mc, None, t0 + CLAIM_TTL).is_ok());
|
||||
}
|
||||
|
|
@ -183,6 +195,10 @@ mod tests {
|
|||
let t0 = Instant::now();
|
||||
store.publish(acc, "s", mc, None, t0).unwrap();
|
||||
store.purge(t0 + CLAIM_TTL);
|
||||
assert!(store.publish(Uuid::from_u128(2), "s", mc, None, t0 + CLAIM_TTL).is_ok());
|
||||
assert!(
|
||||
store
|
||||
.publish(Uuid::from_u128(2), "s", mc, None, t0 + CLAIM_TTL)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ use uuid::Uuid;
|
|||
const KEY: &str = "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk";
|
||||
|
||||
fn server() -> TestServer {
|
||||
let cfg = Config { port: 0, internal_key: KEY.into() };
|
||||
let cfg = Config {
|
||||
port: 0,
|
||||
internal_key: KEY.into(),
|
||||
};
|
||||
let (app, _) = chat_service::build_app(&cfg);
|
||||
TestServer::new(app)
|
||||
}
|
||||
|
|
@ -37,10 +40,20 @@ async fn requires_internal_key_and_identity() {
|
|||
async fn two_players_see_each_other() {
|
||||
let s = server();
|
||||
let (a, b) = (Uuid::from_u128(10), Uuid::from_u128(20));
|
||||
let r = sync(&s, 1, json!({"server": "Play.X", "mc": a, "gui": "AQEAAAA=", "want": [b]})).await;
|
||||
let r = sync(
|
||||
&s,
|
||||
1,
|
||||
json!({"server": "Play.X", "mc": a, "gui": "AQEAAAA=", "want": [b]}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(r.status_code(), 200);
|
||||
assert_eq!(r.json::<Value>()["players"].as_array().unwrap().len(), 0);
|
||||
let r = sync(&s, 2, json!({"server": "play.x", "mc": b, "gui": null, "want": [a]})).await;
|
||||
let r = sync(
|
||||
&s,
|
||||
2,
|
||||
json!({"server": "play.x", "mc": b, "gui": null, "want": [a]}),
|
||||
)
|
||||
.await;
|
||||
let body = r.json::<Value>();
|
||||
let players = body["players"].as_array().unwrap();
|
||||
assert_eq!(players.len(), 1);
|
||||
|
|
@ -56,7 +69,12 @@ async fn rejects_bad_input_and_uuid_theft() {
|
|||
assert_eq!(r.status_code(), 400);
|
||||
let r = sync(&s, 1, json!({"server": "", "mc": mc})).await;
|
||||
assert_eq!(r.status_code(), 400);
|
||||
assert_eq!(sync(&s, 1, json!({"server": "x", "mc": mc})).await.status_code(), 200);
|
||||
assert_eq!(
|
||||
sync(&s, 1, json!({"server": "x", "mc": mc}))
|
||||
.await
|
||||
.status_code(),
|
||||
200
|
||||
);
|
||||
let r = sync(&s, 2, json!({"server": "x", "mc": mc})).await;
|
||||
assert_eq!(r.status_code(), 409);
|
||||
}
|
||||
|
|
@ -65,7 +83,12 @@ async fn rejects_bad_input_and_uuid_theft() {
|
|||
async fn read_only_poll_does_not_publish() {
|
||||
let s = server();
|
||||
let (a, b) = (Uuid::from_u128(10), Uuid::from_u128(20));
|
||||
sync(&s, 1, json!({"server": "x", "mc": a, "publish": false, "want": [b]})).await;
|
||||
sync(
|
||||
&s,
|
||||
1,
|
||||
json!({"server": "x", "mc": a, "publish": false, "want": [b]}),
|
||||
)
|
||||
.await;
|
||||
let r = sync(&s, 2, json!({"server": "x", "mc": b, "want": [a]})).await;
|
||||
assert_eq!(r.json::<Value>()["players"].as_array().unwrap().len(), 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue