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. :
(2)
(3)
(4) (2)
(4) (2) , .. - . , .
go-fuzz
Denial of Service , .. OOM. , go-fuzz , , .
corpus, , ( crashers, , , ). crashers , 0, . , , corpus , .
, , , - . (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.