Kritik am Protokoll und den organisatorischen Ansätzen von Telegram. Teil 1, technisch: Erfahrung beim Schreiben eines Kunden von Grund auf – TL, MT

In letzter Zeit tauchen auf Habré immer häufiger Beiträge darüber auf, wie gut Telegram ist, wie brillant und erfahren die Durov-Brüder beim Aufbau von Netzwerksystemen sind usw. Gleichzeitig haben sich nur sehr wenige Menschen wirklich mit dem technischen Gerät beschäftigt – sie nutzen höchstens eine recht einfache (und ganz anders als MTProto) Bot-API auf JSON-Basis und akzeptieren in der Regel einfach auf den Glauben all die Lobeshymnen und PR, die sich um den Messenger drehen. Vor fast anderthalb Jahren begann mein Kollege bei der Eshelon-NGO Vasily (leider wurde sein Konto bei Habré zusammen mit dem Entwurf gelöscht) damit, seinen eigenen Telegram-Client von Grund auf in Perl zu schreiben, und später schloss sich der Autor dieser Zeilen an. Warum Perl, werden sich manche sofort fragen? Weil solche Projekte bereits in anderen Sprachen existieren. Tatsächlich ist das nicht der Punkt, es könnte jede andere Sprache geben, in der es keine gibt fertige Bibliothek, und dementsprechend muss der Autor den ganzen Weg gehen von Grund auf neu. Darüber hinaus ist Kryptographie eine Vertrauenssache, aber überprüfen Sie sie. Bei einem auf Sicherheit ausgerichteten Produkt kann man sich nicht einfach auf eine fertige Bibliothek des Herstellers verlassen und dieser blind vertrauen (dies ist jedoch ein Thema für den zweiten Teil). Im Moment funktioniert die Bibliothek auf „durchschnittlichem“ Niveau recht gut (ermöglicht das Stellen beliebiger API-Anfragen).

Allerdings wird es in dieser Beitragsreihe nicht viel Kryptographie oder Mathematik geben. Aber es wird noch viele andere technische Details und architektonische Krücken geben (auch nützlich für diejenigen, die nicht von Grund auf schreiben, sondern die Bibliothek in jeder Sprache nutzen möchten). Das Hauptziel bestand also darin, den Client von Grund auf zu implementieren laut offizieller Dokumentation. Nehmen wir also an, dass der Quellcode der offiziellen Clients geschlossen ist (im zweiten Teil werden wir uns noch einmal ausführlicher mit der Tatsache befassen, dass dies wahr ist). es passiert so), aber wie früher gibt es zum Beispiel einen Standard wie RFC – ist es möglich, einen Client allein gemäß der Spezifikation zu schreiben, „ohne einen Blick“ auf den Quellcode zu werfen, sei er offiziell (Telegram Desktop, mobil) oder inoffizielles Telethon?

Table of Contents:

Dokumentation ... es existiert, oder? Ist es wahr?..

Im vergangenen Sommer begann man mit der Sammlung von Notizfragmenten zu diesem Artikel. Die ganze Zeit auf der offiziellen Website https://core.telegram.org Die Dokumentation erfolgte ab Layer 23, d.h. Ich bin irgendwo im Jahr 2014 hängengeblieben (erinnern Sie sich, dass es damals noch nicht einmal Kanäle gab?). Theoretisch hätte uns dies natürlich ermöglichen sollen, einen Client mit der damaligen Funktionalität im Jahr 2014 zu implementieren. Aber auch in diesem Zustand war die Dokumentation erstens unvollständig und zweitens teilweise widersprüchlich. Vor etwas mehr als einem Monat, im September 2019, war es soweit versehentlich Es wurde festgestellt, dass es auf der Website ein großes Update der Dokumentation für den völlig neuen Layer 105 gab, mit dem Hinweis, dass jetzt alles noch einmal gelesen werden muss. Tatsächlich wurden viele Artikel überarbeitet, viele blieben jedoch unverändert. Deshalb sollte man bei der Lektüre der untenstehenden Kritik an der Dokumentation bedenken, dass einige dieser Dinge nicht mehr relevant sind, andere aber durchaus noch. Schließlich sind 5 Jahre in der modernen Welt nicht nur eine lange Zeit, sondern sehr viel. Seitdem ist die Anzahl der API-Methoden im Schema von einhundert auf über zweihundertfünfzig gestiegen (insbesondere wenn man die seitdem verworfenen und wiederbelebten Geochat-Sites nicht berücksichtigt!)

Wo soll man als junger Autor anfangen?

Dabei spielt es keine Rolle, ob Sie von Grund auf schreiben oder beispielsweise vorgefertigte Bibliotheken wie verwenden Telethon für Python oder Madeline für PHP, in jedem Fall benötigen Sie zuerst Registrieren Sie Ihre Bewerbung - Parameter abrufen api_id и api_hash (Wer mit der VKontakte-API gearbeitet hat, versteht sofort), anhand derer der Server die Anwendung identifiziert. Das haben Tun Sie dies aus rechtlichen Gründen, aber wir werden im zweiten Teil mehr darüber sprechen, warum Bibliotheksautoren es nicht veröffentlichen können. Sie können mit den Testwerten zufrieden sein, auch wenn sie sehr begrenzt sind – Fakt ist, dass Sie sich jetzt registrieren können nur einer App, also stürzen Sie sich nicht kopfüber hinein.

Aus technischer Sicht dürfte uns nun die Tatsache interessieren, dass wir nach der Registrierung Benachrichtigungen von Telegram über Aktualisierungen der Dokumentation, des Protokolls usw. erhalten sollen. Das heißt, man könnte davon ausgehen, dass der Standort mit den Docks einfach aufgegeben wurde und gezielt mit denen weitergearbeitet wurde, die anfingen, Kunden zu gewinnen, denn es ist einfacher. Aber nein, es wurde nichts dergleichen beobachtet, es kamen keine Informationen.

Und wenn Sie von Grund auf schreiben, ist die Verwendung der erhaltenen Parameter tatsächlich noch in weiter Ferne. Obwohl https://core.telegram.org/ und spricht im Abschnitt „Erste Schritte“ zunächst davon, tatsächlich müssen Sie sie zunächst umsetzen MTProto-Protokoll - aber wenn du glaubst Layout nach dem OSI-Modell Wenn Sie am Ende der Seite eine allgemeine Beschreibung des Protokolls finden, ist dies völlig vergeblich.

Tatsächlich wird sowohl vor als auch nach MTProto auf mehreren Ebenen gleichzeitig (wie ausländische Netzwerker, die im Betriebssystemkernel arbeiten, Layer-Verletzung sagen) ein großes, schmerzhaftes und schreckliches Thema im Weg stehen ...

Binäre Serialisierung: TL (Type Language) und sein Schema, Ebenen und viele andere gruselige Wörter

Dieses Thema ist tatsächlich der Schlüssel zu den Problemen von Telegram. Und es wird viele schreckliche Worte geben, wenn Sie versuchen, sich damit zu befassen.

Also, hier ist das Diagramm. Wenn Ihnen dieses Wort in den Sinn kommt, sagen Sie: JSON-Schema, Du hast richtig gedacht. Das Ziel ist dasselbe: eine Sprache zur Beschreibung eines möglichen Satzes übertragener Daten. Hier enden die Ähnlichkeiten. Wenn von der Seite MTProto-Protokoll, oder aus dem Quellbaum des offiziellen Clients werden wir versuchen, ein Schema zu öffnen, wir werden so etwas sehen wie:

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;

Eine Person, die dies zum ersten Mal sieht, wird intuitiv nur einen Teil des Geschriebenen erkennen können – nun ja, das sind scheinbar Strukturen (wo ist allerdings der Name, links oder rechts?), es sind Felder darin, Danach folgt ein Typ nach einem Doppelpunkt ... wahrscheinlich. Hier in spitzen Klammern gibt es wahrscheinlich Vorlagen wie in C++ (tatsächlich nicht wirklich). Und was bedeuten all die anderen Symbole, Fragezeichen, Ausrufezeichen, Prozentsätze, Rautezeichen (und natürlich bedeuten sie an verschiedenen Stellen unterschiedliche Dinge), manchmal vorhanden und manchmal nicht, hexadezimale Zahlen – und vor allem, wie kommt man daraus? Recht (was vom Server nicht abgelehnt wird) Bytestream? Sie müssen die Dokumentation lesen (Ja, es gibt Links zum Schema in der JSON-Version in der Nähe – aber das macht es nicht klarer).

Öffnen Sie die Seite Binäre Datenserialisierung und tauchen Sie ein in die magische Welt der Pilze und der diskreten Mathematik, ähnlich wie Matan im 4. Jahr. Alphabet, Typ, Wert, Kombinator, Funktionskombinator, Normalform, zusammengesetzter Typ, polymorpher Typ ... und das ist alles nur die erste Seite! Als nächstes erwartet Sie TL-Sprache, das zwar bereits ein Beispiel für eine triviale Anfrage und Antwort enthält, aber überhaupt keine Antwort auf typischere Fälle liefert, was bedeutet, dass Sie sich durch eine Nacherzählung der aus dem Russischen ins Englischen übersetzten Mathematik auf weiteren acht eingebetteten Seiten kämpfen müssen Seiten!

Leser, die sich mit funktionalen Sprachen und automatischer Typinferenz auskennen, werden die Beschreibungssprache in dieser Sprache natürlich auch aus dem Beispiel als viel vertrauter empfinden und können sagen, dass dies im Prinzip eigentlich nicht schlecht ist. Die Einwände hiergegen sind:

  • ja, Ziel hört sich gut an, aber leider, sie Nicht erreicht
  • Die Ausbildung an russischen Universitäten variiert selbst zwischen den IT-Fachgebieten – nicht jeder hat den entsprechenden Kurs belegt
  • Wie wir sehen werden, ist dies in der Praxis der Fall nicht erforderlich, da nur eine begrenzte Teilmenge der beschriebenen TL verwendet wird

Wie gesagt LeoNerd auf Kanal #perl im FreeNode IRC-Netzwerk, der versucht hat, ein Tor von Telegram zu Matrix zu implementieren (die Übersetzung des Zitats ist aus dem Gedächtnis ungenau):

Es fühlt sich an, als ob jemand zum ersten Mal in die Typentheorie eingeführt wurde, begeistert war und anfing, damit herumzuspielen, ohne sich wirklich darum zu kümmern, ob es in der Praxis benötigt wurde.

Überzeugen Sie sich selbst, ob die Notwendigkeit von Bare-Types (int, long usw.) als etwas Elementares keine Fragen aufwirft – letztendlich müssen sie manuell implementiert werden – versuchen wir beispielsweise, daraus abzuleiten Vektor. Das ist in der Tat Array, wenn man die resultierenden Dinge beim richtigen Namen nennt.

Aber vorher

Eine kurze Beschreibung einer Teilmenge der TL-Syntax für diejenigen, die die offizielle Dokumentation nicht lesen

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;

Die Definition beginnt immer Konstrukteur, danach optional (in der Praxis - immer) durch das Symbol # sollte CRC32 aus der normalisierten Beschreibungszeichenfolge dieses Typs. Als nächstes folgt eine Beschreibung der Felder; falls vorhanden, kann der Typ leer sein. Dies alles endet mit einem Gleichheitszeichen, dem Namen des Typs, zu dem dieser Konstruktor – also eigentlich der Untertyp – gehört. Der Typ rechts vom Gleichheitszeichen ist polymorph - das heißt, ihm können mehrere spezifische Typen entsprechen.

Wenn die Definition nach der Zeile steht ---functions---, dann bleibt die Syntax dieselbe, aber die Bedeutung wird anders sein: Der Konstruktor wird zum Namen der RPC-Funktion, die Felder werden zu Parametern (also, das heißt, es bleibt genau die gleiche gegebene Struktur, wie unten beschrieben , dies ist einfach die zugewiesene Bedeutung) und der „polymorphe Typ“ – der Typ des zurückgegebenen Ergebnisses. Zwar bleibt es weiterhin polymorph – es wurde gerade im Abschnitt definiert ---types---, aber dieser Konstruktor wird „nicht berücksichtigt“. Überladen der Typen aufgerufener Funktionen durch ihre Argumente, d.h. Aus irgendeinem Grund sind mehrere Funktionen mit demselben Namen, aber unterschiedlichen Signaturen, wie in C++, im TL nicht vorgesehen.

Warum „Konstruktor“ und „polymorph“, wenn es nicht OOP ist? Tatsächlich wird es für jemanden einfacher sein, dies in OOP-Begriffen zu betrachten – ein polymorpher Typ als abstrakte Klasse, und Konstruktoren sind seine direkt abgeleiteten Klassen und final in der Terminologie mehrerer Sprachen. Eigentlich natürlich nur hier Ähnlichkeit mit echten überladenen Konstruktormethoden in OO-Programmiersprachen. Da es sich hier nur um Datenstrukturen handelt und es keine Methoden gibt (obwohl die weitere Beschreibung von Funktionen und Methoden durchaus zu Verwirrung im Kopf führen kann, dass sie existieren, aber das ist eine andere Sache) – können Sie sich einen Konstruktor als einen Wert von vorstellen welche wird gebaut Geben Sie beim Lesen eines Byte-Streams ein.

Wie kommt es dazu? Der Deserialisierer, der immer 4 Bytes liest, sieht den Wert 0xcrc32 - und versteht, was als nächstes passieren wird field1 mit Typ int, d.h. liest genau 4 Bytes, darauf das darüber liegende Feld mit dem Typ PolymorType lesen. Sieht 0x2crc32 und versteht, dass es zunächst zwei weitere Felder gibt long, was bedeutet, dass wir 8 Bytes lesen. Und dann wieder ein komplexer Typ, der auf die gleiche Weise deserialisiert wird. Zum Beispiel, Type3 Könnten in der Schaltung jeweils zwei Konstruktoren deklariert werden, dann müssen sie beide erfüllen 0x12abcd34, danach müssen Sie 4 weitere Bytes lesen intOder 0x6789cdef, danach wird es nichts mehr geben. Alles andere – Sie müssen eine Ausnahme auslösen. Wie auch immer, danach lesen wir wieder 4 Bytes int Feld field_c в constructorTwo Und damit beenden wir die Lektüre unseres PolymorType.

