#![no_std]
#![warn(missing_docs)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]
#![warn(clippy::panic)]
extern crate no_std_compat as std;
extern crate alloc;
use core::marker::PhantomData;
use core::str::FromStr;
use std::prelude::v1::*;
pub mod error;
pub use error::{ProductOSServerError, Result as ServerResult};
pub mod config;
pub use config::{ServerConfig, Network, Certificate, CertificateFiles, CertificateFilesKind, Compression};
#[cfg(feature = "tls")]
mod https_server;
#[cfg(feature = "security_certificates")]
mod certificates;
mod logging;
mod http_server;
#[cfg(feature = "serverless-vercel")]
mod serverless;
#[cfg(feature = "serverless-vercel")]
pub use serverless::{rewrite_vercel_api_path, run_vercel};
#[cfg(all(feature = "framework_flow", feature = "executor_tokio"))]
mod flow_http_server;
#[cfg(all(feature = "framework_flow", feature = "tls", feature = "executor_tokio"))]
mod flow_https_server;
#[cfg(feature = "cspolicy")]
mod csp;
#[cfg(all(feature = "controller", any(feature = "framework_axum", feature = "flow_axum_compat")))]
mod command_handler;
#[cfg(all(feature = "controller", any(feature = "framework_axum", feature = "flow_axum_compat")))]
mod feature_handler;
#[cfg(all(feature = "dual_server", feature = "framework_axum"))]
mod dual_server;
mod server_executor;
#[cfg(any(feature = "oidc", feature = "authentication", feature = "content", feature = "service_handler"))]
mod features_services;
use std::collections::BTreeMap;
use std::sync::Arc;
#[cfg(feature = "controller")]
use parking_lot::Mutex;
#[cfg(feature = "framework_axum")]
pub use product_os_router::{
Layer, Service, ServiceExt, service_fn,
Method, ProductOSRouter, ServiceBuilder,
Json, Form, Path, Query, State, ConnectInfo,
Body, BodyBytes, Bytes, HttpBody,
Request, StatusCode, Response, IntoResponse,
Handler, Uri, BoxError, Scheme
};
#[cfg(all(feature = "flow_axum_compat", not(feature = "framework_axum")))]
pub use product_os_router::{
Layer, Service, ServiceExt, service_fn,
Method, ProductOSRouter, ServiceBuilder,
Json, Form, Path, Query, State, ConnectInfo, ClientAddr,
Body, BodyBytes, Bytes, HttpBody,
Request, StatusCode, Response, IntoResponse,
Handler, Uri, BoxError, Scheme
};
#[cfg(all(feature = "framework_flow", not(feature = "framework_axum"), not(feature = "flow_axum_compat")))]
pub use product_os_router::{
Layer, Service, ServiceExt, service_fn,
Method, ProductOSRouter, ServiceBuilder,
Json, Form, Path, Query, State, ConnectInfo, ClientAddr,
Body, BodyBytes, Bytes, HttpBody,
Request, StatusCode, Response, IntoResponse,
Handler, Uri, BoxError, Scheme
};
#[cfg(feature = "sse")]
pub use product_os_router::{Event, Sse};
#[cfg(all(feature = "sse", feature = "framework_flow", not(feature = "framework_axum"), not(feature = "flow_axum_compat")))]
pub use product_os_router::KeepAlive;
#[cfg(feature = "ws")]
pub use product_os_router::{WebSocket, Message};
#[cfg(all(feature = "ws", any(feature = "framework_axum", feature = "flow_axum_compat")))]
pub use product_os_router::WebSocketUpgrade;
#[cfg(feature = "core")]
pub use product_os_router::Extension;
#[cfg(all(feature = "extract_headers", any(feature = "framework_axum", feature = "flow_axum_compat")))]
pub use axum_extra::typed_header::TypedHeader;
#[cfg(feature = "framework_flow")]
pub use product_os_router::Headers;
#[cfg(all(feature = "extract_headers", feature = "framework_axum", not(feature = "framework_flow")))]
pub use axum_extra::typed_header::TypedHeader as Headers;
pub use product_os_router::{TraceContext, RequestId};
#[cfg(feature = "middleware")]
pub use product_os_router::*;
#[cfg(feature = "controller")]
use product_os_command_control::ProductOSController;
#[cfg(feature = "controller")]
use product_os_store::{ ProductOSKeyValueStore, ProductOSRelationalStore };
#[cfg(all(feature = "core", feature = "framework_axum"))]
use axum::routing::Route;
#[cfg(feature = "framework_axum")]
use product_os_router::Router as AxumRouter;
#[cfg(all(feature = "compression", any(feature = "framework_axum", feature = "flow_axum_compat")))]
use tower_http::compression::CompressionLayer;
#[cfg(feature = "csrf")]
use axum_csrf::{CsrfConfig, CsrfLayer};
#[cfg(feature = "executor_tokio")]
use product_os_async_executor::TokioExecutor;
#[allow(unused_imports)]
use product_os_async_executor::Executor;
#[cfg(feature = "security_certificates")]
use crate::certificates::ProductOSServerCertificates;
const DEFAULT_HSTS_MAX_AGE: u32 = 86400;
#[cfg(feature = "controller")]
pub(crate) const CONTROLLER_LOCK_TIMEOUT: core::time::Duration = core::time::Duration::from_secs(10);
fn apply_security_headers<S: Clone + Send + Sync + 'static>(
router: &mut ProductOSRouter<S>,
host: &str,
security: &product_os_security::Security,
) {
if !security.enable {
return;
}
#[cfg(feature = "cspolicy")]
{
let csp_config = security
.csp
.as_ref()
.map_or_else(product_os_security::CSPConfig::new, Clone::clone);
let csp_value = csp::ContentSecurityPolicy::from_csp_config(&csp_config).get_csp();
router.add_default_header("content-security-policy", &csp_value);
}
router.add_default_header("cross-origin-embedder-policy", "require-corp");
router.add_default_header("cross-origin-opener-policy", "same-origin");
router.add_default_header("referrer-policy", "strict-origin-when-cross-origin");
let hsts_value = alloc::format!("max-age={}; includeSubDomains; preload", DEFAULT_HSTS_MAX_AGE);
router.add_default_header("strict-transport-security", &hsts_value);
router.add_default_header("x-content-type-options", "nosniff");
router.add_default_header("x-powered-by", host);
#[cfg(all(feature = "csrf", any(feature = "framework_axum", feature = "flow_axum_compat")))]
{
if security.csrf {
router.add_middleware(CsrfLayer::new(CsrfConfig::default()));
#[cfg(feature = "core")]
tracing::info!("CSRF added as extension middleware");
}
}
}
#[cfg(all(feature = "compression", any(feature = "framework_axum", feature = "flow_axum_compat")))]
fn apply_compression<S: Clone + Send + Sync + 'static>(
router: &mut ProductOSRouter<S>,
compression_config: &crate::config::Compression,
) {
if !compression_config.enable {
return;
}
let mut compression = CompressionLayer::new();
if compression_config.gzip { compression = compression.gzip(true); }
if compression_config.deflate { compression = compression.deflate(true); }
if compression_config.brotli { compression = compression.br(true); }
router.add_middleware(compression);
}
#[cfg(all(feature = "compression", feature = "framework_flow", not(feature = "framework_axum"), not(feature = "flow_axum_compat")))]
fn apply_compression<S: Clone + Send + Sync + 'static>(
router: &mut ProductOSRouter<S>,
compression_config: &crate::config::Compression,
) {
if !compression_config.enable {
return;
}
if compression_config.deflate {
#[cfg(feature = "core")]
tracing::warn!("Flow compression does not support deflate encoding; deflate will be ignored");
}
use product_os_router::flow_impl::middleware::compression::{CompressionLayer as FlowCompressionLayer, CompressionConfig};
let config = CompressionConfig {
min_size: 256,
gzip: compression_config.gzip,
brotli: compression_config.brotli,
zstd: false,
};
router.add_middleware(FlowCompressionLayer::new(config));
}
pub enum ExecutorType {
Tokio,
Embassy,
}
pub struct ProductOSServer<S, E, X>
where
E: product_os_async_executor::Executor<X> + product_os_async_executor::ExecutorPerform<X> + product_os_async_executor::Timer
{
router: ProductOSRouter<S>,
config: crate::config::ServerConfig,
#[cfg(feature = "security_certificates")]
certificates: product_os_security::certificates::Certificates,
#[cfg(feature = "controller")]
controller: Option<Arc<Mutex<ProductOSController>>>,
#[allow(dead_code)]
executor: Arc<E>,
executor_underlying: PhantomData<X>,
#[cfg(feature = "compression")]
compression_applied: bool,
}
#[cfg(feature = "executor_tokio")]
impl<X> ProductOSServer<(), TokioExecutor, X>
where
TokioExecutor: product_os_async_executor::Executor<X>, TokioExecutor: product_os_async_executor::ExecutorPerform<X>
{
#[allow(clippy::panic)]
pub fn new_with_config(config: crate::config::ServerConfig) -> Self {
let executor = match TokioExecutor::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create Tokio executor context: {:?}. Ensure a Tokio runtime is active.", e),
};
ProductOSServer::new_with_executor_with_state_with_config(Some(executor), config, ())
}
#[allow(clippy::panic)]
pub fn new(_executor: Option<Arc<TokioExecutor>>) -> Self {
let executor = match TokioExecutor::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create Tokio executor context: {:?}. Ensure a Tokio runtime is active.", e),
};
ProductOSServer::new_with_executor_with_state(Some(executor), ())
}
}
impl<E, X> ProductOSServer<(), E, X>
where
E: product_os_async_executor::Executor<X> + product_os_async_executor::ExecutorPerform<X> + product_os_async_executor::Timer + 'static
{
pub fn new_with_executor_with_config(executor: Option<Arc<E>>, config: crate::config::ServerConfig) -> Self {
ProductOSServer::new_with_executor_with_state_with_config(executor, config, ())
}
pub fn new_with_executor(executor: Option<Arc<E>>) -> Self {
ProductOSServer::new_with_executor_with_state(executor, ())
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.53", note = "Use try_add_feature which returns Result instead of panicking")]
pub async fn add_feature(&mut self, feature_arc: Arc<dyn product_os_capabilities::Feature>, base_path: Option<String>) {
self.try_add_feature(feature_arc, base_path).await
.unwrap_or_else(|e| ::std::panic!("{}", e));
}
#[cfg(feature = "controller")]
pub async fn try_add_feature(&mut self, feature_arc: Arc<dyn product_os_capabilities::Feature>, base_path: Option<String>) -> crate::error::Result<()> {
match &self.controller {
None => {
Err(ProductOSServerError::ControllerError {
operation: "add_feature".to_string(),
message: alloc::format!("Feature {} failed to add to server: no controller", feature_arc.identifier()),
})
}
Some(c) => {
let controller_unlocked = c.clone();
let mut controller = controller_unlocked.lock();
let feature_base_path = base_path.unwrap_or_default();
controller.add_feature(feature_arc.clone(), feature_base_path, &mut self.router).await;
#[cfg(feature = "core")]
tracing::info!("Feature {} successfully added to server", feature_arc.identifier());
Ok(())
}
}
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.53", note = "Use try_add_feature_mut which returns Result instead of panicking")]
pub async fn add_feature_mut(&mut self, feature_arc_mut: Arc<Mutex<dyn product_os_capabilities::Feature>>, base_path: Option<String>) {
self.try_add_feature_mut(feature_arc_mut, base_path).await
.unwrap_or_else(|e| ::std::panic!("{}", e));
}
#[cfg(feature = "controller")]
pub async fn try_add_feature_mut(&mut self, feature_arc_mut: Arc<Mutex<dyn product_os_capabilities::Feature>>, base_path: Option<String>) -> crate::error::Result<()> {
match &self.controller {
None => {
let feature_locked = feature_arc_mut.lock();
Err(ProductOSServerError::ControllerError {
operation: "add_feature_mut".to_string(),
message: alloc::format!("Feature {} failed to add to server: no controller", feature_locked.identifier()),
})
}
Some(c) => {
let controller_unlocked = c.clone();
let mut controller = controller_unlocked.lock();
let feature_base_path = base_path.unwrap_or_default();
controller.add_feature_mut(feature_arc_mut.clone(), feature_base_path, &mut self.router).await;
let feature_locked = feature_arc_mut.lock();
#[cfg(feature = "core")]
tracing::info!("Feature {} successfully added to server", feature_locked.identifier());
Ok(())
}
}
}
}
#[cfg(feature = "executor_tokio")]
impl<S, X> ProductOSServer<S, TokioExecutor, X>
where
S: Clone + Send + Sync + 'static,
TokioExecutor: product_os_async_executor::Executor<X>, TokioExecutor: product_os_async_executor::ExecutorPerform<X>
{
#[allow(clippy::panic)]
pub fn new_with_state_with_config(config: crate::config::ServerConfig, state: S) -> Self {
let executor = match TokioExecutor::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create Tokio executor context: {:?}. Ensure a Tokio runtime is active.", e),
};
ProductOSServer::new_with_executor_with_state_with_config(Some(executor), config, state)
}
#[allow(clippy::panic)]
pub fn new_with_state(state: S) -> Self {
let executor = match TokioExecutor::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create Tokio executor context: {:?}. Ensure a Tokio runtime is active.", e),
};
ProductOSServer::new_with_executor_with_state(Some(executor), state)
}
}
impl<S, E, X> ProductOSServer<S, E, X>
where
S: Clone + Send + Sync + 'static,
E: product_os_async_executor::Executor<X> + product_os_async_executor::ExecutorPerform<X> + product_os_async_executor::Timer + 'static
{
#[allow(clippy::panic)]
pub fn new_with_executor_with_state_with_config(executor: Option<Arc<E>>, config: crate::config::ServerConfig, state: S) -> Self {
#[cfg(feature = "core")]
let _ = logging::set_global_logger(logging::define_logging(config.log_level()));
#[cfg(feature = "core")]
tracing::info!("Log Level: {}", config.log_level());
let mut router = ProductOSRouter::new_with_state(state);
#[cfg(feature = "security_certificates")]
let certificates = ProductOSServerCertificates::setup_certificates(&config.certificate);
let security_config: Option<product_os_security::Security> = config.security.as_ref()
.and_then(|v| {
serde_json::from_value(v.clone()).map_err(|e| {
#[cfg(feature = "core")]
tracing::warn!("Failed to parse security config: {}", e);
e
}).ok()
});
if let Some(ref security) = security_config {
apply_security_headers(&mut router, &config.network.host, security);
}
#[cfg(feature = "controller")]
let mut option_controller_mutex = None;
#[cfg(feature = "controller")]
{
let cc_config: Option<product_os_command_control::CommandControl> = config.command_control.as_ref()
.and_then(|v| serde_json::from_value(v.clone()).map_err(|e| {
#[cfg(feature = "core")]
tracing::warn!("Failed to parse command_control config: {}", e);
e
}).ok());
let store_config: Option<product_os_store::Stores> = config.store.as_ref()
.and_then(|v| serde_json::from_value(v.clone()).map_err(|e| {
#[cfg(feature = "core")]
tracing::warn!("Failed to parse store config: {}", e);
e
}).ok());
match &cc_config {
None => {}
Some(command_control) => {
if command_control.enable {
match &store_config {
None => {}
Some(store) => {
match &store.key_value {
None => {}
Some(key_value_config) => {
if let Ok(mut key_value) = ProductOSKeyValueStore::try_new(key_value_config).map_err(|e| {
#[cfg(feature = "core")]
tracing::error!("Failed to create key value store: {:?}", e);
}) {
match key_value.connect() {
Ok(_) => {
#[cfg(feature = "core")]
tracing::info!("Successfully connected to key value database: {:?}", key_value_config.kind);
}
Err(e) => {
#[cfg(feature = "core")]
tracing::error!("Connect to key value database error: {:?}", e);
}
}
key_value.set_group("product-os-node");
let key_value_store = Arc::new(key_value);
if let Some(relational_config) = &store.relational {
if let Ok(mut relational) = ProductOSRelationalStore::try_new(relational_config).map_err(|e| {
#[cfg(feature = "core")]
tracing::error!("Failed to create relational store: {:?}", e);
}) {
match relational.connect_sync() {
Ok(_) => {
#[cfg(feature = "core")]
tracing::info!("Successfully connected to relational database: {:?}", relational_config.kind);
}
Err(e) => {
#[cfg(feature = "core")]
tracing::error!("Connect to relational database error: {:?}", e);
}
};
#[cfg(any(feature = "framework_axum", feature = "flow_axum_compat"))]
{
command_handler::command_responder(&mut router);
feature_handler::feature_responder(&mut router, None);
}
#[cfg(any(feature = "postgres_store", feature = "sqlite_store"))]
let controller = ProductOSController::new(command_control.clone(), certificates.clone(), Some(key_value_store.clone()), Some(Arc::new(relational)));
#[cfg(not(any(feature = "postgres_store", feature = "sqlite_store")))]
let controller = {
let _ = relational;
ProductOSController::new(command_control.clone(), certificates.clone(), Some(key_value_store.clone()), None)
};
let controller_mutex = Arc::new(Mutex::new(controller));
#[cfg(any(feature = "framework_axum", feature = "flow_axum_compat"))]
router.add_extension(controller_mutex.clone());
#[cfg(feature = "core")]
tracing::info!("Controller added as extension middleware");
option_controller_mutex = Some(controller_mutex);
}
}
}
}
}
}
}
}
}
}
}
let executor = match executor {
Some(exec) => exec,
None => {
#[cfg(feature = "core")]
tracing::error!("Unable to create executor context - no executor available");
match E::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create default executor context: {:?}. An executor must be available.", e),
}
}
};
Self {
router,
config,
#[cfg(feature = "security_certificates")]
certificates,
#[cfg(feature = "controller")]
controller: option_controller_mutex,
executor,
executor_underlying: PhantomData,
#[cfg(feature = "compression")]
compression_applied: false,
}
}
#[allow(clippy::panic)]
pub fn new_with_executor_with_state(executor: Option<Arc<E>>, state: S) -> Self {
let config = crate::config::ServerConfig::new();
let router = ProductOSRouter::new_with_state(state);
#[cfg(feature = "core")]
{
let log_level = config.log_level();
let _ = logging::set_global_logger(logging::define_logging(log_level));
tracing::info!("Log Level: {}", config.log_level());
}
#[cfg(feature = "security_certificates")]
let certificates = ProductOSServerCertificates::setup_certificates(&config.certificate);
let executor = match executor {
Some(exec) => exec,
None => {
#[cfg(feature = "core")]
tracing::error!("Unable to create executor context - no executor available");
match E::context_sync() {
Ok(exec) => Arc::new(exec),
Err(e) => ::std::panic!("Failed to create default executor context: {:?}. An executor must be available.", e),
}
}
};
Self {
router,
config,
#[cfg(feature = "security_certificates")]
certificates,
#[cfg(feature = "controller")]
controller: None,
executor,
executor_underlying: PhantomData,
#[cfg(feature = "compression")]
compression_applied: false,
}
}
#[cfg(feature = "core")]
pub fn set_logging(&mut self, log_level: tracing::Level) {
let _ = logging::set_global_logger(logging::define_logging(log_level));
tracing::info!("Log Level: {}", self.config.log_level());
}
#[cfg(feature = "security_certificates")]
pub fn set_certificate(&mut self, certificate_config: &Option<crate::config::Certificate>) {
self.certificates = ProductOSServerCertificates::setup_certificates(certificate_config);
}
pub fn set_security(&mut self, security: Option<product_os_security::Security>) {
if let Some(security) = security {
apply_security_headers(&mut self.router, &self.config.network.host, &security);
}
}
#[cfg(feature = "compression")]
pub fn set_compression(&mut self, compression_config: Option<crate::config::Compression>) {
if let Some(ref compress) = compression_config {
apply_compression(&mut self.router, compress);
self.compression_applied = true;
}
}
#[cfg(feature = "compression")]
fn ensure_compression(&mut self) {
if self.compression_applied {
return;
}
if let Some(ref compress) = self.config.compression.clone() {
apply_compression(&mut self.router, &compress);
self.compression_applied = true;
}
}
#[cfg(feature = "controller")]
pub fn set_controller(&mut self, controller: Option<Arc<Mutex<ProductOSController>>>) {
self.controller = controller;
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.54", note = "Use try_add_service which returns Result instead of panicking")]
pub async fn add_service(&mut self, service_arc: Arc<dyn product_os_capabilities::Service>) {
self.try_add_service(service_arc).await
.unwrap_or_else(|e| ::std::panic!("{}", e));
}
#[cfg(feature = "controller")]
pub async fn try_add_service(&mut self, service_arc: Arc<dyn product_os_capabilities::Service>) -> crate::error::Result<()> {
match &self.controller {
None => {
Err(ProductOSServerError::ControllerError {
operation: "add_service".to_string(),
message: alloc::format!("Service {} failed to add to server: no controller", service_arc.identifier()),
})
}
Some(c) => {
let controller_unlocked = c.clone();
let mut controller = controller_unlocked.lock();
controller.add_service(service_arc.clone()).await;
#[cfg(feature = "core")]
tracing::info!("Service {} successfully added to server", service_arc.identifier());
Ok(())
}
}
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.54", note = "Use try_add_service_mut which returns Result instead of panicking")]
pub async fn add_service_mut(&mut self, service_arc_mut: Arc<Mutex<dyn product_os_capabilities::Service>>) {
self.try_add_service_mut(service_arc_mut).await
.unwrap_or_else(|e| ::std::panic!("{}", e));
}
#[cfg(feature = "controller")]
pub async fn try_add_service_mut(&mut self, service_arc_mut: Arc<Mutex<dyn product_os_capabilities::Service>>) -> crate::error::Result<()> {
match &self.controller {
None => {
let service_locked = service_arc_mut.lock();
Err(ProductOSServerError::ControllerError {
operation: "add_service_mut".to_string(),
message: alloc::format!("Service {} failed to add to server: no controller", service_locked.identifier()),
})
}
Some(c) => {
let controller_unlocked = c.clone();
let mut controller = controller_unlocked.lock();
controller.add_service_mut(service_arc_mut.clone()).await;
let service_locked = service_arc_mut.lock();
#[cfg(feature = "core")]
tracing::info!("Service {} successfully added to server", service_locked.identifier());
Ok(())
}
}
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.53", note = "Use try_get_controller which returns Result instead of panicking")]
pub fn get_controller(&self) -> Arc<Mutex<ProductOSController>> {
self.try_get_controller()
.unwrap_or_else(|e| ::std::panic!("{}", e))
}
#[cfg(feature = "controller")]
pub fn try_get_controller(&self) -> crate::error::Result<Arc<Mutex<ProductOSController>>> {
match &self.controller {
None => Err(ProductOSServerError::ControllerError {
operation: "get_controller".to_string(),
message: "No controller configured on this server".to_string(),
}),
Some(c) => Ok(c.clone()),
}
}
pub fn get_router(&mut self) -> &mut ProductOSRouter<S> {
&mut self.router
}
#[cfg(feature = "framework_axum")]
#[deprecated(since = "0.0.55", note = "Use add_get/add_post/add_handler instead for framework-agnostic routing")]
pub fn add_route(&mut self, path: &str, service_handler: product_os_router::MethodRouter<S>) {
self.router.add_route(path, service_handler);
}
#[cfg(feature = "framework_axum")]
#[deprecated(since = "0.0.55", note = "Use set_fallback_handler instead for framework-agnostic routing")]
pub fn set_fallback(&mut self, service_handler: product_os_router::MethodRouter<S>) {
self.router.set_fallback(service_handler);
}
pub fn add_get<H, T>(&mut self, path: &str, handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_get(path, handler);
}
pub fn add_post<H, T>(&mut self, path: &str, handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_post(path, handler);
}
pub fn add_handler<H, T>(&mut self, path: &str, method: Method, handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_handler(path, method, handler);
}
pub fn set_fallback_handler<H, T>(&mut self, handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.set_fallback_handler(handler);
}
#[cfg(feature = "cors")]
pub fn add_cors_handler<H, T>(&mut self, path: &str, method: Method, handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_cors_handler(path, method, handler);
}
#[cfg(all(feature = "ws", any(feature = "framework_axum", feature = "flow_axum_compat")))]
pub fn add_ws_handler<H, T>(&mut self, path: &str, ws_handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_ws_handler(path, ws_handler);
}
#[cfg(all(feature = "ws", feature = "framework_flow", not(feature = "framework_axum"), not(feature = "flow_axum_compat")))]
pub fn add_ws_handler<F, Fut>(&mut self, path: &str, handler: F)
where
F: Fn(product_os_router::flow_impl::ws::WebSocket) -> Fut + Send + Sync + Clone + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.router.add_ws_handler(path, handler);
}
#[cfg(feature = "sse")]
pub fn add_sse_handler<H, T>(&mut self, path: &str, sse_handler: H)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_sse_handler(path, sse_handler);
}
pub fn add_handlers<H, T>(&mut self, path: &str, handlers: BTreeMap<Method, H>)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_handlers(path, handlers);
}
#[cfg(feature = "cors")]
pub fn add_cors_handlers<H, T>(&mut self, path: &str, handlers: BTreeMap<Method, H>)
where
H: Handler<T, S>,
T: 'static
{
self.router.add_cors_handlers(path, handlers);
}
#[cfg(feature = "cors")]
pub fn add_cors_middleware(&mut self) {
self.router.add_cors_middleware();
}
#[cfg(all(feature = "core", feature = "framework_axum"))]
pub fn add_middleware<L, ResBody>(&mut self, middleware: L)
where
L: Layer<Route> + Clone + Send + Sync + 'static,
L::Service: Service<Request<Body>, Response = Response<ResBody>> + Clone + Send + Sync + 'static,
<L::Service as Service<Request<Body>>>::Future: Send + 'static,
<L::Service as Service<Request<Body>>>::Error: Into<BoxError> + 'static,
ResBody: HttpBody<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
{
self.router.add_middleware(middleware);
}
#[cfg(all(feature = "core", feature = "framework_flow", feature = "flow_axum_compat"))]
pub fn add_middleware<L, ResBody>(&mut self, middleware: L)
where
L: Layer<product_os_router::flow_impl::router::NextService<S>>
+ Clone + Send + Sync + 'static,
L::Service: Service<Request<Body>, Response = Response<ResBody>>
+ Clone + Send + Sync + 'static,
<L::Service as Service<Request<Body>>>::Future: Send + 'static,
<L::Service as Service<Request<Body>>>::Error: Into<BoxError> + Send + 'static,
ResBody: HttpBody<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
{
self.router.add_middleware(middleware);
}
pub fn set_router(&mut self, router: ProductOSRouter<S>)
where
S: Clone + Send + Sync + 'static
{
self.router = router;
}
#[cfg(all(feature = "dual_server", feature = "framework_axum"))]
pub async fn create_dual_service_server(&mut self, serve_on_main_thread: bool, listen_all_interfaces: bool, custom_port: Option<u16>, custom_router: Option<ProductOSRouter<S>>, with_connect_info: bool, _force_secure: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
#[cfg(feature = "tls")]
{
let certificates = Some(self.certificates.to_owned());
let router: AxumRouter = match custom_router {
None => self.router.clone().into_axum_router(),
Some(r) => r.into_axum_router()
};
let address = self.config.socket_address(custom_port, listen_all_interfaces);
if serve_on_main_thread {
dual_server::create_dual_tokio_service(address, certificates, router, with_connect_info).await?;
}
else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
match dual_server::create_dual_tokio_service(address, certificates, router, with_connect_info).await {
Ok(_) => {}
Err(e) => tracing::error!("Error starting HTTPS server: {}", e)
}
}) {
tracing::error!("Failed to spawn dual server task: {}", e);
}
}
}
#[cfg(not(feature = "tls"))]
{
tracing::info!("TLS feature is not enabled - please include the \"tls\" feature in your toml config file");
}
Ok(())
}
#[cfg(feature = "framework_axum")]
pub async fn create_https_server(&mut self, serve_on_main_thread: bool, listen_all_interfaces: bool, custom_port: Option<u16>, custom_router: Option<ProductOSRouter<S>>, with_connect_info: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
#[cfg(all(feature = "tls", feature = "executor_tokio"))]
{
let certificates = Some(self.certificates.to_owned());
let router: AxumRouter = match custom_router {
None => self.router.clone().into_axum_router(),
Some(r) => r.into_axum_router()
};
let address = self.config.socket_address(custom_port, listen_all_interfaces);
if serve_on_main_thread {
https_server::create_https_tokio_service(address, certificates, router, with_connect_info).await?;
}
else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
match https_server::create_https_tokio_service(address, certificates, router, with_connect_info).await {
Ok(_) => {}
Err(e) => tracing::error!("Error starting HTTPS server: {}", e)
}
}) {
tracing::error!("Failed to spawn HTTPS server task: {}", e);
}
}
}
#[cfg(not(feature = "tls"))]
{
let _ = (serve_on_main_thread, listen_all_interfaces, custom_port, custom_router, with_connect_info);
#[cfg(feature = "core")]
tracing::info!("TLS feature is not enabled - please include the \"tls\" feature in your toml config file");
}
Ok(())
}
#[cfg(feature = "framework_axum")]
pub async fn create_http_server(&mut self, serve_on_main_thread: bool, listen_all_interfaces: bool, custom_port: Option<u16>, custom_router: Option<ProductOSRouter<S>>, with_connect_info: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
#[cfg(feature = "executor_tokio")]
{
let router: AxumRouter = match custom_router {
None => self.router.clone().into_axum_router(),
Some(r) => r.into_axum_router()
};
let address = self.config.socket_address(custom_port, listen_all_interfaces);
if serve_on_main_thread {
http_server::create_http_tokio_service(address, router, with_connect_info).await?;
}
else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
match http_server::create_http_tokio_service(address, router, with_connect_info).await {
Ok(_) => {}
Err(e) => tracing::error!("Error starting HTTP server: {}", e)
}
}) {
tracing::error!("Failed to spawn HTTP server task: {}", e);
}
}
}
#[cfg(not(feature = "executor_tokio"))]
{
let _ = (serve_on_main_thread, listen_all_interfaces, custom_port, custom_router, with_connect_info);
#[cfg(feature = "core")]
tracing::info!("executor_tokio feature is not enabled for HTTP server");
}
Ok(())
}
#[cfg(all(feature = "framework_flow", feature = "executor_tokio"))]
pub async fn create_https_server(
&mut self,
serve_on_main_thread: bool,
listen_all_interfaces: bool,
custom_port: Option<u16>,
custom_router: Option<ProductOSRouter<S>>,
with_connect_info: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
#[cfg(feature = "tls")]
{
let certs = Some(self.certificates.to_owned());
let router = custom_router.unwrap_or_else(|| self.router.clone());
let address = self.config.socket_address(custom_port, listen_all_interfaces);
if serve_on_main_thread {
flow_https_server::create_flow_https_service(address, certs, router, with_connect_info).await?;
} else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_https_server::create_flow_https_service(address, certs, router, with_connect_info).await {
tracing::error!("Error starting Flow HTTPS server: {}", e);
}
}) {
tracing::error!("Failed to spawn Flow HTTPS server task: {}", e);
}
}
}
#[cfg(not(feature = "tls"))]
{
let _ = (serve_on_main_thread, listen_all_interfaces, custom_port, custom_router, with_connect_info);
#[cfg(feature = "core")]
tracing::info!("TLS feature is not enabled - please include the \"tls\" feature in your toml config file");
}
Ok(())
}
#[cfg(all(feature = "framework_flow", feature = "executor_tokio"))]
pub async fn create_http_server(
&mut self,
serve_on_main_thread: bool,
listen_all_interfaces: bool,
custom_port: Option<u16>,
custom_router: Option<ProductOSRouter<S>>,
with_connect_info: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
let router = custom_router.unwrap_or_else(|| self.router.clone());
let address = self.config.socket_address(custom_port, listen_all_interfaces);
if serve_on_main_thread {
flow_http_server::create_flow_http_service(address, router, with_connect_info).await?;
} else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_http_server::create_flow_http_service(address, router, with_connect_info).await {
tracing::error!("Error starting Flow HTTP server: {}", e);
}
}) {
tracing::error!("Failed to spawn Flow HTTP server task: {}", e);
}
}
Ok(())
}
pub async fn init_stores(&mut self) {
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.54", note = "Use try_get_relational_store which returns Result instead of panicking")]
pub fn get_relational_store(&mut self) -> Result<Arc<ProductOSRelationalStore>, ()> {
self.try_get_relational_store().map_err(|_| ())
}
#[cfg(feature = "controller")]
pub fn try_get_relational_store(&mut self) -> crate::error::Result<Arc<ProductOSRelationalStore>> {
let controller_arc = self.try_get_controller()?;
controller_arc.try_lock_for(CONTROLLER_LOCK_TIMEOUT)
.map(|mut controller| controller.get_relational_store())
.ok_or_else(|| ProductOSServerError::LockError { resource: "controller".to_string() })
}
#[cfg(feature = "controller")]
#[deprecated(since = "0.0.54", note = "Use try_get_key_value_store which returns Result instead of panicking")]
pub fn get_key_value_store(&mut self) -> Result<Arc<ProductOSKeyValueStore>, ()> {
self.try_get_key_value_store().map_err(|_| ())
}
#[cfg(feature = "controller")]
pub fn try_get_key_value_store(&mut self) -> crate::error::Result<Arc<ProductOSKeyValueStore>> {
let controller_arc = self.try_get_controller()?;
controller_arc.try_lock_for(CONTROLLER_LOCK_TIMEOUT)
.map(|mut controller| controller.get_key_value_store())
.ok_or_else(|| ProductOSServerError::LockError { resource: "controller".to_string() })
}
pub fn get_config(&self) -> crate::config::ServerConfig {
self.config.clone()
}
pub fn update_config(&mut self, config: crate::config::ServerConfig) {
self.config = config;
}
#[allow(clippy::panic)]
#[deprecated(since = "0.0.53", note = "Use try_get_base_url() which returns Result instead of panicking")]
pub fn get_base_url(&self) -> Uri {
self.try_get_base_url()
.unwrap_or_else(|e| ::std::panic!("Failed to parse base URL: {:?}", e))
}
pub fn try_get_base_url(&self) -> crate::error::Result<Uri> {
let url = self.config.try_url_address()?;
Uri::from_str(&url)
.map_err(|e| ProductOSServerError::ConfigurationError {
component: "base_url".to_string(),
message: alloc::format!("Failed to parse URL as URI: {}", e),
})
}
#[cfg(all(feature = "core", feature = "framework_axum"))]
pub async fn start(&mut self, serve_on_main_thread: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
let with_connect_info = self.config.network.connect_info;
if self.config.is_secure() {
#[cfg(feature = "controller")]
{
if self.config.network.allow_insecure {
if !&self.config.network.insecure_use_different_port {
#[cfg(feature = "dual_server")]
self.create_dual_service_server(false, self.config.network.listen_all_interfaces, Some(self.config.insecure_port()), None, false, self.config.network.insecure_force_secure).await?;
}
else {
if self.config.network.insecure_force_secure {
let secure_cfg = ForceSecureConfig {
host: self.config.network.host.clone(),
port: self.config.network.port,
};
let router = AxumRouter::new()
.fallback(product_os_router::MethodRouter::new()
.get(force_secure_handler)
.post(force_secure_handler)
.put(force_secure_handler)
.patch(force_secure_handler)
.delete(force_secure_handler)
.trace(force_secure_handler)
.head(force_secure_handler)
.options(force_secure_handler))
.layer(axum::extract::Extension(secure_cfg));
let address = self.config.socket_address(Some(self.config.insecure_port()), self.config.network.listen_all_interfaces);
http_server::create_http_tokio_service(address, router, with_connect_info).await?;
}
else {
self.create_http_server(false, self.config.network.listen_all_interfaces, Some(self.config.insecure_port()), None, with_connect_info).await?;
}
}
}
self.create_https_server(false, self.config.network.listen_all_interfaces, None, None, with_connect_info).await?;
}
#[cfg(not(feature = "controller"))]
{
if self.config.network.allow_insecure {
if self.config.network.insecure_force_secure {
let secure_cfg = ForceSecureConfig {
host: self.config.network.host.clone(),
port: self.config.network.port,
};
let router = AxumRouter::new()
.fallback(product_os_router::MethodRouter::new()
.get(force_secure_handler)
.post(force_secure_handler)
.put(force_secure_handler)
.patch(force_secure_handler)
.delete(force_secure_handler)
.trace(force_secure_handler)
.head(force_secure_handler)
.options(force_secure_handler))
.layer(axum::extract::Extension(secure_cfg));
let address = self.config.socket_address(Some(self.config.network.insecure_port), self.config.network.listen_all_interfaces);
http_server::create_http_tokio_service(address, router, with_connect_info).await?;
}
else {
self.create_http_server(serve_on_main_thread, self.config.network.listen_all_interfaces, Some(self.config.network.insecure_port), None, with_connect_info).await?;
}
}
self.create_https_server(serve_on_main_thread, self.config.network.listen_all_interfaces, None, None, with_connect_info).await?;
}
}
else {
#[cfg(feature = "controller")]
{
self.create_http_server(false, self.config.network.listen_all_interfaces, None, None, with_connect_info).await?;
}
#[cfg(not(feature = "controller"))]
{
self.create_http_server(serve_on_main_thread, self.config.network.listen_all_interfaces, None, None, with_connect_info).await?;
}
};
#[cfg(feature = "controller")]
{
self.init_stores().await;
let cc_cfg: Option<product_os_command_control::CommandControl> = self.parse_command_control_config();
let command_control_config = cc_cfg.ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
alloc::format!("No controller config").into()
})?;
if command_control_config.enable {
let controller_arc = self.try_get_controller().map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
alloc::format!("{}", e).into()
})?;
let controller_unlocked = controller_arc.clone();
let executor = self.executor.clone();
if serve_on_main_thread {
product_os_command_control::run_controller(controller_unlocked, executor).await;
match tokio::signal::ctrl_c().await {
Ok(()) => {
#[cfg(feature = "core")]
tracing::info!("Shutdown signal received, stopping server...");
}
Err(e) => {
#[cfg(feature = "core")]
tracing::error!("Failed to listen for shutdown signal: {:?}", e);
}
}
}
else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
product_os_command_control::run_controller(controller_unlocked, executor).await;
}) {
#[cfg(feature = "core")]
tracing::error!("Failed to spawn controller task: {}", e);
}
}
}
}
Ok(())
}
#[cfg(all(feature = "core", feature = "framework_flow", not(feature = "framework_axum"), feature = "executor_tokio"))]
pub async fn start(&mut self, serve_on_main_thread: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "compression")]
self.ensure_compression();
let with_connect_info = self.config.network.connect_info;
let router = self.router.clone();
if self.config.is_secure() {
if self.config.network.allow_insecure {
let insecure_addr = self.config.socket_address(
Some(self.config.insecure_port()),
self.config.network.listen_all_interfaces,
);
let insecure_router = if self.config.network.insecure_force_secure {
build_flow_force_secure_router::<S>(
ForceSecureConfig {
host: self.config.network.host.clone(),
port: self.config.network.port,
},
self.router.get_state(),
)
} else {
router.clone()
};
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_http_server::create_flow_http_service(insecure_addr, insecure_router, with_connect_info).await {
tracing::error!("Error starting insecure Flow HTTP server: {}", e);
}
}) {
tracing::error!("Failed to spawn insecure HTTP task: {}", e);
}
}
#[cfg(feature = "tls")]
{
let address = self.config.socket_address(None, self.config.network.listen_all_interfaces);
let certs = Some(self.certificates.to_owned());
if serve_on_main_thread {
#[cfg(not(feature = "controller"))]
{
flow_https_server::create_flow_https_service(address, certs, router, with_connect_info).await?;
}
#[cfg(feature = "controller")]
{
let https_router = router.clone();
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_https_server::create_flow_https_service(address, certs, https_router, with_connect_info).await {
tracing::error!("Error starting Flow HTTPS server: {}", e);
}
}) {
tracing::error!("Failed to spawn HTTPS task: {}", e);
}
}
} else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_https_server::create_flow_https_service(address, certs, router, with_connect_info).await {
tracing::error!("Error starting Flow HTTPS server: {}", e);
}
}) {
tracing::error!("Failed to spawn HTTPS task: {}", e);
}
}
}
#[cfg(not(feature = "tls"))]
{
tracing::info!("TLS feature is not enabled - please include the \"tls\" feature in your toml config file");
}
} else {
let address = self.config.socket_address(None, self.config.network.listen_all_interfaces);
if serve_on_main_thread {
#[cfg(not(feature = "controller"))]
{
flow_http_server::create_flow_http_service(address, router, with_connect_info).await?;
}
#[cfg(feature = "controller")]
{
let http_router = router.clone();
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_http_server::create_flow_http_service(address, http_router, with_connect_info).await {
tracing::error!("Error starting Flow HTTP server: {}", e);
}
}) {
tracing::error!("Failed to spawn HTTP task: {}", e);
}
}
} else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
if let Err(e) = flow_http_server::create_flow_http_service(address, router, with_connect_info).await {
tracing::error!("Error starting Flow HTTP server: {}", e);
}
}) {
tracing::error!("Failed to spawn HTTP task: {}", e);
}
}
}
#[cfg(feature = "controller")]
{
self.init_stores().await;
let cc_cfg: Option<product_os_command_control::CommandControl> = self.parse_command_control_config();
let command_control_config = cc_cfg.ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
alloc::format!("No controller config").into()
})?;
if command_control_config.enable {
let controller_arc = self.try_get_controller().map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
alloc::format!("{}", e).into()
})?;
let controller_unlocked = controller_arc.clone();
let executor = self.executor.clone();
if serve_on_main_thread {
product_os_command_control::run_controller(controller_unlocked, executor).await;
match tokio::signal::ctrl_c().await {
Ok(()) => {
tracing::info!("Shutdown signal received, stopping server...");
}
Err(e) => {
tracing::error!("Failed to listen for shutdown signal: {:?}", e);
}
}
} else {
if let Err(e) = server_executor::ServerExecutor::spawn(self.executor.clone(), async move {
product_os_command_control::run_controller(controller_unlocked, executor).await;
}) {
tracing::error!("Failed to spawn controller task: {}", e);
}
}
}
}
Ok(())
}
#[cfg(feature = "controller")]
fn parse_command_control_config(&self) -> Option<product_os_command_control::CommandControl> {
self.config.command_control.as_ref()
.and_then(|v| serde_json::from_value(v.clone()).map_err(|e| {
#[cfg(feature = "core")]
tracing::warn!("Failed to parse command_control config: {}", e);
e
}).ok())
}
#[cfg(feature = "controller")]
fn require_enabled_command_control(&self, operation: &str) -> Result<product_os_command_control::CommandControl, ProductOSServerError> {
let config = self.parse_command_control_config()
.ok_or_else(|| ProductOSServerError::ControllerError {
operation: operation.to_string(),
message: "No controller config".to_string(),
})?;
if !config.enable {
return Err(ProductOSServerError::ControllerError {
operation: operation.to_string(),
message: "Controller not enabled".to_string(),
});
}
Ok(config)
}
#[cfg(feature = "controller")]
pub async fn start_services(&mut self) -> Result<(), ProductOSServerError> {
self.require_enabled_command_control("start_services")?;
let controller_arc = self.try_get_controller()?;
let mut controller = controller_arc.try_lock_for(CONTROLLER_LOCK_TIMEOUT)
.ok_or_else(|| ProductOSServerError::LockError { resource: "controller".to_string() })?;
controller.start_services().await
.map_err(|e| ProductOSServerError::ControllerError {
operation: "start_services".to_string(),
message: alloc::format!("Failed to start services: {:?}", e),
})
}
#[cfg(feature = "controller")]
pub async fn start_service(&mut self, identifier: &str) -> Result<(), ProductOSServerError> {
self.require_enabled_command_control("start_service")?;
let controller_arc = self.try_get_controller()?;
let mut controller = controller_arc.try_lock_for(CONTROLLER_LOCK_TIMEOUT)
.ok_or_else(|| ProductOSServerError::LockError { resource: "controller".to_string() })?;
controller.start_service(identifier).await
.map_err(|e| ProductOSServerError::ControllerError {
operation: "start_service".to_string(),
message: alloc::format!("Failed to start service '{}': {:?}", identifier, e),
})
}
#[cfg(feature = "controller")]
pub async fn stop_service(&mut self, identifier: &str) -> Result<(), ProductOSServerError> {
self.require_enabled_command_control("stop_service")?;
let controller_arc = self.try_get_controller()?;
let mut controller = controller_arc.try_lock_for(CONTROLLER_LOCK_TIMEOUT)
.ok_or_else(|| ProductOSServerError::LockError { resource: "controller".to_string() })?;
controller.stop_service(identifier).await
.map_err(|e| ProductOSServerError::ControllerError {
operation: "stop_service".to_string(),
message: alloc::format!("Failed to stop service '{}': {:?}", identifier, e),
})
}
}
#[cfg(feature = "core")]
#[derive(Clone, Debug)]
struct ForceSecureConfig {
host: String,
port: u16,
}
#[cfg(all(feature = "core", feature = "framework_axum"))]
async fn force_secure_handler(
Extension(secure_cfg): Extension<ForceSecureConfig>,
request: Request<Body>,
) -> Response<BodyBytes> {
let uri = request.uri();
let path = uri.path();
let query = uri.query().map(|q| alloc::format!("?{}", q)).unwrap_or_default();
let https_url = if secure_cfg.port == 443 {
alloc::format!("https://{}{}{}", secure_cfg.host, path, query)
} else {
alloc::format!("https://{}:{}{}{}", secure_cfg.host, secure_cfg.port, path, query)
};
let (parts, _) = axum::response::Redirect::permanent(https_url.as_str()).into_response().into_parts();
let mut response_builder = Response::builder().status(parts.status);
for (key, value) in parts.headers.iter() {
response_builder = response_builder.header(key.to_owned(), value.to_owned());
}
response_builder.body(BodyBytes::empty()).unwrap_or_else(|_e| Response::new(BodyBytes::empty()))
}
#[cfg(all(feature = "core", feature = "framework_flow", not(feature = "framework_axum")))]
fn build_flow_force_secure_router<S: Clone + Send + Sync + 'static>(
secure_cfg: ForceSecureConfig,
state: S,
) -> ProductOSRouter<S> {
let mut redirect_router = ProductOSRouter::<S>::new_with_state(state);
let cfg = secure_cfg;
redirect_router.set_fallback_handler(move |request: Request<Body>| {
let cfg = cfg.clone();
async move {
let uri = request.uri();
let path = uri.path();
let query = uri.query()
.map(|q| alloc::format!("?{}", q))
.unwrap_or_default();
let https_url = if cfg.port == 443 {
alloc::format!("https://{}{}{}", cfg.host, path, query)
} else {
alloc::format!("https://{}:{}{}{}", cfg.host, cfg.port, path, query)
};
Response::builder()
.status(StatusCode::MOVED_PERMANENTLY)
.header("location", https_url)
.body(Body::empty())
.unwrap()
}
});
redirect_router
}