176 lines
5.3 KiB
Go
176 lines
5.3 KiB
Go
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
|
||
}
|