| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- package service
- import (
- "encoding/json"
- "fmt"
- "strconv"
- "strings"
- "time"
- "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/xray"
- "gorm.io/gorm"
- )
- func (s *InboundService) MigrationRemoveOrphanedTraffics() {
- db := database.GetDB()
- query := fmt.Sprintf(
- "DELETE FROM client_traffics WHERE email NOT IN (SELECT %s %s)",
- database.JSONFieldText("client.value", "email"),
- database.JSONClientsFromInbound(),
- )
- db.Exec(query)
- }
- func (s *InboundService) MigrationRequirements() {
- db := database.GetDB()
- tx := db.Begin()
- var err error
- defer func() {
- if err == nil {
- tx.Commit()
- if !database.IsPostgres() {
- if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
- logger.Warningf("VACUUM failed: %v", dbErr)
- }
- }
- } else {
- tx.Rollback()
- }
- }()
- if tx.Migrator().HasColumn(&model.Inbound{}, "all_time") {
- if err = tx.Migrator().DropColumn(&model.Inbound{}, "all_time"); err != nil {
- return
- }
- }
- if tx.Migrator().HasColumn(&xray.ClientTraffic{}, "all_time") {
- if err = tx.Migrator().DropColumn(&xray.ClientTraffic{}, "all_time"); err != nil {
- return
- }
- }
- // Normalize "enable" columns to boolean on Postgres. Legacy SQLite data
- // (0/1 integers), partial migrations, or mixed write paths (public API
- // inbound updates that flow through UpdateClientStat + client syncs, plus
- // node traffic merge deltas) can leave the column as integer or with mixed
- // interpretation. This (combined with the dialect-aware
- // ClientTrafficEnableMergeExpr) prevents type problems in the node traffic
- // sync merge (SetRemoteTraffic) and makes the sync robust even when
- // inbounds are updated via the public API (incl. ones carrying
- // externalProxy in streamSettings). The same expression is also safe on
- // SQLite (no PG :: casts).
- if database.IsPostgres() {
- // Use DO block so it is idempotent and doesn't fail if already boolean.
- normalizeBool := func(table, col string) {
- tx.Exec(fmt.Sprintf(`
- DO $$
- BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.columns
- WHERE table_name = '%s' AND column_name = '%s'
- AND data_type <> 'boolean'
- ) THEN
- ALTER TABLE %s ALTER COLUMN %s
- TYPE boolean USING (CASE WHEN %s::text IN ('1','true','t','yes') THEN true ELSE false END);
- END IF;
- END $$;`, table, col, table, col, col))
- }
- normalizeBool("inbounds", "enable")
- normalizeBool("client_traffics", "enable")
- normalizeBool("nodes", "enable")
- normalizeBool("clients", "enable")
- normalizeBool("api_tokens", "enabled")
- normalizeBool("outbound_subscriptions", "enabled")
- }
- // Fix inbounds based problems
- var inbounds []*model.Inbound
- err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error
- if err != nil && err != gorm.ErrRecordNotFound {
- return
- }
- for inbound_index := range inbounds {
- settings := map[string]any{}
- json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
- if raw, exists := settings["clients"]; exists && raw == nil {
- settings["clients"] = []any{}
- }
- clients, ok := settings["clients"].([]any)
- if ok {
- // Fix Client configuration problems
- newClients := make([]any, 0, len(clients))
- hasVisionFlow := false
- for client_index := range clients {
- c := clients[client_index].(map[string]any)
- // Add email='' if it is not exists
- if _, ok := c["email"]; !ok {
- c["email"] = ""
- }
- // Convert string tgId to int64
- if _, ok := c["tgId"]; ok {
- var tgId any = c["tgId"]
- if tgIdStr, ok2 := tgId.(string); ok2 {
- tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64)
- if err == nil {
- c["tgId"] = tgIdInt64
- }
- }
- }
- // Remove "flow": "xtls-rprx-direct"
- if _, ok := c["flow"]; ok {
- if c["flow"] == "xtls-rprx-direct" {
- c["flow"] = ""
- }
- }
- if flow, _ := c["flow"].(string); flow == "xtls-rprx-vision" {
- hasVisionFlow = true
- }
- // Backfill created_at and updated_at
- if _, ok := c["created_at"]; !ok {
- c["created_at"] = time.Now().Unix() * 1000
- }
- c["updated_at"] = time.Now().Unix() * 1000
- newClients = append(newClients, any(c))
- }
- settings["clients"] = newClients
- // Drop orphaned testseed: VLESS-only field, only meaningful when at least
- // one client uses the exact xtls-rprx-vision flow. Older versions saved it
- // for any non-empty flow (including the UDP variant) or kept it after the
- // flow was cleared from the client modal — clean those up here.
- if inbounds[inbound_index].Protocol == model.VLESS && !hasVisionFlow {
- delete(settings, "testseed")
- }
- 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)
- }
- }
- }
- // Heal clients table for installs where the one-shot seeder
- // skipped clients due to a tgId-string unmarshal error.
- if syncErr := s.clientService.SyncInbound(tx, inbounds[inbound_index].Id, modelClients); syncErr != nil {
- logger.Warning("MigrationRequirements sync clients failed:", syncErr)
- }
- }
- 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 string // text column on both DBs; safer than []byte for cross-DB scan
- }
- externalProxyQuery := `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`
- if database.IsPostgres() {
- externalProxyQuery = `select id, port, stream_settings
- from inbounds
- WHERE protocol in ('vmess','vless','trojan')
- AND NULLIF(stream_settings, '')::jsonb #>> '{security}' = 'tls'
- AND NULLIF(stream_settings, '')::jsonb #> '{tlsSettings,settings,domains}' IS NOT NULL`
- }
- err = tx.Raw(externalProxyQuery).Scan(&externalProxy).Error
- if err != nil || len(externalProxy) == 0 {
- return
- }
- for _, ep := range externalProxy {
- var reverses any
- var stream map[string]any
- json.Unmarshal([]byte(ep.StreamSettings), &stream)
- if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok {
- if settings, ok := tlsSettings["settings"].(map[string]any); ok {
- if domains, ok := settings["domains"].([]any); ok {
- for _, domain := range domains {
- if domainMap, ok := domain.(map[string]any); 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)
- }
- // Legacy tag cleanup for old auto-generated tags (e.g. "0.0.0.0:443-...").
- // Must be cross-DB: INSTR/REPLACE work on SQLite; Postgres needs position().
- tagCleanup := `UPDATE inbounds
- SET tag = REPLACE(tag, '0.0.0.0:', '')
- WHERE INSTR(tag, '0.0.0.0:') > 0;`
- if database.IsPostgres() {
- tagCleanup = `UPDATE inbounds
- SET tag = REPLACE(tag, '0.0.0.0:', '')
- WHERE position('0.0.0.0:' in tag) > 0;`
- }
- err = tx.Raw(tagCleanup).Error
- if err != nil {
- return
- }
- }
- func (s *InboundService) MigrateDB() {
- s.MigrationRequirements()
- s.MigrationRemoveOrphanedTraffics()
- }
|