Kritika protokolu a organizačních přístupů Telegramu. Část 1, technická: zkušenosti s psaním klienta od nuly - TL, MT

V poslední době se na Habrého začínají častěji objevovat příspěvky o tom, jak dobrý je Telegram, jak brilantní a zkušení bratři Durovové jsou v budování síťových systémů atd. Zároveň se jen velmi málo lidí skutečně ponořilo do technického zařízení - nanejvýš používají poměrně jednoduché (a velmi odlišné od MTProto) založené na JSON API pro roboty a obvykle jen akceptují o víře všechny ty chvály a PR, které se točí kolem messengeru. Téměř před rokem a půl začal můj kolega v NPO Echelon Vasily (jeho účet na Habré byl smazán spolu s konceptem) od nuly psát vlastního klienta Telegram v Perlu a později se přidal i autor těchto řádků. Proč Perl, zeptají se hned někteří? Protože takové projekty již existují v jiných jazycích, ve skutečnosti o to nejde, může tam být jakýkoli jiný jazyk hotová knihovna, a podle toho musí autor jít celou cestu od nuly. Navíc kryptografie je taková věc – důvěřuj, ale prověřuj. U produktu zaměřeného na bezpečnost se nemůžete spoléhat jen na hotovou knihovnu dodavatele a slepě tomu věřit (to je však téma na více v druhém díle). V tuto chvíli knihovna funguje docela dobře na „střední“ úrovni (umožňuje zadávat jakékoli API požadavky).

V této sérii příspěvků však nebude moc kryptografie a matematiky. Bude tu ale mnoho dalších technických detailů a architektonických berliček (bude se hodit i pro ty, kteří nebudou psát od nuly, ale budou používat knihovnu v jakémkoli jazyce). Hlavním cílem tedy bylo pokusit se implementovat klienta od nuly podle oficiální dokumentace. Tedy předpokládejme, že zdrojový kód oficiálních klientů je uzavřen (opět ve druhém díle prozradíme podrobněji téma, co to vlastně je tam tak), ale jako například za starých časů existuje standard jako RFC - je možné napsat klienta pouze podle specifikace, „bez nakouknutí“ na zdroj, dokonce i oficiální (Telegram Desktop, mobil) , dokonce i neoficiální Telethon?

Obsah:

Dokumentace ... je tam? Je to pravda?..

Fragmenty poznámek k tomuto článku se začaly sbírat loni v létě. To vše na oficiálních stránkách https://core.telegram.org dokumentace byla k vrstvě 23, tzn. uvízl někde v roce 2014 (pamatujete, tehdy ještě nebyly ani kanály?). To samozřejmě mělo teoreticky umožnit v roce 2014 implementovat klienta s funkcionalitou. Ale i v tomto stavu byla dokumentace za prvé neúplná, za druhé si místy odporovala. Bylo to před více než měsícem, v září 2019 náhodou bylo zjištěno, že stránka má velkou aktualizaci dokumentace, pro zcela čerstvou vrstvu 105, s poznámkou, že nyní je třeba vše znovu přečíst. Mnoho článků bylo revidováno, ale mnoho zůstalo nezměněno. Proto při čtení níže uvedené kritiky týkající se dokumentace byste měli mít na paměti, že některé z těchto věcí již nejsou relevantní, ale některé jsou stále docela. Vždyť 5 let v moderním světě není jen hodně, ale velmi hodně. Od té doby (zejména pokud neberete v úvahu od té doby vyřazené a vzkříšené geochaty) se počet API metod ve schématu rozrostl ze stovky na více než dvě stě padesát!

Kde začínáte jako mladý spisovatel?

Nezáleží na tom, zda píšete od začátku nebo používáte například hotové knihovny jako Telethon pro Python nebo Madeline pro PHP, v každém případě budete nejprve potřebovat zaregistrujte svou aplikaci - získat parametry api_id и api_hash (ti, kteří pracovali s VKontakte API, okamžitě rozumí), podle kterého server identifikuje aplikaci. Tento musím z právních důvodů, ale o tom, proč jej autoři knihoven nemohou publikovat, si povíme více v druhé části. Snad budete s testovacími hodnotami spokojeni, i když jsou velmi omezené – faktem je, že nyní se můžete registrovat na své číslo pouze jeden aplikaci, takže bezhlavě nespěchejte.

Nyní nás z technického hlediska mělo zajímat, že po registraci bychom měli od Telegramu dostávat upozornění na aktualizace dokumentace, protokolu atp. To znamená, že by se dalo předpokládat, že stránka s doky byla jednoduše „vybodována“ a pokračovala v práci konkrétně s těmi, kteří začali vytvářet klienty, protože. je to jednodušší. Ale ne, nic takového nebylo pozorováno, žádná informace nepřišla.

A pokud píšete od nuly, pak je využití přijatých parametrů vlastně ještě daleko. Ačkoli https://core.telegram.org/ a mluví o nich jako první v Getting Started, ve skutečnosti je musíte nejprve implementovat protokol MTProto - ale když věříš rozložení podle modelu OSI na konci stránky obecného popisu protokolu pak zcela marně.

Ve skutečnosti, jak před MTProto, tak po něm, na několika úrovních najednou (jak říkají zahraniční síťaři pracující v jádře OS, narušení vrstvy) se do cesty postaví velké, bolestivé a hrozné téma ...

Binární serializace: TL (Type Language) a jeho schéma, vrstvy a mnoho dalších děsivých slov

Toto téma je ve skutečnosti klíčem k problémům Telegramu. A pokud se do toho pokusíte ponořit, objeví se mnoho hrozných slov.

Takže schéma. Pokud si pamatujete toto slovo, řekněte: Schéma JSONMyslel jsi správně. Cíl je stejný: nějaký jazyk pro popis možného souboru přenášených dat. Tím ve skutečnosti podobnost končí. Pokud ze stránky protokol MTProto, nebo ze zdrojového stromu oficiálního klienta, zkusíme otevřít nějaké schéma, uvidíme něco jako:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

Člověk, který to vidí poprvé, intuitivně rozpozná jen část toho, co je napsáno - no, jsou to zřejmě struktury (i když kde je název, vlevo nebo vpravo?), Jsou v nich pole, po kterých typ prochází dvojtečkou... pravděpodobně. Zde v lomených závorkách jsou pravděpodobně šablony jako v C ++ (ve skutečnosti ne ve skutečnosti). A co znamenají všechny ostatní symboly, otazníky, vykřičníky, procenta, mřížky (a samozřejmě znamenají různé věci na různých místech), někde přítomné, ale někde ne, hexadecimální čísla - a hlavně, jak z toho dostat právo (který server neodmítne) byte stream? Musíte si přečíst dokumentaci (Ano, poblíž jsou odkazy na schéma ve verzi JSON - ale to není jasnější).

Otevření stránky Serializace binárních dat a vrhnout se do kouzelného světa hub a diskrétní matematiky, něco podobného jako matan ve 4. ročníku. Abeceda, typ, hodnota, kombinátor, funkční kombinátor, normální tvar, složený typ, polymorfní typ... a to je jen první stránka! Další na vás čeká Jazyk TL, která sice již obsahuje ukázku triviální žádosti a odpovědi, ale na typičtější případy odpověď vůbec neposkytuje, což znamená, že převyprávění matematiky přeložené z ruštiny do angličtiny se budete muset brodit na osmi dalších vnořených stránky!

Čtenáři znalí funkcionálních jazyků a automatického vyvozování typu samozřejmě v tomto jazyce viděli popisy, byť z příkladu, mnohem známější a mohou říci, že to obecně v zásadě není špatné. Námitky k tomu jsou:

  • ano, cíl zní to dobře, ale bohužel nebylo dosaženo
  • vzdělání na ruských univerzitách se liší i mezi IT specializacemi - ne každý čte odpovídající kurz
  • Konečně, jak uvidíme, v praxi tomu tak je není nutné, protože se používá pouze omezená podmnožina dokonce i TL, která byla popsána

Jak bylo řečeno LeonNerd na kanálu #perl v síti FreeNode IRC se snaží implementovat bránu z Telegramu do Matrixu (překlad citátu je z paměti nepřesný):

Připadá mi to, jako by se někdo, kdo byl poprvé seznámen s teorií typů, nadchl a začal si s ní hrát, aniž by mu bylo úplně jedno, jestli je to nutné v praxi.

Přesvědčte se sami, zda potřeba holých typů (int, long atd.) jako něčeho elementárního nevyvolává otázky - nakonec je třeba je implementovat ručně - zkusme z nich například odvodit vektor. to je ve skutečnosti pole, pokud výsledné věci nazýváte pravými jmény.

Ale předtím

Stručný popis podmnožiny syntaxe TL pro ty, kteří ne… přečtěte si oficiální dokumentaci

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

Vždy spustí definici návrhář, načež volitelně (v praxi vždy) přes symbol # by CRC32 z normalizovaného popisného řetězce daného typu. Dále následuje popis polí, pokud jsou - typ může být prázdný. Vše končí rovnítkem, názvem typu, ke kterému daný konstruktor - tedy vlastně podtyp - patří. Typ napravo od rovnítka je polymorfní - to znamená, že může odpovídat několika konkrétním typům.

Pokud se definice vyskytuje za řádkem ---functions---, pak syntaxe zůstane stejná, ale význam bude jiný: konstruktor se stane názvem funkce RPC, pole se stanou parametry (dobře, to znamená, že zůstane přesně stejná daná struktura, jak je popsáno níže, bude to jen daný význam) a "polymorfní typ" je typ vráceného výsledku. Pravda, stále zůstane polymorfní – právě definováno v sekci ---types---a tento konstruktor nebude uvažován. Typ přetížení volaných funkcí jejich argumenty, tzn. z nějakého důvodu několik funkcí se stejným názvem, ale odlišným podpisem, jako v C++, není v TL k dispozici.

Proč "konstruktor" a "polymorfní", když to není OOP? No, ve skutečnosti o tom bude pro někoho jednodušší přemýšlet z hlediska OOP - polymorfního typu jako abstraktní třídy a konstruktory jsou její přímé potomky, navíc final v terminologii řady jazyků. Ve skutečnosti, samozřejmě, zde podobnosti se skutečně přetíženými konstruktorovými metodami v programovacích jazycích OO. Vzhledem k tomu, že zde existují pouze datové struktury, neexistují žádné metody (ačkoli níže uvedený popis funkcí a metod je docela schopný vytvořit v hlavě zmatek, co to je, ale to je o něčem jiném) - konstruktor si můžete představit jako hodnota, ze které staví se zadejte při čtení proudu bajtů.

Jak se to stane? Deserializátor, který vždy čte 4 bajty, vidí hodnotu 0xcrc32 - a chápe, co se bude dít dál field1 s typem int, tj. čte přesně 4 bajty v tomto překrývajícím poli s typem PolymorType číst. Vidí 0x2crc32 a chápe, že existují dvě další pole, první long, takže přečteme 8 bajtů. A pak zase složitý typ, který se stejným způsobem deserializuje. Například, Type3 mohl být deklarován ve schématu, jakmile dva konstruktory, respektive, dále musí splňovat jeden 0x12abcd34, po kterém je třeba přečíst další 4 bajty intNebo 0x6789cdef, po kterém nebude nic. Cokoli jiného - musíte vyvolat výjimku. V každém případě se poté vrátíme ke čtení 4 bajtů int okraje field_c в constructorTwo a tím končíme čtení našeho PolymorType.

