wafrift_encoding/encoding/
method_override.rs1#[must_use]
51pub fn override_header(method: &str) -> String {
52 format!("X-HTTP-Method-Override: {method}")
53}
54
55#[must_use]
58pub fn override_header_alt(method: &str) -> String {
59 format!("X-HTTP-Method: {method}")
60}
61
62#[must_use]
65pub fn override_header_express(method: &str) -> String {
66 format!("X-Method-Override: {method}")
67}
68
69#[must_use]
71pub fn override_header_case_mix(method: &str) -> String {
72 let mixed: String = method
73 .chars()
74 .enumerate()
75 .map(|(i, c)| {
76 if i % 2 == 0 {
77 c.to_ascii_lowercase()
78 } else {
79 c.to_ascii_uppercase()
80 }
81 })
82 .collect();
83 format!("X-HTTP-Method-Override: {mixed}")
84}
85
86#[must_use]
89pub fn override_header_padded(method: &str) -> String {
90 format!("X-HTTP-Method-Override: \t {method} ")
91}
92
93#[must_use]
96pub fn override_header_duplicate(method_a: &str, method_b: &str) -> String {
97 format!("X-HTTP-Method-Override: {method_a}\r\nX-HTTP-Method-Override: {method_b}")
98}
99
100#[must_use]
103pub fn form_field_method(method: &str) -> String {
104 format!("_method={method}")
105}
106
107#[must_use]
110pub fn query_method(method: &str) -> String {
111 format!("?_method={method}")
112}
113
114#[must_use]
117pub fn multipart_method(method: &str, boundary: &str) -> String {
118 format!(
119 "--{boundary}\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\n{method}\r\n--{boundary}--\r\n"
120 )
121}
122
123#[must_use]
127pub fn chunked_trailer_override(method: &str, body: &str) -> String {
128 let body_len_hex = format!("{:x}", body.len());
129 format!(
130 "Transfer-Encoding: chunked\r\nTrailer: X-HTTP-Method-Override\r\n\r\n{body_len_hex}\r\n{body}\r\n0\r\nX-HTTP-Method-Override: {method}\r\n\r\n"
131 )
132}
133
134#[must_use]
138pub fn header_plus_form_disagree(header_method: &str, form_method: &str) -> String {
139 format!(
140 "X-HTTP-Method-Override: {header_method}\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n_method={form_method}"
141 )
142}
143
144#[must_use]
148pub fn all_override_variants(method: &str) -> Vec<(&'static str, String)> {
149 vec![
150 ("header-standard", override_header(method)),
151 ("header-alt", override_header_alt(method)),
152 ("header-express", override_header_express(method)),
153 ("header-case-mix", override_header_case_mix(method)),
154 ("header-padded", override_header_padded(method)),
155 ("header-duplicate", override_header_duplicate("GET", method)),
156 ("form-field", form_field_method(method)),
157 ("query", query_method(method)),
158 (
159 "multipart",
160 multipart_method(method, "------WebKitFormBoundaryXXX"),
161 ),
162 (
163 "chunked-trailer",
164 chunked_trailer_override(method, "name=value"),
165 ),
166 ("header-plus-form", header_plus_form_disagree("GET", method)),
167 ]
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn override_header_basic_delete() {
176 assert_eq!(override_header("DELETE"), "X-HTTP-Method-Override: DELETE");
177 }
178
179 #[test]
180 fn override_header_alt_name() {
181 assert_eq!(override_header_alt("PUT"), "X-HTTP-Method: PUT");
182 }
183
184 #[test]
185 fn override_header_express_name() {
186 assert_eq!(override_header_express("PATCH"), "X-Method-Override: PATCH");
187 }
188
189 #[test]
190 fn override_header_case_mix_alternates() {
191 let h = override_header_case_mix("DELETE");
192 assert!(h.contains("dElEtE"));
194 }
195
196 #[test]
197 fn override_header_case_mix_short() {
198 let h = override_header_case_mix("GET");
199 assert!(h.contains("gEt"));
200 }
201
202 #[test]
203 fn override_header_padded_has_whitespace() {
204 let h = override_header_padded("DELETE");
205 assert!(h.contains("\t"));
206 assert!(h.contains(" ")); assert!(h.contains("DELETE"));
208 }
209
210 #[test]
211 fn override_header_duplicate_emits_two_lines() {
212 let h = override_header_duplicate("GET", "DELETE");
213 assert_eq!(h.matches("X-HTTP-Method-Override:").count(), 2);
214 assert!(h.contains("GET"));
215 assert!(h.contains("DELETE"));
216 }
217
218 #[test]
219 fn form_field_method_basic() {
220 assert_eq!(form_field_method("DELETE"), "_method=DELETE");
221 }
222
223 #[test]
224 fn query_method_has_question_mark() {
225 let q = query_method("DELETE");
226 assert_eq!(q, "?_method=DELETE");
227 assert!(q.starts_with('?'));
228 }
229
230 #[test]
231 fn multipart_method_contains_boundary_and_method() {
232 let m = multipart_method("DELETE", "BOUND");
233 assert!(m.contains("--BOUND"));
234 assert!(m.contains("--BOUND--"));
235 assert!(m.contains("name=\"_method\""));
236 assert!(m.contains("DELETE"));
237 }
238
239 #[test]
240 fn chunked_trailer_contains_trailer_header() {
241 let c = chunked_trailer_override("DELETE", "key=val");
242 assert!(c.contains("Transfer-Encoding: chunked"));
243 assert!(c.contains("Trailer: X-HTTP-Method-Override"));
244 assert!(c.contains("X-HTTP-Method-Override: DELETE"));
245 assert!(c.contains("\r\n0\r\n"));
247 }
248
249 #[test]
250 fn chunked_trailer_body_length_correct_hex() {
251 let c = chunked_trailer_override("DELETE", "abc");
252 assert!(c.contains("3\r\nabc\r\n"));
254 }
255
256 #[test]
257 fn chunked_trailer_body_length_two_digit_hex() {
258 let c = chunked_trailer_override("DELETE", "0123456789ABCDEF0123"); assert!(c.contains("14\r\n"));
261 }
262
263 #[test]
264 fn header_plus_form_uses_both_channels() {
265 let h = header_plus_form_disagree("GET", "DELETE");
266 assert!(h.contains("X-HTTP-Method-Override: GET"));
267 assert!(h.contains("_method=DELETE"));
268 }
269
270 #[test]
271 fn all_override_variants_count() {
272 let v = all_override_variants("DELETE");
273 assert!(v.len() >= 10);
274 }
275
276 #[test]
277 fn all_override_variants_unique_names() {
278 let v = all_override_variants("DELETE");
279 let names: std::collections::HashSet<&&str> = v.iter().map(|(n, _)| n).collect();
280 assert_eq!(names.len(), v.len());
281 }
282
283 #[test]
284 fn all_override_variants_contain_target_method() {
285 let v = all_override_variants("UNIQUEMARKER");
288 let marker_lower = "uniquemarker";
289 for (name, payload) in &v {
290 assert!(
291 payload.to_ascii_lowercase().contains(marker_lower),
292 "{name} doesn't carry the method: {payload}"
293 );
294 }
295 }
296
297 #[test]
298 fn deterministic_across_calls() {
299 let a = all_override_variants("DELETE");
300 let b = all_override_variants("DELETE");
301 assert_eq!(a, b);
302 }
303
304 #[test]
305 fn handles_unicode_method() {
306 let h = override_header("DÉLÊTE");
309 assert!(h.contains("DÉLÊTE"));
310 }
311
312 #[test]
313 fn adversarial_long_method_no_panic() {
314 let big = "X".repeat(10_000);
315 let _ = override_header(&big);
316 let _ = override_header_case_mix(&big);
317 let _ = all_override_variants(&big);
318 }
319
320 #[test]
321 fn override_header_empty_method() {
322 let h = override_header("");
323 assert_eq!(h, "X-HTTP-Method-Override: ");
324 }
325
326 #[test]
327 fn case_mix_idempotent_on_alternation() {
328 let a = override_header_case_mix("HELLO");
331 let b = override_header_case_mix("HELLO");
332 assert_eq!(a, b);
333 }
334
335 #[test]
336 fn duplicate_header_no_crlf_injection_outside_header_block() {
337 let h = override_header_duplicate("A", "B");
341 assert_eq!(h.matches("\r\n").count(), 1);
344 }
345
346 #[test]
347 fn case_mix_empty_string() {
348 assert_eq!(override_header_case_mix(""), "X-HTTP-Method-Override: ");
349 }
350
351 #[test]
352 fn multipart_handles_special_chars_in_method() {
353 let m = multipart_method("DELETE\r\nX-Inject: yes", "B");
355 assert!(m.contains("name=\"_method\""));
358 }
359
360 #[test]
361 fn chunked_trailer_empty_body() {
362 let c = chunked_trailer_override("DELETE", "");
363 assert!(c.contains("0\r\n"));
365 assert!(c.contains("X-HTTP-Method-Override: DELETE"));
366 }
367}