Files
VoCat/internal/store/models.go
T
MengMengCode 22487dbb1f Add PC/SC backend support for USB SIM readers
- Implemented a new `unsupportedBackend` in `backend_stub.go` to handle unsupported platforms.
- Created a `Service` struct in `service.go` to manage interactions with smart cards, including session management and identity reading.
- Added methods for reading identity, checking readiness, and authenticating with USIM applications.
- Introduced a `scriptedCard` for testing purposes in `service_test.go` to simulate card responses.
- Defined necessary types and error handling in `types.go` for better clarity and usability.
- Developed a `PCSCAdapter` in `vowifi/pcsc_adapter.go` to integrate PC/SC service with Wi-Fi calling functionalities.
- Added a new SVG icon for USB SIM readers in `public/sim-reader.svg`.
- Implemented tests to ensure correct functionality and error handling in various scenarios.
2026-08-12 02:33:32 +08:00

708 lines
16 KiB
Go

package store
import (
"bytes"
"encoding/json"
"errors"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
const SecretMask = "********"
type Device struct {
ID string
Name string
DeviceType string
Interface string
ControlDevice string
ATPort string
USBPath string
AudioDevice string
ModemIMEI string
SIMPIN 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 AutomaticTask struct {
ID int64 `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
DeviceID string `json:"device_id"`
ProfileICCID string `json:"profile_iccid"`
ProfileAID string `json:"profile_aid"`
TaskType string `json:"task_type"`
Environment string `json:"environment"`
IntervalDays int `json:"interval_days"`
StartDate string `json:"start_date"`
RunTime string `json:"run_time"`
Timezone string `json:"timezone"`
Payload json.RawMessage `json:"payload"`
RetryCount int `json:"retry_count"`
Notify bool `json:"notify"`
NextRunAt time.Time `json:"next_run_at"`
LastRunAt time.Time `json:"last_run_at,omitempty"`
LastStatus string `json:"last_status"`
LastError string `json:"last_error"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AutomaticTaskRun struct {
ID int64 `json:"id"`
TaskID int64 `json:"task_id"`
DeviceID string `json:"device_id"`
ScheduledAt time.Time `json:"scheduled_at"`
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
Status string `json:"status"`
Attempts int `json:"attempts"`
Output string `json:"output"`
Error string `json:"error"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SMSMessage struct {
ID int64
MessageID string
DeviceID string
ModemIMEI 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
ModemIMEI 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
ModemIMEI 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
ModemIMEI 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 for exactly one eSIM profile.
// ICCID is globally unique, while one proxy may serve profiles on many devices.
type DeviceProxyBinding struct {
DeviceID string
ICCID string
ProfileName 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 {
collectJSONStringValues(getJSONPath(document, field), &values)
}
return uniqueNonemptyStrings(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
CustomPhoneNumber string
Source string
CreatedAt time.Time
UpdatedAt time.Time
}
type CardAPNProfile struct {
ID int64
ICCID string
APN string
Username string
Password string
Proxy string
MCC string
MNC string
IPVersion string
RoamingIPVersion string
AuthType 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 current := getJSONPath(document, field); current != nil {
setJSONPath(document, field, redactJSONValue(current, 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)
if previous := getJSONPath(current, field); previous != nil {
setJSONPath(next, field, mergeJSONSecretValue(value, previous))
}
}
return json.Marshal(next)
}
func redactJSONValue(value any, replacement string) any {
switch typed := value.(type) {
case string:
return replacement
case []any:
result := make([]any, len(typed))
for index, item := range typed {
result[index] = redactJSONValue(item, replacement)
}
return result
default:
return replacement
}
}
func mergeJSONSecretValue(incoming, existing any) any {
if incoming == nil {
return existing
}
switch next := incoming.(type) {
case string:
if next == "" || next == SecretMask {
return existing
}
case []any:
previous, ok := existing.([]any)
if !ok {
return incoming
}
merged := make([]any, len(next))
for index, value := range next {
if index < len(previous) {
merged[index] = mergeJSONSecretValue(value, previous[index])
} else {
merged[index] = value
}
}
return merged
}
return incoming
}
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
}