veloren_server_cli/web/ui/
api.rs1use 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#[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
50async 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
67pub 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}