use actix_web::http::Method as ActixMethod;
use actix_web::http::{StatusCode, header};
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use moka::future::Cache;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue as ReqwestHeaderValue};
use reqwest::{Client, Method};
use serde_json::{Value, json};
use std::str::Split;
use std::sync::Arc;
use tracing::info;
use web::Data;
use reqwest::header::{CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
use crate::AppState;
use crate::api::gateway::fetch::handle_fetch_data_route;
const TARGET_BASE_URL: &str = "https://db-suitsbooks-nl.xylex.cloud";
const HOST_DEXTER: &str = "db-dexter.xylex.cloud";
const TARGET_BASE_URL_DEXTER: &str = "https://athena.dexter.xylex.cloud";
pub async fn proxy_request(
req: HttpRequest,
body: web::Bytes,
app_state: Data<AppState>,
) -> impl Responder {
let client: &Client = &app_state.client;
let cache: &Arc<Cache<String, Value>> = &app_state.cache;
let full_url: reqwest::Url = req.full_url();
let full_url_path: &str = full_url.path();
let query_params: &str = full_url.query().unwrap_or_default();
let path: String = full_url_path.replacen("/rest/v1", "", 1);
info!(
"ATHENA Router | Incoming request path={} has_query_params={}",
full_url_path,
!query_params.is_empty()
);
let has_athena_client: bool = req
.headers()
.get("X-Athena-Client")
.and_then(|value| value.to_str().ok())
.filter(|value| !value.is_empty())
.is_some();
let targets_fetch_routes: [&str; 2] = ["/gateway/data", "/gateway/fetch"];
if has_athena_client
&& targets_fetch_routes
.iter()
.any(|route| full_url_path.starts_with(route))
{
let body_json: Option<web::Json<Value>> = if body.is_empty() {
None
} else {
match serde_json::from_slice::<Value>(&body) {
Ok(parsed) => Some(web::Json(parsed)),
Err(err) => {
info!(
"ATHENA Router | Failed to parse JSON body for fetch route: {}",
err
);
return HttpResponse::BadRequest()
.json(json!({"error": "Invalid JSON body for fetch route"}));
}
}
};
return handle_fetch_data_route(req.clone(), body_json, app_state.clone()).await;
}
let mut query_params_without_apikey: String = String::new();
let mut apikey: Option<&str> = None;
for pair in req.query_string().split('&') {
let mut split: Split<'_, char> = pair.split('=');
if let (Some(key), Some(value)) = (split.next(), split.next()) {
if key == "apikey" {
apikey = Some(value);
} else {
if !query_params_without_apikey.is_empty() {
query_params_without_apikey.push('&');
}
query_params_without_apikey.push_str(pair);
}
}
}
let host: &str = req
.headers()
.get(header::HOST)
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
let target_url_repl: String = if host.contains(HOST_DEXTER) {
format!("{}{}", TARGET_BASE_URL_DEXTER, path)
} else {
format!("{}{}", TARGET_BASE_URL, path)
};
let mut target_url: String = target_url_repl;
if !query_params_without_apikey.is_empty() {
target_url.push('?');
target_url.push_str(&query_params_without_apikey);
}
info!("ATHENA Router | Routing to {}", target_url);
if target_url.contains("favicon.ico") {
return HttpResponse::NotFound().finish();
}
let mut jwt_token: String = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.map(|value| value.to_string())
.unwrap_or_default();
if let Some(apikey) = apikey {
jwt_token = apikey.to_string();
}
info!(
"ATHENA Router | Auth token source={} token_present={}",
if apikey.is_some() {
"query_apikey"
} else {
"authorization_header"
},
!jwt_token.is_empty()
);
let cache_control_header: Option<header::HeaderValue> =
req.headers().get(header::CACHE_CONTROL).cloned();
let cachekey: String = format!("{}-{}-{}", req.method(), full_url, jwt_token)
.replace('*', "_xXx_")
.replace(' ', "_")
.replace(':', "-")
.replace('/', "_");
if cache_control_header
.as_ref()
.is_none_or(|h| h != "no-cache")
&& let Some(cached_response) = cache.get(&cachekey).await
{
info!("Cache hit for key: {}", cachekey);
return HttpResponse::Ok().json(cached_response);
}
let reqwest_method: Method = match *req.method() {
ActixMethod::GET => Method::GET,
ActixMethod::POST => Method::POST,
ActixMethod::PUT => Method::PUT,
ActixMethod::DELETE => Method::DELETE,
ActixMethod::PATCH => Method::PATCH,
_ => Method::GET,
};
let mut client_req: reqwest::RequestBuilder = client.request(reqwest_method, &target_url);
for (key, value) in req.headers().iter() {
if key != header::HOST {
if let (Ok(reqwest_key), Ok(reqwest_value)) = (
HeaderName::from_bytes(key.as_ref()),
ReqwestHeaderValue::from_bytes(value.as_bytes()),
) {
client_req = client_req.header(reqwest_key, reqwest_value);
}
}
}
if let Some(apikey) = apikey {
client_req = client_req.header("Authorization", format!("Bearer {}", apikey));
} else if !jwt_token.is_empty() {
let token: String = if jwt_token.starts_with("Bearer ") {
jwt_token.clone()
} else {
format!("Bearer {}", jwt_token)
};
client_req = client_req.header("apikey", token);
}
match client_req.body(body).send().await {
Ok(res) => {
let status_code: StatusCode =
StatusCode::from_u16(res.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
let headers: HeaderMap = res.headers().clone();
let body_bytes: web::Bytes = res.bytes().await.unwrap_or_default();
let json_body: Value = serde_json::from_slice(&body_bytes).unwrap_or(Value::Null);
cache.insert(cachekey, json_body.clone()).await;
let mut response: actix_web::HttpResponseBuilder = HttpResponse::build(status_code);
for (key, value) in headers.iter() {
if ![
CONTENT_ENCODING,
CONTENT_LENGTH,
TRANSFER_ENCODING,
CONNECTION,
]
.contains(key)
{
let actix_key: actix_web::http::header::HeaderName =
match actix_web::http::header::HeaderName::from_bytes(
key.as_str().as_bytes(),
) {
Ok(parsed) => parsed,
Err(_) => continue,
};
let actix_value: actix_web::http::header::HeaderValue =
match actix_web::http::header::HeaderValue::from_bytes(value.as_bytes()) {
Ok(parsed) => parsed,
Err(_) => continue,
};
if actix_key == header::CONTENT_TYPE {
response.append_header((
actix_key,
header::HeaderValue::from_static("application/json"),
));
} else {
response.append_header((actix_key, actix_value));
}
}
}
response.body(body_bytes)
}
Err(e) => {
info!(
"\x1b[38;5;208mATHENA Router | Error sending request to target URL: {}\x1b[0m",
e
);
HttpResponse::InternalServerError().finish()
}
}
}