Kritika protokola in organizacijskih pristopov Telegrama. 1. del, tehnični: izkušnje pisanja odjemalca iz nič - TL, MT

V zadnjem času so se na Habréju pogosteje začele pojavljati objave o tem, kako dober je Telegram, kako briljantna in izkušena sta brata Durov pri gradnji omrežnih sistemov itd. Hkrati se je zelo malo ljudi zares poglobilo v tehnično napravo - kvečjemu uporabljajo dokaj preprost (in precej drugačen od MTProto) API za robote, ki temelji na JSON, in običajno samo sprejmejo na veri vse pohvale in piar, ki se vrti okoli messengerja. Pred skoraj letom in pol je moj kolega v nevladni organizaciji Eshelon Vasilij (žal je bil njegov račun na Habréju izbrisan skupaj z osnutkom) začel iz nič v Perlu pisati lastnega odjemalca Telegram, kasneje pa se mu je pridružil še avtor teh vrstic. Zakaj Perl, se bodo nekateri takoj vprašali? Ker takšni projekti že obstajajo v drugih jezikih.Pravzaprav to ni bistvo, lahko obstaja kateri koli drug jezik, kjer ni že pripravljena knjižnica, zato mora avtor iti do konca iz nič. Poleg tega je kriptografija stvar zaupanja, vendar preverite. Pri izdelku, ki je namenjen varnosti, se ne morete preprosto zanesti na že pripravljeno knjižnico proizvajalca in ji slepo zaupati (vendar je to tema za drugi del). Trenutno knjižnica deluje precej dobro na "povprečni" ravni (omogoča vam, da naredite kakršne koli zahteve API).

Vendar pa v tej seriji objav ne bo veliko kriptografije ali matematike. Bo pa veliko drugih tehničnih podrobnosti in arhitekturnih bergel (uporabnih tudi za tiste, ki ne bodo pisali iz nič, ampak bodo knjižnico uporabljali v katerem koli jeziku). Torej, glavni cilj je bil poskusiti izvesti stranko iz nič po uradni dokumentaciji. To pomeni, da je izvorna koda uradnih strank zaprta (spet v drugem delu bomo podrobneje obravnavali temo dejstva, da je to res se zgodi tako), vendar, kot v starih časih, na primer, obstaja standard, kot je RFC - ali je mogoče napisati odjemalca samo po specifikaciji, "ne da bi pogledali" izvorno kodo, naj bo uradna (Telegram Desktop, mobilni), ali neuradni Telethon?

Kazalo:

Dokumentacija ... obstaja, kajne? Ali je res?..

Fragmente zapiskov za ta članek so začeli zbirati lani poleti. Ves ta čas na uradni spletni strani https://core.telegram.org Dokumentacija je bila od plasti 23, tj. obtičala nekje v letu 2014 (se spomnite, takrat sploh še ni bilo kanalov?). Seveda bi nam teoretično to moralo omogočiti implementacijo odjemalca s funkcionalnostjo v tistem času leta 2014. Toda tudi v tem stanju je bila dokumentacija, prvič, nepopolna, in drugič, mestoma je bila v nasprotju s samo sabo. Pred dobrim mesecem, septembra 2019, je bilo slučajno Ugotovljeno je bilo, da je bila na spletnem mestu velika posodobitev dokumentacije za povsem nov sloj 105, z opombo, da je treba zdaj vse znova prebrati. Res je bilo veliko členov popravljenih, a mnogi so ostali nespremenjeni. Zato morate ob branju spodnjih kritik o dokumentaciji imeti v mislih, da nekatere od teh stvari niso več relevantne, nekatere pa so še čisto. Navsezadnje 5 let v sodobnem svetu ni le dolga doba, ampak zelo veliko. Od tistih časov (še posebej, če ne upoštevamo od takrat zavrženih in oživljenih geochat strani) je število metod API v shemi naraslo s sto na več kot dvesto petdeset!

Kje začeti kot mladi avtor?

Ni pomembno, ali pišete iz nič ali uporabljate na primer že pripravljene knjižnice, kot je Telethon za Python ali Madeline za PHP, v vsakem primeru boste najprej potrebovali registrirajte svojo prijavo - pridobi parametre api_id и api_hash (tisti, ki so delali z API-jem VKontakte, takoj razumejo), s katerim bo strežnik identificiral aplikacijo. to moram to storijo iz pravnih razlogov, o tem, zakaj avtorji knjižnic tega ne morejo objaviti, pa bomo več govorili v drugem delu. Morda boste zadovoljni s testnimi vrednostmi, čeprav so zelo omejene - dejstvo je, da se zdaj lahko registrirate samo en aplikacijo, zato ne hitite brezglavo vanjo.

Zdaj bi nas s tehničnega vidika moralo zanimati, da bi morali po registraciji od Telegrama prejemati obvestila o posodobitvah dokumentacije, protokola itd. To pomeni, da bi lahko domnevali, da je bilo spletno mesto z doki preprosto opuščeno in je še naprej sodelovalo posebej s tistimi, ki so začeli ustvarjati stranke, ker je lažje. Ampak ne, nič takega ni bilo opaziti, nobena informacija ni prišla.

In če pišete iz nič, potem je uporaba pridobljenih parametrov pravzaprav še daleč. čeprav https://core.telegram.org/ in govori o njih v Prvi koraki, v bistvu boste najprej morali implementirati protokol MTProto - ampak če bi verjeli postavitev po modelu OSI na koncu strani za splošen opis protokola, potem je popolnoma zaman.

Pravzaprav bo pred in po MTProto na več ravneh hkrati (kot pravijo tuji omrežneži, ki delajo v jedru OS, kršitev slojev) velika, boleča in strašna tema prišla na pot ...

Binarna serializacija: TL (Type Language) in njegova shema, plasti in številne druge strašne besede

Ta tema je pravzaprav ključ do težav Telegrama. In veliko groznih besed bo, če se boste poskušali poglobiti vanj.

Torej, tukaj je diagram. Če vam pride na misel ta beseda, recite, Shema JSON, prav si mislil. Cilj je enak: nek jezik za opis možnega nabora prenesenih podatkov. Tu se podobnosti končajo. Če s strani protokol MTProto, ali iz izvornega drevesa uradnega odjemalca, poskusili bomo odpreti neko shemo, videli bomo nekaj takega:

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;

Človek, ki to vidi prvič, bo intuitivno lahko prepoznal le del zapisanega - no, to so navidezno strukture (čeprav kje je ime, na levi ali na desni?), v njih so polja, po katerem za dvopičjem sledi vrsta... verjetno. Tukaj v oglatih oklepajih so verjetno predloge kot v C++ (pravzaprav, ne čisto). In kaj pomenijo vsi drugi simboli, vprašaji, klicaji, odstotki, zgoščenke (in očitno pomenijo različne stvari na različnih mestih), včasih prisotni in včasih ne, šestnajstiška števila - in kar je najpomembneje, kako priti do tega pravilen (ki ga strežnik ne bo zavrnil) tok bajtov? Prebrati boš moral dokumentacijo (da, v bližini so povezave do sheme v različici JSON - vendar zaradi tega ni nič bolj jasno).

Odprite stran Serializacija binarnih podatkov in se potopite v čarobni svet gob in diskretne matematike, nekaj podobnega kot matan v 4. letniku. Abeceda, tip, vrednost, kombinator, funkcionalni kombinator, normalna oblika, sestavljen tip, polimorfni tip ... in to je vse samo prva stran! Naslednja vas čaka Jezik TL, ki sicer že vsebuje primer trivialne zahteve in odgovora, na bolj tipične primere pa sploh ne odgovori, kar pomeni, da se boste morali prebijati skozi pripovedovanje matematike, prevedeno iz ruščine v angleščino, še na osmih vgrajenih strani!

Bralci, ki poznajo funkcionalne jezike in samodejno sklepanje tipov, bodo seveda opisni jezik v tem jeziku, tudi iz primera, videli kot veliko bolj poznanega in lahko rečejo, da to načeloma pravzaprav ni slabo. Ugovori glede tega so:

  • da namen sliši se dobro, a žal, ona ni dosežen
  • Izobraževanje na ruskih univerzah se razlikuje tudi med posebnostmi IT - vsi niso opravili ustreznega tečaja
  • Končno, kot bomo videli, je v praksi tako To ne zahteva, saj se uporablja le omejena podmnožica celo opisanega TL

Kot rečeno Leon Nerd na kanalu #perl v omrežju FreeNode IRC, ki je poskušal implementirati vrata iz Telegrama v Matrix (prevod citata po spominu ni točen):

Zdi se, kot da bi se nekdo prvič seznanil s teorijo tipov, se navdušil in se začel igrati z njo, pri čemer mu ni bilo mar, ali je potrebna v praksi.

Prepričajte se sami, ali potreba po golih tipih (int, long itd.) kot nečem elementarnem ne sproža vprašanj - navsezadnje jih je treba implementirati ročno - na primer, poskusimo izpeljati iz njih vektor. To je v resnici matrika, če poimenujete nastale stvari s pravim imenom.

Ampak prej

Kratek opis podnabora sintakse TL za tiste, ki ne berejo uradne dokumentacije

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;

Definicija se vedno začne oblikovalec, nato pa po želji (v praksi - vedno) prek simbola # mora biti CRC32 iz normaliziranega opisnega niza te vrste. Sledi opis polj; če obstajajo, je lahko vrsta prazna. Vse se konča z enačajem, imenom tipa, ki mu pripada ta konstruktor – torej pravzaprav podtip. Tip desno od znaka enačaja je polimorfna - to pomeni, da mu lahko ustreza več posebnih vrst.

Če se definicija pojavi za vrstico ---functions---, potem bo sintaksa ostala enaka, vendar bo pomen drugačen: konstruktor bo postal ime funkcije RPC, polja bodo postala parametri (no, to pomeni, da bo ostala popolnoma enaka dana struktura, kot je opisano spodaj , bo to preprosto dodeljeni pomen) in "polimorfni tip" - tip vrnjenega rezultata. Res je, da bo še vedno ostal polimorfen - samo definiran v razdelku ---types---, vendar ta konstruktor "ne bo upoštevan". Preobremenitev vrst klicanih funkcij z njihovimi argumenti, tj. Iz neznanega razloga več funkcij z istim imenom, vendar različnimi podpisi, kot v C++, ni predvidenih v TL.

Zakaj "konstruktor" in "polimorfen", če ni OOP? No, pravzaprav bo nekomu lažje razmišljati o tem v smislu OOP - polimorfni tip kot abstrakten razred, konstruktorji pa so njegovi neposredni potomci in final v terminologiji številnih jezikov. Pravzaprav seveda samo tukaj podobnost z resničnimi preobremenjenimi metodami konstruktorja v OO programskih jezikih. Ker so tukaj samo podatkovne strukture, ni nobenih metod (čeprav lahko nadaljnji opis funkcij in metod povzroči zmedo v glavi, da obstajajo, vendar je to druga stvar) - o konstruktorju lahko razmišljate kot o vrednosti iz ki se gradi tip pri branju toka bajtov.

Kako se to zgodi? Deserializator, ki vedno prebere 4 bajte, vidi vrednost 0xcrc32 - in razume, kaj se bo zgodilo naslednje field1 s tipom int, tj. bere natanko 4 bajte, na tem zgornjem polju z vrsto PolymorType prebrati. vidi 0x2crc32 in razume, da obstajata še dve polji, najprej long, kar pomeni, da preberemo 8 bajtov. In potem spet kompleksen tip, ki je deserializiran na enak način. na primer Type3 bi bilo mogoče prijaviti v vezju takoj, ko sta dva konstruktorja, potem se morata srečati bodisi 0x12abcd34, nato pa morate prebrati še 4 bajte intAli 0x6789cdef, potem pa ne bo nič. Karkoli drugega - vrči morate izjemo. Kakorkoli že, po tem se vrnemo k branju 4 bajtov int marže field_c в constructorTwo in s tem končamo branje našega PolymorType.