Schließlich, wenn Sie erwischt werden 0xdeadcrc für constructorThree, dann wird alles komplizierter. Unser erstes Feld ist bit_flags_of_what_really_present mit Typ # - Tatsächlich ist dies nur ein Alias ​​​​für den Typ nat, was „natürliche Zahl“ bedeutet. Das heißt, unsigned int ist übrigens der einzige Fall, in dem vorzeichenlose Zahlen in realen Schaltkreisen vorkommen. Als nächstes folgt eine Konstruktion mit einem Fragezeichen, was bedeutet, dass dieses Feld nur dann auf der Leitung vorhanden ist, wenn das entsprechende Bit in dem Feld gesetzt ist, auf das Bezug genommen wird (ungefähr wie ein ternärer Operator). Nehmen wir also an, dass dieses Bit gesetzt wurde, was bedeutet, dass wir ein Feld wie „weiter“ lesen müssen Type, die in unserem Beispiel 2 Konstruktoren hat. Eines ist leer (besteht nur aus dem Bezeichner), das andere hat ein Feld ids mit Typ ids:Vector<long>.

Sie könnten denken, dass sowohl Vorlagen als auch Generika in den Profis oder Java enthalten sind. Aber nein. Fast. Das lediglich Dies ist bei der Verwendung von spitzen Klammern in realen Schaltkreisen der Fall und wird NUR für Vektoren verwendet. In einem Byte-Stream sind dies 4 CRC32-Bytes für den Vector-Typ selbst, immer gleich, dann 4 Bytes – die Anzahl der Array-Elemente und dann diese Elemente selbst.

Hinzu kommt, dass die Serialisierung immer in Worten von 4 Byte erfolgt, alle Typen sind Vielfache davon – die integrierten Typen werden ebenfalls beschrieben bytes и string mit manueller Serialisierung der Länge und dieser Ausrichtung um 4 – nun, das klingt normal und sogar relativ effektiv? Obwohl behauptet wird, dass TL eine effektive binäre Serialisierung ist, wird JSON mit der Erweiterung von fast allem, sogar booleschen Werten und Einzelzeichenfolgen auf 4 Bytes, immer noch viel dicker sein? Schauen Sie, selbst unnötige Felder können mit Bit-Flags übersprungen werden, alles ist ganz gut und sogar für die Zukunft erweiterbar, warum also nicht später neue optionale Felder zum Konstruktor hinzufügen?

Aber nein, wenn Sie nicht meine kurze Beschreibung, sondern die vollständige Dokumentation lesen und über die Implementierung nachdenken. Erstens wird der CRC32 des Konstruktors anhand der normalisierten Zeile der Textbeschreibung des Schemas berechnet (zusätzliche Leerzeichen entfernen usw.). Wenn also ein neues Feld hinzugefügt wird, ändert sich die Typbeschreibungszeile und damit auch ihr CRC32 und , folglich Serialisierung. Und was würde der alte Kunde tun, wenn er ein Feld mit neuen Flags erhält und nicht weiß, was er als nächstes damit machen soll?

Zweitens: Erinnern wir uns CRC32, was hier im Wesentlichen als verwendet wird Hash-Funktionen um eindeutig zu bestimmen, welcher Typ (de)serialisiert wird. Hier stehen wir vor dem Problem der Kollisionen – und nein, die Wahrscheinlichkeit ist nicht eins zu 232, sondern viel größer. Wer hat sich daran erinnert, dass CRC32 darauf ausgelegt ist, Fehler im Kommunikationskanal zu erkennen (und zu korrigieren) und diese Eigenschaften dementsprechend zum Nachteil anderer verbessert? Beispielsweise ist die Neuanordnung der Bytes egal: Wenn Sie CRC32 aus zwei Zeilen berechnen, tauschen Sie in der zweiten Zeile die ersten 4 Bytes mit den nächsten 4 Bytes aus – es bleibt dasselbe. Wenn es sich bei unserer Eingabe um Textzeichenfolgen aus dem lateinischen Alphabet (und ein wenig Satzzeichen) handelt und diese Namen nicht besonders zufällig sind, erhöht sich die Wahrscheinlichkeit einer solchen Neuanordnung erheblich.

Übrigens, wer hat überprüft, was da war? wirklich CRC32? Einer der frühen Quellcodes (noch vor Waltman) hatte eine Hash-Funktion, die jedes Zeichen mit der Zahl 239 multiplizierte, was bei diesen Leuten so beliebt ist, ha ha!

Endlich, okay, wir haben erkannt, dass Konstruktoren mit einem Feldtyp Vector<int> и Vector<PolymorType> wird einen anderen CRC32 haben. Wie sieht es mit der Online-Leistung aus? Und aus theoretischer Sicht, Wird dies Teil des Typs?? Nehmen wir an, wir übergeben ein Array mit zehntausend Zahlen, also mit Vector<int> Alles ist klar, die Länge und weitere 40000 Bytes. Was wäre, wenn das? Vector<Type2>, das nur aus einem Feld besteht int und es ist allein in der Art – müssen wir 10000xabcdef0 34 Mal wiederholen und dann 4 Bytes int, oder die Sprache ist in der Lage, es für uns UNABHÄNGIG vom Konstruktor zu machen fixedVec und statt 80000 Bytes wieder nur 40000 übertragen?

Dies ist überhaupt keine leere theoretische Frage – stellen Sie sich vor, Sie erhalten eine Liste von Gruppenbenutzern, von denen jeder eine ID, einen Vornamen und einen Nachnamen hat – der Unterschied in der über eine mobile Verbindung übertragenen Datenmenge kann erheblich sein. Es ist genau die Wirksamkeit der Telegram-Serialisierung, die uns angepriesen wird.

So ...

Vector, das nie veröffentlicht wurde

Wenn Sie versuchen, die Seiten mit der Beschreibung von Kombinatoren usw. durchzublättern, werden Sie feststellen, dass ein Vektor (und sogar eine Matrix) formal versucht, durch Tupel mehrerer Blätter ausgegeben zu werden. Aber am Ende vergessen sie es, der letzte Schritt wird übersprungen und es wird einfach eine Definition eines Vektors angegeben, der noch nicht an einen Typ gebunden ist. Was ist los? In Sprachen Programmierung, insbesondere bei funktionalen, ist es durchaus typisch, die Struktur rekursiv zu beschreiben – der Compiler mit seiner verzögerten Auswertung wird alles selbst verstehen und erledigen. In der Sprache Datenserialisierung Was benötigt wird, ist EFFIZIENZ: Es reicht aus, es einfach zu beschreiben Liste, d.h. Struktur aus zwei Elementen – das erste ist ein Datenelement, das zweite ist dieselbe Struktur selbst oder ein leerer Raum für das Ende (Pack (cons) in Lisp). Dies wird aber offensichtlich erforderlich sein jeder Das Element benötigt zusätzlich 4 Bytes (CRC32 im Fall von TL), um seinen Typ zu beschreiben. Auch ein Array lässt sich leicht beschreiben feste Größe, aber im Falle eines Arrays mit unbekannter Länge im Voraus brechen wir ab.

Da TL die Ausgabe eines Vektors nicht zulässt, musste dieser daher nebenbei hinzugefügt werden. Letztendlich heißt es in der Dokumentation:

Bei der Serialisierung wird immer derselbe Konstruktor „vector“ (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t“) verwendet, der nicht vom spezifischen Wert der Variablen vom Typ t abhängig ist.

Der Wert des optionalen Parameters t ist nicht an der Serialisierung beteiligt, da er vom Ergebnistyp abgeleitet wird (der vor der Deserialisierung immer bekannt ist).

Schau genauer hin: vector {t:Type} # [ t ] = Vector t - aber nirgends Diese Definition selbst besagt nicht, dass die erste Zahl gleich der Länge des Vektors sein muss! Und es kommt nicht von irgendwoher. Dies ist eine Selbstverständlichkeit, die es zu bedenken und mit den eigenen Händen umzusetzen gilt. An anderer Stelle wird in der Dokumentation sogar ehrlich erwähnt, dass der Typ nicht echt ist:

Der polymorphe Pseudotyp Vector t ist ein „Typ“, dessen Wert eine Folge von Werten eines beliebigen Typs t ist, entweder geschachtelt oder nackt.

... konzentriert sich aber nicht darauf. Wenn Sie es satt haben, sich durch die Strapazen der Mathematik zu quälen (die Ihnen vielleicht sogar aus einem Universitätskurs bekannt sind), und sich dazu entschließen, aufzugeben und tatsächlich zu prüfen, wie man damit in der Praxis umgeht, hinterlässt der Eindruck in Ihrem Kopf, dass es sich um eine ernste Angelegenheit handelt Mathematik im Kern, sie wurde eindeutig von Cool People (zwei Mathematikern – ACM-Gewinner) erfunden und nicht von irgendjemandem. Das Ziel – Angeben – wurde erreicht.

Übrigens, was die Zahl angeht. Wir möchten Sie daran erinnern # es ist ein Synonym nat, natürliche Zahl:

Es gibt Typausdrücke (Typausdruck) und numerische Ausdrücke (nat-expr). Sie sind jedoch auf die gleiche Weise definiert.

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

aber in der Grammatik werden sie auf die gleiche Weise beschrieben, d.h. Dieser Unterschied muss erneut beachtet und manuell umgesetzt werden.

Nun ja, Vorlagentypen (vector<int>, vector<User>) haben einen gemeinsamen Bezeichner (#1cb5c415), d.h. wenn Sie wissen, dass der Anruf als angekündigt wird

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

Dann warten Sie nicht mehr nur auf einen Vektor, sondern auf einen Vektor von Benutzern. Etwas präziser, sollte Warten Sie - in echtem Code hat jedes Element, wenn es sich nicht um einen bloßen Typ handelt, einen Konstruktor, und bei der Implementierung wäre es notwendig, dies zu überprüfen - aber wir wurden genau in jedem Element dieses Vektors gesendet dieser Typ? Was wäre, wenn es eine Art PHP wäre, in dem ein Array verschiedene Typen in verschiedenen Elementen enthalten kann?

An diesem Punkt beginnen Sie zu denken: Ist ein solcher TL notwendig? Vielleicht wäre es für den Wagen möglich, einen menschlichen Serialisierer zu verwenden, denselben Protobuf, den es damals bereits gab? Das war die Theorie, schauen wir uns die Praxis an.

Vorhandene TL-Implementierungen im Code

TL wurde in den Tiefen von VKontakte bereits vor den berühmten Ereignissen mit dem Verkauf von Durovs Anteilen geboren und (sicherlich), noch bevor die Entwicklung von Telegram begann. Und in Open Source Quellcode der ersten Implementierung Es gibt viele lustige Krücken. Und die Sprache selbst wurde dort umfassender umgesetzt als jetzt in Telegram. Beispielsweise werden Hashes im Schema überhaupt nicht verwendet (gemeint ist ein eingebauter Pseudotyp (wie ein Vektor) mit abweichendem Verhalten). Oder

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

Aber der Vollständigkeit halber wollen wir sozusagen die Entwicklung des Giganten des Denkens verfolgen.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Oder dieses schöne:

    static const char *reserved_words_polymorhic[] = {

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

      };

In diesem Fragment geht es um Vorlagen wie:

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

Dies ist die Definition eines Hashmap-Vorlagentyps als Vektor von int-Typ-Paaren. In C++ würde es etwa so aussehen:

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

also, alpha - Stichwort! Aber nur in C++ kann man T schreiben, aber man sollte Alpha, Beta schreiben ... Aber nicht mehr als 8 Parameter, da hört die Fantasie auf. Es scheint, dass in St. Petersburg einst solche Dialoge stattgefunden haben:

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

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

Aber hier ging es um die erste veröffentlichte Implementierung von TL „im Allgemeinen“. Kommen wir nun zu den Implementierungen in den Telegram-Clients selbst.

Wort an Wassili:

Vasily, [09.10.18 17:07] Vor allem ist der Arsch heiß, weil sie eine Menge Abstraktionen erstellt und dann einen Bolzen darauf gehämmert und den Codegenerator mit Krücken abgedeckt haben
Als Ergebnis zunächst von dock pilot.jpg
Dann aus dem Code dzhekichan.webp

Von Leuten, die sich mit Algorithmen und Mathematik auskennen, können wir natürlich erwarten, dass sie Aho und Ullmann gelesen haben und mit den Tools vertraut sind, die im Laufe der Jahrzehnte zum De-facto-Standard in der Branche zum Schreiben ihrer DSL-Compiler geworden sind, oder?

Vom Autor telegram-cli Vitaly Valtman ist, wie aus dem Vorkommen des TLO-Formats außerhalb seiner (cli) Grenzen hervorgeht, ein Mitglied des Teams – jetzt wurde eine Bibliothek für das TL-Parsing zugewiesen getrennt, was ist der Eindruck von ihr TL-Parser? ..

16.12 04:18 Vasily: Ich glaube, jemand beherrscht Lex+Yacc nicht
16.12 04:18 Wassili: Anders kann ich es mir nicht erklären
16.12 04:18 Vasily: Nun, oder sie wurden für die Anzahl der Zeilen in VK bezahlt
16.12 04:19 Vasily: 3k+ Linien usw.<censored> anstelle eines Parsers

Vielleicht eine Ausnahme? Mal sehen, wie Marken Dies ist der OFFIZIELLE Client – ​​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);

Über 1100 Zeilen in Python, ein paar reguläre Ausdrücke + Sonderfälle wie ein Vektor, der natürlich im Schema so deklariert ist, wie es gemäß der TL-Syntax sein sollte, aber sie haben sich zum Parsen auf diese Syntax verlassen ... Es stellt sich die Frage: Warum war das alles ein Wunder?иEs ist vielschichtiger, wenn es sowieso niemand gemäß der Dokumentation analysiert?!

