1use super::{
6 app_session::{AppAuthority, AppSession},
7 signaling::CallOperation,
8};
9use crate::protobuf::{self as p, Field, Value};
10use anyhow::{bail, ensure, Result};
11use std::collections::BTreeMap;
12
13const MAX_BODY: usize = 1024 * 1024;
14const MAX_HEADER: usize = 0x2800;
15
16pub struct AppFrameParameters<'a> {
19 pub sdk_version: u32,
20 pub cloud_device_id: &'a [u8],
21 pub host_client_version: u32,
22 pub app_config_version: u32,
23}
24
25pub struct TaskFrame {
27 pub command: u32,
28 pub integers: BTreeMap<u32, u64>,
29 pub strings: BTreeMap<u32, Vec<u8>>,
30 pub payload: Vec<u8>,
31}
32
33pub fn prepare_transfer(
37 session: &AppSession,
38 current: AppAuthority,
39 operation: CallOperation,
40 inner: &[u8],
41 parameters: &AppFrameParameters<'_>,
42) -> Result<Vec<u8>> {
43 session.validate_authority(current)?;
44 i32::try_from(parameters.sdk_version)?;
45 std::str::from_utf8(parameters.cloud_device_id)?;
46 ensure!(
47 !inner.is_empty() && inner.len() <= MAX_BODY,
48 "Invalid Cloud operation body"
49 );
50 ensure!(
51 !parameters.cloud_device_id.is_empty() && parameters.cloud_device_id.len() <= 128,
52 "Invalid Cloud device identity"
53 );
54 let base = p::message(&[
55 p::uint(1, session.app_uin()),
56 p::bytes(2, parameters.cloud_device_id),
57 p::uint(3, u64::from(parameters.sdk_version)),
58 ])?;
59 let cloud = p::message(&[p::bytes(1, &base), p::bytes(2, inner)])?;
60 let integers = BTreeMap::from([
61 (1, 1),
62 (2, session.app_uin()),
63 (3, 0),
64 (4, 0),
65 (5, 0xb1400),
66 (6, 15),
67 (7, 0),
68 (8, 0),
69 (9, 0),
70 (10, 1),
71 (11, 0),
72 (12, 0),
73 (13, 0),
74 (17, 0),
75 (18, 1),
76 (20, 1506),
77 (21, 0),
78 (22, session.app_uin()),
79 (23, 0),
80 (25, 0),
81 (26, 4),
82 (28, u64::from(parameters.app_config_version)),
83 (29, 1),
84 (30, u64::from(parameters.host_client_version)),
85 (31, 0),
86 ]);
87 let strings = BTreeMap::from([
88 (14, vec![]),
89 (24, b"wechat".to_vec()),
90 (27, session.server_id().to_vec()),
91 ]);
92 let frame = TaskFrame {
93 command: operation.command_id(),
94 integers,
95 strings,
96 payload: lz4_literal(&cloud)?,
97 }
98 .encode()?;
99 p::message(&[
100 p::uint(1, 1901),
101 p::uint(2, u64::from(operation.command_id())),
102 p::bytes(3, b"wechat"),
103 p::bytes(4, operation.path().as_bytes()),
104 p::bytes(5, &frame),
105 ])
106}
107
108pub fn decode_transfer_reply(
112 session: &AppSession,
113 current: AppAuthority,
114 operation: CallOperation,
115 delegate: &[u8],
116) -> Result<Vec<u8>> {
117 session.validate_authority(current)?;
118 let fields = decode(delegate)?;
119 ensure!(
120 signed(&fields, 1, Some(0))? == 0,
121 "Cloud delegate operation rejected"
122 );
123 ensure!(
124 p::optional_data(&fields, 3)?.is_none_or(|v| v.is_empty()),
125 "Unsupported Cloud delegate extension"
126 );
127 let task = TaskFrame::decode(required_data(&fields, 2)?, operation.command_id())?;
128 let integer = |key: u32| {
129 task.integers
130 .get(&key)
131 .copied()
132 .ok_or_else(|| anyhow::anyhow!("Missing app task header {key}"))
133 };
134 ensure!(
135 signed_value(integer(4)?)? == 0,
136 "Cloud task operation rejected"
137 );
138 ensure!(
139 signed_value(integer(23)?)? == 0,
140 "Cloud app operation rejected"
141 );
142 ensure!(
143 integer(7)? == 0 && integer(8)? == 0 && integer(21)? == 0 && integer(25)? == 0,
144 "Unsupported app task transform"
145 );
146 if let Some(uin) = task.integers.get(&22) {
147 ensure!(*uin == session.app_uin(), "Cloud app identity changed");
148 }
149 if let Some(app) = task.strings.get(&24) {
150 ensure!(
151 app.is_empty() || app == b"wechat",
152 "Cloud application changed"
153 );
154 }
155 if let Some(server) = task.strings.get(&27) {
156 ensure!(
157 server.is_empty() || server == session.server_id(),
158 "Cloud app session changed"
159 );
160 }
161 let clear = match integer(26)? {
162 0 => task.payload,
163 4 => lz4_decode(&task.payload, MAX_BODY)?,
164 _ => bail!("Unsupported app compression"),
165 };
166 let cloud = decode(&clear)?;
167 let base = decode(required_data(&cloud, 1)?)?;
168 ensure!(signed(&base, 1, None)? == 0, "Cloud operation rejected");
169 if let Some(Value::Varint(uin)) = p::optional(&cloud, 2)? {
170 ensure!(*uin == session.app_uin(), "Cloud response identity changed");
171 } else {
172 ensure!(
173 p::optional(&cloud, 2)?.is_none(),
174 "Invalid Cloud response identity"
175 );
176 }
177 Ok(required_data(&cloud, 3)?.to_vec())
178}
179
180impl TaskFrame {
181 pub fn encode(&self) -> Result<Vec<u8>> {
182 ensure!(
183 self.command != 0 && self.payload.len() <= MAX_BODY,
184 "Invalid app task command or body"
185 );
186 ensure!(
187 self.integers.len() <= 1024 && self.strings.len() <= 1024,
188 "Too many app task headers"
189 );
190 let mut header = vec![1];
191 for (key, value) in &self.integers {
192 ensure!(*key != 0, "Invalid app task header key");
193 header.extend(p::varint(u64::from(*key)));
194 header.extend(p::varint(*value));
195 }
196 header.push(0);
197 for (key, value) in &self.strings {
198 ensure!(
199 *key != 0 && value.len() <= 1024,
200 "Invalid app task byte header"
201 );
202 header.extend(p::varint(u64::from(*key)));
203 header.extend(p::varint(value.len() as u64));
204 header.extend(value);
205 }
206 header.push(0);
207 ensure!(header.len() <= MAX_HEADER, "App task header too large");
208 header.extend(p::varint(header.len() as u64));
209 let total = 16 + header.len() + self.payload.len();
210 ensure!(total <= MAX_BODY, "App task frame too large");
211 let mut out = Vec::with_capacity(total);
212 out.extend((total as u32).to_be_bytes());
213 out.extend([0x11, 0x10, 0x07, 0x6d]);
214 out.extend(self.command.to_be_bytes());
215 out.extend([0; 4]);
216 out.extend(header);
217 out.extend(&self.payload);
218 Ok(out)
219 }
220 pub fn decode(raw: &[u8], expected_command: u32) -> Result<Self> {
221 ensure!(
222 (16..=MAX_BODY).contains(&raw.len()),
223 "Invalid app task size"
224 );
225 ensure!(
226 u32::from_be_bytes(raw[..4].try_into()?) as usize == raw.len()
227 && raw[4..8] == [0x11, 0x10, 0x07, 0x6d]
228 && raw[12..16] == [0; 4],
229 "Invalid app task prefix"
230 );
231 let command = u32::from_be_bytes(raw[8..12].try_into()?);
232 ensure!(command == expected_command, "Unexpected app task command");
233 let header = &raw[16..];
234 let mut pos = 0;
235 ensure!(
236 read(header, &mut pos)? == 1,
237 "Unsupported app task header version"
238 );
239 let mut integers = BTreeMap::new();
240 let mut strings = BTreeMap::new();
241 let mut previous = 0;
242 loop {
243 let key = u32::try_from(read(header, &mut pos)?)?;
244 if key == 0 {
245 break;
246 }
247 ensure!(
248 key > previous && integers.len() < 1024,
249 "Invalid app integer header order"
250 );
251 previous = key;
252 integers.insert(key, read(header, &mut pos)?);
253 ensure!(pos <= MAX_HEADER, "App header too large");
254 }
255 previous = 0;
256 loop {
257 let key = u32::try_from(read(header, &mut pos)?)?;
258 if key == 0 {
259 break;
260 }
261 ensure!(
262 key > previous && strings.len() < 1024,
263 "Invalid app byte header order"
264 );
265 previous = key;
266 let len = usize::try_from(read(header, &mut pos)?)?;
267 ensure!(
268 len <= 1024 && len <= header.len() - pos,
269 "App byte header truncated"
270 );
271 strings.insert(key, header[pos..pos + len].to_vec());
272 pos += len;
273 ensure!(pos <= MAX_HEADER, "App header too large");
274 }
275 let footer = pos;
276 ensure!(
277 read(header, &mut pos)? == footer as u64 && footer <= MAX_HEADER,
278 "Invalid app header footer"
279 );
280 Ok(Self {
281 command,
282 integers,
283 strings,
284 payload: header[pos..].to_vec(),
285 })
286 }
287}
288
289fn read(raw: &[u8], pos: &mut usize) -> Result<u64> {
290 let start = *pos;
291 let value = p::read_varint(raw, pos)?;
292 ensure!(
293 raw[start..*pos] == p::varint(value),
294 "Noncanonical app header integer"
295 );
296 Ok(value)
297}
298fn decode(raw: &[u8]) -> Result<Vec<Field>> {
299 ensure!(raw.len() <= MAX_BODY, "App message too large");
300 super::signaling::decode_fields(raw)
301}
302fn required_data(fields: &[Field], number: u32) -> Result<&[u8]> {
303 p::optional_data(fields, number)?
304 .ok_or_else(|| anyhow::anyhow!("Missing app bytes field {number}"))
305}
306fn signed_value(value: u64) -> Result<i32> {
307 Ok(i32::try_from(value as i64)?)
308}
309fn signed(fields: &[Field], number: u32, default: Option<i32>) -> Result<i32> {
310 match p::optional(fields, number)? {
311 Some(Value::Varint(value)) => signed_value(*value),
312 None => default.ok_or_else(|| anyhow::anyhow!("Missing app result {number}")),
313 _ => bail!("Invalid app result {number}"),
314 }
315}
316
317pub fn lz4_literal(raw: &[u8]) -> Result<Vec<u8>> {
319 ensure!(raw.len() <= MAX_BODY, "App compression input too large");
320 let mut out = vec![(raw.len().min(15) as u8) << 4];
321 if raw.len() >= 15 {
322 let mut remaining = raw.len() - 15;
323 while remaining >= 255 {
324 out.push(255);
325 remaining -= 255;
326 }
327 out.push(remaining as u8);
328 }
329 out.extend(raw);
330 Ok(out)
331}
332pub fn lz4_decode(raw: &[u8], limit: usize) -> Result<Vec<u8>> {
333 ensure!(
334 raw.len() <= MAX_BODY && limit <= MAX_BODY && !raw.is_empty(),
335 "Invalid app LZ4 size"
336 );
337 let mut pos = 0;
338 let mut out = Vec::new();
339 while pos < raw.len() {
340 let token = raw[pos];
341 pos += 1;
342 let literals = length(raw, &mut pos, usize::from(token >> 4), limit)?;
343 ensure!(
344 literals <= raw.len() - pos && literals <= limit - out.len(),
345 "App LZ4 literal exceeds bounds"
346 );
347 out.extend(&raw[pos..pos + literals]);
348 pos += literals;
349 if pos == raw.len() {
350 return Ok(out);
351 }
352 ensure!(raw.len() - pos >= 2, "App LZ4 offset truncated");
353 let offset = usize::from(u16::from_le_bytes(raw[pos..pos + 2].try_into()?));
354 pos += 2;
355 ensure!(
356 offset > 0 && offset <= out.len(),
357 "Invalid app LZ4 backreference"
358 );
359 let repeated = length(raw, &mut pos, usize::from(token & 15), limit)?
360 .checked_add(4)
361 .ok_or_else(|| anyhow::anyhow!("App LZ4 length overflow"))?;
362 ensure!(
363 repeated <= limit - out.len(),
364 "App LZ4 output exceeds bound"
365 );
366 for _ in 0..repeated {
367 out.push(out[out.len() - offset]);
368 }
369 }
370 bail!("App LZ4 final literal missing")
371}
372fn length(raw: &[u8], pos: &mut usize, initial: usize, limit: usize) -> Result<usize> {
373 let mut value = initial;
374 if initial == 15 {
375 loop {
376 let next = *raw
377 .get(*pos)
378 .ok_or_else(|| anyhow::anyhow!("App LZ4 length truncated"))?;
379 *pos += 1;
380 value = value
381 .checked_add(usize::from(next))
382 .ok_or_else(|| anyhow::anyhow!("App LZ4 length overflow"))?;
383 ensure!(value <= limit, "App LZ4 length exceeds bound");
384 if next != 255 {
385 break;
386 }
387 }
388 }
389 Ok(value)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 #[test]
396 fn literal_and_overlapping_lz4_vectors() {
397 assert_eq!(lz4_decode(&[0x30, b'a', b'b', b'c'], 3).unwrap(), b"abc");
398 assert_eq!(lz4_decode(&[0x10, b'a', 1, 0, 0], 5).unwrap(), b"aaaaa");
399 for n in [0, 1, 15, 270, 4096] {
400 let v = vec![42; n];
401 assert_eq!(lz4_decode(&lz4_literal(&v).unwrap(), n).unwrap(), v);
402 }
403 assert!(lz4_decode(&[0x10, b'a', 0, 0], 20).is_err());
404 assert!(lz4_decode(&[0xf0, 255], 16).is_err());
405 }
406 #[test]
407 fn task_frame_full_width_and_corruption() {
408 let task = TaskFrame {
409 command: 3361,
410 integers: BTreeMap::from([(2, u64::MAX), (22, u64::MAX)]),
411 strings: BTreeMap::from([(24, b"wechat".to_vec())]),
412 payload: vec![1, 2, 3],
413 };
414 let raw = task.encode().unwrap();
415 let decoded = TaskFrame::decode(&raw, 3361).unwrap();
416 assert_eq!(decoded.integers[&2], u64::MAX);
417 assert_eq!(decoded.payload, vec![1, 2, 3]);
418 assert!(TaskFrame::decode(&raw, 3380).is_err());
419 let mut corrupt = raw;
420 corrupt[0] = 1;
421 assert!(TaskFrame::decode(&corrupt, 3361).is_err());
422 let mut noncanonical = vec![0, 0, 0, 21, 0x11, 0x10, 7, 0x6d, 0, 0, 13, 33, 0, 0, 0, 0];
423 noncanonical.extend([0x81, 0, 0, 0, 4]);
424 assert!(TaskFrame::decode(&noncanonical, 3361).is_err());
425 }
426}