From ef77f6d2d6ebe11d2c1afd93314d44840f6c7d77 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 18 Mar 2026 18:39:14 +0900 Subject: init --- src/main.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/main.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1f336ae --- /dev/null +++ b/src/main.rs @@ -0,0 +1,147 @@ +use std::env; +use std::net::SocketAddr; + +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Method, Request, Response, StatusCode, Uri}; +use hyper_util::client::legacy::Client; +use hyper_util::rt::TokioExecutor; +use tokio::net::TcpListener; + +const DEFAULT_BASE_URL: &str = "http://100.102.55.49:3000"; +const DEFAULT_PORT: u16 = 8080; + +type BoxError = Box; + +/// Parse the incoming path into the Dokploy deploy API path. +/// - "/" -> "/api/deploy/" +/// - "/compose/" -> "/api/deploy/compose/" +/// Returns None if the path doesn't match. +fn map_path(path: &str) -> Option { + let trimmed = path.trim_start_matches('/'); + if trimmed.is_empty() { + return None; + } + + if let Some(token) = trimmed.strip_prefix("compose/") { + if !token.is_empty() && !token.contains('/') { + return Some(format!("/api/deploy/compose/{}", token)); + } + return None; + } + + if !trimmed.contains('/') { + return Some(format!("/api/deploy/{}", trimmed)); + } + + None +} + +async fn handle( + base_url: String, + req: Request, +) -> Result>, BoxError> { + // Only allow POST + if req.method() != Method::POST { + eprintln!("{} {} -> 405 Method Not Allowed", req.method(), req.uri().path()); + return Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Full::new(Bytes::from("Method Not Allowed\n"))) + .unwrap()); + } + + let path = req.uri().path().to_string(); + + // Map the path + let upstream_path = match map_path(&path) { + Some(p) => p, + None => { + eprintln!("POST {} -> 404 Not Found", path); + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from("Not Found\n"))) + .unwrap()); + } + }; + + let upstream_uri: Uri = format!("{}{}", base_url, upstream_path).parse()?; + eprintln!("POST {} -> forwarding to {}", path, upstream_uri); + + // Build upstream request preserving headers and body + let mut builder = Request::builder() + .method(Method::POST) + .uri(&upstream_uri); + + // Copy headers (skip Host, it should match the upstream) + for (name, value) in req.headers() { + if name != hyper::header::HOST { + builder = builder.header(name, value); + } + } + + // Collect the incoming body + let body_bytes = req.into_body().collect().await?.to_bytes(); + let upstream_req = builder.body(Full::new(body_bytes))?; + + // Send to Dokploy + let client = Client::builder(TokioExecutor::new()).build_http(); + let upstream_resp = match client.request(upstream_req).await { + Ok(resp) => resp, + Err(e) => { + eprintln!("POST {} -> 502 upstream error: {}", path, e); + return Ok(Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(Full::new(Bytes::from(format!("Bad Gateway: {}\n", e)))) + .unwrap()); + } + }; + + // Forward the upstream response back + let status = upstream_resp.status(); + let resp_body = upstream_resp.into_body().collect().await?.to_bytes(); + + eprintln!("POST {} -> upstream responded {}", path, status); + + Ok(Response::builder() + .status(status) + .body(Full::new(resp_body)) + .unwrap()) +} + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + let base_url = env::var("DOKPLOY_BASE_URL") + .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()) + .trim_end_matches('/') + .to_string(); + + let port: u16 = env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(DEFAULT_PORT); + + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + let listener = TcpListener::bind(addr).await?; + eprintln!("webhook-forwarder listening on {}", addr); + eprintln!("forwarding to {}", base_url); + + loop { + let (stream, _) = listener.accept().await?; + let base_url = base_url.clone(); + + tokio::task::spawn(async move { + let io = hyper_util::rt::TokioIo::new(stream); + let service = service_fn(move |req| { + let base_url = base_url.clone(); + handle(base_url, req) + }); + + if let Err(e) = http1::Builder::new().serve_connection(io, service).await { + eprintln!("connection error: {}", e); + } + }); + } +} -- cgit v1.2.3