mod api;
pub mod dtos;
pub mod error;
mod extractors;
mod services;
mod utils;
pub use services::browser_profile_services::{
Additional, BrowserProfileConfig, DolphinSpecific, Fonts, Geolocation, Hints,
MediaDevices, Navigator, PortsProtection, Screen, Synchronize, Voices, WebGL,
WebGPU, LINUX_MOCK_PROFILE, MACOS_MOCK_PROFILE, WEBSITE_URLS,
WINDOWS_MOCK_PROFILE,
};
use utils::metrics::build_recorder;
use std::{
net::{Ipv4Addr, SocketAddr},
sync::Arc,
time::{Duration, Instant},
};
use anyhow::Context;
use axum::{
body::Body,
error_handling::HandleErrorLayer,
extract::MatchedPath,
http::{HeaderValue, Request, StatusCode},
middleware::{self, Next},
response::IntoResponse,
routing::get,
BoxError, Extension, Json, Router,
};
use serde_json::json;
use services::Services;
use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use tracing::{debug, info};
use crate::{cache::Cache, config::DarkwingConfig, database::Database};
pub struct ApplicationServer;
impl ApplicationServer {
const HTTP_TIMEOUT: u64 = 60;
pub async fn serve(
config: DarkwingConfig,
db: Database,
cache: Cache,
) -> anyhow::Result<()> {
let recorder_handle = build_recorder()?;
debug!("installed metrics recorder. handle: {:?}", recorder_handle);
let services =
Services::new(db, cache, Arc::new(config.clone()), recorder_handle).await;
let cors = CorsLayer::new()
.allow_origin(HeaderValue::from_static("*"))
.allow_methods(tower_http::cors::Any)
.allow_headers(tower_http::cors::Any);
let router = Router::new()
.nest("/api/v1", api::app())
.route("/", get(api::health))
.route("/metrics", get(api::metrics))
.layer(
ServiceBuilder::new()
.layer(sentry_tower::NewSentryLayer::new_from_top())
.layer(sentry_tower::SentryHttpLayer::new())
.layer(TraceLayer::new_for_http())
.layer(HandleErrorLayer::new(Self::handle_timeout_error))
.timeout(Duration::from_secs(Self::HTTP_TIMEOUT))
.layer(cors)
.layer(Extension(services))
.layer(BufferLayer::new(1024))
.layer(RateLimitLayer::new(
config.rate_limit_per_second,
Duration::from_secs(1),
)),
)
.route_layer(middleware::from_fn(Self::track_metrics));
let router = router.fallback(Self::handle_404);
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, config.port));
let listener = tokio::net::TcpListener::bind(addr)
.await
.context(format!("failed to bind to address {}", addr))?;
info!("server is starting at {addr}");
axum::serve(listener, router)
.with_graceful_shutdown(Self::shutdown_signal())
.await
.context("error while starting axum server")?;
Ok(())
}
async fn handle_timeout_error(
err: BoxError,
) -> (StatusCode, Json<serde_json::Value>) {
if err.is::<tower::timeout::error::Elapsed>() {
(
StatusCode::REQUEST_TIMEOUT,
Json(json!({
"error":
format!(
"request took longer than the configured {} second timeout",
Self::HTTP_TIMEOUT
)
})),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("unhandled internal error: {}", err)
})),
)
}
}
async fn track_metrics(
request: Request<Body>,
next: Next,
) -> impl IntoResponse {
let path =
if let Some(matched_path) = request.extensions().get::<MatchedPath>() {
matched_path.as_str().to_owned()
} else {
request.uri().path().to_owned()
};
let start = Instant::now();
let method = request.method().clone();
let response = next.run(request).await;
let latency = start.elapsed().as_secs_f64();
let status = response.status().as_u16().to_string();
let labels = [
("method", method.to_string()),
("path", path),
("status", status),
];
metrics::counter!("darkwing_http_requests_total", &labels).increment(1);
metrics::histogram!("darkwing_http_requests_duration_seconds", &labels)
.record(latency);
response
}
async fn shutdown_signal() {
#[allow(
clippy::expect_used,
reason = "if this function panics, then something gone insanely wrong and we do not mind panicking"
)]
tokio::signal::ctrl_c()
.await
.expect("expect tokio signal ctrl-c");
if let Some(client) = sentry::Hub::current().client() {
client.close(Some(Duration::from_secs(2)));
}
eprintln!("signal shutdown");
}
async fn handle_404() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
axum::response::Json(serde_json::json!({
"errors":{
"message": vec!(String::from("The requested resource does not exist on this server!")),}
})),
)
}
}