Končno, če te ujamejo 0xdeadcrc za constructorThree, potem postane vse bolj zapleteno. Naše prvo polje je bit_flags_of_what_really_present s tipom # - pravzaprav je to le vzdevek za tip nat, kar pomeni "naravno število". To je pravzaprav unsigned int, mimogrede, edini primer, ko se nepredznačena števila pojavljajo v realnih vezjih. Torej, naslednja je konstrukcija z vprašajem, kar pomeni, da bo to polje - prisotno na žici samo, če je ustrezen bit nastavljen v navedenem polju (približno kot ternarni operator). Torej, predpostavimo, da je bil ta bit nastavljen, kar pomeni, da moramo nadalje prebrati polje, kot je Type, ki ima v našem primeru 2 konstruktorja. Ena je prazna (sestoji samo iz identifikatorja), druga ima polje ids s tipom ids:Vector<long>.

Morda mislite, da so predloge in generiki v prednosti ali Javi. Vendar ne. Skoraj. to edini primeru uporabe kotnih oklepajev v realnih vezjih in se uporablja SAMO za Vector. V toku bajtov bodo to 4 bajti CRC32 za sam tip Vector, vedno enaki, nato 4 bajti - število elementov matrike, nato pa ti elementi sami.

K temu dodajte dejstvo, da se serializacija vedno pojavi v besedah ​​s 4 bajti, vsi tipi so večkratniki tega - opisani so tudi vgrajeni tipi bytes и string z ročno serijo dolžine in to poravnavo na 4 - no, zdi se, da se sliši normalno in celo relativno učinkovito? Čeprav se za TL trdi, da je učinkovita binarna serializacija, bo k hudiču z njimi, z razširitvijo skoraj vsega, tudi logičnih vrednosti in nizov z enim znakom na 4 bajte, ali bo JSON še vedno veliko debelejši? Poglejte, tudi nepotrebna polja je mogoče preskočiti z bitnimi zastavicami, vse je precej dobro in celo razširljivo za prihodnost, zakaj torej ne bi konstruktorju kasneje dodali novih neobveznih polj?..

Ampak ne, če ne berete mojega kratkega opisa, ampak celotno dokumentacijo in razmišljate o izvedbi. Prvič, CRC32 konstruktorja se izračuna glede na normalizirano vrstico besedilnega opisa sheme (odstranite odvečne presledke itd.) - torej, če se doda novo polje, se spremeni vrstica opisa tipa in s tem njen CRC32 in , posledično serializacija. In kaj bi storil stari odjemalec, če bi prejel polje z novimi nastavljenimi zastavicami in ne ve, kaj bi z njimi?..

Drugič, spomnimo se CRC32, ki se tukaj uporablja predvsem kot hash funkcije da enolično določite, kateri tip se (de)serializira. Tukaj se soočamo s problemom trkov – in ne, verjetnost ni ena proti 232, ampak veliko večja. Kdo se je spomnil, da je CRC32 namenjen odkrivanju (in odpravljanju) napak v komunikacijskem kanalu in temu primerno izboljša te lastnosti na škodo drugih? Na primer, ne skrbi za preurejanje bajtov: če izračunate CRC32 iz dveh vrstic, v drugi zamenjate prve 4 bajte z naslednjimi 4 bajti - bo enako. Ko so naš vnos besedilni nizi iz latinske abecede (in malo ločil) in ta imena niso posebej naključna, se verjetnost takšne preureditve močno poveča.

Mimogrede, kdo je preveril, kaj je tam? res CRC32? Ena od zgodnjih izvornih kod (še pred Waltmanom) je imela funkcijo zgoščevanja, ki je vsak znak pomnožila s številom 239, ki so ga ti ljudje tako ljubili, ha ha!

Končno smo ugotovili, da so konstruktorji s tipom polja Vector<int> и Vector<PolymorType> bo imel drugačen CRC32. Kaj pa delovanje na spletu? In s teoretičnega vidika, ali to postane del vrste? Recimo, da posredujemo niz deset tisoč števil, dobro z Vector<int> vse je jasno, dolžina in še 40000 bajtov. Kaj pa če to Vector<Type2>, ki je sestavljen iz samo enega polja int in je sam v vrsti - ali moramo 10000xabcdef0 ponoviti 34-krat in nato 4 bajte int, ali pa ga je jezik sposoben NEODVISITI namesto nas od konstruktorja fixedVec in namesto 80000 bajtov ponovno prenesti le 40000?

To sploh ni prazno teoretično vprašanje - predstavljajte si, da prejmete seznam uporabnikov skupine, od katerih ima vsak ID, ime, priimek - razlika v količini prenesenih podatkov po mobilni povezavi je lahko precejšnja. Oglašuje se nam ravno učinkovitost serializacije Telegrama.

Torej ...

Vector, ki ni bil nikoli izdan

Če se poskušate prebiti skozi strani z opisom kombinatorjev in tako naprej, boste videli, da se vektor (in celo matrika) formalno poskuša izpisati skozi tuple več listov. Toda na koncu pozabijo, zadnji korak je preskočen in preprosto podana definicija vektorja, ki še ni vezana na tip. Kaj je narobe? V jezikih programiranje, še posebej funkcionalnih, je precej značilno, da strukturo opišemo rekurzivno - prevajalnik s svojim lenim vrednotenjem bo vse razumel in naredil sam. V jeziku serializacija podatkov potrebna je UČINKOVITOST: dovolj je preprosto opisati Seznam, tj. struktura dveh elementov - prvi je podatkovni element, drugi je ista struktura sama ali prazen prostor za rep (pack (cons) v Lispu). Toda to bo očitno zahtevalo vsakega element porabi dodatne 4 bajte (CRC32 v primeru v TL), da opiše svoj tip. Niz lahko tudi enostavno opišemo fiksna velikost, pri matriki vnaprej neznane dolžine pa prekinemo.

Ker torej TL ne dovoljuje izpisa vektorja, ga je bilo treba dodati ob strani. Konec koncev v dokumentaciji piše:

Serializacija vedno uporablja isti konstruktor »vektor« (const 0x1cb5c415 = crc32(»vector t:Type # [ t ] = Vector t«), ki ni odvisen od specifične vrednosti spremenljivke tipa t.

Vrednost neobveznega parametra t ni vključena v serializacijo, ker je izpeljan iz tipa rezultata (vedno znan pred deserializacijo).

Poglejte si pobližje: vector {t:Type} # [ t ] = Vector t - ampak nikjer Sama definicija ne pravi, da mora biti prvo število enako dolžini vektorja! In ne pride od nikoder. To je danost, ki jo je treba imeti v mislih in izvajati s svojimi rokami. Drugje dokumentacija celo iskreno omenja, da tip ni pravi:

Polimorfni psevdotip vektorja t je »tip«, katerega vrednost je zaporedje vrednosti katerega koli tipa t, bodisi v okvirjih ali golih.

... vendar se ne osredotoča na to. Ko se utrujeni od prebijanja skozi raztezanje matematike (morda celo znane iz univerzitetnega tečaja) odločite obupati in dejansko pogledati, kako z njo delati v praksi, ostane v vaši glavi vtis, da je to resno Matematika v jedru, očitno so jo izumili Cool People (dva matematika - zmagovalca ACM) in ne kdorkoli. Cilj – pokazati se – je bil dosežen.

Mimogrede, o številki. Naj vas spomnimo, da # to je sinonim nat, naravno število:

Obstajajo tipski izrazi (vrsta-izraz) in številski izrazi (nat-izraz). Vendar so opredeljeni na enak način.

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

v slovnici pa so opisani na enak način, tj. To razliko si je treba ponovno zapomniti in ročno uvesti v izvedbo.

No, ja, vrste predlog (vector<int>, vector<User>) imajo skupni identifikator (#1cb5c415), tj. če veste, da je razpis najavljen kot

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

potem ne čakaš več samo na vektor, ampak na vektor uporabnikov. Natančneje, bi počakaj - v resnični kodi bo imel vsak element, če ne bo goli tip, konstruktor, in v dobrem smislu bi ga bilo treba v implementaciji preveriti - vendar smo bili poslani točno v vsakem elementu tega vektorja ta tip? Kaj pa, če bi šlo za nekakšen PHP, v katerem lahko niz vsebuje različne tipe v različnih elementih?

Na tej točki začnete razmišljati - ali je tak TL potreben? Mogoče bi bilo za voziček mogoče uporabiti človeški serializator, isti protobuf, ki je že obstajal takrat? To je bila teorija, poglejmo prakso.

Obstoječe implementacije TL v kodi

TL se je rodil v globinah VKontakte še pred slavnimi dogodki s prodajo Durovovega deleža in (zagotovo), še preden se je začel razvoj Telegrama. In to v odprti kodi izvorna koda prve izvedbe lahko najdete veliko smešnih bergel. In sam jezik je bil tam implementiran bolj v celoti, kot je zdaj v Telegramu. Na primer, zgoščene vrednosti se v shemi sploh ne uporabljajo (kar pomeni vgrajen psevdotip (kot vektor) z deviantnim vedenjem). oz

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

ampak razmislimo, zaradi popolnosti, da tako rekoč sledimo evoluciji Velikana misli.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ali pa ta lepa:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ta fragment govori o predlogah, kot so:

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

To je definicija tipa predloge hashmap kot vektorja parov int – tip. V C++ bi to izgledalo nekako takole:

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

torej, alpha - ključna beseda! Toda samo v C++ lahko napišete T, vendar bi morali napisati alfa, beta ... Ampak ne več kot 8 parametrov, tu se fantazija konča. Zdi se, da so nekoč v Sankt Peterburgu potekali takšni dialogi:

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

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

Vendar je šlo za prvo objavljeno izvedbo TL "na splošno". Preidimo k obravnavi implementacij v samih odjemalcih Telegrama.

Beseda Vasiliju:

Vasilij, [09.10.18 17:07] Predvsem pa je rit vroča, ker so ustvarili kup abstrakcij, nato pa vanje zabili vijak, generator kode pa prekrili z berglami.
Kot rezultat, najprej iz dock pilot.jpg
Nato iz kode dzhekichan.webp

Seveda lahko od ljudi, ki poznajo algoritme in matematiko, pričakujemo, da so prebrali Aho, Ullmanna in poznajo orodja, ki so v desetletjih postala de facto standard v industriji za pisanje njihovih prevajalnikov DSL, kajne?..

Avtor telegram-cli je Vitaly Valtman, kot je razvidno iz pojava formata TLO zunaj njegovih (cli) meja, član ekipe - zdaj je bila dodeljena knjižnica za razčlenjevanje TL ločeno, kakšen je vtis o njej Razčlenjevalnik TL? ..

16.12 04:18 Vasilij: Mislim, da nekdo ni obvladal lex+yacc
16.12 04:18 Vasilij: Drugače si ne znam razložiti
16.12 04:18 Vasilij: no, ali pa so bili plačani za število vrstic v VK
16.12 04:19 Vasilij: 3k+ vrstic itd.<censored> namesto razčlenjevalnika

Mogoče izjema? Poglejmo, kako naredi To je URADNI odjemalec - 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+ vrstic v Pythonu, par regularnih izrazov + posebni primeri, kot je vektor, ki je seveda deklariran v shemi, kot mora biti po sintaksi TL, vendar so se pri razčlenjevanju zanašali na to sintakso... Postavlja se vprašanje, zakaj je bil vse skupaj čudež?иJe bolj slojevit, če ga itak nihče ne bo razčlenil po dokumentaciji?!

Mimogrede ... Se spomnite, da smo govorili o preverjanju CRC32? Torej je v generatorju kode Telegram Desktop seznam izjem za tiste vrste, pri katerih je izračunani CRC32 se ne ujema s tistim, ki je prikazan na diagramu!

Vasily, [18.12/22 49:XNUMX] in tukaj bi razmislil, ali je tak TL potreben
če bi se želel zapletati z alternativnimi implementacijami, bi začel vstavljati prelome vrstic, polovica razčlenjevalnikov se bo zlomila pri večvrstičnih definicijah
tdesktop pa tudi

Zapomnite si točko o enovrstičnici, k njej se bomo vrnili malo kasneje.

V redu, telegram-cli je neuraden, Telegram Desktop je uraden, kaj pa ostali? Kdo ve?.. V kodi odjemalca Android sploh ni bilo razčlenjevalnika sheme (kar sproža vprašanja o odprtokodnosti, vendar je to za drugi del), vendar je bilo več drugih smešnih delov kode, a več o njih v pododdelek spodaj.

Katera druga vprašanja sproža serializacija v praksi? Na primer, naredili so veliko stvari, seveda, z bitnimi polji in pogojnimi polji:

Vasilij: flags.0? true
pomeni, da je polje prisotno in je enako true, če je zastavica nastavljena

Vasilij: flags.1? int
pomeni, da je polje prisotno in ga je treba deserializirati

Vasilij: Rit, ne skrbi, kaj počneš!
Vasily: Nekje v dokumentu je omenjeno, da je true goli tip z ničelno dolžino, vendar je nemogoče kar koli sestaviti iz njihovega dokumenta
Vasily: Tudi v odprtokodnih izvedbah ni tako, vendar obstaja kup bergel in opor

Kaj pa Telethon? Če pogledam naprej na temo MTProto, primer - v dokumentaciji so taki deli, vendar znak % opisano je le kot »ustreza danemu golemu tipu«, tj. v spodnjih primerih je napaka ali nekaj nedokumentiranega:

Vasilij, [22.06.18 18:38] Na enem mestu:

msg_container#73f1f8dc messages:vector message = MessageContainer;

V drugačnem:

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

In to sta dve veliki razliki, v resničnem življenju pride nekakšen goli vektor

Nisem videl gole vektorske definicije in nisem naletel nanjo

Analiza se v teletonu piše ročno

V njegovem diagramu je definicija komentirana msg_container

Spet ostaja vprašanje o %. Ni opisano.

Vadim Goncharov, [22.06.18 19:22] in v tdesktop?

Vasily, [22.06.18 19:23] Toda njihov razčlenjevalnik TL na običajnih motorjih verjetno tudi tega ne bo jedel

// parsed manually

TL je lepa abstrakcija, nihče je ne implementira v celoti

In % ni v njihovi različici sheme

Toda tukaj je dokumentacija v nasprotju sama s seboj, zato idk

Najdeno je bilo v slovnici, lahko so preprosto pozabili opisati semantiko

Videli ste dokument na TL, brez pol litra ne morete ugotoviti

"No, recimo," bo rekel drug bralec, "nekaj kritizirate, zato mi pokažite, kako je treba narediti."

Vasily odgovarja: »Kar zadeva razčlenjevalnik, so mi všeč stvari, kot so

    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 ) ); }
            ;

