LCOV - code coverage report
Current view: top level - src/infrastructure - api_call.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 80.3 % 127 102
Test Date: 2026-05-06 13:17:06 Functions: 100.0 % 5 5

            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          573 : void api_config_init(ApiConfig *cfg) {
      20          573 :     if (!cfg) return;
      21          573 :     memset(cfg, 0, sizeof(*cfg));
      22          573 :     cfg->device_model = "tg-cli";
      23          573 :     cfg->system_version = "Linux";
      24          573 :     cfg->app_version = "0.1.0";
      25          573 :     cfg->system_lang_code = "en";
      26          573 :     cfg->lang_pack = "";
      27          573 :     cfg->lang_code = "en";
      28              : }
      29              : 
      30          743 : 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          743 :     if (!cfg || !query || !out || !out_len) return -1;
      34              : 
      35              :     TlWriter w;
      36          739 :     tl_writer_init(&w);
      37              : 
      38              :     /* invokeWithLayer#da9b0d0d layer:int query:!X = X */
      39          739 :     tl_write_uint32(&w, CRC_invokeWithLayer);
      40          739 :     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          739 :     tl_write_uint32(&w, CRC_initConnection);
      47          739 :     tl_write_int32(&w, 0);  /* flags = 0 (no proxy, no params) */
      48          739 :     tl_write_int32(&w, cfg->api_id);
      49          739 :     tl_write_string(&w, cfg->device_model ? cfg->device_model : "");
      50          739 :     tl_write_string(&w, cfg->system_version ? cfg->system_version : "");
      51          739 :     tl_write_string(&w, cfg->app_version ? cfg->app_version : "");
      52          739 :     tl_write_string(&w, cfg->system_lang_code ? cfg->system_lang_code : "");
      53          739 :     tl_write_string(&w, cfg->lang_pack ? cfg->lang_pack : "");
      54          739 :     tl_write_string(&w, cfg->lang_code ? cfg->lang_code : "");
      55              : 
      56              :     /* Append the inner query */
      57          739 :     tl_write_raw(&w, query, qlen);
      58              : 
      59          739 :     if (w.len > max_len) {
      60            1 :         tl_writer_free(&w);
      61            1 :         return -1;
      62              :     }
      63              : 
      64          738 :     memcpy(out, w.data, w.len);
      65          738 :     *out_len = w.len;
      66          738 :     tl_writer_free(&w);
      67          738 :     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          866 : static int classify_service_frame(MtProtoSession *s,
      83              :                                    const uint8_t *resp, size_t resp_len) {
      84          866 :     if (resp_len < 4) return SVC_ERROR;
      85              :     uint32_t crc;
      86          866 :     memcpy(&crc, resp, 4);
      87              : 
      88          866 :     if (crc == TL_bad_server_salt) {
      89            5 :         if (resp_len < 28) return SVC_ERROR;
      90              :         uint64_t new_salt;
      91            5 :         memcpy(&new_salt, resp + 20, 8);
      92            5 :         s->server_salt = new_salt;
      93            5 :         logger_log(LOG_INFO,
      94              :                    "api_call: server issued new salt 0x%016llx (retry)",
      95              :                    (unsigned long long)new_salt);
      96            5 :         return SVC_BAD_SALT;
      97              :     }
      98              : 
      99          861 :     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            7 :         int32_t error_code = 0;
     104            7 :         if (resp_len >= 20) memcpy(&error_code, resp + 16, 4);
     105            7 :         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            7 :         return SVC_ERROR;
     108              :     }
     109              : 
     110          854 :     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            3 :         if (resp_len >= 28) {
     115              :             uint64_t fresh_salt;
     116            3 :             memcpy(&fresh_salt, resp + 20, 8);
     117            3 :             s->server_salt = fresh_salt;
     118            3 :             logger_log(LOG_INFO,
     119              :                        "api_call: new_session_created, salt=0x%016llx",
     120              :                        (unsigned long long)fresh_salt);
     121              :         }
     122            3 :         return SVC_SKIP;
     123              :     }
     124              : 
     125          851 :     if (crc == TL_msgs_ack || crc == TL_pong) {
     126          133 :         logger_log(LOG_DEBUG, "api_call: ignoring service frame 0x%08x", crc);
     127          133 :         return SVC_SKIP;
     128              :     }
     129              : 
     130          718 :     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          718 :     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          737 : 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          737 :     *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          737 :     RAII_STRING uint8_t *wrapped = (uint8_t *)malloc(API_BUF_SIZE);
     167          737 :     if (!wrapped) return -1;
     168          737 :     size_t wrapped_len = 0;
     169          737 :     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          737 :     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          737 :     RAII_STRING uint8_t *raw_resp = (uint8_t *)malloc(API_BUF_SIZE);
     181          737 :     if (!raw_resp) return -1;
     182          737 :     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          737 :     int saw_result = 0;
     191          873 :     for (int attempt = 0; attempt < SERVICE_FRAME_LIMIT; attempt++) {
     192          871 :         if (rpc_recv_encrypted(s, t, raw_resp, API_BUF_SIZE, &raw_len) != 0) {
     193            5 :             logger_log(LOG_ERROR, "api_call: failed to receive");
     194            5 :             return -1;
     195              :         }
     196          866 :         int klass = classify_service_frame(s, raw_resp, raw_len);
     197          866 :         if (klass == SVC_ERROR) return -1;
     198          859 :         if (klass == SVC_BAD_SALT) { *bad_salt = 1; return 0; }
     199          854 :         if (klass == SVC_SKIP)     continue;
     200          718 :         saw_result = 1;
     201          718 :         break; /* SVC_RESULT */
     202              :     }
     203          720 :     if (!saw_result) {
     204            2 :         logger_log(LOG_ERROR,
     205              :                    "api_call: drained %d service frames without a real result",
     206              :                    SERVICE_FRAME_LIMIT);
     207            2 :         return -1;
     208              :     }
     209              : 
     210          718 :     const uint8_t *payload = raw_resp;
     211          718 :     size_t payload_len = raw_len;
     212              : 
     213              :     /* If the server sent a msg_container, find the rpc_result inside it. */
     214          718 :     if (payload_len >= 4) {
     215              :         uint32_t frame_crc;
     216          718 :         memcpy(&frame_crc, payload, 4);
     217          718 :         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          718 :     if (rpc_unwrap_result(payload, payload_len, &req_msg_id,
     241              :                           &inner, &inner_len) == 0) {
     242          596 :         payload = inner;
     243          596 :         payload_len = inner_len;
     244              :     }
     245              : 
     246          718 :     if (rpc_unwrap_gzip(payload, payload_len,
     247              :                         resp, max_len, resp_len) != 0) {
     248            2 :         logger_log(LOG_ERROR, "api_call: failed to unwrap response");
     249            2 :         return -1;
     250              :     }
     251          716 :     return 0;
     252              : }
     253              : 
     254          732 : 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          732 :     if (!cfg || !s || !t || !query || !resp || !resp_len) return -1;
     259              : 
     260          732 :     int bad_salt = 0;
     261          732 :     int rc = api_call_once(cfg, s, t, query, qlen,
     262              :                             resp, max_len, resp_len, &bad_salt);
     263          732 :     if (rc != 0) return rc;
     264          716 :     if (!bad_salt) return 0;
     265              : 
     266              :     /* One-shot retry with the newly-received salt. */
     267            5 :     rc = api_call_once(cfg, s, t, query, qlen,
     268              :                         resp, max_len, resp_len, &bad_salt);
     269            5 :     if (rc != 0) return rc;
     270            5 :     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            5 :     return 0;
     276              : }
        

Generated by: LCOV version 2.0-1