db.go 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. // Package database provides database initialization, migration, and management utilities
  2. // for the 3x-ui panel using GORM with SQLite or PostgreSQL.
  3. package database
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "log"
  11. "math"
  12. "os"
  13. "path"
  14. "slices"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/mhsanaei/3x-ui/v3/internal/config"
  19. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  20. "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
  21. "github.com/mhsanaei/3x-ui/v3/internal/util/random"
  22. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  23. "gorm.io/driver/postgres"
  24. "gorm.io/driver/sqlite"
  25. "gorm.io/gorm"
  26. "gorm.io/gorm/logger"
  27. )
  28. var db *gorm.DB
  29. const (
  30. DialectSQLite = "sqlite"
  31. DialectPostgres = "postgres"
  32. )
  33. // IsPostgres reports whether the active connection is a PostgreSQL backend.
  34. func IsPostgres() bool {
  35. if db == nil {
  36. return config.GetDBKind() == "postgres"
  37. }
  38. return db.Dialector.Name() == "postgres"
  39. }
  40. // Dialect returns the active GORM dialect name, or "" if the DB is not open.
  41. func Dialect() string {
  42. if db == nil {
  43. return ""
  44. }
  45. return db.Dialector.Name()
  46. }
  47. const (
  48. defaultUsername = "admin"
  49. defaultPassword = "admin"
  50. )
  51. func initModels() error {
  52. models := []any{
  53. &model.User{},
  54. &model.Inbound{},
  55. &model.OutboundTraffics{},
  56. &model.Setting{},
  57. &model.InboundClientIps{},
  58. &xray.ClientTraffic{},
  59. &model.HistoryOfSeeders{},
  60. &model.Node{},
  61. &model.ApiToken{},
  62. &model.ClientRecord{},
  63. &model.ClientInbound{},
  64. &model.ClientExternalLink{},
  65. &model.ClientGroup{},
  66. &model.InboundFallback{},
  67. &model.Host{},
  68. &model.NodeClientTraffic{},
  69. &model.NodeClientIp{},
  70. &model.ClientGlobalTraffic{},
  71. &model.OutboundSubscription{},
  72. }
  73. for _, mdl := range models {
  74. if err := db.AutoMigrate(mdl); err != nil {
  75. if isIgnorableDuplicateColumnErr(err, mdl) {
  76. log.Printf("Ignoring duplicate column during auto migration for %T: %v", mdl, err)
  77. continue
  78. }
  79. log.Printf("Error auto migrating model: %v", err)
  80. return err
  81. }
  82. }
  83. if err := dropLegacyForeignKeys(); err != nil {
  84. return err
  85. }
  86. if err := pruneOrphanedClientInbounds(); err != nil {
  87. return err
  88. }
  89. if err := pruneOrphanedHosts(); err != nil {
  90. return err
  91. }
  92. if err := normalizeInboundSubSortIndex(); err != nil {
  93. return err
  94. }
  95. if IsPostgres() {
  96. if err := resyncPostgresSequences(db, models); err != nil {
  97. log.Printf("Error resyncing postgres sequences: %v", err)
  98. return err
  99. }
  100. }
  101. return nil
  102. }
  103. func dropLegacyForeignKeys() error {
  104. if !IsPostgres() {
  105. return nil
  106. }
  107. if err := db.Exec("ALTER TABLE client_traffics DROP CONSTRAINT IF EXISTS fk_inbounds_client_stats").Error; err != nil {
  108. log.Printf("Error dropping legacy foreign key fk_inbounds_client_stats: %v", err)
  109. return err
  110. }
  111. return nil
  112. }
  113. // seedHostsFromExternalProxy is a one-time, self-gated migration that creates a
  114. // Host row for every legacy externalProxy entry on every inbound. Additive: the
  115. // externalProxy arrays are left intact in StreamSettings.
  116. func seedHostsFromExternalProxy() error {
  117. var history []string
  118. if err := db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &history).Error; err != nil {
  119. return err
  120. }
  121. if slices.Contains(history, "HostsFromExternalProxy") {
  122. return nil
  123. }
  124. var inbounds []model.Inbound
  125. if err := db.Find(&inbounds).Error; err != nil {
  126. return err
  127. }
  128. return db.Transaction(func(tx *gorm.DB) error {
  129. for _, inbound := range inbounds {
  130. if strings.TrimSpace(inbound.StreamSettings) == "" {
  131. continue
  132. }
  133. var stream map[string]any
  134. if err := json.Unmarshal([]byte(inbound.StreamSettings), &stream); err != nil {
  135. log.Printf("HostsFromExternalProxy: skip inbound %d (invalid stream json): %v", inbound.Id, err)
  136. continue
  137. }
  138. eps, ok := stream["externalProxy"].([]any)
  139. if !ok || len(eps) == 0 {
  140. continue
  141. }
  142. for i, raw := range eps {
  143. ep, ok := raw.(map[string]any)
  144. if !ok {
  145. continue
  146. }
  147. if err := tx.Create(externalProxyEntryToHost(inbound.Id, i, ep)).Error; err != nil {
  148. return err
  149. }
  150. }
  151. }
  152. return tx.Create(&model.HistoryOfSeeders{SeederName: "HostsFromExternalProxy"}).Error
  153. })
  154. }
  155. // externalProxyEntryToHost maps one legacy externalProxy entry onto a Host.
  156. // forceTls (same|tls|none) maps straight to Security; an unknown value falls back
  157. // to "same" (inherit). An empty remark gets a stable generated label so the row
  158. // stays valid/editable, and the remark is capped at the model's 256-char limit.
  159. func externalProxyEntryToHost(inboundId, index int, ep map[string]any) *model.Host {
  160. security, _ := ep["forceTls"].(string)
  161. switch security {
  162. case "same", "tls", "none":
  163. default:
  164. security = "same"
  165. }
  166. dest, _ := ep["dest"].(string)
  167. port := 0
  168. if p, ok := ep["port"].(float64); ok {
  169. port = int(p)
  170. }
  171. remark, _ := ep["remark"].(string)
  172. if strings.TrimSpace(remark) == "" {
  173. remark = "imported " + strconv.Itoa(index+1)
  174. }
  175. if len(remark) > 256 {
  176. remark = remark[:256]
  177. }
  178. sni, _ := ep["sni"].(string)
  179. fingerprint, _ := ep["fingerprint"].(string)
  180. ech, _ := ep["echConfigList"].(string)
  181. return &model.Host{
  182. InboundId: inboundId,
  183. SortOrder: index,
  184. Remark: remark,
  185. Address: dest,
  186. Port: port,
  187. Security: security,
  188. Sni: sni,
  189. Fingerprint: fingerprint,
  190. Alpn: anyToNonEmptyStrings(ep["alpn"]),
  191. PinnedPeerCertSha256: anyToNonEmptyStrings(ep["pinnedPeerCertSha256"]),
  192. EchConfigList: ech,
  193. }
  194. }
  195. func anyToNonEmptyStrings(v any) []string {
  196. switch t := v.(type) {
  197. case []any:
  198. out := make([]string, 0, len(t))
  199. for _, e := range t {
  200. if s, ok := e.(string); ok && s != "" {
  201. out = append(out, s)
  202. }
  203. }
  204. return out
  205. case []string:
  206. out := make([]string, 0, len(t))
  207. for _, s := range t {
  208. if s != "" {
  209. out = append(out, s)
  210. }
  211. }
  212. return out
  213. default:
  214. return nil
  215. }
  216. }
  217. func pruneOrphanedHosts() error {
  218. res := db.Exec("DELETE FROM hosts WHERE inbound_id NOT IN (SELECT id FROM inbounds)")
  219. if res.Error != nil {
  220. log.Printf("Error pruning orphaned hosts rows: %v", res.Error)
  221. return res.Error
  222. }
  223. if res.RowsAffected > 0 {
  224. log.Printf("Pruned %d orphaned hosts row(s)", res.RowsAffected)
  225. }
  226. return nil
  227. }
  228. func pruneOrphanedClientInbounds() error {
  229. res := db.Exec("DELETE FROM client_inbounds WHERE inbound_id NOT IN (SELECT id FROM inbounds)")
  230. if res.Error != nil {
  231. log.Printf("Error pruning orphaned client_inbounds rows: %v", res.Error)
  232. return res.Error
  233. }
  234. if res.RowsAffected > 0 {
  235. log.Printf("Pruned %d orphaned client_inbounds row(s)", res.RowsAffected)
  236. }
  237. return nil
  238. }
  239. // normalizeInboundSubSortIndex lifts sub_sort_index values below the 1-based
  240. // minimum (rows written by builds that defaulted the column to 0, or by nodes
  241. // predating the field) so they cannot sort ahead of explicitly ranked inbounds.
  242. func normalizeInboundSubSortIndex() error {
  243. res := db.Exec("UPDATE inbounds SET sub_sort_index = 1 WHERE sub_sort_index < 1")
  244. if res.Error != nil {
  245. log.Printf("Error normalizing inbound sub_sort_index: %v", res.Error)
  246. return res.Error
  247. }
  248. if res.RowsAffected > 0 {
  249. log.Printf("Normalized sub_sort_index on %d inbound(s)", res.RowsAffected)
  250. }
  251. return nil
  252. }
  253. func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
  254. if err == nil {
  255. return false
  256. }
  257. errMsg := strings.ToLower(err.Error())
  258. // SQLite: "duplicate column name: foo"
  259. // Postgres: `pq: column "foo" of relation "bar" already exists` / `sqlstate 42701`
  260. const sqlitePrefix = "duplicate column name:"
  261. if _, after, ok := strings.Cut(errMsg, sqlitePrefix); ok {
  262. col := strings.TrimSpace(after)
  263. col = strings.Trim(col, "`\"[]")
  264. return col != "" && db != nil && db.Migrator().HasColumn(mdl, col)
  265. }
  266. if strings.Contains(errMsg, "already exists") && strings.Contains(errMsg, "column ") {
  267. // Best effort: extract the column name between the first pair of double quotes.
  268. if _, after, ok := strings.Cut(errMsg, "column \""); ok {
  269. rest := after
  270. if e := strings.Index(rest, "\""); e > 0 {
  271. col := rest[:e]
  272. return col != "" && db != nil && db.Migrator().HasColumn(mdl, col)
  273. }
  274. }
  275. }
  276. return false
  277. }
  278. // initUser creates a default admin user if the users table is empty.
  279. func initUser() error {
  280. empty, err := isTableEmpty("users")
  281. if err != nil {
  282. log.Printf("Error checking if users table is empty: %v", err)
  283. return err
  284. }
  285. if empty {
  286. hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)
  287. if err != nil {
  288. log.Printf("Error hashing default password: %v", err)
  289. return err
  290. }
  291. user := &model.User{
  292. Username: defaultUsername,
  293. Password: hashedPassword,
  294. }
  295. return db.Create(user).Error
  296. }
  297. return nil
  298. }
  299. // runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
  300. func runSeeders(isUsersEmpty bool) error {
  301. empty, err := isTableEmpty("history_of_seeders")
  302. if err != nil {
  303. log.Printf("Error checking if users table is empty: %v", err)
  304. return err
  305. }
  306. if empty && isUsersEmpty {
  307. seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash", "LegacyProxySettingsCleanup"}
  308. for _, name := range seeders {
  309. if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
  310. return err
  311. }
  312. }
  313. return seedApiTokens()
  314. }
  315. var seedersHistory []string
  316. if err := db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory).Error; err != nil {
  317. log.Printf("Error fetching seeder history: %v", err)
  318. return err
  319. }
  320. if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
  321. var users []model.User
  322. if err := db.Find(&users).Error; err != nil {
  323. log.Printf("Error fetching users for password migration: %v", err)
  324. return err
  325. }
  326. for _, user := range users {
  327. if crypto.IsHashed(user.Password) {
  328. continue
  329. }
  330. hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password)
  331. if err != nil {
  332. log.Printf("Error hashing password for user '%s': %v", user.Username, err)
  333. return err
  334. }
  335. if err := db.Model(&user).Update("password", hashedPassword).Error; err != nil {
  336. log.Printf("Error updating password for user '%s': %v", user.Username, err)
  337. return err
  338. }
  339. }
  340. hashSeeder := &model.HistoryOfSeeders{
  341. SeederName: "UserPasswordHash",
  342. }
  343. if err := db.Create(hashSeeder).Error; err != nil {
  344. return err
  345. }
  346. }
  347. if !slices.Contains(seedersHistory, "ApiTokensTable") {
  348. if err := seedApiTokens(); err != nil {
  349. return err
  350. }
  351. }
  352. if !slices.Contains(seedersHistory, "ApiTokensHash") {
  353. if err := hashExistingApiTokens(); err != nil {
  354. return err
  355. }
  356. }
  357. if !slices.Contains(seedersHistory, "ClientsTable") {
  358. if err := seedClientsFromInboundJSON(); err != nil {
  359. return err
  360. }
  361. }
  362. if !slices.Contains(seedersHistory, "InboundClientsArrayFix") {
  363. if err := normalizeInboundClientsArray(); err != nil {
  364. return err
  365. }
  366. }
  367. if !slices.Contains(seedersHistory, "InboundClientTgIdFix") {
  368. if err := normalizeInboundClientTgId(); err != nil {
  369. return err
  370. }
  371. }
  372. if !slices.Contains(seedersHistory, "InboundClientSubIdFix") {
  373. if err := normalizeInboundClientSubId(); err != nil {
  374. return err
  375. }
  376. }
  377. if !slices.Contains(seedersHistory, "FreedomFinalRulesReverseFix") {
  378. if err := normalizeFreedomFinalRules(); err != nil {
  379. return err
  380. }
  381. }
  382. if !slices.Contains(seedersHistory, "LegacyProxySettingsCleanup") {
  383. if err := clearLegacyProxySettings(); err != nil {
  384. return err
  385. }
  386. }
  387. // Self-gated on the "HostsFromExternalProxy" row, so it is safe to call
  388. // unconditionally here.
  389. if err := seedHostsFromExternalProxy(); err != nil {
  390. return err
  391. }
  392. return nil
  393. }
  394. // clearLegacyProxySettings drops the deprecated panelProxy/tgBotProxy rows so a
  395. // stale tgBotProxy no longer masks the panelOutbound egress fallback.
  396. func clearLegacyProxySettings() error {
  397. return db.Transaction(func(tx *gorm.DB) error {
  398. if err := tx.Where("key IN ?", []string{"panelProxy", "tgBotProxy"}).
  399. Delete(&model.Setting{}).Error; err != nil {
  400. return err
  401. }
  402. return tx.Create(&model.HistoryOfSeeders{SeederName: "LegacyProxySettingsCleanup"}).Error
  403. })
  404. }
  405. func normalizeInboundClientTgId() error {
  406. var inbounds []model.Inbound
  407. if err := db.Find(&inbounds).Error; err != nil {
  408. return err
  409. }
  410. return db.Transaction(func(tx *gorm.DB) error {
  411. for _, inbound := range inbounds {
  412. if strings.TrimSpace(inbound.Settings) == "" {
  413. continue
  414. }
  415. var settings map[string]any
  416. if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
  417. log.Printf("InboundClientTgIdFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
  418. continue
  419. }
  420. clients, ok := settings["clients"].([]any)
  421. if !ok {
  422. continue
  423. }
  424. mutated := false
  425. for i, raw := range clients {
  426. obj, ok := raw.(map[string]any)
  427. if !ok {
  428. continue
  429. }
  430. tgRaw, present := obj["tgId"]
  431. if !present {
  432. continue
  433. }
  434. v, isFloat := tgRaw.(float64)
  435. if isFloat && !math.IsNaN(v) && !math.IsInf(v, 0) && v == math.Trunc(v) {
  436. continue
  437. }
  438. obj["tgId"] = int64(0)
  439. clients[i] = obj
  440. mutated = true
  441. }
  442. if !mutated {
  443. continue
  444. }
  445. settings["clients"] = clients
  446. newSettings, err := json.MarshalIndent(settings, "", " ")
  447. if err != nil {
  448. log.Printf("InboundClientTgIdFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
  449. continue
  450. }
  451. if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
  452. Update("settings", string(newSettings)).Error; err != nil {
  453. return err
  454. }
  455. }
  456. return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientTgIdFix"}).Error
  457. })
  458. }
  459. func normalizeInboundClientSubId() error {
  460. var inbounds []model.Inbound
  461. if err := db.Find(&inbounds).Error; err != nil {
  462. return err
  463. }
  464. return db.Transaction(func(tx *gorm.DB) error {
  465. for _, inbound := range inbounds {
  466. if strings.TrimSpace(inbound.Settings) == "" {
  467. continue
  468. }
  469. var settings map[string]any
  470. if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
  471. log.Printf("InboundClientSubIdFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
  472. continue
  473. }
  474. clients, ok := settings["clients"].([]any)
  475. if !ok {
  476. continue
  477. }
  478. mutated := false
  479. for i, raw := range clients {
  480. obj, ok := raw.(map[string]any)
  481. if !ok {
  482. continue
  483. }
  484. existing, _ := obj["subId"].(string)
  485. if strings.TrimSpace(existing) != "" {
  486. continue
  487. }
  488. obj["subId"] = random.NumLower(16)
  489. clients[i] = obj
  490. mutated = true
  491. }
  492. if !mutated {
  493. continue
  494. }
  495. settings["clients"] = clients
  496. newSettings, err := json.MarshalIndent(settings, "", " ")
  497. if err != nil {
  498. log.Printf("InboundClientSubIdFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
  499. continue
  500. }
  501. if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
  502. Update("settings", string(newSettings)).Error; err != nil {
  503. return err
  504. }
  505. }
  506. return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientSubIdFix"}).Error
  507. })
  508. }
  509. func normalizeInboundClientsArray() error {
  510. var inbounds []model.Inbound
  511. if err := db.Find(&inbounds).Error; err != nil {
  512. return err
  513. }
  514. return db.Transaction(func(tx *gorm.DB) error {
  515. for _, inbound := range inbounds {
  516. if strings.TrimSpace(inbound.Settings) == "" {
  517. continue
  518. }
  519. var settings map[string]any
  520. if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
  521. log.Printf("InboundClientsArrayFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
  522. continue
  523. }
  524. raw, exists := settings["clients"]
  525. if !exists || raw != nil {
  526. continue
  527. }
  528. settings["clients"] = []any{}
  529. newSettings, err := json.MarshalIndent(settings, "", " ")
  530. if err != nil {
  531. log.Printf("InboundClientsArrayFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
  532. continue
  533. }
  534. if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
  535. Update("settings", string(newSettings)).Error; err != nil {
  536. return err
  537. }
  538. }
  539. return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientsArrayFix"}).Error
  540. })
  541. }
  542. func normalizeFreedomFinalRules() error {
  543. var setting model.Setting
  544. err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(&setting).Error
  545. if errors.Is(err, gorm.ErrRecordNotFound) {
  546. return db.Create(&model.HistoryOfSeeders{SeederName: "FreedomFinalRulesReverseFix"}).Error
  547. }
  548. if err != nil {
  549. return err
  550. }
  551. updated, changed, rErr := rewriteFreedomFinalRules(setting.Value)
  552. if rErr != nil {
  553. log.Printf("FreedomFinalRulesReverseFix: skip (invalid xrayTemplateConfig json): %v", rErr)
  554. return db.Create(&model.HistoryOfSeeders{SeederName: "FreedomFinalRulesReverseFix"}).Error
  555. }
  556. return db.Transaction(func(tx *gorm.DB) error {
  557. if changed {
  558. if err := tx.Model(&model.Setting{}).Where("key = ?", "xrayTemplateConfig").
  559. Update("value", updated).Error; err != nil {
  560. return err
  561. }
  562. }
  563. return tx.Create(&model.HistoryOfSeeders{SeederName: "FreedomFinalRulesReverseFix"}).Error
  564. })
  565. }
  566. func rewriteFreedomFinalRules(raw string) (string, bool, error) {
  567. if strings.TrimSpace(raw) == "" {
  568. return raw, false, nil
  569. }
  570. var cfg map[string]any
  571. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  572. return raw, false, err
  573. }
  574. outbounds, ok := cfg["outbounds"].([]any)
  575. if !ok {
  576. return raw, false, nil
  577. }
  578. changed := false
  579. for _, ob := range outbounds {
  580. obj, ok := ob.(map[string]any)
  581. if !ok {
  582. continue
  583. }
  584. if proto, _ := obj["protocol"].(string); proto != "freedom" {
  585. continue
  586. }
  587. settings, ok := obj["settings"].(map[string]any)
  588. if !ok {
  589. continue
  590. }
  591. if !isLegacyPrivateOnlyFinalRules(settings["finalRules"]) {
  592. continue
  593. }
  594. settings["finalRules"] = []any{map[string]any{"action": "allow"}}
  595. changed = true
  596. }
  597. if !changed {
  598. return raw, false, nil
  599. }
  600. out, err := json.MarshalIndent(cfg, "", " ")
  601. if err != nil {
  602. return raw, false, err
  603. }
  604. return string(out), true, nil
  605. }
  606. func isLegacyPrivateOnlyFinalRules(v any) bool {
  607. rules, ok := v.([]any)
  608. if !ok || len(rules) != 1 {
  609. return false
  610. }
  611. rule, ok := rules[0].(map[string]any)
  612. if !ok {
  613. return false
  614. }
  615. if action, _ := rule["action"].(string); action != "allow" {
  616. return false
  617. }
  618. ips, ok := rule["ip"].([]any)
  619. if !ok || len(ips) != 1 {
  620. return false
  621. }
  622. if s, _ := ips[0].(string); s != "geoip:private" {
  623. return false
  624. }
  625. for k := range rule {
  626. if k != "action" && k != "ip" {
  627. return false
  628. }
  629. }
  630. return true
  631. }
  632. // normalizeClientJSONFields coerces loosely-typed numeric fields in a raw
  633. // settings.clients entry so json.Unmarshal into model.Client doesn't fail
  634. // when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings
  635. // drop the key so the field falls back to its zero value.
  636. func normalizeClientJSONFields(obj map[string]any) {
  637. normalizeInt := func(key string) {
  638. raw, exists := obj[key]
  639. if !exists {
  640. return
  641. }
  642. s, ok := raw.(string)
  643. if !ok {
  644. return
  645. }
  646. trimmed := strings.ReplaceAll(strings.TrimSpace(s), " ", "")
  647. if trimmed == "" {
  648. delete(obj, key)
  649. return
  650. }
  651. if n, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
  652. obj[key] = n
  653. } else {
  654. delete(obj, key)
  655. }
  656. }
  657. for _, k := range []string{"tgId", "limitIp", "totalGB", "expiryTime", "reset", "created_at", "updated_at"} {
  658. normalizeInt(k)
  659. }
  660. }
  661. func seedClientsFromInboundJSON() error {
  662. var inbounds []model.Inbound
  663. if err := db.Find(&inbounds).Error; err != nil {
  664. return err
  665. }
  666. return db.Transaction(func(tx *gorm.DB) error {
  667. byEmail := map[string]*model.ClientRecord{}
  668. var existing []model.ClientRecord
  669. if err := tx.Find(&existing).Error; err != nil {
  670. return err
  671. }
  672. for i := range existing {
  673. byEmail[existing[i].Email] = &existing[i]
  674. }
  675. for _, inbound := range inbounds {
  676. if strings.TrimSpace(inbound.Settings) == "" {
  677. continue
  678. }
  679. var settings map[string]any
  680. if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
  681. log.Printf("ClientsTable seed: skip inbound %d (invalid settings json): %v", inbound.Id, err)
  682. continue
  683. }
  684. rawList, ok := settings["clients"].([]any)
  685. if !ok {
  686. continue
  687. }
  688. for _, raw := range rawList {
  689. obj, ok := raw.(map[string]any)
  690. if !ok {
  691. continue
  692. }
  693. normalizeClientJSONFields(obj)
  694. blob, err := json.Marshal(obj)
  695. if err != nil {
  696. continue
  697. }
  698. var c model.Client
  699. if err := json.Unmarshal(blob, &c); err != nil {
  700. log.Printf("ClientsTable seed: skip client in inbound %d (unmarshal failed): %v; payload=%s",
  701. inbound.Id, err, string(blob))
  702. continue
  703. }
  704. email := strings.TrimSpace(c.Email)
  705. if email == "" {
  706. continue
  707. }
  708. incoming := c.ToRecord()
  709. row, dup := byEmail[email]
  710. if !dup {
  711. if err := tx.Create(incoming).Error; err != nil {
  712. return err
  713. }
  714. byEmail[email] = incoming
  715. row = incoming
  716. } else {
  717. conflicts := model.MergeClientRecord(row, incoming)
  718. for _, x := range conflicts {
  719. log.Printf("client merge: email=%s conflict on %s old=%v new=%v kept=%v",
  720. email, x.Field, x.Old, x.New, x.Kept)
  721. }
  722. if err := tx.Save(row).Error; err != nil {
  723. return err
  724. }
  725. }
  726. link := model.ClientInbound{
  727. ClientId: row.Id,
  728. InboundId: inbound.Id,
  729. FlowOverride: c.Flow,
  730. }
  731. if err := tx.Where("client_id = ? AND inbound_id = ?", row.Id, inbound.Id).
  732. FirstOrCreate(&link).Error; err != nil {
  733. return err
  734. }
  735. }
  736. }
  737. return tx.Create(&model.HistoryOfSeeders{SeederName: "ClientsTable"}).Error
  738. })
  739. }
  740. // seedApiTokens copies the legacy `apiToken` setting into the new
  741. // api_tokens table as a row named "default" so existing central panels
  742. // keep working after the upgrade. Idempotent — records itself in
  743. // history_of_seeders and only runs when api_tokens is empty.
  744. func seedApiTokens() error {
  745. empty, err := isTableEmpty("api_tokens")
  746. if err != nil {
  747. return err
  748. }
  749. if empty {
  750. var legacy model.Setting
  751. err := db.Model(model.Setting{}).Where("key = ?", "apiToken").First(&legacy).Error
  752. if err == nil && legacy.Value != "" {
  753. row := &model.ApiToken{
  754. Name: "default",
  755. Token: legacy.Value,
  756. Enabled: true,
  757. }
  758. if err := db.Create(row).Error; err != nil {
  759. log.Printf("Error migrating legacy apiToken: %v", err)
  760. return err
  761. }
  762. }
  763. }
  764. return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensTable"}).Error
  765. }
  766. // hashExistingApiTokens replaces any plaintext token stored before tokens were
  767. // hashed at rest with its SHA-256 digest. Callers keep their plaintext copy
  768. // (used on remote nodes), so existing tokens keep authenticating; the panel
  769. // just can no longer reveal them. Idempotent — already-hashed rows are skipped.
  770. func hashExistingApiTokens() error {
  771. var rows []*model.ApiToken
  772. if err := db.Find(&rows).Error; err != nil {
  773. return err
  774. }
  775. for _, r := range rows {
  776. if crypto.IsSHA256Hex(r.Token) {
  777. continue
  778. }
  779. hashed := crypto.HashTokenSHA256(r.Token)
  780. if err := db.Model(model.ApiToken{}).Where("id = ?", r.Id).Update("token", hashed).Error; err != nil {
  781. log.Printf("Error hashing api token %d: %v", r.Id, err)
  782. return err
  783. }
  784. }
  785. return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensHash"}).Error
  786. }
  787. // isTableEmpty returns true if the named table contains zero rows.
  788. func isTableEmpty(tableName string) (bool, error) {
  789. var count int64
  790. err := db.Table(tableName).Count(&count).Error
  791. return count == 0, err
  792. }
  793. // InitDB sets up the database connection, migrates models, and runs seeders.
  794. // When XUI_DB_TYPE=postgres, dbPath is ignored and XUI_DB_DSN is used instead.
  795. func InitDB(dbPath string) error {
  796. var gormLogger logger.Interface
  797. if config.IsDebug() {
  798. gormLogger = logger.New(
  799. log.New(os.Stdout, "\r\n", log.LstdFlags),
  800. logger.Config{
  801. SlowThreshold: time.Second,
  802. LogLevel: logger.Info,
  803. IgnoreRecordNotFoundError: true,
  804. Colorful: true,
  805. },
  806. )
  807. } else {
  808. gormLogger = logger.Discard
  809. }
  810. c := &gorm.Config{Logger: gormLogger, DisableForeignKeyConstraintWhenMigrating: true}
  811. var err error
  812. switch config.GetDBKind() {
  813. case "postgres":
  814. dsn := config.GetDBDSN()
  815. if dsn == "" {
  816. return errors.New("XUI_DB_TYPE=postgres but XUI_DB_DSN is empty")
  817. }
  818. db, err = gorm.Open(postgres.Open(dsn), c)
  819. if err != nil {
  820. return err
  821. }
  822. default:
  823. dir := path.Dir(dbPath)
  824. if err = os.MkdirAll(dir, 0755); err != nil {
  825. return err
  826. }
  827. // Keep journal_mode=DELETE so the DB stays a single file (no -wal/-shm
  828. // sidecars). synchronous defaults to FULL for durability but is tunable.
  829. sync := sqliteSynchronous()
  830. dsn := dbPath + "?_journal_mode=DELETE&_busy_timeout=10000&_synchronous=" + sync + "&_txlock=immediate"
  831. db, err = gorm.Open(sqlite.Open(dsn), c)
  832. if err != nil {
  833. return err
  834. }
  835. sqlDB, err := db.DB()
  836. if err != nil {
  837. return err
  838. }
  839. // Re-assert the DSN pragmas plus scan-friendly ones for large datasets.
  840. // cache_size/mmap_size/temp_store create no extra files, so the single-file
  841. // guarantee holds; they just cut disk I/O on the 50k-row hot paths.
  842. pragmas := []string{
  843. "PRAGMA journal_mode=DELETE",
  844. "PRAGMA busy_timeout=10000",
  845. "PRAGMA synchronous=" + sync,
  846. fmt.Sprintf("PRAGMA cache_size=-%d", envInt("XUI_DB_CACHE_MB", 32)*1024),
  847. fmt.Sprintf("PRAGMA mmap_size=%d", int64(envInt("XUI_DB_MMAP_MB", 256))*1024*1024),
  848. "PRAGMA temp_store=MEMORY",
  849. }
  850. for _, p := range pragmas {
  851. if _, err := sqlDB.Exec(p); err != nil {
  852. return err
  853. }
  854. }
  855. }
  856. sqlDB, err := db.DB()
  857. if err != nil {
  858. return err
  859. }
  860. var maxOpen, maxIdle int
  861. switch config.GetDBKind() {
  862. case "postgres":
  863. maxOpen = envInt("XUI_DB_MAX_OPEN_CONNS", 25)
  864. maxIdle = envInt("XUI_DB_MAX_IDLE_CONNS", 25)
  865. default:
  866. maxOpen = envInt("XUI_DB_MAX_OPEN_CONNS", 8)
  867. maxIdle = envInt("XUI_DB_MAX_IDLE_CONNS", 4)
  868. }
  869. sqlDB.SetMaxOpenConns(maxOpen)
  870. sqlDB.SetMaxIdleConns(maxIdle)
  871. sqlDB.SetConnMaxLifetime(time.Hour)
  872. sqlDB.SetConnMaxIdleTime(30 * time.Minute)
  873. if err := initModels(); err != nil {
  874. return err
  875. }
  876. isUsersEmpty, err := isTableEmpty("users")
  877. if err != nil {
  878. return err
  879. }
  880. if err := initUser(); err != nil {
  881. return err
  882. }
  883. return runSeeders(isUsersEmpty)
  884. }
  885. // sqliteSynchronous returns the SQLite synchronous mode, defaulting to FULL.
  886. // Whitelisted because the value is interpolated directly into a PRAGMA string.
  887. func sqliteSynchronous() string {
  888. switch strings.ToUpper(strings.TrimSpace(os.Getenv("XUI_DB_SYNCHRONOUS"))) {
  889. case "OFF":
  890. return "OFF"
  891. case "NORMAL":
  892. return "NORMAL"
  893. case "EXTRA":
  894. return "EXTRA"
  895. default:
  896. return "FULL"
  897. }
  898. }
  899. func envInt(key string, def int) int {
  900. v := strings.TrimSpace(os.Getenv(key))
  901. if v == "" {
  902. return def
  903. }
  904. n, err := strconv.Atoi(v)
  905. if err != nil || n <= 0 {
  906. return def
  907. }
  908. return n
  909. }
  910. // CloseDB closes the database connection if it exists.
  911. func CloseDB() error {
  912. if db != nil {
  913. sqlDB, err := db.DB()
  914. if err != nil {
  915. return err
  916. }
  917. return sqlDB.Close()
  918. }
  919. return nil
  920. }
  921. // GetDB returns the global GORM database instance.
  922. func GetDB() *gorm.DB {
  923. return db
  924. }
  925. func IsNotFound(err error) bool {
  926. return errors.Is(err, gorm.ErrRecordNotFound)
  927. }
  928. // IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
  929. func IsSQLiteDB(file io.ReaderAt) (bool, error) {
  930. signature := []byte("SQLite format 3\x00")
  931. buf := make([]byte, len(signature))
  932. _, err := file.ReadAt(buf, 0)
  933. if err != nil {
  934. return false, err
  935. }
  936. return bytes.Equal(buf, signature), nil
  937. }
  938. // Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
  939. // No-op on PostgreSQL (WAL there is managed by the server).
  940. func Checkpoint() error {
  941. if IsPostgres() {
  942. return nil
  943. }
  944. return db.Exec("PRAGMA wal_checkpoint;").Error
  945. }
  946. // ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
  947. // and runs a PRAGMA integrity_check to ensure the file is structurally sound.
  948. // It does not mutate global state or run migrations.
  949. func ValidateSQLiteDB(dbPath string) error {
  950. if _, err := os.Stat(dbPath); err != nil { // file must exist
  951. return err
  952. }
  953. gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
  954. if err != nil {
  955. return err
  956. }
  957. sqlDB, err := gdb.DB()
  958. if err != nil {
  959. return err
  960. }
  961. defer sqlDB.Close()
  962. var res string
  963. if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
  964. return err
  965. }
  966. if res != "ok" {
  967. return errors.New("sqlite integrity check failed: " + res)
  968. }
  969. return nil
  970. }