product-os-server 0.0.55

Product OS : Server provides a full functioning advanced server capable of acting as a web server, command and control distributed network, authentication server, crawling server and more. Fully featured with high level of flexibility.
Documentation
//! Feature handler module
//!
//! This module provides HTTP endpoint handlers for feature-based routing in the
//! command-and-control distributed networking system.
//!
//! # Overview
//!
//! The feature handler enables dynamic routing to registered features based on
//! semantic versioning paths. Features are modular capabilities that can be
//! dynamically added to the server at runtime.
//!
//! # Route Pattern
//!
//! Routes follow the pattern: `/{prefix}/:semver/*path`
//!
//! - `prefix`: Optional API prefix (defaults to "api")
//! - `semver`: Semantic version for API versioning
//! - `path`: The feature path to route to
//!
//! # HTTP Methods
//!
//! All standard HTTP methods are supported: GET, POST, PUT, PATCH, DELETE, TRACE, HEAD
//!
//! GET, HEAD, and TRACE use a handler that does not require a JSON body.
//! POST, PUT, PATCH, and DELETE use a handler that accepts a JSON body.

use std::prelude::v1::*;

use std::collections::BTreeMap;
use std::sync::Arc;
use parking_lot::Mutex;
use product_os_capabilities::What;
#[cfg(feature = "controller")]
use product_os_command_control::ProductOSController;
use product_os_router::{Body, Response, IntoResponse, StatusCode, Extension, Path};
use serde_json::Value;

use crate::CONTROLLER_LOCK_TIMEOUT;

/// Registers feature-based API routes on the provided router.
///
/// This sets up catch-all routes at `/{prefix}/:semver/*path` for all HTTP methods.
/// Methods with bodies (POST, PUT, PATCH, DELETE) use `api_features_with_body`.
/// Methods without bodies (GET, HEAD, TRACE) use `api_features_no_body`.
///
/// # Arguments
///
/// * `router` - The router to add feature routes to
/// * `prefix` - Optional API prefix (defaults to "api")
#[cfg(feature = "controller")]
pub fn feature_responder<S>(router: &mut product_os_router::ProductOSRouter<S>, prefix: Option<String>)
where
    S: Clone + Send + Sync + 'static,
{
    let mut path = "/".to_string();

    match prefix {
        None => path.push_str("api"),
        Some(p) => {
            path.push_str(p.as_str());
        }
    }

    path.push_str("/:semver");
    path.push_str("/*path");

    router.add_handler(path.as_str(), product_os_router::Method::GET, api_features_no_body);
    router.add_handler(path.as_str(), product_os_router::Method::HEAD, api_features_no_body);
    router.add_handler(path.as_str(), product_os_router::Method::TRACE, api_features_no_body);

    router.add_handler(path.as_str(), product_os_router::Method::POST, api_features_with_body);
    router.add_handler(path.as_str(), product_os_router::Method::PUT, api_features_with_body);
    router.add_handler(path.as_str(), product_os_router::Method::PATCH, api_features_with_body);
    router.add_handler(path.as_str(), product_os_router::Method::DELETE, api_features_with_body);
}

/// Builds a JSON error response with the given status code and message.
#[cfg(feature = "controller")]
fn error_response(status: StatusCode, message: &str) -> Response<Body> {
    Response::builder()
        .status(status)
        .header("content-type", "application/json")
        .body(Body::from(alloc::format!("{{\"error\":\"{}\"}}", message)))
        .unwrap_or_else(|_| {
            let mut resp = Response::new(Body::from("{}"));
            *resp.status_mut() = status;
            resp
        })
        .into_response()
}

/// Dispatches a feature request using the controller, with an optional JSON payload.
#[cfg(feature = "controller")]
async fn dispatch_feature(
    params: &BTreeMap<String, String>,
    controller_mutex: Arc<Mutex<ProductOSController>>,
    payload: Option<Value>,
) -> Response<Body> {
    let path = match params.get("path") {
        Some(p) => p,
        None => return error_response(StatusCode::BAD_REQUEST, "Missing path parameter"),
    };
    let semver = match params.get("semver") {
        Some(s) => s,
        None => return error_response(StatusCode::BAD_REQUEST, "Missing semver parameter"),
    };

    tracing::info!("Feature path requested: {} on version: {}", path, semver);

    match controller_mutex.try_lock_for(CONTROLLER_LOCK_TIMEOUT) {
        None => error_response(StatusCode::GATEWAY_TIMEOUT, "Controller lock timeout"),
        Some(controller_locked) => {
            let me_features = controller_locked.get_registry().get_me().get_features();

            match me_features.find(path.as_str()) {
                None => {
                    tracing::error!("Not found: {}", path);
                    error_response(StatusCode::NOT_FOUND, "Feature not found")
                }
                Some((registry_feature, _)) => match &registry_feature.feature {
                    Some(feature) => {
                        feature
                            .request(
                                &What::Characteristic(path.to_owned()),
                                &payload,
                                semver,
                            )
                            .await
                    }
                    None => match &registry_feature.feature_mut {
                        Some(feature_mut) => {
                            match feature_mut.try_lock_for(CONTROLLER_LOCK_TIMEOUT) {
                                Some(mut feature) => {
                                    feature
                                        .request_mut(
                                            &What::Characteristic(path.to_owned()),
                                            &payload,
                                            semver,
                                        )
                                        .await
                                }
                                None => error_response(
                                    StatusCode::SERVICE_UNAVAILABLE,
                                    "Feature lock timeout",
                                ),
                            }
                        }
                        None => error_response(
                            StatusCode::SERVICE_UNAVAILABLE,
                            "Feature not available",
                        ),
                    },
                },
            }
        }
    }
}

/// Handler for HTTP methods that carry a JSON body (POST, PUT, PATCH, DELETE).
#[cfg(feature = "controller")]
async fn api_features_with_body(
    Path(params): Path<BTreeMap<String, String>>,
    Extension(controller_mutex): Extension<Arc<Mutex<ProductOSController>>>,
    axum::Json(payload): axum::Json<Value>,
) -> Response<Body> {
    dispatch_feature(&params, controller_mutex, Some(payload)).await
}

/// Handler for HTTP methods that typically have no body (GET, HEAD, TRACE).
#[cfg(feature = "controller")]
async fn api_features_no_body(
    Path(params): Path<BTreeMap<String, String>>,
    Extension(controller_mutex): Extension<Arc<Mutex<ProductOSController>>>,
) -> Response<Body> {
    dispatch_feature(&params, controller_mutex, None).await
}