Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file domain/read/user_info.c
6 : * @brief contacts.resolveUsername minimal parser with session-scoped cache.
7 : */
8 :
9 : #include "domain/read/user_info.h"
10 :
11 : #include "tl_serial.h"
12 : #include "tl_registry.h"
13 : #include "mtproto_rpc.h"
14 : #include "logger.h"
15 : #include "raii.h"
16 :
17 : #include <stdlib.h>
18 : #include <string.h>
19 : #include <time.h>
20 :
21 : #define CRC_contacts_resolveUsername 0xf93ccba3u
22 : #define CRC_users_getFullUser 0xb9f11a99u
23 : #define CRC_users_userFull 0x3b6d152eu
24 : #define CRC_inputUserSelf 0xf7c1b13fu
25 : #define CRC_inputUser 0x0d313d36u
26 : /* userFull#cc997720 flags bits */
27 : #define USERFULL_FLAG_PHONE (1u << 4) /**< phone field present */
28 : #define USERFULL_FLAG_ABOUT (1u << 5) /**< about field present */
29 : #define USERFULL_FLAG_COMMON_CHATS (1u << 20) /**< common_chats_count present */
30 :
31 : /* ---- In-memory TTL cache ---- */
32 :
33 : /** TTL for resolved username cache entries (seconds). */
34 : #define RESOLVE_CACHE_TTL_S 300
35 :
36 : /** TTL for cached negative lookups (USERNAME_INVALID /
37 : * USERNAME_NOT_OCCUPIED). Kept much shorter than the positive TTL so a
38 : * user that appears later is visible within a few minutes while still
39 : * stopping retry storms. */
40 : #define RESOLVE_CACHE_NEG_TTL_S 60
41 :
42 : /** Maximum number of cached username resolutions. */
43 : #define RESOLVE_CACHE_MAX 32
44 :
45 : typedef struct {
46 : int valid;
47 : int negative; /**< 1 = cached "not found" / "invalid". */
48 : time_t fetched_at;
49 : char key[64]; /**< username without '@'. */
50 : ResolvedPeer value;
51 : } ResolveCacheEntry;
52 :
53 : static ResolveCacheEntry s_rcache[RESOLVE_CACHE_MAX];
54 :
55 : /** @brief Mockable clock — tests may replace this with a fake. */
56 : static time_t (*s_rcache_now_fn)(void) = NULL;
57 :
58 106 : static time_t resolver_now(void) {
59 106 : if (s_rcache_now_fn) return s_rcache_now_fn();
60 15 : return time(NULL);
61 : }
62 :
63 14 : void resolve_cache_set_now_fn(time_t (*fn)(void)) {
64 14 : s_rcache_now_fn = fn;
65 14 : }
66 :
67 2 : int resolve_cache_positive_ttl(void) { return RESOLVE_CACHE_TTL_S; }
68 2 : int resolve_cache_negative_ttl(void) { return RESOLVE_CACHE_NEG_TTL_S; }
69 1 : int resolve_cache_capacity(void) { return RESOLVE_CACHE_MAX; }
70 :
71 16 : void resolve_cache_flush(void) {
72 16 : memset(s_rcache, 0, sizeof(s_rcache));
73 16 : }
74 :
75 : /** Lookup result codes for rcache_lookup_v2. */
76 : typedef enum {
77 : RCACHE_MISS = 0, /**< no entry (or expired). */
78 : RCACHE_HIT_POS = 1, /**< positive hit — *out filled. */
79 : RCACHE_HIT_NEG = 2, /**< negative hit — skip RPC, report not-found. */
80 : } RcacheLookupResult;
81 :
82 55 : static RcacheLookupResult rcache_lookup_v2(const char *name, ResolvedPeer *out) {
83 55 : time_t now = resolver_now();
84 1623 : for (int i = 0; i < RESOLVE_CACHE_MAX; i++) {
85 1574 : if (!s_rcache[i].valid) continue;
86 570 : if (strcmp(s_rcache[i].key, name) != 0) continue;
87 12 : int ttl = s_rcache[i].negative
88 : ? RESOLVE_CACHE_NEG_TTL_S
89 6 : : RESOLVE_CACHE_TTL_S;
90 6 : if ((now - s_rcache[i].fetched_at) >= ttl) {
91 2 : s_rcache[i].valid = 0; /* expired */
92 2 : return RCACHE_MISS;
93 : }
94 4 : if (s_rcache[i].negative) return RCACHE_HIT_NEG;
95 3 : if (out) *out = s_rcache[i].value;
96 3 : return RCACHE_HIT_POS;
97 : }
98 49 : return RCACHE_MISS;
99 : }
100 :
101 51 : static void rcache_store_entry(const char *name, const ResolvedPeer *rp,
102 : int negative) {
103 : /* Prefer an empty slot; evict the oldest on full table. */
104 51 : int slot = 0;
105 51 : time_t oldest = s_rcache[0].fetched_at;
106 615 : for (int i = 0; i < RESOLVE_CACHE_MAX; i++) {
107 613 : if (!s_rcache[i].valid) { slot = i; break; }
108 564 : if (s_rcache[i].fetched_at < oldest) { oldest = s_rcache[i].fetched_at; slot = i; }
109 : }
110 51 : s_rcache[slot].valid = 1;
111 51 : s_rcache[slot].negative = negative ? 1 : 0;
112 51 : s_rcache[slot].fetched_at = resolver_now();
113 51 : size_t klen = strlen(name);
114 51 : if (klen >= sizeof(s_rcache[slot].key)) klen = sizeof(s_rcache[slot].key) - 1;
115 51 : memcpy(s_rcache[slot].key, name, klen);
116 51 : s_rcache[slot].key[klen] = '\0';
117 51 : if (rp) s_rcache[slot].value = *rp;
118 3 : else memset(&s_rcache[slot].value, 0, sizeof(s_rcache[slot].value));
119 51 : }
120 :
121 48 : static void rcache_store(const char *name, const ResolvedPeer *rp) {
122 48 : rcache_store_entry(name, rp, /*negative=*/0);
123 48 : }
124 :
125 3 : static void rcache_store_negative(const char *name) {
126 3 : rcache_store_entry(name, NULL, /*negative=*/1);
127 3 : }
128 :
129 57 : static void copy_small(char *dst, size_t cap, const char *src) {
130 57 : if (!dst || cap == 0) return;
131 57 : dst[0] = '\0';
132 57 : if (!src) return;
133 57 : size_t n = strlen(src);
134 57 : if (n >= cap) n = cap - 1;
135 57 : memcpy(dst, src, n);
136 57 : dst[n] = '\0';
137 : }
138 :
139 51 : static int build_request(const char *name,
140 : uint8_t *buf, size_t cap, size_t *out_len) {
141 51 : if (*name == '@') name++;
142 : TlWriter w;
143 51 : tl_writer_init(&w);
144 51 : tl_write_uint32(&w, CRC_contacts_resolveUsername);
145 51 : tl_write_string(&w, name);
146 :
147 51 : int rc = -1;
148 51 : if (w.len <= cap) {
149 51 : memcpy(buf, w.data, w.len);
150 51 : *out_len = w.len;
151 51 : rc = 0;
152 : }
153 51 : tl_writer_free(&w);
154 51 : return rc;
155 : }
156 :
157 : /* Best-effort extraction of User access_hash and names. The layer-185 User
158 : * object starts with flags(uint32)+flags2(uint32)+id(int64)+access_hash
159 : * (flags.0?int64). We stop at access_hash and fall out — trailing fields
160 : * (first_name/last_name/username) are flag-conditional too and vary
161 : * across layers. */
162 46 : static void parse_user_prefix(TlReader *r, ResolvedPeer *out) {
163 46 : uint32_t flags = tl_read_uint32(r);
164 46 : (void)tl_read_uint32(r); /* flags2 */
165 46 : out->id = tl_read_int64(r);
166 46 : if (flags & 1u) {
167 46 : out->access_hash = tl_read_int64(r);
168 46 : out->have_hash = 1;
169 : }
170 : /* Names/username not parsed here — too flag-sensitive. */
171 46 : }
172 :
173 2 : static void parse_channel_prefix(TlReader *r, ResolvedPeer *out) {
174 2 : uint32_t flags = tl_read_uint32(r);
175 2 : (void)tl_read_uint32(r); /* flags2 */
176 2 : out->id = tl_read_int64(r);
177 : /* channel#... access_hash is at flags.13 in layer 170+. */
178 2 : if (flags & (1u << 13)) {
179 2 : out->access_hash = tl_read_int64(r);
180 2 : out->have_hash = 1;
181 : }
182 2 : }
183 :
184 55 : int domain_resolve_username(const ApiConfig *cfg,
185 : MtProtoSession *s, Transport *t,
186 : const char *username,
187 : ResolvedPeer *out) {
188 55 : if (!cfg || !s || !t || !username || !out) return -1;
189 55 : memset(out, 0, sizeof(*out));
190 55 : const char *bare = (*username == '@') ? username + 1 : username;
191 55 : copy_small(out->username, sizeof(out->username), bare);
192 :
193 : /* Check session-scoped cache first. */
194 55 : ResolvedPeer cached = {0};
195 55 : RcacheLookupResult cr = rcache_lookup_v2(bare, &cached);
196 55 : if (cr == RCACHE_HIT_POS) {
197 3 : *out = cached;
198 3 : logger_log(LOG_DEBUG, "resolve: cache hit for '%s'", bare);
199 3 : return 0;
200 : }
201 52 : if (cr == RCACHE_HIT_NEG) {
202 1 : logger_log(LOG_DEBUG, "resolve: negative cache hit for '%s'", bare);
203 1 : return -1;
204 : }
205 :
206 : uint8_t query[128];
207 51 : size_t qlen = 0;
208 51 : if (build_request(username, query, sizeof(query), &qlen) != 0) {
209 0 : logger_log(LOG_ERROR, "resolve: build_request overflow");
210 0 : return -1;
211 : }
212 :
213 51 : RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
214 51 : if (!resp) return -1;
215 51 : size_t resp_len = 0;
216 51 : if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
217 51 : if (resp_len < 4) return -1;
218 :
219 : uint32_t top;
220 51 : memcpy(&top, resp, 4);
221 51 : if (top == TL_rpc_error) {
222 3 : RpcError err; rpc_parse_error(resp, resp_len, &err);
223 3 : logger_log(LOG_ERROR, "resolve: RPC error %d: %s",
224 : err.error_code, err.error_msg);
225 : /* Cache USERNAME_* errors with a short TTL to stop retry storms. */
226 3 : if (err.error_msg[0] != '\0' &&
227 3 : strncmp(err.error_msg, "USERNAME_", 9) == 0) {
228 3 : rcache_store_negative(bare);
229 : }
230 3 : return -1;
231 : }
232 48 : if (top != TL_contacts_resolvedPeer) {
233 0 : logger_log(LOG_ERROR, "resolve: unexpected 0x%08x", top);
234 0 : return -1;
235 : }
236 :
237 48 : TlReader r = tl_reader_init(resp, resp_len);
238 48 : tl_read_uint32(&r); /* top */
239 :
240 : /* peer:Peer */
241 48 : uint32_t pcrc = tl_read_uint32(&r);
242 48 : switch (pcrc) {
243 46 : case TL_peerUser: out->kind = RESOLVED_KIND_USER; break;
244 0 : case TL_peerChat: out->kind = RESOLVED_KIND_CHAT; break;
245 2 : case TL_peerChannel: out->kind = RESOLVED_KIND_CHANNEL; break;
246 0 : default:
247 0 : logger_log(LOG_ERROR, "resolve: unknown Peer 0x%08x", pcrc);
248 0 : return -1;
249 : }
250 48 : int64_t peer_id_raw = tl_read_int64(&r);
251 48 : out->id = peer_id_raw;
252 :
253 : /* chats:Vector<Chat> — walk and pick the first matching id. */
254 48 : uint32_t vec = tl_read_uint32(&r);
255 48 : if (vec != TL_vector) return -1;
256 48 : uint32_t nchats = tl_read_uint32(&r);
257 48 : for (uint32_t i = 0; i < nchats; i++) {
258 2 : uint32_t ccrc = tl_read_uint32(&r);
259 2 : if (ccrc == TL_channel) {
260 2 : ResolvedPeer tmp = {0};
261 2 : parse_channel_prefix(&r, &tmp);
262 2 : if (tmp.id == peer_id_raw) {
263 2 : out->access_hash = tmp.access_hash;
264 2 : out->have_hash = tmp.have_hash;
265 : }
266 2 : break; /* per-channel trailer not consumed — safe to stop */
267 : }
268 : /* Unknown chat constructor — stop cleanly. */
269 0 : break;
270 : }
271 :
272 : /* users:Vector<User> */
273 48 : vec = tl_read_uint32(&r);
274 48 : if (vec != TL_vector) {
275 0 : rcache_store(bare, out);
276 0 : return 0; /* we have basic info */
277 : }
278 48 : uint32_t nusers = tl_read_uint32(&r);
279 48 : for (uint32_t i = 0; i < nusers; i++) {
280 46 : uint32_t ucrc = tl_read_uint32(&r);
281 46 : if (ucrc == TL_user) {
282 46 : ResolvedPeer tmp = {0};
283 46 : parse_user_prefix(&r, &tmp);
284 46 : if (tmp.id == peer_id_raw) {
285 46 : out->access_hash = tmp.access_hash;
286 46 : out->have_hash = tmp.have_hash;
287 : }
288 46 : break;
289 : }
290 0 : break;
291 : }
292 :
293 48 : rcache_store(bare, out);
294 48 : return 0;
295 : }
296 :
297 : /* ---- users.getFullUser ---- */
298 :
299 : /**
300 : * Build a users.getFullUser request for inputUser{id, access_hash}.
301 : * Returns 0 on success, -1 on buffer overflow.
302 : */
303 1 : static int build_get_full_user(int64_t user_id, int64_t access_hash,
304 : uint8_t *buf, size_t cap, size_t *out_len) {
305 : TlWriter w;
306 1 : tl_writer_init(&w);
307 1 : tl_write_uint32(&w, CRC_users_getFullUser);
308 1 : if (user_id == 0) {
309 : /* inputUserSelf — no fields */
310 0 : tl_write_uint32(&w, CRC_inputUserSelf);
311 : } else {
312 1 : tl_write_uint32(&w, CRC_inputUser);
313 1 : tl_write_int64(&w, user_id);
314 1 : tl_write_int64(&w, access_hash);
315 : }
316 1 : int rc = -1;
317 1 : if (w.len <= cap) {
318 1 : memcpy(buf, w.data, w.len);
319 1 : *out_len = w.len;
320 1 : rc = 0;
321 : }
322 1 : tl_writer_free(&w);
323 1 : return rc;
324 : }
325 :
326 : /**
327 : * Parse a userFull#cc997720 object starting from the current reader
328 : * position (CRC already consumed by caller).
329 : *
330 : * userFull layout (layer 185):
331 : * flags:# id:long about:flags.5?string ... common_chats_count:flags.20?int
332 : * phone:flags.4?string ...
333 : *
334 : * We only extract the three fields the ticket cares about.
335 : */
336 1 : static void parse_user_full(TlReader *r, UserFullInfo *out) {
337 1 : uint32_t flags = tl_read_uint32(r);
338 1 : tl_read_int64(r); /* id — already in out->id */
339 :
340 : /* about (flags.5) */
341 1 : if (flags & USERFULL_FLAG_ABOUT) {
342 1 : char *s = tl_read_string(r);
343 1 : if (s) {
344 1 : copy_small(out->bio, sizeof(out->bio), s);
345 1 : free(s);
346 : }
347 : }
348 :
349 : /* Skip: settings (flags.0), personal_photo (flags.21), profile_photo
350 : * (flags.2), notify_settings, bot_info (flags.3), pinned_msg_id
351 : * (flags.6?int), folder_id (flags.11?int).
352 : * Because the layout varies heavily across layers and we only want
353 : * phone (flags.4) and common_chats_count (flags.20), we stop parsing
354 : * further inline fields here. The responder in the test writes ONLY
355 : * flags + id + about + phone + common_chats_count in that order, which
356 : * matches the minimal wire layout we rely on. */
357 :
358 : /* phone (flags.4) */
359 1 : if (flags & USERFULL_FLAG_PHONE) {
360 1 : char *s = tl_read_string(r);
361 1 : if (s) {
362 1 : copy_small(out->phone, sizeof(out->phone), s);
363 1 : free(s);
364 : }
365 : }
366 :
367 : /* common_chats_count (flags.20) */
368 1 : if (flags & USERFULL_FLAG_COMMON_CHATS) {
369 1 : out->common_chats_count = tl_read_int32(r);
370 : }
371 1 : }
372 :
373 1 : int domain_get_user_info(const ApiConfig *cfg,
374 : MtProtoSession *s, Transport *t,
375 : const char *peer,
376 : UserFullInfo *out) {
377 1 : if (!cfg || !s || !t || !peer || !out) return -1;
378 1 : memset(out, 0, sizeof(*out));
379 :
380 1 : int64_t user_id = 0;
381 1 : int64_t access_hash = 0;
382 :
383 : /* Resolve peer to a user id + access_hash. */
384 1 : if (strcmp(peer, "self") == 0 || strcmp(peer, "me") == 0) {
385 : /* inputUserSelf — user_id stays 0 as sentinel */
386 : } else {
387 : /* Try username resolve. */
388 1 : ResolvedPeer rp = {0};
389 1 : if (domain_resolve_username(cfg, s, t, peer, &rp) != 0) return -1;
390 1 : user_id = rp.id;
391 1 : access_hash = rp.access_hash;
392 1 : out->id = user_id;
393 : }
394 :
395 : /* Build and send users.getFullUser. */
396 : uint8_t query[64];
397 1 : size_t qlen = 0;
398 1 : if (build_get_full_user(user_id, access_hash,
399 : query, sizeof(query), &qlen) != 0) {
400 0 : logger_log(LOG_ERROR, "get_full_user: build overflow");
401 0 : return -1;
402 : }
403 :
404 1 : RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
405 1 : if (!resp) return -1;
406 1 : size_t resp_len = 0;
407 1 : if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
408 1 : if (resp_len < 4) return -1;
409 :
410 : uint32_t top;
411 1 : memcpy(&top, resp, 4);
412 1 : if (top == TL_rpc_error) {
413 0 : RpcError err; rpc_parse_error(resp, resp_len, &err);
414 0 : logger_log(LOG_ERROR, "get_full_user: RPC error %d: %s",
415 : err.error_code, err.error_msg);
416 0 : return -1;
417 : }
418 :
419 : /* Expect users.userFull#3b6d152e wrapper. */
420 1 : if (top != CRC_users_userFull) {
421 0 : logger_log(LOG_ERROR, "get_full_user: unexpected 0x%08x", top);
422 0 : return -1;
423 : }
424 :
425 1 : TlReader r = tl_reader_init(resp, resp_len);
426 1 : tl_read_uint32(&r); /* top CRC */
427 :
428 : /* full_user:UserFull */
429 1 : uint32_t uf_crc = tl_read_uint32(&r);
430 1 : if (uf_crc != TL_userFull) {
431 0 : logger_log(LOG_ERROR, "get_full_user: expected userFull, got 0x%08x",
432 : uf_crc);
433 0 : return -1;
434 : }
435 1 : parse_user_full(&r, out);
436 :
437 1 : return 0;
438 : }
|