Telegramm unterwegs: Teil 1, Analyse des Schemas

Der Wunsch, einen hochwertigen Kunden für meinen Lieblingsboten unterwegs zu schreiben, war lange Zeit reif, aber erst vor einem Monat entschied ich, dass die Zeit gekommen war und ich über ausreichende Qualifikationen dafür verfügte.





Die Entwicklung ist noch im Gange (und vollständig Open Source), aber der faszinierende Weg hat sich bereits von einem völligen Unverständnis des Protokolls zu einem relativ stabilen Client entwickelt. In einer Reihe von Artikeln werde ich erklären, vor welchen Herausforderungen ich stand und wie ich damit umgegangen bin. Die Techniken, die ich angewendet habe, können nützlich sein, wenn ich einen Client für ein beliebiges Binärprotokoll mit einem Schema entwickle.






Typ Sprache

Beginnen wir mit Type Language oder TL, einem Protokollbeschreibungsschema. Ich werde nicht auf die Beschreibung des Formats eingehen, das Habré hat bereits seine Analyse, ich werde Ihnen nur kurz darüber erzählen. Es ähnelt gRPC und beschreibt das Interaktionsschema zwischen Client und Server: eine Datenstruktur und eine Reihe von Methoden.





Hier ist ein Beispiel für eine Typbeschreibung:





error#1fbadfee code:int32 message:string = Error;
      
      



Hier ist 1fbadfee



dies die Typ-ID, error



ihr Name, Code und Nachricht sind Felder, und Error



dies ist der Klassenname.





Methoden werden auf die gleiche Weise beschrieben, nur anstelle eines Typnamens gibt es einen Methodennamen und anstelle einer Klasse einen Ergebnistyp:





sendPM#3faceff text:string habrauser:string = Error;	
      
      



Dies bedeutet, dass die Methode sendPM



Argumente verwendet text



und Varianten (Konstruktoren) habrauser



zurückgibt Error



, die zuvor beschrieben wurden, z error#1fbadfee



.





, - . : ad-hoc, .. . participle, go, . ad-hoc .





, , , . : , , .





, . Definition



, :





func TestDefinition(t *testing.T) {
	for _, tt := range []struct {
		Case       string
		Input      string
		String     string
		Definition Definition
	}{
		{
			Case:  "inputPhoneCall",
			Input: "inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall",
			Definition: Definition{
				ID:   0x1e36fded,
				Name: "inputPhoneCall",
				Params: []Parameter{
					{
						Name: "id",
						Type: bareLong,
					},
					{
						Name: "access_hash",
						Type: bareLong,
					},
				},
				Type: Type{Name: "InputPhoneCall"},
			},
		},
    // ...
  } {
		t.Run(tt.Case, func(t *testing.T) {
			var d Definition
			if err := d.Parse(tt.Input); err != nil {
				t.Fatal(err)
			}
			require.Equal(t, tt.Definition, d)
		})
  } 
}  
      
      



, Flag



( , ), .





, , . :





	t.Run("Error", func(t *testing.T) {
		for _, invalid := range []string{
			"=0",
			"0 :{.0?InputFi00=0",
		} {
			t.Run(invalid, func(t *testing.T) {
				var d Definition
				if err := d.Parse(invalid); err == nil {
					t.Error("should error")
				}
			})
		}
	})
      
      



testdata

. _testdata



: , , go .





Sample.tl _testdata :





func TestParseSample(t *testing.T) {
	data, err := ioutil.ReadFile(filepath.Join("_testdata", "Sample.tl"))
	if err != nil {
		t.Fatal(err)
	}
	schema, err := Parse(bytes.NewReader(data))
	if err != nil {
		t.Fatal(err)
	}
  // ...
}
      
      



go , , filepath.Join



-.





(golden)

"golden files". , . , ( -update



). , . goldie .





func TestParser(t *testing.T) {
	for _, v := range []string{
		"td_api.tl",
		"telegram_api.tl",
		"telegram_api_header.tl",
		"layer.tl",
	} {
		t.Run(v, func(t *testing.T) {
			data, err := ioutil.ReadFile(filepath.Join("_testdata", v))
			if err != nil {
				t.Fatal(err)
			}
			schema, err := Parse(bytes.NewReader(data))
			if err != nil {
				t.Fatal(err)
			}
			t.Run("JSON", func(t *testing.T) {
				g := goldie.New(t,
					goldie.WithFixtureDir(filepath.Join("_golden", "parser", "json")),
					goldie.WithDiffEngine(goldie.ColoredDiff),
					goldie.WithNameSuffix(".json"),
				)
				g.AssertJson(t, v, schema)
			})
		})
	}
}
      
      



, json ( json). -update



, , _golden



.





(, json ) , .





Decode-Encode-Decode

, , decode-encode-decode, .





String() string



:





// Annotation represents an annotation comment, like //@name value.
type Annotation struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

func (a Annotation) String() string {
	var b strings.Builder
	b.WriteString("//")
	b.WriteRune('@')
	b.WriteString(a.Name)
	b.WriteRune(' ')
	b.WriteString(a.Value)
	return b.String()
}
      
      



, strings.Builder, String()



.





, , .





Fuzzing

() . , , (coverage-guided fuzzing). go go-fuzz . ( ) , . , syzkaller, go, Linux .





, , , , .





, Definition:





// +build fuzz

package tl

import "fmt"

func FuzzDefinition(data []byte) int {
	var d Definition
	if err := d.Parse(string(data)); err != nil {
		return 0
	}

	var other Definition
	if err := other.Parse(d.String()); err != nil {
		fmt.Printf("input: %s\n", string(data))
		fmt.Printf("parsed: %#v\n", d)
		panic(err)
	}

	return 1
}

      
      



, .





Decode-encode-decode-encode

We need to go deeper. :













  1. (2)





  2. (3)





  3. (4) (2)





(4) (2) , .. - . , .





go-fuzz

Denial of Service , .. OOM. , go-fuzz , , .





corpus, , ( crashers, , , ). crashers , 0, . , , corpus , .





, go, , , .





, , , - . (STUN, TURN, SDP, MTProto, ...) .





, - . , , ( ) Telegram go:









  • ( )









  • Testen der Netzwerkkommunikation (Einheit, e2e)





  • Testarbeiten mit Nebenwirkungen (Time, Timeouts, PRNG)





  • CI, oder richten Sie die Pipeline so ein, dass das Drücken der Schaltfläche Zusammenführen nicht unheimlich ist





Und ich möchte mich noch mehr bei den Projektteilnehmern bedanken, die sich dem Projekt angeschlossen haben. Ohne sie wäre es viel schwieriger.








All Articles