Skip to main content

appcore_api/
http.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: http.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/05/31 13:38:42 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 13:24:05 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Minimal HTTP host for runtime health, status, and command ingress.
12
13mod auth;
14mod command;
15mod connection;
16mod handlers;
17mod ingress;
18mod query;
19mod reload;
20mod reload_generation;
21mod response;
22mod state;
23mod trace;
24
25pub use auth::{
26    CommandTokenVerifier, HttpCommandAuth, RequestPayloadRef, RequestValidationDetails,
27    RequestValidationDetailsRef,
28};
29pub use reload::{
30    HttpReloadPhase, HttpReloadPolicy, HttpReloadSnapshot, PreparedRuntimeHttpGeneration,
31    ReloadableRuntimeHttpHost, RuntimeHttpReloadError,
32};
33pub use reload_generation::{HttpRoutingGenerationSnapshot, HttpRoutingGenerationsSnapshot};
34pub use state::{
35    CommandCapabilityPolicy, CommandCapabilityPolicyError, HttpApiConfig, RuntimeStaticInfo,
36    SyncLogView, SyncLogViewError,
37};
38
39#[cfg(test)]
40use crate::command_contract::{CommandRequest, CommandResponse, CommandResponseEvent};
41use crate::ApiRouter;
42use appcore_core::{RuntimeController, RuntimeOperationalMode};
43use axum::extract::DefaultBodyLimit;
44use axum::http::StatusCode;
45use axum::routing::{get, post};
46use axum::Router;
47use command::command_handler;
48use handlers::{
49    diagnostics_handler, health_handler, private_status_handler, public_status_handler,
50    status_handler,
51};
52use parking_lot::Mutex;
53use query::query_handler;
54use state::HttpState;
55use std::io;
56use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
57use std::sync::{Arc, LazyLock};
58use std::time::Duration;
59use tokio::sync::{OwnedSemaphorePermit, Semaphore};
60
61const HTTP_MAX_BLOCKING_TASKS: usize = 16;
62const HTTP_THREAD_STACK_BYTES: usize = 1024 * 1024;
63// appcore-norm: allow(global-state) reason: process-wide gate bounds all embedded HTTP blocking dispatch
64static HTTP_BLOCKING_SLOTS: LazyLock<Arc<Semaphore>> =
65    LazyLock::new(|| Arc::new(Semaphore::new(HTTP_MAX_BLOCKING_TASKS)));
66
67/// Embedded HTTP host for stable Runtime health, status, command, and query routes.
68pub struct RuntimeHttpHost {
69    config: HttpApiConfig,
70    router: Router,
71    authentication_required: bool,
72}
73
74impl RuntimeHttpHost {
75    /// Creates a host exposing only static operational information.
76    pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
77        Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
78    }
79
80    /// Creates a host connected to a Runtime controller.
81    pub fn with_controller(
82        config: HttpApiConfig,
83        static_info: RuntimeStaticInfo,
84        controller: Arc<Mutex<RuntimeController>>,
85    ) -> Self {
86        Self::with_runtime_state(
87            config,
88            static_info,
89            RuntimeHttpStateParts {
90                controller: Some(controller),
91                ..RuntimeHttpStateParts::default()
92            },
93        )
94    }
95
96    /// Creates a host with controller, sync status, tick counter, and token policy.
97    pub fn with_runtime_state_and_auth(
98        config: HttpApiConfig,
99        static_info: RuntimeStaticInfo,
100        controller: Arc<Mutex<RuntimeController>>,
101        sync_log: Option<Arc<dyn SyncLogView>>,
102        tick_counter: Option<Arc<AtomicU64>>,
103        auth: HttpCommandAuth,
104    ) -> Self {
105        Self::with_runtime_state(
106            config,
107            static_info,
108            RuntimeHttpStateParts {
109                controller: Some(controller),
110                sync_log,
111                tick_counter,
112                auth,
113                ..RuntimeHttpStateParts::default()
114            },
115        )
116    }
117
118    /// Creates an authenticated host with a live operational-mode source.
119    pub fn with_runtime_state_auth_and_operation_mode(
120        config: HttpApiConfig,
121        static_info: RuntimeStaticInfo,
122        controller: Arc<Mutex<RuntimeController>>,
123        sync_log: Option<Arc<dyn SyncLogView>>,
124        tick_counter: Option<Arc<AtomicU64>>,
125        auth: HttpCommandAuth,
126        operation_mode: Arc<Mutex<appcore_core::RuntimeOperationalMode>>,
127    ) -> Self {
128        Self::with_runtime_state(
129            config,
130            static_info,
131            RuntimeHttpStateParts {
132                controller: Some(controller),
133                sync_log,
134                tick_counter,
135                operation_mode: Some(operation_mode),
136                auth,
137                ..RuntimeHttpStateParts::default()
138            },
139        )
140    }
141
142    /// Creates an authenticated host with application query routing.
143    pub fn with_runtime_state_auth_and_app_queries(
144        config: HttpApiConfig,
145        static_info: RuntimeStaticInfo,
146        controller: Arc<Mutex<RuntimeController>>,
147        sync_log: Option<Arc<dyn SyncLogView>>,
148        tick_counter: Option<Arc<AtomicU64>>,
149        auth: HttpCommandAuth,
150        app_query_router: Arc<Mutex<ApiRouter>>,
151    ) -> Self {
152        Self::with_runtime_state(
153            config,
154            static_info,
155            RuntimeHttpStateParts {
156                controller: Some(controller),
157                sync_log,
158                tick_counter,
159                app_query_router: Some(app_query_router),
160                auth,
161                ..RuntimeHttpStateParts::default()
162            },
163        )
164    }
165
166    /// Creates a host from the complete set of optional shared state parts.
167    pub fn with_state_parts(
168        config: HttpApiConfig,
169        static_info: RuntimeStaticInfo,
170        parts: RuntimeHttpStateParts,
171    ) -> Self {
172        Self::with_runtime_state(config, static_info, parts)
173    }
174
175    fn with_runtime_state(
176        config: HttpApiConfig,
177        static_info: RuntimeStaticInfo,
178        parts: RuntimeHttpStateParts,
179    ) -> Self {
180        if let Some(router) = &parts.app_query_router {
181            router.lock().freeze_queries();
182        }
183        let authentication_required = parts.auth.requires_token();
184        let state = HttpState {
185            static_info: Arc::new(static_info),
186            controller: parts.controller,
187            app_query_router: parts.app_query_router,
188            sync_log: parts.sync_log,
189            tick_counter: parts.tick_counter,
190            operation_mode: parts.operation_mode,
191            command_policy: parts.command_policy,
192            supervisor: parts.supervisor,
193            auth: parts.auth,
194            max_payload_bytes: config.max_payload_bytes,
195            clock: Arc::new(appcore_core::SystemClock::new()),
196        };
197        let ingress_routes = Router::new()
198            .route("/v1/command", post(command_handler))
199            .route("/v1/query", post(query_handler))
200            .route_layer(axum::middleware::from_fn_with_state(
201                ingress::Ingress::new(config.max_payload_bytes),
202                ingress::admit,
203            ));
204        let router = Router::new()
205            .merge(ingress_routes)
206            .route("/v1/health", get(health_handler))
207            .route("/v1/status", get(status_handler))
208            .route("/v1/status/public", get(public_status_handler))
209            .route("/v1/status/private", get(private_status_handler))
210            .route("/v1/diagnostics", get(diagnostics_handler))
211            .route("/health", get(update_required_handler))
212            .route("/status", get(update_required_handler))
213            .route("/status/public", get(update_required_handler))
214            .route("/status/private", get(update_required_handler))
215            .route("/diagnostics", get(update_required_handler))
216            .route("/command", post(update_required_handler))
217            .route("/query", post(update_required_handler))
218            .layer(DefaultBodyLimit::max(config.max_payload_bytes))
219            .with_state(state);
220        Self {
221            config,
222            router,
223            authentication_required,
224        }
225    }
226
227    /// Returns immutable listener and payload-limit configuration.
228    pub fn config(&self) -> &HttpApiConfig {
229        &self.config
230    }
231
232    /// Returns an Axum router suitable for embedding in another listener.
233    pub fn router(&self) -> Router {
234        self.router.clone()
235    }
236
237    pub(crate) fn authentication_required(&self) -> bool {
238        self.authentication_required
239    }
240
241    /// Runs the configured listener until `shutdown` becomes true.
242    pub fn run_until_shutdown(&self, shutdown: Arc<AtomicBool>) -> io::Result<()> {
243        if !self.config.enabled {
244            return Ok(());
245        }
246        let address = format!("{}:{}", self.config.host, self.config.port);
247        let router = self.router();
248        let runtime = build_http_runtime()?;
249        runtime.block_on(async move {
250            let listener = tokio::net::TcpListener::bind(address).await?;
251            validate_listener_auth_boundary(&listener, self.authentication_required)?;
252            axum::serve(connection::with_read_timeout(listener), router)
253                .with_graceful_shutdown(wait_for_shutdown(shutdown))
254                .await
255        })
256    }
257}
258
259fn validate_listener_auth_boundary(
260    listener: &tokio::net::TcpListener,
261    authentication_required: bool,
262) -> io::Result<()> {
263    if authentication_required || listener.local_addr()?.ip().is_loopback() {
264        return Ok(());
265    }
266    Err(io::Error::new(
267        io::ErrorKind::PermissionDenied,
268        "HTTP authentication may be disabled only on a loopback listener",
269    ))
270}
271
272fn build_http_runtime() -> io::Result<tokio::runtime::Runtime> {
273    tokio::runtime::Builder::new_current_thread()
274        .max_blocking_threads(HTTP_MAX_BLOCKING_TASKS)
275        .thread_stack_size(HTTP_THREAD_STACK_BYTES)
276        .thread_name("appcore-api-blocking")
277        .thread_keep_alive(Duration::from_secs(5))
278        .enable_all()
279        .build()
280        .map_err(io::Error::other)
281}
282
283fn try_acquire_blocking_slot() -> Option<OwnedSemaphorePermit> {
284    Arc::clone(&HTTP_BLOCKING_SLOTS).try_acquire_owned().ok()
285}
286
287async fn update_required_handler() -> (StatusCode, &'static str) {
288    (
289        StatusCode::UPGRADE_REQUIRED,
290        "NO MORE SUPPORTED PLEASE UPDATE",
291    )
292}
293
294#[derive(Default)]
295/// Optional shared state used to compose a [`RuntimeHttpHost`].
296pub struct RuntimeHttpStateParts {
297    /// Runtime controller used by command and operational query routes.
298    pub controller: Option<Arc<Mutex<RuntimeController>>>,
299    /// Read-only synchronization-log view used by status routes.
300    pub sync_log: Option<Arc<dyn SyncLogView>>,
301    /// Runtime tick counter exposed through diagnostics.
302    pub tick_counter: Option<Arc<AtomicU64>>,
303    /// Application-owned query router.
304    pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
305    /// Live operational mode used to gate writes.
306    pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
307    /// Capability and leadership authorization policy for commands.
308    pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
309    /// Managed-service supervisor exposed through private diagnostics.
310    pub supervisor: Option<appcore_supervisor::Supervisor>,
311    /// Bearer-token authorization configuration.
312    pub auth: HttpCommandAuth,
313}
314
315async fn wait_for_shutdown(shutdown: Arc<AtomicBool>) {
316    loop {
317        if shutdown.load(Ordering::SeqCst) {
318            break;
319        }
320        tokio::time::sleep(Duration::from_millis(100)).await;
321    }
322}
323
324#[cfg(test)]
325mod http_tests;
326#[cfg(test)]
327mod reload_tests;