Übrigens... Erinnern Sie sich, dass wir über die CRC32-Prüfung gesprochen haben? Daher gibt es im Telegram Desktop-Codegenerator eine Liste von Ausnahmen für die Typen, bei denen der berechnete CRC32 stimmt nicht überein mit dem im Diagramm angegebenen!

Vasily, [18.12. 22:49] und hier würde ich darüber nachdenken, ob so ein TL nötig ist
Wenn ich mich mit alternativen Implementierungen herumschlagen wollte, würde ich anfangen, Zeilenumbrüche einzufügen. Die Hälfte der Parser unterbricht bei mehrzeiligen Definitionen
tdesktop jedoch auch

Denken Sie an den Punkt zum Einzeiler, wir werden etwas später darauf zurückkommen.

Okay, telegram-cli ist inoffiziell, Telegram Desktop ist offiziell, aber was ist mit den anderen? Wer weiß?... Im Android-Client-Code gab es überhaupt keinen Schema-Parser (was Fragen zu Open Source aufwirft, aber das ist für den zweiten Teil), aber es gab mehrere andere lustige Codeteile, aber mehr dazu im Unterabschnitt unten.

Welche weiteren Fragen wirft die Serialisierung in der Praxis auf? Zum Beispiel haben sie natürlich viele Dinge mit Bitfeldern und bedingten Feldern gemacht:

Wassili: flags.0? true
bedeutet, dass das Feld vorhanden ist und den Wert „true“ hat, wenn das Flag gesetzt ist

Wassili: flags.1? int
bedeutet, dass das Feld vorhanden ist und deserialisiert werden muss

Vasily: Arsch, mach dir keine Sorgen darüber, was du tust!
Vasily: Irgendwo im Dokument wird erwähnt, dass es sich bei true um einen reinen Typ mit der Länge Null handelt, aber es ist unmöglich, etwas aus ihrem Dokument zusammenzustellen
Vasily: Bei den Open-Source-Implementierungen ist das auch nicht der Fall, aber es gibt eine Menge Krücken und Stützen

Was ist mit Telethon? Schauen wir uns das Thema MTProto an, ein Beispiel – in der Dokumentation gibt es solche Teile, aber das Zeichen % es wird nur als „einem bestimmten bloßen Typ entsprechend“ beschrieben, d. h. In den folgenden Beispielen liegt entweder ein Fehler oder etwas Undokumentiertes vor:

Wassili, [22.06.18 18:38] An einer Stelle:

msg_container#73f1f8dc messages:vector message = MessageContainer;

In einem anderen:

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

Und das sind zwei große Unterschiede, im wirklichen Leben gibt es eine Art nackten Vektor

Ich habe keine bloße Vektordefinition gesehen und bin auch nicht auf eine gestoßen

Die Analyse wird im Telethon handschriftlich verfasst

In seinem Diagramm ist die Definition auskommentiert msg_container

Auch hier bleibt die Frage nach %. Es wird nicht beschrieben.

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

Vasily, [22.06.18 19:23] Aber ihr TL-Parser auf regulären Engines wird dies höchstwahrscheinlich auch nicht fressen

// parsed manually

TL ist eine schöne Abstraktion, niemand implementiert sie vollständig

Und % ist nicht in ihrer Version des Schemas

Aber hier widerspricht sich die Dokumentation, also keine Ahnung

Es wurde in der Grammatik gefunden, sie könnten einfach vergessen haben, die Semantik zu beschreiben

Sie haben das Dokument auf TL gesehen, ohne einen halben Liter können Sie es nicht herausfinden

„Sagen wir mal“, wird ein anderer Leser sagen, „Sie kritisieren etwas, also zeigen Sie mir, wie es gemacht werden soll.“

Vasily antwortet: „Was den Parser betrifft, gefallen mir Dinge wie

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

gefällt mir irgendwie besser als

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

oder

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

das ist der GANZE Lexer:

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

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

diese. einfacher ist es gelinde ausgedrückt.“

Im Allgemeinen passen der Parser und der Codegenerator für die tatsächlich verwendete Teilmenge von TL in etwa 100 Grammatikzeilen und etwa 300 Zeilen des Generators (alle zusammengezählt). print's generierten Code), einschließlich Typinformationsbrötchen zur Selbstbeobachtung in jeder Klasse. Jeder polymorphe Typ wird zu einer leeren abstrakten Basisklasse, und Konstruktoren erben von ihr und verfügen über Methoden zur Serialisierung und Deserialisierung.

Mangel an Typen in der Typsprache

Starkes Tippen ist eine gute Sache, oder? Nein, das ist kein Holivar (obwohl ich dynamische Sprachen bevorzuge), sondern ein Postulat im Rahmen von TL. Darauf basierend sollte die Sprache alle möglichen Kontrollen für uns bereitstellen. Na gut, vielleicht nicht er selbst, aber die Umsetzung, aber er sollte sie zumindest beschreiben. Und welche Möglichkeiten wollen wir?

Zunächst einmal Einschränkungen. Hier sehen wir in der Dokumentation zum Hochladen von Dateien:

Der binäre Inhalt der Datei wird dann in Teile aufgeteilt. Alle Teile müssen die gleiche Größe haben ( part_size ) und die folgenden Bedingungen müssen erfüllt sein:

  • part_size % 1024 = 0 (teilbar durch 1 KB)
  • 524288 % part_size = 0 (512 KB müssen gleichmäßig durch part_size teilbar sein)

Der letzte Teil muss diese Bedingungen nicht erfüllen, sofern seine Größe kleiner als part_size ist.

Jeder Teil sollte eine Sequenznummer haben, file_part, mit einem Wert zwischen 0 und 2,999.

Nachdem die Datei partitioniert wurde, müssen Sie eine Methode zum Speichern der Datei auf dem Server auswählen. Verwenden upload.saveBigFilePart falls die Gesamtgröße der Datei mehr als 10 MB beträgt und upload.saveFilePart für kleinere Dateien.
[…] Einer der folgenden Dateneingabefehler kann zurückgegeben werden:

  • FILE_PARTS_INVALID – Ungültige Anzahl von Teilen. Der Wert liegt nicht dazwischen 1..3000

Steht davon etwas im Diagramm? Ist das irgendwie mit TL ausdrückbar? Nein. Aber entschuldigen Sie, schon Großvaters Turbo Pascal konnte die angegebenen Typen beschreiben Bereiche. Und er wusste noch etwas, jetzt besser bekannt als enum - ein Typ, der aus einer Aufzählung einer festen (kleinen) Anzahl von Werten besteht. Beachten Sie, dass wir in Sprachen wie C – numerisch – bisher nur über Typen gesprochen haben Zahlen. Es gibt aber auch Arrays, Strings ... Es wäre zum Beispiel schön zu beschreiben, dass dieser String nur eine Telefonnummer enthalten kann, oder?

Nichts davon steht im TL. Aber es gibt es zum Beispiel im JSON-Schema. Und wenn jemand anderes über die Teilbarkeit von 512 KB argumentieren könnte, dass dies noch im Code überprüft werden muss, dann stellen Sie sicher, dass der Client dies einfach tut konnte nicht Senden Sie eine Nummer außerhalb des gültigen Bereichs 1..3000 (und der entsprechende Fehler hätte nicht auftreten können) Es wäre doch möglich gewesen, oder?..

Übrigens zu Fehlern und Rückgabewerten. Sogar diejenigen, die mit TL gearbeitet haben, verschwimmen vor Augen – das war uns nicht sofort klar jeweils Eine Funktion in TL kann tatsächlich nicht nur den beschriebenen Rückgabetyp, sondern auch einen Fehler zurückgeben. Aus dem TL selbst lässt sich dies jedoch in keiner Weise ableiten. Natürlich ist es bereits klar und es besteht in der Praxis kein Bedarf für irgendetwas (obwohl RPC tatsächlich auf unterschiedliche Weise durchgeführt werden kann, wir werden später darauf zurückkommen) – aber was ist mit der Reinheit der Konzepte der Mathematik abstrakter Typen? aus der himmlischen Welt?.. Ich habe den Schlepper aufgehoben – also kombiniere ihn.

Und schließlich: Wie sieht es mit der Lesbarkeit aus? Nun, im Allgemeinen würde ich gerne Beschreibung Haben Sie es richtig im Schema (im JSON-Schema wiederum ist es das), aber wenn Sie damit bereits überfordert sind, wie sieht es dann mit der praktischen Seite aus – zumindest trivial, wenn Sie sich Unterschiede während Aktualisierungen ansehen? Überzeugen Sie sich selbst unter echte Beispiele:

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

oder

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

Es hängt von jedem ab, aber GitHub beispielsweise weigert sich, Änderungen innerhalb solch langer Zeilen hervorzuheben. Das Spiel „Finde 10 Unterschiede“, und was das Gehirn sofort sieht, ist, dass der Anfang und das Ende in beiden Beispielen gleich sind, man muss mühsam irgendwo in der Mitte lesen … Meiner Meinung nach ist das nicht nur theoretisch, aber rein optisch schmutzig und schlampig.

Übrigens zur Reinheit der Theorie. Warum brauchen wir Bitfelder? Scheint es nicht, dass sie Geruch schlecht aus typtheoretischer Sicht? Die Erklärung ist in früheren Versionen des Diagramms zu sehen. Zuerst ja, so war das, für jedes Niesen wurde ein neuer Typ geschaffen. Diese Rudimente existieren in dieser Form noch, zum Beispiel:

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;

Aber stellen Sie sich nun vor, wenn Ihre Struktur 5 optionale Felder enthält, dann benötigen Sie 32 Typen für alle möglichen Optionen. Kombinatorische Explosion. Damit zerbrach die kristallklare Reinheit der TL-Theorie erneut am gusseisernen Arsch der harten Realität der Serialisierung.

Darüber hinaus verletzen diese Jungs an manchen Stellen selbst ihre eigene Typologie. Beispielsweise kann in MTProto (nächstes Kapitel) die Antwort durch Gzip komprimiert werden, alles ist in Ordnung – außer dass die Schichten und der Schaltkreis verletzt werden. Wieder einmal wurde nicht RpcResult selbst geerntet, sondern sein Inhalt. Nun, warum sollte ich das tun? Ich musste eine Krücke einschneiden, damit die Kompression überall funktionierte.

Oder ein anderes Beispiel: Wir haben einmal einen Fehler entdeckt - er wurde gesendet InputPeerUser statt InputUser. Oder umgekehrt. Aber es hat funktioniert! Das heißt, dem Server war der Typ egal. Wie kann das sein? Die Antwort können uns Codefragmente von telegram-cli geben:

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

Mit anderen Worten: Hier erfolgt die Serialisierung MANUELL, nicht generierter Code! Vielleicht ist der Server auf ähnliche Weise implementiert? Im Prinzip funktioniert das, wenn man es einmal macht, aber wie kann es später bei Updates unterstützt werden? Wurde das System deshalb erfunden? Und hier kommen wir zur nächsten Frage.

Versionierung. Lagen

Warum die Schaltplanversionen als Layer bezeichnet werden, lässt sich anhand der Geschichte der veröffentlichten Schaltpläne nur spekulieren. Anscheinend dachten die Autoren zunächst, dass grundlegende Dinge mit dem unveränderten Schema erledigt werden könnten, und gaben nur bei Bedarf und bei konkreten Anfragen an, dass sie mit einer anderen Version erledigt würden. Im Prinzip sogar eine gute Idee – und das Neue wird sozusagen „gemischt“, über das Alte geschichtet. Aber schauen wir mal, wie es gemacht wurde. Es stimmt, ich konnte es mir nicht von Anfang an ansehen – es ist lustig, aber das Diagramm der Basisschicht existiert einfach nicht. Die Ebenen begannen mit 2. In der Dokumentation erfahren wir von einer besonderen TL-Funktion:

Wenn ein Client Layer 2 unterstützt, muss der folgende Konstruktor verwendet werden:

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

In der Praxis bedeutet dies, dass vor jedem API-Aufruf ein int mit dem Wert erstellt wird 0x289dd1f6 muss vor der Methodennummer hinzugefügt werden.

Klingt normal. Aber was geschah als nächstes? Dann erschien

invokeWithLayer3#b7475268 query:!X = X;

Und weiter? Wie Sie vielleicht erraten haben,

invokeWithLayer4#dea0d430 query:!X = X;

Lustig? Nein, es ist zu früh zum Lachen, denken Sie darüber nach jeder Eine Anfrage von einer anderen Ebene muss in einen so speziellen Typ verpackt werden. Wenn sie für Sie alle unterschiedlich sind, wie können Sie sie sonst unterscheiden? Und das Hinzufügen von nur 4 Bytes davor ist eine ziemlich effiziente Methode. Also,

invokeWithLayer5#417a57ae query:!X = X;

Aber es ist klar, dass dies nach einer Weile zu einer Art Bacchanie werden wird. Und die Lösung kam:

Update: Ab Layer 9 Hilfsmethoden invokeWithLayerN Kann nur zusammen mit verwendet werden initConnection

Hurra! Nach 9 Versionen kamen wir endlich zu dem, was in den 80er Jahren bei Internetprotokollen gemacht wurde – die Einigung auf die Version einmal zu Beginn der Verbindung!

Und weiter?..

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

Aber jetzt kann man immer noch lachen. Erst nach weiteren 9 Schichten wurde schließlich ein universeller Konstruktor mit einer Versionsnummer hinzugefügt, der zu Beginn der Verbindung nur einmal aufgerufen werden muss, und die Bedeutung der Schichten schien verschwunden zu sein, jetzt handelt es sich nur noch um eine bedingte Version überall sonst. Das Problem ist gelöst.

Genau?..

Vasily, [16.07.18 14:01] Noch am Freitag dachte ich:
Der Teleserver sendet Ereignisse ohne Anfrage. Anforderungen müssen in InvokeWithLayer eingeschlossen werden. Der Server umschließt keine Aktualisierungen; es gibt keine Struktur zum Umschließen von Antworten und Aktualisierungen.

Diese. Der Client kann nicht angeben, auf welcher Ebene er Aktualisierungen wünscht

