pagesift_frontier/frontier/
clean.rs1use super::*;
4
5const TRACKERS: &[&str] = &[
7 "fbclid",
8 "gclid",
9 "gclsrc",
10 "dclid",
11 "gbraid",
12 "wbraid",
13 "msclkid",
14 "yclid",
15 "ysclid",
16 "twclid",
17 "ttclid",
18 "li_fat_id",
19 "igshid",
20 "igsh",
21 "mc_cid",
22 "mc_eid",
23 "_ga",
24 "_gl",
25 "_hsenc",
26 "_hsmi",
27 "__hstc",
28 "__hssc",
29 "__hsfp",
30 "hsctatracking",
31 "mkt_tok",
32 "oly_anon_id",
33 "oly_enc_id",
34 "vero_id",
35 "vero_conv",
36 "rb_clickid",
37 "s_cid",
38 "_openstat",
39 "srsltid",
40 "wt_mc",
41 "pk_campaign",
42 "pk_kwd",
43 "pk_source",
44 "pk_medium",
45 "pk_content",
46 "spm",
47 "scm",
48];
49const TRACKER_PREFIXES: &[&str] = &["utm_", "mtm_", "itm_", "internal_", "elq"];
52const SESSIONS: &[&str] = &[
54 "jsessionid",
55 "phpsessid",
56 "sessionid",
57 "session_id",
58 "cfid",
59 "cftoken",
60 "zenid",
61 "oscsid",
62];
63
64const BINARY_EXT: &[&str] = &[
66 "jpg", "jpeg", "png", "gif", "webp", "avif", "bmp", "ico", "svg", "tif", "tiff", "heic", "psd",
67 "mp3", "mp4", "m4a", "m4v", "mov", "avi", "wmv", "flv", "mkv", "webm", "wav", "ogg", "oga",
68 "ogv", "flac", "aac", "mid", "midi", "zip", "gz", "tgz", "bz2", "xz", "7z", "rar", "tar",
69 "zst", "iso", "dmg", "exe", "msi", "apk", "ipa", "deb", "rpm", "jar", "woff", "woff2", "ttf",
70 "otf", "eot", "css", "js", "mjs", "map", "wasm", "swf",
71];
72const MAX_SEGMENTS: usize = 20;
77const MAX_URL_LEN: usize = 2048;
80const MAX_REPEATS: usize = 2;
83
84fn is_session(name: &str, value: &str) -> bool {
85 if SESSIONS.contains(&name) || name.starts_with("aspsessionid") {
86 return true;
87 }
88 name == "sid" && value.len() >= 16 && value.bytes().all(|b| b.is_ascii_alphanumeric())
89}
90
91fn is_tracker(name: &str) -> bool {
92 TRACKERS.contains(&name) || TRACKER_PREFIXES.iter().any(|p| name.starts_with(p))
93}
94
95pub fn clean(u: &mut Url) {
98 u.set_fragment(None);
99 let path = u.path();
100 let lower = path.to_ascii_lowercase();
101 if lower.contains(";jsessionid=") || lower.contains(";phpsessid=") || path.contains("//") {
102 let raw: Vec<&str> = path.split('/').skip(1).collect();
103 let mut trailing = path.ends_with('/');
104 let mut parts: Vec<&str> = Vec::with_capacity(raw.len());
105 for (i, seg) in raw.iter().enumerate() {
106 let low = seg.to_ascii_lowercase();
107 let seg = match low.find(";jsessionid=").or_else(|| low.find(";phpsessid=")) {
108 Some(c) => &seg[..c],
109 None => seg,
110 };
111 if seg.is_empty() {
112 trailing |= i + 1 == raw.len();
114 continue;
115 }
116 parts.push(seg);
117 }
118 let mut out = format!("/{}", parts.join("/"));
119 if trailing && !parts.is_empty() {
120 out.push('/');
121 }
122 u.set_path(&out);
123 }
124 if u.query().is_some() {
125 let pairs: Vec<(String, String)> = u
126 .query_pairs()
127 .map(|(k, v)| (k.into_owned(), v.into_owned()))
128 .collect();
129 let kept: Vec<&(String, String)> = pairs
130 .iter()
131 .filter(|(k, v)| {
132 let k = k.to_ascii_lowercase();
133 !is_tracker(&k) && !is_session(&k, v)
134 })
135 .collect();
136 if kept.len() != pairs.len() {
137 if kept.is_empty() {
138 u.set_query(None);
139 } else {
140 u.query_pairs_mut()
141 .clear()
142 .extend_pairs(kept.iter().map(|(k, v)| (k, v)));
143 }
144 }
145 }
146}
147
148pub fn skip(u: &Url) -> bool {
150 if u.as_str().len() > MAX_URL_LEN {
151 return true;
152 }
153 let path = u.path();
154 let last = path.rsplit('/').next().unwrap_or_default();
155 if let Some((_, ext)) = last.rsplit_once('.')
156 && ext.len() <= 5
157 && BINARY_EXT.contains(&ext.to_ascii_lowercase().as_str())
158 {
159 return true;
160 }
161 let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
162 segs.len() > MAX_SEGMENTS || repeats(&segs)
163}
164
165fn repeats(segs: &[&str]) -> bool {
167 for len in 1..=3 {
168 let need = len * (MAX_REPEATS + 1);
169 if segs.len() < need {
170 break;
171 }
172 for start in 0..=segs.len() - need {
173 let unit = &segs[start..start + len];
174 if (1..=MAX_REPEATS).all(|r| &segs[start + r * len..start + (r + 1) * len] == unit) {
175 return true;
176 }
177 }
178 }
179 false
180}
181
182pub fn shape(u: &Url) -> String {
184 let mut s = String::with_capacity(u.as_str().len());
185 s.push_str(u.host_str().unwrap_or_default());
186 for seg in u.path().split('/') {
187 s.push('/');
188 if seg.len() >= 16 && seg.bytes().any(|b| b.is_ascii_digit()) {
189 s.push('*');
190 continue;
191 }
192 let mut in_digits = false;
193 for c in seg.chars() {
194 if c.is_ascii_digit() {
195 if !in_digits {
196 s.push('#');
197 }
198 in_digits = true;
199 } else {
200 in_digits = false;
201 s.push(c);
202 }
203 }
204 }
205 if u.query().is_some() {
206 let mut keys: Vec<String> = u.query_pairs().map(|(k, _)| k.into_owned()).collect();
207 keys.sort();
208 keys.dedup();
209 s.push('?');
210 s.push_str(&keys.join("&"));
211 }
212 s
213}
214
215pub fn shape_key(u: &Url) -> u64 {
217 fnv(shape(u).bytes())
218}