1018 lines
30 KiB
Go
1018 lines
30 KiB
Go
package main
|
|
|
|
// Shared data model + JSON plumbing for the Go port of the NexusGate panel.
|
|
//
|
|
// Compatibility contract with the Node implementation:
|
|
// - The database is one JSON object (see Data). Every record type mirrors
|
|
// the JS object shapes field by field; fields JS may leave absent use
|
|
// pointer+omitempty only where "absent" and "null" are semantically
|
|
// identical in the JS code (all reads are truthiness-based). Where the
|
|
// JS create paths write an explicit null, the Go field has NO omitempty
|
|
// so a nil pointer re-marshals as null.
|
|
// - Unknown fields (written by other panel versions) are captured per
|
|
// record in Extra and re-emitted verbatim on marshal, so a load/save
|
|
// round trip never drops data.
|
|
// - All JSON output goes through marshalJSON (HTML escaping disabled) to
|
|
// match JSON.stringify.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ---------- errors ----------
|
|
|
|
// HTTPError mirrors a JS Error carrying statusCode.
|
|
type HTTPError struct {
|
|
Status int
|
|
Message string
|
|
}
|
|
|
|
func (e *HTTPError) Error() string { return e.Message }
|
|
|
|
func httpErr(status int, format string, args ...any) *HTTPError {
|
|
return &HTTPError{Status: status, Message: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// ---------- time ----------
|
|
|
|
// NowISO is JS `new Date().toISOString()`: UTC, millisecond precision, Z.
|
|
func NowISO() string { return ISOFromTime(time.Now()) }
|
|
|
|
func ISOFromTime(t time.Time) string {
|
|
return t.UTC().Format("2006-01-02T15:04:05.000Z")
|
|
}
|
|
|
|
// ParseISO accepts the forms JS `new Date(value)` accepts for the inputs
|
|
// this panel produces/consumes: RFC3339 (with optional fractional seconds
|
|
// and offsets) and date-only "2006-01-02" (interpreted as UTC midnight).
|
|
func ParseISO(s string) (time.Time, bool) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return time.Time{}, false
|
|
}
|
|
for _, layout := range []string{
|
|
time.RFC3339Nano,
|
|
"2006-01-02T15:04:05.000Z0700",
|
|
"2006-01-02T15:04:05Z0700",
|
|
"2006-01-02T15:04Z0700",
|
|
"2006-01-02",
|
|
} {
|
|
if t, err := time.Parse(layout, s); err == nil {
|
|
return t, true
|
|
}
|
|
}
|
|
return time.Time{}, false
|
|
}
|
|
|
|
// ParseISOMs is JS Date.parse: milliseconds since epoch; ok=false is NaN
|
|
// (comparisons against NaN are always false in the JS code paths).
|
|
func ParseISOMs(s string) (int64, bool) {
|
|
t, ok := ParseISO(s)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return t.UnixMilli(), true
|
|
}
|
|
|
|
// ---------- ids ----------
|
|
|
|
func NewUUID() string {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
panic(err)
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|
|
|
|
// NewID mirrors JS `id(prefix)` => `${prefix}_${crypto.randomUUID()}`.
|
|
func NewID(prefix string) string { return prefix + "_" + NewUUID() }
|
|
|
|
func StrPtr(s string) *string { return &s }
|
|
func BoolPtr(b bool) *bool { return &b }
|
|
|
|
// boolVal is JS truthiness for an optional boolean field.
|
|
func boolVal(p *bool) bool { return p != nil && *p }
|
|
func IntPtr(v int) *int { return &v }
|
|
|
|
// ---------- JSON helpers ----------
|
|
|
|
// marshalJSON is JSON.stringify: compact, no HTML escaping.
|
|
func marshalJSON(v any) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
|
}
|
|
|
|
// unmarshalJSONUseNumber decodes with UseNumber so generic `any` fields
|
|
// (activity detail, job payloads from agents) keep integer literals exact.
|
|
func unmarshalJSONUseNumber(b []byte, v any) error {
|
|
dec := json.NewDecoder(bytes.NewReader(b))
|
|
dec.UseNumber()
|
|
return dec.Decode(v)
|
|
}
|
|
|
|
// cloneJSONValue is structuredClone for JSON-shaped values.
|
|
func cloneJSONValue[T any](v T) T {
|
|
raw, err := marshalJSON(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var out T
|
|
if err := unmarshalJSONUseNumber(raw, &out); err != nil {
|
|
panic(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------- unknown-field passthrough ----------
|
|
|
|
type extraFields map[string]json.RawMessage
|
|
|
|
var knownKeyCache sync.Map // reflect.Type -> map[string]struct{}
|
|
|
|
func knownJSONKeys(t reflect.Type) map[string]struct{} {
|
|
if cached, ok := knownKeyCache.Load(t); ok {
|
|
return cached.(map[string]struct{})
|
|
}
|
|
keys := map[string]struct{}{}
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
if f.PkgPath != "" { // unexported
|
|
continue
|
|
}
|
|
tag := f.Tag.Get("json")
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
name := strings.Split(tag, ",")[0]
|
|
if name == "" {
|
|
name = f.Name
|
|
}
|
|
keys[name] = struct{}{}
|
|
}
|
|
knownKeyCache.Store(t, keys)
|
|
return keys
|
|
}
|
|
|
|
func captureExtra(raw []byte, sample any) extraFields {
|
|
var all map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &all); err != nil || len(all) == 0 {
|
|
return nil
|
|
}
|
|
known := knownJSONKeys(reflect.TypeOf(sample))
|
|
var extra extraFields
|
|
for k, v := range all {
|
|
if _, ok := known[k]; !ok {
|
|
if extra == nil {
|
|
extra = extraFields{}
|
|
}
|
|
extra[k] = v
|
|
}
|
|
}
|
|
return extra
|
|
}
|
|
|
|
// mergeMarshal marshals a record and re-attaches its unknown fields.
|
|
func mergeMarshal(sample any, extra extraFields) ([]byte, error) {
|
|
base, err := marshalJSON(sample)
|
|
if err != nil || len(extra) == 0 {
|
|
return base, err
|
|
}
|
|
var merged map[string]json.RawMessage
|
|
if err := json.Unmarshal(base, &merged); err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range extra {
|
|
if _, ok := merged[k]; !ok {
|
|
merged[k] = v
|
|
}
|
|
}
|
|
return marshalJSON(merged)
|
|
}
|
|
|
|
// recordJSON implements the UnmarshalJSON half shared by every record type.
|
|
func recordJSON[T any](raw []byte, dst *T, extra *extraFields) error {
|
|
var plain T
|
|
if err := unmarshalJSONUseNumber(raw, &plain); err != nil {
|
|
return err
|
|
}
|
|
*dst = plain
|
|
*extra = captureExtra(raw, plain)
|
|
return nil
|
|
}
|
|
|
|
// ---------- database ----------
|
|
|
|
type Data struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
CreatedAt string `json:"createdAt"`
|
|
Settings Settings `json:"settings"`
|
|
Users []*User `json:"users"`
|
|
Servers []*Server `json:"servers"`
|
|
Agents []*Agent `json:"agents"`
|
|
EnrollmentTokens []*EnrollmentToken `json:"enrollmentTokens"`
|
|
Customers []*Customer `json:"customers"`
|
|
Nodes []*Node `json:"nodes"`
|
|
NodeClients []*NodeClient `json:"nodeClients"`
|
|
Jobs []*Job `json:"jobs"`
|
|
Activity []*Activity `json:"activity"`
|
|
Observations []*Observation `json:"observations"`
|
|
SubscriptionAccess []*SubscriptionAccess `json:"subscriptionAccess"`
|
|
SubscriptionClients []*SubscriptionClient `json:"subscriptionClients"`
|
|
KeyStore map[string]*KeyEntry `json:"keyStore"`
|
|
SubSourceCache map[string]*SubSourceCacheEntry `json:"subSourceCache,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (d *Data) UnmarshalJSON(raw []byte) error {
|
|
type plain Data
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*d = Data(p)
|
|
d.Extra = extra
|
|
return nil
|
|
}
|
|
|
|
func (d Data) MarshalJSON() ([]byte, error) {
|
|
type plain Data
|
|
return mergeMarshal(plain(d), d.Extra)
|
|
}
|
|
|
|
// Clone is structuredClone(this.data).
|
|
func (d *Data) Clone() *Data {
|
|
raw, err := marshalJSON(d)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var out Data
|
|
if err := unmarshalJSONUseNumber(raw, &out); err != nil {
|
|
panic(err)
|
|
}
|
|
return &out
|
|
}
|
|
|
|
type Settings struct {
|
|
BrandName string `json:"brandName"`
|
|
SessionHours float64 `json:"sessionHours"`
|
|
ObservationTtlMinutes float64 `json:"observationTtlMinutes"`
|
|
ActivityRetentionDays float64 `json:"activityRetentionDays"`
|
|
CompletedJobRetentionDays float64 `json:"completedJobRetentionDays"`
|
|
ExternalSubRefreshMinutes float64 `json:"externalSubRefreshMinutes,omitempty"`
|
|
AllowPrivateSubSources bool `json:"allowPrivateSubSources,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (s *Settings) UnmarshalJSON(raw []byte) error {
|
|
type plain Settings
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*s = Settings(p)
|
|
s.Extra = extra
|
|
return nil
|
|
}
|
|
func (s Settings) MarshalJSON() ([]byte, error) {
|
|
type plain Settings
|
|
return mergeMarshal(plain(s), s.Extra)
|
|
}
|
|
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
PasswordHash string `json:"passwordHash"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt *string `json:"updatedAt,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (u *User) UnmarshalJSON(raw []byte) error {
|
|
type plain User
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*u = User(p)
|
|
u.Extra = extra
|
|
return nil
|
|
}
|
|
func (u User) MarshalJSON() ([]byte, error) {
|
|
type plain User
|
|
return mergeMarshal(plain(u), u.Extra)
|
|
}
|
|
|
|
type EngineState struct {
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
SingBoxInstalled bool `json:"singBoxInstalled"`
|
|
Certificates []string `json:"certificates"`
|
|
SingBoxStatus string `json:"singBoxStatus"`
|
|
At string `json:"at"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (e *EngineState) UnmarshalJSON(raw []byte) error {
|
|
type plain EngineState
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*e = EngineState(p)
|
|
e.Extra = extra
|
|
return nil
|
|
}
|
|
func (e EngineState) MarshalJSON() ([]byte, error) {
|
|
type plain EngineState
|
|
return mergeMarshal(plain(e), e.Extra)
|
|
}
|
|
|
|
type ServerUsage struct {
|
|
LastReportAt string `json:"lastReportAt"`
|
|
SampleCount int `json:"sampleCount"`
|
|
Error *string `json:"error"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (u *ServerUsage) UnmarshalJSON(raw []byte) error {
|
|
type plain ServerUsage
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*u = ServerUsage(p)
|
|
u.Extra = extra
|
|
return nil
|
|
}
|
|
func (u ServerUsage) MarshalJSON() ([]byte, error) {
|
|
type plain ServerUsage
|
|
return mergeMarshal(plain(u), u.Extra)
|
|
}
|
|
|
|
type Server struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
Region string `json:"region"`
|
|
PublicAddress string `json:"publicAddress"`
|
|
PublicAddressV6 string `json:"publicAddressV6"`
|
|
TLSDomain string `json:"tlsDomain"`
|
|
PortRangeStart int `json:"portRangeStart"`
|
|
PortRangeEnd int `json:"portRangeEnd"`
|
|
Labels []string `json:"labels"`
|
|
Status string `json:"status"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
LastSeenAt *string `json:"lastSeenAt,omitempty"`
|
|
System json.RawMessage `json:"system,omitempty"`
|
|
Engine *EngineState `json:"engine,omitempty"`
|
|
EnginePresent bool `json:"-"`
|
|
Usage *ServerUsage `json:"usage,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (s *Server) UnmarshalJSON(raw []byte) error {
|
|
type plain Server
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*s = Server(p)
|
|
s.Extra = extra
|
|
var probe map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &probe) == nil {
|
|
_, s.EnginePresent = probe["engine"]
|
|
}
|
|
return nil
|
|
}
|
|
func (s Server) MarshalJSON() ([]byte, error) {
|
|
type plain Server
|
|
base, err := mergeMarshal(plain(s), s.Extra)
|
|
if err != nil || s.Engine != nil || !s.EnginePresent {
|
|
return base, err
|
|
}
|
|
// JS assigns server.engine = ... || null on heartbeat: once the key
|
|
// exists it stays present with value null.
|
|
var merged map[string]json.RawMessage
|
|
if err := json.Unmarshal(base, &merged); err != nil {
|
|
return nil, err
|
|
}
|
|
merged["engine"] = json.RawMessage("null")
|
|
return marshalJSON(merged)
|
|
}
|
|
|
|
type Agent struct {
|
|
ID string `json:"id"`
|
|
ServerID string `json:"serverId"`
|
|
KeyHash string `json:"keyHash"`
|
|
KeyFingerprint *string `json:"keyFingerprint,omitempty"`
|
|
Status string `json:"status"`
|
|
Hostname string `json:"hostname"`
|
|
Version string `json:"version"`
|
|
CreatedAt string `json:"createdAt"`
|
|
LastSeenAt string `json:"lastSeenAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (a *Agent) UnmarshalJSON(raw []byte) error {
|
|
type plain Agent
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*a = Agent(p)
|
|
a.Extra = extra
|
|
return nil
|
|
}
|
|
func (a Agent) MarshalJSON() ([]byte, error) {
|
|
type plain Agent
|
|
return mergeMarshal(plain(a), a.Extra)
|
|
}
|
|
|
|
type EnrollmentToken struct {
|
|
ID string `json:"id"`
|
|
ServerID string `json:"serverId"`
|
|
TokenHash string `json:"tokenHash"`
|
|
CreatedAt string `json:"createdAt"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
UsedAt *string `json:"usedAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (t *EnrollmentToken) UnmarshalJSON(raw []byte) error {
|
|
type plain EnrollmentToken
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*t = EnrollmentToken(p)
|
|
t.Extra = extra
|
|
return nil
|
|
}
|
|
func (t EnrollmentToken) MarshalJSON() ([]byte, error) {
|
|
type plain EnrollmentToken
|
|
return mergeMarshal(plain(t), t.Extra)
|
|
}
|
|
|
|
type UsageCounters struct {
|
|
Uplink int64 `json:"uplink"`
|
|
Downlink int64 `json:"downlink"`
|
|
Epoch *string `json:"epoch"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (c *UsageCounters) UnmarshalJSON(raw []byte) error {
|
|
type plain UsageCounters
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*c = UsageCounters(p)
|
|
c.Extra = extra
|
|
return nil
|
|
}
|
|
func (c UsageCounters) MarshalJSON() ([]byte, error) {
|
|
type plain UsageCounters
|
|
return mergeMarshal(plain(c), c.Extra)
|
|
}
|
|
|
|
type UsageSlot struct {
|
|
UsageCounters *UsageCounters `json:"usageCounters,omitempty"`
|
|
RetiredUsageEpochs []string `json:"retiredUsageEpochs,omitempty"`
|
|
LastSampleAt *string `json:"lastSampleAt,omitempty"`
|
|
LastActiveAt *string `json:"lastActiveAt,omitempty"`
|
|
SpeedUpBps *float64 `json:"speedUpBps,omitempty"`
|
|
SpeedDownBps *float64 `json:"speedDownBps,omitempty"`
|
|
SpeedAt *string `json:"speedAt,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (s *UsageSlot) UnmarshalJSON(raw []byte) error {
|
|
type plain UsageSlot
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*s = UsageSlot(p)
|
|
s.Extra = extra
|
|
return nil
|
|
}
|
|
func (s UsageSlot) MarshalJSON() ([]byte, error) {
|
|
type plain UsageSlot
|
|
return mergeMarshal(plain(s), s.Extra)
|
|
}
|
|
|
|
type Customer struct {
|
|
ID string `json:"id"`
|
|
SubscriptionToken string `json:"subscriptionToken"`
|
|
Name string `json:"name"`
|
|
Group string `json:"group"`
|
|
Status string `json:"status"`
|
|
SuspendReason *string `json:"suspendReason,omitempty"`
|
|
SuspendReasonPresent bool `json:"-"`
|
|
TrafficLimitBytes float64 `json:"trafficLimitBytes"`
|
|
UsedBytes int64 `json:"usedBytes"`
|
|
UsedUplinkBytes int64 `json:"usedUplinkBytes"`
|
|
UsedDownlinkBytes int64 `json:"usedDownlinkBytes"`
|
|
TrafficResetDay int `json:"trafficResetDay,omitempty"`
|
|
TrafficResetPeriod string `json:"trafficResetPeriod,omitempty"`
|
|
TrafficResetAt *string `json:"trafficResetAt,omitempty"`
|
|
ExpiresAt *string `json:"expiresAt"`
|
|
IPLimit int64 `json:"ipLimit"`
|
|
DeviceLimit int64 `json:"deviceLimit"`
|
|
Tags []string `json:"tags"`
|
|
Notes string `json:"notes"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"`
|
|
LastUsageAt *string `json:"lastUsageAt,omitempty"`
|
|
LastOnlineAt *string `json:"lastOnlineAt,omitempty"`
|
|
ExternalSources []ExtSource `json:"externalSources,omitempty"`
|
|
ManualLinks []ManualLink `json:"manualLinks,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
// ExtSource is one external subscription source on a customer
|
|
// (SUB-AGGREGATION-DESIGN.md). UserAgent is optional.
|
|
type ExtSource struct {
|
|
ID string `json:"id"`
|
|
URL string `json:"url"`
|
|
Remark string `json:"remark"`
|
|
Enabled bool `json:"enabled"`
|
|
UserAgent string `json:"userAgent,omitempty"`
|
|
}
|
|
|
|
// ManualLink is one hand-added node link on a customer.
|
|
type ManualLink struct {
|
|
ID string `json:"id"`
|
|
URI string `json:"uri"`
|
|
Remark string `json:"remark"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// SubCacheItem is one parsed entry of a fetched external source.
|
|
type SubCacheItem struct {
|
|
URI string `json:"uri"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// SubSourceCacheEntry is the fetch cache for one external source,
|
|
// stored top-level in data.subSourceCache keyed by source id.
|
|
type SubSourceCacheEntry struct {
|
|
FetchedAt *string `json:"fetchedAt"`
|
|
OkAt *string `json:"okAt"`
|
|
FailCount int `json:"failCount"`
|
|
LastError *string `json:"lastError"`
|
|
DroppedCount int `json:"droppedCount"`
|
|
Items []SubCacheItem `json:"items"`
|
|
}
|
|
|
|
func (c *Customer) UnmarshalJSON(raw []byte) error {
|
|
type plain Customer
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*c = Customer(p)
|
|
c.Extra = extra
|
|
var probe map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &probe) == nil {
|
|
_, c.SuspendReasonPresent = probe["suspendReason"]
|
|
}
|
|
return nil
|
|
}
|
|
func (c Customer) MarshalJSON() ([]byte, error) {
|
|
type plain Customer
|
|
base, err := mergeMarshal(plain(c), c.Extra)
|
|
if err != nil || c.SuspendReason != nil || !c.SuspendReasonPresent {
|
|
return base, err
|
|
}
|
|
// JS writes suspendReason: null explicitly on resume paths.
|
|
var merged map[string]json.RawMessage
|
|
if err := json.Unmarshal(base, &merged); err != nil {
|
|
return nil, err
|
|
}
|
|
merged["suspendReason"] = json.RawMessage("null")
|
|
return marshalJSON(merged)
|
|
}
|
|
|
|
type RealityConfig struct {
|
|
KeyID string `json:"keyId"`
|
|
ServerName string `json:"serverName"`
|
|
DestPort int `json:"destPort"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (r *RealityConfig) UnmarshalJSON(raw []byte) error {
|
|
type plain RealityConfig
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*r = RealityConfig(p)
|
|
r.Extra = extra
|
|
return nil
|
|
}
|
|
func (r RealityConfig) MarshalJSON() ([]byte, error) {
|
|
type plain RealityConfig
|
|
return mergeMarshal(plain(r), r.Extra)
|
|
}
|
|
|
|
type ExitCredentials struct {
|
|
ClientID string `json:"clientId"`
|
|
Password string `json:"password"`
|
|
SocksUser string `json:"socksUser"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (c *ExitCredentials) UnmarshalJSON(raw []byte) error {
|
|
type plain ExitCredentials
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*c = ExitCredentials(p)
|
|
c.Extra = extra
|
|
return nil
|
|
}
|
|
func (c ExitCredentials) MarshalJSON() ([]byte, error) {
|
|
type plain ExitCredentials
|
|
return mergeMarshal(plain(c), c.Extra)
|
|
}
|
|
|
|
type MeteredTraffic struct {
|
|
Uplink int64 `json:"uplink"`
|
|
Downlink int64 `json:"downlink"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (m *MeteredTraffic) UnmarshalJSON(raw []byte) error {
|
|
type plain MeteredTraffic
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*m = MeteredTraffic(p)
|
|
m.Extra = extra
|
|
return nil
|
|
}
|
|
func (m MeteredTraffic) MarshalJSON() ([]byte, error) {
|
|
type plain MeteredTraffic
|
|
return mergeMarshal(plain(m), m.Extra)
|
|
}
|
|
|
|
type Node struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
ServerID string `json:"serverId"`
|
|
ExitServerID *string `json:"exitServerId"`
|
|
Protocol string `json:"protocol"`
|
|
Topology string `json:"topology"`
|
|
ExitProtocol *string `json:"exitProtocol"`
|
|
NetworkMode string `json:"networkMode"`
|
|
RealityServerName string `json:"realityServerName"`
|
|
RealityDestPort int `json:"realityDestPort"`
|
|
PortMode string `json:"portMode"`
|
|
Port int `json:"port"`
|
|
ExitPortMode *string `json:"exitPortMode"`
|
|
ExitPort *int `json:"exitPort"`
|
|
Status string `json:"status"`
|
|
EntryReady bool `json:"entryReady"`
|
|
ExitReady bool `json:"exitReady"`
|
|
ResourceID string `json:"resourceId"`
|
|
ExitResourceID string `json:"exitResourceId"`
|
|
Artifacts map[string]json.RawMessage `json:"artifacts"`
|
|
Error *string `json:"error"`
|
|
PendingDelete bool `json:"pendingDelete"`
|
|
AutoRemoving *bool `json:"autoRemoving,omitempty"`
|
|
WSPath *string `json:"wsPath,omitempty"`
|
|
SSServerKey *string `json:"ssServerKey,omitempty"`
|
|
Reality *RealityConfig `json:"reality,omitempty"`
|
|
ExitCredentials *ExitCredentials `json:"exitCredentials,omitempty"`
|
|
UsageCounters *UsageCounters `json:"usageCounters"`
|
|
RetiredUsageEpochs []string `json:"retiredUsageEpochs"`
|
|
MeteredTraffic *MeteredTraffic `json:"meteredTraffic"`
|
|
LastUsageAt *string `json:"lastUsageAt"`
|
|
SortIndex int `json:"sortIndex"`
|
|
SpeedUpBps *float64 `json:"speedUpBps,omitempty"`
|
|
SpeedDownBps *float64 `json:"speedDownBps,omitempty"`
|
|
SpeedAt *string `json:"speedAt,omitempty"`
|
|
LastActiveAt *string `json:"lastActiveAt,omitempty"`
|
|
LastSampleAt *string `json:"lastSampleAt,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (n *Node) UnmarshalJSON(raw []byte) error {
|
|
type plain Node
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*n = Node(p)
|
|
n.Extra = extra
|
|
return nil
|
|
}
|
|
func (n Node) MarshalJSON() ([]byte, error) {
|
|
type plain Node
|
|
return mergeMarshal(plain(n), n.Extra)
|
|
}
|
|
|
|
type Credentials struct {
|
|
ClientID string `json:"clientId"`
|
|
RelayPassword string `json:"relayPassword"`
|
|
ShortID string `json:"shortId"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (c *Credentials) UnmarshalJSON(raw []byte) error {
|
|
type plain Credentials
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*c = Credentials(p)
|
|
c.Extra = extra
|
|
return nil
|
|
}
|
|
func (c Credentials) MarshalJSON() ([]byte, error) {
|
|
type plain Credentials
|
|
return mergeMarshal(plain(c), c.Extra)
|
|
}
|
|
|
|
type NodeClient struct {
|
|
ID string `json:"id"`
|
|
NodeID string `json:"nodeId"`
|
|
CustomerID string `json:"customerId"`
|
|
Credentials Credentials `json:"credentials"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (n *NodeClient) UnmarshalJSON(raw []byte) error {
|
|
type plain NodeClient
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*n = NodeClient(p)
|
|
n.Extra = extra
|
|
return nil
|
|
}
|
|
func (n NodeClient) MarshalJSON() ([]byte, error) {
|
|
type plain NodeClient
|
|
return mergeMarshal(plain(n), n.Extra)
|
|
}
|
|
|
|
// KeyPair is a Reality keypair as shipped in job payload keyMaterial.
|
|
type KeyPair struct {
|
|
PrivateKey string `json:"privateKey"`
|
|
PublicKey string `json:"publicKey"`
|
|
}
|
|
|
|
// KeyEntry is a keyStore record (KeyPair + updatedAt).
|
|
type KeyEntry struct {
|
|
PrivateKey string `json:"privateKey"`
|
|
PublicKey string `json:"publicKey"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (k *KeyEntry) UnmarshalJSON(raw []byte) error {
|
|
type plain KeyEntry
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*k = KeyEntry(p)
|
|
k.Extra = extra
|
|
return nil
|
|
}
|
|
func (k KeyEntry) MarshalJSON() ([]byte, error) {
|
|
type plain KeyEntry
|
|
return mergeMarshal(plain(k), k.Extra)
|
|
}
|
|
|
|
type JobPayload struct {
|
|
Resource json.RawMessage `json:"resource,omitempty"`
|
|
Kind string `json:"kind,omitempty"`
|
|
KeyMaterial map[string]KeyPair `json:"keyMaterial,omitempty"`
|
|
ResourceID string `json:"resourceId,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (p *JobPayload) UnmarshalJSON(raw []byte) error {
|
|
type plain JobPayload
|
|
var q plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &q, &extra); err != nil {
|
|
return err
|
|
}
|
|
*p = JobPayload(q)
|
|
p.Extra = extra
|
|
return nil
|
|
}
|
|
func (p JobPayload) MarshalJSON() ([]byte, error) {
|
|
type plain JobPayload
|
|
return mergeMarshal(plain(p), p.Extra)
|
|
}
|
|
|
|
// KindOf mirrors JS `payload.kind || payload.resource?.meta?.kind`.
|
|
func (p *JobPayload) KindOf() string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
if p.Kind != "" {
|
|
return p.Kind
|
|
}
|
|
if len(p.Resource) > 0 {
|
|
var probe struct {
|
|
Meta struct {
|
|
Kind string `json:"kind"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(p.Resource, &probe); err == nil {
|
|
return probe.Meta.Kind
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type Job struct {
|
|
ID string `json:"id"`
|
|
ServerID string `json:"serverId"`
|
|
NodeID string `json:"nodeId"`
|
|
Action string `json:"action"`
|
|
Payload *JobPayload `json:"payload"`
|
|
Status string `json:"status"`
|
|
Attempts int `json:"attempts"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
Error *string `json:"error"`
|
|
LeaseUntil *string `json:"leaseUntil"`
|
|
StartedAt *string `json:"startedAt,omitempty"`
|
|
CompletedAt *string `json:"completedAt,omitempty"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (j *Job) UnmarshalJSON(raw []byte) error {
|
|
type plain Job
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*j = Job(p)
|
|
j.Extra = extra
|
|
return nil
|
|
}
|
|
func (j Job) MarshalJSON() ([]byte, error) {
|
|
type plain Job
|
|
return mergeMarshal(plain(j), j.Extra)
|
|
}
|
|
|
|
type Activity struct {
|
|
ID string `json:"id"`
|
|
At string `json:"at"`
|
|
Actor string `json:"actor"`
|
|
Action string `json:"action"`
|
|
Target string `json:"target"`
|
|
Detail map[string]any `json:"detail"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (a *Activity) UnmarshalJSON(raw []byte) error {
|
|
type plain Activity
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*a = Activity(p)
|
|
a.Extra = extra
|
|
return nil
|
|
}
|
|
func (a Activity) MarshalJSON() ([]byte, error) {
|
|
type plain Activity
|
|
return mergeMarshal(plain(a), a.Extra)
|
|
}
|
|
|
|
type Observation struct {
|
|
ID string `json:"id"`
|
|
CustomerID string `json:"customerId"`
|
|
IP string `json:"ip"`
|
|
FirstSeen string `json:"firstSeenAt"`
|
|
LastSeenAt string `json:"lastSeenAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (o *Observation) UnmarshalJSON(raw []byte) error {
|
|
type plain Observation
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*o = Observation(p)
|
|
o.Extra = extra
|
|
return nil
|
|
}
|
|
func (o Observation) MarshalJSON() ([]byte, error) {
|
|
type plain Observation
|
|
return mergeMarshal(plain(o), o.Extra)
|
|
}
|
|
|
|
type SubscriptionAccess struct {
|
|
ID string `json:"id"`
|
|
CustomerID string `json:"customerId"`
|
|
At string `json:"at"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"userAgent"`
|
|
IdentityType string `json:"identityType"`
|
|
ClientKey string `json:"clientKey"`
|
|
Format string `json:"format"`
|
|
Status int `json:"status"`
|
|
Reason string `json:"reason"`
|
|
Bytes int64 `json:"bytes"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (a *SubscriptionAccess) UnmarshalJSON(raw []byte) error {
|
|
type plain SubscriptionAccess
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*a = SubscriptionAccess(p)
|
|
a.Extra = extra
|
|
return nil
|
|
}
|
|
func (a SubscriptionAccess) MarshalJSON() ([]byte, error) {
|
|
type plain SubscriptionAccess
|
|
return mergeMarshal(plain(a), a.Extra)
|
|
}
|
|
|
|
type SubscriptionClient struct {
|
|
CustomerID string `json:"customerId"`
|
|
ClientKey string `json:"clientKey"`
|
|
IdentityType string `json:"identityType"`
|
|
FirstSeenAt string `json:"firstSeenAt"`
|
|
LastSeenAt string `json:"lastSeenAt"`
|
|
Extra extraFields `json:"-"`
|
|
}
|
|
|
|
func (c *SubscriptionClient) UnmarshalJSON(raw []byte) error {
|
|
type plain SubscriptionClient
|
|
var p plain
|
|
var extra extraFields
|
|
if err := recordJSON(raw, &p, &extra); err != nil {
|
|
return err
|
|
}
|
|
*c = SubscriptionClient(p)
|
|
c.Extra = extra
|
|
return nil
|
|
}
|
|
func (c SubscriptionClient) MarshalJSON() ([]byte, error) {
|
|
type plain SubscriptionClient
|
|
return mergeMarshal(plain(c), c.Extra)
|
|
}
|