diff --git a/config/config.go b/config/config.go index 7973b655..2c275530 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,9 @@ var version string //go:embed name var name string +//go:embed default_xray.json +var defaultXrayTemplate string + type LogLevel string const ( @@ -86,3 +89,7 @@ func GetDBFolderPath() string { func GetDBPath() string { return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) } + +func GetDefaultXrayTemplate() string { + return defaultXrayTemplate +} diff --git a/web/service/config.json b/config/default_xray.json similarity index 99% rename from web/service/config.json rename to config/default_xray.json index e16b9f4c..72b1c16b 100644 --- a/web/service/config.json +++ b/config/default_xray.json @@ -39,4 +39,4 @@ "rules": [] }, "stats": {} -} \ No newline at end of file +} diff --git a/database/db.go b/database/db.go index c048d9c7..7647f0aa 100644 --- a/database/db.go +++ b/database/db.go @@ -8,6 +8,7 @@ import ( "path" "github.com/alireza0/x-ui/config" + "github.com/alireza0/x-ui/database/migrations" "github.com/alireza0/x-ui/database/model" "github.com/alireza0/x-ui/util/common" "github.com/alireza0/x-ui/xray" @@ -77,7 +78,7 @@ func InitDB(dbPath string) error { return err } - return nil + return migrations.Run(db) } func CloseDB() error { diff --git a/database/migrations/helpers.go b/database/migrations/helpers.go new file mode 100644 index 00000000..c7f4f267 --- /dev/null +++ b/database/migrations/helpers.go @@ -0,0 +1,5 @@ +package migrations + +func removeIndex(s []interface{}, index int) []interface{} { + return append(s[:index], s[index+1:]...) +} diff --git a/database/migrations/migrations.go b/database/migrations/migrations.go new file mode 100644 index 00000000..b8da901f --- /dev/null +++ b/database/migrations/migrations.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "gorm.io/gorm" +) + +const ( + VersionOutbound = 2 + VersionRouting = 3 + VersionInbound = 4 +) + +type migration struct { + version int + fn func(*gorm.DB) error +} + +var registry = []migration{ + {VersionOutbound, migrateV002Outbound}, + {VersionRouting, migrateV003Routing}, + {VersionInbound, migrateV004Inbound}, +} + +func Run(db *gorm.DB) error { + if err := ensureSchemaVersionTable(db); err != nil { + return err + } + for _, m := range registry { + applied, err := isVersionApplied(db, m.version) + if err != nil { + return err + } + if applied { + continue + } + if err := m.fn(db); err != nil { + return err + } + if err := recordVersion(db, m.version); err != nil { + return err + } + } + return nil +} + +func ensureSchemaVersionTable(db *gorm.DB) error { + return db.Exec(`CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY + )`).Error +} + +func isVersionApplied(db *gorm.DB, version int) (bool, error) { + var count int64 + err := db.Raw(`SELECT COUNT(*) FROM schema_version WHERE version = ?`, version).Scan(&count).Error + return count > 0, err +} + +func recordVersion(db *gorm.DB, version int) error { + return db.Exec(`INSERT INTO schema_version (version) VALUES (?)`, version).Error +} diff --git a/database/migrations/settings.go b/database/migrations/settings.go new file mode 100644 index 00000000..28d83646 --- /dev/null +++ b/database/migrations/settings.go @@ -0,0 +1,38 @@ +package migrations + +import ( + "github.com/alireza0/x-ui/config" + "github.com/alireza0/x-ui/database/model" + + "gorm.io/gorm" +) + +const xrayTemplateConfigKey = "xrayTemplateConfig" + +func getXrayTemplate(db *gorm.DB) (string, error) { + setting := &model.Setting{} + err := db.Model(model.Setting{}).Where("key = ?", xrayTemplateConfigKey).First(setting).Error + if err == gorm.ErrRecordNotFound { + return config.GetDefaultXrayTemplate(), nil + } + if err != nil { + return "", err + } + return setting.Value, nil +} + +func saveXrayTemplate(db *gorm.DB, value string) error { + setting := &model.Setting{} + err := db.Model(model.Setting{}).Where("key = ?", xrayTemplateConfigKey).First(setting).Error + if err == gorm.ErrRecordNotFound { + return db.Create(&model.Setting{ + Key: xrayTemplateConfigKey, + Value: value, + }).Error + } + if err != nil { + return err + } + setting.Value = value + return db.Save(setting).Error +} diff --git a/database/migrations/v002_outbound.go b/database/migrations/v002_outbound.go new file mode 100644 index 00000000..0ea271c7 --- /dev/null +++ b/database/migrations/v002_outbound.go @@ -0,0 +1,143 @@ +package migrations + +import ( + "encoding/json" + + "github.com/alireza0/x-ui/database/model" + "github.com/alireza0/x-ui/logger" + + "gorm.io/gorm" +) + +func migrateV002Outbound(db *gorm.DB) error { + var count int64 + if err := db.Model(&model.Outbound{}).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + + if err := migrateOutboundsFromTemplate(db); err != nil { + return err + } + + if err := db.Model(&model.Outbound{}).Count(&count).Error; err != nil { + return err + } + if count == 0 { + initDefaultOutbounds(db) + } + return nil +} + +func migrateOutboundsFromTemplate(db *gorm.DB) error { + templateConfig, err := getXrayTemplate(db) + if err != nil { + logger.Warning("outbound migration: get xray template failed:", err) + return nil + } + + var cfg map[string]interface{} + if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil { + logger.Warning("outbound migration: parse template failed:", err) + return nil + } + + rawOutbounds, ok := cfg["outbounds"].([]interface{}) + if !ok || len(rawOutbounds) == 0 { + return nil + } + + migrated := 0 + for i, item := range rawOutbounds { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + outbound := outboundFromMap(raw, i) + if outbound.Tag == "" || outbound.Protocol == "" { + continue + } + if err := db.Create(outbound).Error; err != nil { + logger.Warning("outbound migration: create failed:", err) + continue + } + migrated++ + } + if migrated == 0 { + return nil + } + + cfg["outbounds"] = []interface{}{} + newTemplate, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + logger.Warning("outbound migration: marshal template failed:", err) + return nil + } + if err := saveXrayTemplate(db, string(newTemplate)); err != nil { + logger.Warning("outbound migration: save template failed:", err) + return nil + } + logger.Info("Migrated", migrated, "outbound(s) from xray settings to database") + return nil +} + +func initDefaultOutbounds(db *gorm.DB) { + for _, outbound := range defaultOutbounds() { + if err := db.Create(outbound).Error; err != nil { + logger.Warning("outbound init: create default failed:", err) + } + } + logger.Info("Initialized default outbound(s): direct, blocked") +} + +func outboundFromMap(raw map[string]interface{}, sort int) *model.Outbound { + o := &model.Outbound{Sort: sort} + if v, ok := raw["sendThrough"].(string); ok { + o.SendThrough = v + } + if v, ok := raw["protocol"].(string); ok { + o.Protocol = v + } + if v, ok := raw["tag"].(string); ok { + o.Tag = v + } + if v, ok := raw["targetStrategy"].(string); ok { + o.TargetStrategy = v + } + if v, ok := raw["settings"]; ok && v != nil { + b, _ := json.MarshalIndent(v, "", " ") + o.Settings = string(b) + } + if v, ok := raw["streamSettings"]; ok && v != nil { + b, _ := json.MarshalIndent(v, "", " ") + o.StreamSettings = string(b) + } + if v, ok := raw["proxySettings"]; ok && v != nil { + b, _ := json.MarshalIndent(v, "", " ") + o.ProxySettings = string(b) + } + if v, ok := raw["mux"]; ok && v != nil { + b, _ := json.MarshalIndent(v, "", " ") + o.Mux = string(b) + } + return o +} + +func defaultOutbounds() []*model.Outbound { + return []*model.Outbound{ + { + Sort: 0, + Protocol: "freedom", + Tag: "direct", + Settings: `{"domainStrategy":"UseIP","noises":[],"redirect":""}`, + }, + { + Sort: 1, + Protocol: "blackhole", + Tag: "blocked", + Settings: `{}`, + }, + } +} diff --git a/database/migrations/v003_routing.go b/database/migrations/v003_routing.go new file mode 100644 index 00000000..6b1f6238 --- /dev/null +++ b/database/migrations/v003_routing.go @@ -0,0 +1,224 @@ +package migrations + +import ( + "encoding/json" + "fmt" + + "github.com/alireza0/x-ui/config" + "github.com/alireza0/x-ui/database/model" + "github.com/alireza0/x-ui/logger" + + "gorm.io/gorm" +) + +func migrateV003Routing(db *gorm.DB) error { + templateConfig, err := getXrayTemplate(db) + if err != nil { + logger.Warning("routing rule migration: get xray template failed:", err) + return nil + } + + var cfg map[string]interface{} + if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil { + logger.Warning("routing rule migration: parse template failed:", err) + return nil + } + + changed := migrateLegacyApiConfig(cfg) + if changed { + newTemplate, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + logger.Warning("routing rule migration: marshal legacy api config failed:", err) + return nil + } + if err := saveXrayTemplate(db, string(newTemplate)); err != nil { + logger.Warning("routing rule migration: save legacy api config failed:", err) + return nil + } + logger.Info("Migrated legacy api inbound to api.listen in xray settings") + } + + var count int64 + if err := db.Model(&model.RoutingRule{}).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + + routing, ok := cfg["routing"].(map[string]interface{}) + if !ok { + return nil + } + rawRules, ok := routing["rules"].([]interface{}) + if !ok || len(rawRules) == 0 { + return nil + } + + for i, item := range rawRules { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + rule := routingRuleFromMap(raw, i) + if err := db.Create(rule).Error; err != nil { + logger.Warning("routing rule migration: create failed:", err) + } + } + + routing["rules"] = []interface{}{} + cfg["routing"] = routing + newTemplate, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + logger.Warning("routing rule migration: marshal template failed:", err) + return nil + } + if err := saveXrayTemplate(db, string(newTemplate)); err != nil { + logger.Warning("routing rule migration: save template failed:", err) + return nil + } + logger.Info("Migrated", len(rawRules), "routing rule(s) from xray settings to database") + return nil +} + +func apiListenMissing(api map[string]interface{}) bool { + _, ok := api["listen"] + return !ok +} + +func defaultApiListen() string { + var cfg map[string]interface{} + if err := json.Unmarshal([]byte(config.GetDefaultXrayTemplate()), &cfg); err != nil { + return "127.0.0.1:62789" + } + api, ok := cfg["api"].(map[string]interface{}) + if !ok { + return "127.0.0.1:62789" + } + s, _ := api["listen"].(string) + if s == "" { + return "127.0.0.1:62789" + } + return s +} + +func inboundListenAddress(inbound map[string]interface{}) string { + port := 0 + switch p := inbound["port"].(type) { + case float64: + port = int(p) + case int: + port = p + case int64: + port = int(p) + } + if port == 0 { + return "" + } + + host := "127.0.0.1" + if listen, ok := inbound["listen"]; ok { + switch v := listen.(type) { + case string: + if v != "" { + host = v + } + case []interface{}: + if len(v) > 0 { + if s, ok := v[0].(string); ok && s != "" { + host = s + } + } + } + } + return fmt.Sprintf("%s:%d", host, port) +} + +func isLegacyApiRoutingRule(rule map[string]interface{}) bool { + outboundTag, _ := rule["outboundTag"].(string) + if outboundTag != "api" { + return false + } + val, ok := rule["inboundTag"] + if !ok { + return false + } + switch v := val.(type) { + case []interface{}: + return len(v) == 1 && v[0] == "api" + case []string: + return len(v) == 1 && v[0] == "api" + default: + return false + } +} + +func migrateLegacyApiConfig(cfg map[string]interface{}) bool { + api, ok := cfg["api"].(map[string]interface{}) + if !ok || !apiListenMissing(api) { + return false + } + + var listenAddr string + inbounds, _ := cfg["inbounds"].([]interface{}) + + apiInboundIndex := -1 + var apiInbound map[string]interface{} + for i, item := range inbounds { + inbound, ok := item.(map[string]interface{}) + if !ok { + continue + } + tag, _ := inbound["tag"].(string) + protocol, _ := inbound["protocol"].(string) + if tag == "api" && protocol == "dokodemo-door" { + apiInboundIndex = i + apiInbound = inbound + break + } + } + + if apiInboundIndex >= 0 { + listenAddr = inboundListenAddress(apiInbound) + if listenAddr == "" { + listenAddr = defaultApiListen() + } + cfg["inbounds"] = removeIndex(inbounds, apiInboundIndex) + + if routing, ok := cfg["routing"].(map[string]interface{}); ok { + if rawRules, ok := routing["rules"].([]interface{}); ok { + for i, item := range rawRules { + rule, ok := item.(map[string]interface{}) + if !ok { + continue + } + if isLegacyApiRoutingRule(rule) { + routing["rules"] = removeIndex(rawRules, i) + cfg["routing"] = routing + break + } + } + } + } + } else { + listenAddr = defaultApiListen() + } + + api["listen"] = listenAddr + cfg["api"] = api + return true +} + +func routingRuleFromMap(raw map[string]interface{}, index int) *model.RoutingRule { + tag, _ := raw["ruleTag"].(string) + delete(raw, "ruleTag") + if tag == "" { + tag = fmt.Sprintf("migrated-rule-%d", index) + } + b, _ := json.Marshal(raw) + return &model.RoutingRule{ + Tag: tag, + Sort: index, + RawJson: string(b), + } +} diff --git a/database/migrations/v004_inbound.go b/database/migrations/v004_inbound.go new file mode 100644 index 00000000..5514c141 --- /dev/null +++ b/database/migrations/v004_inbound.go @@ -0,0 +1,190 @@ +package migrations + +import ( + "encoding/json" + + "github.com/alireza0/x-ui/database/model" + "github.com/alireza0/x-ui/logger" + "github.com/alireza0/x-ui/xray" + + "gorm.io/gorm" +) + +func migrateV004Inbound(db *gorm.DB) error { + if err := migrationInboundRequirements(db); err != nil { + return err + } + migrationRemoveOrphanedTraffics(db) + return nil +} + +func migrationInboundRequirements(db *gorm.DB) error { + tx := db.Begin() + var err error + defer func() { + if err == nil { + tx.Commit() + if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil { + logger.Warningf("VACUUM failed: %v", dbErr) + } + } else { + tx.Rollback() + } + }() + + var inbounds []*model.Inbound + err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + for inboundIndex := range inbounds { + settings := map[string]interface{}{} + json.Unmarshal([]byte(inbounds[inboundIndex].Settings), &settings) + clients, ok := settings["clients"].([]interface{}) + if ok { + var newClients []interface{} + for clientIndex := range clients { + c := clients[clientIndex].(map[string]interface{}) + + if _, ok := c["email"]; !ok { + c["email"] = "" + } + + if _, ok := c["flow"]; ok { + if c["flow"] == "xtls-rprx-direct" { + c["flow"] = "" + } + } + newClients = append(newClients, interface{}(c)) + } + settings["clients"] = newClients + modifiedSettings, marshalErr := json.MarshalIndent(settings, "", " ") + if marshalErr != nil { + err = marshalErr + return err + } + + inbounds[inboundIndex].Settings = string(modifiedSettings) + } + + modelClients := parseInboundClients(inbounds[inboundIndex].Settings) + for _, modelClient := range modelClients { + if len(modelClient.Email) > 0 { + var count int64 + tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) + if count == 0 { + if addErr := addClientStat(tx, inbounds[inboundIndex].Id, &modelClient); addErr != nil { + err = addErr + return err + } + } + } + } + } + if saveErr := tx.Save(inbounds).Error; saveErr != nil { + err = saveErr + return err + } + + tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) + + var externalProxy []struct { + Id int + Port int + StreamSettings []byte + } + err = tx.Raw(`select id, port, stream_settings + from inbounds + WHERE protocol in ('vmess','vless','trojan') + AND json_extract(stream_settings, '$.security') = 'tls' + AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error + if err != nil || len(externalProxy) == 0 { + return nil + } + + for _, ep := range externalProxy { + var reverses interface{} + var stream map[string]interface{} + json.Unmarshal(ep.StreamSettings, &stream) + if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok { + if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok { + if domains, ok := settings["domains"].([]interface{}); ok { + for _, domain := range domains { + if domainMap, ok := domain.(map[string]interface{}); ok { + domainMap["forceTls"] = "same" + domainMap["port"] = ep.Port + domainMap["dest"] = domainMap["domain"].(string) + delete(domainMap, "domain") + } + } + } + reverses = settings["domains"] + delete(settings, "domains") + } + } + stream["externalProxy"] = reverses + newStream, _ := json.MarshalIndent(stream, " ", " ") + tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) + } + return nil +} + +func migrationRemoveOrphanedTraffics(db *gorm.DB) { + db.Exec(` + DELETE FROM client_traffics + WHERE email NOT IN ( + SELECT JSON_EXTRACT(client.value, '$.email') + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + ) + `) +} + +func parseInboundClients(settingsJSON string) []model.Client { + settings := map[string]interface{}{} + if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil { + return nil + } + raw, ok := settings["clients"] + if !ok || raw == nil { + return nil + } + + switch v := raw.(type) { + case []interface{}: + b, err := json.Marshal(v) + if err != nil { + return nil + } + var clients []model.Client + if json.Unmarshal(b, &clients) != nil { + return nil + } + return clients + case string: + if v == "" { + return nil + } + var clients []model.Client + if json.Unmarshal([]byte(v), &clients) != nil { + return nil + } + return clients + default: + return nil + } +} + +func addClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { + clientTraffic := xray.ClientTraffic{ + InboundId: inboundId, + Email: client.Email, + Total: client.TotalGB, + ExpiryTime: client.ExpiryTime, + Enable: client.Enable, + Up: 0, + Down: 0, + Reset: client.Reset, + } + return tx.Create(&clientTraffic).Error +} diff --git a/main.go b/main.go index dc5194cd..5bf1e788 100644 --- a/main.go +++ b/main.go @@ -47,11 +47,6 @@ func runWebServer() { log.Fatal(err) } - outboundService := service.OutboundService{} - outboundService.MigrateDB() - routingRuleService := service.RoutingRuleService{} - routingRuleService.MigrateDB() - var server *web.Server server = web.NewServer() @@ -368,16 +363,10 @@ func getPanelURI() { } func migrateDb() { - inboundService := service.InboundService{} - outboundService := service.OutboundService{} - err := database.InitDB(config.GetDBPath()) if err != nil { log.Fatal(err) } - fmt.Println("Start migrating database...") - inboundService.MigrateDB() - outboundService.MigrateDB() fmt.Println("Migration done!") } diff --git a/web/service/inbound.go b/web/service/inbound.go index 61b1c1b2..a9441b40 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1143,18 +1143,6 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) return needRestart, count, err } -func (s *InboundService) MigrationRemoveOrphanedTraffics() { - db := database.GetDB() - db.Exec(` - DELETE FROM client_traffics - WHERE email NOT IN ( - SELECT JSON_EXTRACT(client.value, '$.email') - FROM inbounds, - JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client - ) - `) -} - func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { clientTraffic := xray.ClientTraffic{} clientTraffic.InboundId = inboundId @@ -1488,125 +1476,6 @@ func (s *InboundService) GetClientReverseTags() (string, error) { return string(result), nil } -func (s *InboundService) MigrationRequirements() { - db := database.GetDB() - tx := db.Begin() - var err error - defer func() { - if err == nil { - tx.Commit() - if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil { - logger.Warningf("VACUUM failed: %v", dbErr) - } - } else { - tx.Rollback() - } - }() - - // Fix inbounds based problems - var inbounds []*model.Inbound - err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error - if err != nil && err != gorm.ErrRecordNotFound { - return - } - for inbound_index := range inbounds { - settings := map[string]interface{}{} - json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) - clients, ok := settings["clients"].([]interface{}) - if ok { - // Fix Client configuration problems - var newClients []interface{} - for client_index := range clients { - c := clients[client_index].(map[string]interface{}) - - // Add email='' if it is not exists - if _, ok := c["email"]; !ok { - c["email"] = "" - } - - // Remove "flow": "xtls-rprx-direct" - if _, ok := c["flow"]; ok { - if c["flow"] == "xtls-rprx-direct" { - c["flow"] = "" - } - } - newClients = append(newClients, interface{}(c)) - } - settings["clients"] = newClients - modifiedSettings, err := json.MarshalIndent(settings, "", " ") - if err != nil { - return - } - - inbounds[inbound_index].Settings = string(modifiedSettings) - } - - // Add client traffic row for all clients which has email - modelClients, err := s.GetClients(inbounds[inbound_index]) - if err != nil { - return - } - for _, modelClient := range modelClients { - if len(modelClient.Email) > 0 { - var count int64 - tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) - if count == 0 { - s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient) - } - } - } - } - tx.Save(inbounds) - - // Remove orphaned traffics - tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) - - // Migrate old MultiDomain to External Proxy - var externalProxy []struct { - Id int - Port int - StreamSettings []byte - } - err = tx.Raw(`select id, port, stream_settings - from inbounds - WHERE protocol in ('vmess','vless','trojan') - AND json_extract(stream_settings, '$.security') = 'tls' - AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error - if err != nil || len(externalProxy) == 0 { - return - } - - for _, ep := range externalProxy { - var reverses interface{} - var stream map[string]interface{} - json.Unmarshal(ep.StreamSettings, &stream) - if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok { - if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok { - if domains, ok := settings["domains"].([]interface{}); ok { - for _, domain := range domains { - if domainMap, ok := domain.(map[string]interface{}); ok { - domainMap["forceTls"] = "same" - domainMap["port"] = ep.Port - domainMap["dest"] = domainMap["domain"].(string) - delete(domainMap, "domain") - } - } - } - reverses = settings["domains"] - delete(settings, "domains") - } - } - stream["externalProxy"] = reverses - newStream, _ := json.MarshalIndent(stream, " ", " ") - tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) - } -} - -func (s *InboundService) MigrateDB() { - s.MigrationRequirements() - s.MigrationRemoveOrphanedTraffics() -} - func (s *InboundService) GetOnlineClients() []string { return p.GetOnlineClients() } diff --git a/web/service/outbound.go b/web/service/outbound.go index e85d8f9b..ec350821 100644 --- a/web/service/outbound.go +++ b/web/service/outbound.go @@ -294,131 +294,3 @@ func (s *OutboundService) GetOnlineOutbounds() []string { } return p.GetOnlineOutbounds() } - -func outboundFromMap(raw map[string]interface{}, sort int) *model.Outbound { - o := &model.Outbound{Sort: sort} - if v, ok := raw["sendThrough"].(string); ok { - o.SendThrough = v - } - if v, ok := raw["protocol"].(string); ok { - o.Protocol = v - } - if v, ok := raw["tag"].(string); ok { - o.Tag = v - } - if v, ok := raw["targetStrategy"].(string); ok { - o.TargetStrategy = v - } - if v, ok := raw["settings"]; ok && v != nil { - b, _ := json.Marshal(v) - o.Settings = string(b) - } - if v, ok := raw["streamSettings"]; ok && v != nil { - b, _ := json.Marshal(v) - o.StreamSettings = string(b) - } - if v, ok := raw["proxySettings"]; ok && v != nil { - b, _ := json.Marshal(v) - o.ProxySettings = string(b) - } - if v, ok := raw["mux"]; ok && v != nil { - b, _ := json.Marshal(v) - o.Mux = string(b) - } - return o -} - -func defaultOutbounds() []*model.Outbound { - return []*model.Outbound{ - { - Sort: 0, - Protocol: "freedom", - Tag: "direct", - Settings: `{"domainStrategy":"UseIP","noises":[],"redirect":""}`, - }, - { - Sort: 1, - Protocol: "blackhole", - Tag: "blocked", - Settings: `{}`, - }, - } -} - -func (s *OutboundService) initDefaultOutbounds() { - db := database.GetDB() - for _, outbound := range defaultOutbounds() { - if err := db.Create(outbound).Error; err != nil { - logger.Warning("outbound init: create default failed:", err) - } - } - logger.Info("Initialized default outbound(s): direct, blocked") -} - -func (s *OutboundService) migrateOutboundsFromTemplate() { - db := database.GetDB() - templateConfig, err := s.settingService.GetXrayConfigTemplate() - if err != nil { - logger.Warning("outbound migration: get xray template failed:", err) - return - } - - var cfg map[string]interface{} - if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil { - logger.Warning("outbound migration: parse template failed:", err) - return - } - - rawOutbounds, ok := cfg["outbounds"].([]interface{}) - if !ok || len(rawOutbounds) == 0 { - return - } - - migrated := 0 - for i, item := range rawOutbounds { - raw, ok := item.(map[string]interface{}) - if !ok { - continue - } - outbound := outboundFromMap(raw, i) - if outbound.Tag == "" || outbound.Protocol == "" { - continue - } - if err := db.Create(outbound).Error; err != nil { - logger.Warning("outbound migration: create failed:", err) - continue - } - migrated++ - } - if migrated == 0 { - return - } - - cfg["outbounds"] = []interface{}{} - newTemplate, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - logger.Warning("outbound migration: marshal template failed:", err) - return - } - if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil { - logger.Warning("outbound migration: save template failed:", err) - return - } - logger.Info("Migrated", migrated, "outbound(s) from xray settings to database") -} - -func (s *OutboundService) MigrateDB() { - db := database.GetDB() - var count int64 - db.Model(&model.Outbound{}).Count(&count) - if count > 0 { - return - } - - s.migrateOutboundsFromTemplate() - - db.Model(&model.Outbound{}).Count(&count) - if count == 0 { - s.initDefaultOutbounds() - } -} diff --git a/web/service/routing_rule.go b/web/service/routing_rule.go index 6cb013ce..3ef32fb6 100644 --- a/web/service/routing_rule.go +++ b/web/service/routing_rule.go @@ -353,216 +353,6 @@ func (s *RoutingRuleService) ReplaceBalancerTag(oldTag, newTag string) error { return nil } -func apiListenMissing(api map[string]interface{}) bool { - _, ok := api["listen"] - return !ok -} - -func defaultApiListen() string { - var cfg map[string]interface{} - if err := json.Unmarshal([]byte(xrayTemplateConfig), &cfg); err != nil { - return "127.0.0.1:62789" - } - api, ok := cfg["api"].(map[string]interface{}) - if !ok { - return "127.0.0.1:62789" - } - s, _ := api["listen"].(string) - if s == "" { - return "127.0.0.1:62789" - } - return s -} - -func inboundListenAddress(inbound map[string]interface{}) string { - port := 0 - switch p := inbound["port"].(type) { - case float64: - port = int(p) - case int: - port = p - case int64: - port = int(p) - } - if port == 0 { - return "" - } - - host := "127.0.0.1" - if listen, ok := inbound["listen"]; ok { - switch v := listen.(type) { - case string: - if v != "" { - host = v - } - case []interface{}: - if len(v) > 0 { - if s, ok := v[0].(string); ok && s != "" { - host = s - } - } - } - } - return fmt.Sprintf("%s:%d", host, port) -} - -func isLegacyApiRoutingRule(rule map[string]interface{}) bool { - outboundTag, _ := rule["outboundTag"].(string) - if outboundTag != "api" { - return false - } - val, ok := rule["inboundTag"] - if !ok { - return false - } - switch v := val.(type) { - case []interface{}: - return len(v) == 1 && v[0] == "api" - case []string: - return len(v) == 1 && v[0] == "api" - default: - return false - } -} - -func migrateLegacyApiConfig(cfg map[string]interface{}) bool { - api, ok := cfg["api"].(map[string]interface{}) - if !ok || !apiListenMissing(api) { - return false - } - - var listenAddr string - inbounds, _ := cfg["inbounds"].([]interface{}) - - apiInboundIndex := -1 - var apiInbound map[string]interface{} - for i, item := range inbounds { - inbound, ok := item.(map[string]interface{}) - if !ok { - continue - } - tag, _ := inbound["tag"].(string) - protocol, _ := inbound["protocol"].(string) - if tag == "api" && protocol == "dokodemo-door" { - apiInboundIndex = i - apiInbound = inbound - break - } - } - - if apiInboundIndex >= 0 { - listenAddr = inboundListenAddress(apiInbound) - if listenAddr == "" { - listenAddr = defaultApiListen() - } - cfg["inbounds"] = RemoveIndex(inbounds, apiInboundIndex) - - if routing, ok := cfg["routing"].(map[string]interface{}); ok { - if rawRules, ok := routing["rules"].([]interface{}); ok { - for i, item := range rawRules { - rule, ok := item.(map[string]interface{}) - if !ok { - continue - } - if isLegacyApiRoutingRule(rule) { - routing["rules"] = RemoveIndex(rawRules, i) - cfg["routing"] = routing - break - } - } - } - } - } else { - listenAddr = defaultApiListen() - } - - api["listen"] = listenAddr - cfg["api"] = api - return true -} - -func routingRuleFromMap(raw map[string]interface{}, index int) *model.RoutingRule { - tag, _ := raw["ruleTag"].(string) - delete(raw, "ruleTag") - if tag == "" { - tag = fmt.Sprintf("migrated-rule-%d", index) - } - b, _ := json.Marshal(raw) - return &model.RoutingRule{ - Tag: tag, - Sort: index, - RawJson: string(b), - } -} - -func (s *RoutingRuleService) MigrateDB() { - db := database.GetDB() - var count int64 - db.Model(&model.RoutingRule{}).Count(&count) - - templateConfig, err := s.settingService.GetXrayConfigTemplate() - if err != nil { - logger.Warning("routing rule migration: get xray template failed:", err) - return - } - - var cfg map[string]interface{} - if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil { - logger.Warning("routing rule migration: parse template failed:", err) - return - } - - if migrateLegacyApiConfig(cfg) { - newTemplate, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - logger.Warning("routing rule migration: marshal legacy api config failed:", err) - return - } - if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil { - logger.Warning("routing rule migration: save legacy api config failed:", err) - return - } - logger.Info("Migrated legacy api inbound to api.listen in xray settings") - } - - if count > 0 { - return - } - - routing, ok := cfg["routing"].(map[string]interface{}) - if !ok { - return - } - rawRules, ok := routing["rules"].([]interface{}) - if !ok || len(rawRules) == 0 { - return - } - - for i, item := range rawRules { - raw, ok := item.(map[string]interface{}) - if !ok { - continue - } - rule := routingRuleFromMap(raw, i) - if err := db.Create(rule).Error; err != nil { - logger.Warning("routing rule migration: create failed:", err) - } - } - - routing["rules"] = []interface{}{} - cfg["routing"] = routing - newTemplate, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - logger.Warning("routing rule migration: marshal template failed:", err) - return - } - if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil { - logger.Warning("routing rule migration: save template failed:", err) - return - } - logger.Info("Migrated", len(rawRules), "routing rule(s) from xray settings to database") -} - func (s *RoutingRuleService) BuildRulesArray() ([]interface{}, error) { rules, err := s.GetAllRules() if err != nil { diff --git a/web/service/server.go b/web/service/server.go index 5c27ad22..118b6f01 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -581,10 +581,6 @@ func (s *ServerService) ImportDB(file multipart.File) error { } return common.NewErrorf("Error migrating db: %v", err) } - s.inboundService.MigrateDB() - s.outboundService.MigrateDB() - s.routingRuleService.MigrateDB() - // Start Xray err = s.RestartXrayService() if err != nil { diff --git a/web/service/setting.go b/web/service/setting.go index 3cef9e76..ed267905 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -1,7 +1,6 @@ package service import ( - _ "embed" "encoding/json" "errors" "fmt" @@ -10,6 +9,7 @@ import ( "strings" "time" + "github.com/alireza0/x-ui/config" "github.com/alireza0/x-ui/database" "github.com/alireza0/x-ui/database/model" "github.com/alireza0/x-ui/logger" @@ -19,11 +19,8 @@ import ( "github.com/alireza0/x-ui/web/entity" ) -//go:embed config.json -var xrayTemplateConfig string - var defaultValueMap = map[string]string{ - "xrayTemplateConfig": xrayTemplateConfig, + "xrayTemplateConfig": config.GetDefaultXrayTemplate(), "webListen": "", "webDomain": "", "webPort": "54321", @@ -472,7 +469,7 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) { var jsonData interface{} - err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) + err := json.Unmarshal([]byte(config.GetDefaultXrayTemplate()), &jsonData) if err != nil { return nil, err }