Konečně, pokud je chycen 0xdeadcrc pro constructorThree, pak se věci zkomplikují. Naše první pole bit_flags_of_what_really_present s typem # - ve skutečnosti je to jen alias pro typ natcož znamená "přirozené číslo". To znamená, že ve skutečnosti je int bez znaménka jediný případ, kdy se čísla bez znaménka nacházejí ve skutečných schématech. Takže další je konstrukce s otazníkem, což znamená, že toto je pole - bude přítomno na drátu pouze tehdy, pokud je v odkazovaném poli nastaven odpovídající bit (přibližně jako ternární operátor). Předpokládejme, že tento bit byl zapnutý, pak musíte přečíst pole jako Type, který má v našem příkladu 2 konstruktory. Jeden je prázdný (skládá se pouze z identifikátoru), druhý má pole ids s typem ids:Vector<long>.

Možná si myslíte, že jak šablony, tak generika jsou dobré nebo Java. Ale ne. Téměř. Tento jediný případ úhlových závorek v reálných obvodech a používá se POUZE pro Vector. V bajtovém toku to budou 4 CRC32 bajty pro samotný typ Vector, vždy stejné, pak 4 bajty - počet prvků pole a pak tyto prvky samotné.

Přidejte k tomu skutečnost, že serializace vždy probíhá ve slovech o velikosti 4 bajtů, všechny typy jsou její násobky - jsou také popsány vestavěné typy bytes и string s ruční serializací délky a tímto zarovnáním po 4 - no, zdá se, že to zní normálně a dokonce relativně efektivně? Ačkoli se o TL tvrdí, že je to efektivní binární serializace, ale k čertu s nimi, s rozšířením čehokoli, dokonce i booleovských hodnot a jednoznakových řetězců až do 4 bajtů, bude JSON stále mnohem tlustší? Podívejte, i nepotřebná pole lze přeskočit pomocí bitových příznaků, vše je v pořádku a dokonce rozšiřitelné do budoucna, přidali jste později do konstruktoru nová volitelná pole?..

Ale ne, pokud si nepřečtete můj stručný popis, ale celou dokumentaci a budete přemýšlet o implementaci. Za prvé, CRC32 konstruktoru je vypočítáno pomocí normalizovaného řetězce popisu textu schématu (odstranění přebytečných bílých znaků atd.) - takže pokud je přidáno nové pole, změní se řetězec popisu typu a tím i jeho CRC32 a následně serializace. A co by starý klient dělal, kdyby dostal pole s novými vlajkami, ale nevěděl, co s nimi dál? ..

Za druhé, pamatujme CRC32, který se zde používá v podstatě jako hashovací funkce jednoznačně určit, jaký typ je (de)serializován. Zde stojíme před problémem kolizí – a ne, pravděpodobnost není jedna ku 232, ale mnohem více. Kdo si pamatoval, že CRC32 je navržen tak, aby detekoval (a opravoval) chyby v komunikačním kanálu a podle toho zlepšoval tyto vlastnosti na úkor ostatních? Například se nestará o permutaci bajtů: pokud počítáte CRC32 ze dvou řádků, ve druhém prohodíte první 4 bajty s dalšími 4 bajty - bude to stejné. Když máme jako vstup textové řetězce z latinské abecedy (a malou interpunkci) a tato jména nejsou nijak zvlášť náhodná, pravděpodobnost takové permutace se značně zvyšuje.

Mimochodem, kdo zkontroloval, co tam bylo doopravdy CRC32? V jednom z raných zdrojů (dokonce před Waltmanem) existovala hašovací funkce, která násobila každý znak číslem 239, tak milovaným těmito lidmi, ha ha!

Nakonec, dobře, jsme si uvědomili, že konstruktéři s typem pole Vector<int> и Vector<PolymorType> bude mít jiný CRC32. A co prezentace na lince? A pokud jde o teorii, stane se součástí typu? Řekněme, že předáme pole deseti tisíc čísel, dobře, s Vector<int> vše je jasné, délka a dalších 40000 XNUMX bajtů. A jestli tohle Vector<Type2>, který se skládá pouze z jednoho pole int a je to jediný v typu - musíme opakovat 10000xabcdef0 34 4krát a pak XNUMX bajty int, nebo nám to jazyk dokáže ZOBRAZIT z konstruktoru fixedVec a místo 80000 40000 bajtů přenést zase jen XNUMX XNUMX?

To není vůbec prázdná teoretická otázka - představte si, že dostanete seznam uživatelů skupiny, z nichž každý má id, jméno, příjmení - rozdíl v množství dat přenesených přes mobilní připojení může být značný. Je to efektivita serializace telegramu, která je nám inzerována.

Tak…

Vektor, který se nepodařilo odvodit

Pokud se pokusíte procházet popisnými stránkami kombinátorů a kolem nich, uvidíte, že vektor (a dokonce i matice) se formálně snaží odvodit několik listů pomocí n-tic. Ale nakonec jsou zatlučeni, poslední krok je přeskočen a je jednoduše dána definice vektoru, který také není vázán na typ. Co se tady děje? V jazycích programování, zejména funkčních, je zcela typické popisovat strukturu rekurzivně - překladač s jeho líným vyhodnocením vše pochopí a udělá to. V jazyce serializace dat ale je zapotřebí EFEKTIVITA: stačí jednoduše popsat seznam, tj. struktura dvou prvků - první je datový prvek, druhý je stejná struktura samotná nebo prázdné místo pro ocas (balení (cons) v Lisp). Ale to bude evidentně vyžadovat každý element navíc utratí 4 bajty (CRC32 v případě TL) k popisu jeho typu. Pole je snadné popsat pevná velikost, ale v případě pole dříve neznámé délky odlomíme.

Takže protože TL neumožňuje výstup vektoru, musel být přidán na stranu. Nakonec dokumentace říká:

Serializace vždy používá stejný konstruktor „vector“ (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”), který není závislý na konkrétní hodnotě proměnné typu t.

Hodnota volitelného parametru t není zapojena do serializace, protože je odvozena od typu výsledku (vždy známého před deserializací).

Podívat se zblízka: vector {t:Type} # [ t ] = Vector t - ale nikde samotná definice neříká, že první číslo se musí rovnat délce vektoru! A to odnikud nevyplývá. To je daná věc, kterou musíte mít na paměti a realizovat ji rukama. Jinde dokumentace dokonce upřímně uvádí, že typ je falešný:

Polymorfní pseudotyp Vector t je „typ“, jehož hodnota je posloupnost hodnot libovolného typu t, ať už v rámečku nebo holé.

… ale nesoustředí se na to. Když se unavení broděním protahováním matematiky (možná vám známe i z vysokoškolského kurzu) rozhodnete zabodovat a sledovat, jak se s ní vlastně v praxi pracuje, zůstane vám v hlavě dojem: zde Serious Mathematics vychází z , samozřejmě Cool People (dva matematici - vítěz ACM), a ne jen tak někdo. Cíl – marnotratnost – byl dosažen.

Mimochodem, o počtu. Odvolání # je to synonymum nat, přirozené číslo:

Existují typové výrazy (typeexpr) a číselné výrazy (nat-expr). Jsou však definovány stejným způsobem.

type-expr ::= expr
nat-expr ::= expr

ale v gramatice jsou popsány stejně, tzn. tento rozdíl je opět nutné pamatovat a vložit do realizace ručně.

