feat(ws): release generic WebSocket client and server v0.31.0-preview
This commit is contained in:
129
ws/rpc/protocol.go
Normal file
129
ws/rpc/protocol.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type envelope struct {
|
||||
Version int `json:"v"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
TimeoutMS int64 `json:"timeoutMs,omitempty"`
|
||||
}
|
||||
|
||||
func validName(s string, max int) bool {
|
||||
if s == "" || len(s) > max {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("._-/:", c)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func validError(e *Error) bool {
|
||||
return e != nil && validName(e.Code, 64) && len(e.Message) <= 1024 && utf8.ValidString(e.Message)
|
||||
}
|
||||
|
||||
func decode(data []byte, limit int) (envelope, error) {
|
||||
var e envelope
|
||||
if len(data) > limit || !utf8.Valid(data) {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
// Reject duplicate keys at every depth instead of accepting last-key-wins.
|
||||
scan := json.NewDecoder(bytes.NewReader(data))
|
||||
scan.UseNumber()
|
||||
if err := uniqueValue(scan, 0); err != nil {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
if _, err := scan.Token(); err != io.EOF {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
d := json.NewDecoder(bytes.NewReader(data))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&e); err != nil {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
if e.Version != 1 || e.TimeoutMS < 0 || e.TimeoutMS > int64((24*60*60)*1000) {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
switch e.Type {
|
||||
case "call":
|
||||
if !validName(e.ID, 64) || !validName(e.Method, 128) || len(e.Params) == 0 || len(e.Result) != 0 || e.Error != nil {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
case "event":
|
||||
if e.ID != "" || !validName(e.Method, 128) || len(e.Params) == 0 || len(e.Result) != 0 || e.Error != nil || e.TimeoutMS != 0 {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
case "reply":
|
||||
if !validName(e.ID, 64) || e.Method != "" || len(e.Params) != 0 || e.TimeoutMS != 0 || (len(e.Result) == 0) == (e.Error == nil) || (e.Error != nil && !validError(e.Error)) {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
case "cancel":
|
||||
if !validName(e.ID, 64) || e.Method != "" || len(e.Params) != 0 || len(e.Result) != 0 || e.Error != nil || e.TimeoutMS != 0 {
|
||||
return e, ErrProtocol
|
||||
}
|
||||
default:
|
||||
return e, ErrProtocol
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func uniqueValue(d *json.Decoder, depth int) error {
|
||||
if depth > 32 {
|
||||
return ErrProtocol
|
||||
}
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delim, ok := t.(json.Delim)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
switch delim {
|
||||
case '{':
|
||||
seen := make(map[string]bool)
|
||||
for d.More() {
|
||||
k, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, ok := k.(string)
|
||||
if !ok || seen[key] {
|
||||
return ErrProtocol
|
||||
}
|
||||
seen[key] = true
|
||||
if err := uniqueValue(d, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
t, err = d.Token()
|
||||
if err != nil || t != json.Delim('}') {
|
||||
return ErrProtocol
|
||||
}
|
||||
case '[':
|
||||
for d.More() {
|
||||
if err := uniqueValue(d, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
t, err = d.Token()
|
||||
if err != nil || t != json.Delim(']') {
|
||||
return ErrProtocol
|
||||
}
|
||||
default:
|
||||
return ErrProtocol
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user