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. :
(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:
( )
-
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.