tgin 0.1.0

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

use async_trait::async_trait;
use axum::http::StatusCode;
use futures::{SinkExt, StreamExt};
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::oneshot;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_util::sync::CancellationToken;

use crate::base::{Envelope, Ingress, SendError};
use crate::types::request::{RequestData, ResponseData};
use crate::types::tunnel::{decode_request, encode_response};

pub struct TunnelIngress {
    url: String,
    token: String,
    reconnect: Duration,
}

impl TunnelIngress {
    pub fn new(url: &str, token: &str) -> Self {
        Self {
            url: url.to_string(),
            token: token.to_string(),
            reconnect: Duration::from_secs(5),
        }
    }

    pub fn reconnect(mut self, delay: Duration) -> Self {
        self.reconnect = delay;
        self
    }

    async fn session(&self, tx: &Sender<Envelope<RequestData, ResponseData>>, shutdown: &CancellationToken) {
        let Ok(mut request) = self.url.as_str().into_client_request() else {
            return;
        };
        let Ok(authorization) = format!("Bearer {}", self.token).parse() else {
            return;
        };
        request.headers_mut().insert("authorization", authorization);

        let Ok((socket, _)) = connect_async(request).await else {
            return;
        };
        let (mut sink, mut stream) = socket.split();
        let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(256);

        let writer = tokio::spawn(async move {
            while let Some(message) = out_rx.recv().await {
                if sink.send(Message::Binary(message)).await.is_err() {
                    return;
                }
            }
        });

        loop {
            let message = tokio::select! {
                _ = shutdown.cancelled() => break,
                next = stream.next() => match next {
                    Some(Ok(message)) => message,
                    _ => break,
                },
            };

            let Message::Binary(payload) = message else {
                continue;
            };
            let Some((id, data)) = decode_request(&payload) else {
                continue;
            };

            let (reply_tx, reply_rx) = oneshot::channel();

            if tx.send(Envelope::backward(data, reply_tx)).await.is_err() {
                break;
            }

            let out = out_tx.clone();
            tokio::spawn(async move {
                let response = match reply_rx.await {
                    Ok(Ok(response)) => response,
                    Ok(Err(SendError::DeadlineExceeded)) => ResponseData {
                        status: StatusCode::GATEWAY_TIMEOUT,
                        ..Default::default()
                    },
                    _ => ResponseData {
                        status: StatusCode::BAD_GATEWAY,
                        ..Default::default()
                    },
                };
                let _ = out.send(encode_response(id, &response)).await;
            });
        }

        writer.abort();
    }
}

#[async_trait]
impl Ingress<RequestData, ResponseData> for TunnelIngress {
    async fn start(&self, tx: Sender<Envelope<RequestData, ResponseData>>, shutdown: CancellationToken) {
        loop {
            if shutdown.is_cancelled() {
                return;
            }
            self.session(&tx, &shutdown).await;
            tokio::select! {
                _ = shutdown.cancelled() => return,
                _ = tokio::time::sleep(self.reconnect) => {}
            }
        }
    }
}