Skip to main content

cegla_cgi/
client.rs

1//! CGI implementation for web servers.
2
3use std::{ffi::OsStr, future::Future, path::PathBuf};
4
5pub use cegla::client::CgiBuilder;
6
7use cegla::{
8  client::{convert_to_http_response, CgiResponseInner},
9  CgiEnvironment, CgiIncoming,
10};
11use http_body::Body;
12use tokio::io::{AsyncRead, AsyncWrite};
13use tokio_util::io::StreamReader;
14
15/// Runtime trait for CGI "client".
16pub trait Runtime {
17  type Child: Child;
18
19  /// Spawns a new task to execute the given future.
20  fn spawn(&self, future: impl Future + 'static);
21
22  /// Starts a child process with the given command and arguments.
23  fn start_child(
24    &self,
25    cmd: &OsStr,
26    args: &[&OsStr],
27    env: CgiEnvironment,
28    cwd: Option<PathBuf>,
29  ) -> Result<Self::Child, std::io::Error>;
30}
31
32/// `Send` runtime trait for CGI "client".
33pub trait SendRuntime {
34  type Child: SendChild;
35
36  /// Spawns a new task to execute the given future.
37  fn spawn(&self, future: impl Future + Send + 'static);
38
39  /// Starts a child process with the given command and arguments.
40  fn start_child(
41    &self,
42    cmd: &OsStr,
43    args: &[&OsStr],
44    env: CgiEnvironment,
45    cwd: Option<PathBuf>,
46  ) -> Result<Self::Child, std::io::Error>;
47}
48
49/// Runtime trait for CGI child process.
50pub trait Child {
51  type Stdin: AsyncWrite + Unpin + 'static;
52  type Stdout: AsyncRead + Unpin + 'static;
53  type Stderr: AsyncRead + Unpin + 'static;
54
55  /// Obtains the standard input stream.
56  fn stdin(&mut self) -> Option<Self::Stdin>;
57
58  /// Obtains the standard output stream.
59  fn stdout(&mut self) -> Option<Self::Stdout>;
60
61  /// Obtains the standard error stream.
62  fn stderr(&mut self) -> Option<Self::Stderr>;
63
64  /// Returns the exit status if the process has exited.
65  fn try_status(&mut self) -> std::io::Result<Option<std::process::ExitStatus>>;
66}
67
68/// `Send` runtime trait for CGI child process.
69pub trait SendChild {
70  type Stdin: AsyncWrite + Send + Unpin + 'static;
71  type Stdout: AsyncRead + Send + Unpin + 'static;
72  type Stderr: AsyncRead + Send + Unpin + 'static;
73
74  /// Obtains the standard input stream.
75  fn stdin(&mut self) -> Option<Self::Stdin>;
76
77  /// Obtains the standard output stream.
78  fn stdout(&mut self) -> Option<Self::Stdout>;
79
80  /// Obtains the standard error stream.
81  fn stderr(&mut self) -> Option<Self::Stderr>;
82
83  /// Returns the exit status if the process has exited.
84  fn try_status(&mut self) -> std::io::Result<Option<std::process::ExitStatus>>;
85}
86
87/// Executes a CGI program, returning the response and error streams.
88pub async fn execute_cgi<B, R>(
89  request: http::Request<B>,
90  runtime: R,
91  cmd: &OsStr,
92  args: &[&OsStr],
93  env: CgiBuilder,
94  cwd: Option<PathBuf>,
95) -> Result<
96  (
97    http::Response<CgiIncoming<CgiResponseInner<<R::Child as Child>::Stdout>>>,
98    Option<<R::Child as Child>::Stderr>,
99    Option<std::process::ExitStatus>,
100  ),
101  std::io::Error,
102>
103where
104  B: Body + 'static,
105  B::Data: AsRef<[u8]> + Send + 'static,
106  B::Error: Into<std::io::Error>,
107  R: Runtime,
108{
109  let (cgi_environment, cgi_data) = env.build(request);
110  let mut child = runtime.start_child(cmd, args, cgi_environment, cwd)?;
111
112  let mut stdin = child.stdin().ok_or(std::io::Error::other("Failed to take stdin"))?;
113  let stdout = child.stdout().ok_or(std::io::Error::other("Failed to take stdout"))?;
114  let stderr = child.stderr();
115
116  let mut cgi_data_reader = StreamReader::new(cgi_data);
117  runtime.spawn(async move {
118    let _ = tokio::io::copy(&mut cgi_data_reader, &mut stdin).await;
119  });
120
121  Ok((convert_to_http_response(stdout).await?, stderr, child.try_status()?))
122}
123
124/// Executes a CGI program, on a `Send` runtime, returning the response and error streams.
125pub async fn execute_cgi_send<B, R>(
126  request: http::Request<B>,
127  runtime: R,
128  cmd: &OsStr,
129  args: &[&OsStr],
130  env: CgiBuilder,
131  cwd: Option<PathBuf>,
132) -> Result<
133  (
134    http::Response<CgiIncoming<CgiResponseInner<<R::Child as SendChild>::Stdout>>>,
135    Option<<R::Child as SendChild>::Stderr>,
136    Option<std::process::ExitStatus>,
137  ),
138  std::io::Error,
139>
140where
141  B: Body + Send + 'static,
142  B::Data: AsRef<[u8]> + Send + 'static,
143  B::Error: Into<std::io::Error>,
144  R: SendRuntime,
145{
146  let (cgi_environment, cgi_data) = env.build(request);
147  let mut child = runtime.start_child(cmd, args, cgi_environment, cwd)?;
148
149  let mut stdin = child.stdin().ok_or(std::io::Error::other("Failed to take stdin"))?;
150  let stdout = child.stdout().ok_or(std::io::Error::other("Failed to take stdout"))?;
151  let stderr = child.stderr();
152
153  let mut cgi_data_reader = StreamReader::new(cgi_data);
154  runtime.spawn(async move {
155    let _ = tokio::io::copy(&mut cgi_data_reader, &mut stdin).await;
156  });
157
158  Ok((convert_to_http_response(stdout).await?, stderr, child.try_status()?))
159}