LCOV - code coverage report
Current view: top level - src/app - auth_flow.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 24.1 % 145 35
Test Date: 2026-05-06 13:17:06 Functions: 60.0 % 5 3

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file app/auth_flow.c
       6              :  * @brief High-level login flow with DC migration.
       7              :  */
       8              : 
       9              : #include "app/auth_flow.h"
      10              : #include "app/dc_config.h"
      11              : #include "app/session_store.h"
      12              : 
      13              : #include "auth_session.h"
      14              : #include "infrastructure/auth_2fa.h"
      15              : #include "mtproto_auth.h"
      16              : #include "mtproto_rpc.h"
      17              : #include "logger.h"
      18              : 
      19              : #include <stdio.h>
      20              : #include <string.h>
      21              : 
      22              : /** Maximum redirects we follow before giving up. Telegram usually needs 1. */
      23              : #define AUTH_MAX_MIGRATIONS 3
      24              : 
      25              : /**
      26              :  * Connect to the physical endpoint of @p phys_dc but use @p dh_dc in the
      27              :  * DH p_q_inner_data_dc field.  Pass dh_dc=0 for the Telegram test-DC
      28              :  * wildcard (the test server rejects any non-zero value there).
      29              :  */
      30            8 : static int connect_dc_ex(int phys_dc, int dh_dc,
      31              :                          Transport *t, MtProtoSession *s) {
      32            8 :     const DcEndpoint *ep = dc_lookup(phys_dc);
      33            8 :     if (!ep) {
      34            3 :         logger_log(LOG_ERROR, "auth_flow: unknown DC id %d", phys_dc);
      35            3 :         return -1;
      36              :     }
      37              : 
      38            5 :     if (transport_connect(t, ep->host, ep->port) != 0) {
      39            2 :         logger_log(LOG_ERROR, "auth_flow: connect failed for DC%d (%s:%d)",
      40            2 :                    phys_dc, ep->host, ep->port);
      41            2 :         return -1;
      42              :     }
      43            3 :     t->dc_id = dh_dc;   /* DH uses this for p_q_inner_data_dc */
      44              : 
      45            3 :     if (mtproto_auth_key_gen(t, s) != 0) {
      46            3 :         logger_log(LOG_ERROR, "auth_flow: DH auth key gen failed on DC%d",
      47              :                    phys_dc);
      48            3 :         transport_close(t);
      49            3 :         return -1;
      50              :     }
      51            0 :     t->dc_id = phys_dc; /* restore physical DC id after DH */
      52            0 :     logger_log(LOG_INFO, "auth_flow: connected to DC%d, auth key ready", phys_dc);
      53            0 :     return 0;
      54              : }
      55              : 
      56           10 : int auth_flow_connect_dc(int dc_id, Transport *t, MtProtoSession *s) {
      57           10 :     if (!t || !s) return -1;
      58            6 :     return connect_dc_ex(dc_id, dc_id, t, s);
      59              : }
      60              : 
      61              : /** Full migration: tear down session+transport, reconnect, redo DH. */
      62            0 : static int migrate(int new_dc, int dh_dc, Transport *t, MtProtoSession *s) {
      63            0 :     logger_log(LOG_INFO, "auth_flow: migrating to DC%d (dh_dc=%d)",
      64              :                new_dc, dh_dc);
      65            0 :     transport_close(t);
      66            0 :     mtproto_session_init(s);
      67            0 :     return connect_dc_ex(new_dc, dh_dc, t, s);
      68              : }
      69              : 
      70              : /**
      71              :  * Soft migrate: reuse the existing auth key, just reconnect TCP to the
      72              :  * target DC endpoint and update t->dc_id.  Used when the server accepts
      73              :  * only dc=0 for DH (Telegram test DC) so a full re-DH would fail.
      74              :  */
      75            0 : static int migrate_soft(int new_dc, Transport *t) {
      76            0 :     logger_log(LOG_INFO, "auth_flow: soft-migrating to DC%d (keep auth key)",
      77              :                new_dc);
      78            0 :     transport_close(t);
      79            0 :     const DcEndpoint *ep = dc_lookup(new_dc);
      80            0 :     if (!ep) {
      81            0 :         logger_log(LOG_ERROR, "auth_flow: unknown DC id %d", new_dc);
      82            0 :         return -1;
      83              :     }
      84            0 :     if (transport_connect(t, ep->host, ep->port) != 0) {
      85            0 :         logger_log(LOG_ERROR, "auth_flow: connect failed for DC%d (%s:%d)",
      86            0 :                    new_dc, ep->host, ep->port);
      87            0 :         return -1;
      88              :     }
      89            0 :     t->dc_id = new_dc;
      90            0 :     return 0;
      91              : }
      92              : 
      93           16 : int auth_flow_login(const ApiConfig *cfg,
      94              :                     const AuthFlowCallbacks *cb,
      95              :                     Transport *t, MtProtoSession *s,
      96              :                     AuthFlowResult *out) {
      97           16 :     if (!cfg || !cb || !t || !s) return -1;
      98            8 :     if (!cb->get_phone || !cb->get_code) {
      99            4 :         logger_log(LOG_ERROR, "auth_flow: get_phone/get_code callbacks required");
     100            4 :         return -1;
     101              :     }
     102              : 
     103            4 :     if (out) memset(out, 0, sizeof(*out));
     104              : 
     105              :     /* Fast path: restore a persisted session. */
     106              :     {
     107            4 :         int saved_dc = 0;
     108            4 :         mtproto_session_init(s);
     109            4 :         if (session_store_load(s, &saved_dc) == 0 && s->has_auth_key) {
     110              :             /* New session_id per TCP connection; server resets seqno tracking. */
     111            2 :             mtproto_session_renew_id(s);
     112            2 :             const DcEndpoint *ep = dc_lookup(saved_dc);
     113            2 :             if (ep && transport_connect(t, ep->host, ep->port) == 0) {
     114            2 :                 t->dc_id = saved_dc;
     115            2 :                 logger_log(LOG_INFO,
     116              :                            "auth_flow: reusing persisted session on DC%d",
     117              :                            saved_dc);
     118            2 :                 if (out) { out->dc_id = saved_dc; out->user_id = 0; }
     119            2 :                 return 0;
     120              :             }
     121            0 :             logger_log(LOG_WARN,
     122              :                        "auth_flow: persisted session unusable, re-login");
     123            0 :             if (ep) transport_close(t);
     124            0 :             mtproto_session_init(s);
     125              :         }
     126              :     }
     127              : 
     128            2 :     int current_dc = cfg->start_dc_set ? cfg->start_dc : DEFAULT_DC_ID;
     129            2 :     if (connect_dc_ex(current_dc, current_dc, t, s) != 0) return -1;
     130              : 
     131              :     char phone[64];
     132            0 :     if (cb->get_phone(cb->user, phone, sizeof(phone)) != 0) {
     133            0 :         logger_log(LOG_ERROR, "auth_flow: phone number input failed");
     134            0 :         return -1;
     135              :     }
     136              : 
     137              :     /* ---- auth.sendCode (with migration) ---- */
     138            0 :     AuthSentCode sent = {0};
     139            0 :     int migrations = 0;
     140            0 :     for (;;) {
     141            0 :         RpcError err = {0};
     142            0 :         err.migrate_dc = -1;
     143            0 :         int rc = auth_send_code(cfg, s, t, phone, &sent, &err);
     144            0 :         if (rc == 0) break;
     145            0 :         if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
     146            0 :             migrations++;
     147            0 :             int target = (current_dc < 0) ? -err.migrate_dc : err.migrate_dc;
     148            0 :             if (migrate(target, target, t, s) == 0) {
     149            0 :                 current_dc = target;
     150            0 :                 continue;
     151              :             }
     152              :             /* Full re-DH failed (e.g. test DC only accepts dc=0).
     153              :              * Fall back: reuse existing auth key, just reconnect TCP. */
     154            0 :             logger_log(LOG_WARN,
     155              :                        "auth_flow: full migrate failed, trying soft migrate");
     156            0 :             if (migrate_soft(target, t) != 0) return -1;
     157            0 :             current_dc = target;
     158            0 :             continue;
     159              :         }
     160            0 :         logger_log(LOG_ERROR, "auth_flow: sendCode failed (%d: %s)",
     161              :                    err.error_code, err.error_msg);
     162            0 :         return -1;
     163              :     }
     164              : 
     165              :     /* If the code was routed to a Telegram app session (sentCodeTypeApp) but the
     166              :      * next delivery method is SMS, request a resend so the code arrives via SMS.
     167              :      * This is the standard path for real phone numbers that have no active
     168              :      * test-DC client to receive the app notification. */
     169            0 :     if (sent.code_type == CRC_auth_sentCodeTypeApp
     170            0 :             && sent.next_type == CRC_codeTypeSms) {
     171            0 :         logger_log(LOG_INFO,
     172              :                    "auth_flow: sentCodeTypeApp with SMS fallback — resending via SMS");
     173            0 :         RpcError rerr = {0};
     174            0 :         rerr.migrate_dc = -1;
     175            0 :         if (auth_resend_code(cfg, s, t, phone, &sent, &rerr) == 0) {
     176            0 :             logger_log(LOG_INFO, "auth_flow: code resent via SMS");
     177              :         } else {
     178            0 :             logger_log(LOG_WARN,
     179              :                        "auth_flow: resend failed (%d: %s); "
     180              :                        "code may still arrive in Telegram app",
     181              :                        rerr.error_code, rerr.error_msg);
     182              :         }
     183              :     }
     184              : 
     185              :     /* ---- user enters code ---- */
     186              :     char code[32];
     187            0 :     if (cb->get_code(cb->user, code, sizeof(code)) != 0) {
     188            0 :         logger_log(LOG_ERROR, "auth_flow: code input failed");
     189            0 :         return -1;
     190              :     }
     191              : 
     192              :     /* ---- auth.signIn (with migration; 2FA; sign-up for new accounts) ---- */
     193            0 :     int64_t uid = 0;
     194              :     for (;;) {
     195            0 :         RpcError err = {0};
     196            0 :         err.migrate_dc = -1;
     197            0 :         int signup_required = 0;
     198            0 :         int rc = auth_sign_in(cfg, s, t, phone, sent.phone_code_hash, code,
     199              :                               &uid, &signup_required, &err);
     200            0 :         if (rc == 0) break;
     201              : 
     202              :         /* New account: no existing record for this phone on this DC. */
     203            0 :         if (signup_required) {
     204            0 :             logger_log(LOG_INFO,
     205              :                        "auth_flow: new account — registering with signUp");
     206            0 :             RpcError su_err = {0};
     207            0 :             if (auth_sign_up(cfg, s, t, phone, sent.phone_code_hash,
     208              :                              "Test", "Account", &uid, &su_err) != 0) {
     209            0 :                 logger_log(LOG_ERROR,
     210              :                            "auth_flow: signUp failed (%d: %s)",
     211              :                            su_err.error_code, su_err.error_msg);
     212            0 :                 return -1;
     213              :             }
     214            0 :             break;
     215              :         }
     216              : 
     217            0 :         if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
     218            0 :             migrations++;
     219            0 :             int target = (current_dc < 0) ? -err.migrate_dc : err.migrate_dc;
     220            0 :             if (migrate(target, target, t, s) != 0) return -1;
     221            0 :             current_dc = target;
     222            0 :             logger_log(LOG_ERROR,
     223              :                        "auth_flow: unexpected migration during signIn — "
     224              :                        "restart login flow");
     225            0 :             return -1;
     226              :         }
     227            0 :         if (strcmp(err.error_msg, "SESSION_PASSWORD_NEEDED") == 0) {
     228            0 :             if (out) out->needs_password = 1;
     229            0 :             if (!cb->get_password) {
     230            0 :                 logger_log(LOG_ERROR,
     231              :                            "auth_flow: 2FA required but no get_password callback");
     232            0 :                 return -1;
     233              :             }
     234            0 :             logger_log(LOG_INFO,
     235              :                        "auth_flow: 2FA password required — running SRP flow");
     236              : 
     237            0 :             Account2faPassword params = {0};
     238            0 :             RpcError gp_err = {0};
     239            0 :             if (auth_2fa_get_password(cfg, s, t, &params, &gp_err) != 0) {
     240            0 :                 logger_log(LOG_ERROR,
     241              :                            "auth_flow: account.getPassword failed (%d: %s)",
     242              :                            gp_err.error_code, gp_err.error_msg);
     243            0 :                 return -1;
     244              :             }
     245            0 :             if (!params.has_password) {
     246            0 :                 logger_log(LOG_ERROR,
     247              :                            "auth_flow: SESSION_PASSWORD_NEEDED but server "
     248              :                            "reports no password configured");
     249            0 :                 return -1;
     250              :             }
     251              :             char pwd[128];
     252            0 :             if (cb->get_password(cb->user, pwd, sizeof(pwd)) != 0) {
     253            0 :                 logger_log(LOG_ERROR, "auth_flow: password input failed");
     254            0 :                 return -1;
     255              :             }
     256            0 :             RpcError cp_err = {0};
     257            0 :             int64_t cp_uid = 0;
     258            0 :             if (auth_2fa_check_password(cfg, s, t, &params, pwd,
     259              :                                            &cp_uid, &cp_err) != 0) {
     260            0 :                 logger_log(LOG_ERROR,
     261              :                            "auth_flow: auth.checkPassword failed (%d: %s)",
     262              :                            cp_err.error_code, cp_err.error_msg);
     263            0 :                 return -1;
     264              :             }
     265            0 :             uid = cp_uid;
     266            0 :             break;
     267              :         }
     268            0 :         logger_log(LOG_ERROR, "auth_flow: signIn failed (%d: %s)",
     269              :                    err.error_code, err.error_msg);
     270            0 :         return -1;
     271              :     }
     272              : 
     273            0 :     if (out) {
     274            0 :         out->dc_id = current_dc;
     275            0 :         out->user_id = uid;
     276              :     }
     277            0 :     logger_log(LOG_INFO, "auth_flow: login complete on DC%d, user_id=%lld",
     278              :                current_dc, (long long)uid);
     279              : 
     280              :     /* Persist so the next run can skip sendCode + signIn. */
     281            0 :     if (session_store_save(s, current_dc) != 0) {
     282            0 :         logger_log(LOG_WARN,
     283              :                    "auth_flow: failed to persist session (non-fatal)");
     284              :     }
     285            0 :     return 0;
     286              : }
        

Generated by: LCOV version 2.0-1