| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608 |
- package store
- import (
- "bytes"
- "encoding/json"
- "errors"
- "io"
- "reflect"
- "sort"
- "strconv"
- "strings"
- "time"
- )
- const SecretMask = "********"
- type Device struct {
- ID string
- Name string
- Interface string
- ControlDevice string
- ATPort string
- USBPath string
- AudioDevice string
- ModemIMEI string
- APN string
- ProxyPort int
- BaudRate int
- DataBits int
- StopBits int
- Parity string
- DeviceBackend string
- ESIMTransport string
- QMIUseProxy bool
- QMIProxyPath string
- QMIProxyExecutable string
- NetworkEnabled bool
- SMSEnabled bool
- VoWiFiEnabled bool
- Extra json.RawMessage
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- type DeviceRuntime struct {
- DeviceID string
- Running bool
- Healthy bool
- ControlOnline bool
- PhysicalPresent bool
- WorkerRunning bool
- DataConnected bool
- RadioRegistered bool
- NetworkConnected bool
- FlightMode bool
- LifecyclePhase string
- LifecycleReason string
- PublicIP string
- PrivateIP string
- Operator string
- NativeMCC string
- NativeMNC string
- NativeSPN string
- NetworkMode string
- NetworkDuplex string
- RadioBand string
- RadioChannel int
- SignalDBM int
- SignalRSRP *int
- SignalRSRQ *int
- SignalSINR *int
- IMEI string
- ICCID string
- IMSI string
- Firmware string
- RegStatus int
- RegStatusText string
- PSAttached *bool
- SIMInserted *bool
- OperatingMode *int
- PhoneNumber string
- PhoneNumberSource string
- Traffic json.RawMessage
- Extra json.RawMessage
- UpdatedAt time.Time
- }
- type VoWiFiRuntime struct {
- DeviceID string
- Phase string
- DataplaneMode string
- ICCID string
- IMSI string
- SIMReady bool
- AccessReady bool
- TunnelReady bool
- IMSReady bool
- SMSReady bool
- RegStatus int
- RegStatusText string
- NetworkMode string
- LocalPhone string
- PhoneNumberSource string
- LastErrorClass string
- LastError string
- LastReason string
- Tunnel json.RawMessage
- IMSCore json.RawMessage
- SMSIP json.RawMessage
- Extra json.RawMessage
- UpdatedAt time.Time
- }
- // PhoneAssociation is a number explicitly published by IMS for one SIM. It is
- // keyed by ICCID so a verified number survives service restarts and follows the
- // SIM without ever being inferred from IMSI.
- type PhoneAssociation struct {
- ICCID string
- DeviceID string
- Number string
- Source string
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- type SMSMessage struct {
- ID int64
- MessageID string
- DeviceID string
- IMSI string
- Peer string
- Direction string
- Body string
- Timestamp time.Time
- Status string
- Source string
- PartsTotal int
- DeliveryState string
- Read bool
- Extra json.RawMessage
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- type SMSFilter struct {
- DeviceID string
- IMSI string
- Peer string
- Since time.Time
- Until time.Time
- BeforeID int64
- Limit int
- }
- // SMSDeliveryReport is network evidence for one submitted SMS part. The
- // message reference is the TP-MR returned in SMS-STATUS-REPORT.
- type SMSDeliveryReport struct {
- DeviceID string
- IMSI string
- Peer string
- Source string
- MessageReference int
- StatusCode int
- DeliveryState string
- ServiceCenterTime *time.Time
- DischargeTime *time.Time
- ReceivedAt time.Time
- }
- type SMSContact struct {
- DeviceID string
- DeviceName string
- IMSI string
- LocalPhone string
- Peer string
- DisplayName string
- LastMessage string
- LastTimestamp time.Time
- Direction string
- LastSMSID int64
- UnreadCount int
- MessageCount int
- }
- type LocalProxyConfig struct {
- ID string
- Name string
- Mode string
- DeviceID string
- ListenAddr string
- ListenPort int
- Enabled bool
- AuthEnabled bool
- Username string
- Password string
- Extra json.RawMessage
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- func (value LocalProxyConfig) Redacted() LocalProxyConfig {
- if value.Password != "" {
- value.Password = SecretMask
- }
- return value
- }
- func (value LocalProxyConfig) Public() LocalProxyConfig {
- value.Password = ""
- return value
- }
- func (value LocalProxyConfig) SensitiveValues() []string {
- if value.Password == "" || value.Password == SecretMask {
- return nil
- }
- return []string{value.Password}
- }
- type UpstreamProxy struct {
- ID string
- Name string
- Addr string
- Username string
- Password string
- Enabled bool
- Extra json.RawMessage
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- func (value UpstreamProxy) Redacted() UpstreamProxy {
- if value.Password != "" {
- value.Password = SecretMask
- }
- return value
- }
- func (value UpstreamProxy) Public() UpstreamProxy {
- value.Password = ""
- return value
- }
- func (value UpstreamProxy) SensitiveValues() []string {
- if value.Password == "" || value.Password == SecretMask {
- return nil
- }
- return []string{value.Password}
- }
- type CountryRule struct {
- CountryCode string
- CountryName string
- UpstreamProxyID string
- Enabled bool
- Extra json.RawMessage
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- // DeviceProxyBinding selects the SOCKS5 upstream used by one device's whole
- // VoWiFi runtime. The IKE/IPsec transport uses this route and IMS/SMS then
- // travel inside that tunnel.
- type DeviceProxyBinding struct {
- DeviceID string
- UpstreamProxyID string
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- type NotificationSetting struct {
- Channel string
- Enabled bool
- Config json.RawMessage
- SensitiveFields []string
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- func (value NotificationSetting) Redacted() NotificationSetting {
- value.Config = redactJSONFields(value.Config, value.SensitiveFields, SecretMask)
- return value
- }
- func (value NotificationSetting) Public() NotificationSetting {
- value.Config = redactJSONFields(value.Config, value.SensitiveFields, "")
- return value
- }
- func (value NotificationSetting) SensitiveValues() []string {
- document, err := decodeJSONObject(value.Config)
- if err != nil {
- return nil
- }
- values := make([]string, 0, len(value.SensitiveFields))
- for _, field := range value.SensitiveFields {
- if secret, ok := getJSONPath(document, field).(string); ok &&
- secret != "" && secret != SecretMask {
- values = append(values, secret)
- }
- }
- return values
- }
- type AppSetting struct {
- Key string
- Value json.RawMessage
- Sensitive bool
- UpdatedAt time.Time
- }
- func (value AppSetting) Redacted() AppSetting {
- if value.Sensitive {
- value.Value = json.RawMessage(strconv.Quote(SecretMask))
- }
- return value
- }
- func (value AppSetting) Public() AppSetting {
- if value.Sensitive {
- value.Value = json.RawMessage(`null`)
- }
- return value
- }
- func (value AppSetting) SensitiveValues() []string {
- if !value.Sensitive {
- return nil
- }
- var decoded any
- decoder := json.NewDecoder(bytes.NewReader(value.Value))
- decoder.UseNumber()
- if decoder.Decode(&decoded) != nil {
- return nil
- }
- var values []string
- collectJSONStringValues(decoded, &values)
- return uniqueNonemptyStrings(values)
- }
- type SensitiveValuesProvider interface {
- SensitiveValues() []string
- }
- func RedactText(text string, providers ...SensitiveValuesProvider) string {
- var secrets []string
- for _, provider := range providers {
- if !nilSensitiveProvider(provider) {
- secrets = append(secrets, provider.SensitiveValues()...)
- }
- }
- sort.Slice(secrets, func(i, j int) bool {
- return len(secrets[i]) > len(secrets[j])
- })
- for _, secret := range secrets {
- if secret != "" && secret != SecretMask {
- text = strings.ReplaceAll(text, secret, SecretMask)
- }
- }
- return text
- }
- func nilSensitiveProvider(provider SensitiveValuesProvider) bool {
- if provider == nil {
- return true
- }
- value := reflect.ValueOf(provider)
- switch value.Kind() {
- case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
- reflect.Pointer, reflect.Slice:
- return value.IsNil()
- default:
- return false
- }
- }
- func collectJSONStringValues(value any, result *[]string) {
- switch typed := value.(type) {
- case string:
- if typed != "" && typed != SecretMask {
- *result = append(*result, typed)
- }
- case []any:
- for _, item := range typed {
- collectJSONStringValues(item, result)
- }
- case map[string]any:
- for _, item := range typed {
- collectJSONStringValues(item, result)
- }
- }
- }
- type AuditEvent struct {
- ID int64
- Actor string
- Action string
- EntityType string
- EntityID string
- Outcome string
- RemoteAddr string
- Details json.RawMessage
- CreatedAt time.Time
- }
- type AuditFilter struct {
- Actor string
- Action string
- EntityType string
- EntityID string
- Since time.Time
- Until time.Time
- BeforeID int64
- Limit int
- }
- type LogEvent struct {
- ID int64
- Time time.Time
- Level string
- Message string
- Caller string
- Fields json.RawMessage
- }
- type LogFilter struct {
- Level string
- Since time.Time
- Until time.Time
- BeforeID int64
- Limit int
- }
- type CardPolicy struct {
- ICCID string
- NetworkEnabled bool
- VoWiFiEnabled bool
- AirplaneEnabled bool
- APN string
- IPVersion string
- Source string
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- type TrafficBucket struct {
- DeviceID string
- Bucket string
- PeriodStart time.Time
- RXBytes int64
- TXBytes int64
- }
- func (value TrafficBucket) TotalBytes() int64 {
- return value.RXBytes + value.TXBytes
- }
- type TrafficFilter struct {
- DeviceID string
- Bucket string
- Since time.Time
- Until time.Time
- Limit int
- }
- func normalizeJSONObject(value json.RawMessage) (json.RawMessage, error) {
- if len(bytes.TrimSpace(value)) == 0 {
- return json.RawMessage(`{}`), nil
- }
- document, err := decodeJSONObject(value)
- if err != nil {
- return nil, err
- }
- normalized, err := json.Marshal(document)
- if err != nil {
- return nil, err
- }
- return normalized, nil
- }
- func normalizeJSONValue(value json.RawMessage) (json.RawMessage, error) {
- if len(bytes.TrimSpace(value)) == 0 {
- return json.RawMessage(`null`), nil
- }
- if !json.Valid(value) {
- return nil, errors.New("invalid JSON value")
- }
- return append(json.RawMessage(nil), value...), nil
- }
- func decodeJSONObject(value json.RawMessage) (map[string]any, error) {
- var document map[string]any
- decoder := json.NewDecoder(bytes.NewReader(value))
- decoder.UseNumber()
- if err := decoder.Decode(&document); err != nil {
- return nil, err
- }
- var trailing any
- if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
- if err == nil {
- return nil, errors.New("JSON value must contain exactly one object")
- }
- return nil, err
- }
- if document == nil {
- return nil, errors.New("JSON value must be an object")
- }
- return document, nil
- }
- func redactJSONFields(value json.RawMessage, fields []string, replacement string) json.RawMessage {
- document, err := decodeJSONObject(value)
- if err != nil {
- return json.RawMessage(`{}`)
- }
- for _, field := range fields {
- if getJSONPath(document, field) != nil {
- setJSONPath(document, field, replacement)
- }
- }
- encoded, err := json.Marshal(document)
- if err != nil {
- return json.RawMessage(`{}`)
- }
- return encoded
- }
- func mergeJSONSecrets(
- incoming json.RawMessage,
- existing json.RawMessage,
- fields []string,
- ) (json.RawMessage, error) {
- next, err := decodeJSONObject(incoming)
- if err != nil {
- return nil, err
- }
- current, err := decodeJSONObject(existing)
- if err != nil {
- current = map[string]any{}
- }
- for _, field := range fields {
- value := getJSONPath(next, field)
- text, stringValue := value.(string)
- if value == nil || (stringValue && (text == "" || text == SecretMask)) {
- if previous := getJSONPath(current, field); previous != nil {
- setJSONPath(next, field, previous)
- }
- }
- }
- return json.Marshal(next)
- }
- func getJSONPath(document map[string]any, path string) any {
- if strings.TrimSpace(path) == "" {
- return nil
- }
- parts := strings.Split(path, ".")
- var current any = document
- for _, part := range parts {
- object, ok := current.(map[string]any)
- if !ok {
- return nil
- }
- current, ok = object[part]
- if !ok {
- return nil
- }
- }
- return current
- }
- func setJSONPath(document map[string]any, path string, value any) {
- if strings.TrimSpace(path) == "" {
- return
- }
- parts := strings.Split(path, ".")
- current := document
- for _, part := range parts[:len(parts)-1] {
- next, ok := current[part].(map[string]any)
- if !ok {
- next = map[string]any{}
- current[part] = next
- }
- current = next
- }
- current[parts[len(parts)-1]] = value
- }
- func boolInt(value bool) int {
- if value {
- return 1
- }
- return 0
- }
- func nullableBool(value *bool) any {
- if value == nil {
- return nil
- }
- return boolInt(*value)
- }
- func nullableInt(value *int) any {
- if value == nil {
- return nil
- }
- return *value
- }
|