scatter_proxy/
classifier.rs1use http::{HeaderMap, StatusCode};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum BodyVerdict {
14 Success,
15 ProxyBlocked,
16 TargetError,
17 Challenge,
19}
20
21pub trait BodyClassifier: Send + Sync + 'static {
25 fn classify(&self, status: StatusCode, headers: &HeaderMap, body: &[u8]) -> BodyVerdict;
26}
27
28pub struct DefaultClassifier;
38
39impl BodyClassifier for DefaultClassifier {
40 fn classify(&self, status: StatusCode, _headers: &HeaderMap, body: &[u8]) -> BodyVerdict {
41 if status.is_success() {
42 if body.is_empty() {
43 BodyVerdict::ProxyBlocked
44 } else {
45 BodyVerdict::Success
46 }
47 } else if status == StatusCode::FORBIDDEN || status == StatusCode::TOO_MANY_REQUESTS {
48 BodyVerdict::ProxyBlocked
49 } else if status.is_server_error() {
50 BodyVerdict::TargetError
51 } else {
52 BodyVerdict::ProxyBlocked
53 }
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 fn classify(status: u16, body: &[u8]) -> BodyVerdict {
64 let classifier = DefaultClassifier;
65 let headers = HeaderMap::new();
66 classifier.classify(StatusCode::from_u16(status).unwrap(), &headers, body)
67 }
68
69 #[test]
72 fn success_200_with_body() {
73 assert_eq!(classify(200, b"hello"), BodyVerdict::Success);
74 }
75
76 #[test]
77 fn success_201_with_body() {
78 assert_eq!(classify(201, b"{\"id\":1}"), BodyVerdict::Success);
79 }
80
81 #[test]
82 fn success_204_no_content_empty_body() {
83 assert_eq!(classify(204, b""), BodyVerdict::ProxyBlocked);
85 }
86
87 #[test]
88 fn proxy_blocked_200_empty_body() {
89 assert_eq!(classify(200, b""), BodyVerdict::ProxyBlocked);
90 }
91
92 #[test]
95 fn proxy_blocked_403() {
96 assert_eq!(classify(403, b"Forbidden"), BodyVerdict::ProxyBlocked);
97 }
98
99 #[test]
100 fn proxy_blocked_429() {
101 assert_eq!(classify(429, b"Rate limited"), BodyVerdict::ProxyBlocked);
102 }
103
104 #[test]
107 fn target_error_500() {
108 assert_eq!(
109 classify(500, b"Internal Server Error"),
110 BodyVerdict::TargetError
111 );
112 }
113
114 #[test]
115 fn target_error_502() {
116 assert_eq!(classify(502, b"Bad Gateway"), BodyVerdict::TargetError);
117 }
118
119 #[test]
120 fn target_error_503() {
121 assert_eq!(
122 classify(503, b"Service Unavailable"),
123 BodyVerdict::TargetError
124 );
125 }
126
127 #[test]
130 fn proxy_blocked_301_redirect() {
131 assert_eq!(classify(301, b"Moved"), BodyVerdict::ProxyBlocked);
132 }
133
134 #[test]
135 fn proxy_blocked_400_bad_request() {
136 assert_eq!(classify(400, b"Bad Request"), BodyVerdict::ProxyBlocked);
137 }
138
139 #[test]
140 fn proxy_blocked_401_unauthorised() {
141 assert_eq!(classify(401, b"Unauthorised"), BodyVerdict::ProxyBlocked);
142 }
143
144 #[test]
145 fn proxy_blocked_404_not_found() {
146 assert_eq!(classify(404, b"Not Found"), BodyVerdict::ProxyBlocked);
147 }
148
149 #[test]
150 fn proxy_blocked_407_proxy_auth_required() {
151 assert_eq!(
152 classify(407, b"Proxy Authentication Required"),
153 BodyVerdict::ProxyBlocked
154 );
155 }
156
157 #[test]
160 fn headers_are_available_to_classifier() {
161 let classifier = DefaultClassifier;
162 let mut headers = HeaderMap::new();
163 headers.insert("x-custom", "value".parse().unwrap());
164 assert_eq!(
166 classifier.classify(StatusCode::OK, &headers, b"data"),
167 BodyVerdict::Success,
168 );
169 }
170
171 #[test]
174 fn can_be_used_as_trait_object() {
175 let classifier: Box<dyn BodyClassifier> = Box::new(DefaultClassifier);
176 let headers = HeaderMap::new();
177 assert_eq!(
178 classifier.classify(StatusCode::OK, &headers, b"body"),
179 BodyVerdict::Success,
180 );
181 }
182
183 struct AlwaysSuccess;
186
187 impl BodyClassifier for AlwaysSuccess {
188 fn classify(&self, _status: StatusCode, _headers: &HeaderMap, _body: &[u8]) -> BodyVerdict {
189 BodyVerdict::Success
190 }
191 }
192
193 #[test]
194 fn custom_classifier_overrides_defaults() {
195 let classifier = AlwaysSuccess;
196 let headers = HeaderMap::new();
197 assert_eq!(
199 classifier.classify(StatusCode::INTERNAL_SERVER_ERROR, &headers, b""),
200 BodyVerdict::Success,
201 );
202 }
203
204 #[test]
207 fn verdict_is_copy_and_clone() {
208 let v = BodyVerdict::Success;
209 let v2 = v; let v3 = v; assert_eq!(v, v2);
212 assert_eq!(v2, v3);
213 }
214
215 #[test]
216 fn verdict_debug_format() {
217 assert_eq!(format!("{:?}", BodyVerdict::Success), "Success");
218 assert_eq!(format!("{:?}", BodyVerdict::ProxyBlocked), "ProxyBlocked");
219 assert_eq!(format!("{:?}", BodyVerdict::TargetError), "TargetError");
220 assert_eq!(format!("{:?}", BodyVerdict::Challenge), "Challenge");
221 }
222
223 struct WafChallengeClassifier;
226
227 impl BodyClassifier for WafChallengeClassifier {
228 fn classify(&self, status: StatusCode, _headers: &HeaderMap, body: &[u8]) -> BodyVerdict {
229 if status.is_success() {
230 if body.starts_with(b"<script>") {
231 BodyVerdict::Challenge
232 } else if body.is_empty() {
233 BodyVerdict::ProxyBlocked
234 } else {
235 BodyVerdict::Success
236 }
237 } else {
238 BodyVerdict::ProxyBlocked
239 }
240 }
241 }
242
243 #[test]
244 fn custom_classifier_returns_challenge() {
245 let classifier = WafChallengeClassifier;
246 let headers = HeaderMap::new();
247 assert_eq!(
248 classifier.classify(StatusCode::OK, &headers, b"<script>var x=1</script>"),
249 BodyVerdict::Challenge,
250 );
251 }
252
253 #[test]
254 fn challenge_variant_is_eq() {
255 assert_eq!(BodyVerdict::Challenge, BodyVerdict::Challenge);
256 assert_ne!(BodyVerdict::Challenge, BodyVerdict::ProxyBlocked);
257 }
258}