ferron/optional_modules/
scgi.rs

1// SCGI handler code inspired by SVR.JS's OrangeCircle mod, translated from JavaScript to Rust.
2// Based on the "cgi" module
3use std::env;
4use std::error::Error;
5use std::path::{Path, PathBuf};
6
7use crate::ferron_common::{
8  ErrorLogger, HyperRequest, HyperResponse, RequestData, ResponseData, ServerConfig, ServerModule,
9  ServerModuleHandlers, SocketData,
10};
11use crate::ferron_common::{HyperUpgraded, WithRuntime};
12use async_trait::async_trait;
13use futures_util::TryStreamExt;
14use hashlink::LinkedHashMap;
15use http_body_util::{BodyExt, StreamBody};
16use httparse::EMPTY_HEADER;
17use hyper::body::Frame;
18use hyper::{header, Response, StatusCode};
19use hyper_tungstenite::HyperWebsocket;
20use tokio::fs;
21use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
22use tokio::net::TcpStream;
23use tokio::runtime::Handle;
24use tokio_util::io::{ReaderStream, StreamReader};
25
26use crate::ferron_res::server_software::SERVER_SOFTWARE;
27use crate::ferron_util::cgi_response::CgiResponse;
28
29pub fn server_module_init(
30  _config: &ServerConfig,
31) -> Result<Box<dyn ServerModule + Send + Sync>, Box<dyn Error + Send + Sync>> {
32  Ok(Box::new(ScgiModule::new()))
33}
34
35struct ScgiModule;
36
37impl ScgiModule {
38  fn new() -> Self {
39    Self
40  }
41}
42
43impl ServerModule for ScgiModule {
44  fn get_handlers(&self, handle: Handle) -> Box<dyn ServerModuleHandlers + Send> {
45    Box::new(ScgiModuleHandlers { handle })
46  }
47}
48struct ScgiModuleHandlers {
49  handle: Handle,
50}
51
52#[async_trait]
53impl ServerModuleHandlers for ScgiModuleHandlers {
54  async fn request_handler(
55    &mut self,
56    request: RequestData,
57    config: &ServerConfig,
58    socket_data: &SocketData,
59    error_logger: &ErrorLogger,
60  ) -> Result<ResponseData, Box<dyn Error + Send + Sync>> {
61    WithRuntime::new(self.handle.clone(), async move {
62      let mut scgi_to = "tcp://localhost:4000/";
63      let scgi_to_yaml = &config["scgiTo"];
64      if let Some(scgi_to_obtained) = scgi_to_yaml.as_str() {
65        scgi_to = scgi_to_obtained;
66      }
67
68      let mut scgi_path = None;
69      if let Some(scgi_path_obtained) = config["scgiPath"].as_str() {
70        scgi_path = Some(scgi_path_obtained.to_string());
71      }
72
73      let hyper_request = request.get_hyper_request();
74
75      let request_path = hyper_request.uri().path();
76      let mut request_path_bytes = request_path.bytes();
77      if request_path_bytes.len() < 1 || request_path_bytes.nth(0) != Some(b'/') {
78        return Ok(
79          ResponseData::builder(request)
80            .status(StatusCode::BAD_REQUEST)
81            .build(),
82        );
83      }
84
85      if let Some(scgi_path) = scgi_path {
86        let mut canonical_scgi_path: &str = &scgi_path;
87        if canonical_scgi_path.bytes().last() == Some(b'/') {
88          canonical_scgi_path = &canonical_scgi_path[..(canonical_scgi_path.len() - 1)];
89        }
90
91        let request_path_with_slashes = match request_path == canonical_scgi_path {
92          true => format!("{request_path}/"),
93          false => request_path.to_string(),
94        };
95        if let Some(stripped_request_path) =
96          request_path_with_slashes.strip_prefix(canonical_scgi_path)
97        {
98          let wwwroot_yaml = &config["wwwroot"];
99          let wwwroot = wwwroot_yaml.as_str().unwrap_or("/nonexistent");
100
101          let wwwroot_unknown = PathBuf::from(wwwroot);
102          let wwwroot_pathbuf = match wwwroot_unknown.as_path().is_absolute() {
103            true => wwwroot_unknown,
104            false => match fs::canonicalize(&wwwroot_unknown).await {
105              Ok(pathbuf) => pathbuf,
106              Err(_) => wwwroot_unknown,
107            },
108          };
109          let wwwroot = wwwroot_pathbuf.as_path();
110
111          let mut relative_path = &request_path[1..];
112          while relative_path.as_bytes().first().copied() == Some(b'/') {
113            relative_path = &relative_path[1..];
114          }
115
116          let decoded_relative_path = match urlencoding::decode(relative_path) {
117            Ok(path) => path.to_string(),
118            Err(_) => {
119              return Ok(
120                ResponseData::builder(request)
121                  .status(StatusCode::BAD_REQUEST)
122                  .build(),
123              );
124            }
125          };
126
127          let joined_pathbuf = wwwroot.join(decoded_relative_path);
128          let execute_pathbuf = joined_pathbuf;
129          let execute_path_info = stripped_request_path
130            .strip_prefix("/")
131            .map(|s| s.to_string());
132
133          return execute_scgi_with_environment_variables(
134            request,
135            socket_data,
136            error_logger,
137            wwwroot,
138            execute_pathbuf,
139            execute_path_info,
140            config["serverAdministratorEmail"].as_str(),
141            scgi_to,
142          )
143          .await;
144        }
145      }
146      Ok(ResponseData::builder(request).build())
147    })
148    .await
149  }
150
151  async fn proxy_request_handler(
152    &mut self,
153    request: RequestData,
154    _config: &ServerConfig,
155    _socket_data: &SocketData,
156    _error_logger: &ErrorLogger,
157  ) -> Result<ResponseData, Box<dyn Error + Send + Sync>> {
158    Ok(ResponseData::builder(request).build())
159  }
160
161  async fn response_modifying_handler(
162    &mut self,
163    response: HyperResponse,
164  ) -> Result<HyperResponse, Box<dyn Error + Send + Sync>> {
165    Ok(response)
166  }
167
168  async fn proxy_response_modifying_handler(
169    &mut self,
170    response: HyperResponse,
171  ) -> Result<HyperResponse, Box<dyn Error + Send + Sync>> {
172    Ok(response)
173  }
174
175  async fn connect_proxy_request_handler(
176    &mut self,
177    _upgraded_request: HyperUpgraded,
178    _connect_address: &str,
179    _config: &ServerConfig,
180    _socket_data: &SocketData,
181    _error_logger: &ErrorLogger,
182  ) -> Result<(), Box<dyn Error + Send + Sync>> {
183    Ok(())
184  }
185
186  fn does_connect_proxy_requests(&mut self) -> bool {
187    false
188  }
189
190  async fn websocket_request_handler(
191    &mut self,
192    _websocket: HyperWebsocket,
193    _uri: &hyper::Uri,
194    _headers: &hyper::HeaderMap,
195    _config: &ServerConfig,
196    _socket_data: &SocketData,
197    _error_logger: &ErrorLogger,
198  ) -> Result<(), Box<dyn Error + Send + Sync>> {
199    Ok(())
200  }
201
202  fn does_websocket_requests(&mut self, _config: &ServerConfig, _socket_data: &SocketData) -> bool {
203    false
204  }
205}
206
207#[allow(clippy::too_many_arguments)]
208async fn execute_scgi_with_environment_variables(
209  request: RequestData,
210  socket_data: &SocketData,
211  error_logger: &ErrorLogger,
212  wwwroot: &Path,
213  execute_pathbuf: PathBuf,
214  path_info: Option<String>,
215  server_administrator_email: Option<&str>,
216  scgi_to: &str,
217) -> Result<ResponseData, Box<dyn Error + Send + Sync>> {
218  let mut environment_variables: LinkedHashMap<String, String> = LinkedHashMap::new();
219
220  let hyper_request = request.get_hyper_request();
221  let original_request_uri = request.get_original_url().unwrap_or(hyper_request.uri());
222
223  if let Some(auth_user) = request.get_auth_user() {
224    if let Some(authorization) = hyper_request.headers().get(header::AUTHORIZATION) {
225      let authorization_value = String::from_utf8_lossy(authorization.as_bytes()).to_string();
226      let mut authorization_value_split = authorization_value.split(" ");
227      if let Some(authorization_type) = authorization_value_split.next() {
228        environment_variables.insert("AUTH_TYPE".to_string(), authorization_type.to_string());
229      }
230    }
231    environment_variables.insert("REMOTE_USER".to_string(), auth_user.to_string());
232  }
233
234  environment_variables.insert(
235    "QUERY_STRING".to_string(),
236    match hyper_request.uri().query() {
237      Some(query) => query.to_string(),
238      None => "".to_string(),
239    },
240  );
241
242  environment_variables.insert("SERVER_SOFTWARE".to_string(), SERVER_SOFTWARE.to_string());
243  environment_variables.insert(
244    "SERVER_PROTOCOL".to_string(),
245    match hyper_request.version() {
246      hyper::Version::HTTP_09 => "HTTP/0.9".to_string(),
247      hyper::Version::HTTP_10 => "HTTP/1.0".to_string(),
248      hyper::Version::HTTP_11 => "HTTP/1.1".to_string(),
249      hyper::Version::HTTP_2 => "HTTP/2.0".to_string(),
250      hyper::Version::HTTP_3 => "HTTP/3.0".to_string(),
251      _ => "HTTP/Unknown".to_string(),
252    },
253  );
254  environment_variables.insert(
255    "SERVER_PORT".to_string(),
256    socket_data.local_addr.port().to_string(),
257  );
258  environment_variables.insert(
259    "SERVER_ADDR".to_string(),
260    socket_data.local_addr.ip().to_canonical().to_string(),
261  );
262  if let Some(server_administrator_email) = server_administrator_email {
263    environment_variables.insert(
264      "SERVER_ADMIN".to_string(),
265      server_administrator_email.to_string(),
266    );
267  }
268  if let Some(host) = hyper_request.headers().get(header::HOST) {
269    environment_variables.insert(
270      "SERVER_NAME".to_string(),
271      String::from_utf8_lossy(host.as_bytes()).to_string(),
272    );
273  }
274
275  environment_variables.insert(
276    "DOCUMENT_ROOT".to_string(),
277    wwwroot.to_string_lossy().to_string(),
278  );
279  environment_variables.insert(
280    "PATH_INFO".to_string(),
281    match &path_info {
282      Some(path_info) => format!("/{path_info}"),
283      None => "".to_string(),
284    },
285  );
286  environment_variables.insert(
287    "PATH_TRANSLATED".to_string(),
288    match &path_info {
289      Some(path_info) => {
290        let mut path_translated = execute_pathbuf.clone();
291        path_translated.push(path_info);
292        path_translated.to_string_lossy().to_string()
293      }
294      None => "".to_string(),
295    },
296  );
297  environment_variables.insert(
298    "REQUEST_METHOD".to_string(),
299    hyper_request.method().to_string(),
300  );
301  environment_variables.insert("GATEWAY_INTERFACE".to_string(), "CGI/1.1".to_string());
302  environment_variables.insert("SCGI".to_string(), "1".to_string());
303  environment_variables.insert(
304    "REQUEST_URI".to_string(),
305    format!(
306      "{}{}",
307      original_request_uri.path(),
308      match original_request_uri.query() {
309        Some(query) => format!("?{query}"),
310        None => String::from(""),
311      }
312    ),
313  );
314
315  environment_variables.insert(
316    "REMOTE_PORT".to_string(),
317    socket_data.remote_addr.port().to_string(),
318  );
319  environment_variables.insert(
320    "REMOTE_ADDR".to_string(),
321    socket_data.remote_addr.ip().to_canonical().to_string(),
322  );
323
324  environment_variables.insert(
325    "SCRIPT_FILENAME".to_string(),
326    execute_pathbuf.to_string_lossy().to_string(),
327  );
328  if let Ok(script_path) = execute_pathbuf.as_path().strip_prefix(wwwroot) {
329    environment_variables.insert(
330      "SCRIPT_NAME".to_string(),
331      format!(
332        "/{}",
333        match cfg!(windows) {
334          true => script_path.to_string_lossy().to_string().replace("\\", "/"),
335          false => script_path.to_string_lossy().to_string(),
336        }
337      ),
338    );
339  }
340
341  if socket_data.encrypted {
342    environment_variables.insert("HTTPS".to_string(), "on".to_string());
343  }
344
345  let mut content_length_set = false;
346  for (header_name, header_value) in hyper_request.headers().iter() {
347    let env_header_name = match *header_name {
348      header::CONTENT_LENGTH => {
349        content_length_set = true;
350        "CONTENT_LENGTH".to_string()
351      }
352      header::CONTENT_TYPE => "CONTENT_TYPE".to_string(),
353      _ => {
354        let mut result = String::new();
355
356        result.push_str("HTTP_");
357
358        for c in header_name.as_str().to_uppercase().chars() {
359          if c.is_alphanumeric() {
360            result.push(c);
361          } else {
362            result.push('_');
363          }
364        }
365
366        result
367      }
368    };
369    if environment_variables.contains_key(&env_header_name) {
370      let value = environment_variables.get_mut(&env_header_name);
371      if let Some(value) = value {
372        if env_header_name == "HTTP_COOKIE" {
373          value.push_str("; ");
374        } else {
375          // See https://stackoverflow.com/a/1801191
376          value.push_str(", ");
377        }
378        value.push_str(String::from_utf8_lossy(header_value.as_bytes()).as_ref());
379      } else {
380        environment_variables.insert(
381          env_header_name,
382          String::from_utf8_lossy(header_value.as_bytes()).to_string(),
383        );
384      }
385    } else {
386      environment_variables.insert(
387        env_header_name,
388        String::from_utf8_lossy(header_value.as_bytes()).to_string(),
389      );
390    }
391  }
392
393  if !content_length_set {
394    environment_variables.insert("CONTENT_LENGTH".to_string(), "0".to_string());
395  }
396
397  let (hyper_request, _, _, _) = request.into_parts();
398
399  execute_scgi(hyper_request, error_logger, scgi_to, environment_variables).await
400}
401
402async fn execute_scgi(
403  hyper_request: HyperRequest,
404  error_logger: &ErrorLogger,
405  scgi_to: &str,
406  mut environment_variables: LinkedHashMap<String, String>,
407) -> Result<ResponseData, Box<dyn Error + Send + Sync>> {
408  let (_, body) = hyper_request.into_parts();
409
410  // Insert other environment variables
411  for (key, value) in env::vars_os() {
412    let key_string = key.to_string_lossy().to_string();
413    let value_string = value.to_string_lossy().to_string();
414    environment_variables
415      .entry(key_string)
416      .or_insert(value_string);
417  }
418
419  let scgi_to_fixed = if let Some(stripped) = scgi_to.strip_prefix("unix:///") {
420    // hyper::Uri fails to parse a string if there is an empty authority, so add an "ignore" authority to Unix socket URLs
421    &format!("unix://ignore/{stripped}")
422  } else {
423    scgi_to
424  };
425
426  let scgi_to_url = scgi_to_fixed.parse::<hyper::Uri>()?;
427  let scheme_str = scgi_to_url.scheme_str();
428
429  let (socket_reader, mut socket_writer) = match scheme_str {
430    Some("tcp") => {
431      let host = match scgi_to_url.host() {
432        Some(host) => host,
433        None => Err(anyhow::anyhow!("The SCGI URL doesn't include the host"))?,
434      };
435
436      let port = match scgi_to_url.port_u16() {
437        Some(port) => port,
438        None => Err(anyhow::anyhow!("The SCGI URL doesn't include the port"))?,
439      };
440
441      let addr = format!("{host}:{port}");
442
443      match connect_tcp(&addr).await {
444        Ok(data) => data,
445        Err(err) => match err.kind() {
446          tokio::io::ErrorKind::ConnectionRefused
447          | tokio::io::ErrorKind::NotFound
448          | tokio::io::ErrorKind::HostUnreachable => {
449            error_logger
450              .log(&format!("Service unavailable: {err}"))
451              .await;
452            return Ok(
453              ResponseData::builder_without_request()
454                .status(StatusCode::SERVICE_UNAVAILABLE)
455                .build(),
456            );
457          }
458          _ => Err(err)?,
459        },
460      }
461    }
462    Some("unix") => {
463      let path = scgi_to_url.path();
464      match connect_unix(path).await {
465        Ok(data) => data,
466        Err(err) => match err.kind() {
467          tokio::io::ErrorKind::ConnectionRefused
468          | tokio::io::ErrorKind::NotFound
469          | tokio::io::ErrorKind::HostUnreachable => {
470            error_logger
471              .log(&format!("Service unavailable: {err}"))
472              .await;
473            return Ok(
474              ResponseData::builder_without_request()
475                .status(StatusCode::SERVICE_UNAVAILABLE)
476                .build(),
477            );
478          }
479          _ => Err(err)?,
480        },
481      }
482    }
483    _ => Err(anyhow::anyhow!(
484      "Only HTTP and HTTPS reverse proxy URLs are supported."
485    ))?,
486  };
487
488  // Create environment variable netstring
489  let mut environment_variables_to_wrap = Vec::new();
490  for (key, value) in environment_variables.iter() {
491    let mut environment_variable = Vec::new();
492    environment_variable.extend_from_slice(key.as_bytes());
493    environment_variable.push(b'\0');
494    environment_variable.extend_from_slice(value.as_bytes());
495    environment_variable.push(b'\0');
496    if key == "CONTENT_LENGTH" {
497      environment_variable.append(&mut environment_variables_to_wrap);
498      environment_variables_to_wrap = environment_variable;
499    } else {
500      environment_variables_to_wrap.append(&mut environment_variable);
501    }
502  }
503
504  let environment_variables_to_wrap_length = environment_variables_to_wrap.len();
505  let mut environment_variables_netstring = Vec::new();
506  environment_variables_netstring
507    .extend_from_slice(environment_variables_to_wrap_length.to_string().as_bytes());
508  environment_variables_netstring.push(b':');
509  environment_variables_netstring.append(&mut environment_variables_to_wrap);
510  environment_variables_netstring.push(b',');
511
512  // Write environment variable netstring
513  socket_writer
514    .write_all(&environment_variables_netstring)
515    .await?;
516
517  let cgi_stdin_reader = StreamReader::new(body.into_data_stream().map_err(std::io::Error::other));
518
519  // Emulated standard input and standard output
520  // SCGI doesn't support standard error
521  let stdin = socket_writer;
522  let stdout = socket_reader;
523
524  let mut cgi_response = CgiResponse::new(stdout);
525
526  let stdin_copy_future = async move {
527    let (mut cgi_stdin_reader, mut stdin) = (cgi_stdin_reader, stdin);
528    tokio::io::copy(&mut cgi_stdin_reader, &mut stdin)
529      .await
530      .map(|_| ())
531  };
532  let mut stdin_copy_future_pinned = Box::pin(stdin_copy_future);
533
534  let mut headers = [EMPTY_HEADER; 128];
535
536  let mut early_stdin_copied = false;
537
538  // Needed to wrap this in another scope to prevent errors with multiple mutable borrows.
539  {
540    let mut head_obtained = false;
541    let stdout_parse_future = cgi_response.get_head();
542    tokio::pin!(stdout_parse_future);
543
544    // Cannot use a loop with tokio::select, since stdin_copy_future_pinned being constantly ready will make the web server stop responding to HTTP requests
545    tokio::select! {
546      biased;
547
548      obtained_head = &mut stdout_parse_future => {
549        let obtained_head = obtained_head?;
550        if !obtained_head.is_empty() {
551          httparse::parse_headers(obtained_head, &mut headers)?;
552        }
553        head_obtained = true;
554      },
555      result = &mut stdin_copy_future_pinned => {
556        early_stdin_copied = true;
557        result?;
558      }
559    }
560
561    if !head_obtained {
562      // Kept it same as in the tokio::select macro
563      let obtained_head = stdout_parse_future.await?;
564      if !obtained_head.is_empty() {
565        httparse::parse_headers(obtained_head, &mut headers)?;
566      }
567    }
568  }
569
570  let mut response_builder = Response::builder();
571  let mut status_code = 200;
572  for header in headers {
573    if header == EMPTY_HEADER {
574      break;
575    }
576    let mut is_status_header = false;
577    match &header.name.to_lowercase() as &str {
578      "location" => {
579        if !(300..=399).contains(&status_code) {
580          status_code = 302;
581        }
582      }
583      "status" => {
584        is_status_header = true;
585        let header_value_cow = String::from_utf8_lossy(header.value);
586        let mut split_status = header_value_cow.split(" ");
587        let first_part = split_status.next();
588        if let Some(first_part) = first_part {
589          if first_part.starts_with("HTTP/") {
590            let second_part = split_status.next();
591            if let Some(second_part) = second_part {
592              if let Ok(parsed_status_code) = second_part.parse::<u16>() {
593                status_code = parsed_status_code;
594              }
595            }
596          } else if let Ok(parsed_status_code) = first_part.parse::<u16>() {
597            status_code = parsed_status_code;
598          }
599        }
600      }
601      _ => (),
602    }
603    if !is_status_header {
604      response_builder = response_builder.header(header.name, header.value);
605    }
606  }
607
608  response_builder = response_builder.status(status_code);
609
610  let reader_stream = ReaderStream::new(cgi_response);
611  let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data));
612  let boxed_body = stream_body.boxed();
613
614  let response = response_builder.body(boxed_body)?;
615
616  Ok(
617    ResponseData::builder_without_request()
618      .response(response)
619      .parallel_fn(async move {
620        if !early_stdin_copied {
621          stdin_copy_future_pinned.await.unwrap_or_default();
622        }
623      })
624      .build(),
625  )
626}
627
628async fn connect_tcp(
629  addr: &str,
630) -> Result<
631  (
632    Box<dyn AsyncRead + Send + Sync + Unpin>,
633    Box<dyn AsyncWrite + Send + Sync + Unpin>,
634  ),
635  tokio::io::Error,
636> {
637  let socket = TcpStream::connect(addr).await?;
638  socket.set_nodelay(true)?;
639
640  let (socket_reader_set, socket_writer_set) = tokio::io::split(socket);
641  Ok((Box::new(socket_reader_set), Box::new(socket_writer_set)))
642}
643
644#[allow(dead_code)]
645#[cfg(unix)]
646async fn connect_unix(
647  path: &str,
648) -> Result<
649  (
650    Box<dyn AsyncRead + Send + Sync + Unpin>,
651    Box<dyn AsyncWrite + Send + Sync + Unpin>,
652  ),
653  tokio::io::Error,
654> {
655  use tokio::net::UnixStream;
656
657  let socket = UnixStream::connect(path).await?;
658
659  let (socket_reader_set, socket_writer_set) = tokio::io::split(socket);
660  Ok((Box::new(socket_reader_set), Box::new(socket_writer_set)))
661}
662
663#[allow(dead_code)]
664#[cfg(not(unix))]
665async fn connect_unix(
666  _path: &str,
667) -> Result<
668  (
669    Box<dyn AsyncRead + Send + Sync + Unpin>,
670    Box<dyn AsyncWrite + Send + Sync + Unpin>,
671  ),
672  tokio::io::Error,
673> {
674  Err(tokio::io::Error::new(
675    tokio::io::ErrorKind::Unsupported,
676    "Unix sockets are not supports on non-Unix platforms.",
677  ))
678}