Vadim Goncharov, [16.07.18 14:02] Ist InvokeWithLayer nicht im Prinzip eine Krücke?

Wassili, [16.07.18 14:02] Das ist der einzige Weg

Vadim Goncharov, [16.07.18 14:02], was im Wesentlichen bedeuten sollte, dass man sich zu Beginn der Sitzung auf die Ebene einigt

Daraus folgt übrigens, dass kein Client-Downgrade vorgesehen ist

Updates, d.h. Typ Updates Im Schema ist dies das, was der Server nicht als Antwort auf eine API-Anfrage an den Client sendet, sondern unabhängig, wenn ein Ereignis auftritt. Dies ist ein komplexes Thema, das in einem anderen Beitrag behandelt wird. Zunächst ist es jedoch wichtig zu wissen, dass der Server Updates speichert, auch wenn der Client offline ist.

Also, wenn Sie sich weigern, einzuwickeln jeder Wenn Sie das Paket auf seine Version hinweisen, führt dies logischerweise zu folgenden möglichen Problemen:

  • Der Server sendet Updates an den Client, noch bevor dieser mitgeteilt hat, welche Version er unterstützt
  • Was soll ich nach dem Upgrade des Clients tun?
  • welche garantiertdass sich die Meinung des Servers über die Layer-Nummer während des Prozesses nicht ändert?

Denken Sie, dass dies eine rein theoretische Spekulation ist und dass dies in der Praxis nicht passieren kann, weil der Server korrekt geschrieben ist (zumindest wurde er gut getestet)? Ha! Egal wie es ist!

Genau das ist uns im August passiert. Am 14. August gab es Meldungen, dass auf den Telegram-Servern etwas aktualisiert wurde ... und dann in den Protokollen:

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.

und dann mehrere Megabyte Stack-Traces (naja, gleichzeitig wurde die Protokollierung behoben). Denn wenn etwas in Ihrem TL nicht erkannt wird, ist es später per Signatur binär ALL geht, wird die Dekodierung unmöglich. Was sollten Sie in einer solchen Situation tun?

Nun, das erste, was einem in den Sinn kommt, ist, die Verbindung zu trennen und es erneut zu versuchen. Hat nicht geholfen. Wir googeln CRC32 – es stellte sich heraus, dass es sich um Objekte aus Schema 73 handelte, obwohl wir an 82 gearbeitet haben. Wir schauen uns die Protokolle genau an – es gibt Identifikatoren aus zwei verschiedenen Schemata!

Vielleicht liegt das Problem nur bei unserem inoffiziellen Kunden? Nein, wir starten Telegram Desktop 1.2.17 (Version, die in einer Reihe von Linux-Distributionen enthalten ist). Es schreibt in das Ausnahmeprotokoll: MTP Unexpected type id #b5223b0f read in MTPMessageMedia…

Kritik am Protokoll und den organisatorischen Ansätzen von Telegram. Teil 1, technisch: Erfahrung beim Schreiben eines Kunden von Grund auf – TL, MT

Google zeigte, dass ein ähnliches Problem bereits bei einem der inoffiziellen Clients aufgetreten war, allerdings waren die Versionsnummern und damit auch die Annahmen unterschiedlich...

Also, was sollten wir tun? Vasily und ich haben uns getrennt: Er hat versucht, die Schaltung auf 91 zu aktualisieren, ich habe beschlossen, ein paar Tage zu warten und 73 auszuprobieren. Beide Methoden haben funktioniert, aber da sie empirisch sind, gibt es kein Verständnis dafür, wie viele Versionen nach oben oder unten Sie benötigen um zu springen, oder wie lange Sie warten müssen.

Später konnte ich die Situation reproduzieren: Wir starten den Client, schalten ihn aus, kompilieren die Schaltung neu auf eine andere Ebene, starten neu, erkennen das Problem erneut, kehren zum vorherigen zurück – ups, keine Schaltungsumschaltung und Client-Neustarts für eine Weile Ein paar Minuten werden helfen. Sie erhalten einen Mix aus Datenstrukturen aus verschiedenen Schichten.

Erläuterung? Wie Sie anhand verschiedener indirekter Symptome erraten können, besteht der Server aus vielen Prozessen unterschiedlichen Typs auf verschiedenen Maschinen. Höchstwahrscheinlich hat der Server, der für die „Pufferung“ verantwortlich ist, das, was seine Vorgesetzten ihm gegeben haben, in die Warteschlange gestellt, und zwar nach dem Schema, das zum Zeitpunkt der Generierung galt. Und bis diese Warteschlange „verfault“ war, konnte nichts dagegen unternommen werden.

Vielleicht... aber das ist eine schreckliche Krücke?!... Nein, bevor wir über verrückte Ideen nachdenken, werfen wir einen Blick auf den Code der offiziellen Clients. In der Android-Version finden wir keinen TL-Parser, aber eine umfangreiche Datei (GitHub weigert sich, sie zu bearbeiten) mit (De)Serialisierung. Hier sind die Codeausschnitte:

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;

oder

    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... sieht wild aus. Aber wahrscheinlich ist das generierter Code, dann okay? Aber es unterstützt auf jeden Fall alle Versionen! Es stimmt, es ist nicht klar, warum alles vermischt ist, geheime Chats und alles Mögliche _old7 Irgendwie sieht es nicht nach Maschinengeneration aus... Am meisten hat es mich jedoch umgehauen

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Leute, könnt ihr nicht einmal entscheiden, was sich in einer Schicht befindet?! Na gut, nehmen wir an, „zwei“ wurden mit einem Fehler veröffentlicht, nun ja, das kommt vor, aber DREI?.. Gleich wieder derselbe Rake? Was für eine Art Pornografie ist das, sorry?

Im Quellcode von Telegram Desktop passiert übrigens etwas Ähnliches – wenn ja, ändern mehrere Commits hintereinander an das Schema nicht seine Layer-Nummer, sondern korrigieren etwas. Wenn es keine offizielle Datenquelle für das System gibt, woher können diese dann bezogen werden, mit Ausnahme des Quellcodes des offiziellen Kunden? Und wenn man es von dort aus betrachtet, kann man erst dann sicher sein, dass das Schema vollständig korrekt ist, wenn man alle Methoden getestet hat.

Wie kann man das überhaupt testen? Ich hoffe, dass Fans von Geräte-, Funktions- und anderen Tests in den Kommentaren mitmachen.

Okay, schauen wir uns einen anderen Code an:

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;

Dieser Kommentar „manuell erstellt“ deutet darauf hin, dass nur ein Teil dieser Datei manuell geschrieben wurde (können Sie sich den ganzen Wartungsalbtraum vorstellen?) und der Rest maschinell generiert wurde. Dann stellt sich jedoch eine andere Frage: ob die Quellen verfügbar sind Nicht mehr verfügbar (a la GPL-Blobs im Linux-Kernel), aber das ist bereits ein Thema für den zweiten Teil.

Aber genug. Kommen wir nun zum Protokoll, auf dem die gesamte Serialisierung läuft.

MT-Proto

Also, lasst uns öffnen allgemeine Beschreibung и detaillierte Beschreibung des Protokolls und das erste, worüber wir stolpern, ist die Terminologie. Und mit einer Fülle von allem. Im Allgemeinen scheint dies eine proprietäre Funktion von Telegram zu sein – Dinge an verschiedenen Orten unterschiedlich aufzurufen, oder unterschiedliche Dinge mit einem Wort, oder umgekehrt (wenn Sie beispielsweise in einer High-Level-API ein Aufkleberpaket sehen, ist dies nicht der Fall). was Sie dachten).

Beispielsweise bedeuten „Nachricht“ und „Sitzung“ hier etwas anderes als in der üblichen Telegram-Client-Oberfläche. Nun, mit der Nachricht ist alles klar, sie könnte in OOP-Begriffen interpretiert oder einfach als „Paket“ bezeichnet werden – dies ist eine niedrige Transportebene, es gibt nicht die gleichen Nachrichten wie in der Schnittstelle, es gibt viele Servicenachrichten . Aber die Sitzung... aber das Wichtigste zuerst.

Transportschicht

Das erste ist der Transport. Sie werden uns über 5 Optionen informieren:

  • TCP
  • Web-Socket
  • Websocket über HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Es gibt auch einen UDP-Transport, der jedoch nicht dokumentiert ist

Und TCP in drei Varianten

Die erste Variante ähnelt UDP über TCP, jedes Paket enthält eine Sequenznummer und einen CRC
Warum ist das Lesen von Dokumenten auf einem Einkaufswagen so schmerzhaft?

Nun, da ist es jetzt TCP bereits in 4 Varianten:

  • Abridged
  • Mittel
  • Gepolstertes Mittelteil
  • Vollständiger

Na gut, gepolstertes Zwischenprodukt für MTProxy, das wurde später aufgrund bekannter Ereignisse hinzugefügt. Aber warum zwei weitere Versionen (insgesamt drei), wenn man auch mit einer auskommen könnte? Alle vier unterscheiden sich im Wesentlichen nur darin, wie die Länge und Nutzlast des Haupt-MTProto eingestellt wird, worauf weiter unten eingegangen wird:

  • In Abridged sind es 1 oder 4 Bytes, aber nicht 0xef, dann der Körper
  • In der Mittelstufe beträgt die Länge 4 Bytes und ein Feld, und beim ersten Mal muss der Client senden 0xeeeeeeee um anzuzeigen, dass es sich um eine Mittelstufe handelt
  • Aus der Sicht eines Netzwerkers am meisten süchtig machend: Länge, Sequenznummer und NICHT DAS EINE, das hauptsächlich MTProto, Körper, CRC32 ist. Ja, das alles läuft über TCP. Dadurch erhalten wir einen zuverlässigen Transport in Form eines sequentiellen Bytestroms; es sind keine Sequenzen erforderlich, insbesondere keine Prüfsummen. Okay, jetzt wird mir jemand einwenden, dass TCP eine 16-Bit-Prüfsumme hat, sodass es zu Datenbeschädigungen kommt. Großartig, aber wir haben tatsächlich ein kryptografisches Protokoll mit Hashes, die länger als 16 Byte sind. Alle diese Fehler – und noch mehr – werden durch eine SHA-Nichtübereinstimmung auf einer höheren Ebene abgefangen. Darüber hinaus hat CRC32 KEINEN Sinn.

Vergleichen wir Abridged, bei dem ein Byte Länge möglich ist, mit Intermediate, was rechtfertigt: „Für den Fall, dass eine 4-Byte-Datenausrichtung erforderlich ist“, was ziemlicher Unsinn ist. Was, es wird angenommen, dass Telegram-Programmierer so inkompetent sind, dass sie keine Daten von einem Socket in einen ausgerichteten Puffer lesen können? Das müssen Sie trotzdem machen, denn beim Lesen können Sie beliebig viele Bytes zurückgeben (und es gibt z. B. auch Proxy-Server...). Oder warum sollten wir andererseits „Abridged“ blockieren, wenn wir immer noch eine kräftige Auffüllung über 16 Bytes haben – sparen Sie 3 Bytes? manchmal ?

Man hat den Eindruck, dass Nikolai Durov wirklich gerne das Rad neu erfindet, einschließlich der Netzwerkprotokolle, ohne dass dafür ein wirklicher praktischer Bedarf besteht.

Weitere Transportmöglichkeiten, inkl. Web und MTProxy werden wir jetzt nicht berücksichtigen, vielleicht in einem anderen Beitrag, wenn es eine Anfrage gibt. Erinnern wir uns jetzt nur daran, dass die Anbieter kurz nach seiner Veröffentlichung im Jahr 2018 schnell gelernt haben, ihn zu blockieren Bypass-BlockierungDurch Packungsgrösse! Und auch die Tatsache, dass der (wieder von Waltman) in C geschriebene MTProxy-Server übermäßig an Linux-Besonderheiten gebunden war, obwohl dies überhaupt nicht erforderlich war (Phil Kulin wird es bestätigen), und dass ein ähnlicher Server entweder in Go oder Node.js dies tun würde passen in weniger als hundert Zeilen.

Aber wir werden am Ende des Abschnitts, nachdem wir andere Fragen berücksichtigt haben, Schlussfolgerungen über die technische Kompetenz dieser Personen ziehen. Kommen wir zunächst zur OSI-Schicht 5, Sitzung – auf der die MTProto-Sitzung platziert wurde.

Schlüssel, Nachrichten, Sitzungen, Diffie-Hellman

Sie haben es dort nicht ganz richtig platziert... Eine Sitzung ist nicht dieselbe Sitzung, die in der Benutzeroberfläche unter Aktive Sitzungen sichtbar ist. Aber der Reihe nach.

Kritik am Protokoll und den organisatorischen Ansätzen von Telegram. Teil 1, technisch: Erfahrung beim Schreiben eines Kunden von Grund auf – TL, MT

Wir haben also eine Bytefolge bekannter Länge von der Transportschicht erhalten. Dabei handelt es sich entweder um eine verschlüsselte Nachricht oder um Klartext – sofern wir uns noch im Stadium der Schlüsselvereinbarung befinden und dies tatsächlich tun. Von welchem ​​Konzept namens „Schlüssel“ sprechen wir? Lassen Sie uns dieses Problem für das Telegram-Team selbst klären (ich entschuldige mich dafür, dass ich meine eigene Dokumentation um 4 Uhr morgens mit müdem Gehirn aus dem Englischen übersetzt habe. Es war einfacher, einige Sätze so zu belassen, wie sie sind):

Es gibt zwei aufgerufene Entitäten Sitzung - eine in der Benutzeroberfläche offizieller Clients unter „Aktuelle Sitzungen“, wobei jede Sitzung einem gesamten Gerät/Betriebssystem entspricht.
Die zweite - MTProto-Sitzung, die die Sequenznummer der Nachricht (im unteren Sinne) enthält und welche kann zwischen verschiedenen TCP-Verbindungen dauern. Es können mehrere MTProto-Sitzungen gleichzeitig installiert werden, um beispielsweise das Herunterladen von Dateien zu beschleunigen.

