1use std::fmt;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23#[non_exhaustive]
24pub enum HeaderTechnique {
25 CaseMixing,
27 TabSeparator,
29 WhitespacePadding,
31 LineFolding,
33 LfOnlyLineFolding,
35 DuplicateHeader,
37 UnderscoreSubstitution,
39 NullByteInjection,
41 TrailingSpace,
43 MultiLineFolding,
45 LfOnlyMultiLineFolding,
47 CommaJoin,
49}
50
51impl fmt::Display for HeaderTechnique {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::CaseMixing => f.write_str("case-mixing"),
55 Self::TabSeparator => f.write_str("tab-separator"),
56 Self::WhitespacePadding => f.write_str("whitespace-padding"),
57 Self::LineFolding => f.write_str("line-folding"),
58 Self::LfOnlyLineFolding => f.write_str("lf-only-line-folding"),
59 Self::DuplicateHeader => f.write_str("duplicate-header"),
60 Self::UnderscoreSubstitution => f.write_str("underscore-substitution"),
61 Self::NullByteInjection => f.write_str("null-byte-injection"),
62 Self::TrailingSpace => f.write_str("trailing-space"),
63 Self::MultiLineFolding => f.write_str("multi-line-folding"),
64 Self::LfOnlyMultiLineFolding => f.write_str("lf-only-multi-line-folding"),
65 Self::CommaJoin => f.write_str("comma-join"),
66 }
67 }
68}
69
70#[must_use]
76pub fn case_mix(header_name: &str) -> String {
77 crate::encoding::keyword::alternating_case(header_name, false)
78}
79
80#[must_use]
82pub fn tab_separator(header_name: &str, value: &str) -> String {
83 format!("{header_name}:\t{value}")
84}
85
86#[must_use]
88pub fn whitespace_pad(header_name: &str, value: &str) -> String {
89 let pad_count = rand::random::<usize>() % 4 + 2; let left = " ".repeat(pad_count);
91 let right = " ".repeat(pad_count);
92 format!("{header_name}:{left}{value}{right}")
93}
94
95fn char_boundary_near(s: &str, byte_idx: usize) -> usize {
96 if byte_idx >= s.len() {
97 return s.len();
98 }
99 let mut i = byte_idx;
100 while i > 0 && !s.is_char_boundary(i) {
101 i -= 1;
102 }
103 i
104}
105
106#[must_use]
112pub fn line_fold(header_name: &str, value: &str) -> String {
113 line_fold_with_ending(header_name, value, "\r\n")
114}
115
116#[must_use]
118pub fn lf_only_line_fold(header_name: &str, value: &str) -> String {
119 line_fold_with_ending(header_name, value, "\n")
120}
121
122fn line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
123 if value.len() < 4 {
124 return format!("{header_name}: {value}");
125 }
126 let mid = char_boundary_near(value, value.len() / 2);
127 format!(
128 "{}: {}{ending}\t{}",
129 header_name,
130 &value[..mid],
131 &value[mid..]
132 )
133}
134
135#[must_use]
140pub fn multi_line_fold(header_name: &str, value: &str) -> String {
141 multi_line_fold_with_ending(header_name, value, "\r\n")
142}
143
144#[must_use]
146pub fn lf_only_multi_line_fold(header_name: &str, value: &str) -> String {
147 multi_line_fold_with_ending(header_name, value, "\n")
148}
149
150fn multi_line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
151 if value.len() < 6 {
152 return format!("{header_name}: {value}");
153 }
154 let t1 = char_boundary_near(value, value.len() / 3);
155 let t2 = char_boundary_near(value, value.len() * 2 / 3);
156 format!(
157 "{}: {}{ending} {}{ending}\t{}",
158 header_name,
159 &value[..t1],
160 &value[t1..t2],
161 &value[t2..]
162 )
163}
164
165#[must_use]
172pub fn duplicate_header(
173 header_name: &str,
174 real_value: &str,
175 benign_value: &str,
176) -> (String, String) {
177 (
178 format!("{header_name}: {benign_value}"),
179 format!("{header_name}: {real_value}"),
180 )
181}
182
183#[must_use]
188pub fn underscore_substitute(header_name: &str) -> String {
189 header_name.replace('-', "_")
190}
191
192#[must_use]
200pub fn null_byte_inject(header_name: &str) -> String {
201 if header_name.len() < 2 {
202 return header_name.to_string();
203 }
204 let mid = char_boundary_near(header_name, header_name.len() / 2);
205 format!("{}\x00{}", &header_name[..mid], &header_name[mid..])
206}
207
208#[must_use]
214pub fn trailing_space(header_name: &str, value: &str) -> String {
215 format!("{header_name} : {value}")
216}
217
218#[must_use]
226pub fn comma_join(header_name: &str, real_value: &str, benign_value: &str) -> String {
227 format!("{header_name}: {benign_value}, {real_value}")
228}
229
230#[must_use]
235pub fn all_obfuscations(header_name: &str, value: &str) -> Vec<(HeaderTechnique, String)> {
236 let benign = "safe_value";
237 vec![
238 (
239 HeaderTechnique::CaseMixing,
240 format!("{}: {}", case_mix(header_name), value),
241 ),
242 (
243 HeaderTechnique::TabSeparator,
244 tab_separator(header_name, value),
245 ),
246 (
247 HeaderTechnique::WhitespacePadding,
248 whitespace_pad(header_name, value),
249 ),
250 (HeaderTechnique::LineFolding, line_fold(header_name, value)),
251 (
252 HeaderTechnique::LfOnlyLineFolding,
253 lf_only_line_fold(header_name, value),
254 ),
255 (HeaderTechnique::DuplicateHeader, {
256 let (a, b) = duplicate_header(header_name, value, benign);
257 format!("{a}\r\n{b}")
258 }),
259 (
260 HeaderTechnique::UnderscoreSubstitution,
261 format!("{}: {}", underscore_substitute(header_name), value),
262 ),
263 (
264 HeaderTechnique::NullByteInjection,
265 format!("{}: {}", null_byte_inject(header_name), value),
266 ),
267 (
268 HeaderTechnique::TrailingSpace,
269 trailing_space(header_name, value),
270 ),
271 (
272 HeaderTechnique::MultiLineFolding,
273 multi_line_fold(header_name, value),
274 ),
275 (
276 HeaderTechnique::LfOnlyMultiLineFolding,
277 lf_only_multi_line_fold(header_name, value),
278 ),
279 (
280 HeaderTechnique::CommaJoin,
281 comma_join(header_name, value, benign),
282 ),
283 ]
284}
285
286#[cfg(test)]
287#[path = "header_tests.rs"]
288mod tests;