api.rs (9133B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{ 18 sync::{ 19 Arc, 20 atomic::{AtomicU32, Ordering}, 21 }, 22 time::Instant, 23 }; 24 25 use axum::{ 26 extract::{Request, State}, 27 middleware::{self, Next}, 28 response::Response, 29 }; 30 use compact_str::CompactString; 31 use rand::RngExt as _; 32 use revenue::Revenue; 33 use taler_common::{ 34 error_code::ErrorCode, 35 log::LOG_TASK_ID, 36 types::amount::{Amount, Currency}, 37 }; 38 use tokio::signal; 39 use tower_http::cors::{Any, CorsLayer}; 40 use tracing::{Level, debug, info}; 41 use wire::WireGateway; 42 43 use crate::{ 44 Listener, Serve, 45 api::{observability::Observability, prepared::PreparedTransfer}, 46 auth::{AuthMethod, AuthMiddlewareState}, 47 error::{ApiResult, LoggedError, failure, failure_code}, 48 }; 49 50 pub mod observability; 51 pub mod prepared; 52 pub mod revenue; 53 pub mod wire; 54 55 pub use axum::Router; 56 57 pub trait Validation { 58 fn check(&self, currency: &Currency) -> ApiResult<()>; 59 } 60 61 fn check_currency(currency: &Currency, amount: &Amount) -> ApiResult<()> { 62 if &amount.currency != currency { 63 Err(failure( 64 ErrorCode::GENERIC_CURRENCY_MISMATCH, 65 format!( 66 "wrong currency expected {} got {}", 67 currency, amount.currency 68 ), 69 )) 70 } else { 71 Ok(()) 72 } 73 } 74 75 pub trait TalerApi: Send + Sync + 'static { 76 fn currency(&self) -> Currency; 77 fn implementation(&self) -> &'static str; 78 } 79 80 pub trait RouterUtils { 81 fn auth(self, auth: AuthMethod, realm: &str) -> Self; 82 } 83 84 impl<S: Send + Clone + Sync + 'static> RouterUtils for Router<S> { 85 fn auth(self, auth: AuthMethod, realm: &str) -> Self { 86 self.route_layer(middleware::from_fn_with_state( 87 Arc::new(AuthMiddlewareState::new(auth, realm)), 88 crate::auth::auth_middleware, 89 )) 90 } 91 } 92 93 pub trait TalerRouter { 94 fn wire_gateway<T: WireGateway>(self, api: Arc<T>, auth: AuthMethod) -> Self; 95 fn prepared_transfer<T: PreparedTransfer>(self, api: Arc<T>) -> Self; 96 fn revenue<T: Revenue>(self, api: Arc<T>, auth: AuthMethod) -> Self; 97 fn observability<T: Observability>(self, api: Arc<T>, auth: AuthMethod) -> Self; 98 fn finalize(self) -> Self; 99 fn serve( 100 self, 101 serve: &Serve, 102 lifetime: Option<u32>, 103 ) -> impl std::future::Future<Output = std::io::Result<()>> + Send; 104 } 105 106 impl TalerRouter for Router { 107 fn wire_gateway<T: WireGateway>(self, api: Arc<T>, auth: AuthMethod) -> Self { 108 self.nest("/taler-wire-gateway", wire::router(api, auth)) 109 } 110 111 fn prepared_transfer<T: PreparedTransfer>(self, api: Arc<T>) -> Self { 112 self.nest("/taler-prepared-transfer", prepared::router(api)) 113 } 114 115 fn revenue<T: Revenue>(self, api: Arc<T>, auth: AuthMethod) -> Self { 116 self.nest("/taler-revenue", revenue::router(api, auth)) 117 } 118 119 fn observability<T: Observability>(self, api: Arc<T>, auth: AuthMethod) -> Self { 120 self.nest("/taler-observability", observability::router(api, auth)) 121 } 122 123 fn finalize(self) -> Router { 124 self.method_not_allowed_fallback(async || failure_code(ErrorCode::GENERIC_METHOD_INVALID)) 125 .fallback(async || failure_code(ErrorCode::GENERIC_ENDPOINT_UNKNOWN)) 126 .layer( 127 CorsLayer::new() 128 .allow_origin(Any) 129 .allow_methods(Any) 130 .allow_headers(Any), 131 ) 132 .layer(middleware::from_fn(logger_middleware)) 133 } 134 135 async fn serve(mut self, serve: &Serve, lifetime: Option<u32>) -> std::io::Result<()> { 136 let listener = serve.resolve()?; 137 138 let notify = Arc::new(tokio::sync::Notify::new()); 139 if let Some(lifetime) = lifetime { 140 self = self.layer(middleware::from_fn_with_state( 141 Arc::new(LifetimeMiddlewareState { 142 notify: notify.clone(), 143 lifetime: AtomicU32::new(lifetime), 144 }), 145 lifetime_middleware, 146 )) 147 } 148 let router = self.finalize(); 149 let signal = shutdown_signal(notify); 150 match listener { 151 Listener::Tcp(tcp_listener) => { 152 axum::serve(tcp_listener, router) 153 .with_graceful_shutdown(signal) 154 .await?; 155 } 156 Listener::Unix(unix_listener) => { 157 axum::serve(unix_listener, router) 158 .with_graceful_shutdown(signal) 159 .await?; 160 } 161 } 162 163 info!(target: "api", "Server stopped"); 164 Ok(()) 165 } 166 } 167 168 struct LifetimeMiddlewareState { 169 lifetime: AtomicU32, 170 notify: Arc<tokio::sync::Notify>, 171 } 172 173 async fn lifetime_middleware( 174 State(state): State<Arc<LifetimeMiddlewareState>>, 175 request: Request, 176 next: Next, 177 ) -> Response { 178 let mut current = state.lifetime.load(Ordering::Relaxed); 179 while current != 0 { 180 match state.lifetime.compare_exchange_weak( 181 current, 182 current - 1, 183 Ordering::Relaxed, 184 Ordering::Relaxed, 185 ) { 186 Ok(_) => break, 187 Err(new) => current = new, 188 } 189 } 190 if current == 0 { 191 state.notify.notify_one(); 192 } 193 next.run(request).await 194 } 195 196 /** Wait for manual shutdown or system signal shutdown */ 197 async fn shutdown_signal(manual_shutdown: Arc<tokio::sync::Notify>) { 198 let ctrl_c = async { 199 signal::ctrl_c() 200 .await 201 .expect("failed to install Ctrl+C handler"); 202 }; 203 204 #[cfg(unix)] 205 let terminate = async { 206 signal::unix::signal(signal::unix::SignalKind::terminate()) 207 .expect("failed to install signal handler") 208 .recv() 209 .await; 210 }; 211 212 #[cfg(not(unix))] 213 let terminate = std::future::pending::<()>(); 214 215 let manual = async { manual_shutdown.notified().await }; 216 217 tokio::select! { 218 _ = ctrl_c => {}, 219 _ = terminate => {}, 220 _ = manual => {} 221 } 222 } 223 224 #[macro_export] 225 macro_rules! dyn_event { 226 ($lvl:ident, $($arg:tt)+) => { 227 match $lvl { 228 ::tracing::Level::TRACE => ::tracing::trace!($($arg)+), 229 ::tracing::Level::DEBUG => ::tracing::debug!($($arg)+), 230 ::tracing::Level::INFO => ::tracing::info!($($arg)+), 231 ::tracing::Level::WARN => ::tracing::warn!($($arg)+), 232 ::tracing::Level::ERROR => ::tracing::error!($($arg)+), 233 } 234 }; 235 } 236 237 /** Taler API logger */ 238 async fn logger_middleware(request: Request, next: Next) -> Response { 239 let now = Instant::now(); 240 let request_id: compact_str::CompactString = { 241 let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 242 let mut rng = rand::rng(); 243 244 let mut ansi = [0u8; 10]; 245 for c in ansi.iter_mut() { 246 let idx = rng.random_range(0..charset.len()); 247 *c = charset[idx]; 248 } 249 unsafe { CompactString::from_utf8_unchecked(ansi) } 250 }; 251 let method = request.method().clone(); 252 let path_and_query = request.uri().path_and_query().cloned(); 253 let path_and_query = path_and_query 254 .as_ref() 255 .map(|it| it.as_str()) 256 .unwrap_or_default(); 257 LOG_TASK_ID 258 .scope(request_id, async { 259 debug!(target: "api", "{method} {path_and_query}"); 260 let response = next.run(request).await; 261 let elapsed = now.elapsed(); 262 let status = response.status(); 263 let level = match status.as_u16() { 264 400..500 => Level::WARN, 265 500..600 => Level::ERROR, 266 _ => Level::INFO, 267 }; 268 269 if let Some(log) = response.extensions().get::<LoggedError>() { 270 let LoggedError { code, info } = log; 271 dyn_event!(level, target: "api", 272 "{} {method} {path_and_query} {}ms: {code}{}", 273 response.status(), 274 elapsed.as_millis(), 275 std::fmt::from_fn(|f|{ 276 if let Some(info) = info { 277 write!(f, " {info}")?; 278 } 279 Ok(()) 280 }) 281 ); 282 } else { 283 dyn_event!(level, target: "api", 284 "{} {method} {path_and_query} {}ms", 285 response.status(), 286 elapsed.as_millis() 287 ); 288 } 289 response 290 }) 291 .await 292 }