Zwischen diesen beiden Sessions Es gibt ein Konzept Genehmigung. Im entarteten Fall können wir das sagen UI-Sitzung ist das gleiche wie Genehmigung, aber leider ist alles kompliziert. Lass uns nachsehen:

  • Der Benutzer auf dem neuen Gerät generiert zunächst Authentifizierungsschlüssel und bindet es an das Konto, zum Beispiel per SMS – deshalb Genehmigung
  • Es geschah im ersten MTProto-Sitzung, die hat session_id in dir selbst.
  • In diesem Schritt erfolgt die Kombination Genehmigung и session_id aufgerufen werden könnte Instanz – Dieses Wort kommt in der Dokumentation und im Code einiger Clients vor
  • Anschließend kann der Client geöffnet werden mehrere MTProto-Sitzungen unter dem gleichen Authentifizierungsschlüssel - zum gleichen DC.
  • Dann muss der Kunde eines Tages die Datei anfordern ein weiterer DC - und für diesen DC wird ein neuer generiert Authentifizierungsschlüssel !
  • Um das System darüber zu informieren, dass es sich nicht um einen neuen Benutzer handelt, sondern um denselben Genehmigung (UI-Sitzung), verwendet der Client API-Aufrufe auth.exportAuthorization im heimischen DC auth.importAuthorization im neuen DC.
  • Alles ist gleich, mehrere können offen sein MTProto-Sitzungen (jedes mit seinem eigenen session_id) zu diesem neuen DC, unter seine Authentifizierungsschlüssel.
  • Schließlich möchte der Kunde möglicherweise Perfect Forward Secrecy. Jeden Authentifizierungsschlüssel war dauerhaft Schlüssel - pro DC - und der Client kann anrufen auth.bindTempAuthKey für den Einsatz vorübergehend Authentifizierungsschlüssel - und wieder nur einer temp_auth_key pro DC, allen gemeinsam MTProto-Sitzungen zu diesem DC.

Beachten Sie, dass Salz (und zukünftige Salze) ist auch ein Thema Authentifizierungsschlüssel diese. zwischen allen geteilt MTProto-Sitzungen zum gleichen DC.

Was bedeutet „zwischen verschiedenen TCP-Verbindungen“? Das bedeutet also etwas wie Autorisierungscookie auf einer Website – es bleibt bei vielen TCP-Verbindungen zu einem bestimmten Server bestehen (überlebt), aber eines Tages geht es kaputt. Nur im Gegensatz zu HTTP werden bei MTProto Nachrichten innerhalb einer Sitzung fortlaufend nummeriert und bestätigt; wenn sie in den Tunnel gelangten, wurde die Verbindung unterbrochen – nach dem Aufbau einer neuen Verbindung sendet der Server freundlicherweise in dieser Sitzung alles, was er in der vorherigen nicht zugestellt hat TCP-Verbindung.

Die oben genannten Informationen sind jedoch nach vielen Monaten der Untersuchung zusammengefasst. Implementieren wir in der Zwischenzeit unseren Client von Grund auf? - Gehen wir zurück zum Anfang.

Also lasst uns generieren auth_key auf Diffie-Hellman-Versionen von Telegram. Versuchen wir, die Dokumentation zu verstehen ...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (beliebige zufällige Bytes); so dass die Länge 255 Bytes beträgt;
verschlüsselte_Daten := RSA(data_with_hash, server_public_key); Eine 255 Byte lange Zahl (Big Endian) wird auf die erforderliche Potenz über den erforderlichen Modul erhöht und das Ergebnis wird als 256 Byte lange Zahl gespeichert.

Sie haben ein tolles DH

Sieht nicht nach dem DH eines gesunden Menschen aus
In dx gibt es keine zwei öffentlichen Schlüssel

Nun, am Ende wurde das geklärt, aber es blieb ein Rückstand übrig – der Arbeitsnachweis des Kunden liegt vor, dass er die Zahl faktorisieren konnte. Art des Schutzes gegen DoS-Angriffe. Und der RSA-Schlüssel wird nur einmal in eine Richtung verwendet, im Wesentlichen zur Verschlüsselung new_nonce. Aber während diese scheinbar einfache Operation gelingt, was müssen Sie dann bewältigen?

Vasily, [20.06.18 00:26] Ich bin noch nicht zur Appid-Anfrage gekommen

Ich habe diese Anfrage an DH gesendet

Und im Transportdock heißt es, dass es mit 4 Bytes eines Fehlercodes antworten kann. Und alle

Nun, er sagte mir -404, na und?

Also sagte ich zu ihm: „Fang deinen Blödsinn, verschlüsselt mit einem Serverschlüssel mit einem Fingerabdruck wie diesem, ich will DH“, und er antwortete mit einem dummen 404

Was würden Sie von dieser Serverantwort halten? Was zu tun ist? Es gibt niemanden, den man fragen kann (mehr dazu im zweiten Teil).

Hier wird das ganze Interesse am Dock erledigt

Ich habe nichts anderes zu tun, ich habe nur davon geträumt, Zahlen hin und her umzurechnen

Zwei 32-Bit-Zahlen. Ich habe sie wie alle anderen eingepackt

Aber nein, diese beiden müssen zuerst als BE zur Zeile hinzugefügt werden

Vadim Goncharov, [20.06.18 15:49] und aus diesem Grund 404?

Wassili, [20.06.18 15:49] JA!

Vadim Goncharov, [20.06.18 15:50] also verstehe ich nicht, was er „nicht gefunden“ haben kann

Wassili, [20.06.18 15:50] über

Eine solche Zerlegung in Primfaktoren konnte ich nicht finden.%)

Wir haben nicht einmal die Fehlerberichterstattung verwaltet

Vasily, [20.06.18 20:18] Oh, da ist auch MD5. Bereits drei verschiedene Hashes

Der Schlüsselfingerabdruck wird wie folgt berechnet:

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

SHA1 und sha2

Sagen wir es mal auth_key Mit Diffie-Hellman haben wir eine Größe von 2048 Bit erhalten. Was weiter? Als nächstes stellen wir fest, dass die unteren 1024 Bits dieses Schlüssels in keiner Weise verwendet werden ... aber lassen Sie uns zunächst einmal darüber nachdenken. In diesem Schritt haben wir ein gemeinsames Geheimnis mit dem Server. Es wurde ein Analogon zur TLS-Sitzung etabliert, bei der es sich um ein sehr teures Verfahren handelt. Aber der Server weiß immer noch nichts darüber, wer wir sind! Eigentlich noch nicht. Genehmigung. Diese. wenn Sie an „Login-Passwort“ denken, wie Sie es früher in ICQ getan haben, oder zumindest an „Login-Schlüssel“, wie in SSH (zum Beispiel auf einigen Gitlabs/Githubs). Wir haben eine anonyme Nachricht erhalten. Was passiert, wenn der Server uns mitteilt, dass „diese Telefonnummern von einem anderen DC bedient werden“? Oder sogar „Ihre Telefonnummer ist gesperrt“? Das Beste, was wir tun können, ist, den Schlüssel aufzubewahren, in der Hoffnung, dass er nützlich ist und bis dahin nicht verrottet.

Wir haben es übrigens unter Vorbehalt „erhalten“. Vertrauen wir beispielsweise dem Server? Was ist, wenn es eine Fälschung ist? Kryptografische Prüfungen wären erforderlich:

Vasily, [21.06.18 17:53] Sie bieten mobilen Clients die Möglichkeit, eine 2-kbit-Nummer auf Primalität zu überprüfen.

Aber es ist überhaupt nicht klar, nafeijoa

Vasily, [21.06.18 18:02] Das Dokument sagt nicht, was zu tun ist, wenn sich herausstellt, dass es nicht einfach ist

Nicht gesagt. Mal sehen, was der offizielle Android-Client in diesem Fall macht? A das ist was (Und ja, die ganze Datei ist interessant) – wie man so schön sagt, lasse ich das einfach hier:

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

Nein, natürlich ist es immer noch da einige Es gibt Tests für die Primzahl einer Zahl, aber ich persönlich verfüge nicht mehr über ausreichende Mathematikkenntnisse.

Okay, wir haben den Hauptschlüssel. Um sich anzumelden, d.h. Um Anfragen zu senden, müssen Sie eine weitere Verschlüsselung mit AES durchführen.

Der Nachrichtenschlüssel ist definiert als die 128 mittleren Bits des SHA256 des Nachrichtentexts (einschließlich Sitzung, Nachrichten-ID usw.), einschließlich der Füllbytes, denen 32 Bytes aus dem Autorisierungsschlüssel vorangestellt sind.

Vasily, [22.06.18 14:08] Durchschnitt, Schlampe, Bits

Erhalten auth_key. Alle. Darüber hinaus ... geht aus dem Dokument nicht klar hervor. Fühlen Sie sich frei, den Open-Source-Code zu studieren.

Beachten Sie, dass MTProto 2.0 eine Auffüllung von 12 bis 1024 Bytes erfordert, allerdings unter der Bedingung, dass die resultierende Nachrichtenlänge durch 16 Bytes teilbar ist.

Wie viel Polsterung sollten Sie also hinzufügen?

Und ja, es gibt auch eine 404 im Fehlerfall

Wenn jemand das Diagramm und den Text der Dokumentation sorgfältig studiert hat, ist ihm aufgefallen, dass dort kein MAC vorhanden ist. Und dass AES in einem bestimmten IGE-Modus verwendet wird, der nirgendwo anders verwendet wird. Darüber schreiben sie natürlich in ihren FAQ... Hier ist der Nachrichtenschlüssel selbst auch der SHA-Hash der entschlüsselten Daten, der zur Überprüfung der Integrität – und im Falle einer Nichtübereinstimmung aus irgendeinem Grund – der Dokumentation verwendet wird empfiehlt, sie stillschweigend zu ignorieren (aber was ist mit der Sicherheit, was ist, wenn sie uns zerstören?).

Ich bin kein Kryptograf, aus theoretischer Sicht ist an diesem Modus in diesem Fall vielleicht nichts auszusetzen. Aber ein praktisches Problem kann ich am Beispiel von Telegram Desktop klar benennen. Es verschlüsselt den lokalen Cache (alle diese D877F783D5D3EF8C) auf die gleiche Weise wie Nachrichten in MTProto (nur in diesem Fall Version 1.0), d. h. zuerst der Nachrichtenschlüssel, dann die Daten selbst (und irgendwo daneben der Hauptschlüssel). auth_key 256 Bytes, ohne die msg_key nutzlos). Daher macht sich das Problem bei großen Dateien bemerkbar. Sie müssen nämlich zwei Kopien der Daten aufbewahren – verschlüsselt und entschlüsselt. Und wenn es Megabyte gibt, oder zum Beispiel Streaming-Video?... Klassische Schemata mit MAC nach dem Chiffretext ermöglichen es Ihnen, den Stream zu lesen und ihn sofort zu übertragen. Aber mit MTProto müssen Sie es tun zuerst Verschlüsseln oder entschlüsseln Sie die gesamte Nachricht und übertragen Sie sie erst dann an das Netzwerk oder auf die Festplatte. Daher in den neuesten Versionen von Telegram Desktop im Cache user_data Es kommt auch ein anderes Format zum Einsatz – mit AES im CTR-Modus.

Vasily, [21.06.18 01:27] Oh, ich habe herausgefunden, was IGE ist: IGE war der erste Versuch eines „authentifizierenden Verschlüsselungsmodus“, ursprünglich für Kerberos. Der Versuch war fehlgeschlagen (er bietet keinen Integritätsschutz) und musste entfernt werden. Das war der Beginn einer 20-jährigen Suche nach einem funktionierenden Authentifizierungs-Verschlüsselungsmodus, der kürzlich in Modi wie OCB und GCM seinen Höhepunkt fand.

Und nun die Argumente von der Warenkorbseite:

Das Team hinter Telegram unter der Leitung von Nikolai Durov besteht aus sechs ACM-Champions, von denen die Hälfte Doktortitel in Mathematik haben. Die Einführung der aktuellen Version von MTProto dauerte etwa zwei Jahre.

Das ist lustig. Zwei Jahre auf der unteren Ebene

Oder Sie könnten einfach TLS nehmen

Okay, nehmen wir an, wir haben die Verschlüsselung und andere Nuancen vorgenommen. Ist es endlich möglich, in TL serialisierte Anfragen zu senden und die Antworten zu deserialisieren? Was und wie sollten Sie also versenden? Hier, sagen wir mal, die Methode initConnection, vielleicht ist es das?

Vasily, [25.06.18 18:46] Initialisiert die Verbindung und speichert Informationen zum Gerät und zur Anwendung des Benutzers.

Es akzeptiert app_id, device_model, system_version, app_version und lang_code.

Und eine Frage

Dokumentation wie immer. Fühlen Sie sich frei, die Open Source zu studieren

Wenn bei invokeWithLayer alles annähernd klar wäre, was ist dann hier falsch? Es stellt sich heraus, dass wir – sagen wir mal – der Client hatte bereits etwas, worüber er den Server fragen wollte – eine Anfrage haben, die wir senden wollten:

Vasily, [25.06.18 19:13] Dem Code nach zu urteilen, ist der erste Aufruf in diesen Mist verpackt, und der Mist selbst ist in invokewithlayer verpackt

Warum konnte initConnection kein separater Aufruf sein, sondern muss ein Wrapper sein? Ja, wie sich herausstellte, muss dies jedes Mal zu Beginn jeder Sitzung erfolgen und nicht nur einmal, wie beim Hauptschlüssel. Aber! Es kann nicht von einem unbefugten Benutzer aufgerufen werden! Jetzt haben wir das Stadium erreicht, in dem es anwendbar ist dieser hier Dokumentationsseite - und sie sagt uns, dass...

Nur ein kleiner Teil der API-Methoden steht nicht autorisierten Benutzern zur Verfügung:

  • 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