nekako bolj všeč kot

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;
}

ali

        # 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)

to je CEL lekser:

    ---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];

tiste. bolj preprosto je milo rečeno.”

Na splošno se posledično razčlenjevalnik in generator kode za dejansko uporabljeno podmnožico TL prilega približno 100 vrsticam slovnice in ~300 vrsticam generatorja (vključno z vsemi printgenerirana koda), vključno z informacijami o vrsti za introspekcijo v vsakem razredu. Vsak polimorfni tip se spremeni v prazen abstraktni osnovni razred, konstruktorji pa ga podedujejo in imajo metode za serializacijo in deserializacijo.

Pomanjkanje tipov v tipnem jeziku

Močno tipkanje je dobra stvar, kajne? Ne, to ni holivar (čeprav imam raje dinamične jezike), ampak postulat v okviru TL. Na podlagi tega bi nam moral jezik omogočati vse vrste preverjanj. No, okej, morda ne on sam, ampak izvedba, a naj jih vsaj opiše. In kakšne priložnosti si želimo?

Najprej omejitve. Tukaj vidimo v dokumentaciji za nalaganje datotek:

Binarna vsebina datoteke je nato razdeljena na dele. Vsi deli morajo biti enake velikosti ( part_size ) in morajo biti izpolnjeni naslednji pogoji:

  • part_size % 1024 = 0 (deljivo z 1KB)
  • 524288 % part_size = 0 (512 KB mora biti enakomerno deljivo z part_size)

Ni nujno, da zadnji del izpolnjuje te pogoje, če je njegova velikost manjša od part_size.

Vsak del mora imeti zaporedno številko, file_part, z vrednostjo v razponu od 0 do 2,999.

Ko je datoteka particionirana, morate izbrati način shranjevanja na strežnik. Uporaba upload.saveBigFilePart v primeru, da je polna velikost datoteke večja od 10 MB in upload.saveFilePart za manjše datoteke.
[…] se lahko vrne ena od naslednjih napak pri vnosu podatkov:

  • FILE_PARTS_INVALID — Neveljavno število delov. Vrednost ni med 1..3000

Je kaj od tega na diagramu? Je to nekako mogoče izraziti z uporabo TL? št. Ampak oprostite, celo dedkov Turbo Pascal je lahko opisal navedene vrste razponi. In vedel je še eno stvar, zdaj bolj znano kot enum - tip, sestavljen iz naštevanja določenega (majhnega) števila vrednosti. V jezikih, kot je C - numeric, upoštevajte, da smo doslej govorili samo o vrstah številke. So pa tudi nizi, nizi ... na primer, lepo bi bilo opisati, da lahko ta niz vsebuje samo telefonsko številko, kajne?

Nič od tega ni v TL. Obstaja pa na primer shema JSON. In če bi kdo drug morda trdil o deljivosti 512 KB, da je to še vedno treba preveriti v kodi, potem poskrbite, da odjemalec preprosto ne bi mogel pošljite številko izven dosega 1..3000 (in ustrezna napaka ne bi mogla nastati) bi bilo mogoče, kajne?..

Mimogrede, o napakah in vrnjenih vrednostih. Tudi tisti, ki so delali s TL, zameglijo oči - tega se nam ni takoj posvetilo vsak funkcija v TL lahko dejansko vrne ne le opisani tip vrnitve, ampak tudi napako. Vendar tega ni mogoče na noben način sklepati z uporabo samega TL. Seveda je že jasno in v praksi ni potrebe po ničemer (čeprav je v resnici RPC mogoče narediti na različne načine, k temu se bomo vrnili kasneje) - kaj pa Čistost konceptov matematike abstraktnih tipov iz nebeškega sveta?.. Vlečko sem pobral - tako se poklopi.

In končno, kaj pa berljivost? No, tam, na splošno, bi rad opis imeti to prav v shemi (spet v shemi JSON je), ampak če ste že napeti s tem, kaj pa praktična stran - vsaj trivialno gledanje diffov med posodobitvami? Prepričajte se sami na resnični primeri:

-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;

ali

-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;

Odvisno je od vsakogar, a GitHub na primer noče poudariti sprememb znotraj tako dolgih vrstic. Igra “poišči 10 razlik”, in kar možgani takoj vidijo, da sta začetki in konci v obeh primerih enaki, je treba dolgočasno brati nekje na sredini... Po mojem mnenju to ni samo v teoriji, ampak ampak čisto vizualno umazan in površen.

Mimogrede, o čistosti teorije. Zakaj potrebujemo bitna polja? Ali se ne zdi, da so vonj slabo z vidika teorije tipov? Razlago je mogoče videti v prejšnjih različicah diagrama. Sprva, ja, tako je bilo, za vsak kihanje se je ustvaril nov tip. Ti zametki še vedno obstajajo v tej obliki, na primer:

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;

Zdaj pa si predstavljajte, da če imate v svoji strukturi 5 neobveznih polj, boste potrebovali 32 vrst za vse možne možnosti. Kombinatorna eksplozija. Tako se je kristalna čistost teorije TL znova razbila ob litoželezno rit surove realnosti serializacije.

Poleg tega ponekod ti fantje sami kršijo svojo lastno tipologijo. Na primer, v MTProto (naslednje poglavje) je odgovor mogoče stisniti z Gzip, vse je v redu - le da so plasti in vezje kršeni. Še enkrat, ni bil požet sam RpcResult, ampak njegova vsebina. No, zakaj to?.. Moral sem poseči v berglo, da bi kompresija delovala kjerkoli.

Ali drug primer, enkrat smo odkrili napako - poslana je bila InputPeerUser namesto InputUser. Ali obratno. Ampak je delovalo! To pomeni, da strežniku ni bilo mar za vrsto. Kako je to mogoče? Odgovor nam lahko dajo fragmenti kode iz 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);

Z drugimi besedami, tukaj se izvede serializacija ROČNO, ni ustvarjena koda! Mogoče je strežnik implementiran na podoben način?.. Načeloma bo to delovalo, če bo narejeno enkrat, ampak kako naj bo podprto kasneje med posodobitvami? Ali je bila shema izumljena zato? In tukaj prehajamo na naslednje vprašanje.

Versioning. Plasti

Zakaj se shematske različice imenujejo plasti, je mogoče ugibati le na podlagi zgodovine objavljenih shem. Očitno so avtorji sprva mislili, da je mogoče osnovne stvari narediti z nespremenjeno shemo, in le tam, kjer je potrebno, za posebne zahteve navedejo, da se izvajajo z drugo različico. Načeloma celo dobra ideja - in novo bo tako rekoč "mešano", naloženo na staro. Toda poglejmo, kako je bilo to storjeno. Res je, da si ga nisem mogel ogledati od samega začetka - smešno je, vendar diagram osnovne plasti preprosto ne obstaja. Sloji so se začeli z 2. Dokumentacija nam pove o posebni funkciji TL:

Če odjemalec podpira sloj 2, je treba uporabiti naslednji konstruktor:

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

V praksi to pomeni, da pred vsakim klicem API int z vrednostjo 0x289dd1f6 je treba dodati pred številko metode.

Sliši se normalno. Toda kaj se je zgodilo potem? Potem se je pojavil

invokeWithLayer3#b7475268 query:!X = X;

Kaj je torej naslednje? Kot morda ugibate,

invokeWithLayer4#dea0d430 query:!X = X;

Smešno? Ne, prezgodaj je za smeh, pomislite na to, da vsak zahtevo iz drugega sloja je treba zaviti v tako posebno vrsto - če imate vse različne, kako jih lahko drugače ločite? In dodajanje samo 4 bajtov spredaj je precej učinkovita metoda. Torej,

invokeWithLayer5#417a57ae query:!X = X;

Je pa očitno, da bo čez nekaj časa to postala nekakšna bakanalija. In prišla je rešitev:

Posodobitev: Začenši s slojem 9, pomožne metode invokeWithLayerN se lahko uporablja samo skupaj z initConnection

Hura! Po 9 različicah smo končno prišli do tega, kar se je v internetnih protokolih delalo že v 80-ih letih - enkratni dogovor o različici na začetku povezave!

Kaj je torej naslednje?..

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

Ampak zdaj se še lahko smejiš. Šele po nadaljnjih 9 slojih je bil končno dodan univerzalni konstruktor s številko različice, ki ga je treba klicati samo enkrat na začetku povezave, pomen slojev pa je kot da je izginil, zdaj je samo pogojna različica, npr. povsod drugje. Problem rešen.

Točno?..

Vasily, [16.07.18 14:01] Tudi v petek sem pomislil:
Telestrežnik pošilja dogodke brez zahteve. Zahteve morajo biti zavite v InvokeWithLayer. Strežnik ne zavije posodobitev; ni strukture za zavijanje odgovorov in posodobitev.

