feat: add standalone Worker protocol SDK and ACK-loss regression
This commit is contained in:
138
sdk/go/worker_codec.go
Normal file
138
sdk/go/worker_codec.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
func parseWorkerFrame(kind websocket.MessageType, raw []byte) (workerFrame, error) {
|
||||
var f workerFrame
|
||||
if kind != websocket.MessageText || len(raw) > 512<<10 || validateWorkerJSON(raw) != nil {
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
// Envelope 字段严格区分大小写,防止 encoding/json 的大小写别名覆盖控制字段。
|
||||
var fields map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &fields) != nil || fields == nil {
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
for k := range fields {
|
||||
switch k {
|
||||
case "protocol_version", "type", "message_id", "reply_to", "session_id", "body", "error":
|
||||
default:
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(raw, &f) != nil || f.Version != "1" || !validID(f.MessageID) || f.ReplyTo != "" && !validID(f.ReplyTo) || f.SessionID != "" && !validID(f.SessionID) {
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
if f.Type == "error" {
|
||||
if f.Error == nil || !workerName(f.Error.Code, 64) || len(f.Error.Message) > 1024 || len(f.Body) > 0 {
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
} else if f.Error != nil || len(f.Body) == 0 || bytes.Equal(f.Body, []byte("null")) {
|
||||
return f, ErrWorkerProtocol
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// validateWorkerJSON 拒绝重复键、过深嵌套及非法 Unicode,保留数字原文。
|
||||
// 数值的业务范围仍由服务端规范化器最终验证;SDK 不经由 float64 重编码。
|
||||
func validateWorkerJSON(raw []byte) error {
|
||||
if len(raw) == 0 || !utf8.Valid(raw) || !json.Valid(raw) || !workerUnicode(raw) {
|
||||
return ErrWorkerProtocol
|
||||
}
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
d.UseNumber()
|
||||
var value func(int) error
|
||||
value = func(depth int) error {
|
||||
if depth > 32 {
|
||||
return ErrWorkerProtocol
|
||||
}
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case json.Delim('{'):
|
||||
keys := map[string]bool{}
|
||||
for d.More() {
|
||||
k, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, ok := k.(string)
|
||||
if !ok || keys[s] {
|
||||
return ErrWorkerProtocol
|
||||
}
|
||||
keys[s] = true
|
||||
if err := value(depth + 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = d.Token()
|
||||
return err
|
||||
case json.Delim('['):
|
||||
for d.More() {
|
||||
if err := value(depth + 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = d.Token()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := value(0); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := d.Token(); err != io.EOF {
|
||||
return ErrWorkerProtocol
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSON 的未配对 UTF-16 转义不能被静默替换为 U+FFFD。
|
||||
func workerUnicode(raw []byte) bool {
|
||||
inside := false
|
||||
for i := 0; i < len(raw); i++ {
|
||||
if raw[i] == '"' {
|
||||
inside = !inside
|
||||
continue
|
||||
}
|
||||
if !inside || raw[i] != '\\' {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if i >= len(raw) {
|
||||
return false
|
||||
}
|
||||
if raw[i] != 'u' {
|
||||
continue
|
||||
}
|
||||
if i+4 >= len(raw) {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
i += 4
|
||||
if n < 0xd800 || n > 0xdfff {
|
||||
continue
|
||||
}
|
||||
if n > 0xdbff || i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' {
|
||||
return false
|
||||
}
|
||||
low, err := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16)
|
||||
if err != nil || low < 0xdc00 || low > 0xdfff {
|
||||
return false
|
||||
}
|
||||
i += 6
|
||||
}
|
||||
return !inside
|
||||
}
|
||||
Reference in New Issue
Block a user