Der allererste von ihnen, auth.sendCode, und es gibt diese geschätzte erste Anfrage, bei der wir api_id und api_hash senden und anschließend eine SMS mit einem Code erhalten. Und wenn wir uns im falschen DC befinden (Telefonnummern in diesem Land werden beispielsweise von einem anderen bedient), dann erhalten wir eine Fehlermeldung mit der Nummer des gewünschten DC. Helfen Sie uns, anhand der DC-Nummer herauszufinden, mit welcher IP-Adresse Sie eine Verbindung herstellen müssen help.getConfig. Früher gab es nur 5 Einträge, aber nach den berühmten Ereignissen von 2018 ist die Zahl deutlich gestiegen.

Erinnern wir uns nun daran, dass wir dieses Stadium auf dem Server anonym erreicht haben. Ist es nicht zu teuer, nur eine IP-Adresse zu bekommen? Warum führen Sie diese und andere Vorgänge nicht im unverschlüsselten Teil von MTProto durch? Ich höre den Einwand: „Wie können wir sicherstellen, dass nicht RKN mit falschen Adressen antwortet?“ Daran erinnern wir uns, dass im Allgemeinen offizielle Kunden RSA-Schlüssel sind eingebettet, d.h. kannst du einfach Zeichen diese Information. Tatsächlich wird dies bereits durchgeführt, um Informationen zur Umgehung von Blockierungen zu erhalten, die Clients über andere Kanäle erhalten (logischerweise ist dies in MTProto selbst nicht möglich; Sie müssen auch wissen, wo Sie eine Verbindung herstellen müssen).

Okay. Zum jetzigen Zeitpunkt der Kundenautorisierung sind wir noch nicht autorisiert und haben unseren Antrag noch nicht registriert. Wir möchten zunächst nur sehen, wie der Server auf Methoden reagiert, die einem nicht autorisierten Benutzer zur Verfügung stehen. Und hier…

Wassili, [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;

Im Schema kommt der Erste an den Zweiten

Im tdesktop-Schema ist der dritte Wert

Ja, seitdem wurde die Dokumentation natürlich aktualisiert. Auch wenn es bald wieder bedeutungslos werden könnte. Woher sollte ein unerfahrener Entwickler wissen? Vielleicht werden Sie darüber informiert, wenn Sie Ihre Bewerbung registrieren? Vasily tat dies, aber leider schickten sie ihm nichts (wir werden im zweiten Teil noch einmal darüber sprechen).

...Sie haben bemerkt, dass wir bereits irgendwie auf die API umgestiegen sind, d. h. Zum nächsten Level und haben Sie etwas im MTProto-Thema verpasst? Keine Überraschung:

Vasily, [28.06.18 02:04] Mm, sie durchstöbern einige der Algorithmen auf e2e

Mtproto definiert Verschlüsselungsalgorithmen und Schlüssel für beide Domänen sowie eine Art Wrapper-Struktur

Aber sie vermischen ständig verschiedene Ebenen des Stapels, sodass nicht immer klar ist, wo mtproto endete und die nächste Ebene begann

Wie mischen sie sich? Nun, hier ist zum Beispiel derselbe temporäre Schlüssel für PFS (Telegram Desktop kann das übrigens nicht). Es wird durch eine API-Anfrage ausgeführt auth.bindTempAuthKey, d.h. von der obersten Ebene. Gleichzeitig beeinträchtigt es jedoch die Verschlüsselung auf der unteren Ebene – danach müssen Sie es beispielsweise erneut durchführen initConnection usw., das ist nicht der Fall nur normale Anfrage. Das Besondere ist auch, dass Sie pro DC nur EINEN temporären Schlüssel haben können, obwohl das Feld auth_key_id In jeder Nachricht können Sie den Schlüssel in mindestens jeder Nachricht ändern, und der Server hat das Recht, den temporären Schlüssel jederzeit zu „vergessen“ – in der Dokumentation steht nicht, was in diesem Fall zu tun ist ... nun, warum könnte das so sein? Du hast nicht mehrere Schlüssel, wie bei einem Satz zukünftiger Salze, und ?..

Es gibt noch ein paar andere erwähnenswerte Dinge zum MTProto-Thema.

Nachrichtenmeldungen, msg_id, msg_seqno, Bestätigungen, Pings in die falsche Richtung und andere Eigenheiten

Warum müssen Sie über sie Bescheid wissen? Weil sie auf eine höhere Ebene „durchsickern“ und Sie sich ihrer bewusst sein müssen, wenn Sie mit der API arbeiten. Nehmen wir an, dass uns msg_key nicht interessiert; die untere Ebene hat alles für uns entschlüsselt. Aber in den entschlüsselten Daten haben wir die folgenden Felder (auch die Länge der Daten, damit wir wissen, wo der Auffüller ist, aber das ist nicht wichtig):

  • Salz - int64
  • session_id – int64
  • message_id – int64
  • seq_no – int32

Wir möchten Sie daran erinnern, dass es für das gesamte DC nur ein Salz gibt. Warum von ihr wissen? Nicht nur, weil es eine Anfrage gibt get_future_salts, was Ihnen sagt, welche Intervalle gültig sind, aber auch, weil, wenn Ihr Salz „verfault“ ist, die Nachricht (Anfrage) einfach verloren geht. Der Server wird das neue Salt natürlich durch Ausgabe melden new_session_created - aber mit dem alten muss man es zum Beispiel irgendwie noch einmal verschicken. Und dieses Problem betrifft die Anwendungsarchitektur.

Aus vielen Gründen ist es dem Server gestattet, Sitzungen ganz abzubrechen und auf diese Weise zu reagieren. Was ist eigentlich eine MTProto-Sitzung auf Client-Seite? Das sind zwei Zahlen session_id и seq_no Nachrichten innerhalb dieser Sitzung. Nun, und natürlich die zugrunde liegende TCP-Verbindung. Nehmen wir an, unser Kunde weiß immer noch nicht, wie er viele Dinge tun soll, er hat die Verbindung getrennt und die Verbindung wieder hergestellt. Wenn dies schnell passiert ist, wird die alte Sitzung in der neuen TCP-Verbindung fortgesetzt seq_no weiter. Wenn es lange dauert, könnte der Server es löschen, denn auf seiner Seite ist es auch eine Warteschlange, wie wir herausgefunden haben.

Was soll es sein seq_no? Oh, das ist eine knifflige Frage. Versuchen Sie ehrlich zu verstehen, was gemeint war:

Inhaltsbezogene Nachricht

Eine Nachricht, die eine explizite Bestätigung erfordert. Dazu gehören alle Benutzer- und viele Servicenachrichten, praktisch alle mit Ausnahme von Containern und Bestätigungen.

Nachrichtensequenznummer (msg_seqno)

Eine 32-Bit-Zahl, die der doppelten Anzahl „inhaltsbezogener“ Nachrichten (solche, die eine Bestätigung erfordern und insbesondere solche, die keine Container sind) entspricht, die vom Absender vor dieser Nachricht erstellt und anschließend um eins erhöht werden, wenn es sich bei der aktuellen Nachricht um eine handelt inhaltsbezogene Botschaft. Ein Container wird immer nach seinem gesamten Inhalt generiert; Daher ist seine Sequenznummer größer oder gleich den Sequenznummern der darin enthaltenen Nachrichten.

Was ist das für ein Zirkus mit einer Erhöhung um 1 und dann noch einer um 2? Ich vermute, dass sie ursprünglich „das niedrigstwertige Bit für ACK, der Rest ist eine Zahl“ meinten, aber das Ergebnis ist nicht ganz dasselbe – Insbesondere kommt es heraus, kann verschickt werden mehrere Bestätigungen mit dem gleichen seq_no! Wie? Nun, zum Beispiel sendet uns der Server etwas, sendet es, und wir selbst schweigen und antworten nur mit Servicenachrichten, die den Empfang seiner Nachrichten bestätigen. In diesem Fall haben unsere ausgehenden Bestätigungen die gleiche ausgehende Nummer. Wenn Sie mit TCP vertraut sind und denken, dass das irgendwie wild klingt, aber es scheint nicht sehr wild zu sein, denn in TCP seq_no ändert sich nicht, aber die Bestätigung geht zu seq_no Auf der anderen Seite werde ich mich beeilen, Sie zu verärgern. Bestätigungen werden in MTProto bereitgestellt NICHT auf seq_no, wie in TCP, aber von msg_id !

Was ist das msg_id, das wichtigste dieser Felder? Eine eindeutige Nachrichtenkennung, wie der Name schon sagt. Sie ist als 64-Bit-Zahl definiert, deren niedrigste Bits wiederum die „Server-nicht-Server“-Magie haben, und der Rest ist ein Unix-Zeitstempel, einschließlich des Bruchteils, der um 32 Bit nach links verschoben ist. Diese. Zeitstempel per se (und Nachrichten mit zu großen Zeitunterschieden werden vom Server abgelehnt). Daraus ergibt sich, dass es sich im Allgemeinen um einen Identifikator handelt, der für den Client global ist. Angesichts dessen – erinnern wir uns session_id - Wir garantieren: Unter keinen Umständen kann eine Nachricht, die für eine Sitzung bestimmt ist, an eine andere Sitzung gesendet werden. Das heißt, es stellt sich heraus, dass dies bereits der Fall ist drei Ebene – Sitzung, Sitzungsnummer, Nachrichten-ID. Warum diese Überkomplizierung, dieses Rätsel ist sehr groß.

somit msg_id gebraucht für...

RPC: Anfragen, Antworten, Fehler. Bestätigungen.

Wie Sie vielleicht bemerkt haben, gibt es im Diagramm nirgendwo einen speziellen Typ oder eine spezielle Funktion zum Erstellen einer RPC-Anfrage, obwohl es Antworten gibt. Schließlich haben wir inhaltsbezogene Botschaften! Also, keine Die Nachricht könnte eine Anfrage sein! Oder nicht sein. Schließlich, jeder es msg_id. Aber es gibt Antworten:

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

Hier wird angezeigt, auf welche Nachricht geantwortet wird. Daher müssen Sie sich auf der obersten Ebene der API die Nummer Ihrer Anfrage merken. Ich denke, es besteht keine Notwendigkeit zu erklären, dass die Arbeit asynchron ist und mehrere Anfragen gleichzeitig ausgeführt werden können. Welche Antworten können in beliebiger Reihenfolge zurückgegeben werden? Im Prinzip lässt sich hieraus und bei Fehlermeldungen wie kein Worker die Architektur dahinter nachvollziehen: Der Server, der eine TCP-Verbindung mit Ihnen aufrechterhält, ist ein Front-End-Balancer, er leitet Anfragen an die Backends weiter und sammelt sie über wieder ein message_id. Es scheint, dass hier alles klar, logisch und gut ist.

Ja?.. Und wenn Sie darüber nachdenken? Schließlich hat auch die RPC-Antwort selbst ein Feld msg_id! Müssen wir den Server anschreien: „Sie antworten nicht auf meine Antwort!“? Und ja, was war mit Bestätigungen? Über die Seite Nachrichten über Nachrichten sagt uns, was ist

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

und es muss von jeder Seite getan werden. Aber nicht immer! Wenn Sie ein RpcResult erhalten haben, dient dieses selbst als Bestätigung. Das heißt, der Server kann auf Ihre Anfrage mit MsgsAck antworten – etwa „Ich habe sie erhalten.“ RpcResult kann sofort reagieren. Es könnte beides sein.

Und ja, Sie müssen die Antwort noch beantworten! Bestätigung. Andernfalls betrachtet der Server die Nachricht als unzustellbar und sendet sie erneut an Sie zurück. Auch nach erneuter Verbindung. Aber hier stellt sich natürlich das Problem der Zeitüberschreitungen. Schauen wir sie uns etwas später an.

Schauen wir uns in der Zwischenzeit mögliche Fehler bei der Abfrageausführung an.

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

Oh, jemand wird ausrufen, hier ist ein humaneres Format – es gibt eine Zeile! Lass dir Zeit. Hier Liste der Fehler, aber natürlich nicht vollständig. Daraus erfahren wir, dass der Code ist etwas wie HTTP-Fehler (natürlich wird die Semantik der Antworten nicht beachtet, an manchen Stellen sind sie zufällig auf die Codes verteilt), und die Zeile sieht so aus CAPITAL_LETTERS_AND_NUMBERS. Zum Beispiel PHONE_NUMBER_OCCUPIED oder FILE_PART_Х_MISSING. Nun, das heißt, Sie werden diese Zeile noch brauchen analysieren. Zum Beispiel FLOOD_WAIT_3600 bedeutet, dass Sie eine Stunde warten müssen, und PHONE_MIGRATE_5, dass eine Telefonnummer mit diesem Präfix im 5. DC registriert werden muss. Wir haben eine Typsprache, oder? Wir brauchen kein Argument aus einer Zeichenfolge, normale Argumente reichen aus, okay.

Auch dies befindet sich nicht auf der Seite mit den Servicemeldungen, aber wie bei diesem Projekt bereits üblich, sind die Informationen zu finden auf einer anderen Dokumentationsseite. Or Verdacht erregen. Schauen Sie sich zunächst die Verletzung der Eingabe/Ebene an – RpcError kann darin verschachtelt werden RpcResult. Warum nicht draußen? Was haben wir nicht berücksichtigt? Wo ist dementsprechend die Garantie dafür RpcError darf NICHT eingebettet werden RpcResult, aber direkt oder in einem anderen Typ verschachtelt sein? Und wenn das nicht möglich ist, warum befindet es sich dann nicht auf der obersten Ebene, d. h. es fehlt req_msg_id ? ..

Aber machen wir weiter mit den Servicemeldungen. Der Client denkt vielleicht, dass der Server lange nachdenkt und stellt diese wunderbare Anfrage:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Auf diese Frage gibt es drei mögliche Antworten, die sich wiederum mit dem Bestätigungsmechanismus überschneiden; der Versuch, zu verstehen, wie diese lauten sollten (und wie die allgemeine Liste der Typen aussieht, die keiner Bestätigung bedürfen), bleibt dem Leser als Hausaufgabe überlassen (Anmerkung: die Informationen in der Telegram Desktop-Quellcode ist nicht vollständig).

Drogenabhängigkeit: Nachrichtenstatus

Im Allgemeinen hinterlassen viele Stellen in TL, MTProto und Telegram im Allgemeinen ein Gefühl der Sturheit, aber aus Höflichkeit, Taktgefühl und anderen Gründen Soft Skills Wir haben darüber höflich geschwiegen und die Obszönitäten in den Dialogen zensiert. Allerdings dieser OrtОDer größte Teil der Seite handelt von Nachrichten über Nachrichten Es ist selbst für mich schockierend, der ich schon lange mit Netzwerkprotokollen arbeite und Fahrräder unterschiedlicher Schiefheit gesehen habe.

Es beginnt harmlos, mit Bestätigungen. Als nächstes erzählen sie uns davon

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;

Nun, jeder, der anfängt, mit MTProto zu arbeiten, wird sich damit auseinandersetzen müssen; im Zyklus „korrigiert – neu kompiliert – gestartet“ kommt es häufig vor, dass bei Bearbeitungen Zahlenfehler oder Salt auftreten, die fehlerhaft geworden sind. Allerdings gibt es hier zwei Punkte:

  1. Dies bedeutet, dass die ursprüngliche Nachricht verloren geht. Wir müssen einige Warteschlangen erstellen, das sehen wir uns später an.
  2. Was sind das für seltsame Fehlernummern? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... wo sind die anderen Zahlen, Tommy?

In der Dokumentation heißt es:

Die Absicht ist, dass error_code-Werte gruppiert werden (error_code >> 4): Beispielsweise entsprechen die Codes 0x40 – 0x4f Fehlern bei der Containerzerlegung.

aber erstens eine Verschiebung in die andere Richtung, und zweitens ist es egal, wo sind die anderen Codes? Im Kopf des Autors?... Das sind jedoch Kleinigkeiten.

Sucht beginnt bei Nachrichten über Nachrichtenstatus und Nachrichtenkopien:

  • Anfrage nach Informationen zum Nachrichtenstatus
    Wenn eine Partei längere Zeit keine Informationen über den Status ihrer ausgehenden Nachrichten erhalten hat, kann sie diese explizit von der anderen Partei anfordern:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informationsmeldung zum Status der Nachrichten
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Hier info ist eine Zeichenfolge, die genau ein Byte des Nachrichtenstatus für jede Nachricht aus der eingehenden msg_ids-Liste enthält:

    • 1 = Über die Nachricht ist nichts bekannt (msg_id zu niedrig, die andere Partei hat sie möglicherweise vergessen)
    • 2 = Nachricht nicht empfangen (msg_id liegt im Bereich der gespeicherten Identifikatoren; die andere Partei hat jedoch mit Sicherheit keine solche Nachricht erhalten)
    • 3 = Nachricht nicht erhalten (msg_id zu hoch; die andere Partei hat sie jedoch mit Sicherheit noch nicht erhalten)
    • 4 = Nachricht empfangen (beachten Sie, dass diese Antwort gleichzeitig auch eine Empfangsbestätigung ist)
    • +8 = Meldung bereits bestätigt
    • +16 = Meldung, die nicht quittiert werden muss
    • +32 = RPC-Abfrage in verarbeiteter Nachricht enthalten oder Verarbeitung bereits abgeschlossen
    • +64 = Inhaltliche Antwort auf bereits generierte Nachricht
    • +128 = andere Partei weiß mit Sicherheit, dass die Nachricht bereits empfangen wurde
      Für diese Antwort ist keine Bestätigung erforderlich. Es ist an und für sich eine Bestätigung der relevanten msgs_state_req.
      Beachten Sie: Wenn sich plötzlich herausstellt, dass die andere Partei keine Nachricht hat, die aussieht, als wäre sie an sie gesendet worden, kann die Nachricht einfach erneut gesendet werden. Selbst wenn die Gegenpartei zwei Kopien der Nachricht gleichzeitig erhalten sollte, wird das Duplikat ignoriert. (Wenn zu viel Zeit vergangen ist und die ursprüngliche msg_id nicht mehr gültig ist, muss die Nachricht in msg_copy eingeschlossen werden.)
  • Freiwillige Mitteilung des Status von Nachrichten
    Jede Partei kann die andere Partei freiwillig über den Status der von der anderen Partei übermittelten Nachrichten informieren.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Erweiterte freiwillige Mitteilung des Status einer Nachricht
    ...
    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;
  • Explizite Aufforderung zum erneuten Senden von Nachrichten
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Der entfernte Teilnehmer antwortet sofort, indem er die angeforderten Nachrichten erneut sendet […]
  • Explizite Aufforderung, Antworten erneut zu senden
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Der entfernte Teilnehmer antwortet sofort mit einem erneuten Senden Antworten zu den gewünschten Nachrichten […]
  • Nachrichtenkopien
    In manchen Situationen muss eine alte Nachricht mit einer nicht mehr gültigen msg_id erneut gesendet werden. Dann wird es in einen Kopiercontainer verpackt:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Nach dem Empfang wird die Nachricht so verarbeitet, als ob der Wrapper nicht vorhanden wäre. Wenn jedoch mit Sicherheit bekannt ist, dass die Nachricht orig_message.msg_id empfangen wurde, wird die neue Nachricht nicht verarbeitet (während sie und orig_message.msg_id gleichzeitig bestätigt werden). Der Wert von orig_message.msg_id muss niedriger sein als die msg_id des Containers.

Lassen Sie uns sogar darüber schweigen, was msgs_state_info Wieder ragen die Ohren des unvollendeten TL heraus (wir brauchten einen Byte-Vektor, und in den unteren beiden Bits befand sich eine Aufzählung und in den oberen beiden Bits befanden sich Flags). Der Punkt ist ein anderer. Versteht jemand, warum das alles in der Praxis so ist? bei einem echten Kunden notwendig?.. Mit Schwierigkeiten, aber man kann sich einen Vorteil vorstellen, wenn eine Person mit dem Debuggen beschäftigt ist, und zwar in einem interaktiven Modus - fragen Sie den Server, was und wie. Aber hier werden die Wünsche beschrieben in beide Richtungen.

Daraus folgt, dass jede Partei nicht nur Nachrichten verschlüsseln und versenden muss, sondern auch Daten über sich selbst und die Antworten darauf für einen unbekannten Zeitraum speichern muss. Die Dokumentation beschreibt weder den Zeitpunkt noch die praktische Anwendbarkeit dieser Funktionen. in keiner Weise. Das Erstaunlichste ist, dass sie tatsächlich im Code offizieller Kunden verwendet werden! Anscheinend wurde ihnen etwas gesagt, was nicht in der öffentlichen Dokumentation enthalten war. Aus dem Code verstehen warum, ist nicht mehr so ​​einfach wie im Fall von TL – es ist kein (relativ) logisch isolierter Teil, sondern ein an die Anwendungsarchitektur gebundenes Stück, d. h. wird deutlich mehr Zeit benötigen, um den Anwendungscode zu verstehen.

Pings und Timings. Warteschlangen.

Wenn wir uns aus allem an die Vermutungen über die Serverarchitektur (Verteilung von Anfragen auf Backends) erinnern, ergibt sich etwas Trauriges – trotz aller Zustellungsgarantien in TCP (entweder werden die Daten zugestellt, oder Sie werden über die Lücke informiert, aber die Daten werden zugestellt, bevor das Problem auftritt), dass Bestätigungen in MTProto selbst - Keine Garantien. Der Server kann Ihre Nachricht leicht verlieren oder wegwerfen, und Sie können nichts dagegen tun. Verwenden Sie einfach verschiedene Arten von Krücken.

Und vor allem: Nachrichtenwarteschlangen. Nun, eines war von Anfang an klar: Eine unbestätigte Nachricht muss gespeichert und erneut gesendet werden. Und nach welcher Zeit? Und der Narr kennt ihn. Vielleicht lösen diese süchtig machenden Dienstnachrichten dieses Problem irgendwie mit Krücken, sagen wir, in Telegram Desktop gibt es etwa 4 entsprechende Warteschlangen (vielleicht mehr, wie bereits erwähnt, dafür muss man sich gleichzeitig ernsthafter mit dem Code und der Architektur befassen). Zeit, wir Wir wissen, dass es nicht als Beispiel genommen werden kann; eine bestimmte Anzahl von Typen aus dem MTProto-Schema werden darin nicht verwendet.

Warum passiert das? Wahrscheinlich konnten die Serverprogrammierer die Zuverlässigkeit innerhalb des Clusters oder gar die Pufferung auf dem Front-Balancer nicht gewährleisten und haben dieses Problem auf den Client übertragen. Aus Verzweiflung versuchte Vasily, eine alternative Option mit nur zwei Warteschlangen zu implementieren, indem er Algorithmen von TCP nutzte – die RTT zum Server messen und die Größe des „Fensters“ (in Nachrichten) abhängig von der Anzahl unbestätigter Anfragen anpassen. Das heißt, eine so grobe Heuristik zur Beurteilung der Auslastung des Servers ist, wie viele unserer Anfragen er gleichzeitig verarbeiten kann und nicht verliert.

Nun ja, das heißt, Sie verstehen, oder? Wenn Sie TCP zusätzlich zu einem Protokoll, das über TCP läuft, erneut implementieren müssen, deutet dies auf ein sehr schlecht konzipiertes Protokoll hin.

Oh ja, warum braucht man mehr als eine Warteschlange und was bedeutet das überhaupt für eine Person, die mit einer High-Level-API arbeitet? Schauen Sie, Sie stellen eine Anfrage, serialisieren sie, aber oft können Sie sie nicht sofort senden. Warum? Denn die Antwort wird sein msg_id, was vorübergehend istаIch bin ein Etikett, dessen Zuweisung am besten auf einen möglichst späten Zeitpunkt verschoben wird – für den Fall, dass der Server sie aufgrund einer zeitlichen Diskrepanz zwischen uns und ihm ablehnt (natürlich können wir eine Krücke herstellen, die unsere Zeit von der Gegenwart verschiebt). an den Server, indem ein aus den Antworten des Servers berechnetes Delta hinzugefügt wird (offizielle Clients tun dies, aber es ist grob und aufgrund der Pufferung ungenau). Wenn Sie daher eine Anfrage mit einem lokalen Funktionsaufruf aus der Bibliothek stellen, durchläuft die Nachricht die folgenden Phasen:

  1. Es liegt in einer Warteschlange und wartet auf die Verschlüsselung.
  2. Ernennung msg_id und die Nachricht wurde in eine andere Warteschlange verschoben - mögliche Weiterleitung; an den Socket senden.
  3. a) Der Server hat mit MsgsAck geantwortet – die Nachricht wurde zugestellt, wir löschen sie aus der „anderen Warteschlange“.
    b) Oder umgekehrt, ihm gefiel etwas nicht, er antwortete badmsg – erneut senden aus „einer anderen Warteschlange“
    c) Es ist nichts bekannt, die Nachricht muss aus einer anderen Warteschlange erneut gesendet werden – es ist jedoch nicht genau bekannt, wann.
  4. Der Server antwortete schließlich RpcResult - die eigentliche Antwort (oder der eigentliche Fehler) – nicht nur übermittelt, sondern auch verarbeitet.

