ferron/util/
fcgi_decoder.rs

1use hyper::body::{Buf, Bytes};
2use tokio_util::bytes::BytesMut;
3use tokio_util::codec::Decoder;
4
5#[derive(Debug)]
6pub enum FcgiDecodedData {
7  Stdout(Bytes),
8  Stderr(Bytes),
9}
10
11enum FcgiDecodeState {
12  ReadingHead,
13  ReadingContent,
14  Finished,
15}
16
17pub struct FcgiDecoder {
18  header: Vec<u8>,
19  content_length: u16,
20  padding_length: u8,
21  state: FcgiDecodeState,
22}
23
24impl FcgiDecoder {
25  pub fn new() -> Self {
26    Self {
27      header: Vec::new(),
28      content_length: 0,
29      padding_length: 0,
30      state: FcgiDecodeState::ReadingHead,
31    }
32  }
33}
34
35impl Decoder for FcgiDecoder {
36  type Error = std::io::Error;
37  type Item = FcgiDecodedData;
38
39  fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
40    loop {
41      match self.state {
42        FcgiDecodeState::ReadingHead => {
43          if src.len() >= 8 {
44            let header = &src[..8];
45            self.header = header.to_vec();
46            src.advance(8);
47            self.content_length = u16::from_be_bytes(
48              self.header[4..6]
49                .try_into()
50                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?,
51            );
52            self.padding_length = self.header[6];
53            self.state = FcgiDecodeState::ReadingContent;
54          } else {
55            return Ok(None);
56          }
57        }
58        FcgiDecodeState::ReadingContent => {
59          if src.len() >= self.content_length as usize + self.padding_length as usize {
60            let request_id = u16::from_be_bytes(
61              self.header[2..4]
62                .try_into()
63                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?,
64            );
65            let record_type = self.header[1];
66            if request_id != 1 || (record_type != 3 && record_type != 6 && record_type != 7) {
67              // Ignore the record for wrong request ID or if the record isn't END_REQUEST, STDOUT or STDERR
68              src.advance(self.content_length as usize + self.padding_length as usize);
69              return Ok(None);
70            }
71            let content_borrowed = &src[..(self.content_length as usize)];
72            let content = content_borrowed.to_vec();
73            src.advance(self.content_length as usize + self.padding_length as usize);
74
75            match record_type {
76              3 => {
77                // END_REQUEST record
78                if content.len() > 5 {
79                  let app_status = u32::from_be_bytes(
80                    content[0..4]
81                      .try_into()
82                      .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?,
83                  );
84                  let protocol_status = content[4];
85                  match protocol_status {
86                    0 => (),
87                    1 => return Err(std::io::Error::other("FastCGI server overloaded")),
88                    2 => {
89                      return Err(std::io::Error::other(
90                        "Role not supported by the FastCGI application",
91                      ))
92                    }
93                    3 => {
94                      return Err(std::io::Error::other(
95                        "Multiplexed connections not supported by the FastCGI application",
96                      ))
97                    }
98                    _ => return Err(std::io::Error::other("Unknown error")),
99                  }
100
101                  self.state = FcgiDecodeState::Finished;
102                  if app_status != 0 {
103                    // Inject data into standard error stream
104                    return Ok(Some(FcgiDecodedData::Stderr(Bytes::from_owner(format!(
105                      "FastCGI application exited with code {app_status}"
106                    )))));
107                  }
108                } else {
109                  // Record malformed, ignoring the record
110                  return Ok(None);
111                }
112              }
113              6 => {
114                // STDOUT record
115                self.state = FcgiDecodeState::ReadingHead;
116                if content.is_empty() {
117                  return Ok(None);
118                }
119                return Ok(Some(FcgiDecodedData::Stdout(Bytes::from_owner(content))));
120              }
121              7 => {
122                // STDERR record
123                self.state = FcgiDecodeState::ReadingHead;
124                if content.is_empty() {
125                  return Ok(None);
126                }
127                return Ok(Some(FcgiDecodedData::Stderr(Bytes::from_owner(content))));
128              }
129              _ => {
130                // This should be unreachable
131                unreachable!()
132              }
133            };
134          } else {
135            return Ok(None);
136          }
137        }
138        FcgiDecodeState::Finished => {
139          src.clear();
140          return Ok(None);
141        }
142      }
143    }
144  }
145}
146
147#[cfg(test)]
148mod tests {
149  use super::*;
150  use crate::ferron_util::fcgi_record::construct_fastcgi_record;
151  use tokio_util::bytes::BytesMut;
152  use tokio_util::codec::Decoder;
153
154  #[test]
155  fn test_fcgi_decoder_stdout() {
156    let mut decoder = FcgiDecoder::new();
157    let mut buf = BytesMut::new();
158
159    // Construct a STDOUT record
160    let record_type = 6;
161    let request_id = 1;
162    let content = b"Hello, FastCGI!";
163    let record = construct_fastcgi_record(record_type, request_id, content);
164
165    buf.extend_from_slice(&record);
166
167    let result = decoder.decode(&mut buf).unwrap();
168    assert!(result.is_some());
169    if let Some(FcgiDecodedData::Stdout(data)) = result {
170      assert_eq!(&data[..], content);
171    } else {
172      panic!("Expected STDOUT data");
173    }
174  }
175
176  #[test]
177  fn test_fcgi_decoder_stderr() {
178    let mut decoder = FcgiDecoder::new();
179    let mut buf = BytesMut::new();
180
181    // Construct a STDERR record
182    let record_type = 7;
183    let request_id = 1;
184    let content = b"Error message";
185    let record = construct_fastcgi_record(record_type, request_id, content);
186
187    buf.extend_from_slice(&record);
188
189    let result = decoder.decode(&mut buf).unwrap();
190    assert!(result.is_some());
191    if let Some(FcgiDecodedData::Stderr(data)) = result {
192      assert_eq!(&data[..], content);
193    } else {
194      panic!("Expected STDERR data");
195    }
196  }
197
198  #[test]
199  fn test_fcgi_decoder_end_request() {
200    let mut decoder = FcgiDecoder::new();
201    let mut buf = BytesMut::new();
202
203    // Construct an END_REQUEST record
204    let record_type = 3;
205    let request_id = 1;
206    let mut content = [0u8; 4].to_vec(); // App status
207    content.push(0); // Protocol status
208    let record = construct_fastcgi_record(record_type, request_id, &content);
209
210    buf.extend_from_slice(&record);
211
212    let result = decoder.decode(&mut buf).unwrap();
213    assert!(result.is_none()); // No data for END_REQUEST
214  }
215
216  #[test]
217  fn test_fcgi_decoder_invalid_record() {
218    let mut decoder = FcgiDecoder::new();
219    let mut buf = BytesMut::new();
220
221    // Construct an invalid record with wrong request ID
222    let record_type = 6;
223    let request_id = 2; // Invalid request ID
224    let content = b"Invalid record";
225    let record = construct_fastcgi_record(record_type, request_id, content);
226
227    buf.extend_from_slice(&record);
228
229    let result = decoder.decode(&mut buf).unwrap();
230    assert!(result.is_none()); // Invalid record should be ignored
231  }
232}