Tisti. odjemalec ne more določiti sloja, v katerem želi posodobitve

Vadim Goncharov, [16.07.18 14:02] InvokeWithLayer načeloma ni bergla?

Vasily, [16.07.18 14:02] To je edini način

Vadim Goncharov, [16.07.18 14:02] kar bi v bistvu moralo pomeniti dogovor o plasti na začetku seje

Mimogrede, iz tega sledi, da znižanje odjemalca ni na voljo

Posodobitve, tj. vrsta Updates v shemi je to tisto, kar strežnik pošlje odjemalcu, ne kot odgovor na zahtevo API-ja, ampak neodvisno, ko pride do dogodka. To je zapletena tema, o kateri bomo razpravljali v drugi objavi, vendar je za zdaj pomembno vedeti, da strežnik shranjuje posodobitve, tudi ko je odjemalec brez povezave.

Torej, če zavrnete zavijanje vsakega paket za navedbo njegove različice, to logično vodi do naslednjih možnih težav:

  • strežnik odjemalcu pošlje posodobitve, še preden ta sporoči, katero različico podpira
  • kaj naj storim po nadgradnji odjemalca?
  • ki garancijeda se mnenje strežnika o številki sloja med procesom ne bo spremenilo?

Mislite, da je to čisto teoretično ugibanje in da se v praksi to ne more zgoditi, ker je strežnik pravilno napisan (vsaj dobro testiran)? ha! Ne glede na to, kako je!

Prav na to smo naleteli avgusta. 14. avgusta so bila sporočila, da se na strežnikih Telegrama nekaj posodablja ... in nato v dnevnikih:

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.

in nato več megabajtov sledi skladov (no, istočasno je bilo popravljeno beleženje). Konec koncev, če nekaj ni prepoznano v vašem TL, je to binarno s podpisom, nižje VSE gre, dekodiranje bo postalo nemogoče. Kaj storiti v taki situaciji?

No, prva stvar, ki pride vsakomur na misel, je, da prekine povezavo in poskusi znova. Ni pomagalo. Iščemo v Googlu CRC32 - izkazalo se je, da so to objekti iz sheme 73, čeprav smo delali na 82. Pozorno pogledamo dnevnike - tam so identifikatorji iz dveh različnih shem!

Mogoče je problem čisto v našem neuradnem naročniku? Ne, zaženemo Telegram Desktop 1.2.17 (različica, ki je na voljo v številnih distribucijah Linuxa), piše v dnevnik izjem: MTP Unexpected type id #b5223b0f read in MTPMessageMedia…

Kritika protokola in organizacijskih pristopov Telegrama. 1. del, tehnični: izkušnje pisanja odjemalca iz nič - TL, MT

Google je pokazal, da se je podobna težava že zgodila enemu od neuradnih odjemalcev, a so bile takrat številke različice in temu primerno domneve drugačne ...

Torej, kaj naj storimo? Z Vasilijem sva se razšla: on je poskušal posodobiti vezje na 91, jaz sem se odločil počakati nekaj dni in preizkusiti 73. Obe metodi sta delovali, a ker sta empirični, ni razumevanja, koliko različic navzgor ali navzdol potrebujete skočiti ali kako dolgo morate čakati.

Kasneje sem lahko reproduciral situacijo: zaženemo odjemalca, ga izklopimo, znova prevedemo vezje v drugo plast, znova zaženemo, znova ujamemo težavo, vrnemo se na prejšnjega - ups, brez preklopa vezja in odjemalec se znova zažene za nekaj časa. nekaj minut bo pomagalo. Prejeli boste mešanico podatkovnih struktur iz različnih plasti.

Razlaga? Kot lahko sklepate iz različnih posrednih simptomov, je strežnik sestavljen iz številnih procesov različnih vrst na različnih strojih. Najverjetneje je strežnik, ki je odgovoren za »buffering«, dal v čakalno vrsto tisto, kar so mu dali nadrejeni, ti pa so to dali v shemi, ki je veljala v času generiranja. In dokler ta čakalna vrsta ni "zgnila", ni bilo mogoče storiti ničesar.

Morda ... ampak to je strašna bergla?!.. Ne, preden razmišljamo o norih idejah, poglejmo kodo uradnih strank. V različici za Android ne najdemo nobenega razčlenjevalnika TL, najdemo pa zajetno datoteko (GitHub je noče popraviti) z (de)serializacijo. Tukaj so delčki kode:

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;

ali

    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 ... izgleda divje. Ampak verjetno je to generirana koda, potem v redu?.. Zagotovo pa podpira vse različice! Res je, ni jasno, zakaj je vse pomešano, skrivni klepeti in vse sorte _old7 nekako ne izgledajo kot strojna generacija ... Vendar me je najbolj navdušil

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Fantje, ali se sploh ne morete odločiti, kaj je v eni plasti?! No, v redu, recimo, da sta bila "dva" izpuščena z napako, no, to se zgodi, ampak TRI?.. Takoj spet iste grablje? Kakšna pornografija je to, oprostite?..

Mimogrede, v izvorni kodi Telegram Desktop se zgodi podobna stvar - če je tako, več zaporednih zavez v shemo ne spremeni njene številke plasti, ampak nekaj popravi. V pogojih, ko ni uradnega vira podatkov za shemo, kje jih je mogoče pridobiti, razen izvorne kode uradnega naročnika? In če vzamete od tam, ne morete biti prepričani, da je shema popolnoma pravilna, dokler ne preizkusite vseh metod.

Kako se to sploh da testirati? Upam, da bodo ljubitelji enotnih, funkcionalnih in drugih testov delili v komentarjih.

V redu, poglejmo še en del kode:

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;

Ta komentar »ročno ustvarjen« nakazuje, da je bil samo del te datoteke napisan ročno (si predstavljate celotno vzdrževalno nočno moro?), preostanek pa je bil strojno ustvarjen. Vendar se potem pojavi drugo vprašanje - ali so viri na voljo ne povsem (a la GPL blobs v jedru Linuxa), vendar je to že tema za drugi del.

Ampak dovolj. Preidimo na protokol, na vrhu katerega teče vsa ta serializacija.

MT Proto

Torej, odprimo splošen opis и podroben opis protokola in prva stvar, ob katero se spotaknemo, je terminologija. In z obilico vsega. Na splošno se zdi, da je to lastniška funkcija Telegrama – različno klicanje stvari na različnih mestih ali različne stvari z eno besedo ali obratno (če na primer v API-ju na visoki ravni vidite paket nalepk, to ni kar si mislil).

Na primer, "sporočilo" in "seja" tukaj pomenita nekaj drugega kot v običajnem vmesniku odjemalca Telegram. No, s sporočilom je vse jasno, lahko bi ga razlagali v terminih OOP ali preprosto poimenovali besedo "paket" - to je nizka, transportna raven, ni enakih sporočil kot v vmesniku, veliko je servisnih sporočil . Toda seja ... ampak najprej.

Transportna plast

Prva stvar je transport. Povedali nam bodo približno 5 možnosti:

  • TCP
  • Spletna vtičnica
  • Websocket prek HTTPS
  • HTTP
  • HTTPS

Vasilij, [15.06.18 15:04] Obstaja tudi prevoz UDP, vendar ni dokumentiran

In TCP v treh variantah

Prvi je podoben UDP preko TCP, vsak paket vključuje zaporedno številko in crc
Zakaj je branje dokumentov na vozičku tako boleče?

No, tukaj je zdaj TCP že v 4 variantah:

  • Skrajšano
  • Vmesna
  • Podložen vmesni del
  • Polna

No, ok, podložen vmesni vmesnik za MTProxy, to je bilo kasneje dodano zaradi dobro znanih dogodkov. Toda zakaj še dve različici (skupaj tri), ko bi lahko šli z eno? Vsi štirje se bistveno razlikujejo le v tem, kako nastaviti dolžino in obremenitev glavnega MTProto, o čemer bomo razpravljali še naprej:

  • v skrajšanem je 1 ali 4 bajte, vendar ne 0xef, nato telo
  • v Intermediate je to 4 bajte dolžine in polje, in prvič mora odjemalec poslati 0xeeeeeeee da označite, da je vmesna
  • v celoti najbolj zasvojljiv, z vidika omrežnika: dolžina, zaporedna številka in NE TISTA, ki je predvsem MTProto, telo, CRC32. Da, vse to je na vrhu TCP. Kar nam zagotavlja zanesljiv transport v obliki zaporednega bajtnega toka; zaporedja niso potrebna, še posebej kontrolne vsote. V redu, zdaj mi bo nekdo ugovarjal, da ima TCP 16-bitno kontrolno vsoto, zato pride do poškodb podatkov. Odlično, vendar dejansko imamo kriptografski protokol z zgoščenimi vrednostmi, daljšimi od 16 bajtov, vse te napake - in še več - bo ujelo neujemanje SHA na višji ravni. Poleg tega v CRC32 NI smisla.

Primerjajmo Abridged, v katerem je možen en bajt dolžine, z Intermediate, ki opravičuje "V primeru, da je potrebna 4-bajtna poravnava podatkov," kar je prava neumnost. Kaj, domneva se, da so programerji Telegrama tako nesposobni, da ne morejo brati podatkov iz vtičnice v poravnani medpomnilnik? To moraš še vedno narediti, saj ti branje lahko vrne poljubno število bajtov (pa še proxy strežniki so npr....). Ali po drugi strani, zakaj bi blokirali skrajšano, če bomo še vedno imeli veliko oblazinjenje na vrhu 16 bajtov - prihranite 3 bajte včasih ?

Človek dobi vtis, da Nikolaj Durov zelo rad na novo izumlja kolesa, vključno z omrežnimi protokoli, brez kakršne koli resnične praktične potrebe.

Druge možnosti prevoza, vklj. Web in MTProxy, ne bomo obravnavali zdaj, morda v drugi objavi, če bo zahteva. O tem istem MTProxyju spomnimo zdaj le, da so se ponudniki kmalu po izidu leta 2018 hitro naučili blokirati, namenjeno blokiranje obvodaZ velikost paketa! In tudi dejstvo, da je bil strežnik MTProxy, napisan (spet Waltman) v C, preveč vezan na posebnosti Linuxa, čeprav to sploh ni bilo potrebno (Phil Kulin bo potrdil), in da bi podoben strežnik v Go ali Node.js prilega manj kot sto vrstic.

Toda sklepe o tehnični pismenosti teh ljudi bomo naredili na koncu razdelka, potem ko bomo preučili druga vprašanja. Zaenkrat pa preidimo na OSI sloj 5, session - na katerega so postavili MTProto session.

Ključi, sporočila, seje, Diffie-Hellman

Tja so ga postavili ne povsem pravilno... Seja ni ista seja, ki je vidna v vmesniku pod Aktivne seje. Ampak po vrsti.

Kritika protokola in organizacijskih pristopov Telegrama. 1. del, tehnični: izkušnje pisanja odjemalca iz nič - TL, MT

Tako smo iz transportne plasti prejeli bajtni niz znane dolžine. To je bodisi šifrirano sporočilo ali golo besedilo - če smo še vedno na stopnji dogovora o ključu in to dejansko počnemo. O katerem od množice konceptov, imenovanih "ključ", govorimo? Razjasnimo to vprašanje za samo ekipo Telegrama (opravičujem se, ker sem prevajal lastno dokumentacijo iz angleščine z utrujenimi možgani ob 4. uri zjutraj, lažje je bilo pustiti nekatere fraze, kot so):

Obstajata dve entiteti, imenovani Zasedanje - enega v uporabniškem vmesniku uradnih odjemalcev pod »trenutne seje«, kjer vsaka seja ustreza celotni napravi/OS.
Drugi je Seja MTProto, ki vsebuje zaporedno številko sporočila (v smislu nizke ravni) in ki lahko traja med različnimi povezavami TCP. Hkrati je mogoče namestiti več sej MTProto, na primer za pospešitev prenosa datotek.

