O desejo de escrever um cliente de alta qualidade para meu mensageiro favorito on go amadureceu há muito tempo, mas apenas um mês atrás eu decidi que a hora havia chegado e eu tinha qualificações suficientes para isso.
O desenvolvimento ainda está em andamento (e totalmente de código aberto), mas o caminho fascinante já passou de uma completa falta de compreensão do protocolo para um cliente relativamente estável. Em uma série de artigos, explicarei quais desafios enfrentei e como lidei com eles. As técnicas que apliquei podem ser úteis ao desenvolver um cliente para qualquer protocolo binário com um esquema.
Digite o idioma
Vamos começar com Type Language ou TL, um esquema de descrição de protocolo. Não vou me aprofundar na descrição do formato, o Habré já tem sua análise, vou falar apenas brevemente sobre ele. É um pouco semelhante ao gRPC e descreve o esquema de interação entre o cliente e o servidor: uma estrutura de dados e um conjunto de métodos.
Aqui está um exemplo de uma descrição de tipo:
error#1fbadfee code:int32 message:string = Error;
Aqui 1fbadfee
este é o id do tipo, error
seu nome, código e mensagem são campos, e Error
este é o nome da classe.
Os métodos são descritos da mesma maneira, apenas em vez de um nome de tipo, haverá um nome de método e, em vez de uma classe - um tipo de resultado:
sendPM#3faceff text:string habrauser:string = Error;
Isso significa que o método sendPM
recebe argumentos text
e habrauser
, e retorna Error
, variantes (construtores) que foram descritos anteriormente, por exemplo error#1fbadfee
.
Para começar a trabalhar com um protocolo, você precisa aprender de alguma forma como analisar seu esquema. Existem duas maneiras: usar um analisador genérico ou escrever ad-hoc , ou seja, um analisador especializado para um protocolo específico. Para o primeiro caminho, existe o particípio , que, à primeira vista, é um bom analisador go generalizado, por meio do qual se poderia descrever a gramática. Decidi escolher o caminho ad-hoc e essa escolha valeu a pena.
Dados de teste
, , , . : , , .
, . 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:
( )
-
Teste de comunicação de rede (unidade, e2e)
Trabalho de teste com efeitos colaterais (tempo, tempos limite, PRNG)
CI, ou configurar o pipeline de modo que o botão Merge não seja assustador de pressionar
E também quero agradecer mais aos participantes do projeto que aderiram ao projeto, sem eles seria muito mais difícil.