package initialize import ( "context" "encoding/json" "kra/internal/conf" "kra/internal/utils/configutil" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" ) type configurationEnvelope struct { Data json.RawMessage `json:"data"` Admin json.RawMessage `json:"admin"` Email *struct { To string `json:"to"` From string `json:"from"` Host string `json:"host"` Secret string `json:"secret"` Nickname string `json:"nickname"` Port int32 `json:"port"` IsSSL bool `json:"is-ssl"` IsLoginAuth bool `json:"is-loginauth"` } `json:"email"` } func (r *Repo) ConfigurationJSON() (json.RawMessage, error) { dataConfig, adminConfig := r.backend.RuntimeValues() admin := map[string]any{"routerPrefix": ""} email := map[string]any{} if adminConfig != nil { admin["routerPrefix"] = adminConfig.RouterPrefix if adminConfig.System != nil { admin["system"] = map[string]any{ "useRedis": adminConfig.System.UseRedis, "useMultipoint": adminConfig.System.UseMultipoint, "useStrictAuth": adminConfig.System.UseStrictAuth, "disableAutoMigrate": adminConfig.System.DisableAutoMigrate, "useMongo": adminConfig.System.UseMongo, "addr": adminConfig.System.Addr, "iplimitCount": adminConfig.System.IplimitCount, "iplimitTime": adminConfig.System.IplimitTime, } } if adminConfig.Jwt != nil { admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer} } if adminConfig.Captcha != nil { admin["captcha"] = map[string]any{"keyLong": adminConfig.Captcha.KeyLong, "imgWidth": adminConfig.Captcha.ImgWidth, "imgHeight": adminConfig.Captcha.ImgHeight, "storeExpiration": durationString(adminConfig.Captcha.StoreExpiration)} } if adminConfig.Local != nil { admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix} } if adminConfig.Media != nil { chunkDir := adminConfig.Media.ChunkDir if chunkDir == "" { chunkDir = "uploads/chunks" } admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize, "chunkDir": chunkDir} } if adminConfig.Email != nil { secret := "" if adminConfig.Email.Secret != "" { secret = "******" } email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": secret, "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth} } if adminConfig.Storage != nil { storage := proto.Clone(adminConfig.Storage).(*conf.AdminBackend_Storage) maskStorageSecrets(storage) admin["storage"] = storage } disks := make([]map[string]any, 0, len(adminConfig.DiskList)) for _, disk := range adminConfig.DiskList { if disk != nil { disks = append(disks, map[string]any{"mount_point": disk.MountPoint}) } } admin["disk_list"] = disks if adminConfig.Zap != nil { admin["zap"] = adminConfig.Zap } if adminConfig.Cors != nil { admin["cors"] = adminConfig.Cors } if adminConfig.App != nil { admin["app"] = adminConfig.App } } // Never mask secrets on the live runtime object. ConfigurationJSON is a // read-only operation; mutating dataConfig here would replace the actual // database/Redis/Mongo credentials with "******" and make the next reload // fail. Work on a clone, just as we already do for adminConfig. safeData := cloneDataConfig(dataConfig) maskDataSecrets(safeData) safeAdmin := cloneAdminConfig(adminConfig) if safeAdmin != nil { if safeAdmin.Jwt != nil { safeAdmin.Jwt.SigningKey = "******" } if safeAdmin.Email != nil && safeAdmin.Email.Secret != "" { safeAdmin.Email.Secret = "******" } maskStorageSecrets(safeAdmin.Storage) } dataMap := map[string]any{} if safeData != nil { raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(safeData) if err != nil { return nil, err } if err = json.Unmarshal(raw, &dataMap); err != nil { return nil, err } } config := compatibleConfiguration(safeData, safeAdmin) config["admin"], config["email"], config["data"] = admin, email, dataMap return json.Marshal(map[string]any{"config": config}) } func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error { currentData, currentAdmin := r.backend.RuntimeValues() if currentAdmin == nil { return nil } nextData := cloneDataConfig(currentData) nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend) if err := applyCompatibleConfiguration(raw, nextData, nextAdmin); err != nil { return err } var value configurationEnvelope if err := json.Unmarshal(raw, &value); err != nil { return err } options := protojson.UnmarshalOptions{DiscardUnknown: true} if len(value.Data) > 0 && string(value.Data) != "null" { if err := configutil.MergeProtoJSON(nextData, value.Data, options); err != nil { return err } } if len(value.Admin) > 0 && string(value.Admin) != "null" { if err := configutil.MergeProtoJSON(nextAdmin, value.Admin, options); err != nil { return err } } preserveDataSecrets(nextData, currentData) preserveAdminSecrets(nextAdmin, currentAdmin) if value.Email != nil { if nextAdmin.Email == nil { nextAdmin.Email = &conf.AdminBackend_Email{} } nextAdmin.Email.To, nextAdmin.Email.From, nextAdmin.Email.Host = value.Email.To, value.Email.From, value.Email.Host nextAdmin.Email.Nickname, nextAdmin.Email.Port = value.Email.Nickname, value.Email.Port nextAdmin.Email.IsSsl, nextAdmin.Email.IsLoginAuth = value.Email.IsSSL, value.Email.IsLoginAuth if value.Email.Secret != "" && value.Email.Secret != "******" { nextAdmin.Email.Secret = value.Email.Secret } } if err := r.backend.RefreshDatabaseSources(nextData); err != nil { return err } nextAdmin.ConfigPath = currentAdmin.ConfigPath dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData) if err != nil { return err } adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextAdmin) if err != nil { return err } return r.PersistRuntimeConfig(ctx, dataRaw, adminRaw) } func (r *Repo) DiskMountPoints() []string { config := r.backend.RuntimeAdmin() if config == nil { return nil } points := make([]string, 0, len(config.DiskList)) for _, item := range config.DiskList { if item != nil && item.MountPoint != "" { points = append(points, item.MountPoint) } } return points } func cloneDataConfig(value *conf.Data) *conf.Data { if value == nil { return &conf.Data{} } return proto.Clone(value).(*conf.Data) } func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend { if value == nil { return &conf.AdminBackend{} } return proto.Clone(value).(*conf.AdminBackend) } func durationString(value *durationpb.Duration) string { if value == nil { return "0s" } return value.AsDuration().String() } func maskDataSecrets(value *conf.Data) { if value == nil { return } if value.Database != nil { value.Database.Password = "******" // Source may contain an embedded password (for example a MySQL DSN). // The management response must never expose it. Structured database // fields are retained and refreshDatabaseSources rebuilds Source when // the configuration is saved. value.Database.Source = "" } if value.Redis != nil { value.Redis.Password = "******" } if value.Mongo != nil { value.Mongo.Password = "******" } for _, item := range value.DatabaseList { if item != nil { item.Password = "******" item.Source = "" } } for _, item := range value.RedisList { if item != nil { item.Password = "******" } } } func preserveDataSecrets(next, current *conf.Data) { if next == nil || current == nil { return } if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) { next.Database.Password = current.Database.Password } if next.Database != nil && current.Database != nil && next.Database.Source == "" { next.Database.Source = current.Database.Source } if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) { next.Redis.Password = current.Redis.Password } if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) { next.Mongo.Password = current.Mongo.Password } currentDatabases := make(map[string]*conf.Data_Database, len(current.DatabaseList)) for _, item := range current.DatabaseList { if item != nil && item.AliasName != "" { currentDatabases[item.AliasName] = item } } for i, item := range next.DatabaseList { var previous *conf.Data_Database if item != nil && item.AliasName != "" { previous = currentDatabases[item.AliasName] } if previous == nil && i < len(current.DatabaseList) { previous = current.DatabaseList[i] } if item != nil && previous != nil { if maskedSecret(item.Password) { item.Password = previous.Password } if item.Source == "" { item.Source = previous.Source } } } currentRedis := make(map[string]*conf.Data_Redis, len(current.RedisList)) for _, item := range current.RedisList { if item != nil && item.Name != "" { currentRedis[item.Name] = item } } for i, item := range next.RedisList { var previous *conf.Data_Redis if item != nil && item.Name != "" { previous = currentRedis[item.Name] } if previous == nil && i < len(current.RedisList) { previous = current.RedisList[i] } if item != nil && previous != nil && maskedSecret(item.Password) { item.Password = previous.Password } } } func preserveAdminSecrets(next, current *conf.AdminBackend) { if next.Jwt != nil && current.Jwt != nil && maskedSecret(next.Jwt.SigningKey) { next.Jwt.SigningKey = current.Jwt.SigningKey } if next.Email != nil && current.Email != nil && maskedSecret(next.Email.Secret) { next.Email.Secret = current.Email.Secret } preserveStorageSecrets(next.Storage, current.Storage) } func maskedSecret(value string) bool { return value == "" || value == "******" } func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore { if storage == nil { return nil } return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio} } func maskStorageSecrets(storage *conf.AdminBackend_Storage) { if storage == nil { return } if storage.Qiniu != nil && storage.Qiniu.SecretKey != "" { storage.Qiniu.SecretKey = "******" } for _, item := range objectStores(storage) { if item != nil && item.SecretKey != "" { item.SecretKey = "******" } } } func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) { if next == nil || current == nil { return } if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) { next.Qiniu.SecretKey = current.Qiniu.SecretKey } nextItems, currentItems := objectStores(next), objectStores(current) for i := range nextItems { if nextItems[i] != nil && currentItems[i] != nil && maskedSecret(nextItems[i].SecretKey) { nextItems[i].SecretKey = currentItems[i].SecretKey } } }