open_chess_server/http/
mod.rs1pub mod account;
4pub mod app_files;
5pub mod challenges;
6pub mod fairplay;
7pub mod feed;
8pub mod games;
9pub mod lab;
10pub mod oauth;
11pub mod play;
12pub mod streams;
13pub mod web;
14
15use std::convert::Infallible;
16
17use axum::Json;
18use axum::body::Body;
19use axum::extract::State;
20use axum::http::{HeaderValue, header};
21use axum::response::{IntoResponse, Response};
22use bytes::Bytes;
23use serde::Serialize;
24use serde_json::json;
25use tokio::sync::mpsc;
26
27use crate::app::Ctx;
28
29pub const LOGO_SVG: &str = include_str!("logo.svg");
31
32pub fn logo(size: u32) -> String {
34 LOGO_SVG.replacen("<svg ", &format!("<svg width=\"{size}\" height=\"{size}\" aria-hidden=\"true\" "), 1)
35}
36
37pub async fn favicon() -> Response {
40 (
41 [
42 (header::CONTENT_TYPE, HeaderValue::from_static("image/svg+xml")),
43 (header::CACHE_CONTROL, HeaderValue::from_static("public, max-age=86400")),
44 ],
45 LOGO_SVG,
46 )
47 .into_response()
48}
49
50pub async fn docs(State(ctx): State<Ctx>, guide: Option<axum::extract::Path<String>>) -> Response {
53 let name = guide
54 .map(|g| g.0.trim_end_matches(".md").to_owned())
55 .filter(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-'));
56 let to = if ctx.cfg.app_dir.as_deref().is_some_and(|d| !d.is_empty()) {
57 let base = ctx.cfg.base_path().unwrap_or_default();
58 match name {
59 Some(n) => format!("{base}/app/#docs/{n}"),
60 None => format!("{base}/app/#docs"),
61 }
62 } else {
63 let repo = "https://github.com/OffeneDatenmodellierung/open-chess/blob/main/docs";
64 match name {
65 Some(n) => format!("{repo}/{n}.md"),
66 None => repo.to_owned(),
67 }
68 };
69 axum::response::Redirect::to(&to).into_response()
70}
71
72pub async fn index(State(ctx): State<Ctx>, headers: axum::http::HeaderMap) -> Response {
75 if app_files::wants_html(&headers) && ctx.cfg.app_dir.as_deref().is_some_and(|d| !d.is_empty()) {
76 let base = ctx.cfg.base_path().unwrap_or_default();
77 return axum::response::Redirect::to(&format!("{base}/app/")).into_response();
78 }
79 Json(json!({
80 "name": "open-chess",
81 "site": ctx.cfg.site_name,
82 "api": "Lichess-compatible: Board, Bot, Challenge, OAuth (PKCE), event and game streams, game export",
83 "url": ctx.cfg.public_url(),
84 "version": env!("CARGO_PKG_VERSION"),
85 "login": true,
89 "signup": ctx.cfg.signup,
90 "esports": ctx.cfg.esports().is_some(),
91 "app": ctx.cfg.app_dir.as_deref().is_some_and(|d| !d.is_empty()).then(|| format!("{}/app/", ctx.cfg.public_url())),
92 "docs": format!("{}/docs", ctx.cfg.public_url()),
93 }))
94 .into_response()
95}
96
97pub fn ok() -> Response {
98 Json(chess_proto::OK).into_response()
99}
100
101pub fn line<T: Serialize>(v: &T) -> Bytes {
103 let mut b = serde_json::to_vec(v).unwrap_or_default();
104 b.push(b'\n');
105 Bytes::from(b)
106}
107
108pub fn ndjson(rx: mpsc::Receiver<Bytes>) -> Response {
111 let stream = futures_util::stream::unfold(rx, |mut rx| async move {
112 rx.recv().await.map(|b| (Ok::<Bytes, Infallible>(b), rx))
113 });
114 let mut r = Body::from_stream(stream).into_response();
115 let h = r.headers_mut();
116 h.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/x-ndjson"));
117 h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
118 h.insert("x-accel-buffering", HeaderValue::from_static("no"));
120 r
121}
122
123pub fn form(body: &[u8]) -> std::collections::HashMap<String, String> {
125 url::form_urlencoded::parse(body).into_owned().collect()
126}
127
128pub fn html_escape(s: &str) -> String {
129 s.replace('&', "&")
130 .replace('<', "<")
131 .replace('>', ">")
132 .replace('"', """)
133 .replace('\'', "'")
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn escapes() {
142 assert_eq!(html_escape("<a href=\"x\">&'"), "<a href="x">&'");
143 }
144
145 #[test]
146 fn logo_is_the_apps_icon() {
147 let app = concat!(env!("CARGO_MANIFEST_DIR"), "/../../web/chess/icon.svg");
150 if let Ok(icon) = std::fs::read_to_string(app) {
151 assert_eq!(icon, LOGO_SVG, "copy web/chess/icon.svg to crates/chess-server/src/http/logo.svg");
152 }
153 assert!(logo(36).starts_with("<svg width=\"36\" height=\"36\""));
154 }
155
156 #[test]
157 fn lines_end_in_newline() {
158 assert_eq!(&line(&json!({"a": 1}))[..], b"{\"a\":1}\n");
159 }
160}