maia_httpd/
httpd.rs

1//! HTTP server.
2//!
3//! This module contains the HTTP server of maia-httpd, which is a web server
4//! implemented using [`axum`].
5
6use crate::app::AppState;
7use anyhow::Result;
8use axum::{
9    Router,
10    routing::{get, put},
11};
12use axum_server::tls_rustls::RustlsConfig;
13use bytes::Bytes;
14use std::{net::SocketAddr, path::Path};
15use tokio::sync::broadcast;
16use tower_http::{
17    services::{ServeDir, ServeFile},
18    trace::TraceLayer,
19};
20
21mod ad9361;
22mod api;
23mod ddc;
24mod geolocation;
25mod iqengine;
26mod recording;
27mod spectrometer;
28mod time;
29mod version;
30mod websocket;
31mod zeros;
32
33pub use recording::{RecorderFinishWaiter, RecorderState};
34
35/// HTTP server.
36///
37/// This HTTP server is the core of the functionality of maia-httpd. Most
38/// operations are performed as response to an HTTP request handled by this
39/// server.
40#[derive(Debug)]
41pub struct Server {
42    http_server: axum_server::Server,
43    https_server: Option<axum_server::Server<axum_server::tls_rustls::RustlsAcceptor>>,
44    app: Router,
45}
46
47impl Server {
48    /// Creates a new HTTP server.
49    ///
50    /// The `http_address` parameter gives the address in which the server will
51    /// listen using HTTP. The `https_address` parameter gives the address in
52    /// which the server will listen using HTTPS. The `ad9361` and `ip_core`
53    /// parameters give the server shared access to the AD9361 device and the
54    /// Maia SDR FPGA IP core. The `spectrometer_samp_rate` parameter gives
55    /// shared access to update the sample rate of the spectrometer. The
56    /// `waiter_recorder` is the interrupt waiter for the IQ recorder, which is
57    /// contolled by the HTTP server. The `waterfall_sender` is used to obtain
58    /// waterfall channel receivers for the websocket server.
59    ///
60    /// After calling this function, the server needs to be run by calling
61    /// [`Server::run`].
62    pub async fn new(
63        http_address: SocketAddr,
64        https_address: SocketAddr,
65        ssl_cert: Option<impl AsRef<Path>>,
66        ssl_key: Option<impl AsRef<Path>>,
67        ca_cert: Option<impl AsRef<Path>>,
68        state: AppState,
69        waterfall_sender: broadcast::Sender<Bytes>,
70    ) -> Result<Server> {
71        let mut app = Router::new()
72            // all the following routes have .with_state(state)
73            .route("/api", get(api::get_api))
74            .route(
75                "/api/ad9361",
76                get(ad9361::get_ad9361)
77                    .put(ad9361::put_ad9361)
78                    .patch(ad9361::patch_ad9361),
79            )
80            .route(
81                "/api/spectrometer",
82                get(spectrometer::get_spectrometer).patch(spectrometer::patch_spectrometer),
83            )
84            .route(
85                "/api/ddc/config",
86                get(ddc::get_ddc_config)
87                    .put(ddc::put_ddc_config)
88                    .patch(ddc::patch_ddc_config),
89            )
90            .route("/api/ddc/design", put(ddc::put_ddc_design))
91            .route(
92                "/api/geolocation",
93                get(geolocation::get_geolocation).put(geolocation::put_geolocation),
94            )
95            .route(
96                "/api/recorder",
97                get(recording::get_recorder).patch(recording::patch_recorder),
98            )
99            .route(
100                "/api/recording/metadata",
101                get(recording::get_recording_metadata)
102                    .put(recording::put_recording_metadata)
103                    .patch(recording::patch_recording_metadata),
104            )
105            .route("/api/versions", get(version::get_versions))
106            .route("/recording", get(recording::get_recording))
107            .route("/version", get(version::get_version))
108            // IQEngine viewer for IQ recording
109            .route(
110                "/api/datasources/maiasdr/maiasdr/recording/meta",
111                get(recording::iqengine::meta),
112            )
113            .route(
114                "/api/datasources/maiasdr/maiasdr/recording/iq-data",
115                get(recording::iqengine::iq_data),
116            )
117            .route(
118                "/api/datasources/maiasdr/maiasdr/recording/minimap-data",
119                get(recording::iqengine::minimap_data),
120            )
121            .with_state(state)
122            // the following routes have another (or no) state
123            .route(
124                "/api/time",
125                get(time::get_time)
126                    .put(time::put_time)
127                    .patch(time::patch_time),
128            )
129            .route(
130                "/waterfall",
131                get(websocket::handler).with_state(waterfall_sender),
132            )
133            .route("/zeros", get(zeros::get_zeros)); // used for benchmarking
134        if let Some(ca_cert) = &ca_cert {
135            // Maia SDR CA certificate
136            app = app.route_service("/ca.crt", ServeFile::new(ca_cert));
137        }
138        let app = app
139            // IQEngine viewer for IQ recording
140            .route_service(
141                "/view/api/maiasdr/maiasdr/recording",
142                ServeFile::new("iqengine/index.html"),
143            )
144            .route("/assets/{filename}", get(iqengine::serve_assets))
145            .fallback_service(ServeDir::new("."))
146            .layer(TraceLayer::new_for_http());
147        tracing::info!(%http_address, "starting HTTP server");
148        let http_server = axum_server::bind(http_address);
149        tracing::info!(%https_address, "starting HTTPS server");
150        let https_server = match (&ssl_cert, &ssl_key) {
151            (Some(ssl_cert), Some(ssl_key)) => Some(axum_server::bind_rustls(
152                https_address,
153                RustlsConfig::from_pem_file(ssl_cert, ssl_key).await?,
154            )),
155            _ => None,
156        };
157        Ok(Server {
158            http_server,
159            https_server,
160            app,
161        })
162    }
163
164    /// Runs the HTTP server.
165    ///
166    /// This only returns if there is a fatal error.
167    pub async fn run(self) -> Result<()> {
168        let http_server = self.http_server.serve(self.app.clone().into_make_service());
169        if let Some(https_server) = self.https_server {
170            let https_server = https_server.serve(self.app.into_make_service());
171            Ok(tokio::select! {
172                ret = http_server => ret,
173                ret = https_server => ret,
174            }?)
175        } else {
176            Ok(http_server.await?)
177        }
178    }
179}
180
181mod json_error {
182    use anyhow::Error;
183    use axum::{
184        http::StatusCode,
185        response::{IntoResponse, Response},
186    };
187    use serde::Serialize;
188
189    #[derive(Serialize, Debug, Clone, Eq, PartialEq)]
190    pub struct JsonError(maia_json::Error);
191
192    impl JsonError {
193        pub fn from_error<E: Into<Error>>(
194            error: E,
195            status_code: StatusCode,
196            suggested_action: maia_json::ErrorAction,
197        ) -> JsonError {
198            let error: Error = error.into();
199            JsonError(maia_json::Error {
200                http_status_code: status_code.as_u16(),
201                error_description: format!("{error:#}"),
202                suggested_action,
203            })
204        }
205
206        pub fn client_error_alert<E: Into<Error>>(error: E) -> JsonError {
207            JsonError::from_error(
208                error,
209                StatusCode::BAD_REQUEST,
210                maia_json::ErrorAction::Alert,
211            )
212        }
213
214        pub fn client_error<E: Into<Error>>(error: E) -> JsonError {
215            JsonError::from_error(error, StatusCode::BAD_REQUEST, maia_json::ErrorAction::Log)
216        }
217
218        pub fn server_error<E: Into<Error>>(error: E) -> JsonError {
219            JsonError::from_error(
220                error,
221                StatusCode::INTERNAL_SERVER_ERROR,
222                maia_json::ErrorAction::Log,
223            )
224        }
225    }
226
227    impl IntoResponse for JsonError {
228        fn into_response(self) -> Response {
229            let status_code = StatusCode::from_u16(self.0.http_status_code).unwrap();
230            let json = serde_json::to_string(&self.0).unwrap();
231            (status_code, json).into_response()
232        }
233    }
234}