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 313 : static int classify_service_frame(MtProtoSession *s,
83 : const uint8_t *resp, size_t resp_len) {
84 313 : if (resp_len < 4) return SVC_ERROR;
85 : uint32_t crc;
86 313 : memcpy(&crc, resp, 4);
87 :
88 313 : 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 311 : 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 309 : 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 308 : if (crc == TL_msgs_ack || crc == TL_pong) {
126 10 : logger_log(LOG_DEBUG, "api_call: ignoring service frame 0x%08x", crc);
127 10 : return SVC_SKIP;
128 : }
129 :
130 298 : return SVC_RESULT;
131 : }
132 :
133 : /** Maximum number of service frames we'll drain before giving up. */
134 : #define SERVICE_FRAME_LIMIT 8
135 :
136 304 : static int api_call_once(const ApiConfig *cfg,
137 : MtProtoSession *s, Transport *t,
138 : const uint8_t *query, size_t qlen,
139 : uint8_t *resp, size_t max_len, size_t *resp_len,
140 : int *bad_salt) {
141 304 : *bad_salt = 0;
142 :
143 : /* 1 MiB accommodates the largest call we make — a saveBigFilePart
144 : * carrying a 512 KiB chunk + initConnection + invokeWithLayer. */
145 : enum { API_BUF_SIZE = 1024 * 1024 };
146 304 : RAII_STRING uint8_t *wrapped = (uint8_t *)malloc(API_BUF_SIZE);
147 304 : if (!wrapped) return -1;
148 304 : size_t wrapped_len = 0;
149 304 : if (api_wrap_query(cfg, query, qlen, wrapped, API_BUF_SIZE,
150 : &wrapped_len) != 0) {
151 0 : logger_log(LOG_ERROR, "api_call: failed to wrap query");
152 0 : return -1;
153 : }
154 :
155 304 : if (rpc_send_encrypted(s, t, wrapped, wrapped_len, 1) != 0) {
156 0 : logger_log(LOG_ERROR, "api_call: failed to send");
157 0 : return -1;
158 : }
159 :
160 304 : RAII_STRING uint8_t *raw_resp = (uint8_t *)malloc(API_BUF_SIZE);
161 304 : if (!raw_resp) return -1;
162 304 : size_t raw_len = 0;
163 :
164 : /* Drain service frames until we see a real result. If we never see one
165 : * within SERVICE_FRAME_LIMIT iterations, treat it as a protocol failure —
166 : * without this guard the loop would fall through with `raw_resp` still
167 : * holding a service frame (e.g. msgs_ack) and `rpc_unwrap_gzip` — which
168 : * is permissive about non-gzip payloads — would succeed, surfacing the
169 : * service frame bytes to the caller as if they were a real result. */
170 304 : int saw_result = 0;
171 315 : for (int attempt = 0; attempt < SERVICE_FRAME_LIMIT; attempt++) {
172 314 : if (rpc_recv_encrypted(s, t, raw_resp, API_BUF_SIZE, &raw_len) != 0) {
173 1 : logger_log(LOG_ERROR, "api_call: failed to receive");
174 1 : return -1;
175 : }
176 313 : int klass = classify_service_frame(s, raw_resp, raw_len);
177 313 : if (klass == SVC_ERROR) return -1;
178 311 : if (klass == SVC_BAD_SALT) { *bad_salt = 1; return 0; }
179 309 : if (klass == SVC_SKIP) continue;
180 298 : saw_result = 1;
181 298 : break; /* SVC_RESULT */
182 : }
183 299 : if (!saw_result) {
184 1 : logger_log(LOG_ERROR,
185 : "api_call: drained %d service frames without a real result",
186 : SERVICE_FRAME_LIMIT);
187 1 : return -1;
188 : }
189 :
190 : uint64_t req_msg_id;
191 : const uint8_t *inner;
192 : size_t inner_len;
193 298 : const uint8_t *payload = raw_resp;
194 298 : size_t payload_len = raw_len;
195 298 : if (rpc_unwrap_result(raw_resp, raw_len, &req_msg_id,
196 : &inner, &inner_len) == 0) {
197 298 : payload = inner;
198 298 : payload_len = inner_len;
199 : }
200 :
201 298 : if (rpc_unwrap_gzip(payload, payload_len,
202 : resp, max_len, resp_len) != 0) {
203 1 : logger_log(LOG_ERROR, "api_call: failed to unwrap response");
204 1 : return -1;
205 : }
206 297 : return 0;
207 : }
208 :
209 302 : int api_call(const ApiConfig *cfg,
210 : MtProtoSession *s, Transport *t,
211 : const uint8_t *query, size_t qlen,
212 : uint8_t *resp, size_t max_len, size_t *resp_len) {
213 302 : if (!cfg || !s || !t || !query || !resp || !resp_len) return -1;
214 :
215 302 : int bad_salt = 0;
216 302 : int rc = api_call_once(cfg, s, t, query, qlen,
217 : resp, max_len, resp_len, &bad_salt);
218 302 : if (rc != 0) return rc;
219 297 : if (!bad_salt) return 0;
220 :
221 : /* One-shot retry with the newly-received salt. */
222 2 : rc = api_call_once(cfg, s, t, query, qlen,
223 : resp, max_len, resp_len, &bad_salt);
224 2 : if (rc != 0) return rc;
225 2 : if (bad_salt) {
226 0 : logger_log(LOG_ERROR,
227 : "api_call: bad_server_salt after retry — giving up");
228 0 : return -1;
229 : }
230 2 : return 0;
231 : }
|