No, ano, typy šablon (vector<int>, vector<User>) mají společný identifikátor (#1cb5c415), tj. pokud víte, že hovor je deklarován jako

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

pak čekáte nejen na vektor, ale na vektor uživatelů. Přesněji, měl počkat - v reálném kódu bude mít každý prvek, ne-li holý typ, konstruktor a v dobrém slova smyslu při implementaci by bylo nutné zkontrolovat - a byli jsme posláni přesně v každém prvku tohoto vektoru ten typ? A kdyby to bylo nějaké PHP, ve kterém pole může obsahovat různé typy v různých prvcích?

V tuto chvíli vás začne zajímat – je takový TL potřeba? Možná by pro vozík bylo možné použít lidský serializátor, stejný protobuf, který už tehdy existoval? Byla to teorie, podívejme se na praxi.

Stávající implementace TL v kódu

TL se zrodil v útrobách VKontakte ještě před známými událostmi s prodejem Durovova podílu a (jistě), ještě před vývojem Telegramu. A to v open source zdroje první implementace můžete najít spoustu vtipných berliček. A samotný jazyk tam byl implementován úplněji, než je tomu nyní v Telegramu. Například hashe se ve schématu vůbec nepoužívají (myšleno vestavěný pseudotyp (jako vektor) s deviantním chováním). Nebo

Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

ale podívejme se pro úplnost na obrázek, abychom takříkajíc vystopovali vývoj obra myšlení.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Nebo tento krásný:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

Tento fragment se týká šablon, jako jsou:

intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

Toto je definice typu šablony hashmap, jako vektor párů int - Typ. V C++ by to vypadalo nějak takto:

    template <T> class IntHash {
      vector<pair<int,T>> _map;
    }

tak, alpha - klíčové slovo! Ale jen v C++ můžeš napsat T, ale musíš napsat alfa, beta... Ale ne víc než 8 parametrů, fantazie skončila na theta. Zdá se tedy, že kdysi v Petrohradě probíhaly přibližně takové dialogy:

-- Надо сделать в TL шаблоны
-- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
-- Грамматика? Ну потом напишем

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

Ale šlo o první rozvrženou implementaci TL "obecně". Pojďme k úvahám o implementacích ve skutečných telegramových klientech.

Basilovo slovo:

Vasily, [09.10.18/17/07 XNUMX:XNUMX] Nejvíc ze všeho je prdel žhavý z toho, že podělali hromadu abstrakcí a pak na ně zatloukli šroub a dali berle na codegeger
Jako výsledek, nejprve z doků pilot.jpg
Pak z kódu jekichan.webp

Samozřejmě, od lidí obeznámených s algoritmy a matematikou můžeme očekávat, že četli Aho, Ullmana a jsou obeznámeni s nástroji, které se v tomto odvětví staly de facto standardem pro psaní kompilátorů pro jejich DSL, že? ..

Podle telegram-cli je Vitaliy Valtman, jak lze pochopit z výskytu formátu TLO mimo jeho (cli) limity, členem týmu - nyní je přidělena knihovna pro parsování TL oddělenějaký je z ní dojem TL analyzátor? ..

16.12 04:18 Vasily: podle mě někdo nezvládl lex + yacc
16.12 04:18 Vasilij: jinak si to neumím vysvětlit
16.12 04:18 Vasilij: no, nebo byli placeni za počet linek ve VK
16.12 04:19 Vasilij: 3k+ řádků ostatních<censored> místo analyzátoru

Možná výjimka? Podívejme se jak dělá toto je OFICIÁLNÍ klient — Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

1100+ řádků v Pythonu, pár regulárních výrazů + speciální případy typu vector, který je samozřejmě ve schématu deklarován jak má být podle syntaxe TL, ale dali to na tuto syntaxi, více rozebrat ... Otázkou je, proč se vším tím zázrakem obtěžovatиdalší puff, pokud to stejně nikdo nebude analyzovat podle dokumentace?!

Mimochodem... Pamatujete si, že jsme mluvili o kontrole CRC32? Takže v generátoru kódu Telegram Desktop je seznam výjimek pro ty typy, ve kterých se počítá CRC32 nesouhlasí jak je znázorněno na obrázku!

Vasilij, [18.12 22:49] a tady bys měl popřemýšlet, zda je takový TL potřeba
pokud bych se chtěl popasovat s alternativními implementacemi, začal bych vkládat zalomení řádků, polovina parserů se rozbije na víceřádkových definicích
tdesktop však také

Pamatujte na bod o jednolinkách, vrátíme se k němu o něco později.

Dobře, telegram-cli je neoficiální, Telegram Desktop je oficiální, ale co ostatní? A kdo ví?... V kódu klienta Android nebyl vůbec žádný analyzátor schémat (což vyvolává otázky o open source, ale to je pro druhou část), ale bylo tam několik dalších vtipných kousků kódu, ale o nich v podsekci níže.

Jaké další otázky vyvolává serializace v praxi? Například to samozřejmě podělali s bitovými poli a podmíněnými poli:

vasily: flags.0? true
znamená, že pole je přítomno a má hodnotu true, pokud je nastaven příznak

vasily: flags.1? int
znamená, že pole je přítomno a je třeba jej deserializovat

Vasilij: Prde, nespal, co to děláš!
Vasily: Někde v dokumentu je zmínka, že true je holý typ nulové délky, ale je nereálné sbírat něco z jejich dokumentů
Vasilij: Ani v otevřených implementacích nic takového není, ale je tam spousta berliček a rekvizit

Co takhle Telethon? Při pohledu dopředu na téma MTProto, příklad - takové kousky v dokumentaci jsou, ale znaménko % popisuje se pouze jako "odpovídající danému holému typu", tzn. v příkladech níže buď chyba, nebo něco nezdokumentovaného:

Vasilij, [22.06.18/18/38 XNUMX:XNUMX] Na jednom místě:

msg_container#73f1f8dc messages:vector message = MessageContainer;

V jiném:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

A to jsou dva velké rozdíly, v reálném životě přichází nějaký nahý vektor

Holé vektorové definice jsem neviděl a nenarazil jsem na to

Analýza psaná v telethonu ručně

Jeho schéma komentovalo definici msg_container

Opět zůstává otázka o %. Není popsáno.

Vadim Goncharov, [22.06.18/19/22 XNUMX:XNUMX] a na tdesktopu?

Vasily, [22.06.18/19/23 XNUMX:XNUMX] Ale ten jejich TL parser na regulátorech to asi taky nesežere

// parsed manually

TL je krásná abstrakce, nikdo ji neimplementuje úplně

A v jejich verzi schématu není žádné %.

Tady si ale dokumentace odporuje, takže xs

Našlo se to v gramatice, jen mohli zapomenout popsat sémantiku

No viděl jsi dok na TL, bez půl litru na to nepřijdeš

"No, řekněme," řekne jiný čtenář, "kritizujete všechno, tak to ukažte, jak to má."

Vasily odpovídá: „Pokud jde o analyzátor, potřebuji věci jako

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

nějak víc než

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

nebo

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

toto je CELÝ lexer:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

těch. jednodušší je to vyjádřit mírně."

Obecně lze říci, že se analyzátor a generátor kódu pro skutečně používanou podmnožinu TL vejde do asi 100 řádků gramatiky a ~ 300 řádků generátoru (včetně všech printvygenerovaný kód), včetně typových dobrot, typových informací pro introspekci v každé třídě. Každý polymorfní typ se změní na prázdnou abstraktní základní třídu a konstruktéři z ní dědí a mají metody pro serializaci a deserializaci.

Nedostatek typů v typovém jazyce

Silné psaní je dobré, ne? Ne, toto není holivar (i když preferuji dynamické jazyky), ale postulát v rámci TL. Na jeho základě by nám jazyk měl poskytovat nejrůznější kontroly. No dobře, ať ne on, ale realizace, ale měl by je alespoň popsat. A jaké příležitosti chceme?

Za prvé, omezení. Zde vidíme v dokumentaci pro nahrávání souborů:

Binární obsah souboru je poté rozdělen na části. Všechny části musí mít stejnou velikost ( část_velikost ) a musí být splněny následující podmínky:

  • part_size % 1024 = 0 (dělitelné 1 kB)
  • 524288 % part_size = 0 (512 kB musí být rovnoměrně dělitelné hodnotou part_size)

Poslední část nemusí splňovat tyto podmínky, pokud je její velikost menší než velikost_části.

Každá část by měla mít pořadové číslo, část_souborus hodnotou v rozsahu od 0 do 2,999.

Po rozdělení souboru na oddíly musíte zvolit způsob jeho uložení na server. použití upload.saveBigFilePart v případě, že je plná velikost souboru větší než 10 MB a upload.saveFilePart pro menší soubory.
[…] může být vrácena jedna z následujících chyb při zadávání dat:

  • FILE_PARTS_INVALID - Neplatný počet dílů. Hodnota není mezi 1..3000

Jsou některé z nich ve schématu? Je to nějak vyjádřitelné pomocí TL? Ne. Ale promiňte, i staromódní Turbo Pascal uměl popsat typy dané rozsahy. A mohl udělat ještě jednu věc, nyní známější jako enum - typ sestávající z výčtu pevného (malého) počtu hodnot. V jazycích, jako je C - numeric, jsme zatím mluvili pouze o typech. čísla. Ale jsou tam i pole, řetězce ... například by se hodilo popsat, že tento řetězec může obsahovat pouze telefonní číslo, ne?

Nic z toho v TL není. Ale existuje například v JSON Schema. A pokud někdo jiný může namítnout dělitelnost 512 KB, že to je ještě potřeba zkontrolovat v kódu, tak se ujistěte, že klient jednoduše nemohl odeslat číslo mimo rozsah 1..3000 (a odpovídající chyba nemohla vzniknout) bylo by to možné, ne? ..

Mimochodem o chybách a návratových hodnotách. Oko je rozmazané i pro ty, kteří s TL pracovali - to nás hned nenapadlo každý funkce v TL může skutečně vrátit nejen popsaný návratový typ, ale i chybu. To však nelze odvodit pomocí samotného TL. Samozřejmě je to i tak pochopitelné a nafig není v praxi nutný (i když ve skutečnosti se RPC dá dělat různými způsoby, k tomu se ještě vrátíme) - ale co Čistota pojmů Matematika abstraktních typů z nebes svět? .. Popadl remorkér - takže zápas.

A nakonec, jak je to s čitelností? No, tam bych obecně rád popis mít to správně ve schématu (opět je to ve schématu JSON), ale když už je to napjaté, tak co praktická stránka - alespoň je všední sledovat rozdíly během aktualizací? Přesvědčte se sami na skutečné příklady:

-channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

nebo

-message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

Někomu se to líbí, ale například GitHub odmítá zvýrazňovat změny uvnitř tak dlouhých řádků. Hra „najdi 10 rozdílů“ a mozek hned vidí, že začátky a konce jsou v obou příkladech stejné, je třeba únavně číst někde uprostřed... Podle mě to není jen teoreticky, ale čistě vizuálně vypadá špinavé a neupravené.

Mimochodem, o čistotě teorie. Proč jsou potřebná bitová pole? Nezdá se, že ano čich špatné z hlediska teorie typů? Vysvětlení lze vidět v dřívějších verzích schématu. Zpočátku ano, bylo to tak, pro každé kýchnutí vznikl nový typ. Tyto základy jsou stále tam v této podobě, například:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

Ale teď si představte, že pokud máte ve struktuře 5 volitelných polí, pak potřebujete 32 typů pro všechny možné možnosti. kombinatorický výbuch. Takže křišťálová čistota teorie TL opět narazila na litinový osel drsné reality serializace.

Navíc tito chlapíci místy sami porušují své vlastní psaní. Například v MTProto (další kapitola) lze odezvu komprimovat Gzipem, vše je rozumné - až na narušení vrstev a schématu. Jednou a nesklidil samotný RpcResult, ale jeho obsah. No, proč to dělat? .. Musel jsem seknout v berli, aby komprese fungovala kdekoli.

Nebo jiný příklad, jednou jsme našli chybu - odesláno InputPeerUser místo InputUser. Nebo naopak. Ale povedlo se! To znamená, že server se nestaral o typ. Jak to může být? Odpověď bude možná vyvolána fragmenty kódu z telegram-cli:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

Jinými slovy, zde je provedena serializace RUČNĚ, nevygenerovaný kód! Možná je server implementován podobným způsobem?... V zásadě to bude fungovat, pokud se to udělá jednou, ale jak to můžete později podpořit aktualizacemi? Není to to, k čemu to schéma bylo? A pak přejdeme k další otázce.

Verzování. Vrstvy

Proč se verze schémat nazývají vrstvy, lze pouze hádat na základě historie publikovaných schémat. Zřejmě se nejprve autorům zdálo, že základní věci lze dělat na nezměněném schématu a pouze tam, kde je to nutné, naznačit konkrétním požadavkům, že se dělají podle jiné verze. V zásadě i dobrý nápad – a nový se jakoby „přimíchá“, navrství na starý. Ale podívejme se, jak se to povedlo. Pravda, nebylo možné se podívat od samého začátku - je to vtipné, ale schéma základní vrstvy prostě neexistuje. Vrstvy začaly na 2. Dokumentace nám říká o speciální funkci TL:

Pokud klient podporuje vrstvu 2, musí být použit následující konstruktor:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

V praxi to znamená, že před každým voláním API je int s hodnotou 0x289dd1f6 musí být přidáno před číslo metody.

Zní to dobře. Ale co se stalo potom? Pak přišel

invokeWithLayer3#b7475268 query:!X = X;

tak co bude dál? Jak je snadné uhodnout

invokeWithLayer4#dea0d430 query:!X = X;

Legrační? Ne, na smích je příliš brzy, přemýšlej o čem každý požadavek z jiné vrstvy je potřeba zabalit do takového speciálního typu - když je máte všechny jiné, jak jinak je rozlišit? A přidání pouhých 4 bajtů dopředu je docela efektivní metoda. Tak

invokeWithLayer5#417a57ae query:!X = X;

Je ale zřejmé, že se z toho po čase stane nějaká bakchanálie. A řešení přišlo:

Aktualizace: Počínaje vrstvou 9, pomocné metody invokeWithLayerN lze použít společně s initConnection

Hurá! Po 9 verzích jsme se konečně dostali k tomu, co se dělalo v internetových protokolech v 80. letech - vyjednávání verze jednou na začátku připojení!

Tak co bude dál?..

invokeWithLayer10#39620c41 query:!X = X;
...
invokeWithLayer18#1c900537 query:!X = X;

A teď se můžete smát. Až po dalších 9 vrstvách konečně přibyl univerzální konstruktor s číslem verze, který je potřeba volat jen jednou na začátku spojení a význam ve vrstvách jakoby zmizel, nyní je to jen podmíněná verze, jako např. všude jinde. Problém je vyřešen.

Že jo?..

Vasily, [16.07.18/14/01 XNUMX:XNUMX] V pátek jsem si myslel:
Teleserver odesílá události bez požadavku. Požadavky je třeba zabalit do InvokeWithLayer. Server nezalamuje aktualizace, neexistuje žádná struktura pro zalamování odpovědí a aktualizací.

Tito. klient nemůže určit vrstvu, ve které chce aktualizace

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX] Není InvokeWithLayer principiálně berlička?

Vasily, [16.07.18/14/02 XNUMX:XNUMX] Toto je jediná cesta

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] což by v podstatě mělo znamenat vrstvení na začátku relace

Z toho mimochodem vyplývá, že downgrade klienta není poskytován

