1use super::{decode_cdn_config, public_ipv4, CdnConfig, CdnLocation};
2use crate::{protobuf as p, AccountState};
3use anyhow::{bail, ensure, Result};
4use std::{collections::BTreeSet, net::Ipv4Addr};
5
6const MAX_BOOTSTRAP_BYTES: usize = 512 * 1024;
7const MAX_RULE_BYTES: usize = 256 * 1024;
8const MAX_CONFIGS: usize = 64;
9const MAX_RULES: usize = 512;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum LargeCdnRoute {
13 Upload,
14 Download,
15 VideoDownload,
16}
17
18pub fn decode_large_cdn_config(
21 input: &[u8],
22 account: &AccountState,
23 route: LargeCdnRoute,
24) -> Result<CdnConfig> {
25 ensure!(
26 input.len() <= MAX_BOOTSTRAP_BYTES,
27 "CDN bootstrap too large"
28 );
29 let normal = decode_cdn_config(input, account)?;
32 let response = p::decode(input)?;
33 let safe = p::decode(p::data(&response, 7)?)?;
34 let version = number(&safe, 1)?;
35 let uin = number(&safe, 2)?;
36 ensure!(
37 uin != 0 && version == normal.version,
38 "Invalid safe CDN identity"
39 );
40 let safe_auth = p::unbuffer(p::data(&safe, 8)?)?;
41 ensure!(
42 !safe_auth.is_empty() && safe_auth.len() <= 65536,
43 "Invalid safe CDN auth key"
44 );
45
46 let raw = p::unbuffer(p::data(&response, 6)?)?;
47 ensure!(raw.len() <= MAX_RULE_BYTES, "CDN routing table too large");
48 let table = p::decode(&raw)?;
49 ensure!(
50 number(&table, 1)? == version && number(&table, 2)? == uin,
51 "Safe CDN routing identity mismatch"
52 );
53 let location = CdnLocation {
54 province: optional_number(&table, 3)?,
55 city: optional_number(&table, 4)?,
56 isp: optional_number(&table, 5)?,
57 };
58
59 let mut selected: Option<(i32, u32, bool)> = None;
62 for raw_rule in repeated(&table, 8, MAX_RULES)? {
63 let rule = p::decode(raw_rule)?;
64 ensure!(
65 rule.iter().all(|f| (1..=6).contains(&f.number)),
66 "Unsupported CDN routing predicate"
67 );
68 let sequence = number(&rule, 1)? as i32;
69 let predicates = [
72 (number(&rule, 2)?, 1),
73 (number(&rule, 3)?, 1),
74 (
75 number(&rule, 4)?,
76 if route == LargeCdnRoute::VideoDownload {
77 4
78 } else {
79 7
80 },
81 ),
82 (
83 number(&rule, 5)?,
84 match route {
85 LargeCdnRoute::Upload => 1,
86 LargeCdnRoute::Download | LargeCdnRoute::VideoDownload => 2,
87 },
88 ),
89 ];
90 let proid = number(&rule, 6)?;
91 ensure!(proid != 0, "Invalid CDN routing target");
92 if !predicates
93 .iter()
94 .all(|(predicate, actual)| *predicate == 0 || predicate == actual)
95 {
96 continue;
97 }
98 match selected {
99 Some((priority, previous, _)) if priority == sequence => {
100 selected = Some((priority, previous, true));
101 }
102 Some((priority, _, _)) if priority < sequence => {}
103 _ => selected = Some((sequence, proid, false)),
104 }
105 }
106 let (_, proid, ambiguous) =
107 selected.ok_or_else(|| anyhow::anyhow!("No large-file CDN route"))?;
108 ensure!(!ambiguous, "Ambiguous CDN routing priority");
109
110 let mut config_ids = BTreeSet::new();
111 let mut selected_config = None;
112 for raw_config in repeated(&table, 7, MAX_CONFIGS)? {
113 let config = p::decode(raw_config)?;
114 let id = number(&config, 1)?;
115 ensure!(id != 0 && config_ids.insert(id), "Ambiguous CDN config ID");
116 if id != proid {
117 continue;
118 }
119 let auth_key = p::data(&config, 8)?.to_vec();
120 let front_id = number(&config, 2)?;
121 ensure!(front_id != 0, "Missing CDN front ID");
122 let mut server_ips = Vec::new();
123 for field in config.iter().filter(|f| f.number == 3) {
124 ensure!(server_ips.len() < 32, "Too many CDN endpoints");
125 let p::Value::Varint(value) = field.value else {
126 bail!("Invalid CDN endpoint")
127 };
128 let ip = Ipv4Addr::from(wire_u32(value)?.to_le_bytes());
129 ensure!(public_ipv4(ip), "CDN endpoint is not public IPv4");
130 ensure!(!server_ips.contains(&ip), "Duplicate CDN endpoint");
131 server_ips.push(ip);
132 }
133 let mut has_port_443 = false;
135 for (index, port) in config.iter().filter(|f| f.number == 7).enumerate() {
136 ensure!(index < 16, "Too many CDN ports");
137 let p::Value::Varint(value) = port.value else {
138 bail!("Invalid CDN port")
139 };
140 ensure!((1..=65535).contains(&value), "Invalid CDN port");
141 has_port_443 |= value == 443;
142 }
143 ensure!(has_port_443, "CDN route does not advertise port 443");
144 let result = CdnConfig {
145 version,
146 uin: u64::from(uin),
147 client_version: account.device.client_version as u32,
148 client_os_type: account.device.device_type.clone(),
149 auth_key,
150 server_ips,
151 front_id,
152 location: Some(location),
153 };
154 result.validate()?;
155 selected_config = Some(result);
156 }
157 selected_config.ok_or_else(|| anyhow::anyhow!("CDN routing target is missing"))
158}
159
160fn number(fields: &[p::Field], field: u32) -> Result<u32> {
161 wire_u32(p::scalar(fields, field)?)
162}
163
164fn optional_number(fields: &[p::Field], field: u32) -> Result<u32> {
165 match p::optional(fields, field)? {
166 Some(p::Value::Varint(value)) => wire_u32(*value),
167 None => Ok(0),
168 _ => bail!("Invalid CDN location number"),
169 }
170}
171
172fn wire_u32(value: u64) -> Result<u32> {
173 let low = value as u32;
174 ensure!(
175 value == u64::from(low) || value == (low as i32 as i64 as u64),
176 "Invalid 32-bit CDN number"
177 );
178 Ok(low)
179}
180
181fn repeated(fields: &[p::Field], number: u32, max: usize) -> Result<Vec<&[u8]>> {
182 let mut values = Vec::new();
183 for field in fields.iter().filter(|f| f.number == number) {
184 ensure!(values.len() < max, "Too many CDN routing records");
185 let p::Value::Bytes(value) = &field.value else {
186 bail!("Invalid CDN routing record")
187 };
188 values.push(value.as_slice());
189 }
190 ensure!(!values.is_empty(), "Missing CDN routing records");
191 Ok(values)
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::{AuthSnapshot, DeviceProfile};
198
199 const ACCOUNT_UIN: u32 = 123;
200 const SAFE_UIN: u32 = 0xf1234567;
201
202 fn state() -> AccountState {
203 AccountState {
204 schema: 1,
205 device: DeviceProfile {
206 device_id: vec![1; 16],
207 device_type: "UnifiedPCLinux synthetic x86_64".into(),
208 client_version: -227271398,
209 },
210 uin: ACCOUNT_UIN,
211 own_username: "synthetic-user".into(),
212 cookie: vec![5],
213 session_key: vec![6; 16],
214 request_key: vec![7; 24],
215 response_key: vec![8; 24],
216 auto_auth_key: vec![],
217 key_info: vec![],
218 database_password: vec![],
219 auth_snapshot: AuthSnapshot::default(),
220 long_host: "long.weixin.qq.com".into(),
221 short_host: "short.weixin.qq.com".into(),
222 }
223 }
224
225 fn rule(sequence: u32, biz: u32, app: u32, file: u32, op: u32, proid: u32) -> Vec<u8> {
226 [sequence, biz, app, file, op, proid]
227 .into_iter()
228 .enumerate()
229 .flat_map(|(i, value)| p::uint(i as u32 + 1, u64::from(value)))
230 .collect()
231 }
232
233 fn config(id: u32, ip: [u8; 4], auth: &[u8]) -> Vec<u8> {
234 [
235 p::uint(1, u64::from(id)),
236 p::uint(2, 1000 + u64::from(id)),
237 p::int32(3, u32::from_le_bytes(ip) as i32),
238 p::uint(7, 443),
239 p::bytes(8, auth),
240 ]
241 .concat()
242 }
243
244 fn bootstrap(rules: &[Vec<u8>], configs: &[Vec<u8>]) -> Vec<u8> {
245 let group = |uin| {
246 [
247 p::uint(1, 1),
248 p::uint(2, u64::from(uin)),
249 p::bytes(6, &p::bytes(1, b"8.8.8.8")),
250 p::bytes(8, &p::buffer(&[7; 73])),
251 ]
252 .concat()
253 };
254 let table = [
255 p::uint(1, 1),
256 p::int32(2, SAFE_UIN as i32),
257 p::uint(3, 1234),
258 p::uint(4, 56),
259 p::uint(5, 78),
260 configs.iter().flat_map(|v| p::bytes(7, v)).collect(),
261 rules.iter().flat_map(|v| p::bytes(8, v)).collect(),
262 ]
263 .concat();
264 [
265 p::bytes(1, &p::uint(1, 0)),
266 p::bytes(2, &group(ACCOUNT_UIN)),
267 p::bytes(7, &group(SAFE_UIN)),
268 p::bytes(6, &p::buffer(&table)),
269 ]
270 .concat()
271 }
272
273 #[test]
274 fn safe_identity_signed_ips_and_sequence_join_are_preserved() {
275 let input = bootstrap(
276 &[
277 rule(12, 1, 0, 0, 0, 4),
278 rule(2, 11, 0, 7, 0, 4),
279 rule(3, 1, 0, 7, 1, 1),
280 ],
281 &[
282 config(4, [9, 9, 9, 9], &[4; 73]),
283 config(1, [8, 8, 8, 200], &[1; 73]),
284 ],
285 );
286 let selected = decode_large_cdn_config(&input, &state(), LargeCdnRoute::Upload).unwrap();
287 assert_eq!(selected.uin, u64::from(SAFE_UIN));
288 assert_ne!(selected.uin, u64::from(ACCOUNT_UIN));
289 assert_eq!(selected.front_id, 1001);
290 assert_eq!(selected.auth_key, vec![1; 73]);
291 assert_eq!(selected.server_ips, vec![Ipv4Addr::new(8, 8, 8, 200)]);
292 let location = selected.location.unwrap();
293 assert_eq!(location.province, 1234);
294 assert_eq!(location.city, 56);
295 assert_eq!(location.isp, 78);
296 assert!(decode_cdn_config(&input, &state())
297 .unwrap()
298 .location
299 .is_none());
300 let fallback = decode_large_cdn_config(&input, &state(), LargeCdnRoute::Download).unwrap();
301 assert_eq!(fallback.front_id, 1004);
302 }
303
304 #[test]
305 fn route_must_match_all_predicates_and_join_one_config() {
306 let configs = [config(1, [8, 8, 8, 8], &[1; 73])];
307 for candidate in [
308 rule(1, 2, 0, 7, 0, 1),
309 rule(1, 1, 2, 7, 0, 1),
310 rule(1, 1, 0, 5, 0, 1),
311 rule(1, 1, 0, 7, 2, 1),
312 rule(1, 1, 0, 7, 0, 9),
313 ] {
314 assert!(decode_large_cdn_config(
315 &bootstrap(&[candidate], &configs),
316 &state(),
317 LargeCdnRoute::Upload
318 )
319 .is_err());
320 }
321 let rules = [rule(1, 1, 0, 7, 0, 1), rule(1, 1, 0, 0, 0, 1)];
322 assert!(decode_large_cdn_config(
323 &bootstrap(&rules, &configs),
324 &state(),
325 LargeCdnRoute::Upload
326 )
327 .is_err());
328 let duplicated = [configs[0].clone(), configs[0].clone()];
329 assert!(decode_large_cdn_config(
330 &bootstrap(&[rules[0].clone()], &duplicated),
331 &state(),
332 LargeCdnRoute::Upload
333 )
334 .is_err());
335 }
336
337 #[test]
338 fn upload_and_download_select_their_own_operation_with_routing_type_seven() {
339 let input = bootstrap(
340 &[
341 rule(0, 1, 1, 5, 0, 3),
342 rule(1, 1, 1, 7, 1, 1),
343 rule(2, 1, 1, 7, 2, 2),
344 ],
345 &[
346 config(3, [1, 1, 1, 1], &[3; 73]),
347 config(2, [9, 9, 9, 9], &[2; 73]),
348 config(1, [8, 8, 8, 8], &[1; 73]),
349 ],
350 );
351 let upload = decode_large_cdn_config(&input, &state(), LargeCdnRoute::Upload).unwrap();
352 let download = decode_large_cdn_config(&input, &state(), LargeCdnRoute::Download).unwrap();
353 assert_eq!(upload.front_id, 1001);
354 assert_eq!(download.front_id, 1002);
355 assert_eq!(upload.auth_key, vec![1; 73]);
356 assert_eq!(download.auth_key, vec![2; 73]);
357 }
358
359 #[test]
360 fn video_download_selects_type_four_without_file_seven_fallback() {
361 let configs = [
362 config(1, [8, 8, 8, 8], &[1; 73]),
363 config(2, [9, 9, 9, 9], &[2; 73]),
364 ];
365 let input = bootstrap(&[rule(1, 1, 1, 7, 2, 1), rule(2, 1, 1, 4, 2, 2)], &configs);
366 let video =
367 decode_large_cdn_config(&input, &state(), LargeCdnRoute::VideoDownload).unwrap();
368 let file = decode_large_cdn_config(&input, &state(), LargeCdnRoute::Download).unwrap();
369 assert_eq!(video.front_id, 1002);
370 assert_eq!(video.auth_key, vec![2; 73]);
371 assert_eq!(file.front_id, 1001);
372 for wrong in [rule(1, 1, 1, 7, 2, 1), rule(1, 1, 1, 4, 1, 1)] {
373 assert!(decode_large_cdn_config(
374 &bootstrap(&[wrong], &configs),
375 &state(),
376 LargeCdnRoute::VideoDownload
377 )
378 .is_err());
379 }
380 }
381
382 #[test]
383 fn sequence_precedes_specificity_and_only_winning_priority_must_be_unique() {
384 let configs = [
385 config(1, [8, 8, 8, 8], &[1; 73]),
386 config(4, [9, 9, 9, 9], &[4; 73]),
387 ];
388 let rules = [rule(3, 1, 0, 7, 0, 1), rule(1, 1, 0, 0, 0, 4)];
389 assert_eq!(
390 decode_large_cdn_config(
391 &bootstrap(&rules, &configs),
392 &state(),
393 LargeCdnRoute::Upload
394 )
395 .unwrap()
396 .front_id,
397 1004
398 );
399 let rules = [
400 rule(12, 1, 0, 0, 0, 4),
401 rule(12, 1, 0, 7, 0, 4),
402 rule(3, 1, 0, 7, 0, 1),
403 ];
404 assert_eq!(
405 decode_large_cdn_config(
406 &bootstrap(&rules, &configs),
407 &state(),
408 LargeCdnRoute::Upload
409 )
410 .unwrap()
411 .front_id,
412 1001
413 );
414 }
415
416 #[test]
417 fn routing_counts_authentication_and_future_predicates_are_limited() {
418 let configs = [config(1, [8, 8, 8, 8], &[1; 73])];
419 let valid = rule(3, 1, 0, 7, 0, 1);
420 assert!(decode_large_cdn_config(
421 &bootstrap(&vec![valid.clone(); MAX_RULES + 1], &configs),
422 &state(),
423 LargeCdnRoute::Upload
424 )
425 .is_err());
426 assert!(decode_large_cdn_config(
427 &bootstrap(
428 std::slice::from_ref(&valid),
429 &vec![configs[0].clone(); MAX_CONFIGS + 1]
430 ),
431 &state(),
432 LargeCdnRoute::Upload
433 )
434 .is_err());
435 let future = [valid.clone(), p::uint(7, 1)].concat();
436 assert!(decode_large_cdn_config(
437 &bootstrap(&[future], &configs),
438 &state(),
439 LargeCdnRoute::Upload
440 )
441 .is_err());
442 for auth in [vec![], vec![1; 65537]] {
443 assert!(decode_large_cdn_config(
444 &bootstrap(
445 std::slice::from_ref(&valid),
446 &[config(1, [8, 8, 8, 8], &auth)]
447 ),
448 &state(),
449 LargeCdnRoute::Upload
450 )
451 .is_err());
452 }
453 let excessive = vec![0; MAX_BOOTSTRAP_BYTES + 1];
454 assert!(decode_large_cdn_config(&excessive, &state(), LargeCdnRoute::Upload).is_err());
455 }
456
457 #[test]
458 fn routing_identity_and_raw_authentication_are_checked_independently() {
459 let rules = [rule(3, 1, 0, 7, 0, 1)];
460 let config = config(1, [8, 8, 8, 8], &[1; 73]);
461 let response = bootstrap(&rules, &[config]);
462 let fields = p::decode(&response).unwrap();
463 let raw = p::unbuffer(p::data(&fields, 6).unwrap()).unwrap();
464 let table = p::decode(&raw).unwrap();
465 let altered_table = [
466 p::uint(1, 1),
467 p::uint(2, u64::from(SAFE_UIN - 1)),
468 p::bytes(7, p::data(&table, 7).unwrap()),
469 p::bytes(8, p::data(&table, 8).unwrap()),
470 ]
471 .concat();
472 let altered = [
473 p::bytes(1, p::data(&fields, 1).unwrap()),
474 p::bytes(2, p::data(&fields, 2).unwrap()),
475 p::bytes(7, p::data(&fields, 7).unwrap()),
476 p::bytes(6, &p::buffer(&altered_table)),
477 ]
478 .concat();
479 let error = decode_large_cdn_config(&altered, &state(), LargeCdnRoute::Upload)
480 .err()
481 .unwrap()
482 .to_string();
483 assert!(error.contains("routing identity mismatch"));
484 let selected = decode_large_cdn_config(&response, &state(), LargeCdnRoute::Upload).unwrap();
487 assert_eq!(selected.auth_key, vec![1; 73]);
488 }
489
490 #[test]
491 fn malformed_identity_wrappers_and_untrusted_endpoints_are_rejected() {
492 let rules = [rule(3, 1, 0, 7, 0, 1)];
493 let input = bootstrap(&rules, &[config(1, [8, 8, 8, 8], &[1; 73])]);
494 let mut other = state();
495 other.uin += 1;
496 assert!(decode_large_cdn_config(&input, &other, LargeCdnRoute::Upload).is_err());
497 for ip in [[127, 0, 0, 1], [10, 0, 0, 1], [192, 0, 2, 1]] {
498 assert!(decode_large_cdn_config(
499 &bootstrap(&rules, &[config(1, ip, &[1; 73])]),
500 &state(),
501 LargeCdnRoute::Upload
502 )
503 .is_err());
504 }
505 let fields = p::decode(&input).unwrap();
506 for replacement in [
507 p::bytes(1, &p::int32(1, -13)),
508 p::bytes(6, &[p::uint(1, 1), p::bytes(2, b"")].concat()),
509 p::bytes(7, &[p::uint(1, 1), p::uint(2, 99)].concat()),
510 ] {
511 let replacement_field = p::decode(&replacement).unwrap()[0].number;
512 let altered = fields
513 .iter()
514 .filter(|f| f.number != replacement_field)
515 .map(|f| {
516 p::bytes(
517 f.number,
518 match &f.value {
519 p::Value::Bytes(b) => b,
520 _ => unreachable!(),
521 },
522 )
523 })
524 .chain(std::iter::once(replacement))
525 .flatten()
526 .collect::<Vec<_>>();
527 assert!(decode_large_cdn_config(&altered, &state(), LargeCdnRoute::Upload).is_err());
528 }
529 assert!(wire_u32(1u64 << 32).is_err());
530 }
531
532 #[test]
533 fn location_defaults_match_protobuf_and_malformed_values_are_rejected() {
534 let rules = [rule(3, 1, 0, 7, 0, 1)];
535 let input = bootstrap(&rules, &[config(1, [8, 8, 8, 8], &[1; 73])]);
536 let response = p::decode(&input).unwrap();
537 let raw = p::unbuffer(p::data(&response, 6).unwrap()).unwrap();
538 let table = p::decode(&raw).unwrap();
539 let without_location = [
540 p::uint(1, 1),
541 p::int32(2, SAFE_UIN as i32),
542 p::bytes(7, p::data(&table, 7).unwrap()),
543 p::bytes(8, p::data(&table, 8).unwrap()),
544 ]
545 .concat();
546 let replace_table = |location: &[u8]| {
547 [
548 p::bytes(1, p::data(&response, 1).unwrap()),
549 p::bytes(2, p::data(&response, 2).unwrap()),
550 p::bytes(7, p::data(&response, 7).unwrap()),
551 p::bytes(
552 6,
553 &p::buffer(&[without_location.clone(), location.to_vec()].concat()),
554 ),
555 ]
556 .concat()
557 };
558 let selected =
559 decode_large_cdn_config(&replace_table(&[]), &state(), LargeCdnRoute::Upload).unwrap();
560 let location = selected.location.unwrap();
561 assert_eq!((location.province, location.city, location.isp), (0, 0, 0));
562 for malformed in [
563 p::uint(3, 1u64 << 32),
564 p::bytes(4, b"invalid"),
565 [p::uint(5, 1), p::uint(5, 2)].concat(),
566 ] {
567 assert!(decode_large_cdn_config(
568 &replace_table(&malformed),
569 &state(),
570 LargeCdnRoute::Upload
571 )
572 .is_err());
573 }
574 }
575}