rust_web_server/waf/
decode.rs1pub const MAX_VALUE: usize = 16 * 1024;
6
7pub fn normalize(input: &str) -> String {
13 let mut s: String = input.chars().take(MAX_VALUE).collect();
14 for _ in 0..2 {
15 let next = percent_decode(&s);
16 if next == s {
17 break;
18 }
19 s = next;
20 }
21 s = html_entities(&s);
22 s = js_escapes(&s);
23 s = strip_sql_comments(&s);
24 let mut out = String::with_capacity(s.len());
25 let mut space = false;
26 for c in s.chars() {
27 let c = if c.is_whitespace() || c == '\u{0b}' || c == '\u{0c}' { ' ' } else { c };
28 if c.is_control() {
29 continue;
30 }
31 if c == ' ' {
32 if !space {
33 out.push(' ');
34 }
35 space = true;
36 } else {
37 out.extend(c.to_lowercase());
38 space = false;
39 }
40 }
41 out
42}
43
44fn hex(b: u8) -> Option<u32> {
45 (b as char).to_digit(16)
46}
47
48pub fn percent_decode(s: &str) -> String {
50 let b = s.as_bytes();
51 let mut out: Vec<u8> = Vec::with_capacity(b.len());
52 let mut i = 0;
53 while i < b.len() {
54 if b[i] == b'%' {
55 if matches!(b.get(i + 1), Some(b'u' | b'U')) {
56 if let (Some(a), Some(c), Some(d), Some(e)) = (b.get(i + 2).and_then(|x| hex(*x)), b.get(i + 3).and_then(|x| hex(*x)), b.get(i + 4).and_then(|x| hex(*x)), b.get(i + 5).and_then(|x| hex(*x))) {
57 if let Some(ch) = char::from_u32(a << 12 | c << 8 | d << 4 | e) {
58 let mut buf = [0u8; 4];
59 out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
60 i += 6;
61 continue;
62 }
63 }
64 }
65 if let (Some(h), Some(l)) = (b.get(i + 1).and_then(|x| hex(*x)), b.get(i + 2).and_then(|x| hex(*x))) {
66 out.push((h << 4 | l) as u8);
67 i += 3;
68 continue;
69 }
70 }
71 out.push(b[i]);
72 i += 1;
73 }
74 String::from_utf8_lossy(&out).into_owned()
75}
76
77pub fn html_entities(s: &str) -> String {
80 if !s.contains('&') {
81 return s.to_string();
82 }
83 const NAMED: [(&str, char); 14] = [
84 ("lt", '<'), ("gt", '>'), ("quot", '"'), ("apos", '\''), ("amp", '&'), ("colon", ':'), ("lpar", '('), ("rpar", ')'),
85 ("tab", '\t'), ("newline", '\n'), ("sol", '/'), ("bsol", '\\'), ("equals", '='), ("nbsp", ' '),
86 ];
87 let mut out = String::with_capacity(s.len());
88 let mut rest = s;
89 while let Some(at) = rest.find('&') {
90 out.push_str(&rest[..at]);
91 let after = &rest[at + 1..];
92 let mut consumed = None;
93 if let Some(num) = after.strip_prefix('#') {
94 let (digits, radix, skip) = match num.strip_prefix(['x', 'X']) {
95 Some(h) => (h.chars().take_while(|c| c.is_ascii_hexdigit()).take(8).collect::<String>(), 16, 2),
96 None => (num.chars().take_while(|c| c.is_ascii_digit()).take(8).collect::<String>(), 10, 1),
97 };
98 if !digits.is_empty() {
99 if let Some(ch) = u32::from_str_radix(&digits, radix).ok().and_then(char::from_u32) {
100 out.push(ch);
101 let mut len = skip + digits.len();
102 if after[len..].starts_with(';') {
103 len += 1;
104 }
105 consumed = Some(len);
106 }
107 }
108 } else {
109 let name: String = after.chars().take_while(|c| c.is_ascii_alphabetic()).take(10).collect();
110 if let Some((_, ch)) = NAMED.iter().find(|(n, _)| n.eq_ignore_ascii_case(&name)) {
111 out.push(*ch);
112 let mut len = name.len();
113 if after[len..].starts_with(';') {
114 len += 1;
115 }
116 consumed = Some(len);
117 }
118 }
119 match consumed {
120 Some(len) => rest = &after[len..],
121 None => {
122 out.push('&');
123 rest = after;
124 }
125 }
126 }
127 out.push_str(rest);
128 out
129}
130
131pub fn js_escapes(s: &str) -> String {
133 if !s.contains('\\') {
134 return s.to_string();
135 }
136 let chars: Vec<char> = s.chars().collect();
137 let mut out = String::with_capacity(s.len());
138 let mut i = 0;
139 while i < chars.len() {
140 if chars[i] == '\\' && i + 1 < chars.len() {
141 let take = match chars[i + 1] {
142 'x' | 'X' => 2,
143 'u' | 'U' => 4,
144 _ => 0,
145 };
146 if take > 0 && i + 2 + take <= chars.len() {
147 let digits: String = chars[i + 2..i + 2 + take].iter().collect();
148 if let Some(ch) = u32::from_str_radix(&digits, 16).ok().and_then(char::from_u32) {
149 out.push(ch);
150 i += 2 + take;
151 continue;
152 }
153 }
154 }
155 out.push(chars[i]);
156 i += 1;
157 }
158 out
159}
160
161pub fn strip_sql_comments(s: &str) -> String {
164 if !s.contains("/*") {
165 return s.to_string();
166 }
167 let mut out = String::with_capacity(s.len());
168 let mut rest = s;
169 while let Some(start) = rest.find("/*") {
170 out.push_str(&rest[..start]);
171 out.push(' ');
172 match rest[start + 2..].find("*/") {
173 Some(end) => rest = &rest[start + 2 + end + 2..],
174 None => {
175 rest = "";
176 }
177 }
178 }
179 out.push_str(rest);
180 out
181}