Skip to main content

codex_sync/
web.rs

1use crate::{config::Config, discovery, sync};
2use anyhow::Result;
3use axum::{
4    Router,
5    body::Body,
6    extract::{Path, State},
7    http::{HeaderMap, StatusCode},
8    response::{Html, IntoResponse},
9    routing::{get, post},
10};
11use std::{net::SocketAddr, sync::Arc};
12use subtle::ConstantTimeEq;
13
14type AppState = Arc<Config>;
15
16pub async fn serve(cfg: Config) -> Result<()> {
17    let addr: SocketAddr = cfg.listen.parse()?;
18    let discovery_cfg = cfg.clone();
19    tokio::spawn(async move {
20        if let Err(e) = discovery::responder(discovery_cfg).await {
21            eprintln!("发现服务停止:{e}");
22        }
23    });
24    let state = Arc::new(cfg);
25    let app = Router::new()
26        .route("/", get(index))
27        .route("/api/status", get(status))
28        .route("/api/peers", get(peers))
29        .route("/api/manifest", get(manifest))
30        .route("/api/file/{*path}", get(file))
31        .route("/api/pull", post(pull))
32        .with_state(state);
33    println!("Codex Sync 已启动:http://{addr}");
34    axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await?;
35    Ok(())
36}
37
38fn authorized(headers: &HeaderMap, cfg: &Config) -> bool {
39    let expected = format!("Bearer {}", cfg.shared_key);
40    headers
41        .get("authorization")
42        .map(|actual| actual.as_bytes().ct_eq(expected.as_bytes()).into())
43        .unwrap_or(false)
44}
45
46async fn index() -> Html<&'static str> {
47    Html(include_str!("web.html"))
48}
49
50async fn status(State(cfg): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
51    if !authorized(&headers, &cfg) {
52        return StatusCode::UNAUTHORIZED.into_response();
53    }
54    status_response(&cfg)
55}
56
57fn status_response(cfg: &Config) -> axum::response::Response {
58    match sync::core::manifest(cfg) {
59        Ok(m) => axum::Json(m).into_response(),
60        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
61    }
62}
63
64async fn peers(State(cfg): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
65    if !authorized(&headers, &cfg) {
66        return StatusCode::UNAUTHORIZED.into_response();
67    }
68    match discovery::discover(&cfg, 2).await {
69        Ok(p) => axum::Json(p).into_response(),
70        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
71    }
72}
73
74async fn manifest(State(cfg): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
75    if !authorized(&headers, &cfg) {
76        return StatusCode::UNAUTHORIZED.into_response();
77    }
78    status_response(&cfg)
79}
80
81async fn file(
82    State(cfg): State<AppState>,
83    headers: HeaderMap,
84    Path(path): Path<String>,
85) -> impl IntoResponse {
86    if !authorized(&headers, &cfg) {
87        return StatusCode::UNAUTHORIZED.into_response();
88    }
89    match read_served_file(&cfg, &path) {
90        Ok(bytes) => Body::from(bytes).into_response(),
91        Err(_) => StatusCode::NOT_FOUND.into_response(),
92    }
93}
94
95fn read_served_file(cfg: &Config, path: &str) -> Result<Vec<u8>> {
96    sync::core::read_local_file(cfg, path)
97}
98
99#[derive(serde::Deserialize)]
100struct PullRequest {
101    peer: String,
102    overwrite: Option<bool>,
103}
104async fn pull(
105    State(cfg): State<AppState>,
106    headers: HeaderMap,
107    axum::Json(req): axum::Json<PullRequest>,
108) -> impl IntoResponse {
109    if !authorized(&headers, &cfg) {
110        return StatusCode::UNAUTHORIZED.into_response();
111    }
112    match sync::http::pull(&cfg, &req.peer, req.overwrite.unwrap_or(false)).await {
113        Ok(r) => axum::Json(r).into_response(),
114        Err(e) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use axum::http::{HeaderValue, header::AUTHORIZATION};
122
123    #[test]
124    fn every_request_needs_the_exact_shared_key() {
125        let cfg = Config {
126            shared_key: "a very long test key that exceeds thirty two chars".into(),
127            ..Config::default()
128        };
129        let mut valid = HeaderMap::new();
130        valid.insert(
131            AUTHORIZATION,
132            HeaderValue::from_static("Bearer a very long test key that exceeds thirty two chars"),
133        );
134        let mut invalid = HeaderMap::new();
135        invalid.insert(AUTHORIZATION, HeaderValue::from_static("Bearer wrong"));
136        assert!(authorized(&valid, &cfg));
137        assert!(!authorized(&invalid, &cfg));
138        assert!(!authorized(&HeaderMap::new(), &cfg));
139    }
140}