tgin 0.1.0

Universal traffic glue: a Rust construction kit for gateways — HTTP, Telegram bots, RabbitMQ, Kafka, WebSocket tunnels
Documentation
use std::net::SocketAddr;

use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::oneshot;
use async_trait::async_trait;
use axum::{
    Router,
    extract::{ConnectInfo, State},
    http::{Method, StatusCode},
    routing::any,
    response::IntoResponse,
};

use crate::base::{Envelope, Ingress, Runnable, SendError};
use crate::shared::server::HttpServer;
use crate::types::request::{RequestData, ResponseData};

#[derive(Clone)]
struct HandlerState {
    tx: Sender<Envelope<RequestData, ResponseData>>,
    method: Option<Method>,
}

pub struct HttpIngress {
    server: HttpServer,
    path: Option<String>,
    method: Option<Method>,
}

impl HttpIngress {
    pub fn post(server: &HttpServer, path: &str) -> Self {
        Self { server: server.clone(), path: Some(path.to_string()), method: Some(Method::POST) }
    }

    pub fn get(server: &HttpServer, path: &str) -> Self {
        Self { server: server.clone(), path: Some(path.to_string()), method: Some(Method::GET) }
    }

    pub fn any(server: &HttpServer, path: &str) -> Self {
        Self { server: server.clone(), path: Some(path.to_string()), method: None }
    }

    pub fn catch_all(server: &HttpServer) -> Self {
        Self { server: server.clone(), path: None, method: None }
    }
}

#[async_trait]
impl Ingress<RequestData, ResponseData> for HttpIngress {
    fn services(&self) -> Vec<Box<dyn Runnable>> {
        vec![Box::new(self.server.clone())]
    }

    async fn setup(&mut self, tx: Sender<Envelope<RequestData, ResponseData>>) {
        let state = HandlerState { tx, method: self.method.clone() };
        let router = match &self.path {
            Some(path) => Router::new().route(path, any(handler)),
            None => Router::new().fallback(any(handler)),
        };
        self.server.register(router.with_state(state)).await;
    }
}

async fn handler(
    State(state): State<HandlerState>,
    ConnectInfo(peer): ConnectInfo<SocketAddr>,
    mut data: RequestData,
) -> impl IntoResponse {
    data.client_ip = Some(peer.ip());

    if let Some(method) = &state.method {
        if data.method != *method {
            return (StatusCode::METHOD_NOT_ALLOWED, "").into_response();
        }
    }

    let (back_tx, back_rx) = oneshot::channel();

    match state.tx.try_send(Envelope::backward(data, back_tx)) {
        Ok(()) => {}
        Err(TrySendError::Full(_)) => return (StatusCode::TOO_MANY_REQUESTS, "").into_response(),
        Err(TrySendError::Closed(_)) => return (StatusCode::SERVICE_UNAVAILABLE, "").into_response(),
    }

    match back_rx.await {
        Ok(Ok(response)) => {
            let mut builder = axum::response::Response::builder().status(response.status);

            if let Some(headers) = builder.headers_mut() {
                *headers = response.headers;
            }

            builder
                .body(axum::body::Body::from(response.body))
                .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "").into_response())
        }
        Ok(Err(SendError::Overloaded)) => (StatusCode::SERVICE_UNAVAILABLE, "").into_response(),
        Ok(Err(SendError::DeadlineExceeded)) => (StatusCode::GATEWAY_TIMEOUT, "").into_response(),
        Ok(Err(_)) => (StatusCode::BAD_GATEWAY, "").into_response(),
        Err(_) => (StatusCode::BAD_GATEWAY, "").into_response(),
    }
}