Skip to main content

ferogram_connect/
envelope.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2//
3// ferogram: async Telegram MTProto client in Rust
4// https://github.com/ankit-chaubey/ferogram
5//
6// Licensed under either the MIT License or the Apache License 2.0.
7// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
8// https://github.com/ankit-chaubey/ferogram
9//
10// Feel free to use, modify, and share this code.
11// Please keep this notice when redistributing.
12
13use ferogram_tl_types as tl;
14use ferogram_tl_types::{Cursor, Deserializable};
15
16use crate::error::ConnectError;
17
18// Envelope constants
19const ID_RPC_RESULT: u32 = 0xf35c6d01;
20const ID_RPC_ERROR: u32 = 0x2144ca19;
21const ID_MSG_CONTAINER: u32 = 0x73f1f8dc;
22const ID_GZIP_PACKED: u32 = 0x3072cfa1;
23const ID_MSGS_ACK: u32 = 0x62d6b459;
24const ID_BAD_SERVER_SALT: u32 = 0xedab447b;
25const ID_NEW_SESSION: u32 = 0x9ec20908;
26const ID_BAD_MSG_NOTIFY: u32 = 0xa7eff811;
27const ID_UPDATES: u32 = 0x74ae4240;
28const ID_UPDATE_SHORT: u32 = 0x78d4dec1;
29const ID_UPDATES_COMBINED: u32 = 0x725b04c3;
30const ID_UPDATE_SHORT_MSG: u32 = 0x313bc7f8;
31const ID_UPDATE_SHORT_CHAT_MSG: u32 = 0x4d6deea5;
32const ID_UPDATE_SHORT_SENT_MSG: u32 = 0x9015e101;
33const ID_UPDATES_TOO_LONG: u32 = 0xe317af7e;
34
35/// Check a decrypted PFS bind response body for boolTrue.
36/// Telegram always wraps the Bool in an rpc_result container:
37///   rpc_result#f35c6d01 req_msg_id:long result:Bool   (16 bytes total)
38/// But may also return a bare boolTrue in some implementations.
39/// Decode one bare MTProto message body for the auth.bindTempAuthKey response.
40///
41/// Returns Ok(()) if this message body contains boolTrue (success).
42/// Returns Err(msg) for real errors.
43/// Returns Err("skip".to_string()) for informational messages the caller should ignore
44/// (new_session_created, future_salts, msgs_ack, pong, etc.).
45pub fn decode_bind_single(body: &[u8]) -> Result<(), String> {
46    const RPC_RESULT: u32 = 0xf35c6d01;
47    const BOOL_TRUE: u32 = 0x9972_75b5;
48    const BOOL_FALSE: u32 = 0xbc79_9737;
49    const RPC_ERROR: u32 = 0x2144_ca19;
50    const BAD_MSG: u32 = 0xa7ef_f811;
51    const BAD_SALT: u32 = 0xedab_447b;
52    const NEW_SESSION: u32 = 0x9ec2_0908; // new_session_created
53    const FUTURE_SALTS: u32 = 0xae50_0895;
54    const MSGS_ACK: u32 = 0x62d6_b459; // msgs_ack#62d6b459
55    const PONG: u32 = 0x347_73c5;
56
57    if body.len() < 4 {
58        return Err("skip".to_string());
59    }
60    let ctor = u32::from_le_bytes(body[..4].try_into().unwrap());
61
62    match ctor {
63        BOOL_TRUE => Ok(()),
64
65        BOOL_FALSE => Err("server returned boolFalse (binding rejected)".to_string()),
66
67        // Informational: not an error, caller skips these.
68        NEW_SESSION | FUTURE_SALTS | MSGS_ACK | PONG => Err("skip".to_string()),
69
70        RPC_RESULT if body.len() >= 16 => {
71            let inner = u32::from_le_bytes(body[12..16].try_into().unwrap());
72            match inner {
73                BOOL_TRUE => Ok(()),
74                BOOL_FALSE => Err("rpc_result{boolFalse} (server rejected binding)".to_string()),
75                RPC_ERROR if body.len() >= 20 => {
76                    let code = i32::from_le_bytes(body[16..20].try_into().unwrap());
77                    let msg = crate::util::tl_read_string(body.get(20..).unwrap_or(&[]))
78                        .unwrap_or_default();
79                    Err(format!("rpc_error code={code} message={msg:?}"))
80                }
81                _ => Err(format!("rpc_result inner ctor={inner:#010x}")),
82            }
83        }
84
85        BAD_MSG if body.len() >= 16 => {
86            let code = u32::from_le_bytes(body[12..16].try_into().unwrap());
87            let desc = match code {
88                16 => "msg_id too low (clock skew)",
89                17 => "msg_id too high (clock skew)",
90                18 => "incorrect lower 2 bits of msg_id",
91                19 => "duplicate msg_id",
92                20 => "message too old (>300s)",
93                32 => "msg_seqno too low",
94                33 => "msg_seqno too high",
95                34 => "even seqno expected, odd received",
96                35 => "odd seqno expected, even received",
97                48 => "incorrect server salt",
98                64 => "invalid container",
99                _ => "unknown code",
100            };
101            Err(format!("bad_msg_notification code={code} ({desc})"))
102        }
103
104        BAD_SALT if body.len() >= 24 => {
105            let new_salt = i64::from_le_bytes(body[16..24].try_into().unwrap());
106            Err(format!(
107                "bad_server_salt, server wants salt={new_salt:#018x}"
108            ))
109        }
110
111        _ => Err(format!("unknown ctor={ctor:#010x}")),
112    }
113}
114
115/// Decode the server response to auth.bindTempAuthKey.
116///
117/// Handles bare messages AND msg_container (the server frequently bundles
118/// new_session_created + rpc_result together in a container on the very first
119/// encrypted message of a fresh temp session).
120pub fn decode_bind_response(body: &[u8]) -> Result<(), String> {
121    const MSG_CONTAINER: u32 = 0x73f1f8dc;
122
123    if body.len() < 4 {
124        return Err(format!("response body too short ({} bytes)", body.len()));
125    }
126    let ctor = u32::from_le_bytes(body[..4].try_into().unwrap());
127
128    if ctor != MSG_CONTAINER {
129        // Bare message: decode directly.
130        return decode_bind_single(body).map_err(|e| {
131            if e == "skip" {
132                // Informational frame (msgs_ack, new_session_created, etc.).
133                // Caller should read the next frame rather than hard-fail.
134                "__need_more__".to_string()
135            } else {
136                e
137            }
138        });
139    }
140
141    // msg_container#73f1f8dc messages:vector<message> = MessageContainer
142    // Each message: msg_id:long seqno:int bytes:int body:bytes
143    if body.len() < 8 {
144        return Err("msg_container too short to read count".to_string());
145    }
146    let count = u32::from_le_bytes(body[4..8].try_into().unwrap()) as usize;
147    let mut pos = 8usize;
148    let mut last_real_err: Option<String> = None;
149
150    for i in 0..count {
151        // header: msg_id(8) + seqno(4) + bytes(4) = 16 bytes
152        if pos + 16 > body.len() {
153            return Err(format!(
154                "msg_container truncated at message {i}/{count} (pos={pos} body_len={})",
155                body.len()
156            ));
157        }
158        let msg_bytes = u32::from_le_bytes(body[pos + 12..pos + 16].try_into().unwrap()) as usize;
159        pos += 16;
160
161        if pos + msg_bytes > body.len() {
162            return Err(format!(
163                "msg_container message {i} body overflows (need {msg_bytes}, have {})",
164                body.len() - pos
165            ));
166        }
167        let msg_body = &body[pos..pos + msg_bytes];
168        pos += msg_bytes;
169
170        match decode_bind_single(msg_body) {
171            Ok(()) => return Ok(()),           // found boolTrue; done
172            Err(e) if e == "skip" => continue, // new_session_created etc; normal
173            Err(e) => {
174                // Real error: remember it but keep iterating in case
175                // a later message in the container contains boolTrue.
176                last_real_err = Some(e);
177            }
178        }
179    }
180
181    // No message in the container returned boolTrue.
182    // If last_real_err is None, every message was informational → caller should read
183    // the next frame. If there was a real error, propagate it.
184    Err(last_real_err.unwrap_or_else(|| "__need_more__".to_string()))
185}
186
187pub enum EnvelopeResult {
188    Payload(Vec<u8>),
189    /// Raw update bytes to be routed through dispatch_updates for proper pts tracking.
190    RawUpdates(Vec<Vec<u8>>),
191    /// pts/pts_count from updateShortSentMessage: advance counter, emit nothing.
192    Pts(i32, i32),
193    None,
194}
195
196pub fn unwrap_envelope(body: Vec<u8>) -> Result<EnvelopeResult, ConnectError> {
197    if body.len() < 4 {
198        return Err(ConnectError::other("body < 4 bytes"));
199    }
200    let cid = u32::from_le_bytes(body[..4].try_into().unwrap());
201
202    match cid {
203        ID_RPC_RESULT => {
204            if body.len() < 12 {
205                return Err(ConnectError::other("rpc_result too short"));
206            }
207            unwrap_envelope(body[12..].to_vec())
208        }
209        ID_RPC_ERROR => {
210            if body.len() < 8 {
211                return Err(ConnectError::other("rpc_error too short"));
212            }
213            let code    = i32::from_le_bytes(body[4..8].try_into().unwrap());
214            let message = crate::util::tl_read_string(&body[8..]).unwrap_or_default();
215            Err(ConnectError::Rpc { code, message })
216        }
217        ID_MSG_CONTAINER => {
218            if body.len() < 8 {
219                return Err(ConnectError::other("container too short"));
220            }
221            let count = u32::from_le_bytes(body[4..8].try_into().unwrap()) as usize;
222            let mut pos = 8usize;
223            let mut payload: Option<Vec<u8>> = None;
224            let mut raw_updates: Vec<Vec<u8>> = Vec::new();
225
226            for _ in 0..count {
227                if pos + 16 > body.len() { break; }
228                let inner_len = u32::from_le_bytes(body[pos + 12..pos + 16].try_into().unwrap()) as usize;
229                pos += 16;
230                if pos + inner_len > body.len() { break; }
231                let inner = body[pos..pos + inner_len].to_vec();
232                pos += inner_len;
233                match unwrap_envelope(inner)? {
234                    EnvelopeResult::Payload(p)          => { payload = Some(p); }
235                    EnvelopeResult::RawUpdates(mut raws) => { raw_updates.append(&mut raws); }
236                    EnvelopeResult::Pts(_, _)            => {} // handled via spawned task in route_frame
237                    EnvelopeResult::None                 => {}
238                }
239            }
240            if let Some(p) = payload {
241                Ok(EnvelopeResult::Payload(p))
242            } else if !raw_updates.is_empty() {
243                Ok(EnvelopeResult::RawUpdates(raw_updates))
244            } else {
245                Ok(EnvelopeResult::None)
246            }
247        }
248        ID_GZIP_PACKED => {
249            let bytes = crate::util::tl_read_bytes(&body[4..]).unwrap_or_default();
250            unwrap_envelope(crate::util::gz_inflate(&bytes)?)
251        }
252        // MTProto service messages: silently acknowledged, no payload extracted.
253        // NOTE: ID_PONG is intentionally NOT listed here. Pong arrives as a bare
254        // top-level frame (never inside rpc_result), so it is handled in route_frame
255        // directly. Silencing it here would drop it before invoke() can resolve it.
256        ID_MSGS_ACK | ID_NEW_SESSION | ID_BAD_SERVER_SALT | ID_BAD_MSG_NOTIFY
257        // These are correctly silenced ( silences these too)
258        | 0xd33b5459  // MsgsStateReq
259        | 0x04deb57d  // MsgsStateInfo
260        | 0x8cc0d131  // MsgsAllInfo
261        | 0x276d3ec6  // MsgDetailedInfo
262        | 0x809db6df  // MsgNewDetailedInfo
263        | 0x7d861a08  // MsgResendReq / MsgResendAnsReq
264        | 0x0949d9dc  // FutureSalt
265        | 0xae500895  // FutureSalts
266        | 0x9299359f  // HttpWait
267        | 0xe22045fc  // DestroySessionOk
268        | 0x62d350c9  // DestroySessionNone
269        => {
270            Ok(EnvelopeResult::None)
271        }
272        // Route all update containers via RawUpdates so route_frame can call
273        // dispatch_updates, which handles pts/seq tracking. Without this, updates
274        // from RPC responses (e.g. updateNewMessage + updateReadHistoryOutbox from
275        // messages.sendMessage) bypass pts entirely -> false gaps -> getDifference
276        // -> duplicate message delivery.
277        ID_UPDATES | ID_UPDATE_SHORT | ID_UPDATES_COMBINED
278        | ID_UPDATE_SHORT_MSG | ID_UPDATE_SHORT_CHAT_MSG
279        | ID_UPDATES_TOO_LONG => {
280            Ok(EnvelopeResult::RawUpdates(vec![body]))
281        }
282        // updateShortSentMessage carries pts for the bot's own sent message;
283        // extract and advance the pts counter.
284        ID_UPDATE_SHORT_SENT_MSG => {
285            let mut cur = Cursor::from_slice(&body[4..]);
286            match tl::types::UpdateShortSentMessage::deserialize(&mut cur) {
287                Ok(m) => {
288                    tracing::debug!(
289                        "[ferogram] updateShortSentMessage (RPC): pts={} pts_count={}: advancing pts",
290                        m.pts, m.pts_count
291                    );
292                    Ok(EnvelopeResult::Pts(m.pts, m.pts_count))
293                }
294                Err(e) => {
295                    tracing::debug!("[ferogram] updateShortSentMessage deserialize error: {e}");
296                    Ok(EnvelopeResult::None)
297                }
298            }
299        }
300        _ => Ok(EnvelopeResult::Payload(body)),
301    }
302}
303
304/// Extract (users, chats) slices from any `Updates` variant.
305///
306/// Covers `Updates`, `UpdatesCombined`, and `UpdateShortChatMessage` /
307/// `UpdateShortMessage` (which embed no entities; returns empty vecs).
308/// Used to cache entities immediately after any RPC that returns `Updates`.
309pub fn updates_entities(
310    updates: &tl::enums::Updates,
311) -> (Vec<tl::enums::User>, Vec<tl::enums::Chat>) {
312    match updates {
313        tl::enums::Updates::Updates(u) => (u.users.clone(), u.chats.clone()),
314        tl::enums::Updates::Combined(u) => (u.users.clone(), u.chats.clone()),
315        _ => (Vec::new(), Vec::new()),
316    }
317}
318
319/// Convert a `Chat` enum variant to its corresponding `Peer`.
320pub fn chat_to_peer(chat: &tl::enums::Chat) -> Option<tl::enums::Peer> {
321    match chat {
322        tl::enums::Chat::Channel(c) => Some(tl::enums::Peer::Channel(tl::types::PeerChannel {
323            channel_id: c.id,
324        })),
325        tl::enums::Chat::ChannelForbidden(c) => {
326            Some(tl::enums::Peer::Channel(tl::types::PeerChannel {
327                channel_id: c.id,
328            }))
329        }
330        tl::enums::Chat::Chat(c) => {
331            Some(tl::enums::Peer::Chat(tl::types::PeerChat { chat_id: c.id }))
332        }
333        tl::enums::Chat::Forbidden(c) => {
334            Some(tl::enums::Peer::Chat(tl::types::PeerChat { chat_id: c.id }))
335        }
336        tl::enums::Chat::Empty(_) => None,
337    }
338}