veloren_server_cli/web/ui/
api.rs

1use crate::cli::{Message, MessageReturn};
2use axum::{
3    Json, Router,
4    extract::{ConnectInfo, Request, State},
5    http::header::COOKIE,
6    middleware::Next,
7    response::{IntoResponse, Response},
8    routing::{get, post},
9};
10use hyper::StatusCode;
11use serde::Deserialize;
12use std::{
13    collections::HashSet,
14    net::{IpAddr, SocketAddr},
15    sync::Arc,
16};
17use tokio::sync::Mutex;
18
19/// Keep Size small, so we dont have to Clone much for each request.
20#[derive(Clone)]
21struct UiApiToken {
22    secret_token: String,
23}
24
25pub(crate) type UiRequestSender =
26    tokio::sync::mpsc::Sender<(Message, tokio::sync::oneshot::Sender<MessageReturn>)>;
27
28#[derive(Clone, Default)]
29struct IpAddresses {
30    users: Arc<Mutex<HashSet<IpAddr>>>,
31}
32
33async fn validate_secret(
34    State(token): State<UiApiToken>,
35    req: Request,
36    next: Next,
37) -> Result<Response, StatusCode> {
38    let session_cookie = req.headers().get(COOKIE).ok_or(StatusCode::UNAUTHORIZED)?;
39
40    pub const X_SECRET_TOKEN: &str = "X-Secret-Token";
41    let expected = format!("{X_SECRET_TOKEN}={}", token.secret_token);
42
43    if session_cookie.as_bytes() != expected.as_bytes() {
44        return Err(StatusCode::UNAUTHORIZED);
45    }
46
47    Ok(next.run(req).await)
48}
49
50/// Logs each new IP address that accesses this API authenticated
51async fn log_users(
52    State(ip_addresses): State<IpAddresses>,
53    ConnectInfo(addr): ConnectInfo<SocketAddr>,
54    req: Request,
55    next: Next,
56) -> Result<Response, StatusCode> {
57    let mut ip_addresses = ip_addresses.users.lock().await;
58    let ip_addr = addr.ip();
59    if !ip_addresses.contains(&ip_addr) {
60        ip_addresses.insert(ip_addr);
61        let users_so_far = ip_addresses.len();
62        tracing::info!(?ip_addr, ?users_so_far, "Is accessing the /ui_api endpoint");
63    }
64    Ok(next.run(req).await)
65}
66
67//TODO: do security audit before we extend this api with more security relevant
68// functionality (e.g. account management)
69pub fn router(web_ui_request_s: UiRequestSender, secret_token: String) -> Router {
70    let token = UiApiToken { secret_token };
71    let ip_addrs = IpAddresses::default();
72    Router::new()
73        .route("/players", get(players))
74        .route("/logs", get(logs))
75        .route("/send_global_msg", post(send_global_msg))
76        .layer(axum::middleware::from_fn_with_state(ip_addrs, log_users))
77        .layer(axum::middleware::from_fn_with_state(token, validate_secret))
78        .with_state(web_ui_request_s)
79}
80
81async fn players(
82    State(web_ui_request_s): State<UiRequestSender>,
83) -> Result<impl IntoResponse, StatusCode> {
84    let (sender, receiver) = tokio::sync::oneshot::channel();
85    let _ = web_ui_request_s.send((Message::ListPlayers, sender)).await;
86    match receiver
87        .await
88        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
89    {
90        MessageReturn::Players(players) => Ok(Json(players)),
91        _ => Err(StatusCode::INTERNAL_SERVER_ERROR),
92    }
93}
94
95async fn logs(
96    State(web_ui_request_s): State<UiRequestSender>,
97) -> Result<impl IntoResponse, StatusCode> {
98    let (sender, receiver) = tokio::sync::oneshot::channel();
99    let _ = web_ui_request_s.send((Message::ListLogs, sender)).await;
100    match receiver
101        .await
102        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
103    {
104        MessageReturn::Logs(logs) => Ok(Json(logs)),
105        _ => Err(StatusCode::INTERNAL_SERVER_ERROR),
106    }
107}
108
109#[derive(Deserialize)]
110struct SendWorldMsgBody {
111    msg: String,
112}
113
114async fn send_global_msg(
115    State(web_ui_request_s): State<UiRequestSender>,
116    Json(payload): Json<SendWorldMsgBody>,
117) -> Result<impl IntoResponse, StatusCode> {
118    let (dummy_s, _) = tokio::sync::oneshot::channel();
119    let _ = web_ui_request_s
120        .send((Message::SendGlobalMsg { msg: payload.msg }, dummy_s))
121        .await;
122    Ok(())
123}