2021-08-04 00:40:09 +02:00
using System ;
2021-08-06 01:54:29 +02:00
using System.Buffers.Binary ;
2021-08-04 00:40:09 +02:00
using System.Collections.Generic ;
2021-12-05 07:21:30 +01:00
using System.ComponentModel ;
2021-08-04 00:40:09 +02:00
using System.Globalization ;
using System.IO ;
2022-01-17 15:06:29 +01:00
using System.IO.Compression ;
2021-08-04 00:40:09 +02:00
using System.Linq ;
using System.Net ;
using System.Net.Sockets ;
using System.Reflection ;
using System.Security.Cryptography ;
2021-08-13 00:28:34 +02:00
using System.Threading ;
2021-08-04 00:40:09 +02:00
using System.Threading.Tasks ;
2022-01-03 18:15:32 +01:00
using System.Web ;
2021-08-04 00:40:09 +02:00
using TL ;
using static WTelegram . Encryption ;
2022-10-12 23:25:15 +02:00
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
2021-08-04 00:40:09 +02:00
namespace WTelegram
{
2022-04-06 18:38:54 +02:00
public partial class Client : IDisposable
2021-08-04 00:40:09 +02:00
{
2022-06-14 00:58:51 +02:00
/// <summary>This event will be called when unsollicited updates/messages are sent by Telegram servers</summary>
2022-09-02 23:02:44 +02:00
/// <remarks>Make your handler <see langword="async"/>, or return <see cref="Task.CompletedTask"/> or <see langword="null"/><br/>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs">Examples/Program_ListenUpdate.cs</see> for how to use this</remarks>
2022-07-29 15:24:18 +02:00
public event Func < IObject , Task > OnUpdate ;
2021-11-06 05:22:33 +01:00
/// <summary>Used to create a TcpClient connected to the given address/port, or throw an exception on failure</summary>
2022-01-13 14:22:52 +01:00
public TcpFactory TcpHandler { get ; set ; } = DefaultTcpHandler ;
2022-02-11 02:43:48 +01:00
public delegate Task < TcpClient > TcpFactory ( string host , int port ) ;
2022-01-13 14:22:52 +01:00
/// <summary>Url for using a MTProxy. https://t.me/proxy?server=... </summary>
public string MTProxyUrl { get ; set ; }
2021-11-06 05:22:33 +01:00
/// <summary>Telegram configuration, obtained at connection time</summary>
2021-08-04 00:40:09 +02:00
public Config TLConfig { get ; private set ; }
2021-11-06 05:22:33 +01:00
/// <summary>Number of automatic reconnections on connection/reactor failure</summary>
public int MaxAutoReconnects { get ; set ; } = 5 ;
2022-05-31 14:28:37 +02:00
/// <summary>Number of attempts in case of wrong verification_code or password</summary>
public int MaxCodePwdAttempts { get ; set ; } = 3 ;
2021-11-12 20:50:39 +01:00
/// <summary>Number of seconds under which an error 420 FLOOD_WAIT_X will not be raised and your request will instead be auto-retried after the delay</summary>
public int FloodRetryThreshold { get ; set ; } = 60 ;
2022-02-13 02:50:10 +01:00
/// <summary>Number of seconds between each keep-alive ping. Increase this if you have a slow connection or you're debugging your code</summary>
2021-12-07 15:26:53 +01:00
public int PingInterval { get ; set ; } = 60 ;
2022-02-24 16:44:27 +01:00
/// <summary>Size of chunks when uploading/downloading files. Reduce this if you don't have much memory</summary>
public int FilePartSize { get ; set ; } = 512 * 1024 ;
2021-11-06 05:22:33 +01:00
/// <summary>Is this Client instance the main or a secondary DC session</summary>
2021-09-28 16:12:20 +02:00
public bool IsMainDC = > ( _dcSession ? . DataCenter ? . id ? ? 0 ) = = _session . MainDC ;
2021-12-01 15:50:35 +01:00
/// <summary>Has this Client established connection been disconnected?</summary>
2021-10-01 02:44:56 +02:00
public bool Disconnected = > _tcpClient ! = null & & ! ( _tcpClient . Client ? . Connected ? ? false ) ;
2022-03-23 13:50:43 +01:00
/// <summary>ID of the current logged-in user or 0</summary>
public long UserId = > _session . UserId ;
2022-09-20 17:30:32 +02:00
/// <summary>Info about the current logged-in user</summary>
public User User { get ; private set ; }
2022-01-03 18:15:32 +01:00
2022-09-20 17:30:32 +02:00
private Func < string , string > _config ;
2021-08-04 00:40:09 +02:00
private readonly Session _session ;
2022-01-25 16:37:09 +01:00
private string _apiHash ;
2021-09-28 16:12:20 +02:00
private Session . DCSession _dcSession ;
2021-08-04 00:40:09 +02:00
private TcpClient _tcpClient ;
2022-01-11 04:14:23 +01:00
private Stream _networkStream ;
2021-11-07 16:52:58 +01:00
private IObject _lastSentMsg ;
2021-08-14 15:15:41 +02:00
private long _lastRecvMsgId ;
2021-08-04 00:40:09 +02:00
private readonly List < long > _msgsToAck = new ( ) ;
2021-08-10 14:40:41 +02:00
private readonly Random _random = new ( ) ;
2021-09-24 13:21:35 +02:00
private int _saltChangeCounter ;
2021-08-13 00:28:34 +02:00
private Task _reactorTask ;
2022-03-27 22:29:48 +02:00
private Rpc _bareRpc ;
private readonly Dictionary < long , Rpc > _pendingRpcs = new ( ) ;
2021-09-16 04:47:15 +02:00
private SemaphoreSlim _sendSemaphore = new ( 0 ) ;
2021-09-28 16:12:20 +02:00
private readonly SemaphoreSlim _semaphore = new ( 1 ) ;
private Task _connecting ;
2021-08-13 00:28:34 +02:00
private CancellationTokenSource _cts ;
2021-09-18 07:26:06 +02:00
private int _reactorReconnects = 0 ;
2021-11-21 00:43:39 +01:00
private const string ConnectionShutDown = "Could not read payload length : Connection shut down" ;
2021-10-06 07:54:20 +02:00
private readonly SemaphoreSlim _parallelTransfers = new ( 10 ) ; // max parallel part uploads/downloads
2021-09-29 04:38:39 +02:00
private readonly SHA256 _sha256 = SHA256 . Create ( ) ;
private readonly SHA256 _sha256Recv = SHA256 . Create ( ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
private AesCtr _sendCtr , _recvCtr ;
#endif
private bool _paddedMode ;
2021-08-13 00:28:34 +02:00
2022-09-20 17:30:32 +02:00
public Client ( int apiID , string apiHash , string sessionPathname = null )
: this ( what = > what switch
{
"api_id" = > apiID . ToString ( ) ,
"api_hash" = > apiHash ,
"session_pathname" = > sessionPathname ,
_ = > null
} )
{ }
2021-11-06 05:22:33 +01:00
/// <summary>Welcome to WTelegramClient! 🙂</summary>
/// <param name="configProvider">Config callback, is queried for: <b>api_id</b>, <b>api_hash</b>, <b>session_pathname</b></param>
2022-01-26 22:00:52 +01:00
/// <param name="sessionStore">if specified, must support initial Length & Read() of a session, then calls to Write() the updated session. Other calls can be ignored</param>
public Client ( Func < string , string > configProvider = null , Stream sessionStore = null )
2021-08-04 00:40:09 +02:00
{
_config = configProvider ? ? DefaultConfigOrAsk ;
2022-01-26 22:00:52 +01:00
sessionStore ? ? = new SessionStore ( Config ( "session_pathname" ) ) ;
2022-01-25 16:37:09 +01:00
var session_key = _config ( "session_key" ) ? ? ( _apiHash = Config ( "api_hash" ) ) ;
2022-01-26 22:00:52 +01:00
_session = Session . LoadOrCreate ( sessionStore , Convert . FromHexString ( session_key ) ) ;
2022-01-25 16:37:09 +01:00
if ( _session . ApiId = = 0 ) _session . ApiId = int . Parse ( Config ( "api_id" ) ) ;
2021-09-28 16:12:20 +02:00
if ( _session . MainDC ! = 0 ) _session . DCSessions . TryGetValue ( _session . MainDC , out _dcSession ) ;
_dcSession ? ? = new ( ) { Id = Helpers . RandomLong ( ) } ;
_dcSession . Client = this ;
2021-11-04 03:10:03 +01:00
var version = Assembly . GetExecutingAssembly ( ) . GetCustomAttribute < AssemblyInformationalVersionAttribute > ( ) . InformationalVersion ;
Helpers . Log ( 1 , $"WTelegramClient {version[..version.IndexOf('+')]} running under {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" ) ;
2021-09-28 16:12:20 +02:00
}
private Client ( Client cloneOf , Session . DCSession dcSession )
{
_config = cloneOf . _config ;
_session = cloneOf . _session ;
2022-01-13 14:22:52 +01:00
TcpHandler = cloneOf . TcpHandler ;
MTProxyUrl = cloneOf . MTProxyUrl ;
PingInterval = cloneOf . PingInterval ;
2022-09-11 15:34:38 +02:00
TLConfig = cloneOf . TLConfig ;
2021-09-28 16:12:20 +02:00
_dcSession = dcSession ;
2021-08-04 00:40:09 +02:00
}
2021-12-05 11:47:52 +01:00
internal Task < string > ConfigAsync ( string what ) = > Task . Run ( ( ) = > Config ( what ) ) ;
internal string Config ( string what )
= > _config ( what ) ? ? DefaultConfig ( what ) ? ? throw new ApplicationException ( "You must provide a config value for " + what ) ;
2021-08-04 12:03:43 +02:00
2021-11-06 05:22:33 +01:00
/// <summary>Default config values, used if your Config callback returns <see langword="null"/></summary>
2021-12-05 11:47:52 +01:00
public static string DefaultConfig ( string what ) = > what switch
2021-08-04 00:40:09 +02:00
{
"session_pathname" = > Path . Combine (
2022-01-07 00:24:47 +01:00
Path . GetDirectoryName ( Path . GetDirectoryName ( AppDomain . CurrentDomain . BaseDirectory . TrimEnd ( Path . DirectorySeparatorChar ) ) )
? ? AppDomain . CurrentDomain . BaseDirectory , "WTelegram.session" ) ,
2021-08-04 00:40:09 +02:00
#if DEBUG
2022-09-19 22:28:12 +02:00
"server_address" = > "149.154.167.40:443" , // Test DC 2
2021-08-04 00:40:09 +02:00
#else
2022-01-23 01:28:10 +01:00
"server_address" = > "149.154.167.50:443" , // DC 2
2021-08-04 00:40:09 +02:00
#endif
"device_model" = > Environment . Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit" ,
2022-01-25 16:37:09 +01:00
"system_version" = > Helpers . GetSystemVersion ( ) ,
"app_version" = > Helpers . GetAppVersion ( ) ,
2021-08-04 00:40:09 +02:00
"system_lang_code" = > CultureInfo . InstalledUICulture . TwoLetterISOLanguageName ,
"lang_pack" = > "" ,
"lang_code" = > CultureInfo . CurrentUICulture . TwoLetterISOLanguageName ,
2021-09-02 00:39:06 +02:00
"user_id" = > "-1" ,
2022-09-14 18:29:07 +02:00
"verification_code" or "email_verification_code" or "password" = > AskConfig ( what ) ,
2021-10-11 14:44:49 +02:00
_ = > null // api_id api_hash phone_number... it's up to you to reply to these correctly
2021-08-04 00:40:09 +02:00
} ;
2021-11-06 05:22:33 +01:00
internal static string DefaultConfigOrAsk ( string config ) = > DefaultConfig ( config ) ? ? AskConfig ( config ) ;
2021-10-11 14:44:49 +02:00
private static string AskConfig ( string config )
2021-08-04 00:40:09 +02:00
{
2022-01-29 16:47:47 +01:00
if ( config = = "session_key" )
{
Console . WriteLine ( "Welcome! You can obtain your api_id/api_hash at https://my.telegram.org/apps" ) ;
return null ;
}
2021-08-04 00:40:09 +02:00
Console . Write ( $"Enter {config.Replace('_', ' ')}: " ) ;
return Console . ReadLine ( ) ;
}
2021-11-06 05:22:33 +01:00
/// <summary>Load a specific Telegram server public key</summary>
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
public static void LoadPublicKey ( string pem ) = > Encryption . LoadPublicKey ( pem ) ;
2021-08-04 00:40:09 +02:00
2021-12-14 13:39:43 +01:00
/// <summary>Builds a structure that is used to validate a 2FA password</summary>
2022-07-29 15:24:18 +02:00
/// <param name="accountPassword">Password validation configuration. You can obtain this via <c>Account_GetPassword</c> or through OnUpdate as part of the login process</param>
2021-12-14 13:39:43 +01:00
/// <param name="password">The password to validate</param>
public static Task < InputCheckPasswordSRP > InputCheckPassword ( Account_Password accountPassword , string password )
= > Check2FA ( accountPassword , ( ) = > Task . FromResult ( password ) ) ;
2021-08-13 00:28:34 +02:00
public void Dispose ( )
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 2 , $"{_dcSession.DcID}>Disposing the client" ) ;
2021-09-29 02:51:48 +02:00
Reset ( false , IsMainDC ) ;
2022-01-17 16:54:34 +01:00
_networkStream = null ;
2022-01-23 01:28:10 +01:00
if ( IsMainDC ) _session . Dispose ( ) ;
2021-12-10 14:19:28 +01:00
GC . SuppressFinalize ( this ) ;
2021-08-13 00:28:34 +02:00
}
2022-07-12 01:31:18 +02:00
public void DisableUpdates ( bool disable = true ) = > _dcSession . DisableUpdates ( disable ) ;
2021-11-06 05:22:33 +01:00
/// <summary>Disconnect from Telegram <i>(shouldn't be needed in normal usage)</i></summary>
/// <param name="resetUser">Forget about logged-in user</param>
/// <param name="resetSessions">Disconnect secondary sessions with other DCs</param>
2021-09-29 02:51:48 +02:00
public void Reset ( bool resetUser = true , bool resetSessions = true )
2021-08-06 07:28:54 +02:00
{
2021-10-01 02:22:26 +02:00
try
{
if ( CheckMsgsToAck ( ) is MsgsAck msgsAck )
2021-11-07 16:50:59 +01:00
SendAsync ( msgsAck , false ) . Wait ( 1000 ) ;
2021-10-01 02:22:26 +02:00
}
2022-05-09 23:43:06 +02:00
catch { }
2021-08-13 00:28:34 +02:00
_cts ? . Cancel ( ) ;
2022-09-19 22:28:12 +02:00
_sendSemaphore = new ( 0 ) ; // initially taken, first released during DoConnectAsync
2022-05-09 23:43:06 +02:00
try
{
_reactorTask ? . Wait ( 1000 ) ;
}
catch { }
2021-08-13 00:28:34 +02:00
_reactorTask = null ;
2022-01-11 04:42:41 +01:00
_networkStream ? . Close ( ) ;
2021-08-13 00:28:34 +02:00
_tcpClient ? . Dispose ( ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_sendCtr ? . Dispose ( ) ;
_recvCtr ? . Dispose ( ) ;
#endif
_paddedMode = false ;
2021-09-28 16:12:20 +02:00
_connecting = null ;
2022-08-29 02:37:30 +02:00
_bareRpc = null ;
2021-09-23 09:27:52 +02:00
if ( resetSessions )
{
2021-09-28 16:12:20 +02:00
foreach ( var altSession in _session . DCSessions . Values )
if ( altSession . Client ! = null & & altSession . Client ! = this )
{
altSession . Client . Dispose ( ) ;
altSession . Client = null ;
}
2021-09-23 09:27:52 +02:00
}
2021-09-29 02:51:48 +02:00
if ( resetUser )
2022-09-20 17:30:32 +02:00
{
_loginCfg = default ;
2021-11-07 09:09:15 +01:00
_session . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
}
2021-08-06 07:28:54 +02:00
}
2022-04-06 18:38:54 +02:00
private Session . DCSession GetOrCreateDCSession ( int dcId , DcOption . Flags flags )
2021-08-04 00:40:09 +02:00
{
2022-04-06 18:38:54 +02:00
if ( _session . DCSessions . TryGetValue ( dcId , out var dcSession ) )
if ( dcSession . Client ! = null | | dcSession . DataCenter . flags = = flags )
return dcSession ; // if we have already a session with this DC and we are connected or it is a perfect match, use it
// try to find the most appropriate DcOption for this DC
if ( ( dcSession ? . AuthKeyID ? ? 0 ) = = 0 ) // we will need to negociate an AuthKey => can't use media_only DC
flags & = ~ DcOption . Flags . media_only ;
var dcOptions = _session . DcOptions . Where ( dc = > dc . id = = dcId ) . OrderBy ( dc = > dc . flags ^ flags ) ;
var dcOption = dcOptions . FirstOrDefault ( ) ? ? throw new ApplicationException ( $"Could not find adequate dc_option for DC {dcId}" ) ;
dcSession ? ? = new Session . DCSession { Id = Helpers . RandomLong ( ) } ; // create new session only if not already existing
dcSession . DataCenter = dcOption ;
return _session . DCSessions [ dcId ] = dcSession ;
2021-08-04 00:40:09 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Obtain/create a Client for a secondary session on a specific Data Center</summary>
/// <param name="dcId">ID of the Data Center</param>
/// <param name="media_only">Session will be used only for transferring media</param>
/// <param name="connect">Connect immediately</param>
2022-04-06 18:38:54 +02:00
/// <returns>Client connected to the selected DC</returns>
2021-10-01 02:22:26 +02:00
public async Task < Client > GetClientForDC ( int dcId , bool media_only = true , bool connect = true )
2021-08-04 00:40:09 +02:00
{
2021-09-28 16:12:20 +02:00
if ( _dcSession . DataCenter ? . id = = dcId ) return this ;
Session . DCSession altSession ;
lock ( _session )
{
2021-10-01 02:22:26 +02:00
altSession = GetOrCreateDCSession ( dcId , _dcSession . DataCenter . flags | ( media_only ? DcOption . Flags . media_only : 0 ) ) ;
2021-10-01 02:44:56 +02:00
if ( altSession . Client ? . Disconnected ? ? false ) { altSession . Client . Dispose ( ) ; altSession . Client = null ; }
2021-09-28 16:12:20 +02:00
altSession . Client ? ? = new Client ( this , altSession ) ;
}
Helpers . Log ( 2 , $"Requested connection to DC {dcId}..." ) ;
if ( connect )
{
await _semaphore . WaitAsync ( ) ;
try
{
Auth_ExportedAuthorization exported = null ;
2021-11-07 09:09:15 +01:00
if ( _session . UserId ! = 0 & & IsMainDC & & altSession . UserId ! = _session . UserId )
2021-09-28 16:12:20 +02:00
exported = await this . Auth_ExportAuthorization ( dcId ) ;
await altSession . Client . ConnectAsync ( ) ;
if ( exported ! = null )
{
var authorization = await altSession . Client . Auth_ImportAuthorization ( exported . id , exported . bytes ) ;
if ( authorization is not Auth_Authorization { user : User user } )
throw new ApplicationException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
altSession . UserId = user . id ;
}
}
finally
{
_semaphore . Release ( ) ;
}
}
return altSession . Client ;
}
2022-01-11 04:14:23 +01:00
private async Task Reactor ( Stream stream , CancellationTokenSource cts )
2021-09-17 03:12:23 +02:00
{
2021-09-23 05:37:00 +02:00
const int MinBufferSize = 1024 ;
var data = new byte [ MinBufferSize ] ;
2021-09-17 03:12:23 +02:00
while ( ! cts . IsCancellationRequested )
{
2021-11-07 16:52:58 +01:00
IObject obj = null ;
2021-09-16 04:47:15 +02:00
try
2021-08-13 00:28:34 +02:00
{
2022-01-11 04:14:23 +01:00
if ( await stream . FullReadAsync ( data , 4 , cts . Token ) ! = 4 )
2021-11-21 00:43:39 +01:00
throw new ApplicationException ( ConnectionShutDown ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_recvCtr . EncryptDecrypt ( data , 4 ) ;
#endif
2021-09-23 05:37:00 +02:00
int payloadLen = BinaryPrimitives . ReadInt32LittleEndian ( data ) ;
2022-01-11 04:14:23 +01:00
if ( payloadLen < = 0 )
throw new ApplicationException ( "Could not read frame data : Invalid payload length" ) ;
else if ( payloadLen > data . Length )
2021-09-23 05:37:00 +02:00
data = new byte [ payloadLen ] ;
else if ( Math . Max ( payloadLen , MinBufferSize ) < data . Length / 4 )
data = new byte [ Math . Max ( payloadLen , MinBufferSize ) ] ;
2022-01-11 04:14:23 +01:00
if ( await stream . FullReadAsync ( data , payloadLen , cts . Token ) ! = payloadLen )
2021-09-23 05:37:00 +02:00
throw new ApplicationException ( "Could not read frame data : Connection shut down" ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_recvCtr . EncryptDecrypt ( data , payloadLen ) ;
#endif
2021-09-23 05:37:00 +02:00
obj = ReadFrame ( data , payloadLen ) ;
2021-08-13 00:28:34 +02:00
}
2021-09-16 04:47:15 +02:00
catch ( Exception ex ) // an exception in RecvAsync is always fatal
2021-08-13 00:28:34 +02:00
{
2021-09-17 03:12:23 +02:00
if ( cts . IsCancellationRequested ) return ;
2021-11-21 00:43:39 +01:00
Helpers . Log ( 5 , $"{_dcSession.DcID}>An exception occured in the reactor: {ex}" ) ;
2021-09-16 04:47:15 +02:00
var oldSemaphore = _sendSemaphore ;
2021-09-17 03:12:23 +02:00
await oldSemaphore . WaitAsync ( cts . Token ) ; // prevent any sending while we reconnect
2021-09-28 16:12:20 +02:00
var reactorError = new ReactorError { Exception = ex } ;
2021-09-16 04:47:15 +02:00
try
2021-09-28 16:12:20 +02:00
{
2021-11-21 00:43:39 +01:00
lock ( _msgsToAck ) _msgsToAck . Clear ( ) ;
Reset ( false , false ) ;
2021-09-28 16:12:20 +02:00
_reactorReconnects = ( _reactorReconnects + 1 ) % MaxAutoReconnects ;
2022-03-27 22:29:48 +02:00
if ( ! IsMainDC & & _pendingRpcs . Count < = 1 & & ex is ApplicationException { Message : ConnectionShutDown } or IOException { InnerException : SocketException } )
if ( _pendingRpcs . Values . FirstOrDefault ( ) is not Rpc rpc | | rpc . type = = typeof ( Pong ) )
2021-11-21 00:43:39 +01:00
_reactorReconnects = 0 ;
2021-09-28 16:12:20 +02:00
if ( _reactorReconnects ! = 0 )
{
await Task . Delay ( 5000 ) ;
2022-01-17 16:54:34 +01:00
if ( _networkStream = = null ) return ; // Dispose has been called in-between
2021-09-28 16:12:20 +02:00
await ConnectAsync ( ) ; // start a new reactor after 5 secs
2022-03-27 22:29:48 +02:00
lock ( _pendingRpcs ) // retry all pending requests
2021-09-28 16:12:20 +02:00
{
2022-03-27 22:29:48 +02:00
foreach ( var rpc in _pendingRpcs . Values )
2022-07-29 02:19:25 +02:00
rpc . tcs . SetResult ( reactorError ) ; // this leads to a retry (see Invoke<T> method)
2022-03-27 22:29:48 +02:00
_pendingRpcs . Clear ( ) ;
_bareRpc = null ;
2021-09-28 16:12:20 +02:00
}
2021-11-07 09:09:15 +01:00
// TODO: implement an Updates gaps handling system? https://core.telegram.org/api/updates
2021-11-12 20:50:39 +01:00
if ( IsMainDC )
{
2022-06-14 00:58:51 +02:00
var updatesState = await this . Updates_GetState ( ) ; // this call reenables incoming Updates
2022-07-29 15:24:18 +02:00
RaiseUpdate ( updatesState ) ;
2021-11-12 20:50:39 +01:00
}
2021-09-28 16:12:20 +02:00
}
else
throw ;
}
catch
2021-09-16 04:47:15 +02:00
{
2022-07-29 15:24:18 +02:00
RaiseUpdate ( reactorError ) ;
2022-03-27 22:29:48 +02:00
lock ( _pendingRpcs ) // abort all pending requests
2021-09-16 04:47:15 +02:00
{
2022-03-27 22:29:48 +02:00
foreach ( var rpc in _pendingRpcs . Values )
rpc . tcs . SetException ( ex ) ;
_pendingRpcs . Clear ( ) ;
_bareRpc = null ;
2021-09-16 04:47:15 +02:00
}
}
finally
{
oldSemaphore . Release ( ) ;
}
2021-08-13 00:28:34 +02:00
}
2021-09-16 04:47:15 +02:00
if ( obj ! = null )
await HandleMessageAsync ( obj ) ;
2021-08-13 07:06:44 +02:00
}
}
2022-04-06 18:38:54 +02:00
internal DateTime MsgIdToStamp ( long serverMsgId )
= > new ( ( serverMsgId > > 32 ) * 10000000 - _dcSession . ServerTicksOffset + 621355968000000000L , DateTimeKind . Utc ) ;
2021-11-07 16:52:58 +01:00
internal IObject ReadFrame ( byte [ ] data , int dataLen )
2021-08-04 00:40:09 +02:00
{
2022-06-19 18:08:19 +02:00
if ( dataLen < 8 & & data [ 3 ] = = 0xFF )
2021-08-06 07:28:54 +02:00
{
int error_code = - BinaryPrimitives . ReadInt32LittleEndian ( data ) ;
throw new RpcException ( error_code , TransportError ( error_code ) ) ;
}
2021-09-23 05:37:00 +02:00
if ( dataLen < 24 ) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
throw new ApplicationException ( $"Packet payload too small: {dataLen}" ) ;
2021-08-04 00:40:09 +02:00
2021-08-06 01:54:29 +02:00
long authKeyId = BinaryPrimitives . ReadInt64LittleEndian ( data ) ;
2021-09-28 16:12:20 +02:00
if ( authKeyId ! = _dcSession . AuthKeyID )
2021-08-20 02:13:58 +02:00
throw new ApplicationException ( $"Received a packet encrypted with unexpected key {authKeyId:X}" ) ;
2021-08-04 00:40:09 +02:00
if ( authKeyId = = 0 ) // Unencrypted message
{
2021-09-23 05:37:00 +02:00
using var reader = new TL . BinaryReader ( new MemoryStream ( data , 8 , dataLen - 8 ) , this ) ;
2021-08-14 15:15:41 +02:00
long msgId = _lastRecvMsgId = reader . ReadInt64 ( ) ;
2021-08-04 00:40:09 +02:00
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"Invalid server msgId {msgId}" ) ;
int length = reader . ReadInt32 ( ) ;
2022-01-03 18:15:32 +01:00
dataLen - = 20 ;
if ( length > dataLen | | length < dataLen - ( _paddedMode ? 15 : 0 ) )
2022-08-30 15:15:43 +02:00
throw new ApplicationException ( $"Unexpected unencrypted/padding length {dataLen} - {length}" ) ;
2021-08-04 00:40:09 +02:00
2021-08-12 12:37:56 +02:00
var obj = reader . ReadTLObject ( ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {MsgIdToStamp(msgId):u} clear{((msgId & 2) == 0 ? "" : " NAR ")}" ) ;
2022-08-30 15:15:43 +02:00
if ( _bareRpc = = null ) throw new ApplicationException ( "Shouldn't receive unencrypted packet at this point" ) ;
2021-08-12 12:37:56 +02:00
return obj ;
2021-08-04 00:40:09 +02:00
}
else
{
2022-10-01 13:56:43 +02:00
byte [ ] decrypted_data = EncryptDecryptMessage ( data . AsSpan ( 24 , ( dataLen - 24 ) & ~ 0xF ) , false , 8 , _dcSession . AuthKey , data , 8 , _sha256Recv ) ;
2021-08-04 00:40:09 +02:00
if ( decrypted_data . Length < 36 ) // header below+ctorNb
throw new ApplicationException ( $"Decrypted packet too small: {decrypted_data.Length}" ) ;
2022-05-19 01:32:22 +02:00
_sha256Recv . TransformBlock ( _dcSession . AuthKey , 96 , 32 , null , 0 ) ;
_sha256Recv . TransformFinalBlock ( decrypted_data , 0 , decrypted_data . Length ) ;
if ( ! data . AsSpan ( 8 , 16 ) . SequenceEqual ( _sha256Recv . Hash . AsSpan ( 8 , 16 ) ) )
2022-08-30 15:15:43 +02:00
throw new ApplicationException ( "Mismatch between MsgKey & decrypted SHA256" ) ;
2022-05-19 01:32:22 +02:00
_sha256Recv . Initialize ( ) ;
2021-08-30 01:31:08 +02:00
using var reader = new TL . BinaryReader ( new MemoryStream ( decrypted_data ) , this ) ;
2021-12-30 23:45:28 +01:00
var serverSalt = reader . ReadInt64 ( ) ; // int64 salt
var sessionId = reader . ReadInt64 ( ) ; // int64 session_id
var msgId = reader . ReadInt64 ( ) ; // int64 message_id
var seqno = reader . ReadInt32 ( ) ; // int32 msg_seqno
var length = reader . ReadInt32 ( ) ; // int32 message_data_length
2022-09-19 22:28:12 +02:00
2022-05-19 01:32:22 +02:00
if ( length < 0 | | length % 4 ! = 0 ) throw new ApplicationException ( $"Invalid message_data_length: {length}" ) ;
if ( decrypted_data . Length - 32 - length is < 12 or > 1024 ) throw new ApplicationException ( $"Invalid message padding length: {decrypted_data.Length - 32}-{length}" ) ;
if ( sessionId ! = _dcSession . Id ) throw new ApplicationException ( $"Unexpected session ID: {sessionId} != {_dcSession.Id}" ) ;
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"msg_id is not odd: {msgId}" ) ;
if ( ! _dcSession . CheckNewMsgId ( msgId ) )
{
Helpers . Log ( 3 , $"{_dcSession.DcID}>Ignoring duplicate or old msg_id {msgId}" ) ;
return null ;
}
2021-12-30 23:45:28 +01:00
if ( _lastRecvMsgId = = 0 ) // resync ServerTicksOffset on first message
_dcSession . ServerTicksOffset = ( msgId > > 32 ) * 10000000 - DateTime . UtcNow . Ticks + 621355968000000000L ;
var msgStamp = MsgIdToStamp ( _lastRecvMsgId = msgId ) ;
2021-08-04 00:40:09 +02:00
2021-09-28 16:12:20 +02:00
if ( serverSalt ! = _dcSession . Salt ) // salt change happens every 30 min
2021-08-05 16:29:58 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 2 , $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}" ) ;
_dcSession . Salt = serverSalt ;
2022-09-24 15:34:31 +02:00
_saltChangeCounter + = 1200 ; // counter is decreased by KeepAlive (we have margin of 10 min)
if ( _saltChangeCounter > = 1800 )
2022-08-30 15:15:43 +02:00
throw new ApplicationException ( "Server salt changed too often! Security issue?" ) ;
2021-08-05 16:29:58 +02:00
}
2021-09-16 04:47:15 +02:00
if ( ( seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msgId ) ;
2022-02-11 02:43:48 +01:00
2021-08-13 07:06:44 +02:00
var ctorNb = reader . ReadUInt32 ( ) ;
2021-12-30 23:45:28 +01:00
if ( ctorNb ! = Layer . BadMsgCtor & & ( msgStamp - DateTime . UtcNow ) . Ticks / TimeSpan . TicksPerSecond is > 30 or < - 300 )
2022-04-06 18:38:54 +02:00
{ // msg_id values that belong over 30 seconds in the future or over 300 seconds in the past are to be ignored.
2021-12-30 23:45:28 +01:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Ignoring 0x{ctorNb:X8} because of wrong timestamp {msgStamp:u} (svc)" ) ;
return null ;
}
2021-09-17 04:53:02 +02:00
if ( ctorNb = = Layer . MsgContainerCtor )
2021-08-13 07:06:44 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {" MsgContainer ",-40} {msgStamp:u} (svc)" ) ;
2021-08-13 07:06:44 +02:00
return ReadMsgContainer ( reader ) ;
}
2021-09-17 04:53:02 +02:00
else if ( ctorNb = = Layer . RpcResultCtor )
2021-08-13 07:06:44 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {" RpcResult ",-40} {msgStamp:u}" ) ;
2021-08-13 07:06:44 +02:00
return ReadRpcResult ( reader ) ;
}
else
{
var obj = reader . ReadTLObject ( ctorNb ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {msgStamp:u} {((seqno & 1) != 0 ? "" : " ( svc ) ")} {((msgId & 2) == 0 ? " " : " NAR ")}" ) ;
2021-08-13 07:06:44 +02:00
return obj ;
}
2021-08-12 12:37:56 +02:00
}
2021-08-04 00:40:09 +02:00
static string TransportError ( int error_code ) = > error_code switch
{
404 = > "Auth key not found" ,
429 = > "Transport flood" ,
2022-06-19 18:08:19 +02:00
444 = > "Invalid DC" ,
2021-11-03 18:20:54 +01:00
_ = > Enum . GetName ( typeof ( HttpStatusCode ) , error_code ) ? ? "Transport error"
2021-08-04 00:40:09 +02:00
} ;
}
2022-04-06 18:38:54 +02:00
internal MsgContainer ReadMsgContainer ( TL . BinaryReader reader )
2021-09-16 04:47:15 +02:00
{
2022-04-06 18:38:54 +02:00
int count = reader . ReadInt32 ( ) ;
var array = new _Message [ count ] ;
for ( int i = 0 ; i < count ; i + + )
2021-08-04 00:40:09 +02:00
{
2022-03-27 22:29:48 +02:00
var msg = array [ i ] = new _Message ( reader . ReadInt64 ( ) , reader . ReadInt32 ( ) , null ) { bytes = reader . ReadInt32 ( ) } ;
2022-01-11 04:14:23 +01:00
if ( ( msg . seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msg . msg_id ) ;
2021-08-13 07:06:44 +02:00
var pos = reader . BaseStream . Position ;
try
{
var ctorNb = reader . ReadUInt32 ( ) ;
2021-09-17 04:53:02 +02:00
if ( ctorNb = = Layer . RpcResultCtor )
2021-08-13 07:06:44 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $" → {" RpcResult ",-38} {MsgIdToStamp(msg.msg_id):u}" ) ;
2021-08-13 07:06:44 +02:00
msg . body = ReadRpcResult ( reader ) ;
}
else
{
var obj = msg . body = reader . ReadTLObject ( ctorNb ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $" → {obj.GetType().Name,-38} {MsgIdToStamp(msg.msg_id):u} {((msg.seqno & 1) != 0 ? "" : " ( svc ) ")} {((msg.msg_id & 2) == 0 ? " " : " NAR ")}" ) ;
2021-08-13 07:06:44 +02:00
}
}
catch ( Exception ex )
{
Helpers . Log ( 4 , "While deserializing vector<%Message>: " + ex . ToString ( ) ) ;
}
reader . BaseStream . Position = pos + array [ i ] . bytes ;
2021-08-05 16:29:58 +02:00
}
2021-08-13 07:06:44 +02:00
return new MsgContainer { messages = array } ;
2021-08-04 00:40:09 +02:00
}
2021-08-30 01:31:08 +02:00
private RpcResult ReadRpcResult ( TL . BinaryReader reader )
2021-08-04 00:40:09 +02:00
{
2021-08-13 07:06:44 +02:00
long msgId = reader . ReadInt64 ( ) ;
2022-03-27 22:29:48 +02:00
var rpc = PullPendingRequest ( msgId ) ;
2021-08-14 08:55:30 +02:00
object result ;
2022-03-27 22:29:48 +02:00
if ( rpc ! = null )
2021-08-13 00:28:34 +02:00
{
2021-12-07 16:35:23 +01:00
try
{
2022-03-27 22:29:48 +02:00
if ( ! rpc . type . IsArray )
result = reader . ReadTLValue ( rpc . type ) ;
2021-12-07 16:35:23 +01:00
else
{
2022-01-17 15:06:29 +01:00
var peek = reader . ReadUInt32 ( ) ;
if ( peek = = Layer . RpcErrorCtor )
result = reader . ReadTLObject ( Layer . RpcErrorCtor ) ;
else if ( peek = = Layer . GZipedCtor )
using ( var gzipReader = new TL . BinaryReader ( new GZipStream ( new MemoryStream ( reader . ReadTLBytes ( ) ) , CompressionMode . Decompress ) , reader . Client ) )
2022-03-27 22:29:48 +02:00
result = gzipReader . ReadTLValue ( rpc . type ) ;
2022-01-17 15:06:29 +01:00
else
{
reader . BaseStream . Position - = 4 ;
2022-03-27 22:29:48 +02:00
result = reader . ReadTLValue ( rpc . type ) ;
2022-01-17 15:06:29 +01:00
}
2021-12-07 16:35:23 +01:00
}
2022-03-27 22:29:48 +02:00
if ( rpc . type . IsEnum ) result = Enum . ToObject ( rpc . type , result ) ;
2021-12-21 02:28:35 +01:00
if ( result is RpcError rpcError )
Helpers . Log ( 4 , $" → RpcError {rpcError.error_code,3} {rpcError.error_message,-24} #{(short)msgId.GetHashCode():X4}" ) ;
else
Helpers . Log ( 1 , $" → {result?.GetType().Name,-37} #{(short)msgId.GetHashCode():X4}" ) ;
2022-03-27 22:29:48 +02:00
rpc . tcs . SetResult ( result ) ;
2021-12-07 16:35:23 +01:00
}
catch ( Exception ex )
2021-10-01 02:22:26 +02:00
{
2022-03-27 22:29:48 +02:00
rpc . tcs . SetException ( ex ) ;
2021-12-07 16:35:23 +01:00
throw ;
2021-10-01 02:22:26 +02:00
}
2021-08-13 00:28:34 +02:00
}
2021-08-04 00:40:09 +02:00
else
2021-08-13 00:28:34 +02:00
{
2022-01-11 04:14:23 +01:00
var ctorNb = reader . ReadUInt32 ( ) ;
if ( ctorNb = = Layer . VectorCtor )
{
reader . BaseStream . Position - = 4 ;
2022-02-03 16:55:16 +01:00
result = reader . ReadTLVector ( typeof ( IObject [ ] ) ) ;
2022-01-11 04:14:23 +01:00
}
2022-02-03 16:55:16 +01:00
else if ( ctorNb = = ( uint ) Bool . False ) result = false ;
else if ( ctorNb = = ( uint ) Bool . True ) result = true ;
else result = reader . ReadTLObject ( ctorNb ) ;
var typeName = result ? . GetType ( ) . Name ;
2021-09-28 16:12:20 +02:00
if ( MsgIdToStamp ( msgId ) > = _session . SessionStart )
2022-01-11 04:14:23 +01:00
Helpers . Log ( 4 , $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-13 00:28:34 +02:00
else
2022-01-11 04:14:23 +01:00
Helpers . Log ( 1 , $" → {typeName,-37} for past msgId #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-13 00:28:34 +02:00
}
2021-08-14 15:15:41 +02:00
return new RpcResult { req_msg_id = msgId , result = result } ;
2021-08-04 00:40:09 +02:00
}
2022-03-27 22:29:48 +02:00
class Rpc
2021-08-14 15:15:41 +02:00
{
2022-03-27 22:29:48 +02:00
public Type type ;
public TaskCompletionSource < object > tcs = new ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
public long msgId ;
public Task < object > Task = > tcs . Task ;
}
private Rpc PullPendingRequest ( long msgId )
{
Rpc request ;
lock ( _pendingRpcs )
if ( _pendingRpcs . TryGetValue ( msgId , out request ) )
_pendingRpcs . Remove ( msgId ) ;
2021-08-14 15:15:41 +02:00
return request ;
}
2021-11-07 16:52:58 +01:00
private async Task HandleMessageAsync ( IObject obj )
2021-08-04 00:40:09 +02:00
{
2022-08-30 15:15:43 +02:00
if ( _bareRpc ! = null )
{
var rpc = PullPendingRequest ( _bareRpc . msgId ) ;
if ( ( rpc ? . type . IsAssignableFrom ( obj . GetType ( ) ) ) ! = true )
throw new ApplicationException ( $"Received a {obj.GetType()} incompatible with expected bare {rpc?.type}" ) ;
_bareRpc = null ;
rpc . tcs . SetResult ( obj ) ;
return ;
}
2021-08-04 00:40:09 +02:00
switch ( obj )
{
case MsgContainer container :
foreach ( var msg in container . messages )
2021-08-13 07:06:44 +02:00
if ( msg . body ! = null )
await HandleMessageAsync ( msg . body ) ;
2021-08-04 00:40:09 +02:00
break ;
2021-08-14 15:15:41 +02:00
case MsgCopy msgCopy :
if ( msgCopy ? . orig_message ? . body ! = null )
await HandleMessageAsync ( msgCopy . orig_message . body ) ;
break ;
2021-11-10 17:26:40 +01:00
case TL . Methods . Ping ping :
2021-11-07 16:50:59 +01:00
_ = SendAsync ( new Pong { msg_id = _lastRecvMsgId , ping_id = ping . ping_id } , false ) ;
2021-08-14 15:15:41 +02:00
break ;
case Pong pong :
2021-09-05 01:08:16 +02:00
SetResult ( pong . msg_id , pong ) ;
2021-08-14 15:15:41 +02:00
break ;
case FutureSalts futureSalts :
2021-09-05 01:08:16 +02:00
SetResult ( futureSalts . req_msg_id , futureSalts ) ;
2021-08-04 00:40:09 +02:00
break ;
case RpcResult rpcResult :
2021-08-14 15:15:41 +02:00
break ; // SetResult was already done in ReadRpcResult
case MsgsAck msgsAck :
break ; // we don't do anything with these, for now
case BadMsgNotification badMsgNotification :
2021-12-04 00:14:15 +01:00
await _sendSemaphore . WaitAsync ( ) ;
bool retryLast = badMsgNotification . bad_msg_id = = _dcSession . LastSentMsgId ;
var lastSentMsg = _lastSentMsg ;
_sendSemaphore . Release ( ) ;
2021-12-21 02:28:35 +01:00
var logLevel = badMsgNotification . error_code = = 48 ? 2 : 4 ;
Helpers . Log ( logLevel , $"BadMsgNotification {badMsgNotification.error_code} for msg #{(short)badMsgNotification.bad_msg_id.GetHashCode():X4}" ) ;
2021-12-04 00:14:15 +01:00
switch ( badMsgNotification . error_code )
2021-08-27 22:44:43 +02:00
{
2022-01-23 01:28:10 +01:00
case 16 : // msg_id too low (most likely, client time is wrong; synchronize it using msg_id notifications and re-send the original message)
case 17 : // msg_id too high (similar to the previous case, the client time has to be synchronized, and the message re-sent with the correct msg_id)
2021-12-30 23:45:28 +01:00
_dcSession . LastSentMsgId = 0 ;
var localTime = DateTime . UtcNow ;
_dcSession . ServerTicksOffset = ( _lastRecvMsgId > > 32 ) * 10000000 - localTime . Ticks + 621355968000000000L ;
Helpers . Log ( 1 , $"Time offset: {_dcSession.ServerTicksOffset} | Server: {MsgIdToStamp(_lastRecvMsgId).AddTicks(_dcSession.ServerTicksOffset).TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC" ) ;
break ;
2021-12-04 00:14:15 +01:00
case 32 : // msg_seqno too low (the server has already received a message with a lower msg_id but with either a higher or an equal and odd seqno)
case 33 : // msg_seqno too high (similarly, there is a message with a higher msg_id but with either a lower or an equal and odd seqno)
if ( _dcSession . Seqno < = 1 )
retryLast = false ;
else
{
Reset ( false , false ) ;
_dcSession . Renew ( ) ;
await ConnectAsync ( ) ;
}
break ;
case 48 : // incorrect server salt (in this case, the bad_server_salt response is received with the correct salt, and the message is to be re-sent with it)
_dcSession . Salt = ( ( BadServerSalt ) badMsgNotification ) . new_server_salt ;
break ;
default :
retryLast = false ;
break ;
}
if ( retryLast )
{
2022-03-27 22:29:48 +02:00
Rpc prevRequest ;
lock ( _pendingRpcs )
_pendingRpcs . TryGetValue ( badMsgNotification . bad_msg_id , out prevRequest ) ;
await SendAsync ( lastSentMsg , true , prevRequest ) ;
lock ( _pendingRpcs )
_pendingRpcs . Remove ( badMsgNotification . bad_msg_id ) ;
2021-08-27 22:44:43 +02:00
}
2022-03-27 22:29:48 +02:00
else if ( PullPendingRequest ( badMsgNotification . bad_msg_id ) is Rpc rpc )
2021-12-04 00:14:15 +01:00
{
2022-06-15 12:21:22 +02:00
if ( _bareRpc ? . msgId = = badMsgNotification . bad_msg_id ) _bareRpc = null ;
2022-03-27 22:29:48 +02:00
rpc . tcs . SetException ( new ApplicationException ( $"BadMsgNotification {badMsgNotification.error_code}" ) ) ;
2021-12-04 00:14:15 +01:00
}
else
2022-07-29 15:24:18 +02:00
RaiseUpdate ( obj ) ;
2021-08-27 22:44:43 +02:00
break ;
2021-08-04 00:40:09 +02:00
default :
2022-07-29 15:24:18 +02:00
RaiseUpdate ( obj ) ;
2021-08-04 00:40:09 +02:00
break ;
}
2021-08-14 15:15:41 +02:00
2021-09-05 01:08:16 +02:00
void SetResult ( long msgId , object result )
2021-08-14 15:15:41 +02:00
{
2022-03-27 22:29:48 +02:00
var rpc = PullPendingRequest ( msgId ) ;
if ( rpc ! = null )
rpc . tcs . SetResult ( result ) ;
2021-09-03 00:06:48 +02:00
else
2022-07-29 15:24:18 +02:00
RaiseUpdate ( obj ) ;
2021-09-05 01:08:16 +02:00
}
}
2022-07-29 15:24:18 +02:00
private async void RaiseUpdate ( IObject obj )
2021-09-05 01:08:16 +02:00
{
try
{
2022-08-06 13:06:46 +02:00
var task = OnUpdate ? . Invoke ( obj ) ;
if ( task ! = null ) await task ;
2021-09-05 01:08:16 +02:00
}
catch ( Exception ex )
{
2022-07-29 15:24:18 +02:00
Helpers . Log ( 4 , $"{nameof(OnUpdate)}({obj?.GetType().Name}) raised {ex}" ) ;
2021-08-14 15:15:41 +02:00
}
2021-08-04 00:40:09 +02:00
}
2022-04-06 18:38:54 +02:00
static async Task < TcpClient > DefaultTcpHandler ( string host , int port )
2021-08-24 17:24:46 +02:00
{
2022-04-06 18:38:54 +02:00
var tcpClient = new TcpClient ( ) ;
try
2021-11-06 05:22:33 +01:00
{
2022-04-06 18:38:54 +02:00
await tcpClient . ConnectAsync ( host , port ) ;
2021-08-27 14:14:24 +02:00
}
2022-05-09 23:43:06 +02:00
catch
2022-04-06 18:38:54 +02:00
{
tcpClient . Dispose ( ) ;
throw ;
}
return tcpClient ;
2021-08-24 17:24:46 +02:00
}
2022-08-12 21:19:10 +02:00
/// <summary>Establish connection to Telegram servers without negociating a user session</summary>
/// <remarks>Usually you shouldn't need to call this method: Use <see cref="LoginUserIfNeeded">LoginUserIfNeeded</see> instead. <br/>Config callback is queried for: <b>server_address</b></remarks>
2022-04-06 18:38:54 +02:00
/// <returns>Most methods of this class are async (Task), so please use <see langword="await"/></returns>
public async Task ConnectAsync ( )
2021-08-04 00:40:09 +02:00
{
2022-04-06 18:38:54 +02:00
lock ( this )
_connecting ? ? = DoConnectAsync ( ) ;
await _connecting ;
}
private async Task DoConnectAsync ( )
{
_cts = new ( ) ;
IPEndPoint endpoint = null ;
byte [ ] preamble , secret = null ;
int dcId = _dcSession ? . DcID ? ? 0 ;
if ( dcId = = 0 ) dcId = 2 ;
if ( MTProxyUrl ! = null )
2021-08-27 14:14:24 +02:00
{
2022-04-06 18:38:54 +02:00
#if OBFUSCATION
2022-09-11 15:34:38 +02:00
if ( TLConfig ? . test_mode = = true ) dcId + = 10000 ;
2022-06-19 18:08:19 +02:00
if ( _dcSession . DataCenter ? . flags . HasFlag ( DcOption . Flags . media_only ) = = true ) dcId = - dcId ;
2022-04-06 18:38:54 +02:00
var parms = HttpUtility . ParseQueryString ( MTProxyUrl [ MTProxyUrl . IndexOf ( '?' ) . . ] ) ;
var server = parms [ "server" ] ;
int port = int . Parse ( parms [ "port" ] ) ;
var str = parms [ "secret" ] ; // can be hex or base64
var secretBytes = secret = str . All ( "0123456789ABCDEFabcdef" . Contains ) ? Convert . FromHexString ( str ) :
System . Convert . FromBase64String ( str . Replace ( '_' , '/' ) . Replace ( '-' , '+' ) + new string ( '=' , ( 2147483644 - str . Length ) % 4 ) ) ;
var tlsMode = secret . Length > = 21 & & secret [ 0 ] = = 0xEE ;
if ( tlsMode | | ( secret . Length = = 17 & & secret [ 0 ] = = 0xDD ) )
2021-08-25 15:32:25 +02:00
{
2022-04-06 18:38:54 +02:00
_paddedMode = true ;
secret = secret [ 1. . 17 ] ;
2021-08-25 15:32:25 +02:00
}
2022-04-06 18:38:54 +02:00
else if ( secret . Length ! = 16 ) throw new ArgumentException ( "Invalid/unsupported secret" , nameof ( secret ) ) ;
Helpers . Log ( 2 , $"Connecting to DC {dcId} via MTProxy {server}:{port}..." ) ;
_tcpClient = await TcpHandler ( server , port ) ;
_networkStream = _tcpClient . GetStream ( ) ;
if ( tlsMode )
_networkStream = await TlsStream . HandshakeAsync ( _networkStream , secret , secretBytes [ 17. . ] , _cts . Token ) ;
#else
throw new Exception ( "Library was not compiled with OBFUSCATION symbol" ) ;
#endif
2021-08-06 07:28:54 +02:00
}
2022-04-06 18:38:54 +02:00
else
2021-08-06 07:28:54 +02:00
{
2022-04-06 18:38:54 +02:00
endpoint = _dcSession ? . EndPoint ? ? Compat . IPEndPoint_Parse ( Config ( "server_address" ) ) ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
TcpClient tcpClient = null ;
try
{
try
{
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
catch ( SocketException ex ) // cannot connect to target endpoint, try to find an alternate
{
Helpers . Log ( 4 , $"SocketException {ex.SocketErrorCode} ({ex.ErrorCode}): {ex.Message}" ) ;
if ( _dcSession ? . DataCenter = = null ) throw ;
var triedEndpoints = new HashSet < IPEndPoint > { endpoint } ;
if ( _session . DcOptions ! = null )
{
var altOptions = _session . DcOptions . Where ( dco = > dco . id = = _dcSession . DataCenter . id & & dco . flags ! = _dcSession . DataCenter . flags
& & ( dco . flags & ( DcOption . Flags . cdn | DcOption . Flags . tcpo_only | DcOption . Flags . media_only ) ) = = 0 )
. OrderBy ( dco = > dco . flags ) ;
// try alternate addresses for this DC
foreach ( var dcOption in altOptions )
{
endpoint = new ( IPAddress . Parse ( dcOption . ip_address ) , dcOption . port ) ;
if ( ! triedEndpoints . Add ( endpoint ) ) continue ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
try
{
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
_dcSession . DataCenter = dcOption ;
break ;
}
catch ( SocketException ) { }
}
}
if ( tcpClient = = null )
{
endpoint = Compat . IPEndPoint_Parse ( Config ( "server_address" ) ) ; // re-ask callback for an address
if ( ! triedEndpoints . Add ( endpoint ) ) throw ;
_dcSession . Client = null ;
// is it address for a known DCSession?
_dcSession = _session . DCSessions . Values . FirstOrDefault ( dcs = > dcs . EndPoint . Equals ( endpoint ) ) ;
_dcSession ? ? = new ( ) { Id = Helpers . RandomLong ( ) } ;
_dcSession . Client = this ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
}
}
2022-05-09 23:43:06 +02:00
catch
2022-04-06 18:38:54 +02:00
{
tcpClient ? . Dispose ( ) ;
throw ;
}
_tcpClient = tcpClient ;
_networkStream = _tcpClient . GetStream ( ) ;
}
byte protocolId = ( byte ) ( _paddedMode ? 0xDD : 0xEE ) ;
#if OBFUSCATION
( _sendCtr , _recvCtr , preamble ) = InitObfuscation ( secret , protocolId , dcId ) ;
#else
preamble = new byte [ ] { protocolId , protocolId , protocolId , protocolId } ;
#endif
await _networkStream . WriteAsync ( preamble , 0 , preamble . Length , _cts . Token ) ;
_saltChangeCounter = 0 ;
_reactorTask = Reactor ( _networkStream , _cts ) ;
_sendSemaphore . Release ( ) ;
try
{
if ( _dcSession . AuthKeyID = = 0 )
await CreateAuthorizationKey ( this , _dcSession ) ;
var keepAliveTask = KeepAlive ( _cts . Token ) ;
TLConfig = await this . InvokeWithLayer ( Layer . Version ,
new TL . Methods . InitConnection < Config >
{
api_id = _session . ApiId ,
device_model = Config ( "device_model" ) ,
system_version = Config ( "system_version" ) ,
app_version = Config ( "app_version" ) ,
system_lang_code = Config ( "system_lang_code" ) ,
lang_pack = Config ( "lang_pack" ) ,
lang_code = Config ( "lang_code" ) ,
query = new TL . Methods . Help_GetConfig ( )
} ) ;
_session . DcOptions = TLConfig . dc_options ;
_saltChangeCounter = 0 ;
if ( _dcSession . DataCenter = = null )
{
_dcSession . DataCenter = _session . DcOptions . Where ( dc = > dc . id = = TLConfig . this_dc )
. OrderByDescending ( dc = > dc . ip_address = = endpoint ? . Address . ToString ( ) )
. ThenByDescending ( dc = > dc . port = = endpoint ? . Port )
. ThenByDescending ( dc = > dc . flags = = ( endpoint ? . AddressFamily = = AddressFamily . InterNetworkV6 ? DcOption . Flags . ipv6 : 0 ) )
. First ( ) ;
_session . DCSessions [ TLConfig . this_dc ] = _dcSession ;
}
if ( _session . MainDC = = 0 ) _session . MainDC = TLConfig . this_dc ;
}
finally
{
lock ( _session ) _session . Save ( ) ;
}
Helpers . Log ( 2 , $"Connected to {(TLConfig.test_mode ? " Test DC " : " DC ")} {TLConfig.this_dc}... {TLConfig.flags & (Config.Flags)~0xE00U}" ) ;
}
private async Task KeepAlive ( CancellationToken ct )
{
int ping_id = _random . Next ( ) ;
while ( ! ct . IsCancellationRequested )
{
await Task . Delay ( Math . Abs ( PingInterval ) * 1000 , ct ) ;
2022-09-24 15:34:31 +02:00
if ( _saltChangeCounter > 0 ) _saltChangeCounter - = Math . Abs ( PingInterval ) ;
2022-04-06 18:38:54 +02:00
if ( PingInterval < = 0 )
await this . Ping ( ping_id + + ) ;
else // see https://core.telegram.org/api/optimisation#grouping-updates
#if DEBUG
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 ) ;
#else
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 / 4 ) ;
#endif
}
}
2022-09-20 17:30:32 +02:00
/// <summary>Login as a user with given phone number (or resume previous session)<br/>Call this method again to provide additional requested login information</summary>
/// <param name="loginInfo">First call should be with phone number<br/>Further calls should be with the requested configuration value</param>
/// <returns>Configuration item requested to continue login, or <see langword="null"/> when login is successful<br/>
2022-10-12 23:25:15 +02:00
/// Possible values: <b>verification_code</b>, <b>name</b> (signup), <b>password</b> (2FA), <b>email</b> & <b>email_verification_code</b> (email registration)</returns>
2022-09-20 17:30:32 +02:00
/// <exception cref="ApplicationException"/><exception cref="RpcException"/>
public async Task < string > Login ( string loginInfo )
{
if ( _loginCfg . request = = default ) RunLoginAsync ( loginInfo ) ;
else
{
if ( await _loginCfg . request . Task = = null ) return null ;
loginInfo ? ? = AskConfig ( await _loginCfg . request . Task ) ;
_loginCfg . request = new ( ) ;
_loginCfg . response . SetResult ( loginInfo ) ;
}
return await _loginCfg . request . Task ;
}
private ( TaskCompletionSource < string > request , TaskCompletionSource < string > response ) _loginCfg ;
private async void RunLoginAsync ( string phone )
{
_loginCfg . request = new ( ) ;
var prevConfig = _config ;
_config = what = >
{
if ( prevConfig ( what ) is string value ) return value ;
switch ( what )
{
case "phone_number" : return phone ;
case "last_name" : break ;
case "first_name" : what = "name" ; goto case "email" ;
case "email" : case "email_verification_code" :
case "password" : case "verification_code" : _loginCfg . response = new ( ) ; _loginCfg . request . SetResult ( what ) ; break ;
default : return null ;
} ;
value = _loginCfg . response . Task . Result ;
if ( what = = "name" & & value ! = null )
{
var lf = value . IndexOf ( '\n' ) ;
if ( lf < 0 ) lf = value . IndexOf ( ' ' ) ;
_loginCfg . response = new ( ) ;
_loginCfg . response . SetResult ( lf < 0 ? "" : value [ ( lf + 1 ) . . ] ) ;
value = lf < 0 ? value : value [ 0. . lf ] ;
return value ;
}
return value ;
} ;
try
{
// Login logic is executed on TaskScheduler while request TCS are still received on current SynchronizationContext
await Task . Run ( ( ) = > LoginUserIfNeeded ( ) ) ;
_loginCfg . request . SetResult ( null ) ;
}
catch ( Exception ex )
{
_loginCfg . request . SetException ( ex ) ;
}
finally
{
_config = prevConfig ;
}
}
2022-04-06 18:38:54 +02:00
/// <summary>Login as a bot (if not already logged-in).</summary>
2022-09-20 17:30:32 +02:00
/// <param name="bot_token">bot token, or <see langword="null"/> if token is provided by Config callback</param>
/// <remarks>Config callback may be queried for: <b>bot_token</b>
2022-04-06 18:38:54 +02:00
/// <br/>Bots can only call API methods marked with [bots: ✓] in their documentation. </remarks>
/// <returns>Detail about the logged-in bot</returns>
2022-09-20 17:30:32 +02:00
public async Task < User > LoginBotIfNeeded ( string bot_token = null )
2022-04-06 18:38:54 +02:00
{
await ConnectAsync ( ) ;
2022-09-20 17:30:32 +02:00
string botToken = bot_token ? ? Config ( "bot_token" ) ;
2022-04-06 18:38:54 +02:00
if ( _session . UserId ! = 0 ) // a user is already logged-in
{
try
{
2022-09-14 18:22:52 +02:00
var users = await this . Users_GetUsers ( InputUser . Self ) ; // this calls also reenable incoming Updates
2022-04-06 18:38:54 +02:00
var self = users [ 0 ] as User ;
if ( self . id = = long . Parse ( botToken . Split ( ':' ) [ 0 ] ) )
{
_session . UserId = _dcSession . UserId = self . id ;
2022-09-20 17:30:32 +02:00
return User = self ;
2022-04-06 18:38:54 +02:00
}
Helpers . Log ( 3 , $"Current logged user {self.id} mismatched bot_token. Logging out and in..." ) ;
}
catch ( Exception ex )
{
Helpers . Log ( 4 , $"Error while verifying current bot! ({ex.Message}) Proceeding to login..." ) ;
}
await this . Auth_LogOut ( ) ;
_session . UserId = _dcSession . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
2022-04-06 18:38:54 +02:00
}
var authorization = await this . Auth_ImportBotAuthorization ( 0 , _session . ApiId , _apiHash ? ? = Config ( "api_hash" ) , botToken ) ;
return LoginAlreadyDone ( authorization ) ;
}
/// <summary>Login as a user (if not already logged-in).
2022-08-12 21:19:10 +02:00
/// <br/><i>(this method calls <see cref="ConnectAsync">ConnectAsync</see> if necessary)</i></summary>
2022-10-12 23:25:15 +02:00
/// <remarks>Config callback is queried for: <b>phone_number</b>, <b>verification_code</b> <br/>and eventually <b>first_name</b>, <b>last_name</b> (signup required), <b>password</b> (2FA auth), <b>email</b> & <b>email_verification_code</b> (email registration), <b>user_id</b> (alt validation)</remarks>
2022-04-06 18:38:54 +02:00
/// <param name="settings">(optional) Preference for verification_code sending</param>
2022-07-08 01:15:37 +02:00
/// <param name="reloginOnFailedResume">Proceed to logout and login if active user session cannot be resumed successfully</param>
2022-04-06 18:38:54 +02:00
/// <returns>Detail about the logged-in user
/// <br/>Most methods of this class are async (Task), so please use <see langword="await"/> to get the result</returns>
2022-07-08 01:15:37 +02:00
public async Task < User > LoginUserIfNeeded ( CodeSettings settings = null , bool reloginOnFailedResume = true )
2022-04-06 18:38:54 +02:00
{
await ConnectAsync ( ) ;
string phone_number = null ;
if ( _session . UserId ! = 0 ) // a user is already logged-in
{
try
{
2022-09-14 18:22:52 +02:00
var users = await this . Users_GetUsers ( InputUser . Self ) ; // this call also reenable incoming Updates
2022-04-06 18:38:54 +02:00
var self = users [ 0 ] as User ;
// check user_id or phone_number match currently logged-in user
if ( ( long . TryParse ( _config ( "user_id" ) , out long id ) & & ( id = = - 1 | | self . id = = id ) ) | |
self . phone = = string . Concat ( ( phone_number = Config ( "phone_number" ) ) . Where ( char . IsDigit ) ) )
{
_session . UserId = _dcSession . UserId = self . id ;
2022-09-20 17:30:32 +02:00
return User = self ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
var mismatch = $"Current logged user {self.id} mismatched user_id or phone_number" ;
Helpers . Log ( 3 , mismatch ) ;
if ( ! reloginOnFailedResume ) throw new ApplicationException ( mismatch ) ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
catch ( RpcException ex ) when ( reloginOnFailedResume )
2022-04-06 18:38:54 +02:00
{
2022-07-08 01:15:37 +02:00
Helpers . Log ( 4 , $"Error while fetching current user! ({ex.Message})" ) ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
Helpers . Log ( 3 , $"Proceeding to logout and login..." ) ;
2022-04-06 18:38:54 +02:00
await this . Auth_LogOut ( ) ;
_session . UserId = _dcSession . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
2022-04-06 18:38:54 +02:00
}
phone_number ? ? = Config ( "phone_number" ) ;
Auth_SentCode sentCode ;
2022-04-22 23:07:43 +02:00
#pragma warning disable CS0618 // Auth_* methods are marked as obsolete
2022-04-06 18:38:54 +02:00
try
{
sentCode = await this . Auth_SendCode ( phone_number , _session . ApiId , _apiHash ? ? = Config ( "api_hash" ) , settings ? ? = new ( ) ) ;
}
catch ( RpcException ex ) when ( ex . Code = = 500 & & ex . Message = = "AUTH_RESTART" )
{
sentCode = await this . Auth_SendCode ( phone_number , _session . ApiId , _apiHash , settings ) ;
}
2021-12-05 07:21:30 +01:00
Auth_AuthorizationBase authorization = null ;
2022-04-22 23:07:43 +02:00
try
{
2022-09-14 18:29:07 +02:00
if ( sentCode . type is Auth_SentCodeTypeSetUpEmailRequired setupEmail )
{
Helpers . Log ( 3 , "A login email is required" ) ;
2022-09-19 22:28:12 +02:00
RaiseUpdate ( sentCode ) ;
2022-09-14 18:29:07 +02:00
var email = _config ( "email" ) ;
if ( string . IsNullOrEmpty ( email ) )
sentCode = await this . Auth_ResendCode ( phone_number , sentCode . phone_code_hash ) ;
else
{
var purpose = new EmailVerifyPurposeLoginSetup { phone_number = phone_number , phone_code_hash = sentCode . phone_code_hash } ;
if ( email is not "Google" and not "Apple" )
{
var sentEmail = await this . Account_SendVerifyEmailCode ( purpose , email ) ;
Helpers . Log ( 3 , "An email verification code has been sent to " + sentEmail . email_pattern ) ;
RaiseUpdate ( sentEmail ) ;
}
Account_EmailVerified verified = null ;
for ( int retry = 1 ; verified = = null ; retry + + )
try
{
var code = await ConfigAsync ( "email_verification_code" ) ;
if ( email is "Google" )
verified = await this . Account_VerifyEmail ( purpose , new EmailVerificationGoogle { token = code } ) ;
else if ( email is "Apple" )
verified = await this . Account_VerifyEmail ( purpose , new EmailVerificationApple { token = code } ) ;
else
verified = await this . Account_VerifyEmail ( purpose , new EmailVerificationCode { code = code } ) ;
}
catch ( RpcException e ) when ( e . Code = = 400 & & e . Message is "CODE_INVALID" or "EMAIL_TOKEN_INVALID" )
{
Helpers . Log ( 4 , "Wrong email verification code!" ) ;
if ( retry = = MaxCodePwdAttempts ) throw ;
}
if ( verified is Account_EmailVerifiedLogin verifiedLogin ) // (it should always be)
sentCode = verifiedLogin . sent_code ;
}
}
2022-04-22 23:07:43 +02:00
resent :
var timeout = DateTime . UtcNow + TimeSpan . FromSeconds ( sentCode . timeout ) ;
Helpers . Log ( 3 , $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}" ) ;
2022-09-14 18:29:07 +02:00
RaiseUpdate ( sentCode ) ;
2022-04-22 23:07:43 +02:00
for ( int retry = 1 ; authorization = = null ; retry + + )
try
2021-12-13 15:28:06 +01:00
{
2022-04-22 23:07:43 +02:00
var verification_code = await ConfigAsync ( "verification_code" ) ;
if ( verification_code = = "" & & sentCode . next_type ! = 0 )
2021-12-13 15:28:06 +01:00
{
2022-04-22 23:07:43 +02:00
var mustWait = timeout - DateTime . UtcNow ;
if ( mustWait . Ticks > 0 )
{
Helpers . Log ( 3 , $"You must wait {(int)(mustWait.TotalSeconds + 0.5)} more seconds before requesting the code to be sent via {sentCode.next_type}" ) ;
continue ;
}
sentCode = await this . Auth_ResendCode ( phone_number , sentCode . phone_code_hash ) ;
goto resent ;
2021-12-13 15:28:06 +01:00
}
2022-04-22 23:07:43 +02:00
authorization = await this . Auth_SignIn ( phone_number , sentCode . phone_code_hash , verification_code ) ;
2021-12-13 15:28:06 +01:00
}
2022-05-31 14:28:37 +02:00
catch ( RpcException e ) when ( e . Code = = 400 & & e . Message = = "PHONE_CODE_INVALID" )
2022-04-22 23:07:43 +02:00
{
2022-05-31 14:28:37 +02:00
Helpers . Log ( 4 , "Wrong verification code!" ) ;
if ( retry = = MaxCodePwdAttempts ) throw ;
2022-04-22 23:07:43 +02:00
}
2022-05-31 14:28:37 +02:00
catch ( RpcException e ) when ( e . Code = = 401 & & e . Message = = "SESSION_PASSWORD_NEEDED" )
2022-04-22 23:07:43 +02:00
{
2022-05-31 14:28:37 +02:00
for ( int pwdRetry = 1 ; authorization = = null ; pwdRetry + + )
try
{
var accountPassword = await this . Account_GetPassword ( ) ;
2022-07-29 15:24:18 +02:00
RaiseUpdate ( accountPassword ) ;
2022-05-31 14:28:37 +02:00
var checkPasswordSRP = await Check2FA ( accountPassword , ( ) = > ConfigAsync ( "password" ) ) ;
authorization = await this . Auth_CheckPassword ( checkPasswordSRP ) ;
}
catch ( RpcException pe ) when ( pe . Code = = 400 & & pe . Message = = "PASSWORD_HASH_INVALID" )
{
Helpers . Log ( 4 , "Wrong password!" ) ;
if ( pwdRetry = = MaxCodePwdAttempts ) throw ;
}
2022-04-22 23:07:43 +02:00
}
}
2022-10-10 21:55:56 +02:00
catch ( Exception ex ) when ( ex is not RpcException { Message : "FLOOD_WAIT_X" } )
2022-04-22 23:07:43 +02:00
{
2022-10-10 21:55:56 +02:00
try { await this . Auth_CancelCode ( phone_number , sentCode . phone_code_hash ) ; } catch { }
2022-04-22 23:07:43 +02:00
throw ;
}
2021-08-06 07:28:54 +02:00
if ( authorization is Auth_AuthorizationSignUpRequired signUpRequired )
{
2021-08-06 20:17:19 +02:00
var waitUntil = DateTime . UtcNow . AddSeconds ( 3 ) ;
2022-07-29 15:24:18 +02:00
RaiseUpdate ( signUpRequired ) ; // give caller the possibility to read and accept TOS
2021-08-09 11:41:50 +02:00
var first_name = Config ( "first_name" ) ;
var last_name = Config ( "last_name" ) ;
2021-08-06 20:17:19 +02:00
var wait = waitUntil - DateTime . UtcNow ;
if ( wait > TimeSpan . Zero ) await Task . Delay ( wait ) ; // we get a FLOOD_WAIT_3 if we SignUp too fast
2021-09-17 04:53:02 +02:00
authorization = await this . Auth_SignUp ( phone_number , sentCode . phone_code_hash , first_name , last_name ) ;
2021-08-06 07:28:54 +02:00
}
2022-04-22 23:07:43 +02:00
#pragma warning restore CS0618
2021-12-05 07:21:30 +01:00
return LoginAlreadyDone ( authorization ) ;
}
/// <summary><b>[Not recommended]</b> You can use this if you have already obtained a login authorization manually</summary>
/// <param name="authorization">if this was not a successful Auth_Authorization, an exception is thrown</param>
/// <returns>the User that was authorized</returns>
/// <remarks>This approach is not recommended because you likely didn't properly handle all aspects of the login process
/// <br/>(transient failures, unnecessary login, 2FA, sign-up required, slowness to respond, verification code resending, encryption key safety, etc..)
/// <br/>Methods <c>LoginUserIfNeeded</c> and <c>LoginBotIfNeeded</c> handle these automatically for you</remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public User LoginAlreadyDone ( Auth_AuthorizationBase authorization )
{
2021-08-06 07:28:54 +02:00
if ( authorization is not Auth_Authorization { user : User user } )
2021-08-04 00:40:09 +02:00
throw new ApplicationException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = user . id ;
2022-01-27 17:34:16 +01:00
lock ( _session ) _session . Save ( ) ;
2022-09-20 17:30:32 +02:00
return User = user ;
2021-08-04 00:40:09 +02:00
}
2021-08-05 16:29:58 +02:00
2022-04-06 18:38:54 +02:00
private MsgsAck CheckMsgsToAck ( )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
lock ( _msgsToAck )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
if ( _msgsToAck . Count = = 0 ) return null ;
var msgsAck = new MsgsAck { msg_ids = _msgsToAck . ToArray ( ) } ;
_msgsToAck . Clear ( ) ;
return msgsAck ;
2021-08-05 16:29:58 +02:00
}
}
2022-04-06 18:38:54 +02:00
internal ( long msgId , int seqno ) NewMsgId ( bool isContent )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
int seqno ;
long msgId = DateTime . UtcNow . Ticks + _dcSession . ServerTicksOffset - 621355968000000000L ;
msgId = msgId * 428 + ( msgId > > 24 ) * 25110956 ; // approximately unixtime*2^32 and divisible by 4
lock ( _session )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
if ( msgId < = _dcSession . LastSentMsgId ) msgId = _dcSession . LastSentMsgId + = 4 ; else _dcSession . LastSentMsgId = msgId ;
seqno = isContent ? _dcSession . Seqno + + * 2 + 1 : _dcSession . Seqno * 2 ;
_session . Save ( ) ;
}
return ( msgId , seqno ) ;
2021-08-05 16:29:58 +02:00
}
2022-04-06 18:38:54 +02:00
private async Task SendAsync ( IObject msg , bool isContent , Rpc rpc = null )
2021-08-05 16:29:58 +02:00
{
2022-08-01 19:06:31 +02:00
if ( _reactorTask = = null ) throw new ApplicationException ( "You must connect to Telegram first" ) ;
2022-04-06 18:38:54 +02:00
isContent & = _dcSession . AuthKeyID ! = 0 ;
( long msgId , int seqno ) = NewMsgId ( isContent ) ;
if ( rpc ! = null )
lock ( _pendingRpcs )
_pendingRpcs [ rpc . msgId = msgId ] = rpc ;
if ( isContent & & CheckMsgsToAck ( ) is MsgsAck msgsAck )
2021-11-09 01:43:27 +01:00
{
2022-04-06 18:38:54 +02:00
var ( ackId , ackSeqno ) = NewMsgId ( false ) ;
var container = new MsgContainer { messages = new _Message [ ] { new ( msgId , seqno , msg ) , new ( ackId , ackSeqno , msgsAck ) } } ;
await SendAsync ( container , false ) ;
return ;
2021-11-09 01:43:27 +01:00
}
2022-04-06 18:38:54 +02:00
await _sendSemaphore . WaitAsync ( ) ;
try
2021-11-09 01:43:27 +01:00
{
2022-04-06 18:38:54 +02:00
using var memStream = new MemoryStream ( 1024 ) ;
2022-10-04 00:52:44 +02:00
using var writer = new BinaryWriter ( memStream ) ;
2022-04-06 18:38:54 +02:00
writer . Write ( 0 ) ; // int32 payload_len (to be patched with payload length)
2021-09-23 13:13:36 +02:00
2022-04-06 18:38:54 +02:00
if ( _dcSession . AuthKeyID = = 0 ) // send unencrypted message
2022-01-20 02:44:43 +01:00
{
2022-08-30 15:15:43 +02:00
if ( _bareRpc = = null ) throw new ApplicationException ( $"Shouldn't send a {msg.GetType().Name} unencrypted" ) ;
2022-09-19 22:28:12 +02:00
writer . Write ( 0L ) ; // int64 auth_key_id = 0 (Unencrypted)
writer . Write ( msgId ) ; // int64 message_id
writer . Write ( 0 ) ; // int32 message_data_length (to be patched)
2022-04-06 18:38:54 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_')}..." ) ;
2022-09-19 22:28:12 +02:00
writer . WriteTLObject ( msg ) ; // bytes message_data
2022-04-06 18:38:54 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( memStream . GetBuffer ( ) . AsSpan ( 20 ) , ( int ) memStream . Length - 24 ) ; // patch message_data_length
2022-01-20 02:44:43 +01:00
}
2022-04-06 18:38:54 +02:00
else
{
using var clearStream = new MemoryStream ( 1024 ) ;
2022-10-04 00:52:44 +02:00
using var clearWriter = new BinaryWriter ( clearStream ) ;
2022-04-06 18:38:54 +02:00
clearWriter . Write ( _dcSession . AuthKey , 88 , 32 ) ;
2022-09-19 22:28:12 +02:00
clearWriter . Write ( _dcSession . Salt ) ; // int64 salt
clearWriter . Write ( _dcSession . Id ) ; // int64 session_id
clearWriter . Write ( msgId ) ; // int64 message_id
clearWriter . Write ( seqno ) ; // int32 msg_seqno
clearWriter . Write ( 0 ) ; // int32 message_data_length (to be patched)
2022-04-06 18:38:54 +02:00
if ( ( seqno & 1 ) ! = 0 )
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} #{(short)msgId.GetHashCode():X4}" ) ;
else
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} {MsgIdToStamp(msgId):u} (svc)" ) ;
2022-09-19 22:28:12 +02:00
clearWriter . WriteTLObject ( msg ) ; // bytes message_data
2022-04-06 18:38:54 +02:00
int clearLength = ( int ) clearStream . Length - 32 ; // length before padding (= 32 + message_data_length)
int padding = ( 0x7FFFFFF0 - clearLength ) % 16 ;
2022-10-04 00:52:44 +02:00
padding + = _random . Next ( 2 , 16 ) * 16 ; // MTProto 2.0 padding must be between 12..1024 with total length divisible by 16
2022-04-06 18:38:54 +02:00
clearStream . SetLength ( 32 + clearLength + padding ) ;
byte [ ] clearBuffer = clearStream . GetBuffer ( ) ;
BinaryPrimitives . WriteInt32LittleEndian ( clearBuffer . AsSpan ( 60 ) , clearLength - 32 ) ; // patch message_data_length
RNG . GetBytes ( clearBuffer , 32 + clearLength , padding ) ;
var msgKeyLarge = _sha256 . ComputeHash ( clearBuffer , 0 , 32 + clearLength + padding ) ;
const int msgKeyOffset = 8 ; // msg_key = middle 128-bits of SHA256(authkey_part+plaintext+padding)
2022-10-01 13:56:43 +02:00
byte [ ] encrypted_data = EncryptDecryptMessage ( clearBuffer . AsSpan ( 32 , clearLength + padding ) , true , 0 , _dcSession . AuthKey , msgKeyLarge , msgKeyOffset , _sha256 ) ;
2022-01-20 02:44:43 +01:00
2022-09-19 22:28:12 +02:00
writer . Write ( _dcSession . AuthKeyID ) ; // int64 auth_key_id
writer . Write ( msgKeyLarge , msgKeyOffset , 16 ) ; // int128 msg_key
writer . Write ( encrypted_data ) ; // bytes encrypted_data
2022-04-06 18:38:54 +02:00
}
if ( _paddedMode ) // Padded intermediate mode => append random padding
2022-01-20 02:44:43 +01:00
{
2022-04-06 18:38:54 +02:00
var padding = new byte [ _random . Next ( 16 ) ] ;
RNG . GetBytes ( padding ) ;
writer . Write ( padding ) ;
2022-01-20 02:44:43 +01:00
}
2022-04-06 18:38:54 +02:00
var buffer = memStream . GetBuffer ( ) ;
int frameLength = ( int ) memStream . Length ;
BinaryPrimitives . WriteInt32LittleEndian ( buffer , frameLength - 4 ) ; // patch payload_len with correct value
#if OBFUSCATION
_sendCtr . EncryptDecrypt ( buffer , frameLength ) ;
#endif
await _networkStream . WriteAsync ( buffer , 0 , frameLength ) ;
_lastSentMsg = msg ;
}
finally
{
_sendSemaphore . Release ( ) ;
2022-01-20 02:44:43 +01:00
}
2021-09-23 13:13:36 +02:00
}
2022-04-11 12:08:17 +02:00
internal async Task < T > InvokeBare < T > ( IMethod < T > request )
2021-09-23 13:13:36 +02:00
{
2022-04-06 18:38:54 +02:00
if ( _bareRpc ! = null ) throw new ApplicationException ( "A bare request is already undergoing" ) ;
2022-10-08 15:06:36 +02:00
retry :
2022-04-11 12:08:17 +02:00
_bareRpc = new Rpc { type = typeof ( T ) } ;
2022-04-06 18:38:54 +02:00
await SendAsync ( request , false , _bareRpc ) ;
2022-10-08 15:06:36 +02:00
var result = await _bareRpc . Task ;
if ( result is ReactorError ) goto retry ;
return ( T ) result ;
2021-09-23 13:13:36 +02:00
}
2022-04-06 18:38:54 +02:00
/// <summary>Call the given TL method <i>(You shouldn't need to use this method directly)</i></summary>
2022-04-11 12:08:17 +02:00
/// <typeparam name="T">Expected type of the returned object</typeparam>
2022-04-06 18:38:54 +02:00
/// <param name="query">TL method structure</param>
/// <returns>Wait for the reply and return the resulting object, or throws an RpcException if an error was replied</returns>
2022-04-11 12:08:17 +02:00
public async Task < T > Invoke < T > ( IMethod < T > query )
2021-09-23 13:13:36 +02:00
{
2022-07-12 01:31:18 +02:00
if ( _dcSession . WithoutUpdates & & query is not IMethod < Pong > )
query = new TL . Methods . InvokeWithoutUpdates < T > { query = query } ;
2022-04-11 12:08:17 +02:00
bool got503 = false ;
2022-04-06 18:38:54 +02:00
retry :
2022-04-11 12:08:17 +02:00
var rpc = new Rpc { type = typeof ( T ) } ;
2022-04-06 18:38:54 +02:00
await SendAsync ( query , true , rpc ) ;
var result = await rpc . Task ;
switch ( result )
2021-09-25 19:43:39 +02:00
{
2022-04-18 22:07:04 +02:00
case null : return default ;
2022-04-11 12:08:17 +02:00
case T resultT : return resultT ;
2022-04-13 15:53:06 +02:00
case RpcError { error_code : var code , error_message : var message } :
int x = - 1 ;
for ( int index = message . Length - 1 ; index > 0 & & ( index = message . LastIndexOf ( '_' , index - 1 ) ) > = 0 ; )
if ( message [ index + 1 ] is > = '0' and < = '9' )
{
int end = + + index ;
do end + + ; while ( end < message . Length & & message [ end ] is > = '0' and < = '9' ) ;
x = int . Parse ( message [ index . . end ] ) ;
message = $"{message[0..index]}X{message[end..]}" ;
break ;
}
if ( code = = 303 & & message . EndsWith ( "_MIGRATE_X" ) )
2021-10-06 07:54:20 +02:00
{
2022-04-13 15:53:06 +02:00
if ( message ! = "FILE_MIGRATE_X" )
2021-10-06 07:54:20 +02:00
{
2022-04-06 18:38:54 +02:00
// this is a hack to migrate _dcSession in-place (staying in same Client):
Session . DCSession dcSession ;
lock ( _session )
2022-04-11 12:08:17 +02:00
dcSession = GetOrCreateDCSession ( x , _dcSession . DataCenter . flags ) ;
2022-04-06 18:38:54 +02:00
Reset ( false , false ) ;
2022-04-11 12:08:17 +02:00
_session . MainDC = x ;
2022-04-06 18:38:54 +02:00
_dcSession . Client = null ;
_dcSession = dcSession ;
_dcSession . Client = this ;
await ConnectAsync ( ) ;
goto retry ;
2021-10-06 07:54:20 +02:00
}
2022-04-06 18:38:54 +02:00
}
2022-04-26 21:13:23 +02:00
else if ( code = = 420 & & ( message . EndsWith ( "_WAIT_X" ) | | message . EndsWith ( "_DELAY_X" ) ) )
2022-04-06 18:38:54 +02:00
{
2022-04-11 12:08:17 +02:00
if ( x < = FloodRetryThreshold )
2021-10-06 07:54:20 +02:00
{
2022-10-01 13:56:43 +02:00
if ( x = = 0 ) x = 1 ;
2022-04-11 12:08:17 +02:00
await Task . Delay ( x * 1000 ) ;
2022-04-06 18:38:54 +02:00
goto retry ;
2021-10-06 07:54:20 +02:00
}
}
2022-04-13 15:53:06 +02:00
else if ( code = = - 503 & & ! got503 )
2022-02-27 22:06:13 +01:00
{
2022-04-06 18:38:54 +02:00
got503 = true ;
goto retry ;
2022-02-27 22:06:13 +01:00
}
2022-04-13 15:53:06 +02:00
else if ( code = = 500 & & message = = "AUTH_RESTART" )
2022-04-06 18:38:54 +02:00
{
_session . UserId = 0 ; // force a full login authorization flow, next time
2022-09-20 17:30:32 +02:00
User = null ;
2022-04-06 18:38:54 +02:00
lock ( _session ) _session . Save ( ) ;
}
2022-04-13 15:53:06 +02:00
throw new RpcException ( code , message , x ) ;
2022-04-06 18:38:54 +02:00
case ReactorError :
goto retry ;
2021-12-31 08:38:41 +01:00
default :
2022-04-11 12:08:17 +02:00
throw new ApplicationException ( $"{query.GetType().Name} call got a result of type {result.GetType().Name} instead of {typeof(T).Name}" ) ;
2021-12-31 08:38:41 +01:00
}
}
2021-08-04 00:40:09 +02:00
}
}