Aktualizace, tzn. typ Updates ve schématu to je to, co server posílá klientovi nikoli jako odpověď na požadavek API, ale sám o sobě, když dojde k události. Toto je složité téma, které bude probráno v jiném příspěvku, ale prozatím je důležité vědět, že server shromažďuje aktualizace, i když je klient offline.

Tedy při odmítnutí zábalu každý balíček k označení jeho verze, takže logicky nastanou následující možné problémy:

  • server odešle klientovi aktualizace dříve, než klient sdělí, kterou verzi podporuje
  • co by se mělo udělat po upgradu klienta?
  • kdo гарантируетže se názor serveru na číslo vrstvy v procesu nezmění?

Myslíte si, že je to čistě teoretické uvažování a v praxi se to stát nemůže, protože server je napsán správně (v každém případě je dobře otestován)? Ha! Bez ohledu na to, jak!

Přesně na to jsme v srpnu narazili. 14. srpna se objevily zprávy, že se na serverech telegramu něco aktualizuje... a pak v protokolech:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

a pak několik megabajtů trasování zásobníku (dobře, ve stejnou dobu bylo opraveno protokolování). Koneckonců, pokud něco nebylo rozpoznáno ve vašem TL - je to binární podle podpisů, dále ve streamu VŠE bude dekódování nemožné. Co v takové situaci dělat?

No, první, co koho napadne, je odpojit se a zkusit to znovu. Nepomohlo. Vygooglili jsme CRC32 - ukázalo se, že to jsou objekty ze schématu 73, i když jsme pracovali na schématu 82. Pečlivě si prohlížíme protokoly - jsou tam identifikátory ze dvou různých schémat!

Možná je problém čistě v našem neoficiálním klientovi? Ne, používáme Telegram Desktop 1.2.17 (verze dodávaná s řadou linuxových distribucí), zapisuje do protokolu výjimek: MTP Neočekávané ID typu #b5223b0f načteno v MTPMessageMedia…

Kritika protokolu a organizačních přístupů Telegramu. Část 1, technická: zkušenosti s psaním klienta od nuly - TL, MT

Google ukázal, že podobný problém se již stal jednomu z neoficiálních klientů, ale pak se čísla verzí a tedy i předpoklady lišily ...

Tak co dělat? Vasily a já jsme se rozdělili: pokusil se aktualizovat schéma na 91, rozhodl jsem se pár dní počkat a zkusit to na 73. Obě metody fungovaly, ale protože jsou empirické, není jasné, kolik verzí musíte vyskočit nahoru nebo dolů, ani jak dlouho musíte čekat .

Později se mi podařilo situaci zopakovat: spustíme klienta, vypneme ho, překompilujeme schéma na jinou vrstvu, restartujeme, znovu zachytíme problém, vrátíme se k předchozímu - ups, žádné přepínání schématu a restartování klienta na několik minuty pomohou. Obdržíte mix datových struktur z různých vrstev.

Vysvětlení? Jak můžete odhadnout z různých nepřímých příznaků, server se skládá z mnoha různých typů procesů na různých počítačích. S největší pravděpodobností jeden ze serverů, který je zodpovědný za „vyrovnávací paměť“, vložil do fronty to, co mu daly vyšší, a daly to podle schématu, které bylo v době generování. A dokud tato fronta nebyla „shnilá“, nedalo se s tím nic dělat.

Ledaže... ale tohle je hrozná berlička?!.. Ne, než se zamyslíme nad bláznivými nápady, podívejme se na kodex oficiálních klientů. Ve verzi pro Android nenajdeme žádný TL parser, ale najdeme tučný soubor (github ho odmítá obarvit) s (de)serializací. Zde jsou úryvky kódu:

public static class TL_message_layer68 extends TL_message {
    public static int constructor = 0xc09be45f;
//...
//еще пачка подобных
//...
    public static class TL_message_layer47 extends TL_message {
        public static int constructor = 0xc992e15c;
        public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            Message result = null;
            switch (constructor) {
                case 0x1d86f70e:
                    result = new TL_messageService_old2();
                    break;
                case 0xa7ab1991:
                    result = new TL_message_old3();
                    break;
                case 0xc3060325:
                    result = new TL_message_old4();
                    break;
                case 0x555555fa:
                    result = new TL_message_secret();
                    break;
                case 0x555555f9:
                    result = new TL_message_secret_layer72();
                    break;
                case 0x90dddc11:
                    result = new TL_message_layer72();
                    break;
                case 0xc09be45f:
                    result = new TL_message_layer68();
                    break;
                case 0xc992e15c:
                    result = new TL_message_layer47();
                    break;
                case 0x5ba66c13:
                    result = new TL_message_old7();
                    break;
                case 0xc06b9607:
                    result = new TL_messageService_layer48();
                    break;
                case 0x83e5de54:
                    result = new TL_messageEmpty();
                    break;
                case 0x2bebfa86:
                    result = new TL_message_old6();
                    break;
                case 0x44f9b43d:
                    result = new TL_message_layer104();
                    break;
                case 0x1c9b1027:
                    result = new TL_message_layer104_2();
                    break;
                case 0xa367e716:
                    result = new TL_messageForwarded_old2(); //custom
                    break;
                case 0x5f46804:
                    result = new TL_messageForwarded_old(); //custom
                    break;
                case 0x567699b3:
                    result = new TL_message_old2(); //custom
                    break;
                case 0x9f8d60bb:
                    result = new TL_messageService_old(); //custom
                    break;
                case 0x22eb6aba:
                    result = new TL_message_old(); //custom
                    break;
                case 0x555555F8:
                    result = new TL_message_secret_old(); //custom
                    break;
                case 0x9789dac4:
                    result = new TL_message_layer104_3();
                    break;

nebo

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

Hmm... to vypadá šíleně. Ale pravděpodobně se jedná o vygenerovaný kód, dobře? .. Ale určitě podporuje všechny verze! Je pravda, že není jasné, proč je vše smícháno na jedné hromadě a tajné chaty a všechny druhy _old7 nějak se nepodobá strojové generaci...Nicméně ze všeho jsem se zbláznil

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Kluci, to se ani nemůžete rozhodnout v rámci jedné vrstvy?! No dobře, řekněme "dva" byli vypuštěni s chybou, no, to se stává, ale TŘI? .. Okamžitě znovu na stejném hrábě? Co je to za pornografii, promiňte? ..

Mimochodem, podobná věc se děje ve zdrojích Telegram Desktop - pokud ano, a několik commitů v řadě do schématu nezmění číslo jeho vrstvy, ale něco opraví. V podmínkách, kdy neexistuje žádný oficiální zdroj dat pro schéma, odkud je mohu získat, kromě oficiálních zdrojů klientů? A vezmete-li to odtud, nemůžete si být jisti, že schéma je zcela správné, dokud nevyzkoušíte všechny metody.

Jak se to dá vůbec otestovat? Doufám, že se příznivci jednotkových, funkčních a dalších testů podělí v komentářích.

Dobře, podívejme se na další část kódu:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

Tento „ručně vytvořený“ komentář zde naznačuje, že pouze část tohoto souboru je napsána ručně (dovedete si představit noční můru údržby?) a zbytek je generován strojově. Pak však vyvstává další otázka – že zdroje jsou dostupné ne úplně (a la bloby pod GPL v linuxovém jádře), ale to už je téma pro druhý díl.

Ale dost. Přejděme k protokolu, nad kterým se celá tato serializace honí.

MT Proto

Takže otevřeme obecný popis и podrobný popis protokolu a první věc, na kterou narazíme, je terminologie. A s hojností všeho. Obecně se zdá, že je to ochranná známka Telegramu - nazývat věci na různých místech různými způsoby nebo různé věci jedním slovem nebo naopak (například v API na vysoké úrovni, pokud vidíte balíček nálepek - toto není to, co jste si mysleli).

Například „message“ (zpráva) a „session“ (session) – zde znamenají něco jiného než v běžném rozhraní klienta Telegram. Se zprávou je vše jasné, mohla by být interpretována z hlediska OOP, nebo jednoduše nazvána slovo „balíček“ - toto je nízká úroveň přenosu, nejsou zde stejné zprávy jako v rozhraní, existuje mnoho služebních. Ale sezení... ale nejdřív.

transportní vrstva

První věcí je doprava. Bude nám řečeno o 5 možnostech:

  • TCP
  • websocket
  • Websocket přes HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18/15/04 XNUMX:XNUMX] A existuje i přenos UDP, ale není doložen

A TCP ve třech variantách

První je podobný UDP přes TCP, každý paket obsahuje pořadové číslo a crc
Proč je tak bolestivé číst doky na vozíku?

Tak a teď TCP již ve 4 variantách:

  • Zkrácený
  • Střední
  • polstrovaný meziprodukt
  • Plný

Ok, Padded medium pro MTProxy, to bylo později přidáno kvůli známým událostem. Ale proč další dvě verze (celkem tři), když jedna mohla? Všechny čtyři se v podstatě liší pouze v tom, jak nastavit délku a užitečné zatížení skutečného hlavního MTProto, o čemž bude řeč dále:

  • ve Zkráceném je to 1 nebo 4 bajty, ale ne 0xef pak tělo
  • v Intermediate jsou to 4 bajty délky a pole a poprvé musí klient odeslat 0xeeeeeeee pro označení, že jde o středně pokročilý
  • in Full, nejnávykovější, z pohledu networkera: délka, pořadové číslo, a NE TEN, který je v podstatě MTProto, tělo, CRC32. Ano, to vše přes TCP. Což nám zajišťuje spolehlivý transport ve formě sériového proudu bajtů, nejsou potřeba žádné sekvence, zejména kontrolní součty. Dobře, teď budu namítat, že TCP má 16bitový kontrolní součet, takže dochází k poškození dat. Skvělé, až na to, že ve skutečnosti máme kryptografický protokol s hashe delšími než 16 bajtů, všechny tyto chyby – a ještě více – budou zachyceny na nesouladu SHA na vyšší úrovni. V CRC32 to nemá smysl.

Srovnejme Zkrácený, kde je možný jeden bajt délky, s Intermediate, což ospravedlňuje „V případě, že je potřeba 4bajtové zarovnání dat“, což je pěkný nesmysl. Věří se, že programátoři telegramů jsou tak nešikovní, že nedokážou číst data ze zásuvky do zarovnané vyrovnávací paměti? Stále to musíte udělat, protože čtení vám může vrátit libovolný počet bajtů (a existují například i proxy servery ...). Nebo na druhou stranu, proč se obtěžovat se Zkráceným, když máme pořád navrchu pořádné výplně od 16 bajtů – ušetříte 3 bajty někdy ?

Člověk má dojem, že Nikolaj Durov velmi rád vymýšlí kola, včetně síťových protokolů, bez skutečné praktické potřeby.

Další možnosti dopravy vč. Web a MTProxy nebudeme nyní zvažovat, možná v jiném příspěvku, pokud bude požadavek. Právě o této MTProxy si nyní připomeneme, že brzy po jejím vydání v roce 2018 se poskytovatelé rychle naučili přesně ji blokovat, určenou pro blokový obchvatPodle velikost balíku! A také to, že server MTProxy napsaný (opět Waltmanem) v C byl zbytečně vázán na specifika Linuxu, ačkoliv nebyl vůbec vyžadován (Phil Kulin potvrdí), a že podobný server buď na Go nebo na Node.js vejde méně než sto řádků.

Ale závěry o technické gramotnosti těchto lidí vyvodíme na konci oddílu, po zvážení dalších otázek. Přejděme zatím k 5. vrstvě OSI, session – na kterou umístili MTProto session.

Klíče, zprávy, relace, Diffie-Hellman

Dali to tam ne úplně správně... Relace není stejná relace, která je viditelná v rozhraní pod Aktivní relace. Ale v pořádku.

Kritika protokolu a organizačních přístupů Telegramu. Část 1, technická: zkušenosti s psaním klienta od nuly - TL, MT

Zde jsme z transportní vrstvy obdrželi řetězec bajtů známé délky. Toto je buď zašifrovaná zpráva, nebo prostý text – pokud jsme stále ve fázi vyjednávání klíče a skutečně to děláme. O kterém z těch pojmů zvaných „klíč“ mluvíme? Pojďme objasnit tento problém pro samotný Telegramový tým (omlouvám se za překlad vlastní dokumentace z angličtiny buď unavenému mozku ve 4 ráno, bylo jednodušší nechat některé fráze tak, jak jsou):

Existují dvě entity tzv Zasedání - jedna v uživatelském rozhraní oficiálních klientů pod "aktuálními relacemi", kde každá relace odpovídá celému zařízení / OS.
Druhý - relace MTProto, která má v sobě pořadové číslo zprávy (v nízkoúrovňovém smyslu) a která může trvat mezi různými připojeními TCP. Několik relací MTProto lze nastavit současně, například pro urychlení stahování souborů.

Mezi těmito dvěma zasedání je koncept povolení. V degenerovaném případě se to dá říct relace uživatelského rozhraní je stejné jako povoleníAle bohužel, je to složité. Díváme se:

