1use std::sync::Arc;
2use std::time::Instant;
3
4use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
5use tokio::net::TcpListener;
6use tokio_rustls::TlsAcceptor;
7use tracing::{debug, error, info, warn};
8
9use crate::auth::AuthManager;
10use crate::connection::{ConnectionConfig, ConnectionManager};
11use crate::handler::RequestHandler;
12use crate::metrics::ServerMetrics;
13use crate::protocol::{Request, Response};
14use crate::tls::{TlsConfig, extract_client_cn};
15
16#[derive(Debug, Clone)]
18pub struct ServerConfig {
19 pub bind_address: String,
20 pub port: u16,
21 pub connection: ConnectionConfig,
22 pub tls: Option<TlsConfig>,
24}
25
26impl Default for ServerConfig {
27 fn default() -> Self {
28 Self {
29 bind_address: "127.0.0.1".into(),
30 port: 7687,
31 connection: ConnectionConfig::default(),
32 tls: None,
33 }
34 }
35}
36
37pub struct AstraeaServer {
42 config: ServerConfig,
43 handler: Arc<RequestHandler>,
44 auth: Arc<AuthManager>,
45 metrics: Arc<ServerMetrics>,
46 connection_manager: Arc<ConnectionManager>,
47 tls_acceptor: Option<TlsAcceptor>,
48}
49
50impl AstraeaServer {
51 pub fn new(
56 config: ServerConfig,
57 handler: RequestHandler,
58 ) -> Result<Self, crate::tls::TlsError> {
59 let connection_manager = Arc::new(ConnectionManager::new(config.connection.clone()));
60
61 let tls_acceptor = if let Some(ref tls_config) = config.tls {
63 info!("TLS enabled, loading certificates...");
64 let acceptor = tls_config.build_acceptor()?;
65 info!(
66 "TLS configured: cert={}, require_client_cert={}",
67 tls_config.cert_path.display(),
68 tls_config.require_client_cert
69 );
70 Some(acceptor)
71 } else {
72 None
73 };
74
75 Ok(Self {
76 config,
77 handler: Arc::new(handler),
78 auth: Arc::new(AuthManager::disabled()),
79 metrics: Arc::new(ServerMetrics::new()),
80 connection_manager,
81 tls_acceptor,
82 })
83 }
84
85 pub fn new_without_tls(config: ServerConfig, handler: RequestHandler) -> Self {
88 let connection_manager = Arc::new(ConnectionManager::new(config.connection.clone()));
89 Self {
90 config,
91 handler: Arc::new(handler),
92 auth: Arc::new(AuthManager::disabled()),
93 metrics: Arc::new(ServerMetrics::new()),
94 connection_manager,
95 tls_acceptor: None,
96 }
97 }
98
99 pub fn with_auth(mut self, auth: AuthManager) -> Self {
101 self.auth = Arc::new(auth);
102 self
103 }
104
105 pub fn metrics(&self) -> &Arc<ServerMetrics> {
107 &self.metrics
108 }
109
110 pub fn connection_manager(&self) -> &Arc<ConnectionManager> {
112 &self.connection_manager
113 }
114
115 pub async fn run(&self) -> std::io::Result<()> {
117 let addr = format!("{}:{}", self.config.bind_address, self.config.port);
118 let listener = TcpListener::bind(&addr).await?;
119
120 if self.tls_acceptor.is_some() {
121 info!("AstraeaDB server listening on {} (TLS enabled)", addr);
122 } else {
123 info!("AstraeaDB server listening on {} (plaintext)", addr);
124 }
125
126 loop {
127 if self.connection_manager.is_shutting_down() {
129 info!("Server shutting down, stopping accept loop");
130 break;
131 }
132
133 let accept_result =
135 tokio::time::timeout(std::time::Duration::from_secs(1), listener.accept()).await;
136
137 let (stream, peer_addr) = match accept_result {
138 Ok(Ok(conn)) => conn,
139 Ok(Err(e)) => {
140 error!("Accept error: {}", e);
141 continue;
142 }
143 Err(_) => continue, };
145
146 let guard = match self.connection_manager.try_accept() {
148 Some(g) => g,
149 None => {
150 warn!("Connection limit reached, rejecting {}", peer_addr);
151 let mut stream = stream;
153 let msg = r#"{"status":"error","message":"server connection limit reached"}"#;
154 let _ = tokio::io::AsyncWriteExt::write_all(
155 &mut stream,
156 format!("{}\n", msg).as_bytes(),
157 )
158 .await;
159 continue;
160 }
161 };
162
163 self.metrics.connection_opened();
164 info!("New connection from {}", peer_addr);
165
166 let handler = Arc::clone(&self.handler);
167 let auth = Arc::clone(&self.auth);
168 let metrics = Arc::clone(&self.metrics);
169 let idle_timeout = self.connection_manager.idle_timeout();
170 let request_timeout = self.connection_manager.request_timeout();
171 let tls_acceptor = self.tls_acceptor.clone();
172
173 tokio::spawn(async move {
174 let result = if let Some(acceptor) = tls_acceptor {
175 match acceptor.accept(stream).await {
177 Ok(tls_stream) => {
178 let client_cn = tls_stream
180 .get_ref()
181 .1
182 .peer_certificates()
183 .and_then(|certs| extract_client_cn(certs));
184
185 if let Some(ref cn) = client_cn {
186 debug!("TLS client authenticated: CN={}", cn);
187 }
188
189 handle_connection(
190 tls_stream,
191 handler,
192 auth,
193 metrics.clone(),
194 idle_timeout,
195 request_timeout,
196 client_cn,
197 )
198 .await
199 }
200 Err(e) => {
201 error!("TLS handshake error from {}: {}", peer_addr, e);
202 Err(std::io::Error::other(e))
203 }
204 }
205 } else {
206 handle_connection(
208 stream,
209 handler,
210 auth,
211 metrics.clone(),
212 idle_timeout,
213 request_timeout,
214 None,
215 )
216 .await
217 };
218
219 if let Err(e) = result {
220 error!("Connection error from {}: {}", peer_addr, e);
221 }
222 metrics.connection_closed();
223 info!("Connection closed: {}", peer_addr);
224 drop(guard); });
226 }
227
228 info!("Waiting for in-flight connections to drain...");
230 self.connection_manager.wait_for_drain().await;
231 info!("All connections drained. Server stopped.");
232
233 Ok(())
234 }
235
236 pub fn is_tls_enabled(&self) -> bool {
238 self.tls_acceptor.is_some()
239 }
240}
241
242async fn handle_connection<S>(
243 stream: S,
244 handler: Arc<RequestHandler>,
245 auth: Arc<AuthManager>,
246 metrics: Arc<ServerMetrics>,
247 idle_timeout: std::time::Duration,
248 request_timeout: std::time::Duration,
249 _client_cn: Option<String>,
250) -> std::io::Result<()>
251where
252 S: AsyncRead + AsyncWrite + Unpin,
253{
254 let (reader, mut writer) = tokio::io::split(stream);
255 let mut reader = BufReader::new(reader);
256 let mut line = String::new();
257
258 loop {
259 line.clear();
260
261 let read_result = tokio::time::timeout(idle_timeout, reader.read_line(&mut line)).await;
263
264 let bytes_read = match read_result {
265 Ok(Ok(n)) => n,
266 Ok(Err(e)) => return Err(e),
267 Err(_) => {
268 let msg = r#"{"status":"error","message":"idle timeout"}"#;
270 let _ = writer.write_all(format!("{}\n", msg).as_bytes()).await;
271 return Ok(());
272 }
273 };
274
275 if bytes_read == 0 {
276 break; }
278
279 let trimmed = line.trim();
280 if trimmed.is_empty() {
281 continue;
282 }
283
284 let start = Instant::now();
285
286 let request = match serde_json::from_str::<Request>(trimmed) {
288 Ok(req) => req,
289 Err(e) => {
290 let response = Response::error(format!("invalid request: {e}"));
291 let mut response_json = serde_json::to_string(&response).unwrap_or_else(|_| {
292 r#"{"status":"error","message":"serialization failed"}"#.into()
293 });
294 response_json.push('\n');
295 writer.write_all(response_json.as_bytes()).await?;
296 continue;
297 }
298 };
299
300 let request_type = request_type_name(&request);
301
302 if auth.is_enabled() {
304 let auth_token = extract_auth_token(trimmed);
306 match auth_token {
307 Some(token) => {
308 if let Some(role) = auth.authenticate(token) {
309 if !AuthManager::authorize(role, request_type) {
310 auth.audit(token, role, request_type, false);
311 metrics.record_request(request_type);
312 metrics.record_error(request_type);
313 let response = Response::error(format!(
314 "access denied: role '{}' cannot perform '{}'",
315 role, request_type
316 ));
317 let mut rj = serde_json::to_string(&response).unwrap_or_default();
318 rj.push('\n');
319 writer.write_all(rj.as_bytes()).await?;
320 continue;
321 }
322 auth.audit(token, role, request_type, true);
323 } else {
324 metrics.record_request(request_type);
325 metrics.record_error(request_type);
326 let response = Response::error("invalid credentials");
327 let mut rj = serde_json::to_string(&response).unwrap_or_default();
328 rj.push('\n');
329 writer.write_all(rj.as_bytes()).await?;
330 continue;
331 }
332 }
333 None => {
334 metrics.record_request(request_type);
335 metrics.record_error(request_type);
336 let response = Response::error("authentication required: provide auth_token");
337 let mut rj = serde_json::to_string(&response).unwrap_or_default();
338 rj.push('\n');
339 writer.write_all(rj.as_bytes()).await?;
340 continue;
341 }
342 }
343 }
344
345 metrics.record_request(request_type);
346
347 let handler_ref = Arc::clone(&handler);
349 let response =
350 match tokio::time::timeout(request_timeout, async move { handler_ref.handle(request) })
351 .await
352 {
353 Ok(resp) => resp,
354 Err(_) => {
355 metrics.record_error(request_type);
356 Response::error("request timeout")
357 }
358 };
359
360 let duration = start.elapsed();
361 metrics.record_duration(request_type, duration);
362
363 if matches!(response, Response::Error { .. }) {
364 metrics.record_error(request_type);
365 }
366
367 let mut response_json = serde_json::to_string(&response)
368 .unwrap_or_else(|_| r#"{"status":"error","message":"serialization failed"}"#.into());
369 response_json.push('\n');
370 writer.write_all(response_json.as_bytes()).await?;
371 }
372
373 Ok(())
374}
375
376fn request_type_name(request: &Request) -> &'static str {
378 match request {
379 Request::CreateNode { .. } => "CreateNode",
380 Request::CreateEdge { .. } => "CreateEdge",
381 Request::GetNode { .. } => "GetNode",
382 Request::GetEdge { .. } => "GetEdge",
383 Request::UpdateNode { .. } => "UpdateNode",
384 Request::UpdateEdge { .. } => "UpdateEdge",
385 Request::DeleteNode { .. } => "DeleteNode",
386 Request::DeleteEdge { .. } => "DeleteEdge",
387 Request::Neighbors { .. } => "Neighbors",
388 Request::NeighborsAt { .. } => "NeighborsAt",
389 Request::Bfs { .. } => "Bfs",
390 Request::BfsAt { .. } => "BfsAt",
391 Request::ShortestPath { .. } => "ShortestPath",
392 Request::ShortestPathAt { .. } => "ShortestPathAt",
393 Request::VectorSearch { .. } => "VectorSearch",
394 Request::HybridSearch { .. } => "HybridSearch",
395 Request::SemanticNeighbors { .. } => "SemanticNeighbors",
396 Request::SemanticWalk { .. } => "SemanticWalk",
397 Request::Query { .. } => "Query",
398 Request::ExtractSubgraph { .. } => "ExtractSubgraph",
399 Request::GraphRag { .. } => "GraphRag",
400 Request::Dfs { .. } => "Dfs",
401 Request::DfsAt { .. } => "DfsAt",
402 Request::FindByLabel { .. } => "FindByLabel",
403 Request::DeleteByLabel { .. } => "DeleteByLabel",
404 Request::FindEdgeByType { .. } => "FindEdgeByType",
405 Request::RunPageRank { .. } => "RunPageRank",
406 Request::RunLouvain { .. } => "RunLouvain",
407 Request::RunConnectedComponents { .. } => "RunConnectedComponents",
408 Request::RunDegreeCentrality { .. } => "RunDegreeCentrality",
409 Request::RunBetweennessCentrality { .. } => "RunBetweennessCentrality",
410 Request::GraphStats => "GraphStats",
411 Request::GetSubgraph { .. } => "GetSubgraph",
412 Request::Ping => "Ping",
413 }
414}
415
416fn extract_auth_token(json: &str) -> Option<&str> {
419 let marker = "\"auth_token\":\"";
420 if let Some(start) = json.find(marker) {
421 let rest = &json[start + marker.len()..];
422 if let Some(end) = rest.find('"') {
423 return Some(&rest[..end]);
424 }
425 }
426 None
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn default_config() {
435 let config = ServerConfig::default();
436 assert_eq!(config.bind_address, "127.0.0.1");
437 assert_eq!(config.port, 7687);
438 assert_eq!(config.connection.max_connections, 1024);
439 }
440
441 #[test]
442 fn request_type_name_matches() {
443 let req = Request::Ping;
444 assert_eq!(request_type_name(&req), "Ping");
445
446 let req = Request::CreateNode {
447 labels: vec![],
448 properties: serde_json::json!({}),
449 embedding: None,
450 };
451 assert_eq!(request_type_name(&req), "CreateNode");
452 }
453
454 #[test]
455 fn extract_auth_token_works() {
456 let json = r#"{"type":"Ping","auth_token":"my-secret-key"}"#;
457 assert_eq!(extract_auth_token(json), Some("my-secret-key"));
458 }
459
460 #[test]
461 fn extract_auth_token_missing() {
462 let json = r#"{"type":"Ping"}"#;
463 assert_eq!(extract_auth_token(json), None);
464 }
465
466 #[test]
467 fn temporal_request_types() {
468 let req = Request::NeighborsAt {
469 id: 1,
470 direction: "outgoing".into(),
471 timestamp: 100,
472 edge_type: None,
473 };
474 assert_eq!(request_type_name(&req), "NeighborsAt");
475
476 let req = Request::BfsAt {
477 start: 1,
478 max_depth: 3,
479 timestamp: 100,
480 };
481 assert_eq!(request_type_name(&req), "BfsAt");
482
483 let req = Request::ShortestPathAt {
484 from: 1,
485 to: 2,
486 timestamp: 100,
487 weighted: false,
488 };
489 assert_eq!(request_type_name(&req), "ShortestPathAt");
490 }
491
492 #[tokio::test]
500 async fn tcp_and_grpc_vector_search_return_matching_distance() {
501 use astraea_core::traits::VectorIndex;
502 use astraea_core::types::{DistanceMetric, NodeId};
503 use astraea_graph::Graph;
504 use astraea_graph::test_utils::InMemoryStorage;
505 use astraea_vector::HnswVectorIndex;
506 use tokio::io::AsyncWriteExt;
507
508 use crate::grpc::AstraeaGrpcService;
509 use crate::grpc::proto::VectorSearchRequest;
510 use crate::grpc::proto::astraea_service_server::AstraeaService as _;
511
512 let storage = InMemoryStorage::new();
516 let vector_index: Arc<dyn VectorIndex> =
517 Arc::new(HnswVectorIndex::new(3, DistanceMetric::Cosine));
518 let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
519 vector_index.insert(NodeId(1), &[1.0, 0.0, 0.0]).unwrap();
520 vector_index.insert(NodeId(2), &[0.0, 1.0, 0.0]).unwrap();
521 let handler = Arc::new(RequestHandler::new(Arc::new(graph), Some(vector_index)));
522
523 let (client, server_side) = tokio::io::duplex(4096);
528 let tcp_handler = Arc::clone(&handler);
529 let server_task = tokio::spawn(async move {
530 handle_connection(
531 server_side,
532 tcp_handler,
533 Arc::new(AuthManager::disabled()),
534 Arc::new(ServerMetrics::new()),
535 std::time::Duration::from_secs(5),
536 std::time::Duration::from_secs(5),
537 None,
538 )
539 .await
540 });
541
542 let (read_half, mut write_half) = tokio::io::split(client);
543 let query = r#"{"type":"VectorSearch","query":[1.0,0.0,0.0],"k":2}"#;
544 write_half
545 .write_all(format!("{query}\n").as_bytes())
546 .await
547 .unwrap();
548
549 let mut reader = BufReader::new(read_half);
550 let mut line = String::new();
551 reader.read_line(&mut line).await.unwrap();
552
553 drop(write_half);
556 server_task.await.unwrap().unwrap();
557
558 let tcp_resp: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
559 assert_eq!(tcp_resp["status"], "ok", "TCP response: {tcp_resp}");
560 let tcp_results = tcp_resp["data"]["results"].as_array().unwrap();
561 assert_eq!(tcp_results.len(), 2);
562 assert_eq!(tcp_results[0]["node_id"].as_u64().unwrap(), 1);
563 let tcp_distance = tcp_results[0]["distance"]
565 .as_f64()
566 .expect("TCP JSON result must carry a `distance` field");
567 assert!(
568 tcp_distance.abs() < 1e-5,
569 "expected ~0 distance for an exact match, got {tcp_distance}"
570 );
571
572 let svc = AstraeaGrpcService::new(Arc::clone(&handler));
575 let grpc_resp = svc
576 .vector_search(tonic::Request::new(VectorSearchRequest {
577 query: vec![1.0, 0.0, 0.0],
578 k: 2,
579 }))
580 .await
581 .unwrap()
582 .into_inner();
583 assert!(
584 grpc_resp.error.is_empty(),
585 "gRPC error: {}",
586 grpc_resp.error
587 );
588 assert_eq!(grpc_resp.results.len(), 2);
589 assert_eq!(grpc_resp.results[0].node_id, 1);
590 let grpc_distance = grpc_resp.results[0].distance as f64;
593
594 assert!(
597 (tcp_distance - grpc_distance).abs() < 1e-4,
598 "TCP distance {tcp_distance} != gRPC distance {grpc_distance}"
599 );
600 }
601}