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}