  • Uživatel na novém zařízení nejprve vygeneruje auth_key a váže to na účet např. SMS - proto povolení
  • Stalo se to uvnitř prvního relace MTProto, který má session_id uvnitř sebe.
  • V tomto kroku, kombinace povolení и session_id by se dalo nazvat instance - toto slovo se nachází v dokumentaci a kódu některých klientů
  • Poté může klient otevřít někteří MTProto relace pod stejným auth_key - do stejného DC.
  • Jednoho dne si klient musí vyžádat soubor další DC - a pro tento DC bude vygenerován nový auth_key !
  • Sdělit systému, že se nejedná o registraci nového uživatele, ale o toho samého povolení (relace uživatelského rozhraní), klient používá volání API auth.exportAuthorization v domácím DC auth.importAuthorization v novém DC.
  • Přesto jich může být několik otevřených MTProto relace (každý má své session_id) k tomuto novému DC, pod jeho auth_key.
  • Nakonec může klient chtít Perfect Forward Secrecy. Každý auth_key byl stálý klíč - na DC - a klient může volat auth.bindTempAuthKey k použití dočasný auth_key - a opět jen jeden temp_auth_key na DC, společné pro všechny MTProto relace do tohoto DC.

Všimněte si, že sůl (a budoucí soli) také jeden na auth_key těch. sdíleno mezi všemi MTProto relace do stejného DC.

Co znamená „mezi různými připojeními TCP“? Znamená to, že toto něco jako autorizační cookie na webu - přetrvává (přežívá) mnoho TCP spojení s tímto serverem, ale jednoho dne se pokazí. Pouze na rozdíl od HTTP jsou v MTProto uvnitř relace zprávy postupně číslovány a potvrzovány, vstoupily do tunelu, spojení bylo přerušeno - po navázání nového připojení server laskavě odešle v této relaci vše, co nedoručil v předchozí TCP spojení.

Výše uvedené informace jsou však po mnoha měsících soudních sporů ždímačkou. Implementujeme mezitím našeho klienta od nuly? - vraťme se na začátek.

Takže generujeme auth_key na verze Diffie-Hellmana z Telegramu. Zkusme porozumět dokumentaci...

Vasily, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (libovolné náhodné bajty); taková, že délka je rovna XNUMX bytům;
encrypted_data := RSA(data_with_hash, server_public_key); číslo dlouhé 255 bajtů (big endian) se zvýší na požadovanou mocninu nad požadovaným modulem a výsledek se uloží jako číslo o velikosti 256 bajtů.

Dostali trochu drogy DH

Nevypadá jako DH zdravého člověka
V dx nejsou žádné dva veřejné klíče

No, nakonec jsme na to přišli, ale usazenina zůstala - doklad o práci dělá klient, že dokázal číslo rozložit. Typ ochrany proti DoS útokům. A klíč RSA se používá pouze jednou v jednom směru, v podstatě pro šifrování new_nonce. Ale i když se tato zdánlivě jednoduchá operace podaří, čemu budete muset čelit?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ještě jsem nedosáhl žádosti o aplikaci

Poslal jsem žádost na DH

A v doku na transportu je napsáno, že může odpovědět 4 bajty chybového kódu. A to je vše

No, řekl mi -404, tak co?

Tady jsem na něj: „chyťte svou efignu zašifrovanou serverovým klíčem s otiskem prstu takového a takového, chci DH“ a ono hloupě odpoví 404

Co byste si mysleli o takové odpovědi serveru? Co dělat? Není se koho ptát (ale o tom více v druhém díle).

Zde je veškerý zájem o přístaviště dělat

Nic jiného mi nezbývá, jen jsem snil o převodu čísel tam a zpět

Dvě 32bitová čísla. Zabalil jsem je jako ostatní

Ale ne, právě tyto dva potřebujete jako první v řadě jako BE

Vadim Goncharov, [20.06.18/15/49 404:XNUMX] a kvůli tomu XNUMX?

Vasily, [20.06.18/15/49 XNUMX:XNUMX] ANO!

Vadim Goncharov, [20.06.18/15/50 XNUMX:XNUMX], takže nechápu, co může „nenašel“

Vasilij, [20.06.18 15:50] o

Takový rozklad na jednoduché dělitele jsem nenašel%)

Nebylo zvládnuto ani hlášení chyb

Vasily, [20.06.18/20/18 5:XNUMX] Aha, je tam také MDXNUMX. Již tři různé hashe

Klíčový otisk prstu se vypočítá takto:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 a sha2

Tak dáme auth_key Velikost 2048 bitů jsme získali podle Diffie-Hellmana. Co bude dál? Pak zjistíme, že spodních 1024 bitů tohoto klíče není nijak využito ... ale zamysleme se zatím nad tímto. V tomto kroku máme sdílené tajemství se serverem. Byla vytvořena analogie relace TLS, což je velmi nákladný postup. Ale server ještě nic neví o tom, kdo jsme! Vlastně ještě ne oprávnění. Tito. pokud jste mysleli v pojmech „login-password“, jak to bývalo v ICQ, nebo alespoň „login-key“, jako v SSH (například na některých gitlab / github). Dostali jsme anonym. A pokud nám server odpoví "tato telefonní čísla jsou obsluhována jiným DC"? Nebo dokonce „vaše telefonní číslo je zakázáno“? To nejlepší, co můžeme udělat, je uložit klíč v naději, že bude ještě užitečný a do té doby nebude shnilý.

Ten jsme mimochodem "přijali" s rezervou. Například, věříme serveru? je falešný? Potřebujeme kryptografické kontroly:

Vasily, [21.06.18/17/53 2:XNUMX] Nabízejí mobilním klientům kontrolu XNUMXkbitového čísla pro jednoduchost%)

Ale to není vůbec jasné, nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] Dock neříká, co dělat, když se ukázalo, že to není jednoduché

Neřekl. Podívejme se, co v tomto případě dělá oficiální klient pro Android? A to je co (a ano, je tam zajímavý celý soubor) - jak se říká, nechám to tady:

278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
279   if (!strcasecmp(prime, goodPrime)) {

Ne, samozřejmě tam některé existují kontroly na jednoduchost čísla, ale osobně už nemám dostatečné znalosti v matematice.

Dobře, máme hlavní klíč. Pro přihlášení, tzn. odesílat požadavky, je nutné provést další šifrování, již pomocí AES.

Klíč zprávy je definován jako 128 středních bitů SHA256 těla zprávy (včetně relace, ID zprávy atd.), včetně vyplňovacích bajtů, před kterými je 32 bajtů převzatých z autorizačního klíče.

Vasily, [22.06.18 14:08] Průměrné mrchy

Přijato auth_key. Všechno. Dále je... z doků to není jasné. Neváhejte a prostudujte si otevřený zdrojový kód.

Všimněte si, že MTProto 2.0 vyžaduje od 12 do 1024 bajtů výplně, stále pod podmínkou, že výsledná délka zprávy bude dělitelná 16 bajty.

Kolik vycpávek tedy vložit?

A ano, i zde 404 v případě chyby

Pokud si někdo pečlivě prostudoval schéma a text dokumentace, všiml si, že tam žádný MAC není. A ten AES se používá v nějakém IGE režimu, který se jinde nepoužívá. Samozřejmě o tom píší ve svých FAQ... Tady je jako samotný klíč zprávy zároveň SHA hash dešifrovaných dat používaných ke kontrole integrity - a v případě neshody dokumentace pro nějaký důvod doporučuje je mlčky ignorovat (ale co bezpečnost, najednou nás zlomit?).

Nejsem kryptograf, možná v tomto režimu v tomto případě není z teoretického hlediska nic špatného. Rozhodně ale mohu pojmenovat praktický problém na příkladu Telegram Desktop. Šifruje lokální cache (všechny tyto D877F783D5D3EF8C) stejně jako zprávy v MTProto (pouze v tomto případě verze 1.0), tzn. nejprve klíč zprávy, pak samotná data (a někde stranou hlavní velké auth_key 256 bajtů, bez kterých msg_key Zbytečný). Problém se tedy projeví u velkých souborů. Konkrétně je potřeba uchovávat dvě kopie dat – zašifrovanou a dešifrovanou. A jestli tam jsou megabajty, nebo třeba streamované video? .. Klasická schémata s MAC za šifrovým textem vám umožní číst streamování, okamžitě přenášet. A s MTProto musíte nejprve zašifrovat nebo dešifrovat celou zprávu, teprve poté ji přenést do sítě nebo na disk. Proto v nejnovějších verzích Telegram Desktop v mezipaměti v user_data již se používá jiný formát - s AES v režimu CTR.

Vasily, [21.06.18/01/27 20:XNUMX AM] Oh, zjistil jsem, co je IGE: IGE byl první pokus o "ověřovací šifrovací režim", původně pro Kerberos. Byl to neúspěšný pokus (neposkytuje ochranu integrity) a musel být odstraněn. To byl začátek XNUMXletého pátrání po funkčním autentizačním šifrovacím režimu, který nedávno vyvrcholil režimy jako OCB a GCM.

A nyní argumenty ze strany košíku:

Tým za Telegramem, vedený Nikolajem Durovem, se skládá ze šesti šampionů ACM, z nichž polovinu tvoří Ph.D v matematice. Trvalo jim asi dva roky, než zavedli aktuální verzi MTProto.

Co je k smíchu. Dva roky na nižší úroveň

Nebo bychom si mohli vzít TLS

Dobře, řekněme, že jsme provedli šifrování a další nuance. Můžeme konečně posílat požadavky serializované TL a deserializovat odpovědi? Co tedy poslat a jak? Zde je metoda initConnectionmožná je to ono?

Vasily, [25.06.18/18/46 XNUMX:XNUMX] Inicializuje připojení a uloží informace na zařízení a aplikaci uživatele.

Přijímá app_id, device_model, system_version, app_version a lang_code.

A nějaký dotaz

Dokumentace jako vždy. Neváhejte a prostudujte si open source

Pokud bylo s invokeWithLayer vše zhruba jasné, tak co to je? Ukazuje se, že předpokládejme, že máme - klient se již měl serveru na co zeptat - existuje požadavek, který jsme chtěli odeslat:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] Soudě podle kódu je první hovor zabalen do tohoto odpadu a samotný odpad je invoke withlayer

