1use {
2 axum::{
3 Json, Router,
4 body::Body,
5 extract::State,
6 http::{Method, Request, StatusCode, header::AUTHORIZATION},
7 middleware::{Next, from_fn},
8 response::Response,
9 routing::{get, post},
10 },
11 clap::{Args, Parser, Subcommand},
12 color_eyre::Result,
13 glues_core::{
14 Task, Transition,
15 backend::{
16 CoreBackend,
17 local::Db,
18 proxy::{ProxyServer, request::ProxyRequest, response::ProxyResponse},
19 },
20 handle_tasks,
21 },
22 std::{
23 collections::VecDeque,
24 net::SocketAddr,
25 sync::{
26 Arc, Mutex,
27 mpsc::{Sender, channel},
28 },
29 time::Duration,
30 },
31 tokio::{net::TcpListener, signal, sync::Mutex as AsyncMutex, time::sleep},
32 tower_http::cors::{Any, CorsLayer},
33 tracing::{error, info, warn},
34 tracing_subscriber::EnvFilter,
35};
36
37#[derive(Clone, Args)]
38pub struct ServerArgs {
39 #[arg(long, default_value = "127.0.0.1:4000")]
40 pub listen: SocketAddr,
41
42 #[arg(long, env = "GLUES_SERVER_TOKEN")]
43 pub auth_token: Option<String>,
44
45 #[command(subcommand)]
46 pub storage: StorageCommand,
47}
48
49#[derive(Parser)]
50#[command(author, version, about = "Glues proxy server")]
51struct Cli {
52 #[command(flatten)]
53 args: ServerArgs,
54}
55
56#[derive(Subcommand, Clone)]
57pub enum StorageCommand {
58 Memory,
60 File { path: String },
62 Redb { path: String },
64 Git {
66 path: String,
67 remote: String,
68 branch: String,
69 },
70 Mongo { conn_str: String, db_name: String },
72}
73
74pub fn parse_args() -> ServerArgs {
75 Cli::parse().args
76}
77
78pub async fn run(args: ServerArgs) -> Result<()> {
79 color_eyre::install()?;
80 tracing_subscriber::fmt()
81 .with_env_filter(EnvFilter::from_default_env().add_directive("info".parse()?))
82 .with_target(false)
83 .init();
84
85 let ServerArgs {
86 listen,
87 auth_token,
88 storage,
89 } = args;
90
91 let (task_tx, task_rx) = channel();
92 let transition_queue = Arc::new(Mutex::new(VecDeque::<Transition>::new()));
93 let _task_handle = handle_tasks(task_rx, &transition_queue);
94 spawn_transition_drain(Arc::clone(&transition_queue));
95
96 let backend = build_backend(storage, task_tx).await?;
97 let server = Arc::new(AsyncMutex::new(ProxyServer::new(backend)));
98
99 let cors = CorsLayer::new()
100 .allow_origin(Any)
101 .allow_methods(Any)
102 .allow_headers(Any);
103
104 let mut app = Router::new()
105 .route("/", post(handle_proxy))
106 .route("/health", get(health))
107 .with_state(server.clone())
108 .layer(cors);
109
110 if let Some(token) = auth_token.as_ref() {
111 info!("authentication token required for proxy requests");
112 let token = Arc::new(token.clone());
113 let auth_layer = from_fn(move |req, next| {
114 let token = Arc::clone(&token);
115 async move { enforce_bearer(token, req, next).await }
116 });
117 app = app.layer(auth_layer);
118 } else if !listen.ip().is_loopback() {
119 warn!(
120 "proxy server is listening on {listen} without authentication; set GLUES_SERVER_TOKEN or --auth-token to protect access"
121 );
122 }
123
124 let listener = TcpListener::bind(listen).await?;
125 info!("listening on {}", listen);
126
127 axum::serve(listener, app)
128 .with_graceful_shutdown(shutdown_signal())
129 .await?;
130
131 Ok(())
132}
133
134pub async fn run_cli() -> Result<()> {
135 run(parse_args()).await
136}
137
138async fn build_backend(
139 storage: StorageCommand,
140 task_tx: Sender<Task>,
141) -> Result<Box<dyn CoreBackend + Send>> {
142 let backend: Box<dyn CoreBackend + Send> = match storage {
143 StorageCommand::Memory => Box::new(Db::memory(task_tx.clone()).await?),
144 StorageCommand::File { path } => Box::new(Db::file(task_tx.clone(), &path).await?),
145 StorageCommand::Redb { path } => Box::new(Db::redb(task_tx.clone(), &path).await?),
146 StorageCommand::Git {
147 path,
148 remote,
149 branch,
150 } => Box::new(Db::git(task_tx.clone(), &path, remote, branch).await?),
151 StorageCommand::Mongo { conn_str, db_name } => {
152 Box::new(Db::mongo(task_tx, &conn_str, &db_name).await?)
153 }
154 };
155
156 Ok(backend)
157}
158
159async fn handle_proxy(
160 State(server): State<Arc<AsyncMutex<ProxyServer>>>,
161 Json(request): Json<ProxyRequest>,
162) -> (StatusCode, Json<ProxyResponse>) {
163 let mut server = server.lock_owned().await;
164 let response = server.handle(request).await;
165 (StatusCode::OK, Json(response))
166}
167
168async fn health() -> StatusCode {
169 StatusCode::OK
170}
171
172fn spawn_transition_drain(queue: Arc<Mutex<VecDeque<Transition>>>) {
173 tokio::spawn(async move {
174 loop {
175 {
176 let mut guard = queue.lock().expect("transition queue poisoned");
177 guard.clear();
178 }
179 sleep(Duration::from_millis(500)).await;
180 }
181 });
182}
183
184async fn shutdown_signal() {
185 if let Err(err) = signal::ctrl_c().await {
186 error!("failed to install Ctrl+C handler: {err}");
187 return;
188 }
189
190 info!("shutting down");
191}
192
193async fn enforce_bearer(
194 token: Arc<String>,
195 req: Request<Body>,
196 next: Next,
197) -> Result<Response, StatusCode> {
198 if req.method() == Method::OPTIONS {
199 return Ok(next.run(req).await);
200 }
201
202 let Some(header) = req.headers().get(AUTHORIZATION) else {
203 return Err(StatusCode::UNAUTHORIZED);
204 };
205 let Ok(value) = header.to_str() else {
206 return Err(StatusCode::UNAUTHORIZED);
207 };
208 let Some(provided) = value.strip_prefix("Bearer ").map(str::trim) else {
209 return Err(StatusCode::UNAUTHORIZED);
210 };
211 if provided != token.as_str() {
212 return Err(StatusCode::UNAUTHORIZED);
213 }
214
215 Ok(next.run(req).await)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use axum::{Router, routing::get};
222 use tower::ServiceExt;
223
224 async fn ok() -> StatusCode {
225 StatusCode::OK
226 }
227
228 #[tokio::test]
229 async fn options_requests_bypass_auth() {
230 let cors = CorsLayer::new()
231 .allow_origin(Any)
232 .allow_methods(Any)
233 .allow_headers(Any);
234
235 let token = Arc::new("secret".to_owned());
236 let app = Router::new()
237 .route("/", get(ok))
238 .layer(cors)
239 .layer(from_fn(move |req, next| {
240 let token = Arc::clone(&token);
241 async move { enforce_bearer(token, req, next).await }
242 }));
243
244 let response = app
245 .oneshot(
246 Request::builder()
247 .method(Method::OPTIONS)
248 .uri("/")
249 .body(Body::empty())
250 .unwrap(),
251 )
252 .await
253 .expect("preflight request should succeed");
254
255 assert_ne!(response.status(), StatusCode::UNAUTHORIZED);
256 }
257
258 #[tokio::test]
259 async fn missing_token_still_rejected_for_non_preflight() {
260 let cors = CorsLayer::new()
261 .allow_origin(Any)
262 .allow_methods(Any)
263 .allow_headers(Any);
264
265 let token = Arc::new("secret".to_owned());
266 let app = Router::new()
267 .route("/", get(ok))
268 .layer(cors)
269 .layer(from_fn(move |req, next| {
270 let token = Arc::clone(&token);
271 async move { enforce_bearer(token, req, next).await }
272 }));
273
274 let response = app
275 .oneshot(
276 Request::builder()
277 .method(Method::GET)
278 .uri("/")
279 .body(Body::empty())
280 .unwrap(),
281 )
282 .await
283 .expect("request should succeed");
284
285 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
286 }
287}