1use anyhow::{Context, Result};
2use axum::{
3 Extension, Json, Router,
4 body::Body,
5 extract::ConnectInfo,
6 http::{Response, StatusCode},
7 response::IntoResponse,
8 routing::{get, post},
9};
10use chrono::{DateTime, Utc};
11use datafusion::arrow::{
12 array::RecordBatch,
13 json::{Writer, writer::JsonArray},
14};
15use http::{HeaderMap, Uri};
16use micromegas_analytics::time::TimeRange;
17use micromegas_tracing::info;
18use serde::{Deserialize, Serialize};
19use std::net::SocketAddr;
20use std::sync::Arc;
21use thiserror::Error;
22use tonic::transport::{Channel, ClientTlsConfig};
23
24use crate::client::flightsql_client::Client;
25use crate::servers::http_utils;
26
27#[derive(Debug, Clone, Deserialize)]
29pub struct HeaderForwardingConfig {
30 pub allowed_headers: Vec<String>,
32
33 pub allowed_prefixes: Vec<String>,
35
36 pub blocked_headers: Vec<String>,
38}
39
40impl Default for HeaderForwardingConfig {
41 fn default() -> Self {
42 Self {
43 allowed_headers: vec![
45 "Authorization".to_string(),
46 "User-Agent".to_string(),
47 "X-Client-Type".to_string(),
48 "X-Client-Agent".to_string(),
49 "X-Client-Entrypoint".to_string(),
50 "X-Client-Session".to_string(),
51 "X-Client-Notebook".to_string(),
52 "X-Client-Cell".to_string(),
53 "X-Correlation-ID".to_string(),
54 "X-Request-ID".to_string(),
55 "X-User-Email".to_string(),
56 "X-User-ID".to_string(),
57 "X-User-Name".to_string(),
58 ],
59 allowed_prefixes: vec![],
60 blocked_headers: vec![
61 "Cookie".to_string(),
62 "Set-Cookie".to_string(),
63 "X-Client-IP".to_string(),
65 ],
66 }
67 }
68}
69
70impl HeaderForwardingConfig {
71 pub fn from_env() -> Result<Self> {
73 if let Ok(config_json) = std::env::var("MICROMEGAS_GATEWAY_HEADERS") {
74 serde_json::from_str(&config_json).context("Failed to parse MICROMEGAS_GATEWAY_HEADERS")
75 } else {
76 Ok(Self::default())
77 }
78 }
79
80 pub fn should_forward(&self, header_name: &str) -> bool {
82 let name_lower = header_name.to_lowercase();
83
84 if self
86 .blocked_headers
87 .iter()
88 .any(|h| h.to_lowercase() == name_lower)
89 {
90 return false;
91 }
92
93 if self
95 .allowed_headers
96 .iter()
97 .any(|h| h.to_lowercase() == name_lower)
98 {
99 return true;
100 }
101
102 self.allowed_prefixes
104 .iter()
105 .any(|prefix| name_lower.starts_with(&prefix.to_lowercase()))
106 }
107}
108
109#[derive(Debug, Clone)]
113pub struct GatewayConfig {
114 pub flight_url: Uri,
115 pub headers: HeaderForwardingConfig,
116}
117
118impl GatewayConfig {
119 pub fn new(flight_url: Uri) -> Result<Self> {
121 Ok(Self {
122 flight_url,
123 headers: HeaderForwardingConfig::from_env()?,
124 })
125 }
126}
127
128#[derive(Error, Debug)]
129pub enum GatewayError {
130 #[error("Bad request: {0}")]
131 BadRequest(String),
132
133 #[error("Unauthorized: {0}")]
134 Unauthorized(String),
135
136 #[error("Forbidden: {0}")]
137 Forbidden(String),
138
139 #[error("Service unavailable: {0}")]
140 ServiceUnavailable(String),
141
142 #[error("Internal server error: {0}")]
143 Internal(String),
144}
145
146impl IntoResponse for GatewayError {
147 fn into_response(self) -> Response<Body> {
148 let (status, message) = match self {
149 GatewayError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
150 GatewayError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
151 GatewayError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
152 GatewayError::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg),
153 GatewayError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
154 };
155 (status, message).into_response()
156 }
157}
158
159#[derive(Debug, Deserialize)]
160pub struct QueryRequest {
161 sql: String,
162 #[serde(default)]
165 time_range_begin: Option<String>,
166 #[serde(default)]
169 time_range_end: Option<String>,
170}
171
172pub fn build_origin_metadata(
184 headers: &HeaderMap,
185 addr: &SocketAddr,
186) -> tonic::metadata::MetadataMap {
187 let mut metadata = tonic::metadata::MetadataMap::new();
188
189 let original_client_type = headers
191 .get("x-client-type")
192 .and_then(|v| v.to_str().ok())
193 .unwrap_or("unknown");
194 let augmented_client_type = format!("{original_client_type}+gateway");
195 if let Ok(value) = augmented_client_type.parse() {
196 metadata.insert("x-client-type", value);
197 }
198
199 let request_id = headers
201 .get("x-request-id")
202 .and_then(|v| v.to_str().ok())
203 .map(|s| s.to_string())
204 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
205 if let Ok(value) = request_id.parse() {
206 metadata.insert("x-request-id", value);
207 }
208
209 let mut extensions = http::Extensions::new();
212 extensions.insert(axum::extract::ConnectInfo(*addr));
213 let client_ip = http_utils::get_client_ip(headers, &extensions);
214 if let Ok(value) = client_ip.parse() {
215 metadata.insert("x-client-ip", value);
216 }
217
218 metadata
219}
220
221pub async fn handle_query(
222 Extension(config): Extension<Arc<GatewayConfig>>,
223 ConnectInfo(addr): ConnectInfo<SocketAddr>,
224 headers: HeaderMap,
225 Json(request): Json<QueryRequest>,
226) -> Result<String, GatewayError> {
227 let start_time = std::time::Instant::now();
228
229 let origin_metadata = build_origin_metadata(&headers, &addr);
231 let client_type_header = origin_metadata
232 .get("x-client-type")
233 .and_then(|v| v.to_str().ok())
234 .unwrap_or("unknown+gateway");
235 let request_id_header = origin_metadata
236 .get("x-request-id")
237 .and_then(|v| v.to_str().ok())
238 .unwrap_or("unknown");
239
240 let sql = request.sql.trim();
242 if sql.is_empty() {
243 return Err(GatewayError::BadRequest(
244 "SQL query cannot be empty".to_string(),
245 ));
246 }
247
248 const MAX_SQL_SIZE: usize = 1_048_576;
250 if sql.len() > MAX_SQL_SIZE {
251 return Err(GatewayError::BadRequest(format!(
252 "SQL query too large: {} bytes (max: {} bytes)",
253 sql.len(),
254 MAX_SQL_SIZE
255 )));
256 }
257
258 let time_range = match (&request.time_range_begin, &request.time_range_end) {
260 (Some(begin_str), Some(end_str)) => {
261 let begin = DateTime::parse_from_rfc3339(begin_str)
262 .map_err(|e| {
263 GatewayError::BadRequest(format!(
264 "Invalid time_range_begin format (expected RFC3339): {e}"
265 ))
266 })?
267 .with_timezone(&Utc);
268 let end = DateTime::parse_from_rfc3339(end_str)
269 .map_err(|e| {
270 GatewayError::BadRequest(format!(
271 "Invalid time_range_end format (expected RFC3339): {e}"
272 ))
273 })?
274 .with_timezone(&Utc);
275
276 if begin > end {
277 return Err(GatewayError::BadRequest(
278 "time_range_begin must be before time_range_end".to_string(),
279 ));
280 }
281
282 Some(TimeRange::new(begin, end))
283 }
284 (Some(_), None) => {
285 return Err(GatewayError::BadRequest(
286 "time_range_end must be provided when time_range_begin is specified".to_string(),
287 ));
288 }
289 (None, Some(_)) => {
290 return Err(GatewayError::BadRequest(
291 "time_range_begin must be provided when time_range_end is specified".to_string(),
292 ));
293 }
294 (None, None) => None,
295 };
296
297 info!(
298 "Gateway request: request_id={}, client_type={}, time_range={:?}, sql={}",
299 request_id_header, client_type_header, time_range, sql
300 );
301
302 let tls_config = ClientTlsConfig::new().with_native_roots();
303 let channel = Channel::builder(config.flight_url.clone())
304 .tls_config(tls_config)
305 .map_err(|e| GatewayError::Internal(format!("TLS config error: {e}")))?
306 .connect()
307 .await
308 .map_err(|e| {
309 GatewayError::ServiceUnavailable(format!("Failed to connect to FlightSQL: {e}"))
310 })?;
311
312 let mut client = Client::new(channel);
314
315 client
316 .inner_mut()
317 .set_header("x-client-type", client_type_header);
318 client
319 .inner_mut()
320 .set_header("x-request-id", request_id_header);
321
322 if let Some(client_ip) = origin_metadata.get("x-client-ip")
323 && let Ok(ip_str) = client_ip.to_str()
324 {
325 client.inner_mut().set_header("x-client-ip", ip_str);
326 }
327
328 for (name, value) in headers.iter() {
330 let header_name = name.as_str();
331
332 if header_name.eq_ignore_ascii_case("x-client-type")
334 || header_name.eq_ignore_ascii_case("x-request-id")
335 || header_name.eq_ignore_ascii_case("x-client-ip")
336 {
337 continue; }
339
340 if config.headers.should_forward(header_name)
341 && let Ok(value_str) = value.to_str()
342 {
343 client.inner_mut().set_header(header_name, value_str);
344 }
345 }
346
347 let batches = client
349 .query(sql.to_string(), time_range)
350 .await
351 .map_err(|e| {
352 if let Some(status) = e.downcast_ref::<tonic::Status>() {
354 match status.code() {
355 tonic::Code::Unauthenticated => {
356 GatewayError::Unauthorized(status.message().to_string())
357 }
358 tonic::Code::PermissionDenied => {
359 GatewayError::Forbidden(status.message().to_string())
360 }
361 tonic::Code::InvalidArgument => {
362 GatewayError::BadRequest(status.message().to_string())
363 }
364 tonic::Code::Unavailable => {
365 GatewayError::ServiceUnavailable(status.message().to_string())
366 }
367 _ => GatewayError::Internal(format!("Query failed: {}", status.message())),
368 }
369 } else {
370 GatewayError::Internal(format!("Query execution error: {e:?}"))
371 }
372 })?;
373
374 let elapsed = start_time.elapsed();
375 info!(
376 "Gateway request completed: request_id={}, duration={:?}",
377 request_id_header, elapsed
378 );
379
380 if batches.is_empty() {
381 return Ok("[]".to_string());
382 }
383
384 let mut buffer = Vec::new();
385 let mut json_writer = Writer::<_, JsonArray>::new(&mut buffer);
386 let batch_refs: Vec<&RecordBatch> = batches.iter().collect();
387 json_writer
388 .write_batches(&batch_refs)
389 .map_err(|e| GatewayError::Internal(format!("Failed to serialize results: {e}")))?;
390 json_writer
391 .finish()
392 .map_err(|e| GatewayError::Internal(format!("Failed to finish JSON output: {e}")))?;
393
394 String::from_utf8(buffer)
395 .map_err(|e| GatewayError::Internal(format!("Invalid UTF-8 in results: {e}")))
396}
397
398#[derive(Debug, Serialize)]
399pub struct HealthResponse {
400 pub status: &'static str,
401 pub timestamp: DateTime<Utc>,
402}
403
404pub async fn handle_health() -> Json<HealthResponse> {
405 Json(HealthResponse {
406 status: "healthy",
407 timestamp: Utc::now(),
408 })
409}
410
411pub fn register_routes(router: Router) -> Router {
412 router
413 .route("/gateway/query", post(handle_query))
414 .route("/gateway/health", get(handle_health))
415}