1use anyhow::{bail, ensure, Context, Result};
5use serde_json::{json, Map, Value};
6use tokio::io::{AsyncRead, AsyncReadExt};
7
8pub const VERSION: u8 = 1;
9pub const HEADER_LEN: usize = 8;
10pub const MAX_CONTENT: usize = 65_535;
12const CHUNK: usize = 65_528;
13
14pub const BEGIN_REQUEST: u8 = 1;
15pub const ABORT_REQUEST: u8 = 2;
16pub const END_REQUEST: u8 = 3;
17pub const PARAMS: u8 = 4;
18pub const STDIN: u8 = 5;
19pub const STDOUT: u8 = 6;
20pub const STDERR: u8 = 7;
21pub const DATA: u8 = 8;
22pub const GET_VALUES: u8 = 9;
23pub const GET_VALUES_RESULT: u8 = 10;
24pub const UNKNOWN_TYPE: u8 = 11;
25
26pub const ROLE_RESPONDER: u16 = 1;
27pub const ROLE_AUTHORIZER: u16 = 2;
28pub const ROLE_FILTER: u16 = 3;
29pub const KEEP_CONN: u8 = 1;
30
31pub const REQUEST_COMPLETE: u8 = 0;
32pub const CANT_MPX_CONN: u8 = 1;
33pub const OVERLOADED: u8 = 2;
34pub const UNKNOWN_ROLE: u8 = 3;
35
36pub const MAX_PARAMS_BYTES: usize = 64 * 1024;
38pub const MAX_PAIRS: usize = 512;
39pub const MAX_BODY_BYTES: usize = 1024 * 1024;
41pub const MAX_STDERR_BYTES: usize = 64 * 1024;
42
43pub fn protocol_status_name(s: u8) -> &'static str {
44 match s {
45 REQUEST_COMPLETE => "request_complete",
46 CANT_MPX_CONN => "cant_mpx_conn",
47 OVERLOADED => "overloaded",
48 UNKNOWN_ROLE => "unknown_role",
49 _ => "unknown",
50 }
51}
52
53#[derive(Debug, Clone, PartialEq)]
54pub struct Record {
55 pub kind: u8,
56 pub request_id: u16,
57 pub content: Vec<u8>,
58}
59
60pub async fn read_record<R: AsyncRead + Unpin>(r: &mut R) -> Result<Option<Record>> {
62 let mut header = [0u8; HEADER_LEN];
63 let mut got = 0;
64 while got < HEADER_LEN {
65 let n = r.read(&mut header[got..]).await?;
66 if n == 0 {
67 if got == 0 {
68 return Ok(None);
69 }
70 bail!("stream ended inside a record header");
71 }
72 got += n;
73 }
74 ensure!(
75 header[0] == VERSION,
76 "unsupported FastCGI version {}",
77 header[0]
78 );
79 let content_len = u16::from_be_bytes([header[4], header[5]]) as usize;
80 let padding = header[6] as usize;
81 let mut body = vec![0u8; content_len + padding];
82 r.read_exact(&mut body)
83 .await
84 .context("stream ended inside a record")?;
85 body.truncate(content_len);
86 Ok(Some(Record {
87 kind: header[1],
88 request_id: u16::from_be_bytes([header[2], header[3]]),
89 content: body,
90 }))
91}
92
93pub fn encode(kind: u8, request_id: u16, content: &[u8]) -> Vec<u8> {
95 assert!(
96 content.len() <= MAX_CONTENT,
97 "record content over 65535 bytes"
98 );
99 let padding = (8 - content.len() % 8) % 8;
100 let mut out = Vec::with_capacity(HEADER_LEN + content.len() + padding);
101 out.push(VERSION);
102 out.push(kind);
103 out.extend_from_slice(&request_id.to_be_bytes());
104 out.extend_from_slice(&(content.len() as u16).to_be_bytes());
105 out.push(padding as u8);
106 out.push(0);
107 out.extend_from_slice(content);
108 out.resize(out.len() + padding, 0);
109 out
110}
111
112pub fn encode_stream(kind: u8, request_id: u16, data: &[u8]) -> Vec<u8> {
115 let mut out = Vec::new();
116 for chunk in data.chunks(CHUNK) {
117 out.extend(encode(kind, request_id, chunk));
118 }
119 out.extend(encode(kind, request_id, &[]));
120 out
121}
122
123pub fn begin_request(role: u16, flags: u8) -> [u8; 8] {
124 let r = role.to_be_bytes();
125 [r[0], r[1], flags, 0, 0, 0, 0, 0]
126}
127
128pub fn end_request(app_status: u32, protocol_status: u8) -> [u8; 8] {
129 let a = app_status.to_be_bytes();
130 [a[0], a[1], a[2], a[3], protocol_status, 0, 0, 0]
131}
132
133pub fn unknown_type(kind: u8) -> [u8; 8] {
134 [kind, 0, 0, 0, 0, 0, 0, 0]
135}
136
137fn put_len(out: &mut Vec<u8>, n: usize) {
138 if n < 128 {
139 out.push(n as u8);
140 } else {
141 out.extend_from_slice(&((n as u32) | 0x8000_0000).to_be_bytes());
142 }
143}
144
145pub fn encode_pairs<'a>(pairs: impl IntoIterator<Item = (&'a str, &'a str)>) -> Vec<u8> {
146 let mut out = Vec::new();
147 for (k, v) in pairs {
148 put_len(&mut out, k.len());
149 put_len(&mut out, v.len());
150 out.extend_from_slice(k.as_bytes());
151 out.extend_from_slice(v.as_bytes());
152 }
153 out
154}
155
156pub fn decode_pairs(data: &[u8]) -> Result<Vec<(String, String)>> {
159 let mut out = Vec::new();
160 let mut i = 0;
161 let len = |i: &mut usize| -> Result<usize> {
162 let b = *data.get(*i).context("truncated name-value length")?;
163 if b < 0x80 {
164 *i += 1;
165 Ok(b as usize)
166 } else {
167 let bytes = data
168 .get(*i..*i + 4)
169 .context("truncated name-value length")?;
170 *i += 4;
171 Ok(
172 (u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) & 0x7fff_ffff)
173 as usize,
174 )
175 }
176 };
177 while i < data.len() {
178 ensure!(
179 out.len() < MAX_PAIRS,
180 "more than {MAX_PAIRS} name-value pairs"
181 );
182 let nl = len(&mut i)?;
183 let vl = len(&mut i)?;
184 ensure!(
185 nl <= data.len() && vl <= data.len() && i + nl + vl <= data.len(),
186 "name-value pair longer than its stream"
187 );
188 let name = &data[i..i + nl];
189 ensure!(
190 !name.is_empty() && name.iter().all(|b| b.is_ascii_graphic()),
191 "name-value pair name is not printable ASCII"
192 );
193 let value = String::from_utf8_lossy(&data[i + nl..i + nl + vl]).into_owned();
194 out.push((String::from_utf8_lossy(name).into_owned(), value));
195 i += nl + vl;
196 }
197 Ok(out)
198}
199
200pub fn body_text(bytes: &[u8]) -> (String, &'static str) {
202 match std::str::from_utf8(bytes) {
203 Ok(s) => (s.to_owned(), "utf8"),
204 Err(_) => (hex::encode(bytes), "hex"),
205 }
206}
207
208pub fn decode_body(v: &Value) -> Result<Vec<u8>> {
210 let body = match v.get("body") {
211 None | Some(Value::Null) => return Ok(Vec::new()),
212 Some(Value::String(s)) => s,
213 Some(_) => bail!("body must be a string"),
214 };
215 let bytes = match v["body_encoding"].as_str().unwrap_or("utf8") {
216 "utf8" => body.as_bytes().to_vec(),
217 "hex" => hex::decode(body).context("body is not valid hex")?,
218 other => bail!("body_encoding must be utf8 or hex, not {other}"),
219 };
220 ensure!(bytes.len() <= MAX_BODY_BYTES, "body over 1 MiB");
221 Ok(bytes)
222}
223
224pub fn check_headers(headers: Option<&Value>) -> Result<Vec<(String, String)>> {
226 let Some(h) = headers.filter(|h| !h.is_null()) else {
227 return Ok(vec![]);
228 };
229 let map = h.as_object().context("headers must be an object")?;
230 ensure!(map.len() <= 64, "at most 64 headers");
231 map.iter()
232 .map(|(k, v)| {
233 ensure!(
234 !k.is_empty()
235 && k.len() <= 256
236 && k.bytes()
237 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)),
238 "header name {k:?} is not an HTTP token"
239 );
240 let v = v
241 .as_str()
242 .with_context(|| format!("header {k} must be a string"))?;
243 ensure!(
244 v.len() <= 8192 && !v.chars().any(|c| c.is_control() && c != '\t'),
245 "header {k} contains a control character"
246 );
247 Ok((k.clone(), v.to_owned()))
248 })
249 .collect()
250}
251
252fn reason(status: u16) -> &'static str {
253 hyper::StatusCode::from_u16(status)
254 .ok()
255 .and_then(|s| s.canonical_reason())
256 .unwrap_or("Unknown")
257}
258
259pub fn build_cgi_response(status: u16, headers: &[(String, String)], body: &[u8]) -> Vec<u8> {
261 let mut out = format!("Status: {status} {}\r\n", reason(status)).into_bytes();
262 let mut typed = false;
263 for (k, v) in headers {
264 if k.eq_ignore_ascii_case("status") {
265 continue;
266 }
267 typed |= k.eq_ignore_ascii_case("content-type");
268 out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
269 }
270 if !typed {
271 out.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
272 }
273 out.extend_from_slice(b"\r\n");
274 out.extend_from_slice(body);
275 out
276}
277
278pub fn parse_cgi_response(stdout: &[u8]) -> Result<(u16, Map<String, Value>, Vec<u8>)> {
281 let (head, body) = match find(stdout, b"\r\n\r\n") {
282 Some(i) => (&stdout[..i], &stdout[i + 4..]),
283 None => match find(stdout, b"\n\n") {
284 Some(i) => (&stdout[..i], &stdout[i + 2..]),
285 None => bail!("CGI response has no end of headers"),
286 },
287 };
288 ensure!(head.len() <= 64 * 1024, "CGI response headers over 64 KiB");
289 let head = std::str::from_utf8(head).context("CGI response headers are not UTF-8")?;
290 let mut headers = Map::new();
291 let mut status = None;
292 for line in head.split('\n').map(|l| l.trim_end_matches('\r')) {
293 if line.is_empty() {
294 continue;
295 }
296 let (k, v) = line
297 .split_once(':')
298 .with_context(|| format!("CGI header line without a colon: {line:?}"))?;
299 let (k, v) = (k.trim(), v.trim());
300 if k.eq_ignore_ascii_case("status") {
301 let code = v.split_whitespace().next().unwrap_or("");
302 let code: u16 = code.parse().context("Status is not a number")?;
303 ensure!((100..=599).contains(&code), "Status {code} out of range");
304 status = Some(code);
305 } else {
306 headers.insert(k.to_ascii_lowercase(), json!(v));
307 }
308 }
309 let status = status.unwrap_or(if headers.contains_key("location") {
310 302
311 } else {
312 200
313 });
314 Ok((status, headers, body.to_vec()))
315}
316
317fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
318 hay.windows(needle.len()).position(|w| w == needle)
319}