Vielleicht, könnte der Einsatz von Containern das Problem teilweise lösen. Dies ist der Fall, wenn eine Reihe von Nachrichten in einer zusammengefasst werden und der Server mit einer Bestätigung auf alle gleichzeitig in einer Antwort antwortet msg_id. Er wird aber auch dieses Paket, wenn etwas schiefgehen sollte, in Gänze ablehnen.

Und an diesem Punkt kommen nichttechnische Überlegungen ins Spiel. Aus Erfahrung haben wir viele Krücken gesehen, und darüber hinaus werden wir jetzt noch mehr Beispiele für schlechte Beratung und Architektur sehen – lohnt es sich unter solchen Bedingungen, solchen Entscheidungen zu vertrauen und sie zu treffen? Die Frage ist rhetorisch (natürlich nicht).

Worüber reden wir? Wenn man zum Thema „Drogenbotschaften über Botschaften“ immer noch mit Einwänden spekulieren kann wie „Du bist dumm, du hast unseren genialen Plan nicht verstanden!“ (also schreiben Sie zuerst die Dokumentation, wie normale Leute es tun sollten, mit Begründung und Beispielen für den Paketaustausch, dann reden wir), dann sind Timings/Timeouts eine rein praktische und spezifische Frage, hier ist alles schon lange bekannt. Was sagt uns die Dokumentation über Timeouts?

Ein Server bestätigt normalerweise den Empfang einer Nachricht von einem Client (normalerweise eine RPC-Anfrage) mithilfe einer RPC-Antwort. Wenn eine Antwort lange auf sich warten lässt, sendet ein Server möglicherweise zunächst eine Empfangsbestätigung und etwas später die RPC-Antwort selbst.

Normalerweise bestätigt ein Client den Empfang einer Nachricht von einem Server (normalerweise eine RPC-Antwort), indem er der nächsten RPC-Anfrage eine Bestätigung hinzufügt, sofern diese nicht zu spät übertragen wird (wenn sie beispielsweise 60–120 Sekunden nach dem Empfang generiert wird). einer Nachricht vom Server). Wenn es jedoch über einen längeren Zeitraum keinen Grund gibt, Nachrichten an den Server zu senden, oder wenn eine große Anzahl unbestätigter Nachrichten vom Server vorliegt (z. B. über 16), übermittelt der Client eine eigenständige Bestätigung.

... Ich übersetze: Wir selbst wissen nicht, wie viel und wie wir es brauchen, also gehen wir davon aus, dass es so sein soll.

Und zu den Pings:

