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 {}",
106                      app_status
107                    )))));
108                  }
109                } else {
110                  // Record malformed, ignoring the record
111                  return Ok(None);
112                }
113              }
114              6 => {
115                // STDOUT record
116                self.state = FcgiDecodeState::ReadingHead;
117                if content.is_empty() {
118                  return Ok(None);
119                }
120                return Ok(Some(FcgiDecodedData::Stdout(Bytes::from_owner(content))));
121              }
122              7 => {
123                // STDERR record
124                self.state = FcgiDecodeState::ReadingHead;
125                if content.is_empty() {
126                  return Ok(None);
127                }
128                return Ok(Some(FcgiDecodedData::Stderr(Bytes::from_owner(content))));
129              }
130              _ => {
131                // This should be unreachable
132                unreachable!()
133              }
134            };
135          } else {
136            return Ok(None);
137          }
138        }
139        FcgiDecodeState::Finished => {
140          src.clear();
141          return Ok(None);
142        }
143      }
144    }
145  }
146}
147
148#[cfg(test)]
149mod tests {
150  use super::*;
151  use crate::ferron_util::fcgi_record::construct_fastcgi_record;
152  use tokio_util::bytes::BytesMut;
153  use tokio_util::codec::Decoder;
154
155  #[test]
156  fn test_fcgi_decoder_stdout() {
157    let mut decoder = FcgiDecoder::new();
158    let mut buf = BytesMut::new();
159
160    // Construct a STDOUT record
161    let record_type = 6;
162    let request_id = 1;
163    let content = b"Hello, FastCGI!";
164    let record = construct_fastcgi_record(record_type, request_id, content);
165
166    buf.extend_from_slice(&record);
167
168    let result = decoder.decode(&mut buf).unwrap();
169    assert!(result.is_some());
170    if let Some(FcgiDecodedData::Stdout(data)) = result {
171      assert_eq!(&data[..], content);
172    } else {
173      panic!("Expected STDOUT data");
174    }
175  }
176
177  #[test]
178  fn test_fcgi_decoder_stderr() {
179    let mut decoder = FcgiDecoder::new();
180    let mut buf = BytesMut::new();
181
182    // Construct a STDERR record
183    let record_type = 7;
184    let request_id = 1;
185    let content = b"Error message";
186    let record = construct_fastcgi_record(record_type, request_id, content);
187
188    buf.extend_from_slice(&record);
189
190    let result = decoder.decode(&mut buf).unwrap();
191    assert!(result.is_some());
192    if let Some(FcgiDecodedData::Stderr(data)) = result {
193      assert_eq!(&data[..], content);
194    } else {
195      panic!("Expected STDERR data");
196    }
197  }
198
199  #[test]
200  fn test_fcgi_decoder_end_request() {
201    let mut decoder = FcgiDecoder::new();
202    let mut buf = BytesMut::new();
203
204    // Construct an END_REQUEST record
205    let record_type = 3;
206    let request_id = 1;
207    let mut content = [0u8; 4].to_vec(); // App status
208    content.push(0); // Protocol status
209    let record = construct_fastcgi_record(record_type, request_id, &content);
210
211    buf.extend_from_slice(&record);
212
213    let result = decoder.decode(&mut buf).unwrap();
214    assert!(result.is_none()); // No data for END_REQUEST
215  }
216
217  #[test]
218  fn test_fcgi_decoder_invalid_record() {
219    let mut decoder = FcgiDecoder::new();
220    let mut buf = BytesMut::new();
221
222    // Construct an invalid record with wrong request ID
223    let record_type = 6;
224    let request_id = 2; // Invalid request ID
225    let content = b"Invalid record";
226    let record = construct_fastcgi_record(record_type, request_id, content);
227
228    buf.extend_from_slice(&record);
229
230    let result = decoder.decode(&mut buf).unwrap();
231    assert!(result.is_none()); // Invalid record should be ignored
232  }
233}