1use std::sync::Arc;
6
7use plecto_host::{
8 Header, HttpRequest, HttpResponse, LoadedFilter, RequestBodyDecision, RequestDecision,
9 RequestEdit, RequestTrace, ResponseDecision, ResponseEdit,
10};
11
12pub enum ChainOutcome {
14 Respond(HttpResponse),
17 Forward(HttpRequest),
19}
20
21pub enum RequestBodyOutcome {
24 Respond(HttpResponse),
27 Forward(Vec<u8>),
29}
30
31pub enum ResponseOutcome {
34 Forward(HttpResponse),
37 Respond(HttpResponse),
40}
41
42pub(crate) fn dispatch_request(
43 chain: &[Arc<LoadedFilter>],
44 mut request: HttpRequest,
45 trace: &RequestTrace,
46) -> ChainOutcome {
47 for filter in chain {
48 match filter.on_request(&request, trace) {
51 Ok((RequestDecision::Continue, _logs)) => {}
52 Ok((RequestDecision::Modified(edit), _logs)) => apply_request_edit(&mut request, edit),
53 Ok((RequestDecision::ShortCircuit(response), _logs)) => {
54 return ChainOutcome::Respond(response);
55 }
56 Err(err) => return ChainOutcome::Respond(err.fail_closed_response()),
58 }
59 }
60 ChainOutcome::Forward(request)
61}
62
63pub(crate) fn dispatch_request_body(
68 chain: &[Arc<LoadedFilter>],
69 mut body: Vec<u8>,
70 trace: &RequestTrace,
71) -> RequestBodyOutcome {
72 for filter in chain {
73 match filter.on_request_body(&body, trace) {
74 Ok((RequestBodyDecision::Continue(next), _logs)) => body = next,
75 Ok((RequestBodyDecision::ShortCircuit(response), _logs)) => {
76 return RequestBodyOutcome::Respond(response);
77 }
78 Err(err) => return RequestBodyOutcome::Respond(err.fail_closed_response()),
80 }
81 }
82 RequestBodyOutcome::Forward(body)
83}
84
85pub(crate) fn dispatch_response(
86 chain: &[Arc<LoadedFilter>],
87 request: &HttpRequest,
88 mut response: HttpResponse,
89 trace: &RequestTrace,
90) -> ResponseOutcome {
91 for filter in chain.iter().rev() {
98 match filter.on_response(request, &response, trace) {
99 Ok((ResponseDecision::Continue, _logs)) => {}
100 Ok((ResponseDecision::Modified(edit), _logs)) => {
101 apply_response_edit(&mut response, edit)
102 }
103 Ok((ResponseDecision::Replace(replacement), _logs)) => {
104 return ResponseOutcome::Respond(replacement);
105 }
106 Err(err) => return ResponseOutcome::Respond(err.fail_closed_response()),
107 }
108 }
109 ResponseOutcome::Forward(response)
110}
111
112fn apply_request_edit(request: &mut HttpRequest, edit: RequestEdit) {
115 remove_headers(&mut request.headers, &edit.remove_headers);
116 set_headers(&mut request.headers, edit.set_headers);
117}
118
119fn apply_response_edit(response: &mut HttpResponse, edit: ResponseEdit) {
120 if let Some(status) = edit.set_status {
121 response.status = status;
122 }
123 remove_headers(&mut response.headers, &edit.remove_headers);
124 set_headers(&mut response.headers, edit.set_headers);
125}
126
127fn remove_headers(headers: &mut Vec<Header>, names: &[String]) {
128 for name in names {
129 headers.retain(|h| !h.name.eq_ignore_ascii_case(name));
130 }
131}
132
133fn set_headers(headers: &mut Vec<Header>, set: Vec<Header>) {
134 for header in set {
135 if !is_repeatable(&header.name) {
140 headers.retain(|existing| !existing.name.eq_ignore_ascii_case(&header.name));
141 }
142 headers.push(header);
143 }
144}
145
146fn is_repeatable(name: &str) -> bool {
149 name.eq_ignore_ascii_case("set-cookie")
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn h(name: &str, value: &str) -> Header {
157 Header {
158 name: name.to_string(),
159 value: value.as_bytes().to_vec(),
160 }
161 }
162
163 #[test]
164 fn request_edit_sets_replaces_and_removes_case_insensitively() {
165 let mut request = HttpRequest {
166 method: "GET".to_string(),
167 path: "/".to_string(),
168 authority: "a".to_string(),
169 scheme: "https".to_string(),
170 headers: vec![h("X-Keep", "1"), h("X-Drop", "old"), h("X-Replace", "old")],
171 };
172 apply_request_edit(
173 &mut request,
174 RequestEdit {
175 set_headers: vec![h("x-replace", "new"), h("X-Add", "1")],
176 remove_headers: vec!["x-drop".to_string()],
177 },
178 );
179
180 assert!(
181 !request
182 .headers
183 .iter()
184 .any(|x| x.name.eq_ignore_ascii_case("x-drop")),
185 "removed header gone"
186 );
187 let replaced: Vec<_> = request
188 .headers
189 .iter()
190 .filter(|x| x.name.eq_ignore_ascii_case("x-replace"))
191 .collect();
192 assert_eq!(replaced.len(), 1, "set replaces, not duplicates");
193 assert_eq!(replaced[0].value.as_slice(), b"new");
194 assert!(
195 request
196 .headers
197 .iter()
198 .any(|x| x.name.eq_ignore_ascii_case("x-add"))
199 );
200 assert!(request.headers.iter().any(|x| x.name == "X-Keep"));
201 }
202
203 #[test]
204 fn response_edit_sets_status_and_headers() {
205 let mut response = HttpResponse {
206 status: 200,
207 headers: vec![h("X-Old", "1")],
208 body: vec![],
209 };
210 apply_response_edit(
211 &mut response,
212 ResponseEdit {
213 set_status: Some(503),
214 set_headers: vec![h("X-New", "1")],
215 remove_headers: vec!["x-old".to_string()],
216 },
217 );
218
219 assert_eq!(response.status, 503);
220 assert!(response.headers.iter().any(|x| x.name == "X-New"));
221 assert!(
222 !response
223 .headers
224 .iter()
225 .any(|x| x.name.eq_ignore_ascii_case("x-old"))
226 );
227 }
228
229 #[test]
230 fn response_edit_appends_repeatable_set_cookie_but_replaces_others() {
231 let mut response = HttpResponse {
235 status: 200,
236 headers: vec![h("x-keep", "1")],
237 body: vec![],
238 };
239 apply_response_edit(
240 &mut response,
241 ResponseEdit {
242 set_status: None,
243 set_headers: vec![
244 h("set-cookie", "session=abc"),
245 h("set-cookie", "flag=1"),
246 h("x-keep", "2"),
247 ],
248 remove_headers: vec![],
249 },
250 );
251
252 let cookies: Vec<&str> = response
253 .headers
254 .iter()
255 .filter(|x| x.name.eq_ignore_ascii_case("set-cookie"))
256 .map(|x| std::str::from_utf8(&x.value).expect("utf-8"))
257 .collect();
258 assert_eq!(
259 cookies,
260 vec!["session=abc", "flag=1"],
261 "both Set-Cookie field lines must survive (append, not collapse)"
262 );
263 let keep: Vec<&Header> = response
264 .headers
265 .iter()
266 .filter(|x| x.name.eq_ignore_ascii_case("x-keep"))
267 .collect();
268 assert_eq!(keep.len(), 1, "a non-repeatable header still replaces");
269 assert_eq!(keep[0].value.as_slice(), b"2");
270 }
271}