Telegram saat bepergian: bagian 1, menguraikan skema

Keinginan untuk menulis klien berkualitas tinggi untuk utusan favorit saya saat bepergian telah matang sejak lama, tetapi hanya sebulan yang lalu saya memutuskan bahwa waktunya telah tiba dan saya memiliki kualifikasi yang cukup untuk ini.





Pengembangan masih dalam proses (dan sepenuhnya open source), tetapi jalur yang menarik telah beralih dari kurangnya pemahaman protokol ke klien yang relatif stabil. Dalam serangkaian artikel, saya akan menjelaskan tantangan apa yang saya hadapi dan bagaimana saya menghadapinya. Teknik yang telah saya terapkan dapat berguna saat mengembangkan klien untuk protokol biner apa pun dengan skema.






Ketik Bahasa

Mari kita mulai dengan Type Language atau TL, skema deskripsi protokol. Saya tidak akan mendalami deskripsi formatnya, Habré sudah memiliki analisisnya, saya hanya akan memberi tahu Anda secara singkat tentangnya. Ini agak mirip dengan gRPC dan menjelaskan skema interaksi antara klien dan server: struktur data dan sekumpulan metode.





Berikut adalah contoh deskripsi tipe:





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



Di sini 1fbadfee



ini adalah tipe id, error



nama, kode dan pesannya adalah field, dan Error



ini adalah nama kelasnya.





Metode dideskripsikan dengan cara yang serupa, hanya sebagai pengganti nama tipe akan ada nama metode, dan sebagai ganti kelas - tipe hasil:





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



Ini berarti bahwa metode tersebut sendPM



mengambil argumen text



dan habrauser



, dan mengembalikan Error



, varian (konstruktor) yang telah dijelaskan sebelumnya, misalnya error#1fbadfee



.





Untuk mulai bekerja dengan protokol, Anda perlu mempelajari cara mengurai skemanya. Ada dua cara: gunakan pengurai umum atau tulis ad-hoc , mis. parser khusus untuk protokol tertentu. Untuk jalur pertama, ada partisip , yang, sekilas, merupakan parser go umum yang bagus, di mana seseorang dapat mendeskripsikan tata bahasanya. Saya memutuskan untuk memilih jalur ad-hoc dan pilihan ini terbayar.





Uji data

, , , . : , , .





, . 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:









  • ( )









  • Pengujian komunikasi jaringan (unit, e2e)





  • Pengujian bekerja dengan efek samping (waktu, batas waktu, PRNG)





  • CI, atau atur pipeline agar tombol Merge tidak menakutkan untuk ditekan





Dan saya juga ingin mengucapkan lebih banyak terima kasih kepada peserta proyek yang bergabung dengan proyek ini, tanpa mereka akan jauh lebih sulit.








All Articles