El deseo de escribir un cliente de alta calidad para mi mensajero favorito está maduro desde hace mucho tiempo, pero hace solo un mes decidí que había llegado el momento y tenía suficientes calificaciones para esto.
El desarrollo aún está en progreso (y es completamente de código abierto), pero el fascinante camino ya ha pasado de una completa falta de comprensión del protocolo a un cliente relativamente estable. En una serie de artículos, explicaré los desafíos que enfrenté y cómo los enfrenté. Las técnicas que he aplicado pueden ser útiles a la hora de desarrollar un cliente para cualquier protocolo binario con esquema.
Tipo de idioma
Comencemos con Type Language o TL, un esquema de descripción de protocolo. No ahondaré en la descripción del formato, el Habré ya tiene su análisis, solo te lo contaré brevemente. Es algo similar a gRPC y describe el esquema de interacción entre el cliente y el servidor: una estructura de datos y un conjunto de métodos.
A continuación, se muestra un ejemplo de una descripción de tipo:
error#1fbadfee code:int32 message:string = Error;
Aquí 1fbadfee
este es el tipo de identificación, error
su nombre, código y mensaje son campos, y Error
este es el nombre de la clase.
Los métodos se describen de la misma manera, solo que en lugar de un nombre de tipo habrá un nombre de método, y en lugar de una clase, un tipo de resultado:
sendPM#3faceff text:string habrauser:string = Error;
Esto significa que el método sendPM
toma argumentos text
y habrauser
, y devuelve Error
, variantes (constructores) que se han descrito previamente, por ejemplo error#1fbadfee
.
Para comenzar a trabajar con un protocolo, debe aprender de alguna manera a analizar su esquema. Hay dos formas: usar un analizador genérico o escribir ad-hoc , es decir, un analizador especializado para un protocolo específico. Para el primer camino, está el participio , que a primera vista es un buen analizador de go generalizado, a través del cual se podría describir la gramática. Decidí elegir el camino ad-hoc y esta elección valió la pena.
Datos de prueba
, , , . : , , .
, . 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:
( )
-
Prueba de comunicación de red (unidad, e2e)
Prueba de trabajo con efectos secundarios (tiempo, tiempos de espera, PRNG)
CI, o configure la canalización para que el botón Fusionar no dé miedo de presionar
Y también quiero agradecer más a los participantes del proyecto que se unieron al proyecto, sin ellos sería mucho más difícil.