Proč by initConnection nemohlo být samostatné volání, ale musí to být obal? Ano, jak se ukázalo, musí se to dělat pokaždé na začátku každého sezení, a ne jednorázově, jako u hlavního klíče. Ale! Nemůže být volána neoprávněným uživatelem! Zde jsme se dostali do fáze, kdy je použitelná Toto stránka dokumentace - a říká nám, že...

Neoprávněným uživatelům je k dispozici pouze malá část metod API:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importAutorizace
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Úplně první z nich auth.sendCode, a tam je ten cenný první požadavek, ve kterém pošleme api_id a api_hash, a po kterém obdržíme SMS s kódem. A pokud jsme se dostali na špatné DC (telefonní čísla této země obsluhuje například jiná), dostaneme chybu s číslem požadovaného DC. Abychom zjistili, ke které IP adrese se potřebujeme připojit podle čísla DC, pomůže nám help.getConfig. Kdysi bylo jen 5 vstupů, ale po známých událostech roku 2018 se počet výrazně zvýšil.

Nyní si připomeňme, že jsme se v této fázi dostali na anonymní server. Není to příliš drahé získat pouze IP adresu? Proč to a další operace neudělat v nešifrované části MTProto? Slyším námitku: "Jak se můžete ujistit, že to není RKN, kdo bude reagovat s falešnými adresami?". K tomu připomínáme, že ve skutečnosti u oficiálních klientů vestavěné klíče RSA, tj. můžete jen znamení tato informace. Ve skutečnosti se to již dělá pro informace o obcházení zámků, které klienti dostávají přes jiné kanály (je logické, že to nelze provést v samotném MTProto, protože stále musíte vědět, kam se připojit).

OK. V této fázi autorizace klienta ještě nemáme autorizaci a nezaregistrovali jsme naši aplikaci. Chceme zatím jen vidět, jak server reaguje na metody dostupné neoprávněnému uživateli. A tady…

Vasilij, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

Ve schématu přichází první, druhé

Ve schématu tdesktop je třetí hodnota

Ano, od té doby se dokumentace samozřejmě aktualizovala. I když brzy to může být zase irelevantní. A jak by to měl začínající vývojář vědět? Možná, že když zaregistrujete svou žádost, budou vás informovat? Vasily to udělal, ale bohužel, nic mu nebylo zasláno (o tom budeme mluvit ve druhé části).

... Všimli jste si, že už jsme nějak přešli na API, tzn. na další úroveň a vynechali jste něco v tématu MTProto? Nic překvapivého:

Vasily, [28.06.18/02/04 2:XNUMX AM] Mm, prohrabávají se některými algoritmy na eXNUMXe

Mtproto definuje šifrovací algoritmy a klíče pro obě domény, stejně jako trochu struktury obalu

Neustále ale míchají různé úrovně stacků, takže není vždy jasné, kde skončilo mtproto a kde začala další úroveň.

Jak jsou smíšené? No, tady je stejný dočasný klíč například pro PFS (mimochodem, Telegram Desktop neví, jak to udělat). Provádí se žádostí API auth.bindTempAuthKey, tj. z nejvyšší úrovně. Zároveň ale zasahuje do šifrování na nižší úrovni - po něm je třeba to udělat znovu initConnection atd., to není jenom normální požadavek. Samostatně také poskytuje, že můžete mít pouze JEDEN dočasný klíč na DC, i když pole auth_key_id v každé zprávě vám umožňuje změnit klíč alespoň u každé zprávy a že server má právo kdykoli „zapomenout“ dočasný klíč - co dělat v tomto případě, dokumentace neříká ... no, proč nebylo by možné mít několik klíčů, jako u sady budoucích solí, ale?...

V tématu MTProto stojí za zmínku několik dalších věcí.

Zprávy zpráv, msg_id, msg_seqno, potvrzení, ping špatným směrem a další zvláštnosti

Proč o nich potřebujete vědět? Ty totiž „unikají“ o úroveň výš a při práci s API o nich musíte vědět. Předpokládejme, že nás msg_key nezajímá, nižší úroveň za nás vše dešifrovala. Ale uvnitř dešifrovaných dat máme následující pole (také délku dat, abychom věděli, kde je výplň, ale to není důležité):

  • salt-int64
  • session_id - int64
  • message_id - int64
  • seq_no-int32

Připomeňme, že pro celý DC existuje pouze jedna sůl. Proč o tom vědět? Nejen proto, že existuje žádost get_future_salts, který říká, které intervaly budou platné, ale také proto, že pokud je vaše sůl „shnilá“, pak se zpráva (požadavek) jednoduše ztratí. Server samozřejmě ohlásí novou sůl vydáním new_session_created - ale s tím starým budeš muset třeba nějak přeposlat. A tato otázka ovlivňuje architekturu aplikace.

Server může z mnoha důvodů zrušit relace úplně a reagovat tímto způsobem. Co je vlastně relace MTProto ze strany klienta? To jsou dvě čísla session_id и seq_no zprávy v rámci této relace. No a základní připojení TCP, samozřejmě. Řekněme, že náš klient stále neví, jak dělat spoustu věcí, odpojený, znovu připojený. Pokud se to stalo rychle - stará relace pokračovala v novém připojení TCP, zvyšte seq_no dále. Pokud by to trvalo dlouho, mohl by to server smazat, protože na jeho straně je to také fronta, jak jsme zjistili.

Co by mělo být seq_no? Oh, to je záludná otázka. Pokuste se upřímně pochopit, co bylo myšleno:

Zpráva související s obsahem

Zpráva vyžadující výslovné potvrzení. Patří mezi ně všechna uživatelská a mnoho servisních zpráv, prakticky všechny s výjimkou kontejnerů a potvrzení.

Pořadové číslo zprávy (msg_seqno)

32bitové číslo rovnající se dvojnásobku počtu zpráv „souvisejících s obsahem“ (těch, které vyžadují potvrzení, a zejména těch, které nejsou kontejnery), vytvořených odesílatelem před touto zprávou a následně zvýšených o jednu, pokud je aktuální zpráva zprávou zpráva související s obsahem. Kontejner je vždy generován po celém jeho obsahu; proto je jeho pořadové číslo větší nebo rovno pořadovým číslům zpráv v něm obsažených.

Co je to za cirkus s přírůstkem 1 a pak další 2? .. Mám podezření, že původní význam byl „nízký bit pro ACK, zbytek je číslo“, ale výsledek není úplně správný - zejména, ukazuje se, že to lze odeslat někteří potvrzení, která mají totéž seq_no! Jak? No, například nám server něco pošle, odešle a my sami mlčíme, odpovídáme pouze potvrzovacími zprávami služby o přijetí jeho zpráv. V tomto případě budou mít naše odchozí potvrzení stejné odchozí číslo. Pokud jste obeznámeni s TCP a mysleli jste si, že to zní trochu bláznivě, ale zdá se, že to není příliš divoké, protože v TCP seq_no se nezmění a potvrzení přejde na seq_no druhá strana - pak spěchám naštvat. Potvrzení přicházejí na MTProto NOT na seq_no, jako v TCP, ale msg_id !

Co je to? msg_id, nejdůležitější z těchto oborů? Jedinečné ID zprávy, jak název napovídá. Je definováno jako 64bitové číslo, jehož nejméně významné bity mají opět kouzlo server-not-server a zbytek je unixové časové razítko, včetně zlomkové části, posunuté o 32 bitů doleva. Tito. časové razítko jako takové (a zprávy s příliš odlišným časem budou serverem odmítnuty). Z toho vyplývá, že obecně se jedná o identifikátor, který je pro klienta globální. Zatímco - pamatujte session_id - máme zaručeno: Za žádných okolností nemůže být zpráva určená pro jednu relaci odeslána do jiné relace. To znamená, že se ukazuje, že již existuje tři úroveň — relace, číslo relace, id zprávy. Proč taková překomplikace, tato záhada je velmi velká.

To znamená, msg_id potřeba pro…

RPC: požadavky, odpovědi, chyby. Potvrzení.

Jak jste si mohli všimnout, nikde ve schématu není žádný speciální typ nebo funkce „vytvořit požadavek RPC“, i když existují odpovědi. Koneckonců, máme zprávy týkající se obsahu! to znamená, jakýkoli zpráva může být žádost! Nebo nebýt. Po všem, každý je msg_id. A tady jsou odpovědi:

rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

Zde je uvedeno, na kterou zprávu se jedná. Proto si na nejvyšší úrovni API budete muset pamatovat, jaké číslo měl váš požadavek - myslím, že není nutné vysvětlovat, že práce je asynchronní a může existovat několik požadavků současně, jejichž odpovědi lze vrátit v libovolném pořadí? V zásadě lze z tohoto a chybových zpráv jako žádných pracovníků vysledovat architekturu za tím: server, který s vámi udržuje spojení TCP, je front-end balancer, směruje požadavky na backendy a shromažďuje je zpět. message_id. Všechno se zde zdá být jasné, logické a dobré.

Ano?.. A když o tom přemýšlíte? Ostatně samotná RPC odezva má také pole msg_id! Musíme na server křičet „neodpovídáš na moji odpověď!“? A ano, co tam bylo o potvrzení? O stránce zprávy o zprávách nám říká, co je

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

a musí to udělat každá strana. Ale ne vždy! Pokud obdržíte RpcResult, slouží sám o sobě jako potvrzení. To znamená, že server může odpovědět na váš požadavek pomocí MsgsAck - jako: "Dostal jsem to." Může okamžitě odpovědět na RpcResult. Může to být obojí.

A ano, stále musíte odpovědět! Potvrzení. V opačném případě ji server bude považovat za nedoručenou a znovu vám ji vyhodí. I po opětovném připojení. Tady ale samozřejmě vyvstane otázka timeoutů. Pojďme se na ně podívat trochu později.

Mezitím se podívejme na možné chyby při provádění dotazu.

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

Ach, někdo vykřikne, tady je lidštější formát - je tam čára! Nepospíchej. Tady seznam chybale určitě ne kompletní. Z toho se dozvíme, že kód je − něco jako HTTP chyby (no, samozřejmě, sémantika odpovědí není respektována, na některých místech jsou distribuovány kódy náhodně) a řetězec vypadá jako CAPITAL_LETTERS_AND_NUMBERS. Například PHONE_NUMBER_OCCUPIED nebo FILE_PART_X_MISSING. No, to znamená, že stále musíte na tento řádek rozebrat. Například, FLOOD_WAIT_3600 bude znamenat, že budete muset hodinu čekat a PHONE_MIGRATE_5že telefonní číslo s touto předvolbou má být registrováno v 5. DC. Máme typový jazyk, že? Nepotřebujeme argument z řetězce, regulární výrazy postačí, cho.

Opět to není na stránce servisních zpráv, ale jak je již u tohoto projektu zvykem, informace lze najít na jiné stránce dokumentace. Or vzbudit podezření. Nejprve se podívejte, porušení psaní/vrstev - RpcError lze investovat RpcResult. Proč ne venku? Co jsme nevzali v úvahu?... Kde je tedy záruka, že? RpcError nesmí být investováno RpcResult, ale být přímo nebo vnořený do jiného typu? postrádá req_msg_id ? ..

