Skip to main content

wechat_protocol/calls/
signaling.rs

1//! Experimental offline call-signaling codecs.
2//!
3//! These types do not open connections, obtain credentials or negotiate media.
4//! Encoding a packet does not establish permission or readiness to submit it.
5//! Optional fields preserve absence versus explicit zero, false or empty values;
6//! unknown fields survive. Local limits are memory budgets, not server limits.
7
8use crate::protobuf::{self as p, Field, Value};
9use anyhow::{bail, ensure, Result};
10
11/// Local budget for a complete encoded signaling message, including its body.
12pub const MAX_SIGNALING_BYTES: usize = 1024 * 1024;
13const MAX_FIELDS: usize = 1024;
14
15/// Fixed command/path pairs; these do not select audio versus video.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum CallOperation {
18    Create,
19    Join,
20    Acknowledge,
21    Reject,
22    Cancel,
23    Exit,
24}
25
26impl CallOperation {
27    pub const fn command_id(self) -> u32 {
28        match self {
29            Self::Create => 3066,
30            Self::Join => 3361,
31            Self::Acknowledge => 3007,
32            Self::Reject => 3095,
33            Self::Cancel => 3310,
34            Self::Exit => 3380,
35        }
36    }
37
38    pub const fn path(self) -> &'static str {
39        match self {
40            Self::Create => "/cloudim/cloud-bin/cloudvoipmtcreate",
41            Self::Join => "/cloudim/cloud-bin/cloudvoipmtjoin",
42            Self::Acknowledge => "/cloudim/cloud-bin/cloudvoipmtack",
43            Self::Reject => "/cloudim/cloud-bin/cloudvoipmtreject",
44            Self::Cancel => "/cloudim/cloud-bin/cloudvoipmtcancel",
45            Self::Exit => "/cloudim/cloud-bin/cloudvoipmtexit",
46        }
47    }
48
49    pub fn from_parts(command_id: u32, path: &str) -> Result<Self> {
50        for operation in [
51            Self::Create,
52            Self::Join,
53            Self::Acknowledge,
54            Self::Reject,
55            Self::Cancel,
56            Self::Exit,
57        ] {
58            if command_id == operation.command_id() && path == operation.path() {
59                return Ok(operation);
60            }
61        }
62        bail!("Unsupported call command/path pair")
63    }
64}
65
66/// Conference-to-host callback envelope. Bodies can contain private data, so
67/// this and the network envelopes intentionally do not implement `Debug`.
68///
69/// The host's [`CommonRequest`] contains its standard base request and the
70/// callback's inner body, not this entire callback envelope.
71#[derive(Default)]
72pub struct CloudRequest {
73    pub task_id: Option<u32>,
74    pub body: Option<Vec<u8>>,
75    pub command_id: Option<u32>,
76    pub path: Option<String>,
77    pub longpolling: Option<bool>,
78    /// Retained value; this codec does not assume its unit.
79    pub longpolling_timeout: Option<u32>,
80    pub unknown_fields: Vec<Field>,
81}
82
83impl CloudRequest {
84    pub fn for_operation(task_id: u32, operation: CallOperation, body: Vec<u8>) -> Self {
85        Self {
86            task_id: Some(task_id),
87            body: Some(body),
88            command_id: Some(operation.command_id()),
89            path: Some(operation.path().to_owned()),
90            ..Self::default()
91        }
92    }
93
94    /// A partial envelope can decode but must not be dispatched without its
95    /// complete, supported operation identity.
96    pub fn operation(&self) -> Result<CallOperation> {
97        CallOperation::from_parts(
98            self.command_id
99                .ok_or_else(|| anyhow::anyhow!("Missing call command"))?,
100            self.path
101                .as_deref()
102                .ok_or_else(|| anyhow::anyhow!("Missing call path"))?,
103        )
104    }
105
106    pub fn encode(&self) -> Result<Vec<u8>> {
107        self.validate()?;
108        let mut fields = Vec::new();
109        append_u32(&mut fields, 1, self.task_id);
110        append_data(&mut fields, 2, self.body.as_deref());
111        append_u32(&mut fields, 3, self.command_id);
112        append_data(&mut fields, 4, self.path.as_deref().map(str::as_bytes));
113        append_u32(&mut fields, 5, self.longpolling.map(u32::from));
114        append_u32(&mut fields, 6, self.longpolling_timeout);
115        finish(fields, &self.unknown_fields, 6)
116    }
117
118    pub fn decode(input: &[u8]) -> Result<Self> {
119        let fields = decode_fields(input)?;
120        let value = Self {
121            task_id: optional_u32(&fields, 1)?,
122            body: p::optional_data(&fields, 2)?.map(Vec::from),
123            command_id: optional_u32(&fields, 3)?,
124            path: p::optional_data(&fields, 4)?
125                .map(|value| String::from_utf8(value.to_vec()))
126                .transpose()?,
127            longpolling: match optional_u32(&fields, 5)? {
128                None => None,
129                Some(0) => Some(false),
130                Some(1) => Some(true),
131                _ => bail!("Invalid call boolean"),
132            },
133            longpolling_timeout: optional_u32(&fields, 6)?,
134            unknown_fields: unknown_fields(fields, 6),
135        };
136        value.validate()?;
137        Ok(value)
138    }
139
140    fn validate(&self) -> Result<()> {
141        if let Some(body) = &self.body {
142            ensure!(body.len() <= MAX_SIGNALING_BYTES, "Call body too large");
143        }
144        if let Some(path) = &self.path {
145            ensure!(
146                path.len() <= 128 && !path.contains('\0'),
147                "Invalid call path"
148            );
149        }
150        if let (Some(command), Some(path)) = (self.command_id, self.path.as_deref()) {
151            CallOperation::from_parts(command, path)?;
152        }
153        Ok(())
154    }
155}
156
157/// Standard base request plus an opaque inner operation body. The existing
158/// [`crate::wire::base_request`] prepares the standard base message. Validation
159/// checks wire kinds and widths, not credential or account suitability.
160#[derive(Default)]
161pub struct CommonRequest {
162    pub base_request: Option<Vec<u8>>,
163    pub body: Option<Vec<u8>>,
164    pub unknown_fields: Vec<Field>,
165}
166
167impl CommonRequest {
168    pub fn encode(&self) -> Result<Vec<u8>> {
169        if let Some(base) = &self.base_request {
170            validate_base_request(base)?;
171        }
172        encode_common(
173            self.base_request.as_deref(),
174            self.body.as_deref(),
175            &self.unknown_fields,
176        )
177    }
178
179    pub fn decode(input: &[u8]) -> Result<Self> {
180        let (base_request, body, unknown_fields) = decode_common(input)?;
181        if let Some(base) = &base_request {
182            validate_base_request(base)?;
183        }
184        Ok(Self {
185            base_request,
186            body,
187            unknown_fields,
188        })
189    }
190}
191
192/// Standard base response plus an opaque inner response. Decoding does not
193/// establish success: inspect the return code before admitting an inner reply.
194#[derive(Default)]
195pub struct CommonResponse {
196    pub base_response: Option<Vec<u8>>,
197    pub body: Option<Vec<u8>>,
198    pub unknown_fields: Vec<Field>,
199}
200
201impl CommonResponse {
202    pub fn encode(&self) -> Result<Vec<u8>> {
203        if let Some(base) = &self.base_response {
204            validate_base_response(base)?;
205        }
206        encode_common(
207            self.base_response.as_deref(),
208            self.body.as_deref(),
209            &self.unknown_fields,
210        )
211    }
212
213    pub fn decode(input: &[u8]) -> Result<Self> {
214        let (base_response, body, unknown_fields) = decode_common(input)?;
215        if let Some(base) = &base_response {
216            validate_base_response(base)?;
217        }
218        Ok(Self {
219            base_response,
220            body,
221            unknown_fields,
222        })
223    }
224
225    /// Preserve negative failures and reject an absent return code.
226    pub fn return_code(&self) -> Result<i32> {
227        let base = self
228            .base_response
229            .as_deref()
230            .ok_or_else(|| anyhow::anyhow!("Missing call base response"))?;
231        optional_i32(&decode_fields(base)?, 1)?
232            .ok_or_else(|| anyhow::anyhow!("Missing call return code"))
233    }
234}
235
236fn validate_base_request(input: &[u8]) -> Result<()> {
237    let fields = decode_fields(input)?;
238    p::optional_data(&fields, 1)?;
239    optional_u32(&fields, 2)?;
240    p::optional_data(&fields, 3)?;
241    optional_i32(&fields, 4)?;
242    p::optional_data(&fields, 5)?;
243    optional_u32(&fields, 6)?;
244    Ok(())
245}
246
247fn validate_base_response(input: &[u8]) -> Result<()> {
248    let fields = decode_fields(input)?;
249    optional_i32(&fields, 1)?;
250    if let Some(message) = p::optional_data(&fields, 2)? {
251        let message = decode_fields(message)?;
252        if let Some(text) = p::optional_data(&message, 1)? {
253            std::str::from_utf8(text)?;
254        }
255    }
256    Ok(())
257}
258
259pub(super) fn decode_fields(input: &[u8]) -> Result<Vec<Field>> {
260    ensure!(
261        input.len() <= MAX_SIGNALING_BYTES,
262        "Call envelope too large"
263    );
264    let fields = p::decode(input)?;
265    ensure!(fields.len() <= MAX_FIELDS, "Too many call fields");
266    Ok(fields)
267}
268
269pub(super) fn optional_u32(fields: &[Field], number: u32) -> Result<Option<u32>> {
270    match p::optional(fields, number)? {
271        None => Ok(None),
272        Some(Value::Varint(value)) => Ok(Some(u32::try_from(*value)?)),
273        _ => bail!("Invalid call scalar field {number}"),
274    }
275}
276
277fn optional_i32(fields: &[Field], number: u32) -> Result<Option<i32>> {
278    match p::optional(fields, number)? {
279        None => Ok(None),
280        Some(Value::Varint(value)) => Ok(Some(i32::try_from(*value as i64)?)),
281        _ => bail!("Invalid call signed scalar field {number}"),
282    }
283}
284
285pub(super) fn unknown_fields(fields: Vec<Field>, last_known: u32) -> Vec<Field> {
286    fields
287        .into_iter()
288        .filter(|field| field.number > last_known)
289        .collect()
290}
291
292fn append_u32(output: &mut Vec<Vec<u8>>, number: u32, value: Option<u32>) {
293    if let Some(value) = value {
294        output.push(p::uint(number, u64::from(value)));
295    }
296}
297
298fn append_data(output: &mut Vec<Vec<u8>>, number: u32, value: Option<&[u8]>) {
299    if let Some(value) = value {
300        output.push(p::bytes(number, value));
301    }
302}
303
304fn finish(mut fields: Vec<Vec<u8>>, unknown: &[Field], last_known: u32) -> Result<Vec<u8>> {
305    ensure!(
306        fields.len() + unknown.len() <= MAX_FIELDS,
307        "Too many call fields"
308    );
309    let mut total = fields.iter().try_fold(0usize, |total, field| {
310        total
311            .checked_add(field.len())
312            .ok_or_else(|| anyhow::anyhow!("Call length overflow"))
313    })?;
314    ensure!(total <= MAX_SIGNALING_BYTES, "Call envelope too large");
315    for field in unknown {
316        ensure!(
317            field.number > last_known && field.number < 1 << 29,
318            "Unknown call field collides with a known or invalid field"
319        );
320        let wire = match &field.value {
321            Value::Varint(_) => 0,
322            Value::Fixed64(_) => 1,
323            Value::Bytes(_) => 2,
324            Value::Fixed32(_) => 5,
325        };
326        let mut encoded = p::varint(u64::from(field.number) * 8 + wire);
327        let value_len = match &field.value {
328            Value::Varint(value) => p::varint(*value).len(),
329            Value::Fixed64(_) => 8,
330            Value::Fixed32(_) => 4,
331            Value::Bytes(value) => {
332                ensure!(value.len() <= MAX_SIGNALING_BYTES, "Call field too large");
333                p::varint(value.len() as u64).len() + value.len()
334            }
335        };
336        total = total
337            .checked_add(encoded.len() + value_len)
338            .ok_or_else(|| anyhow::anyhow!("Call length overflow"))?;
339        ensure!(total <= MAX_SIGNALING_BYTES, "Call envelope too large");
340        match &field.value {
341            Value::Varint(value) => encoded.extend(p::varint(*value)),
342            Value::Fixed64(value) => encoded.extend(value),
343            Value::Fixed32(value) => encoded.extend(value),
344            Value::Bytes(value) => {
345                encoded.extend(p::varint(value.len() as u64));
346                encoded.extend(value);
347            }
348        }
349        fields.push(encoded);
350    }
351    p::message(&fields)
352}
353
354fn encode_common(base: Option<&[u8]>, body: Option<&[u8]>, unknown: &[Field]) -> Result<Vec<u8>> {
355    if let Some(body) = body {
356        ensure!(body.len() <= MAX_SIGNALING_BYTES, "Call body too large");
357    }
358    let mut fields = Vec::new();
359    append_data(&mut fields, 1, base);
360    append_data(&mut fields, 2, body);
361    finish(fields, unknown, 2)
362}
363
364type CommonParts = (Option<Vec<u8>>, Option<Vec<u8>>, Vec<Field>);
365fn decode_common(input: &[u8]) -> Result<CommonParts> {
366    let fields = decode_fields(input)?;
367    let base = p::optional_data(&fields, 1)?.map(Vec::from);
368    let body = p::optional_data(&fields, 2)?.map(Vec::from);
369    Ok((base, body, unknown_fields(fields, 2)))
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn callback_has_independent_wire_vector() {
378        // f1=7,f2=[8,1],f3=3066,f4=create-path,f5=false,f6=60000.
379        let expected = hex::decode(concat!(
380            "08071202080118fa172224",
381            "2f636c6f7564696d2f636c6f75642d62696e2f636c6f7564766f69706d74637265617465",
382            "280030e0d403"
383        ))
384        .unwrap();
385        let request = CloudRequest {
386            longpolling: Some(false),
387            longpolling_timeout: Some(60_000),
388            ..CloudRequest::for_operation(7, CallOperation::Create, vec![8, 1])
389        };
390        assert_eq!(request.encode().unwrap(), expected);
391        let decoded = CloudRequest::decode(&expected).unwrap();
392        assert_eq!(decoded.task_id, Some(7));
393        assert_eq!(decoded.body.as_deref(), Some([8, 1].as_slice()));
394        assert_eq!(decoded.operation().unwrap(), CallOperation::Create);
395        assert_eq!(decoded.longpolling, Some(false));
396        assert_eq!(decoded.longpolling_timeout, Some(60_000));
397    }
398
399    #[test]
400    fn nested_bases_and_duplicate_bodies_are_checked() {
401        for input in [
402            vec![8, 0],
403            vec![10, 0, 10, 0],
404            vec![18, 0, 18, 0],
405            vec![10, 2, 8, 0],
406            vec![10, 2, 18, 0],
407        ] {
408            assert!(CommonRequest::decode(&input).is_err());
409        }
410        for input in [
411            vec![10, 2, 10, 0],
412            vec![10, 2, 16, 0],
413            vec![10, 4, 8, 0, 8, 0],
414            vec![10, 4, 18, 2, 8, 0],
415        ] {
416            assert!(CommonResponse::decode(&input).is_err());
417        }
418    }
419}