Ping-Nachrichten (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Eine Antwort wird normalerweise an dieselbe Verbindung zurückgegeben:

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

Für diese Nachrichten ist keine Bestätigung erforderlich. Ein Pong wird nur als Reaktion auf einen Ping gesendet, während ein Ping von beiden Seiten initiiert werden kann.

Verzögerter Verbindungsabbau + PING

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

Funktioniert wie Ping. Darüber hinaus startet der Server nach dem Empfang einen Timer, der die aktuelle Verbindung „disconnect_delay“ Sekunden später schließt, es sei denn, er erhält eine neue Nachricht desselben Typs, die alle vorherigen Timer automatisch zurücksetzt. Wenn der Client diese Pings beispielsweise alle 60 Sekunden sendet, kann er „disconnect_delay“ auf 75 Sekunden setzen.

Bist du verrückt?! In 60 Sekunden wird der Zug in den Bahnhof einfahren, Passagiere absetzen und wieder aufnehmen und im Tunnel erneut den Kontakt verlieren. In 120 Sekunden, während Sie es hören, kommt es zu einem anderen und die Verbindung wird höchstwahrscheinlich unterbrochen. Nun, es ist klar, woher die Beine kommen – „Ich habe ein Klingeln gehört, weiß aber nicht, wo es ist“, es gibt den Nagl-Algorithmus und die Option TCP_NODELAY, die für interaktives Arbeiten gedacht ist. Aber entschuldigen Sie, behalten Sie den Standardwert bei – 200 MilliSekunden Wenn Sie wirklich etwas Ähnliches darstellen und möglicherweise ein paar Pakete einsparen möchten, verschieben Sie es um 5 Sekunden oder wie auch immer das Zeitlimit für die Meldung „Benutzer tippt ...“ derzeit lautet. Aber nicht mehr.

Und schließlich Pings. Das heißt, es wird überprüft, ob die TCP-Verbindung aktiv ist. Es ist lustig, aber vor etwa 10 Jahren habe ich einen kritischen Text über den Messenger unseres Fakultätswohnheims geschrieben – die Autoren dort haben auch den Server vom Client aus gepingt und nicht umgekehrt. Aber Studierende im dritten Jahr sind eine Sache und ein internationales Büro eine andere, oder?

Zunächst ein kleines Bildungsprogramm. Eine TCP-Verbindung kann ohne Paketaustausch wochenlang bestehen. Das ist je nach Zweck sowohl gut als auch schlecht. Es ist gut, wenn Sie eine offene SSH-Verbindung zum Server hatten, vom Computer aufstanden, den Router neu starteten und zu Ihrem Platz zurückkehrten – die Sitzung über diesen Server wurde nicht unterbrochen (Sie haben nichts eingegeben, es gab keine Pakete). , es ist praktisch. Es ist schlimm, wenn es Tausende von Clients auf dem Server gibt, von denen jeder Ressourcen beansprucht (Hallo, Postgres!), und der Host des Clients möglicherweise schon vor langer Zeit neu gestartet wurde – aber wir werden nichts davon erfahren.

Chat-/IM-Systeme fallen aus einem weiteren Grund in den zweiten Fall: Online-Status. Wenn der Benutzer „abgefallen“ ist, müssen Sie seine Gesprächspartner darüber informieren. Andernfalls werden Sie mit einem Fehler enden, den die Macher von Jabber gemacht (und 20 Jahre lang korrigiert) haben: Der Benutzer hat die Verbindung getrennt, aber sie schreiben ihm weiterhin Nachrichten, weil sie glauben, dass er online ist (was in diesen auch völlig verloren ging). einige Minuten bevor die Unterbrechung entdeckt wurde). Nein, die TCP_KEEPALIVE-Option, die viele Leute, die nicht verstehen, wie TCP-Timer funktionieren, zufällig einwerfen (indem sie wilde Werte wie zehn Sekunden festlegen), wird hier nicht helfen – Sie müssen sicherstellen, dass nicht nur der Betriebssystemkernel Der Computer des Benutzers ist aktiv, funktioniert aber auch normal, kann reagieren und die Anwendung selbst (glauben Sie, dass sie nicht einfrieren kann? Telegram Desktop unter Ubuntu 18.04 ist bei mir mehr als einmal eingefroren).

Deshalb müssen Sie pingen Server Client und nicht umgekehrt - wenn der Client dies tut und die Verbindung unterbrochen wird, wird der Ping nicht zugestellt und das Ziel wird nicht erreicht.

Was sehen wir auf Telegram? Es ist genau das Gegenteil! Nun ja, das ist. Formal können sich natürlich beide Seiten gegenseitig anpingen. In der Praxis verwenden Klienten eine Krücke ping_delay_disconnect, wodurch der Timer auf dem Server eingestellt wird. Nun, entschuldigen Sie, es liegt nicht am Kunden, zu entscheiden, wie lange er dort ohne Ping leben möchte. Der Server weiß es aufgrund seiner Auslastung besser. Aber natürlich, wenn dir die Ressourcen nichts ausmachen, dann wirst du dein eigener böser Pinocchio sein, und eine Krücke reicht aus ...

Wie hätte es gestaltet sein sollen?

Ich glaube, dass die oben genannten Tatsachen deutlich darauf hinweisen, dass das Team von Telegram/VKontakte nicht sehr kompetent im Bereich des Transports (und der unteren Ebene) von Computernetzwerken ist und in relevanten Angelegenheiten nur über geringe Qualifikationen verfügt.

Warum stellte sich heraus, dass es so kompliziert war, und wie können Telegram-Architekten versuchen, Einwände zu erheben? Die Tatsache, dass sie versucht haben, eine Sitzung zu erstellen, die TCP-Verbindungsunterbrechungen übersteht, d. h. was jetzt nicht geliefert wurde, werden wir später nachliefern. Wahrscheinlich haben sie auch versucht, einen UDP-Transport durchzuführen, aber sie stießen auf Schwierigkeiten und gaben ihn auf (deshalb ist die Dokumentation leer – es gab nichts, worüber man sich rühmen könnte). Aber aufgrund eines mangelnden Verständnisses darüber, wie Netzwerke im Allgemeinen und TCP im Besonderen funktionieren, wo man sich darauf verlassen kann und wo man es selbst tun muss (und wie) und dem Versuch, dies mit Kryptographie zu kombinieren, „zwei Fliegen mit.“ „One Stone“, das ist das Ergebnis.

Wie war es notwendig? Basierend auf der Tatsache, dass msg_id aus kryptografischer Sicht ein Zeitstempel ist, der zur Verhinderung von Replay-Angriffen notwendig ist, ist es ein Fehler, ihm eine eindeutige Identifikatorfunktion hinzuzufügen. Daher müsste man, ohne die aktuelle Architektur grundlegend zu ändern (wenn der Updates-Stream generiert wird, das ist ein übergeordnetes API-Thema für einen anderen Teil dieser Beitragsreihe), Folgendes tun:

  1. Der Server, der die TCP-Verbindung zum Client hält, übernimmt die Verantwortung – wenn er vom Socket gelesen hat, bestätigen Sie ihn bitte, verarbeiten Sie ihn oder geben Sie einen Fehler zurück, kein Verlust. Dann ist die Bestätigung kein Vektor von IDs, sondern einfach „die zuletzt empfangene seq_no“ – nur eine Zahl, wie in TCP (zwei Zahlen – Ihre seq und die bestätigte). Wir sind immer in der Sitzung, nicht wahr?
  2. Der Zeitstempel zur Verhinderung von Replay-Angriffen wird a la nonce zu einem separaten Feld. Es wird überprüft, hat aber keinerlei Auswirkungen auf irgendetwas anderes. Genug und uint32 - Wenn sich unser Salz mindestens jeden halben Tag ändert, können wir 16 Bits den niederwertigen Bits eines ganzzahligen Teils der aktuellen Zeit zuordnen, den Rest - einem Bruchteil einer Sekunde (wie jetzt).
  3. ENTFERNT msg_id überhaupt - im Hinblick auf die Unterscheidung von Anfragen auf den Backends gibt es erstens die Client-ID und zweitens die Sitzungs-ID, um sie zu verketten. Als Anforderungskennung reicht dementsprechend nur eines aus seq_no.

Dies ist auch nicht die erfolgreichste Option; als Identifikator könnte ein vollständiger Zufall dienen – dies geschieht übrigens bereits in der High-Level-API beim Senden einer Nachricht. Es wäre besser, die Architektur komplett von relativ auf absolut umzustellen, aber das ist ein Thema für einen anderen Teil, nicht für diesen Beitrag.

API?

Ta-daam! Nachdem wir uns also durch einen Weg voller Schmerzen und Krücken gekämpft hatten, waren wir endlich in der Lage, alle Anfragen an den Server zu senden und Antworten darauf zu erhalten sowie Updates vom Server zu erhalten (nicht als Antwort auf eine Anfrage, sondern auf diese selbst). sendet uns, wie PUSH, falls jemand es so klarer macht).

Achtung, jetzt wird es im Artikel nur noch ein Beispiel in Perl geben! (Für diejenigen, die mit der Syntax nicht vertraut sind: Das erste Argument von bless ist die Datenstruktur des Objekts, das zweite seine Klasse):

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

Ja, das ist kein absichtlicher Spoiler – wenn Sie es noch nicht gelesen haben, machen Sie es einfach!

Oh, wai~~... wie sieht das aus? Etwas sehr Vertrautes ... vielleicht ist dies die Datenstruktur einer typischen Web-API in JSON, außer dass Klassen auch an Objekte angehängt sind? ...

So kommt es also... Worum geht es, Genossen?... So viel Aufwand - und wir haben bei den Webprogrammierern Rast gemacht Gerade beginnend?..Wäre JSON über HTTPS nicht einfacher?! Was haben wir dafür bekommen? Hat sich der Aufwand gelohnt?

Lassen Sie uns bewerten, was uns TL+MTProto gegeben hat und welche Alternativen möglich sind. Na ja, HTTP, das sich auf das Request-Response-Modell konzentriert, passt nicht, aber zumindest etwas zusätzlich zu TLS?

Kompakte Serialisierung. Wenn ich diese Datenstruktur sehe, die JSON ähnelt, fällt mir ein, dass es Binärversionen davon gibt. Markieren wir MsgPack als nicht ausreichend erweiterbar, aber es gibt zum Beispiel CBOR – übrigens einen Standard, der in beschrieben ist RFC 7049. Es zeichnet sich dadurch aus, dass es definiert Tags, als Expansionsmechanismus und unter anderem bereits standardisiert es gibt:

  • 25 + 256 – Ersetzen wiederholter Zeilen durch einen Verweis auf die Zeilennummer, eine so kostengünstige Komprimierungsmethode
  • 26 – serialisiertes Perl-Objekt mit Klassennamen und Konstruktorargumenten
  • 27 – serialisiertes sprachunabhängiges Objekt mit Typnamen und Konstruktorargumenten

Nun, ich habe versucht, dieselben Daten in TL und in CBOR zu serialisieren, wobei das Packen von Zeichenfolgen und Objekten aktiviert war. Das Ergebnis begann irgendwo im Megabyte-Bereich zugunsten von CBOR zu variieren:

cborlen=1039673 tl_len=1095092

somit Abschluss: Es gibt wesentlich einfachere Formate, bei denen das Problem eines Synchronisationsfehlers oder einer unbekannten Kennung nicht auftritt und die eine vergleichbare Effizienz aufweisen.

Schneller Verbindungsaufbau. Dies bedeutet null RTT nach der erneuten Verbindung (wenn der Schlüssel bereits einmal generiert wurde) – anwendbar ab der allerersten MTProto-Nachricht, aber mit einigen Vorbehalten – treffen Sie das gleiche Salt, die Sitzung ist nicht faul usw. Was bietet uns TLS stattdessen? Zitat zum Thema:

Bei Verwendung von PFS in TLS werden TLS-Sitzungstickets (RFC 5077), um eine verschlüsselte Sitzung fortzusetzen, ohne Schlüssel neu auszuhandeln und ohne Schlüsselinformationen auf dem Server zu speichern. Beim Öffnen der ersten Verbindung und beim Erstellen von Schlüsseln verschlüsselt der Server den Verbindungsstatus und übermittelt ihn an den Client (in Form eines Sitzungstickets). Dementsprechend sendet der Client bei Wiederaufnahme der Verbindung ein Sitzungsticket einschließlich des Sitzungsschlüssels zurück an den Server. Das Ticket selbst wird mit einem temporären Schlüssel (Sitzungsticketschlüssel) verschlüsselt, der auf dem Server gespeichert wird und auf alle Frontend-Server verteilt werden muss, die SSL in Clusterlösungen verarbeiten.[10] Daher kann die Einführung eines Sitzungstickets gegen PFS verstoßen, wenn temporäre Serverschlüssel kompromittiert werden, beispielsweise wenn sie über einen längeren Zeitraum gespeichert werden (OpenSSL, Nginx, Apache speichern sie standardmäßig für die gesamte Dauer des Programms; beliebte Websites verwenden der Schlüssel für mehrere Stunden, bis zu Tage).

Hier ist die RTT nicht Null, Sie müssen mindestens ClientHello und ServerHello austauschen, woraufhin der Client Daten zusammen mit Finished senden kann. Aber hier sollten wir bedenken, dass wir nicht das Web mit seinen vielen neu geöffneten Verbindungen haben, sondern einen Messenger, dessen Verbindung oft aus einer einzigen und mehr oder weniger langlebigen, relativ kurzen Anfragen an Webseiten besteht – alles ist gemultiplext im Inneren. Das heißt, es ist durchaus akzeptabel, wenn wir nicht auf einen wirklich schlechten U-Bahn-Abschnitt stoßen.

Noch etwas vergessen? Schreiben Sie in die Kommentare.

Fortsetzung folgt!

Im zweiten Teil dieser Beitragsreihe werden wir uns nicht mit technischen, sondern mit organisatorischen Fragen befassen – Ansätze, Ideologie, Schnittstelle, Einstellung gegenüber Benutzern usw. Basierend jedoch auf den technischen Informationen, die hier präsentiert wurden.

Im dritten Teil wird weiterhin die technische Komponente/Entwicklungserfahrung analysiert. Sie lernen insbesondere:

  • Fortsetzung des Tumults mit der Vielfalt der TL-Typen
  • Unbekanntes über Kanäle und Supergruppen
  • Warum Dialoge schlechter sind als Dienstpläne
  • über absolute vs. relative Nachrichtenadressierung
  • Was ist der Unterschied zwischen Foto und Bild?
  • Wie Emojis kursiven Text beeinträchtigen

und andere Krücken! Bleiben Sie dran!

Source: habr.com

Kommentar hinzufügen