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