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