Med tema dvema sej obstaja koncept pooblastilo. V degeneriranem primeru lahko rečemo, da seja uporabniškega vmesnika je enako kot pooblastilo, ampak žal, vse je zapleteno. Poglejmo:

  • Uporabnik na novi napravi najprej ustvari auth_key in ga veže na račun, na primer prek SMS-a - zato pooblastilo
  • Zgodilo se je znotraj prvega Seja MTProto, kateri ima session_id znotraj sebe.
  • Na tem koraku kombinacija pooblastilo и session_id bi lahko poklicali primer - ta beseda se pojavlja v dokumentaciji in kodi nekaterih strank
  • Nato lahko stranka odpre nekateri Seje MTProto pod isto auth_key - v isti DC.
  • Nato bo nekega dne morala stranka zahtevati datoteko od še en DC - in za ta DC bo ustvarjen nov auth_key !
  • Obvestiti sistem, da se ne registrira nov uporabnik, ampak isti pooblastilo (seja uporabniškega vmesnika), odjemalec uporablja klice API-ja auth.exportAuthorization v domačem DC auth.importAuthorization v novem DC.
  • Vse je enako, več jih je lahko odprtih Seje MTProto (vsak s svojim session_id) v ta novi DC, pod njegov auth_key.
  • Končno, stranka morda želi Perfect Forward Secrecy. vsak auth_key je bil trajna ključ - na DC - in klient lahko kliče auth.bindTempAuthKey za uporabo začasna auth_key - in spet samo enega temp_auth_key na DC, skupno vsem Seje MTProto v ta DC.

Upoštevajte to sol (in bodoče soli) je prav tako ena na auth_key tiste. razdeljeno med vse Seje MTProto v isti DC.

Kaj pomeni "med različnimi povezavami TCP"? Torej to pomeni nekaj kot avtorizacijski piškotek na spletnem mestu - vztraja (preživi) številne povezave TCP z danim strežnikom, vendar se nekega dne pokvari. Le za razliko od HTTP so v MTProto sporočila znotraj seje zaporedno oštevilčena in potrjena; če so vstopila v tunel, je bila povezava prekinjena – po vzpostavitvi nove povezave bo strežnik v tej seji prijazno poslal vse, česar ni dostavil v prejšnji povezava TCP.

Vendar pa so zgornje informacije povzete po več mesecih preiskave. Ali medtem izvajamo našo stranko iz nič? - vrnimo se na začetek.

Torej ustvarjajmo auth_key o Različice Diffie-Hellman iz Telegrama. Poskusimo razumeti dokumentacijo ...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(podatki) + podatki + (poljubni naključni bajti); tako, da je dolžina enaka 255 bajtom;
šifrirani_podatki := RSA(podatki_z_razpršitvijo, javni_ključ_strežnika); 255-bajtno dolgo število (big endian) se dvigne na zahtevano moč nad zahtevanim modulom, rezultat pa se shrani kot 256-bajtno število.

Imajo nekaj droge DH

Ne izgleda kot DH zdrave osebe
V dx ni dveh javnih ključev

No, na koncu se je to uredilo, a ostanek je ostal - dokazilo o delu opravi naročnik, da je znal faktorizirati številko. Vrsta zaščite pred napadi DoS. In ključ RSA se uporablja samo enkrat v eno smer, v bistvu za šifriranje new_nonce. Toda čeprav bo ta na videz preprosta operacija uspela, s čim se boste morali soočiti?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Nisem še prišel do zahteve appid

To zahtevo sem poslal na DH

In v transportni postaji piše, da se lahko odzove s 4 bajti kode napake. To je vse

No, rekel mi je -404, pa kaj?

Zato sem mu rekel: »Ujemi svoje sranje, šifrirano s strežniškim ključem s takim prstnim odtisom, hočem DH,« in odgovoril je z neumnim 404

Kaj bi si mislili o tem odzivu strežnika? Kaj storiti? Ni koga vprašati (a o tem v drugem delu).

Tu se vse obresti opravijo na zatožni klopi

Nimam kaj drugega početi, samo sanjal sem o pretvarjanju števil naprej in nazaj

Dve 32-bitni številki. Spakiral sem jih kot vse ostale

Ampak ne, to dvoje je treba najprej dodati v vrstico kot BE

Vadim Goncharov, [20.06.18 15:49] in zaradi tega 404?

Vasilij, [20.06.18 15:49] JA!

Vadim Goncharov, [20.06.18 15:50] zato ne razumem, kaj lahko "ni našel"

Vasilij, [20.06.18 15:50] približno

Nisem našel takšne razgradnje na prafaktorje%)

Niti poročanja o napakah nismo upravljali

Vasily, [20.06.18 20:18] Oh, tukaj je tudi MD5. Že trije različni hashi

Prstni odtis ključa se izračuna na naslednji način:

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

SHA1 in sha2

Torej ga postavimo auth_key z uporabo Diffie-Hellmana smo prejeli velikost 2048 bitov. Kaj je naslednje? Nato odkrijemo, da nižjih 1024 bitov tega ključa ni uporabljenih na noben način ... a razmislimo o tem za zdaj. Na tem koraku imamo skupno skrivnost s strežnikom. Vzpostavljen je bil analog seje TLS, kar je zelo drag postopek. Toda strežnik še vedno ne ve ničesar o tem, kdo smo! Pravzaprav še ne. pooblastilo. Tisti. če ste razmišljali v smislu "prijavnega gesla", kot ste nekoč počeli v ICQ, ali vsaj "prijavnega ključa", kot v SSH (na primer na kakšnem gitlabu/githubu). Prejeli smo anonimnega. Kaj pa, če nam strežnik sporoči "te telefonske številke servisira drug DC"? Ali celo "vaša telefonska številka je prepovedana"? Najbolje, kar lahko storimo, je, da obdržimo ključ v upanju, da bo uporaben in da se do takrat ne bo pokvaril.

Mimogrede, "sprejeli" smo ga s pridržkom. Na primer, ali zaupamo strežniku? Kaj če je ponaredek? Potrebni bi bili kriptografski pregledi:

Vasily, [21.06.18 17:53] Mobilnim odjemalcem ponujajo, da preverijo primalnost 2kbit številke%)

Ampak sploh ni jasno, nafeijoa

Vasilij, [21.06.18 18:02] Dokument ne pove, kaj storiti, če se izkaže, da ni preprosto

Ni rečeno. Poglejmo, kaj v tem primeru počne uradni odjemalec Android? A to je tisto (in ja, celotna datoteka je zanimiva) - kot pravijo, bom tole pustil tukaj:

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

Ne, seveda je še vedno tam nekateri Obstajajo testi za primalnost števila, vendar osebno nimam več dovolj znanja iz matematike.

V redu, imamo glavni ključ. Za prijavo, tj. poslati zahteve, morate opraviti nadaljnje šifriranje z uporabo AES.

Ključ sporočila je opredeljen kot 128 srednjih bitov SHA256 telesa sporočila (vključno s sejo, ID-jem sporočila itd.), vključno z polnilnimi bajti, pred katerimi je 32 bajtov, vzetih iz avtorizacijskega ključa.

Vasily, [22.06.18 14:08] Povprečje, prasica, koščki

Polučil auth_key. Vse. Poleg njih ... iz dokumenta ni razvidno. Prosto preučite odprtokodno kodo.

Upoštevajte, da MTProto 2.0 zahteva od 12 do 1024 bajtov polnila, še vedno pod pogojem, da je končna dolžina sporočila deljiva s 16 bajti.

Torej, koliko oblazinjenja morate dodati?

In ja, obstaja tudi 404 v primeru napake

Če je kdo natančno preučil diagram in besedilo dokumentacije, je opazil, da tam ni MAC. In da se AES uporablja v določenem načinu IGE, ki se ne uporablja nikjer drugje. O tem seveda pišejo v svojih pogostih vprašanjih ... Tukaj je kot sam ključ sporočila tudi zgoščena vrednost SHA dešifriranih podatkov, ki se uporablja za preverjanje celovitosti - in v primeru neujemanja dokumentacija iz nekega razloga priporoča jih tiho ignorirajo (kaj pa varnost, kaj če nas zlomijo?).

Nisem kriptograf, mogoče s teoretičnim vidikom v tem primeru ni nič narobe. Lahko pa jasno navedem praktično težavo, pri čemer kot primer uporabim Telegram Desktop. Šifrira lokalni predpomnilnik (vse te D877F783D5D3EF8C) na enak način kot sporočila v MTProto (samo v tem primeru različica 1.0), tj. najprej ključ sporočila, nato sami podatki (in nekje ob strani glavni big auth_key 256 bajtov, brez katerih msg_key neuporaben). Torej postane težava opazna pri velikih datotekah. Hraniti morate namreč dve kopiji podatkov – šifrirano in dešifrirano. In če so na primer megabajti ali pretočni video?.. Klasične sheme z MAC za šifriranim besedilom vam omogočajo, da ga preberete v toku in ga takoj prenesete. Toda z MTProto boste morali na začetku šifrirati ali dešifrirati celotno sporočilo, šele nato ga prenesti v omrežje ali na disk. Zato je v najnovejših različicah Telegram Desktop v predpomnilniku v user_data Uporablja se tudi drug format - z AES v načinu CTR.

Vasily, [21.06.18 01:27] Oh, izvedel sem, kaj je IGE: IGE je bil prvi poskus »avtentifikacijskega načina šifriranja«, prvotno za Kerberos. To je bil neuspešen poskus (ne zagotavlja zaščite integritete) in ga je bilo treba odstraniti. To je bil začetek 20-letnega iskanja načina šifriranja za preverjanje pristnosti, ki deluje, kar je nedavno doseglo vrhunec v načinih, kot sta OCB in GCM.

In zdaj argumenti s strani vozička:

Ekipo, ki stoji za Telegramom, vodi Nikolaj Durov, sestavlja šest prvakov ACM, od katerih jih je polovica doktorjev matematike. Za uvedbo trenutne različice MTProto so potrebovali približno dve leti.

To je smešno. Dve leti na nižji stopnji

Lahko pa samo vzameš tls

V redu, recimo, da smo opravili šifriranje in druge nianse. Ali je končno mogoče poslati zahteve, serializirane v TL, in deserializirati odgovore? Kaj in kako torej poslati? Tukaj, recimo, metoda initConnection, mogoče je to to?

Vasily, [25.06.18 18:46] Inicializira povezavo in shrani podatke v uporabnikovo napravo in aplikacijo.

Sprejema app_id, device_model, system_version, app_version in lang_code.

In nekaj povpraševanja

Dokumentacija kot vedno. Prosto preučite odprto kodo

Če je bilo z invokeWithLayer vse približno jasno, kaj je potem narobe? Izkazalo se je, da imamo - odjemalec je že nekaj vprašal strežnik - obstaja zahteva, ki smo jo želeli poslati:

Vasily, [25.06.18 19:13] Sodeč po kodi je prvi klic zavit v to sranje, samo sranje pa je zavito v invokewithlayer

Zakaj initConnection ne bi mogel biti ločen klic, ampak mora biti ovoj? Da, kot se je izkazalo, je treba to storiti vsakič na začetku vsake seje in ne enkrat, kot pri glavnem ključu. Ampak! Nepooblaščeni uporabnik ga ne more priklicati! Zdaj smo dosegli stopnjo, ko je uporabna ta stran z dokumentacijo - in nam pove, da ...

Samo majhen del metod API je na voljo nepooblaščenim uporabnikom:

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

