1mod 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;
63static HTTP_BLOCKING_SLOTS: LazyLock<Arc<Semaphore>> =
65 LazyLock::new(|| Arc::new(Semaphore::new(HTTP_MAX_BLOCKING_TASKS)));
66
67pub struct RuntimeHttpHost {
69 config: HttpApiConfig,
70 router: Router,
71 authentication_required: bool,
72}
73
74impl RuntimeHttpHost {
75 pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
77 Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
78 }
79
80 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 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 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 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 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 pub fn config(&self) -> &HttpApiConfig {
229 &self.config
230 }
231
232 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 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)]
295pub struct RuntimeHttpStateParts {
297 pub controller: Option<Arc<Mutex<RuntimeController>>>,
299 pub sync_log: Option<Arc<dyn SyncLogView>>,
301 pub tick_counter: Option<Arc<AtomicU64>>,
303 pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
305 pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
307 pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
309 pub supervisor: Option<appcore_supervisor::Supervisor>,
311 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;