Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file api_call.c
6 : * @brief Telegram API call wrapper — initConnection + invokeWithLayer.
7 : */
8 :
9 : #include "api_call.h"
10 : #include "mtproto_rpc.h"
11 : #include "tl_serial.h"
12 : #include "tl_registry.h"
13 : #include "logger.h"
14 : #include "raii.h"
15 :
16 : #include <stdlib.h>
17 : #include <string.h>
18 :
19 225 : void api_config_init(ApiConfig *cfg) {
20 225 : if (!cfg) return;
21 225 : memset(cfg, 0, sizeof(*cfg));
22 225 : cfg->device_model = "tg-cli";
23 225 : cfg->system_version = "Linux";
24 225 : cfg->app_version = "0.1.0";
25 225 : cfg->system_lang_code = "en";
26 225 : cfg->lang_pack = "";
27 225 : cfg->lang_code = "en";
28 : }
29 :
30 304 : int api_wrap_query(const ApiConfig *cfg,
31 : const uint8_t *query, size_t qlen,
32 : uint8_t *out, size_t max_len, size_t *out_len) {
33 304 : if (!cfg || !query || !out || !out_len) return -1;
34 :
35 : TlWriter w;
36 304 : tl_writer_init(&w);
37 :
38 : /* invokeWithLayer#da9b0d0d layer:int query:!X = X */
39 304 : tl_write_uint32(&w, CRC_invokeWithLayer);
40 304 : tl_write_int32(&w, TL_LAYER);
41 :
42 : /* initConnection#c1cd5ea9 flags:# api_id:int device_model:string
43 : system_version:string app_version:string system_lang_code:string
44 : lang_pack:string lang_code:string proxy:flags.0?InputClientProxy
45 : params:flags.1?JSONValue query:!X = X */
46 304 : tl_write_uint32(&w, CRC_initConnection);
47 304 : tl_write_int32(&w, 0); /* flags = 0 (no proxy, no params) */
48 304 : tl_write_int32(&w, cfg->api_id);
49 304 : tl_write_string(&w, cfg->device_model ? cfg->device_model : "");
50 304 : tl_write_string(&w, cfg->system_version ? cfg->system_version : "");
51 304 : tl_write_string(&w, cfg->app_version ? cfg->app_version : "");
52 304 : tl_write_string(&w, cfg->system_lang_code ? cfg->system_lang_code : "");
53 304 : tl_write_string(&w, cfg->lang_pack ? cfg->lang_pack : "");
54 304 : tl_write_string(&w, cfg->lang_code ? cfg->lang_code : "");
55 :
56 : /* Append the inner query */
57 304 : tl_write_raw(&w, query, qlen);
58 :
59 304 : if (w.len > max_len) {
60 0 : tl_writer_free(&w);
61 0 : return -1;
62 : }
63 :
64 304 : memcpy(out, w.data, w.len);
65 304 : *out_len = w.len;
66 304 : tl_writer_free(&w);
67 304 : return 0;
68 : }
69 :
70 : /** @brief Classify an incoming encrypted frame.
71 : *
72 : * Returns one of the SVC_* codes. When SVC_BAD_SALT the salt on @p s is
73 : * already updated; when SVC_SKIP the caller should simply recv again.
74 : */
75 : enum {
76 : SVC_RESULT = 0, /**< ordinary rpc_result / unwrapped payload */
77 : SVC_BAD_SALT, /**< new salt stored, caller should retry send */
78 : SVC_SKIP, /**< service-only frame, loop back to recv */
79 : SVC_ERROR, /**< unrecoverable */
80 : };
81 :
82 369 : static int classify_service_frame(MtProtoSession *s,
83 : const uint8_t *resp, size_t resp_len) {
84 369 : if (resp_len < 4) return SVC_ERROR;
85 : uint32_t crc;
86 369 : memcpy(&crc, resp, 4);
87 :
88 369 : if (crc == TL_bad_server_salt) {
89 2 : if (resp_len < 28) return SVC_ERROR;
90 : uint64_t new_salt;
91 2 : memcpy(&new_salt, resp + 20, 8);
92 2 : s->server_salt = new_salt;
93 2 : logger_log(LOG_INFO,
94 : "api_call: server issued new salt 0x%016llx (retry)",
95 : (unsigned long long)new_salt);
96 2 : return SVC_BAD_SALT;
97 : }
98 :
99 367 : if (crc == TL_bad_msg_notification) {
100 : /* bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int
101 : * error_code:int = BadMsgNotification
102 : * Layout: crc(4) + bad_msg_id(8) + bad_msg_seqno(4) + error_code(4). */
103 2 : int32_t error_code = 0;
104 2 : if (resp_len >= 20) memcpy(&error_code, resp + 16, 4);
105 2 : logger_log(LOG_WARN, "api_call: bad_msg_notification code=%d", error_code);
106 : /* We cannot recover from msg_id / seqno disagreements here; bailing. */
107 2 : return SVC_ERROR;
108 : }
109 :
110 365 : if (crc == TL_new_session_created) {
111 : /* new_session_created#9ec20908 first_msg_id:long unique_id:long
112 : * server_salt:long = NewSession
113 : * Layout: crc(4) + first_msg_id(8) + unique_id(8) + server_salt(8). */
114 1 : if (resp_len >= 28) {
115 : uint64_t fresh_salt;
116 1 : memcpy(&fresh_salt, resp + 20, 8);
117 1 : s->server_salt = fresh_salt;
118 1 : logger_log(LOG_INFO,
119 : "api_call: new_session_created, salt=0x%016llx",
120 : (unsigned long long)fresh_salt);
121 : }
122 1 : return SVC_SKIP;
123 : }
124 :
125 364 : if (crc == TL_msgs_ack || crc == TL_pong) {
126 66 : logger_log(LOG_DEBUG, "api_call: ignoring service frame 0x%08x", crc);
127 66 : return SVC_SKIP;
128 : }
129 :
130 298 : if (crc == TL_msg_container) {
131 : /* Recurse into the container: process all service frames within it.
132 : * If any non-service message exists (e.g. rpc_result), return
133 : * SVC_RESULT so the caller extracts it; otherwise return SVC_SKIP. */
134 : RpcContainerMsg msgs[16];
135 0 : size_t msg_count = 0;
136 0 : if (rpc_parse_container(resp, resp_len, msgs, 16, &msg_count) != 0)
137 0 : return SVC_ERROR;
138 0 : int has_result = 0;
139 0 : for (size_t ci = 0; ci < msg_count; ci++) {
140 0 : int sub = classify_service_frame(s, msgs[ci].body, msgs[ci].body_len);
141 0 : if (sub == SVC_ERROR) return SVC_ERROR;
142 0 : if (sub == SVC_BAD_SALT) return SVC_BAD_SALT;
143 0 : if (sub == SVC_RESULT) has_result = 1;
144 : }
145 0 : return has_result ? SVC_RESULT : SVC_SKIP;
146 : }
147 :
148 298 : return SVC_RESULT;
149 : }
150 :
151 : /** Maximum number of service frames we'll drain before giving up.
152 : * Must be larger than the number of pong/ack frames that can accumulate
153 : * during a long-lived session (e.g. 90 s of pings every 10 s = ~9 pongs). */
154 : #define SERVICE_FRAME_LIMIT 64
155 :
156 304 : static int api_call_once(const ApiConfig *cfg,
157 : MtProtoSession *s, Transport *t,
158 : const uint8_t *query, size_t qlen,
159 : uint8_t *resp, size_t max_len, size_t *resp_len,
160 : int *bad_salt) {
161 304 : *bad_salt = 0;
162 :
163 : /* 1 MiB accommodates the largest call we make — a saveBigFilePart
164 : * carrying a 512 KiB chunk + initConnection + invokeWithLayer. */
165 : enum { API_BUF_SIZE = 1024 * 1024 };
166 304 : RAII_STRING uint8_t *wrapped = (uint8_t *)malloc(API_BUF_SIZE);
167 304 : if (!wrapped) return -1;
168 304 : size_t wrapped_len = 0;
169 304 : if (api_wrap_query(cfg, query, qlen, wrapped, API_BUF_SIZE,
170 : &wrapped_len) != 0) {
171 0 : logger_log(LOG_ERROR, "api_call: failed to wrap query");
172 0 : return -1;
173 : }
174 :
175 304 : if (rpc_send_encrypted(s, t, wrapped, wrapped_len, 1) != 0) {
176 0 : logger_log(LOG_ERROR, "api_call: failed to send");
177 0 : return -1;
178 : }
179 :
180 304 : RAII_STRING uint8_t *raw_resp = (uint8_t *)malloc(API_BUF_SIZE);
181 304 : if (!raw_resp) return -1;
182 304 : size_t raw_len = 0;
183 :
184 : /* Drain service frames until we see a real result. If we never see one
185 : * within SERVICE_FRAME_LIMIT iterations, treat it as a protocol failure —
186 : * without this guard the loop would fall through with `raw_resp` still
187 : * holding a service frame (e.g. msgs_ack) and `rpc_unwrap_gzip` — which
188 : * is permissive about non-gzip payloads — would succeed, surfacing the
189 : * service frame bytes to the caller as if they were a real result. */
190 304 : int saw_result = 0;
191 371 : for (int attempt = 0; attempt < SERVICE_FRAME_LIMIT; attempt++) {
192 370 : if (rpc_recv_encrypted(s, t, raw_resp, API_BUF_SIZE, &raw_len) != 0) {
193 1 : logger_log(LOG_ERROR, "api_call: failed to receive");
194 1 : return -1;
195 : }
196 369 : int klass = classify_service_frame(s, raw_resp, raw_len);
197 369 : if (klass == SVC_ERROR) return -1;
198 367 : if (klass == SVC_BAD_SALT) { *bad_salt = 1; return 0; }
199 365 : if (klass == SVC_SKIP) continue;
200 298 : saw_result = 1;
201 298 : break; /* SVC_RESULT */
202 : }
203 299 : if (!saw_result) {
204 1 : logger_log(LOG_ERROR,
205 : "api_call: drained %d service frames without a real result",
206 : SERVICE_FRAME_LIMIT);
207 1 : return -1;
208 : }
209 :
210 298 : const uint8_t *payload = raw_resp;
211 298 : size_t payload_len = raw_len;
212 :
213 : /* If the server sent a msg_container, find the rpc_result inside it. */
214 298 : if (payload_len >= 4) {
215 : uint32_t frame_crc;
216 298 : memcpy(&frame_crc, payload, 4);
217 298 : if (frame_crc == TL_msg_container) {
218 : RpcContainerMsg msgs[16];
219 0 : size_t msg_count = 0;
220 0 : if (rpc_parse_container(payload, payload_len,
221 : msgs, 16, &msg_count) == 0) {
222 0 : for (size_t ci = 0; ci < msg_count; ci++) {
223 0 : if (msgs[ci].body_len >= 4) {
224 : uint32_t mcrc;
225 0 : memcpy(&mcrc, msgs[ci].body, 4);
226 0 : if (mcrc == TL_rpc_result) {
227 0 : payload = msgs[ci].body;
228 0 : payload_len = msgs[ci].body_len;
229 0 : break;
230 : }
231 : }
232 : }
233 : }
234 : }
235 : }
236 :
237 : uint64_t req_msg_id;
238 : const uint8_t *inner;
239 : size_t inner_len;
240 298 : if (rpc_unwrap_result(payload, payload_len, &req_msg_id,
241 : &inner, &inner_len) == 0) {
242 298 : payload = inner;
243 298 : payload_len = inner_len;
244 : }
245 :
246 298 : if (rpc_unwrap_gzip(payload, payload_len,
247 : resp, max_len, resp_len) != 0) {
248 1 : logger_log(LOG_ERROR, "api_call: failed to unwrap response");
249 1 : return -1;
250 : }
251 297 : return 0;
252 : }
253 :
254 302 : int api_call(const ApiConfig *cfg,
255 : MtProtoSession *s, Transport *t,
256 : const uint8_t *query, size_t qlen,
257 : uint8_t *resp, size_t max_len, size_t *resp_len) {
258 302 : if (!cfg || !s || !t || !query || !resp || !resp_len) return -1;
259 :
260 302 : int bad_salt = 0;
261 302 : int rc = api_call_once(cfg, s, t, query, qlen,
262 : resp, max_len, resp_len, &bad_salt);
263 302 : if (rc != 0) return rc;
264 297 : if (!bad_salt) return 0;
265 :
266 : /* One-shot retry with the newly-received salt. */
267 2 : rc = api_call_once(cfg, s, t, query, qlen,
268 : resp, max_len, resp_len, &bad_salt);
269 2 : if (rc != 0) return rc;
270 2 : if (bad_salt) {
271 0 : logger_log(LOG_ERROR,
272 : "api_call: bad_server_salt after retry — giving up");
273 0 : return -1;
274 : }
275 2 : return 0;
276 : }
|