Prvi med njimi, auth.sendCode, in tu je tista cenjena prva zahteva, v kateri pošljemo api_id in api_hash, po kateri prejmemo SMS s kodo. In če smo v napačnem DC (telefonske številke v tej državi na primer streže druga), potem bomo prejeli napako s številko želenega DC. Pomagajte nam, da ugotovite, na kateri naslov IP glede na številko DC se morate povezati help.getConfig. Nekoč je bilo le 5 vnosov, po znamenitih dogodkih leta 2018 pa se je število močno povečalo.

Zdaj pa se spomnimo, da smo do te stopnje na strežniku prišli anonimno. Ali ni predrago samo pridobiti naslov IP? Zakaj ne bi tega in drugih operacij izvajali v nešifriranem delu MTProto? Slišim ugovor: "kako se lahko prepričamo, da ne bo RKN odgovoril z lažnimi naslovi?" Na to se spomnimo, da so na splošno uradne stranke RSA ključi so vdelani, tj. lahko samo znak te informacije. Pravzaprav se to že izvaja za informacije o obhodu blokiranja, ki jih odjemalci prejmejo prek drugih kanalov (logično, da tega ni mogoče storiti v samem MTProto; morate tudi vedeti, kje se povezati).

V REDU. V tej fazi avtorizacije stranke še nismo avtorizirani in nismo registrirali naše aplikacije. Za zdaj želimo le videti, kaj se strežnik odziva na metode, ki so na voljo nepooblaščenemu uporabniku. In tukaj …

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;

V shemi je prvi na drugem mestu

V shemi tdesktop je tretja vrednost

Da, od takrat je seveda dokumentacija posodobljena. Čeprav lahko kmalu spet postane nepomembno. Kako naj razvijalec začetnik ve? Mogoče, če prijavo prijaviš, te bodo obvestili? Vasilij je to storil, a žal mu niso poslali ničesar (o tem bomo spet govorili v drugem delu).

...Opazili ste, da smo že nekako prešli na API, tj. na naslednjo stopnjo in ste kaj zamudili v temi MTProto? Brez presenečenja:

Vasily, [28.06.18 02:04] Mm, brskajo po nekaterih algoritmih na e2e

Mtproto definira šifrirne algoritme in ključe za obe domeni ter delček ovojne strukture

Vendar nenehno mešajo različne ravni sklada, tako da ni vedno jasno, kje se je mtproto končal in kje se je začela naslednja raven

Kako se mešajo? No, tukaj je na primer isti začasni ključ za PFS (mimogrede, Telegram Desktop tega ne zmore). Izvede se z zahtevo API auth.bindTempAuthKey, tj. z najvišje ravni. Toda hkrati moti šifriranje na nižji ravni - po njem, na primer, morate to storiti znova initConnection itd., to ni samo običajna zahteva. Posebnost je tudi ta, da imate lahko samo EN začasni ključ na DC, čeprav polje auth_key_id v vsakem sporočilu omogoča spremembo ključa vsaj pri vsakem sporočilu in da ima strežnik pravico "pozabiti" začasni ključ kadarkoli - v dokumentaciji ne piše, kaj storiti v tem primeru... no, zakaj bi lahko Ali nimate več ključev, kot pri naboru prihodnjih soli, in?..

O temi MTProto je vredno omeniti še nekaj stvari.

Sporočila, msg_id, msg_seqno, potrditve, pingi v napačno smer in druge posebnosti

Zakaj morate vedeti o njih? Ker »puščajo« na višjo raven in se jih morate zavedati pri delu z API-jem. Predpostavimo, da nas msg_key ne zanima; nižja raven je vse dešifrirala namesto nas. Toda znotraj dešifriranih podatkov imamo naslednja polja (tudi dolžino podatkov, tako da vemo, kje je oblazinjenje, vendar to ni pomembno):

  • sol - int64
  • session_id - int64
  • ID_sporočila — int64
  • seq_no - int32

Naj vas spomnimo, da obstaja samo ena sol za celoten DC. Zakaj bi vedel zanjo? Ne samo zato, ker obstaja zahteva get_future_salts, ki vam pove, kateri intervali bodo veljavni, ampak tudi zato, ker če je vaša sol "gnila", potem se bo sporočilo (zahteva) preprosto izgubilo. Strežnik bo novo sol seveda sporočil z izdajo new_session_created - pri starem pa ga boš moral nekako preposlati npr. In ta težava vpliva na arhitekturo aplikacije.

Strežnik lahko popolnoma opusti seje in se odzove na ta način iz več razlogov. Kaj je pravzaprav seja MTProto s strani odjemalca? To sta dve številki session_id и seq_no sporočil znotraj te seje. No, in osnovna povezava TCP, seveda. Recimo, da naša stranka še vedno ne zna marsičesa narediti, se je odklopila in ponovno povezala. Če se je to zgodilo hitro - stara seja se je nadaljevala v novi povezavi TCP, povečajte seq_no naprej. Če traja dlje časa, bi ga strežnik lahko izbrisal, saj je na njegovi strani tudi čakalna vrsta, kot smo ugotovili.

Kaj naj bi bilo seq_no? Oh, to je kočljivo vprašanje. Poskusite pošteno razumeti, kaj je bilo mišljeno:

Sporočilo v zvezi z vsebino

Sporočilo, ki zahteva izrecno potrditev. Sem spadajo vsa uporabniška in mnoga storitvena sporočila, skoraj vsa z izjemo vsebnikov in potrdil.

Zaporedna številka sporočila (msg_seqno)

32-bitno število, ki je enako dvakratnemu številu sporočil, »povezanih z vsebino« (tistih, ki zahtevajo potrditev, in zlasti tistih, ki niso vsebniki), ki jih je ustvaril pošiljatelj pred tem sporočilom in se nato poveča za ena, če je trenutno sporočilo sporočilo, povezano z vsebino. Vsebnik se vedno ustvari po celotni vsebini; zato je njegova zaporedna številka večja ali enaka zaporednim številkam sporočil, ki jih vsebuje.

Kakšen cirkus je to s prirastkom za 1 in potem še za 2?.. Sumim, da so sprva mislili "najmanj pomemben bit za ACK, ostalo je številka", vendar rezultat ni povsem enak - še posebej, ko pride ven, se lahko pošlje nekateri potrditve, ki imajo enake seq_no! kako No, na primer, strežnik nam nekaj pošilja, pošilja, sami pa molčimo in odgovarjamo samo s servisnimi sporočili, ki potrjujejo prejem njegovih sporočil. V tem primeru bodo naše odhodne potrditve imele isto izhodno številko. Če ste seznanjeni s TCP in mislite, da to zveni nekako divje, vendar se zdi ne zelo divje, ker v TCP seq_no ne spremeni, ampak gre za potrditev seq_no po drugi strani pa te bom pohitel razburiti. Potrdila so na voljo v MTProto NE o seq_no, kot v TCP, vendar z msg_id !

Kaj je to msg_id, najpomembnejše od teh področij? Enolični identifikator sporočila, kot pove že ime. Definirano je kot 64-bitno število, katerega najnižji biti imajo spet magijo »strežnik-ne-strežnik«, ostalo pa je časovni žig Unixa, vključno z delnim delom, pomaknjenim za 32 bitov v levo. Tisti. časovni žig per se (in sporočila s časi, ki se preveč razlikujejo, bo strežnik zavrnil). Iz tega se izkaže, da je to na splošno identifikator, ki je za stranko globalen. Glede na to – spomnimo session_id - zagotovljeno nam je: Pod nobenim pogojem sporočila, namenjenega eni seji, ni mogoče poslati v drugo sejo. Se pravi, izkaže se, da že obstaja 3 raven - seja, številka seje, ID sporočila. Zakaj tako prekomerno kompliciranje, ta skrivnost je zelo velika.

Torej, msg_id potrebno za...

RPC: zahteve, odgovori, napake. Potrditve.

Kot ste morda opazili, nikjer v diagramu ni posebne vrste ali funkcije »izdelaj zahtevo RPC«, čeprav obstajajo odgovori. Navsezadnje imamo sporočila, povezana z vsebino! to je katerikoli sporočilo je lahko prošnja! Ali pa ne biti. Konec koncev, vsakega obstaja msg_id. Vendar obstajajo odgovori:

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

Tukaj je označeno, na katero sporočilo je to odgovor. Zato si boste morali na najvišji ravni API-ja zapomniti, kakšna je bila številka vaše zahteve - mislim, da ni treba pojasnjevati, da je delo asinhrono in da je lahko v teku več zahtev hkrati, katere odgovore je mogoče vrniti v poljubnem vrstnem redu? Načeloma je iz tega in sporočil o napakah, kot da ni delavcev, mogoče izslediti arhitekturo za tem: strežnik, ki vzdržuje povezavo TCP z vami, je izravnalnik sprednjega dela, posreduje zahteve v zaledja in jih zbira nazaj prek message_id. Zdi se, da je tukaj vse jasno, logično in dobro.

Ja?.. In če pomisliš na to? Konec koncev ima tudi odziv RPC sam polje msg_id! Ali moramo strežniku zavpiti »ne odgovarjaš na moj odgovor!«? In ja, kaj je bilo s potrditvami? O strani sporočila o sporočilih nam pove, kaj je

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

in to mora storiti vsaka stran. Ampak ne vedno! Če ste prejeli RpcResult, služi kot potrditev. To pomeni, da lahko strežnik odgovori na vašo zahtevo z MsgsAck - na primer "Prejel sem." RpcResult se lahko takoj odzove. Lahko bi bilo oboje.

In ja, še vedno moraš odgovoriti na odgovor! Potrditev. V nasprotnem primeru ga bo strežnik štel za nedostavljivega in vam ga znova poslal nazaj. Tudi po ponovni povezavi. Tu pa se seveda pojavi vprašanje časovnih omejitev. Poglejmo jih malo kasneje.

Medtem si poglejmo morebitne napake pri izvajanju poizvedbe.

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

Oh, nekdo bo vzkliknil, tukaj je bolj human format - tam je vrstica! Vzemi si čas. Tukaj seznam napak, seveda pa ne v celoti. Iz nje izvemo, da je koda nekaj kot Napake HTTP (no, seveda, semantika odgovorov ni upoštevana, ponekod so naključno razporejeni med kodami), vrstica pa izgleda kot CAPITAL_LETTERS_AND_NUMBERS. Na primer PHONE_NUMBER_OCCUPIED ali FILE_PART_Х_MISSING. No, to pomeni, da boste še vedno potrebovali to vrstico razčleniti. Na primer FLOOD_WAIT_3600 bo pomenilo, da morate počakati eno uro, in PHONE_MIGRATE_5, da mora biti telefonska številka s to predpono registrirana v 5. DC. Imamo tipski jezik, kajne? Ne potrebujemo argumenta iz niza, običajni bodo zadostovali, v redu.

Tega spet ni na strani s servisnimi sporočili, vendar je, kot je že običajno pri tem projektu, informacije mogoče najti na drugi strani dokumentacije. Or vzbujati sum. Najprej poglejte, kršitev tipkanja/sloja - RpcError se lahko vgnezdi RpcResult. Zakaj ne zunaj? Česa nismo upoštevali?.. Kje je torej zagotovilo, da RpcError NI mogoče vdelati v RpcResult, ampak neposredno ali ugnezdeno v drugi vrsti?.. In če ne more, zakaj ni na najvišji ravni, tj. manjka req_msg_id ? ..

A nadaljujmo s servisnimi sporočili. Odjemalec lahko misli, da strežnik dolgo razmišlja, in poda to čudovito zahtevo:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Obstajajo trije možni odgovori na to vprašanje, ki se spet sekajo s potrditvenim mehanizmom; poskušanje razumeti, kakšni bi morali biti (in kakšen je splošni seznam vrst, ki ne zahtevajo potrditve), je prepuščeno bralcu kot domača naloga (opomba: informacije v izvorna koda Telegram Desktop ni popolna).

Zasvojenost z drogami: statusi sporočil

