|
|
@@ -30,6 +30,7 @@ import (
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/sys"
|
|
|
@@ -1434,9 +1435,81 @@ func (s *ServerService) GetMigration() ([]byte, string, error) {
|
|
|
return data, "x-ui.dump", nil
|
|
|
}
|
|
|
|
|
|
-func (s *ServerService) ImportDB(file multipart.File) error {
|
|
|
+// hostBoundSettingKeys are the settings that describe *this* machine rather
|
|
|
+// than the configuration being carried: where the panel and the subscription
|
|
|
+// service listen, the certificates they present, and the identity this panel
|
|
|
+// uses towards its nodes. An import that overwrites them leaves the
|
|
|
+// destination unreachable on its own address, or impersonating the source.
|
|
|
+var hostBoundSettingKeys = []string{
|
|
|
+ "webListen", "webDomain", "webPort", "webCertFile", "webKeyFile", "webBasePath",
|
|
|
+ "subListen", "subDomain", "subPort", "subCertFile", "subKeyFile", "subURI", "subJsonURI",
|
|
|
+ "secret", "panelGuid",
|
|
|
+ "nodeMtlsCaCertPem", "nodeMtlsCaKeyPem", "nodeMtlsClientCertPem",
|
|
|
+ "nodeMtlsClientKeyPem", "nodeMtlsClientCertSha256", "nodeMtlsClientCAPem",
|
|
|
+}
|
|
|
+
|
|
|
+// hostBoundSnapshot records this machine's values, and just as importantly
|
|
|
+// which keys it had no row for: an absent row means the built-in default is in
|
|
|
+// force, and leaving the imported row in place would silently adopt the source
|
|
|
+// machine's certificate path or listen address.
|
|
|
+type hostBoundSnapshot struct {
|
|
|
+ values map[string]string
|
|
|
+ present map[string]struct{}
|
|
|
+ taken bool
|
|
|
+}
|
|
|
+
|
|
|
+func captureHostBoundSettings() hostBoundSnapshot {
|
|
|
+ db := database.GetDB()
|
|
|
+ if db == nil {
|
|
|
+ return hostBoundSnapshot{}
|
|
|
+ }
|
|
|
+ var rows []model.Setting
|
|
|
+ if err := db.Model(&model.Setting{}).Where("key IN ?", hostBoundSettingKeys).Find(&rows).Error; err != nil {
|
|
|
+ logger.Warningf("Import: could not read this machine's settings, they will come from the uploaded file: %v", err)
|
|
|
+ return hostBoundSnapshot{}
|
|
|
+ }
|
|
|
+ snap := hostBoundSnapshot{
|
|
|
+ values: make(map[string]string, len(rows)),
|
|
|
+ present: make(map[string]struct{}, len(rows)),
|
|
|
+ taken: true,
|
|
|
+ }
|
|
|
+ for _, row := range rows {
|
|
|
+ snap.values[row.Key] = row.Value
|
|
|
+ snap.present[row.Key] = struct{}{}
|
|
|
+ }
|
|
|
+ return snap
|
|
|
+}
|
|
|
+
|
|
|
+func restoreHostBoundSettings(snap hostBoundSnapshot) {
|
|
|
+ if !snap.taken {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ db := database.GetDB()
|
|
|
+ if db == nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ for _, key := range hostBoundSettingKeys {
|
|
|
+ if _, had := snap.present[key]; !had {
|
|
|
+ // No row here before the import, so the default applied. Drop the
|
|
|
+ // imported row rather than inherit the source machine's value.
|
|
|
+ if err := db.Where("key = ?", key).Delete(&model.Setting{}).Error; err != nil {
|
|
|
+ logger.Warningf("Import: could not drop imported setting %q: %v", key, err)
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // The imported row may or may not exist; settings are key-value, so an
|
|
|
+ // upsert keyed on the name is the only safe write here.
|
|
|
+ if err := db.Where(model.Setting{Key: key}).
|
|
|
+ Assign(model.Setting{Value: snap.values[key]}).
|
|
|
+ FirstOrCreate(&model.Setting{}).Error; err != nil {
|
|
|
+ logger.Warningf("Import: could not restore setting %q for this machine: %v", key, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (s *ServerService) ImportDB(file multipart.File, keepHostSettings bool) error {
|
|
|
if database.IsPostgres() {
|
|
|
- return s.importPostgresDB(file)
|
|
|
+ return s.importPostgresDB(file, keepHostSettings)
|
|
|
}
|
|
|
kind, err := sniffUploadKind(file)
|
|
|
if err != nil {
|
|
|
@@ -1488,6 +1561,11 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|
|
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
|
|
|
}
|
|
|
|
|
|
+ var keptSettings hostBoundSnapshot
|
|
|
+ if keepHostSettings {
|
|
|
+ keptSettings = captureHostBoundSettings()
|
|
|
+ }
|
|
|
+
|
|
|
if errClose := database.CloseDB(); errClose != nil {
|
|
|
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
|
|
|
}
|
|
|
@@ -1543,6 +1621,8 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|
|
}
|
|
|
dbReopened = true
|
|
|
|
|
|
+ restoreHostBoundSettings(keptSettings)
|
|
|
+
|
|
|
s.inboundService.MigrateDB()
|
|
|
|
|
|
xrayStopped = false
|
|
|
@@ -1697,14 +1777,14 @@ func sniffUploadKind(file multipart.File) (int, error) {
|
|
|
return sniffImportKind(header[:n]), nil
|
|
|
}
|
|
|
|
|
|
-func (s *ServerService) importPostgresDB(file multipart.File) error {
|
|
|
+func (s *ServerService) importPostgresDB(file multipart.File, keepHostSettings bool) error {
|
|
|
kind, err := sniffUploadKind(file)
|
|
|
if err != nil {
|
|
|
return common.NewErrorf("Error reading uploaded file: %v", err)
|
|
|
}
|
|
|
switch kind {
|
|
|
case importKindPgDump:
|
|
|
- return s.restorePostgresDump(file)
|
|
|
+ return s.restorePostgresDump(file, keepHostSettings)
|
|
|
case importKindSQLiteDB:
|
|
|
return s.migrateSQLiteIntoPostgres(file, false)
|
|
|
case importKindSQLiteDump:
|
|
|
@@ -1714,7 +1794,7 @@ func (s *ServerService) importPostgresDB(file multipart.File) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
|
|
+func (s *ServerService) restorePostgresDump(file multipart.File, keepHostSettings bool) error {
|
|
|
bin, err := exec.LookPath("pg_restore")
|
|
|
if err != nil {
|
|
|
return common.NewError("pg_restore not found on the server; install the postgresql-client package to restore a PostgreSQL database")
|
|
|
@@ -1754,6 +1834,11 @@ func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
|
|
logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
|
|
|
}
|
|
|
|
|
|
+ var keptSettings hostBoundSnapshot
|
|
|
+ if keepHostSettings {
|
|
|
+ keptSettings = captureHostBoundSettings()
|
|
|
+ }
|
|
|
+
|
|
|
if errClose := database.CloseDB(); errClose != nil {
|
|
|
logger.Warningf("Failed to close existing DB before restore: %v", errClose)
|
|
|
}
|
|
|
@@ -1770,6 +1855,8 @@ func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
|
|
if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
|
|
|
return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
|
|
|
}
|
|
|
+ restoreHostBoundSettings(keptSettings)
|
|
|
+
|
|
|
s.inboundService.MigrateDB()
|
|
|
|
|
|
if runErr != nil {
|