Skip to main content

gatel_core/proxy/
scgi.rs

1//! SCGI (Simple Common Gateway Interface) handler.
2//!
3//! SCGI is a simpler alternative to FastCGI. The request is encoded as a
4//! netstring (length-prefixed) of NUL-separated key-value pairs followed by a
5//! comma and the raw request body. The response is parsed as a CGI-style
6//! response (headers separated from body by `\r\n\r\n`).
7//!
8//! Reference: <https://python.ca/scgi/protocol.txt>
9
10use std::collections::HashMap;
11
12use http::Response;
13use http_body_util::BodyExt;
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::net::TcpStream;
16use tracing::debug;
17
18use crate::{Body, ProxyError, goals};
19
20/// SCGI handler: forwards requests to an SCGI server.
21pub struct ScgiHandler {
22    /// Address of the SCGI server, e.g. `"127.0.0.1:9000"`.
23    addr: String,
24    /// Extra environment variables injected into every request.
25    env: HashMap<String, String>,
26}
27
28impl ScgiHandler {
29    pub fn new(addr: String, env: HashMap<String, String>) -> Self {
30        Self { addr, env }
31    }
32}
33
34#[salvo::async_trait]
35impl salvo::Handler for ScgiHandler {
36    async fn handle(
37        &self,
38        req: &mut salvo::Request,
39        _depot: &mut salvo::Depot,
40        res: &mut salvo::Response,
41        ctrl: &mut salvo::FlowCtrl,
42    ) {
43        let client_addr = crate::hoops::client_addr(req);
44        let request = match goals::strip_request(req) {
45            Ok(r) => r,
46            Err(e) => {
47                goals::merge_response(res, e.into_response());
48                ctrl.skip_rest();
49                return;
50            }
51        };
52        let response = self
53            .run(request, client_addr)
54            .await
55            .unwrap_or_else(|e| e.into_response());
56        goals::merge_response(res, response);
57        ctrl.skip_rest();
58    }
59}
60
61impl ScgiHandler {
62    async fn run(
63        &self,
64        request: http::Request<Body>,
65        client_addr: std::net::SocketAddr,
66    ) -> Result<Response<Body>, ProxyError> {
67        let (parts, body) = request.into_parts();
68        let body_bytes = body
69            .collect()
70            .await
71            .map_err(|e| ProxyError::Internal(format!("body collect: {e}")))?
72            .to_bytes();
73
74        // Build the SCGI headers buffer (NUL-separated key\0value\0 pairs).
75        // Per the SCGI spec, CONTENT_LENGTH must appear first, followed by
76        // the SCGI marker, then all other parameters.
77        let mut headers: Vec<u8> = Vec::new();
78        push_scgi_header(
79            &mut headers,
80            "CONTENT_LENGTH",
81            &body_bytes.len().to_string(),
82        );
83        push_scgi_header(&mut headers, "SCGI", "1");
84        push_scgi_header(&mut headers, "REQUEST_METHOD", parts.method.as_str());
85        push_scgi_header(&mut headers, "REQUEST_URI", &parts.uri.to_string());
86        push_scgi_header(
87            &mut headers,
88            "QUERY_STRING",
89            parts.uri.query().unwrap_or(""),
90        );
91        push_scgi_header(
92            &mut headers,
93            "SERVER_PROTOCOL",
94            &format!("{:?}", parts.version),
95        );
96        push_scgi_header(&mut headers, "REMOTE_ADDR", &client_addr.ip().to_string());
97        push_scgi_header(&mut headers, "REMOTE_PORT", &client_addr.port().to_string());
98        push_scgi_header(&mut headers, "SERVER_SOFTWARE", "gatel");
99        push_scgi_header(&mut headers, "GATEWAY_INTERFACE", "CGI/1.1");
100
101        let path = parts
102            .uri
103            .path_and_query()
104            .map(|pq| pq.as_str().to_string())
105            .unwrap_or_else(|| parts.uri.path().to_string());
106        push_scgi_header(&mut headers, "SCRIPT_NAME", parts.uri.path());
107        push_scgi_header(&mut headers, "PATH_INFO", parts.uri.path());
108        push_scgi_header(&mut headers, "DOCUMENT_URI", &path);
109
110        if let Some(ct) = parts
111            .headers
112            .get("content-type")
113            .and_then(|v| v.to_str().ok())
114        {
115            push_scgi_header(&mut headers, "CONTENT_TYPE", ct);
116        }
117
118        if let Some(host) = parts.headers.get("host").and_then(|v| v.to_str().ok()) {
119            push_scgi_header(
120                &mut headers,
121                "SERVER_NAME",
122                host.split(':').next().unwrap_or(host),
123            );
124            if let Some(port) = host.split(':').nth(1) {
125                push_scgi_header(&mut headers, "SERVER_PORT", port);
126            }
127        }
128
129        // Translate HTTP headers to HTTP_* environment variables.
130        for (name, value) in &parts.headers {
131            if let Ok(v) = value.to_str() {
132                let env_name = format!("HTTP_{}", name.as_str().to_uppercase().replace('-', "_"));
133                push_scgi_header(&mut headers, &env_name, v);
134            }
135        }
136
137        // Inject custom environment variables.
138        for (k, v) in &self.env {
139            push_scgi_header(&mut headers, k, v);
140        }
141
142        // Build the full SCGI payload:  "<header_len>:<headers>,<body>"
143        let header_len = headers.len();
144        let mut payload = format!("{header_len}:").into_bytes();
145        payload.extend_from_slice(&headers);
146        payload.push(b',');
147        payload.extend_from_slice(&body_bytes);
148
149        debug!(addr = %self.addr, "connecting to SCGI server");
150
151        // Connect, send, and read the full response.
152        let mut stream = TcpStream::connect(&self.addr)
153            .await
154            .map_err(|e| ProxyError::Internal(format!("SCGI connect to {}: {e}", self.addr)))?;
155
156        stream.write_all(&payload).await.map_err(ProxyError::Io)?;
157        stream.flush().await.map_err(ProxyError::Io)?;
158
159        let mut response_buf = Vec::new();
160        stream
161            .read_to_end(&mut response_buf)
162            .await
163            .map_err(ProxyError::Io)?;
164
165        // Parse the response using the shared CGI response parser.
166        crate::proxy::cgi::parse_cgi_response(&response_buf)
167    }
168}
169
170// ---------------------------------------------------------------------------
171// SCGI helpers
172// ---------------------------------------------------------------------------
173
174/// Append a single `name\0value\0` pair to the SCGI headers buffer.
175fn push_scgi_header(buf: &mut Vec<u8>, name: &str, value: &str) {
176    buf.extend_from_slice(name.as_bytes());
177    buf.push(0);
178    buf.extend_from_slice(value.as_bytes());
179    buf.push(0);
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_push_scgi_header() {
188        let mut buf = Vec::new();
189        push_scgi_header(&mut buf, "CONTENT_LENGTH", "0");
190        // "CONTENT_LENGTH" (14) + NUL (1) + "0" (1) + NUL (1) = 17 bytes
191        assert_eq!(buf.len(), 17);
192        assert_eq!(buf[14], 0); // NUL after name
193        assert_eq!(buf[15], b'0');
194        assert_eq!(buf[16], 0); // NUL after value
195    }
196}