Na splošno številna mesta v TL, MTProto in Telegramu na splošno pustijo občutek trme, vendar zaradi vljudnosti, takta in drugih mehke veščine O tem smo vljudno zamolčali, opolzkosti v dialogih pa cenzurirali. Vendar pa ta krajОvečina strani govori o sporočila o sporočilih Šokantno je celo zame, ki že dolgo delam z omrežnimi protokoli in sem videl kolesa različnih stopenj ukrivljenosti.

Začne se neškodljivo, s potrditvami. Naprej nam povedo 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, vsi, ki začnejo delati z MTProto, se bodo morali soočiti z njimi; v ciklu "popravljeno - ponovno prevedeno - zagnano" je pogosta stvar, da dobite napake v številu ali sol, ki se je pokvarila med urejanjem. Vendar sta tukaj dve točki:

  1. To pomeni, da je izvirno sporočilo izgubljeno. Ustvariti moramo nekaj čakalnih vrst, to bomo pogledali pozneje.
  2. Kakšne so te čudne številke napak? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... kje so ostale številke, Tommy?

V dokumentaciji je navedeno:

Namen je, da so vrednosti error_code združene (error_code >> 4): kode 0x40 — 0x4f na primer ustrezajo napakam v razgradnji vsebnika.

ampak, prvič, premik v drugo smer, in drugič, ni pomembno, kje so druge kode? V avtorjevi glavi?.. Vendar so to malenkosti.

Zasvojenost se začne v sporočilih o statusih sporočil in kopijah sporočil:

  • Zahteva za informacije o statusu sporočila
    Če katera od strank nekaj časa ni prejela informacij o statusu svojih odhodnih sporočil, jih lahko izrecno zahteva od druge stranke:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informativno sporočilo o statusu sporočil
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Tu info je niz, ki vsebuje točno en bajt statusa sporočila za vsako sporočilo s seznama dohodnih msg_ids:

    • 1 = nič ni znanega o sporočilu (msg_id prenizek, druga stran ga je morda pozabila)
    • 2 = sporočilo ni prejeto (msg_id spada v obseg shranjenih identifikatorjev; vendar druga stran zagotovo ni prejela takšnega sporočila)
    • 3 = sporočilo ni prejeto (msg_id je previsok; druga stran pa ga gotovo še ni prejela)
    • 4 = sporočilo prejeto (upoštevajte, da je ta odgovor hkrati tudi potrditev prejema)
    • +8 = sporočilo je že potrjeno
    • +16 = sporočilo, ki ne zahteva potrditve
    • +32 = poizvedba RPC v sporočilu, ki se obdeluje ali je obdelava že končana
    • +64 = vsebinski odgovor na sporočilo je že ustvarjen
    • +128 = druga oseba zagotovo ve, da je sporočilo že prejeto
      Ta odgovor ne zahteva potrditve. To je samo po sebi potrditev ustreznega msgs_state_req.
      Upoštevajte, da če se nenadoma izkaže, da druga stranka nima sporočila, ki bi bilo videti, kot da ji je bilo poslano, lahko sporočilo preprosto ponovno pošljete. Tudi če druga stranka prejme dve kopiji sporočila hkrati, bo dvojnik prezrt. (Če je minilo preveč časa in prvotni msg_id ni več veljaven, je treba sporočilo zaviti v msg_copy).
  • Prostovoljno sporočanje statusa sporočil
    Vsaka stranka lahko prostovoljno obvesti drugo stranko o statusu sporočil, ki jih je poslala druga stranka.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Razširjeno prostovoljno sporočanje statusa enega sporočila
    ...
    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;
  • Eksplicitna zahteva za ponovno pošiljanje sporočil
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Oddaljena stranka se takoj odzove s ponovnim pošiljanjem zahtevanih sporočil […]
  • Eksplicitna zahteva za ponovno pošiljanje odgovorov
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Oddaljena stranka takoj odgovori s ponovnim pošiljanjem odgovori na zahtevana sporočila […]
  • Kopije sporočil
    V nekaterih primerih je treba staro sporočilo z msg_id, ki ni več veljaven, ponovno poslati. Nato se zavije v vsebnik za kopiranje:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Ko je sporočilo prejeto, se obdela, kot da ovoja ne bi bilo. Če pa je zagotovo znano, da je bilo sporočilo orig_message.msg_id prejeto, potem novo sporočilo ni obdelano (hkrati pa se potrdita prejem le-tega in orig_message.msg_id). Vrednost orig_message.msg_id mora biti nižja od vrednosti msg_id vsebnika.

Naj celo zamolčimo, kaj msgs_state_info spet štrlijo ušesa nedokončanega TL (potrebovali smo vektor bajtov in v spodnjih dveh bitih je bil enum, v višjih dveh pa so bile zastavice). Bistvo je drugačno. Je komu jasno zakaj je vse to v praksi? v pravi stranki potrebno?.. S težavo, vendar si lahko predstavljate nekaj koristi, če se oseba ukvarja z odpravljanjem napak in v interaktivnem načinu - vprašajte strežnik, kaj in kako. Toda tukaj so opisane zahteve povratno potovanje.

Iz tega sledi, da mora vsaka stran ne samo šifrirati in pošiljati sporočila, ampak tudi hraniti podatke o sebi, o odzivih nanje, za neznan čas. Dokumentacija ne opisuje niti časov niti praktične uporabnosti teh funkcij. nikakor ne. Najbolj neverjetno je, da se dejansko uporabljajo v kodi uradnih strank! Očitno so jim povedali nekaj, česar v javni dokumentaciji ni. Razumeti iz kode zakaj, ni več tako preprosta kot v primeru TL - ni (relativno) logično izoliran del, ampak kos, vezan na arhitekturo aplikacije, tj. bo potreboval precej več časa za razumevanje aplikacijske kode.

Pingi in časi. Čakalne vrste.

Iz vsega, če se spomnimo ugibanj o arhitekturi strežnika (distribucija zahtevkov po zaledjih), sledi precej žalostna stvar - kljub vsem zagotovilom dostave v TCP (bodisi so podatki dostavljeni, bodisi boste obveščeni o vrzeli, ampak podatki bodo dostavljeni, preden pride do težave), da potrditve v samem MTProto - nobenih garancij. Strežnik lahko zlahka izgubi ali vrže vaše sporočilo in glede tega ni mogoče storiti ničesar, uporabite samo različne vrste bergel.

In najprej - čakalne vrste sporočil. No, z eno stvarjo je bilo vse očitno že od samega začetka - nepotrjeno sporočilo je treba shraniti in ponovno poslati. In po katerem času? In norček ga pozna. Morda tista zasvojena servisna sporočila nekako z berglami rešijo to težavo, recimo v Telegram Desktopu jim ustrezajo približno 4 čakalne vrste (morda več, kot že omenjeno, za to se je treba resneje poglobiti v njegovo kodo in arhitekturo; hkrati Vemo, da ga ne moremo vzeti za vzorec, določeno število tipov iz sheme MTProto se v njem ne uporablja).

Zakaj se to dogaja? Verjetno strežniški programerji niso mogli zagotoviti zanesljivosti znotraj gruče ali celo medpomnilnika na sprednjem balanserju in so to težavo prenesli na odjemalca. Iz obupa je Vasilij poskušal implementirati alternativno možnost, samo z dvema čakalnima vrstama, z uporabo algoritmov iz TCP - merjenje RTT do strežnika in prilagajanje velikosti "okna" (v sporočilih) glede na število nepotrjenih zahtev. To pomeni, da je tako groba hevristika za oceno obremenitve strežnika, koliko naših zahtev lahko prežveči hkrati in ne izgubi.

No, to je, razumete, kajne? Če morate znova implementirati TCP na vrhu protokola, ki teče prek TCP, to pomeni, da je protokol zelo slabo zasnovan.

Oh ja, zakaj potrebujete več kot eno čakalno vrsto in kaj to sploh pomeni za osebo, ki dela z API-jem na visoki ravni? Poglejte, naredite zahtevo, jo serializirate, vendar je pogosto ne morete poslati takoj. Zakaj? Ker bo odgovor msg_id, ki je začasenаSem oznaka, katere dodelitev je najbolje odložiti na čim poznejši čas - v primeru, da jo strežnik zavrne zaradi neusklajenosti časa med nami in njim (seveda lahko naredimo berglo, ki premakne naš čas iz sedanjosti). strežniku z dodajanjem delte, izračunane iz odgovorov strežnika – uradni odjemalci to počnejo, vendar je grobo in netočno zaradi medpomnjenja). Zato, ko naredite zahtevo s klicem lokalne funkcije iz knjižnice, gre sporočilo skozi naslednje stopnje:

  1. Leži v eni čakalni vrsti in čaka na šifriranje.
  2. Imenovan msg_id in sporočilo je šlo v drugo čakalno vrsto - možno posredovanje; pošlji v vtičnico.
  3. a) Strežnik je odgovoril MsgsAck - sporočilo je bilo dostavljeno, izbrišemo ga iz "druge čakalne vrste".
    b) Ali obratno, nekaj mu ni bilo všeč, odgovoril je na badmsg - ponovno pošlji iz "druge čakalne vrste"
    c) Nič ni znano, sporočilo je treba ponovno poslati iz druge čakalne vrste - vendar se ne ve točno kdaj.
  4. Strežnik se je končno odzval RpcResult - dejanski odgovor (ali napaka) - ne samo dostavljen, ampak tudi obdelan.

Mogoče, bi lahko uporaba kontejnerjev delno rešila problem. To je, ko je kup sporočil zapakiranih v eno in strežnik odgovori s potrditvijo vsem naenkrat, v enem msg_id. Toda ta paket bo tudi zavrnil, če bi šlo kaj narobe, v celoti.

In na tej točki pridejo v poštev netehnični premisleki. Iz izkušenj smo videli veliko bergel, poleg tega pa bomo zdaj videli še več primerov slabih nasvetov in arhitekture – ali se v takih razmerah splača zaupati in sprejemati takšne odločitve? Vprašanje je retorično (seveda ne).

O čem govorimo? Če na temo »sporočila o drogah o sporočilih« še lahko špekulirate z ugovori, kot je »neumen si, nisi razumel našega briljantnega načrta!« (tako da najprej napiši dokumentacijo, kot bi morali normalni ljudje, z utemeljitvijo in primeri izmenjave paketov, potem se bomo pogovarjali), potem pa so časi/timeouti čisto praktično in specifično vprašanje, tukaj je vse že dolgo znano. Kaj nam dokumentacija pove o časovnih omejitvah?

Strežnik običajno potrdi prejem sporočila od odjemalca (običajno poizvedba RPC) z odgovorom RPC. Če je odgovor dolgotrajen, lahko strežnik najprej pošlje potrdilo o prejemu, nekoliko kasneje pa še sam odgovor RPC.

Odjemalec običajno potrdi prejem sporočila s strežnika (običajno odgovor RPC) tako, da doda potrditev naslednji poizvedbi RPC, če ni poslana prepozno (če je ustvarjena, recimo, 60-120 sekund po prejemu sporočila s strežnika). Če pa dalj časa ni razloga za pošiljanje sporočil strežniku ali če je s strežnika veliko število neprejetih sporočil (recimo več kot 16), odjemalec pošlje samostojno potrditev.

... Prevajam: sami ne vemo, koliko in kako ga potrebujemo, zato predpostavimo, da naj bo tako.

In še glede pingov:

