veloren_server_cli/web/
chat.rs

1use axum::{
2    Json, Router,
3    extract::{ConnectInfo, Query, Request, State},
4    middleware::Next,
5    response::{IntoResponse, Response},
6    routing::get,
7};
8use chrono::DateTime;
9use hyper::StatusCode;
10use serde::{Deserialize, Deserializer};
11use server::chat::ChatCache;
12use std::{
13    collections::HashSet,
14    net::{IpAddr, SocketAddr},
15    str::FromStr,
16    sync::Arc,
17};
18use tokio::sync::Mutex;
19
20/// Keep Size small, so we dont have to Clone much for each request.
21#[derive(Clone)]
22struct ChatToken {
23    secret_token: Option<String>,
24}
25
26#[derive(Clone, Default)]
27struct IpAddresses {
28    users: Arc<Mutex<HashSet<IpAddr>>>,
29}
30
31async fn validate_secret(
32    State(token): State<ChatToken>,
33    req: Request,
34    next: Next,
35) -> Result<Response, StatusCode> {
36    // check if this endpoint is disabled
37    let secret_token = token.secret_token.ok_or(StatusCode::METHOD_NOT_ALLOWED)?;
38
39    pub const X_SECRET_TOKEN: &str = "X-Secret-Token";
40    let session_cookie = req
41        .headers()
42        .get(X_SECRET_TOKEN)
43        .ok_or(StatusCode::UNAUTHORIZED)?;
44
45    if session_cookie.as_bytes() != secret_token.as_bytes() {
46        return Err(StatusCode::UNAUTHORIZED);
47    }
48
49    Ok(next.run(req).await)
50}
51
52/// Logs each new IP address that accesses this API authenticated
53async fn log_users(
54    State(ip_addresses): State<IpAddresses>,
55    ConnectInfo(addr): ConnectInfo<SocketAddr>,
56    req: Request,
57    next: Next,
58) -> Result<Response, StatusCode> {
59    let mut ip_addresses = ip_addresses.users.lock().await;
60    let ip_addr = addr.ip();
61    if !ip_addresses.contains(&ip_addr) {
62        ip_addresses.insert(ip_addr);
63        let users_so_far = ip_addresses.len();
64        tracing::info!(?ip_addr, ?users_so_far, "Is accessing the /chat endpoint");
65    }
66    Ok(next.run(req).await)
67}
68
69pub fn router(cache: ChatCache, secret_token: Option<String>) -> Router {
70    let token = ChatToken { secret_token };
71    let ip_addrs = IpAddresses::default();
72    Router::new()
73        .route("/history", get(history))
74        .layer(axum::middleware::from_fn_with_state(ip_addrs, log_users))
75        .layer(axum::middleware::from_fn_with_state(token, validate_secret))
76        .with_state(cache)
77}
78
79#[derive(Debug, Deserialize)]
80struct Params {
81    #[serde(default, deserialize_with = "empty_string_as_none")]
82    /// To be used to get all messages without duplicates nor losing messages
83    from_time_exclusive_rfc3339: Option<String>,
84}
85
86fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
87where
88    D: Deserializer<'de>,
89    T: FromStr,
90    T::Err: core::fmt::Display,
91{
92    let opt = Option::<String>::deserialize(de)?;
93    match opt.as_deref() {
94        None | Some("") => Ok(None),
95        Some(s) => FromStr::from_str(s)
96            .map_err(serde::de::Error::custom)
97            .map(Some),
98    }
99}
100
101async fn history(
102    State(cache): State<ChatCache>,
103    Query(params): Query<Params>,
104) -> Result<impl IntoResponse, StatusCode> {
105    // first validate parameters before we take lock
106    let from_time_exclusive = if let Some(rfc3339) = params.from_time_exclusive_rfc3339 {
107        Some(DateTime::parse_from_rfc3339(&rfc3339).map_err(|_| StatusCode::BAD_REQUEST)?)
108    } else {
109        None
110    };
111
112    let messages = cache.messages.lock().await;
113    let filtered: Vec<_> = match from_time_exclusive {
114        Some(from_time_exclusive) => messages
115            .iter()
116            .filter(|msg| msg.time > from_time_exclusive)
117            .cloned()
118            .collect(),
119        None => messages.iter().cloned().collect(),
120    };
121    Ok(Json(filtered))
122}