LCOV - code coverage report
Current view: top level - src/infrastructure - api_call.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 94.2 % 104 98
Test Date: 2026-04-20 19:54:22 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          578 : void api_config_init(ApiConfig *cfg) {
      20          578 :     if (!cfg) return;
      21          578 :     memset(cfg, 0, sizeof(*cfg));
      22          578 :     cfg->device_model = "tg-cli";
      23          578 :     cfg->system_version = "Linux";
      24          578 :     cfg->app_version = "0.1.0";
      25          578 :     cfg->system_lang_code = "en";
      26          578 :     cfg->lang_pack = "";
      27          578 :     cfg->lang_code = "en";
      28              : }
      29              : 
      30          751 : 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          751 :     if (!cfg || !query || !out || !out_len) return -1;
      34              : 
      35              :     TlWriter w;
      36          747 :     tl_writer_init(&w);
      37              : 
      38              :     /* invokeWithLayer#da9b0d0d layer:int query:!X = X */
      39          747 :     tl_write_uint32(&w, CRC_invokeWithLayer);
      40          747 :     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          747 :     tl_write_uint32(&w, CRC_initConnection);
      47          747 :     tl_write_int32(&w, 0);  /* flags = 0 (no proxy, no params) */
      48          747 :     tl_write_int32(&w, cfg->api_id);
      49          747 :     tl_write_string(&w, cfg->device_model ? cfg->device_model : "");
      50          747 :     tl_write_string(&w, cfg->system_version ? cfg->system_version : "");
      51          747 :     tl_write_string(&w, cfg->app_version ? cfg->app_version : "");
      52          747 :     tl_write_string(&w, cfg->system_lang_code ? cfg->system_lang_code : "");
      53          747 :     tl_write_string(&w, cfg->lang_pack ? cfg->lang_pack : "");
      54          747 :     tl_write_string(&w, cfg->lang_code ? cfg->lang_code : "");
      55              : 
      56              :     /* Append the inner query */
      57          747 :     tl_write_raw(&w, query, qlen);
      58              : 
      59          747 :     if (w.len > max_len) {
      60            1 :         tl_writer_free(&w);
      61            1 :         return -1;
      62              :     }
      63              : 
      64          746 :     memcpy(out, w.data, w.len);
      65          746 :     *out_len = w.len;
      66          746 :     tl_writer_free(&w);
      67          746 :     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          762 : static int classify_service_frame(MtProtoSession *s,
      83              :                                    const uint8_t *resp, size_t resp_len) {
      84          762 :     if (resp_len < 4) return SVC_ERROR;
      85              :     uint32_t crc;
      86          762 :     memcpy(&crc, resp, 4);
      87              : 
      88          762 :     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          757 :     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          750 :     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          747 :     if (crc == TL_msgs_ack || crc == TL_pong) {
     126           21 :         logger_log(LOG_DEBUG, "api_call: ignoring service frame 0x%08x", crc);
     127           21 :         return SVC_SKIP;
     128              :     }
     129              : 
     130          726 :     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          745 : 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          745 :     *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          745 :     RAII_STRING uint8_t *wrapped = (uint8_t *)malloc(API_BUF_SIZE);
     147          745 :     if (!wrapped) return -1;
     148          745 :     size_t wrapped_len = 0;
     149          745 :     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          745 :     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          745 :     RAII_STRING uint8_t *raw_resp = (uint8_t *)malloc(API_BUF_SIZE);
     161          745 :     if (!raw_resp) return -1;
     162          745 :     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          745 :     int saw_result = 0;
     171          769 :     for (int attempt = 0; attempt < SERVICE_FRAME_LIMIT; attempt++) {
     172          767 :         if (rpc_recv_encrypted(s, t, raw_resp, API_BUF_SIZE, &raw_len) != 0) {
     173            5 :             logger_log(LOG_ERROR, "api_call: failed to receive");
     174            5 :             return -1;
     175              :         }
     176          762 :         int klass = classify_service_frame(s, raw_resp, raw_len);
     177          762 :         if (klass == SVC_ERROR) return -1;
     178          755 :         if (klass == SVC_BAD_SALT) { *bad_salt = 1; return 0; }
     179          750 :         if (klass == SVC_SKIP)     continue;
     180          726 :         saw_result = 1;
     181          726 :         break; /* SVC_RESULT */
     182              :     }
     183          728 :     if (!saw_result) {
     184            2 :         logger_log(LOG_ERROR,
     185              :                    "api_call: drained %d service frames without a real result",
     186              :                    SERVICE_FRAME_LIMIT);
     187            2 :         return -1;
     188              :     }
     189              : 
     190              :     uint64_t req_msg_id;
     191              :     const uint8_t *inner;
     192              :     size_t inner_len;
     193          726 :     const uint8_t *payload = raw_resp;
     194          726 :     size_t payload_len = raw_len;
     195          726 :     if (rpc_unwrap_result(raw_resp, raw_len, &req_msg_id,
     196              :                           &inner, &inner_len) == 0) {
     197          604 :         payload = inner;
     198          604 :         payload_len = inner_len;
     199              :     }
     200              : 
     201          726 :     if (rpc_unwrap_gzip(payload, payload_len,
     202              :                         resp, max_len, resp_len) != 0) {
     203            2 :         logger_log(LOG_ERROR, "api_call: failed to unwrap response");
     204            2 :         return -1;
     205              :     }
     206          724 :     return 0;
     207              : }
     208              : 
     209          740 : 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          740 :     if (!cfg || !s || !t || !query || !resp || !resp_len) return -1;
     214              : 
     215          740 :     int bad_salt = 0;
     216          740 :     int rc = api_call_once(cfg, s, t, query, qlen,
     217              :                             resp, max_len, resp_len, &bad_salt);
     218          740 :     if (rc != 0) return rc;
     219          724 :     if (!bad_salt) return 0;
     220              : 
     221              :     /* One-shot retry with the newly-received salt. */
     222            5 :     rc = api_call_once(cfg, s, t, query, qlen,
     223              :                         resp, max_len, resp_len, &bad_salt);
     224            5 :     if (rc != 0) return rc;
     225            5 :     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            5 :     return 0;
     231              : }
        

Generated by: LCOV version 2.0-1