Ping sporočila (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Odgovor se običajno vrne na isto povezavo:

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

Ta sporočila ne zahtevajo potrditev. Pong se prenese samo kot odgovor na ping, medtem ko lahko ping sproži katera koli stran.

Odloženo zaprtje povezave + PING

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

Deluje kot ping. Poleg tega, potem ko je to prejeto, strežnik zažene časovnik, ki zapre trenutno povezavo disconnect_delay sekund kasneje, razen če prejme novo sporočilo iste vrste, ki samodejno ponastavi vse prejšnje časovnike. Če odjemalec pošlje te pinge enkrat vsakih 60 sekund, lahko na primer nastavi disconnect_delay na 75 sekund.

Si nor?! V 60 sekundah bo vlak vstopil na postajo, izstopil in pobral potnike ter spet izgubil stik v predoru. Čez 120 sekund, ko ga slišite, bo prišel do drugega in povezava se bo najverjetneje prekinila. No, jasno je, od kod prihajajo noge - "Slišal sem zvonjenje, a ne vem, kje je", tu je Naglov algoritem in možnost TCP_NODELAY, namenjena interaktivnemu delu. Ampak, oprostite, ohranite privzeto vrednost - 200 Millisekund Če res želite upodobiti nekaj podobnega in prihraniti na možnih parih paketih, potem odložite za 5 sekund ali karkoli že je časovna omejitev sporočila »Uporabnik tipka ...« zdaj. Ampak nič več.

In končno, pingi. To je preverjanje živahnosti TCP povezave. Smešno, ampak pred približno 10 leti sem napisal kritično besedilo o messengerju študentskega doma naše fakultete - avtorji so tudi tam pingali strežnik od odjemalca in ne obratno. A študenti 3. letnika so eno, mednarodna pisarna pa drugo, kajne?..

Najprej majhen izobraževalni program. Povezava TCP, če ni izmenjave paketov, lahko živi tedne. To je dobro in slabo, odvisno od namena. Dobro je, če ste imeli odprto povezavo SSH s strežnikom, vstali ste iz računalnika, znova zagnali usmerjevalnik, se vrnili na svoje mesto - seja prek tega strežnika ni bila pretrgana (ničesar niste vnesli, ni bilo paketov) , je priročno. Slabo je, če je na strežniku na tisoče odjemalcev, od katerih vsak zaseda vire (pozdravljeni, Postgres!), in odjemalčev gostitelj se je morda že zdavnaj ponovno zagnal - vendar tega ne bomo vedeli.

Sistemi za klepet/neposredovanje sodijo v drugi primer zaradi enega dodatnega razloga – spletnih statusov. Če je uporabnik "padel", morate o tem obvestiti njegove sogovornike. V nasprotnem primeru boste prišli do napake, ki so jo naredili ustvarjalci Jabberja (in jo 20 let popravljali) – uporabnik je prekinil povezavo, vendar mu še naprej pišejo sporočila, saj verjamejo, da je na spletu (ki so se v teh nekaj minut preden je bila odkrita prekinitev povezave). Ne, možnost TCP_KEEPALIVE, ki jo mnogi ljudje, ki ne razumejo, kako delujejo časovniki TCP, vržejo naključno (z nastavitvijo divjih vrednosti, kot so desetine sekund), tukaj ne bo pomagala - zagotoviti morate, da ne samo jedro OS uporabnikovega stroja je živ, vendar tudi deluje normalno, se lahko odzove, in sama aplikacija (mislite, da ne more zamrzniti? Telegram Desktop na Ubuntu 18.04 mi je zamrznil več kot enkrat).

Zato moraš pingati strežnik odjemalec, in ne obratno – če odjemalec to stori, če je povezava prekinjena, ping ne bo dostavljen, cilj ne bo dosežen.

Kaj vidimo na Telegramu? Ravno nasprotno je! No, to je. Formalno seveda obe strani lahko druga drugi pingata. V praksi stranke uporabljajo berglo ping_delay_disconnect, ki nastavi časovnik na strežniku. No, oprostite, ni na stranki, da se odloči, kako dolgo želi živeti tam brez pinga. Strežnik glede na svojo obremenitev ve bolje. Ampak, seveda, če nimate nič proti virom, potem boste sami svoj zlobni Ostržek in bergla bo zadostovala ...

Kako bi moral biti zasnovan?

Menim, da zgornja dejstva jasno kažejo na premalo usposobljenost ekipe Telegram/VKontakte na področju transportne (in nižje) ravni računalniških omrežij in njihovo nizko usposobljenost v zadevnih zadevah.

Zakaj se je izkazalo za tako zapleteno in kako lahko arhitekti Telegrama poskušajo ugovarjati? Dejstvo, da so poskušali narediti sejo, ki preživi prekinitve TCP povezave, torej kar ni bilo dostavljeno zdaj, bomo dostavili kasneje. Verjetno so poskušali narediti tudi UDP transport, pa so naleteli na težave in so ga opustili (zato je dokumentacija prazna - ni se bilo s čim pohvaliti). Toda zaradi pomanjkanja razumevanja, kako delujejo omrežja na splošno in posebej TCP, kje se lahko zanesete nanj in kje morate to storiti sami (in kako), ter poskusa, da bi to združili s kriptografijo, »dve muhi s en kamen«, to je rezultat.

Kako je bilo potrebno? Na podlagi dejstva, da msg_id je časovni žig potreben s kriptografskega vidika za preprečevanje napadov ponovnega predvajanja, je napaka, če mu pripnemo edinstveno identifikacijsko funkcijo. Zato bi bilo treba, ne da bi bistveno spremenili trenutno arhitekturo (ko se ustvari tok posodobitev, je to tema API-ja na visoki ravni za drug del te serije objav):

  1. Strežnik, ki ima povezavo TCP z odjemalcem, prevzame odgovornost - če je prebral iz vtičnice, potrdite, obdelajte ali vrnite napako, brez izgube. Potem potrditev ni vektor ID-jev, ampak preprosto "zadnji prejeti seq_no" - samo številka, kot v TCP (dve številki - vaš seq in potrjena). Vedno smo znotraj seje, kajne?
  2. Časovni žig za preprečevanje napadov s ponovnim predvajanjem postane ločeno polje, a la nonce. Je preverjeno, vendar ne vpliva na nič drugega. Dovolj in uint32 - če se naša sol spremeni vsaj vsake pol dneva, lahko 16 bitov dodelimo nižjim bitom celega dela trenutnega časa, ostalo - delnemu delu sekunde (kot zdaj).
  3. Odstranjeno msg_id sploh - z vidika razlikovanja zahtev na ozadjih obstaja, prvič, id odjemalca in drugič, id seje, združite ju. V skladu s tem za identifikator zahteve zadostuje samo ena stvar seq_no.

Tudi to ni najuspešnejša možnost; popolno naključje bi lahko služilo kot identifikator - to je mimogrede že storjeno v API-ju na visoki ravni pri pošiljanju sporočila. Bolje bi bilo popolnoma predelati arhitekturo iz relativne v absolutno, vendar je to tema za drug del, ne za to objavo.

API?

Ta-dam! Tako smo, ko smo se borili skozi pot, polno bolečin in bergel, končno lahko pošiljali vse zahteve strežniku in prejemali vse odgovore nanje ter prejemali posodobitve s strežnika (ne kot odgovor na zahtevo, ampak njega samega nam pošlje, všečkajte PUSH, če je komu tako bolj jasno).

Pozor, zdaj bo v članku edini primer v Perlu! (za tiste, ki sintakse ne poznate, je prvi argument bless podatkovna struktura objekta, drugi pa njegov razred):

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' )
};

Da, ni namenoma spojler - če tega še niste prebrali, ga preberite!

Oh, čakaj~~... kako to izgleda? Nekaj ​​zelo znanega ... morda je to podatkovna struktura tipičnega spletnega API-ja v JSON, le da so predmetom pripeti tudi razredi?..

Tako se je torej izkazalo ... Za kaj gre, tovariši?.. Toliko truda - in ustavili smo se počivati ​​tam, kjer so spletni programerji šele začenja?..Ali ne bi bil preprostejši samo JSON prek HTTPS?! Kaj smo dobili v zameno? Je bil trud vreden?

Ocenimo, kaj nam je dal TL+MTProto in katere alternative so možne. No, HTTP, ki se osredotoča na model zahteva-odgovor, se slabo prilega, a vsaj nekaj nad TLS?

Kompaktna serializacija. Ko vidim to podatkovno strukturo, podobno JSON, se spomnim, da obstajajo njene binarne različice. Označimo MsgPack kot premalo razširljiv, obstaja pa na primer CBOR - mimogrede, standard, opisan v RFC 7049. Znana je po tem, da opredeljuje oznake, kot razširitveni mehanizem in med že standardizirana na voljo:

  • 25 + 256 - zamenjava ponovljenih vrstic s sklicevanjem na številko vrstice, tako poceni način stiskanja
  • 26 - serializiran objekt Perl z imenom razreda in argumenti konstruktorja
  • 27 - serializiran objekt, neodvisen od jezika, z imenom tipa in argumenti konstruktorja

No, poskušal sem serializirati iste podatke v TL in v CBOR z omogočenim pakiranjem nizov in predmetov. Rezultat se je začel spreminjati v prid CBOR nekje od megabajta:

cborlen=1039673 tl_len=1095092

Torej, zaključek: Obstajajo bistveno enostavnejši formati, ki niso podvrženi problemu napake pri sinhronizaciji ali neznanega identifikatorja, s primerljivo učinkovitostjo.

Hitra vzpostavitev povezave. To pomeni nič RTT po ponovni povezavi (ko je bil ključ enkrat že ustvarjen) - velja od prvega sporočila MTProto, vendar z nekaterimi pridržki - zadeni isto sol, seja ni pokvarjena itd. Kaj nam namesto tega ponuja TLS? Citat na temo:

Ko uporabljate PFS v TLS, vstopnice za sejo TLS (RFC 5077) za nadaljevanje šifrirane seje brez ponovnega pogajanja o ključih in brez shranjevanja informacij o ključu na strežnik. Ob odprtju prve povezave in ustvarjanju ključev strežnik šifrira stanje povezave in ga posreduje odjemalcu (v obliki sejne vstopnice). V skladu s tem, ko se povezava ponovno vzpostavi, odjemalec pošlje vstopnico seje, vključno s ključem seje, nazaj na strežnik. Sama vstopnica je šifrirana z začasnim ključem (session card key), ki je shranjen na strežniku in mora biti razdeljen med vse čelne strežnike, ki obdelujejo SSL v gručnih rešitvah.[10] Tako lahko uvedba vstopnice za sejo krši PFS, če so začasni ključi strežnika ogroženi, na primer, ko so shranjeni dlje časa (OpenSSL, nginx, Apache jih privzeto shranijo za celotno trajanje programa; priljubljena spletna mesta uporabljajo ključ več ur, do dni).

Tukaj RTT ni enak nič, izmenjati morate vsaj ClientHello in ServerHello, po kateri lahko odjemalec pošlje podatke skupaj z Finished. A pri tem se moramo spomniti, da nimamo spleta s kopico na novo odprtih povezav, temveč messenger, katerega povezava je pogosto ena in bolj ali manj dolgotrajne, razmeroma kratke zahteve do spletnih strani – vse je multipleksirano. interno. Se pravi, čisto sprejemljivo, če ne bi naleteli na res slab odsek podzemne železnice.

Ste še kaj pozabili? Zapiši v komentarje.

Se nadaljuje!

V drugem delu te serije objav ne bomo obravnavali tehničnih, ampak organizacijskih vprašanj - pristopov, ideologije, vmesnika, odnosa do uporabnikov itd. Na podlagi tehničnih informacij, ki so bile predstavljene tukaj.

Tretji del bo še naprej analiziral tehnično komponento / razvojne izkušnje. Naučili se boste zlasti:

  • nadaljevanje pandemonija z različnimi tipi TL
  • neznane stvari o kanalih in super skupinah
  • zakaj so dialogi slabši od seznama
  • o absolutnem in relativnem naslavljanju sporočil
  • kakšna je razlika med fotografijo in sliko
  • kako emoji motijo ​​ležeče besedilo

in druge bergle! Ostani na vezi!

Vir: www.habr.com

Dodaj komentar