Ale pokračujme o servisních zprávách. Klient se může domnívat, že server dlouho přemýšlí, a vznést takový úžasný požadavek:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Existují tři možné odpovědi na ni, opět se prolínající s mechanismem potvrzení, aby se pokusili pochopit, jaké by měly být (a jaký je seznam typů, které obecně potvrzení nevyžadují), čtenář je ponechán jako domácí úkol (pozn. informace ve zdrojích Telegram Desktop nejsou úplné).

Závislost: Stavy zpráv

Obecně platí, že mnoho míst v TL, MTProto a Telegram obecně zanechává pocit tvrdohlavosti, ale ze zdvořilosti, taktu a dalších jemné dovednosti zdvořile jsme o tom pomlčeli a obscénnosti v dialozích byly cenzurovány. Nicméně toto místoОvětšina stránky o zprávy o zprávách působí šok i pro mě, který se síťovými protokoly dlouhodobě zabývá a viděl kola různého stupně zakřivení.

Začíná to neškodně, potvrzeními. Dále je nám řečeno o

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

No, každý, kdo začne pracovat s MTProto, jim bude muset čelit, v cyklu „opraveno – překompilováno – spuštěno“ je běžné, že se při úpravách objeví chyby v počtech nebo sůl, která se zkazila. Jsou zde však dva body:

  1. Z toho vyplývá, že původní zpráva je ztracena. Musíme ohradit nějaké fronty, zvážíme to později.
  2. Jaká jsou ta podivná čísla chyb? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64… kde jsou ostatní čísla, Tommy?

Dokumentace uvádí:

Záměrem je, aby hodnoty error_code byly seskupeny (error_code >> 4): například kódy 0x40 - 0x4f odpovídají chybám při rozkladu kontejneru.

ale zaprvé posun jiným směrem a zadruhé nezáleží na tom, kde je zbytek kódů? V hlavě autora?.. To jsou však maličkosti.

Závislost začíná ve zprávách o stavu příspěvků a kopiích příspěvků:

  • Žádost o informace o stavu zprávy
    Pokud některá ze stran nějakou dobu neobdrží informace o stavu svých odchozích zpráv, může si je od druhé strany výslovně vyžádat:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informační zpráva týkající se stavu zpráv
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Zde, info je řetězec, který obsahuje přesně jeden bajt stavu zprávy pro každou zprávu ze seznamu příchozích msg_ids:

    • 1 = o zprávě není nic známo (msg_id je příliš nízké, druhá strana ji možná zapomněla)
    • 2 = zpráva nepřijata (msg_id spadá do rozsahu uložených identifikátorů; druhá strana však takovou zprávu určitě neobdržela)
    • 3 = zpráva nebyla přijata (msg_id je příliš vysoké, ale druhá strana ji ještě určitě neobdržela)
    • 4 = zpráva přijata (všimněte si, že tato odpověď je zároveň potvrzením o přijetí)
    • +8 = zpráva již potvrzena
    • +16 = zpráva nevyžadující potvrzení
    • +32 = RPC dotaz obsažený ve zpracovávané zprávě nebo zpracování již dokončeno
    • +64 = odpověď související s obsahem na již vygenerovanou zprávu
    • +128 = druhá strana jistě ví, že zpráva již byla přijata
      Tato odpověď nevyžaduje potvrzení. Je to potvrzení příslušné msgs_state_req, a to samo o sobě.
      Všimněte si, že pokud se náhle ukáže, že druhá strana nemá zprávu, která vypadá, jako by jí byla odeslána, lze zprávu jednoduše odeslat znovu. I když by druhá strana měla obdržet dvě kopie zprávy současně, duplikát bude ignorován. (Pokud uplynulo příliš mnoho času a původní msg_id již není platné, zpráva má být zabalena do msg_copy).
  • Dobrovolné sdělování stavu zpráv
    Kterákoli strana může dobrovolně informovat druhou stranu o stavu zpráv předávaných druhou stranou.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Rozšířené dobrovolné sdělení stavu jedné zprávy
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • Explicitní požadavek na opětovné odeslání zpráv
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Vzdálená strana okamžitě odpoví opětovným odesláním požadovaných zpráv […]
  • Explicitní žádost o opětovné zaslání odpovědí
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Vzdálená strana okamžitě odpoví opětovným odesláním odpovědi na požadované zprávy […]
  • Kopie zpráv
    V některých situacích je třeba znovu odeslat starou zprávu s msg_id, která již není platná. Poté je zabalen do kopírovacího kontejneru:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Jakmile je zpráva přijata, je zpracována tak, jako by tam obal nebyl. Pokud je však jisté, že zpráva orig_message.msg_id byla přijata, pak se nová zpráva nezpracuje (zatímco ve stejnou dobu jsou potvrzena ona a orig_message.msg_id). Hodnota orig_message.msg_id musí být nižší než msg_id kontejneru.

Pomlčme i o tom, že v msgs_state_info opět trčí uši nedokončeného TL (potřebovali jsme vektor bajtů a ve spodních dvou bitech enum a ve starších bitech příznaky). Pointa je v něčem jiném. Chápe někdo, proč je to všechno v praxi u skutečného klienta nutné?... S obtížemi, ale dokážete si představit nějakou výhodu, pokud se člověk zabývá laděním a v interaktivním režimu - zeptejte se serveru, co a jak. Ale požadavky jsou zde popsány okružní výlet.

Z toho vyplývá, že každá strana musí zprávy nejen šifrovat a odesílat, ale také uchovávat data o nich, o odpovědích na ně a po neznámou dobu. Dokumentace nepopisuje načasování ani praktickou použitelnost těchto funkcí. nijak. Nejpřekvapivější je, že se skutečně používají v kodexu oficiálních klientů! Zřejmě jim bylo řečeno něco, co nebylo součástí otevřené dokumentace. Pochopte z kódu proč, již není tak jednoduchý jako v případě TL - nejedná se o (poměrně) logicky izolovanou část, ale o kus vázaný na architekturu aplikace, tzn. bude vyžadovat mnohem více času na pochopení kódu aplikace.

Pingy a časování. Fronty.

Ze všeho, pokud si vzpomenete na dohady o architektuře serveru (distribuce požadavků napříč backendy), vyplývá poněkud otřepaná věc - přes všechny záruky doručení, které v TCP (buď byla data doručena, nebo budete informováni o přestávka, ale data budou doručena až do okamžiku problému), že potvrzení v samotném MTProto - žádné záruky. Server může vaši zprávu snadno ztratit nebo vyhodit a nedá se s tím nic dělat, pouze ohradit berle různých typů.

A za prvé - fronty zpráv. No, za prvé, vše bylo zřejmé od samého začátku – nepotvrzená zpráva musí být uložena a znovu odeslána. A po jaké době? A ten šašek ho zná. Možná ty zprávy odvykací služby nějak řeší tento problém s berličkami, řekněme, v Telegram Desktop jim odpovídají asi 4 fronty (možná více, jak již bylo zmíněno, k tomu se musíte vážněji ponořit do jeho kódu a architektury; zároveň čas, víme, že jej nelze brát jako vzorek, určitý počet typů ze schématu MTProto v něm není použit).

Proč se tohle děje? Programátoři serveru pravděpodobně nedokázali zajistit spolehlivost v rámci clusteru nebo alespoň vyrovnávací paměť na předním balanceru a přesunuli tento problém na klienta. Vasily se ze zoufalství pokusil implementovat alternativní možnost, pouze se dvěma frontami, pomocí algoritmů z TCP - měření RTT na server a úpravy velikosti „okna“ (ve zprávách) v závislosti na počtu nepotvrzených požadavků. Tedy taková hrubá heuristika pro odhad zátěže serveru – kolik našich požadavků dokáže rozžvýkat zároveň a neztratit.

No, to je, rozumíš, ne? Pokud musíte znovu implementovat TCP nad protokol, který funguje přes TCP, znamená to velmi špatně navržený protokol.

Ach ano, proč je potřeba více než jedna fronta a co to obecně znamená pro člověka pracujícího s API na vysoké úrovni? Podívejte, zadáte žádost, serializujete ji, ale často je nemožné ji odeslat okamžitě. Proč? Protože odpověď bude msg_id, která je dočasnáаJsem label, jehož jmenování je lepší odložit co nejpozději – najednou ho server odmítne kvůli časovému nesouladu mezi námi a ním (samozřejmě si můžeme udělat berličku, která posune náš čas od současnosti k času serveru přidáním delty vypočítané z odpovědí serveru - oficiální klienti to dělají, ale tato metoda je hrubá a nepřesná kvůli ukládání do vyrovnávací paměti). Když tedy zadáte požadavek pomocí místního volání funkce z knihovny, zpráva projde následujícími fázemi:

  1. Leží ve stejné frontě a čeká na šifrování.
  2. Jmenován msg_id a zpráva šla do jiné fronty - možné přeposlání; poslat do zásuvky.
  3. a) Server odpověděl MsgsAck - zpráva byla doručena, smažeme ji z "jiné fronty".
    b) Nebo naopak, něco se mu nelíbilo, odpověděl badmsg - znovu posíláme z „jiné fronty“
    c) Nic není známo, je nutné znovu odeslat zprávu z jiné fronty – neví se ale přesně kdy.
  4. Server konečně odpověděl RpcResult - skutečná odpověď (nebo chyba) - nejen doručena, ale také zpracována.

možná, použití kontejnerů by mohlo problém částečně vyřešit. To je, když je spousta zpráv zabalena do jedné a server odpověděl potvrzením na všechny najednou, jednou msg_id. Ale odmítne i tuto smečku, pokud se něco pokazilo, i celou věc.

A v tomto okamžiku přicházejí na řadu netechnické úvahy. Ze zkušenosti jsme viděli mnoho berliček a navíc nyní uvidíme další příklady špatných rad a architektury – má cenu v takových podmínkách věřit a dělat taková rozhodnutí? Otázka je řečnická (samozřejmě že ne).

o čem to mluvíme? Pokud na téma „zprávy závislé na zprávách“ můžete stále spekulovat s námitkami typu „jste hloupý, nepochopil jste náš skvělý nápad!“ (takže nejdřív napiš dokumentaci, jak by měli normální lidé, s odůvodněním a příklady výměny paketů, pak si promluvíme), pak jsou časování / timeouty čistě praktická a specifická záležitost, vše je zde dávno známé. Co nám ale dokumentace říká o vypršení časového limitu?

Server obvykle potvrdí přijetí zprávy od klienta (obvykle dotaz RPC) pomocí odpovědi RPC. Pokud odpověď přichází dlouho, server může nejprve odeslat potvrzení o přijetí a o něco později samotnou odpověď RPC.

Klient obvykle potvrdí přijetí zprávy ze serveru (obvykle odpověď RPC) přidáním potvrzení k dalšímu dotazu RPC, pokud není odeslána příliš pozdě (pokud je vygenerována například 60–120 sekund po přijetí zprávy ze serveru). Pokud však po dlouhou dobu není důvod odesílat zprávy na server nebo pokud je ze serveru velký počet nepotvrzených zpráv (řekněme více než 16), klient odešle samostatné potvrzení.

... překládám: sami nevíme, jak moc a jak je to nutné, no, odhadněme, že nechť je to takhle.

A o pingech:

