Skip to main content

cegla/client/
request.rs

1use std::{net::SocketAddr, path::PathBuf, pin::Pin, task::Poll};
2
3use bytes::Bytes;
4use futures_util::Stream;
5use http_body::Body;
6
7use crate::CgiEnvironment;
8
9/// A builder for CGI-like requests
10pub struct CgiBuilder {
11  inner: hashlink::LinkedHashMap<String, String>,
12  request_uri_set: bool,
13}
14
15impl CgiBuilder {
16  /// Creates a new `CgiBuilder` instance
17  pub fn new() -> Self {
18    Self {
19      inner: hashlink::LinkedHashMap::new(),
20      request_uri_set: false,
21    }
22  }
23
24  /// Inserts an environment variable
25  pub fn var(mut self, key: String, value: String) -> Self {
26    self.inner.insert(key.to_uppercase(), value);
27    self
28  }
29
30  /// Inserts an environment variable if it doesn't already exist
31  pub fn var_noreplace(mut self, key: String, value: String) -> Self {
32    if let hashlink::linked_hash_map::Entry::Vacant(entry) = self.inner.entry(key.to_uppercase()) {
33      entry.insert(value);
34    }
35    self
36  }
37
38  /// Inserts HTTP authentication-related data
39  pub fn auth(mut self, auth_type: Option<String>, username: String) -> Self {
40    if let Some(auth_type) = auth_type {
41      self.inner.insert("AUTH_TYPE".to_string(), auth_type);
42    }
43    self.inner.insert("REMOTE_USER".to_string(), username);
44    self
45  }
46
47  /// Inserts server software information
48  pub fn server(mut self, server_software: String) -> Self {
49    self.inner.insert("SERVER_SOFTWARE".to_string(), server_software);
50    self
51  }
52
53  /// Inserts server administrator information
54  pub fn server_admin(mut self, server_admin: String) -> Self {
55    self.inner.insert("SERVER_ADMIN".to_string(), server_admin);
56    self
57  }
58
59  /// Inserts server address information
60  pub fn server_address(mut self, server_address: SocketAddr) -> Self {
61    self.inner.insert(
62      "SERVER_ADDR".to_string(),
63      server_address.ip().to_canonical().to_string(),
64    );
65    self
66      .inner
67      .insert("SERVER_PORT".to_string(), server_address.port().to_string());
68    self
69  }
70
71  /// Inserts client address information
72  pub fn client_address(mut self, client_address: SocketAddr) -> Self {
73    self.inner.insert(
74      "REMOTE_ADDR".to_string(),
75      client_address.ip().to_canonical().to_string(),
76    );
77    self
78      .inner
79      .insert("REMOTE_PORT".to_string(), client_address.port().to_string());
80    self
81  }
82
83  /// Inserts server hostname information
84  pub fn hostname(mut self, server_name: String) -> Self {
85    self.inner.insert("SERVER_NAME".to_string(), server_name);
86    self
87  }
88
89  /// Inserts script path information
90  pub fn script_path(mut self, script_path: PathBuf, wwwroot: PathBuf, path_info: Option<String>) -> Self {
91    self
92      .inner
93      .insert("SCRIPT_FILENAME".to_string(), script_path.to_string_lossy().to_string());
94    if let Ok(script_path) = script_path.as_path().strip_prefix(&wwwroot) {
95      self.inner.insert(
96        "SCRIPT_NAME".to_string(),
97        format!(
98          "/{}",
99          match cfg!(windows) {
100            true => script_path.to_string_lossy().to_string().replace("\\", "/"),
101            false => script_path.to_string_lossy().to_string(),
102          }
103        ),
104      );
105    }
106    self
107      .inner
108      .insert("DOCUMENT_ROOT".to_string(), wwwroot.to_string_lossy().to_string());
109    self.inner.insert(
110      "PATH_INFO".to_string(),
111      match &path_info {
112        Some(path_info) => format!("/{path_info}"),
113        None => "".to_string(),
114      },
115    );
116    self.inner.insert(
117      "PATH_TRANSLATED".to_string(),
118      match &path_info {
119        Some(path_info) => {
120          let mut path_translated = script_path.clone();
121          path_translated.push(path_info);
122          path_translated.to_string_lossy().to_string()
123        }
124        None => "".to_string(),
125      },
126    );
127    self
128  }
129
130  /// Sets the HTTPS environment variable to "on"
131  pub fn https(mut self) -> Self {
132    self.inner.insert("HTTPS".to_string(), "on".to_string());
133    self
134  }
135
136  /// Sets the REQUEST_URI environment variable
137  pub fn request_uri(mut self, uri: &http::Uri) -> Self {
138    self.request_uri_set = true;
139    self.inner.insert(
140      "REQUEST_URI".to_string(),
141      format!(
142        "{}{}",
143        uri.path(),
144        match uri.query() {
145          Some(query) => format!("?{query}"),
146          None => String::from(""),
147        }
148      ),
149    );
150    self
151  }
152
153  /// Inserts environment variables from the system
154  pub fn system(mut self) -> Self {
155    for (key, value) in std::env::vars_os() {
156      if let Ok(key) = key.into_string() {
157        if let Ok(value) = value.into_string() {
158          self.inner.insert(key, value);
159        }
160      }
161    }
162    self
163  }
164
165  /// Builds the CGI request
166  pub fn build<B>(mut self, request: http::Request<B>) -> (CgiEnvironment, CgiRequest<B>)
167  where
168    B: Body,
169    B::Data: AsRef<[u8]> + Send + 'static,
170    B::Error: Into<std::io::Error>,
171  {
172    let (parts, body) = request.into_parts();
173    self.inner.insert(
174      "QUERY_STRING".to_string(),
175      match parts.uri.query() {
176        Some(query) => query.to_string(),
177        None => "".to_string(),
178      },
179    );
180    self
181      .inner
182      .insert("REQUEST_METHOD".to_string(), parts.method.to_string());
183    if !self.request_uri_set {
184      self.inner.insert(
185        "REQUEST_URI".to_string(),
186        format!(
187          "{}{}",
188          parts.uri.path(),
189          match parts.uri.query() {
190            Some(query) => format!("?{query}"),
191            None => String::from(""),
192          }
193        ),
194      );
195    }
196    self.inner.insert(
197      "SERVER_PROTOCOL".to_string(),
198      match parts.version {
199        http::Version::HTTP_09 => "HTTP/0.9".to_string(),
200        http::Version::HTTP_10 => "HTTP/1.0".to_string(),
201        http::Version::HTTP_11 => "HTTP/1.1".to_string(),
202        http::Version::HTTP_2 => "HTTP/2.0".to_string(),
203        http::Version::HTTP_3 => "HTTP/3.0".to_string(),
204        _ => "HTTP/Unknown".to_string(),
205      },
206    );
207    for (header_name, header_value) in parts.headers.iter() {
208      let env_header_name = match *header_name {
209        http::header::CONTENT_LENGTH => "CONTENT_LENGTH".to_string(),
210        http::header::CONTENT_TYPE => "CONTENT_TYPE".to_string(),
211        _ => {
212          let mut result = String::new();
213
214          result.push_str("HTTP_");
215
216          for c in header_name.as_str().to_uppercase().chars() {
217            if c.is_alphanumeric() {
218              result.push(c);
219            } else {
220              result.push('_');
221            }
222          }
223
224          result
225        }
226      };
227      if let Some(value) = if env_header_name.starts_with("HTTP_") {
228        self.inner.get_mut(&env_header_name)
229      } else {
230        None
231      } {
232        if env_header_name == "HTTP_COOKIE" {
233          value.push_str("; ");
234        } else {
235          // See https://stackoverflow.com/a/1801191
236          value.push_str(", ");
237        }
238        value.push_str(String::from_utf8_lossy(header_value.as_bytes()).as_ref());
239      } else {
240        self.inner.insert(
241          env_header_name,
242          String::from_utf8_lossy(header_value.as_bytes()).to_string(),
243        );
244      }
245    }
246    self
247      .inner
248      .insert("GATEWAY_INTERFACE".to_string(), "CGI/1.1".to_string());
249    (
250      CgiEnvironment { inner: self.inner },
251      CgiRequest { body: Box::pin(body) },
252    )
253  }
254}
255
256impl Default for CgiBuilder {
257  fn default() -> Self {
258    Self::new()
259  }
260}
261
262/// A CGI-like client request
263pub struct CgiRequest<B> {
264  body: Pin<Box<B>>,
265}
266
267impl<B> Stream for CgiRequest<B>
268where
269  B: Body,
270  B::Data: AsRef<[u8]> + Send + 'static,
271  B::Error: Into<std::io::Error>,
272{
273  type Item = Result<Bytes, std::io::Error>;
274
275  fn poll_next(
276    mut self: std::pin::Pin<&mut Self>,
277    cx: &mut std::task::Context<'_>,
278  ) -> std::task::Poll<Option<Self::Item>> {
279    match Pin::new(&mut self.body).poll_frame(cx) {
280      Poll::Ready(Some(Ok(frame))) => {
281        if let Ok(data) = frame.into_data() {
282          Poll::Ready(Some(Ok(Bytes::from_owner(data))))
283        } else {
284          Poll::Ready(None)
285        }
286      }
287      Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err.into()))),
288      Poll::Ready(None) => Poll::Ready(None),
289      Poll::Pending => Poll::Pending,
290    }
291  }
292}