cegla-scgi 0.2.3

High-level SCGI implementation for Rust
Documentation
//! Server-side SCGI implementation.

use std::future::Future;

use cegla::{
  server::{convert_cgi_request, convert_from_http_response},
  CgiEnvironment, CgiIncoming,
};
use hashlink::LinkedHashMap;
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, BufReader, ReadHalf};
use tokio_util::io::StreamReader;

/// Reads environment variables from the SCGI request.
#[inline]
pub async fn read_environment_netstring<R>(mut reader: R) -> Result<CgiEnvironment, std::io::Error>
where
  R: AsyncBufRead + Unpin,
{
  // Netstring length
  let mut length_buf = Vec::new();
  reader.read_until(b':', &mut length_buf).await?;
  length_buf.pop(); // Remove ':'
  let environment_length = String::from_utf8(length_buf)
    .ok()
    .and_then(|s| s.parse::<usize>().ok())
    .ok_or(std::io::Error::new(
      std::io::ErrorKind::InvalidData,
      "Invalid environment length",
    ))?;

  // Netstring body
  let mut environment = vec![0u8; environment_length];
  reader.read_exact(&mut environment).await?;
  let _ = reader.read_u8().await?; // Discard ','

  // Parse netstring body into environment variables
  let mut split_environment = environment.split(|b| *b == 0);
  let mut environment = LinkedHashMap::new();
  while let Some(key) = split_environment.next() {
    if let Some(value) = split_environment.next() {
      environment.insert(
        String::from_utf8(key.to_vec())
          .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid environment key"))?,
        String::from_utf8(value.to_vec())
          .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid environment value"))?,
      );
    }
  }

  Ok(CgiEnvironment::from(environment))
}

/// Handles a SCGI request by converting it to an HTTP request, invoking the provided request function,
/// and then converting the HTTP response back to a SCGI response.
pub async fn server_handle_scgi<Io, F, Fut, B, Err>(io: Io, request_fn: F) -> Result<(), std::io::Error>
where
  Io: AsyncRead + AsyncWrite + Send + Unpin + 'static,
  F: FnOnce(http::Request<CgiIncoming<BufReader<ReadHalf<Io>>>>) -> Fut,
  Fut: Future<Output = Result<http::Response<B>, Err>>,
  B: http_body::Body,
  B::Data: AsRef<[u8]> + Send + 'static,
  B::Error: Into<std::io::Error>,
  Err: Into<std::io::Error>,
{
  let (read_half, mut write_half) = tokio::io::split(io);
  let mut read_half = BufReader::new(read_half);

  let environment = read_environment_netstring(&mut read_half).await?;
  let request = convert_cgi_request(read_half, environment)?;

  let response = request_fn(request).await.map_err(|err| err.into())?;

  let response_data = convert_from_http_response(response)?;
  let mut response_reader = StreamReader::new(response_data);
  tokio::io::copy(&mut response_reader, &mut write_half).await?;

  Ok(())
}