Ping zprávy (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Odpověď je obvykle vrácena stejnému připojení:

pong#347773c5 msg_id:long ping_id:long = Pong;

Tyto zprávy nevyžadují potvrzení. Pong je vysílán pouze jako odpověď na ping, zatímco ping může být zahájen kteroukoli stranou.

Odložené uzavření připojení + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

Funguje jako ping. Kromě toho po přijetí této zprávy server spustí časovač, který uzavře aktuální připojení connect_delay o několik sekund později, pokud neobdrží novou zprávu stejného typu, která automaticky vynuluje všechny předchozí časovače. Pokud klient odesílá tyto pingy například jednou za 60 sekund, může nastavit odpojení_delay na 75 sekund.

Zbláznil jsi se?! Za 60 sekund vlak vjede do stanice, vysadí a nabere cestující a opět ztratí spojení v tunelu. Za 120 sekund, když budete šťourat, dorazí k dalšímu a spojení se s největší pravděpodobností přeruší. No, je jasné, odkud nohy rostou - „Slyšel jsem zvonění, ale nevím, kde to je“, existuje algoritmus Nagle a možnost TCP_NODELAY, která byla určena pro interaktivní práci. Ale omlouvám se, zpožďuje se výchozí hodnota - 200 Millisekundy. Pokud opravdu chcete něco podobného vykreslit a ušetřit na případném páru paketů - odložte to, alespoň na 5 sekund, nebo jakkoli, jak se nyní rovná timeout zprávy „Uživatel píše ...“. Ale už ne.

A nakonec pingy. Tedy kontrola živosti TCP spojení. Je to legrační, ale asi před 10 lety jsem napsal kritický text o messengeru ubytovny naší fakulty - tam autoři také pingli server od klienta a ne naopak. Ale studenti třetího ročníku jsou jedna věc a mezinárodní kancelář druhá, že? ..

Nejprve malý vzdělávací program. Připojení TCP může při absenci výměny paketů fungovat týdny. To je dobré i špatné, v závislosti na účelu. Pokud jste měli otevřené připojení SSH k serveru, vstali jste z počítače, restartovali napájecí router a vrátili se na své místo - relace přes tento server se nepřerušila (nic nenapsali, nebyly žádné pakety), pohodlné. Je špatné, pokud jsou na serveru tisíce klientů, každý zabírá zdroje (ahoj Postgres!) a klientský hostitel se možná už dávno restartoval – ale my se o tom nedozvíme.

Chat/IM systémy patří do druhého případu z dalšího, dodatečného důvodu – online statusy. Pokud uživatel „spadl“, je nutné o tom informovat své partnery. V opačném případě dojde k chybě, kterou tvůrci Jabberu udělali (a opravovali 20 let) - uživatel se odpojil, ale nadále mu píší zprávy v domnění, že je online (které se v těchto pár minutách předtím také zcela ztratily byl zjištěn zlom). Ne, možnost TCP_KEEPALIVE, která mnohým lidem, kteří nerozumí tomu, jak časovače TCP fungují, se objeví kdekoli (nastavením divokých hodnot, jako jsou desítky sekund), zde nepomůže - musíte se ujistit, že nejen jádro operačního systému stroj uživatele je naživu, ale také normálně funguje, je schopen odpovědět a samotná aplikace (myslíte, že nemůže zamrznout? Telegram Desktop na Ubuntu 18.04 mi opakovaně spadl).

Proto byste měli pingnout serveru klient, a ne naopak - pokud to klient udělá, při přerušení spojení se ping nedoručí, cíle není dosaženo.

A co vidíme v Telegramu? Všechno je přesně naopak! No, tj. formálně si samozřejmě mohou obě strany navzájem pingnout. V praxi klienti používají berličku ping_delay_disconnect, který na serveru spouští časovač. No, pardon, není věcí klienta, aby se rozhodoval, jak dlouho tam chce žít bez pingu. Server na základě své zátěže ví lépe. Ale samozřejmě, pokud vám není líto zdrojů, pak jsou zlý Pinocchio sami a berlička spadne ...

Jak to mělo být navrženo?

Domnívám se, že výše uvedené skutečnosti zcela jasně svědčí o nepříliš vysoké kompetentnosti týmu Telegram / VKontakte v oblasti transportní (a nižší) úrovně počítačových sítí a jejich nízké kvalifikaci v relevantních věcech.

Proč to dopadlo tak komplikovaně a jak se mohou architekti Telegramu pokusit oponovat? Skutečnost, že se pokusili vytvořit relaci, která přežije přerušení TCP spojení, tedy to, co jsme nedodali nyní, dodáme později. Pravděpodobně se také pokusili o přenos UDP, i když se dostali do potíží a opustili to (proto je dokumentace prázdná - nebylo se čím chlubit). Ale kvůli nepochopení toho, jak sítě obecně a TCP konkrétně fungují, kde se na to můžete spolehnout a kde to musíte udělat sami (a jak), a pokusům o kombinaci s kryptografií „jeden výstřel ze dvou ptáci jednou ranou“ - taková mrtvola se ukázala.

Jak to mělo být? Na základě toho, že msg_id je časové razítko, které je kryptograficky nezbytné, aby se zabránilo útokům opakovaného přehrávání, je chybou připojit k němu funkci jedinečného identifikátoru. Proto, aniž bychom drasticky změnili současnou architekturu (když se vytvoří vlákno aktualizací, toto je téma API na vysoké úrovni pro jinou část této série příspěvků), museli bychom:

  1. Server držící TCP spojení s klientem přebírá odpovědnost - pokud odečtete ze soketu, prosím, potvrďte, zpracujte nebo vraťte chybu, žádná ztráta. Pak potvrzení není vektor id, ale prostě "poslední přijaté seq_no" - jen číslo, jako v TCP (dvě čísla - vaše vlastní a potvrzené seq). Jsme pořád na relaci, že?
  2. Časové razítko, které má zabránit útokům opakovaného přehrávání, se stává samostatným polem, a la nonce. Zkontrolováno, ale nic jiného není ovlivněno. Dost a uint32 - pokud se naše sůl mění alespoň každých půl dne, můžeme přidělit 16 bitů nižším bitům celočíselné části aktuálního času, zbytek - zlomkové části sekundy (jako je tomu nyní).
  3. Je odebrán msg_id vůbec - z hlediska rozlišování požadavků na backendech je za prvé id klienta a za druhé id relace a zřetězit je. Proto jako identifikátor požadavku stačí pouze jeden seq_no.

Také to není nejlepší možnost, jako identifikátor by mohl posloužit úplný náhodný – to se mimochodem již dělá v API na vysoké úrovni při odesílání zprávy. Bylo by lepší změnit architekturu z relativní na absolutní úplně, ale to je téma na jinou část, ne na tento příspěvek.

API?

Ta-daam! Když jsme si tedy prošli cestu plnou bolesti a berliček, mohli jsme konečně odesílat jakékoli požadavky na server a dostávat na ně jakékoli odpovědi a také přijímat aktualizace ze serveru (ne jako odpověď na žádost, ale posílá nám to samo, např. PUSH, pokud někdo tak jasněji).

Pozor, nyní bude v článku jediný příklad Perlu! (pro ty, kteří nejsou obeznámeni se syntaxí, je prvním argumentem k požehnání datová struktura objektu, druhým je jeho třída):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

Ano, speciálně ne pod spoilerem - pokud jste to nečetli, jděte a udělejte to!

Oh, počkej~~... jak to vypadá? Něco velmi známého... možná je to datová struktura typického webového rozhraní API v JSON, kromě toho, že k objektům byly připojeny třídy?...

Tak to dopadá... Co je to, soudruzi? .. Tolik námahy - a zastavili jsme se, abychom si odpočinuli tam, kde programátoři webu právě začíná?.. Nebylo by jednodušší jen JSON přes HTTPS?! A co jsme dostali výměnou? Stálo toto úsilí za to?

Pojďme si zhodnotit, co nám TL+MTProto dalo a jaké alternativy jsou možné. No, HTTP požadavek-odpověď se nehodí, ale alespoň něco nad TLS?

kompaktní serializace. Při pohledu na tuto datovou strukturu, podobnou JSON, je třeba si uvědomit, že existují její binární varianty. Označme MsgPack jako nedostatečně rozšiřitelný, ale existuje např. CBOR - mimochodem standard popsaný v RFC 7049. Je pozoruhodný tím, že definuje tagy, jako prodlužovací mechanismus a mezi již standardizované existují:

  • 25 + 256 - nahrazení duplicitních řádků odkazem na číslo řádku, taková levná kompresní metoda
  • 26 - serializovaný objekt Perl s názvem třídy a argumenty konstruktoru
  • 27 - serializovaný objekt nezávislý na jazyce s názvem typu a argumenty konstruktoru

No, zkusil jsem serializovat stejná data v TL a CBOR s povoleným balením řetězců a objektů. Výsledek se začal lišit ve prospěch CBOR někde od megabajtu:

cborlen=1039673 tl_len=1095092

To znamená, výkon: Existují podstatně jednodušší formáty, které nepodléhají selhání synchronizace nebo problému s neznámým identifikátorem, se srovnatelnou účinností.

Rychlé navázání spojení. To znamená nulové RTT po opětovném připojení (když byl klíč již jednou vygenerován) - použitelné od úplně první zprávy MTProto, ale s určitými výhradami - dostali se do stejné soli, relace se neprohnilá atd. Co nám TLS nabízí na oplátku? Související citát:

Při použití PFS v TLS, lístky relace TLS (RFC 5077) pro obnovení šifrované relace bez opětovného vyjednávání klíčů a bez ukládání informací o klíčích na server. Při otevření prvního připojení a generování klíčů server zašifruje stav připojení a odešle jej klientovi (ve formě session ticketu). V souladu s tím, když je spojení obnoveno, klient odešle lístek relace obsahující, mimo jiné, klíč relace zpět na server. Samotný tiket je zašifrován dočasným klíčem (klíč relace), který je uložen na serveru a musí být distribuován na všechny frontend servery, které zpracovávají SSL v clusterových řešeních.[10] Zavedení lístku relace tedy může narušit PFS, pokud dojde ke kompromitaci dočasných klíčů serveru, například když jsou uloženy po dlouhou dobu (OpenSSL, nginx, Apache je ve výchozím nastavení ukládají po celou dobu běhu programu; oblíbené weby používat klíč několik hodin, až dní).

Zde RTT není nula, je potřeba vyměnit alespoň ClientHello a ServerHello, načež spolu s Finished již klient může odesílat data. Zde je však třeba připomenout, že nemáme Web s jeho hromadou nově otevřených spojení, ale messenger, jehož spojením je často jeden a víceméně dlouhodobý, relativně krátký požadavek na webové stránky – vše je multiplexované uvnitř. Tedy celkem přijatelné, pokud jsme nenarazili na velmi špatný úsek metra.

Zapomněli jste na něco jiného? Pište do komentářů.

Pokračování příště!

Ve druhé části této série příspěvků se budeme zabývat spíše organizačními záležitostmi než technickými – přístupy, ideologie, rozhraní, přístup k uživatelům atd. Na základě technických informací, které zde byly uvedeny.

Třetí část bude pokračovat analýzou technické komponenty / vývojových zkušeností. Dozvíte se zejména:

  • pokračování pandemonia s různými typy TL
  • neznámé věci o kanálech a superskupinách
  • než dialogy je horší než seznam
  • o absolutní vs relativní adresování zpráv
  • jaký je rozdíl mezi fotkou a obrázkem
  • jak emotikony zasahují do textu psaného kurzívou

a další berle! Zůstaňte naladěni!

Zdroj: www.habr.com

Přidat komentář