use crate::app::AppState;
use anyhow::Result;
use axum::{
Router,
routing::{get, put},
};
use axum_server::tls_rustls::RustlsConfig;
use bytes::Bytes;
use std::{net::SocketAddr, path::Path};
use tokio::sync::broadcast;
use tower_http::{
services::{ServeDir, ServeFile},
trace::TraceLayer,
};
mod ad9361;
mod api;
mod ddc;
mod geolocation;
mod iqengine;
mod recording;
mod spectrometer;
mod time;
mod version;
mod websocket;
mod zeros;
pub use recording::{RecorderFinishWaiter, RecorderState};
#[derive(Debug)]
pub struct Server {
http_server: axum_server::Server,
https_server: Option<axum_server::Server<axum_server::tls_rustls::RustlsAcceptor>>,
app: Router,
}
impl Server {
pub async fn new(
http_address: SocketAddr,
https_address: SocketAddr,
ssl_cert: Option<impl AsRef<Path>>,
ssl_key: Option<impl AsRef<Path>>,
ca_cert: Option<impl AsRef<Path>>,
state: AppState,
waterfall_sender: broadcast::Sender<Bytes>,
) -> Result<Server> {
let mut app = Router::new()
.route("/api", get(api::get_api))
.route(
"/api/ad9361",
get(ad9361::get_ad9361)
.put(ad9361::put_ad9361)
.patch(ad9361::patch_ad9361),
)
.route(
"/api/spectrometer",
get(spectrometer::get_spectrometer).patch(spectrometer::patch_spectrometer),
)
.route(
"/api/ddc/config",
get(ddc::get_ddc_config)
.put(ddc::put_ddc_config)
.patch(ddc::patch_ddc_config),
)
.route("/api/ddc/design", put(ddc::put_ddc_design))
.route(
"/api/geolocation",
get(geolocation::get_geolocation).put(geolocation::put_geolocation),
)
.route(
"/api/recorder",
get(recording::get_recorder).patch(recording::patch_recorder),
)
.route(
"/api/recording/metadata",
get(recording::get_recording_metadata)
.put(recording::put_recording_metadata)
.patch(recording::patch_recording_metadata),
)
.route("/api/versions", get(version::get_versions))
.route("/recording", get(recording::get_recording))
.route("/version", get(version::get_version))
.route(
"/api/datasources/maiasdr/maiasdr/recording/meta",
get(recording::iqengine::meta),
)
.route(
"/api/datasources/maiasdr/maiasdr/recording/iq-data",
get(recording::iqengine::iq_data),
)
.route(
"/api/datasources/maiasdr/maiasdr/recording/minimap-data",
get(recording::iqengine::minimap_data),
)
.with_state(state)
.route(
"/api/time",
get(time::get_time)
.put(time::put_time)
.patch(time::patch_time),
)
.route(
"/waterfall",
get(websocket::handler).with_state(waterfall_sender),
)
.route("/zeros", get(zeros::get_zeros)); if let Some(ca_cert) = &ca_cert {
app = app.route_service("/ca.crt", ServeFile::new(ca_cert));
}
let app = app
.route_service(
"/view/api/maiasdr/maiasdr/recording",
ServeFile::new("iqengine/index.html"),
)
.route("/assets/{filename}", get(iqengine::serve_assets))
.fallback_service(ServeDir::new("."))
.layer(TraceLayer::new_for_http());
tracing::info!(%http_address, "starting HTTP server");
let http_server = axum_server::bind(http_address);
tracing::info!(%https_address, "starting HTTPS server");
let https_server = match (&ssl_cert, &ssl_key) {
(Some(ssl_cert), Some(ssl_key)) => Some(axum_server::bind_rustls(
https_address,
RustlsConfig::from_pem_file(ssl_cert, ssl_key).await?,
)),
_ => None,
};
Ok(Server {
http_server,
https_server,
app,
})
}
pub async fn run(self) -> Result<()> {
let http_server = self.http_server.serve(self.app.clone().into_make_service());
if let Some(https_server) = self.https_server {
let https_server = https_server.serve(self.app.into_make_service());
Ok(tokio::select! {
ret = http_server => ret,
ret = https_server => ret,
}?)
} else {
Ok(http_server.await?)
}
}
}
mod json_error {
use anyhow::Error;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Serialize, Debug, Clone, Eq, PartialEq)]
pub struct JsonError(maia_json::Error);
impl JsonError {
pub fn from_error<E: Into<Error>>(
error: E,
status_code: StatusCode,
suggested_action: maia_json::ErrorAction,
) -> JsonError {
let error: Error = error.into();
JsonError(maia_json::Error {
http_status_code: status_code.as_u16(),
error_description: format!("{error:#}"),
suggested_action,
})
}
pub fn client_error_alert<E: Into<Error>>(error: E) -> JsonError {
JsonError::from_error(
error,
StatusCode::BAD_REQUEST,
maia_json::ErrorAction::Alert,
)
}
pub fn client_error<E: Into<Error>>(error: E) -> JsonError {
JsonError::from_error(error, StatusCode::BAD_REQUEST, maia_json::ErrorAction::Log)
}
pub fn server_error<E: Into<Error>>(error: E) -> JsonError {
JsonError::from_error(
error,
StatusCode::INTERNAL_SERVER_ERROR,
maia_json::ErrorAction::Log,
)
}
}
impl IntoResponse for JsonError {
fn into_response(self) -> Response {
let status_code = StatusCode::from_u16(self.0.http_status_code).unwrap();
let json = serde_json::to_string(&self.0).unwrap();
(status_code, json).into_response()
}
}
}