Files
orbit/internal/transport/httpserver/event_codec.go

176 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package httpserver
import (
"bytes"
"encoding/json"
"io"
"mime"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
core "git.yuntuo.hk/k2safe/orbit/model"
)
const EventSubprotocol = "orbit.events.v1"
const eventMaxFrame = 512 << 10
type eventFrame struct {
Version string `json:"protocol_version"`
Type string `json:"type"`
MessageID core.ID `json:"message_id"`
ReplyTo core.ID `json:"reply_to,omitempty"`
SessionID core.ID `json:"session_id,omitempty"`
Body json.RawMessage `json:"body,omitempty"`
Error *fault.Error `json:"error,omitempty"`
}
type eventHello struct {
SubscriptionID core.ID `json:"subscription_id"`
Capacity uint32 `json:"capacity"`
}
type eventWelcome struct {
SessionID core.ID `json:"session_id"`
SubscriptionID core.ID `json:"subscription_id"`
Capacity uint32 `json:"capacity"`
LeaseDurationMS int64 `json:"lease_duration_ms"`
HeartbeatIntervalMS int64 `json:"heartbeat_interval_ms"`
ServerTime time.Time `json:"server_time"`
Enabled bool `json:"enabled"`
}
type eventPong struct {
ServerTime time.Time `json:"server_time"`
Enabled bool `json:"enabled"`
}
func decodeEventBody(r *http.Request, out any) error {
content, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || content != "application/json" {
return fault.New("UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json")
}
raw, err := io.ReadAll(io.LimitReader(r.Body, eventMaxFrame+1))
if err != nil {
return fault.Invalid("cannot read request body")
}
if len(raw) > eventMaxFrame {
return fault.New("BODY_TOO_LARGE", "request body exceeds 512 KiB")
}
return decodeEventJSON(raw, out)
}
func decodeEventJSON(raw []byte, out any) error {
if len(raw) == 0 || len(raw) > eventMaxFrame {
return fault.Invalid("invalid event message size")
}
if _, err := jsonvalue.Canonical(raw); err != nil {
return fault.Invalid("invalid event JSON")
}
typ := reflect.TypeOf(out).Elem()
if exactFields(raw, typ) != nil || requiredEventFields(raw, typ) != nil {
return fault.Invalid("missing, unknown or invalid event field")
}
d := json.NewDecoder(bytes.NewReader(raw))
d.DisallowUnknownFields()
if d.Decode(out) != nil {
return fault.Invalid("invalid event field value")
}
return nil
}
// requiredEventFields 同时验证显式控制意图及规范十进制字符串;可选历史游标允许0和null。
func requiredEventFields(raw []byte, typ reflect.Type) error {
if typ == reflect.TypeOf(json.RawMessage{}) || typ == reflect.TypeOf(time.Time{}) {
return nil
}
if typ.Kind() == reflect.Pointer {
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return nil
}
return requiredEventFields(raw, typ.Elem())
}
if typ.Kind() == reflect.Array || typ.Kind() == reflect.Slice {
var values []json.RawMessage
if json.Unmarshal(raw, &values) != nil {
return fault.Invalid("invalid event array")
}
for _, v := range values {
if err := requiredEventFields(v, typ.Elem()); err != nil {
return err
}
}
return nil
}
if typ.Kind() != reflect.Struct {
return nil
}
var fields map[string]json.RawMessage
if json.Unmarshal(raw, &fields) != nil {
return fault.Invalid("invalid event object")
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tags := strings.Split(field.Tag.Get("json"), ",")
if tags[0] == "" || tags[0] == "-" {
continue
}
optional, numberString := false, false
for _, v := range tags[1:] {
optional = optional || v == "omitempty"
numberString = numberString || v == "string"
}
value, found := fields[tags[0]]
if !found {
if optional {
continue
}
return fault.Invalid("missing event field")
}
ft := field.Type
nullable := ft.Kind() == reflect.Pointer
if nullable {
if bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
continue
}
ft = ft.Elem()
}
if ft.Kind() == reflect.Uint64 && numberString {
var text string
if json.Unmarshal(value, &text) != nil {
return fault.Invalid("event counter must be a decimal string")
}
n, err := strconv.ParseUint(text, 10, 64)
if err != nil || strconv.FormatUint(n, 10) != text || (!nullable && n == 0) {
return fault.Invalid("invalid event counter")
}
}
if err := requiredEventFields(value, field.Type); err != nil {
return err
}
}
return nil
}
func decodeEventRequest(raw []byte, hello bool) (eventFrame, error) {
var frame eventFrame
if err := decodeEventJSON(raw, &frame); err != nil {
return frame, err
}
if frame.Version != "1" || frame.MessageID.Validate() != nil || len(frame.Body) == 0 {
return frame, fault.Invalid("invalid event envelope")
}
var fields map[string]json.RawMessage
_ = json.Unmarshal(raw, &fields)
if fields["error"] != nil || fields["reply_to"] != nil {
return frame, fault.Invalid("response fields are not allowed in event requests")
}
if hello {
if frame.Type != "events.hello" || fields["session_id"] != nil {
return frame, fault.Invalid("invalid event hello")
}
} else if frame.SessionID.Validate() != nil || (frame.Type != "event.ack" && frame.Type != "events.ping") {
return frame, fault.Invalid("invalid event session or type")
}
return frame, nil
}