Telegram on Go: partie 1, analyse du schéma

Le désir d'écrire un client de haute qualité pour mon messager préféré est mûr depuis longtemps, mais il y a seulement un mois, j'ai décidé que le moment était venu et que j'avais suffisamment de qualifications pour cela.





Le développement est toujours en cours (et complètement open source), mais le chemin passionnant est déjà passé d'une incompréhension totale du protocole à un client relativement stable. Dans une série d'articles, je vais vous expliquer les défis auxquels j'ai été confronté et comment je les ai affrontés. Les techniques que j'ai appliquées peuvent être utiles lors du développement d'un client pour tout protocole binaire avec un schéma.






Langue de type

Commençons par Type Language ou TL, un schéma de description de protocole. Je ne vais pas approfondir la description du format, le Habré a déjà son analyse, je ne vous en parlerai que brièvement. Il est quelque peu similaire à gRPC et décrit le schéma d'interaction entre le client et le serveur: une structure de données et un ensemble de méthodes.





Voici un exemple de description de type:





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



Voici 1fbadfee



l'id du type, error



son nom, son code et son message sont des champs, et Error



c'est le nom de la classe.





Les méthodes sont décrites de la même manière, seulement à la place d'un nom de type, il y aura un nom de méthode, et au lieu d'une classe - un type de résultat:





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



Cela signifie que la méthode sendPM



prend des arguments text



et habrauser



, et retourne Error



, des variantes (constructeurs) qui ont été précédemment décrites, par exemple 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:









  • ( )









  • Test de communication réseau (unité, e2e)





  • Tester le travail avec des effets secondaires (temps, délais, PRNG)





  • CI, ou configurer le pipeline pour que le bouton Fusionner ne soit pas effrayant d'appuyer





Et je veux aussi en dire plus grâce aux participants au projet qui ont rejoint le projet, sans eux ce serait beaucoup plus difficile.








All Articles