Files
HY2XS_flamy/apps/dao/sqlite.go
T
founder b52fac1394 fix(admin): убрать каналы утечки секретов и остатки H UI из runtime
Экспорт в панели формировался через os.Create в /var/lib/hy2xs-admin/export,
и файл там оставался навсегда. При includeSecrets=true это означало
расшифрованные секреты пиров — фактические учётные данные доступа — в
открытом виде на диске, накапливающиеся с каждым нажатием кнопки. Выгрузки
формируются в памяти, каталога export/ больше нет.

Generic export/import таблицы config удалён целиком. Он исключал только сырой
Hysteria YAML, а в той же таблице лежат JWT_SECRET, PEER_SECRET_KEY,
PEER_SECRET_ENCRYPTION_KEY и HYSTERIA2_TRAFFIC_STATS_SECRET: кнопка Export
выгружала их открытым текстом, импорт позволял подменить. Для
PEER_SECRET_ENCRYPTION_KEY подмена ломает расшифровку секретов уже
существующих пиров. Production-сценария у этой пары не было.

Импорт пиров шёл мимо всей валидации, которую проходит обычное создание пира:
в базу попадало имя любой длины и с любыми символами, disabled с произвольным
числом, отрицательные счётчики. Файл применялся построчно, поэтому ошибка в
середине оставляла список наполовину изменённым, а импорт мог перезаписать
bootstrap-admin-peer, чей секрет продублирован в bootstrap-admin.secret.
Партия проверяется целиком до первой записи, неизвестные поля отклоняются.

Убран слой сетевых настроек панели: H_UI_WEB_PORT, H_UI_WEB_CONTEXT,
H_UI_CRT_PATH, H_UI_KEY_PATH и собственный TLS. Оркестратор передавал порт
аргументом, админка писала его в SQLite и тут же читала обратно, а UI
показывал поля disabled — второй источник истины, из которого ничего нельзя
было изменить. HUI_DATA/HUI_LOG заменены на HY2XS_DATA_DIR/HY2XS_LOG_DIR,
база переименована в hy2xs-admin.db, reference-схема — в schema.sql.

API namespace разделён по природе маршрутов: операторский API на /api,
machine-auth Hysteria на /internal/hysteria/auth. Путь machine-auth —
runtime-контракт, он уезжает в config.yaml и post-install.env, поэтому
объявлен одной константой на компонент.

Go-санитайзер экспорта вырезал секреты из URL только у ключей url/addr:
будущее upstream-поле с другим именем уносило учётные данные и access_token
целиком, а URL внутри списков не обрабатывались вовсе. Граница определяется
значением, а не именем ключа — как в TS-санитайзере оркестратора.

Заодно индикатор загрузки и цвета 401/404 переведены на брендовый токен:
NProgress приходил со своим #29d и был единственным элементом вне палитры.
2026-08-27 20:49:07 +05:00

762 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dao
import (
"encoding/base64"
"errors"
"fmt"
"github.com/glebarez/sqlite"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"hy2xs-admin/util"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
var sqliteDB *gorm.DB
func InitSqliteDB() error {
var err error
sqliteDB, err = gorm.Open(sqlite.Open(constant.SqliteDBPath), &gorm.Config{
TranslateError: true,
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Silent,
IgnoreRecordNotFoundError: true,
ParameterizedQueries: true,
Colorful: false,
},
),
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
logrus.Errorf("sqlite open err: %v", err)
return errors.New("sqlite open err")
}
return nil
}
// InitSql поднимает базу админки.
//
// Порт сюда больше не передаётся: раньше InitSql записывал в config тот самый
// порт, который получил аргументом из ExecStart, а startup тут же читал его
// обратно. База не была источником этого факта ни на одном шаге.
func InitSql() error {
if err := InitSqliteDB(); err != nil {
return err
}
if err := runMigrations(); err != nil {
return err
}
if err := ensureSecureBootstrapAdmin(); err != nil {
return err
}
if err := ensureSecureBootstrapPeer(); err != nil {
return err
}
if _, err := getOrCreateJwtSecret(); err != nil {
return err
}
if err := ensureTrafficStatsSecret(); err != nil {
return err
}
return nil
}
func envInt(name string, fallback int) int {
if raw := strings.TrimSpace(os.Getenv(name)); raw != "" {
if value, err := strconv.Atoi(raw); err == nil {
return value
}
}
return fallback
}
func envBoolAsInt(name string, fallback int) int {
raw := strings.TrimSpace(strings.ToLower(os.Getenv(name)))
if raw == "" {
return fallback
}
if raw == "true" || raw == "1" {
return 1
}
if raw == "false" || raw == "0" {
return 0
}
return fallback
}
func ensureSecureBootstrapAdmin() error {
adminUser := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_USER"))
if adminUser == "" {
adminUser = "hy2xsadmin"
}
if _, err := GetAdminUser("username = ?", adminUser); err == nil {
return nil
}
adminPassword := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD"))
passwordGenerated := false
if adminPassword == "" {
password, pwdErr := util.RandomString(18)
if pwdErr != nil {
return pwdErr
}
adminPassword = password
passwordGenerated = true
}
forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1)
status := int64(1)
tokenVersion := int64(1)
passwordChangedAt := time.Now().UnixMilli()
hash, hashErr := util.HashPassword(adminPassword)
if hashErr != nil {
return hashErr
}
username := adminUser
account := entity.AdminUser{
Username: &username,
PasswordHash: &hash,
Status: &status,
TokenVersion: &tokenVersion,
PasswordChangedAt: &passwordChangedAt,
ForcePasswordChange: func() *int64 { v := int64(forcePasswordChange); return &v }(),
}
if _, saveErr := SaveAdminUser(account); saveErr != nil {
return saveErr
}
if passwordGenerated {
logrus.Warnf("Initial admin username: %s", adminUser)
logrus.Warnf("Initial admin password: %s", adminPassword)
}
return nil
}
func ensureSecureBootstrapPeer() error {
bootstrapSecret := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_CON_PASS"))
if bootstrapSecret == "" {
generated, err := util.RandomString(24)
if err != nil {
return err
}
bootstrapSecret = generated
}
if _, err := GetPeer("auth_id = ?", "bootstrap-admin-peer"); err == nil {
return nil
}
if _, err := GetPeer("name = ?", "bootstrap-admin-peer"); err == nil {
return nil
}
secretDigest, err := buildPeerSecretDigest(bootstrapSecret)
if err != nil {
return err
}
if _, err := GetPeer("secret_digest = ?", secretDigest); err == nil {
return nil
}
secretEncrypted, err := encryptBootstrapPeerSecret(bootstrapSecret)
if err != nil {
return err
}
name := "bootstrap-admin-peer"
authID := "bootstrap-admin-peer"
remark := "bootstrap peer seeded from HY2XS_ADMIN_CON_PASS"
quota := int64(-1)
download := int64(0)
upload := int64(0)
expires := int64(0)
maxDevices := int64(3)
disabled := int64(0)
bannedUntil := int64(0)
lastConnection := int64(0)
peer := entity.Peer{
Name: &name,
Remark: &remark,
AuthId: &authID,
SecretDigest: &secretDigest,
SecretEncrypted: &secretEncrypted,
QuotaBytes: &quota,
DownloadBytes: &download,
UploadBytes: &upload,
ExpiresAt: &expires,
MaxDevices: &maxDevices,
Disabled: &disabled,
BannedUntil: &bannedUntil,
LastConnectionAt: &lastConnection,
}
_, saveErr := SavePeer(peer)
return saveErr
}
func buildPeerSecretDigest(rawSecret string) (string, error) {
secretKey, err := getOrCreatePeerSecretDigestKey()
if err != nil {
return "", err
}
return util.HmacSHA256Hex(rawSecret, secretKey), nil
}
func getOrCreatePeerSecretEncryptionKey() ([]byte, error) {
raw, err := getOrCreateConfigKey(constant.PeerSecretEncryptionKey, 32)
if err != nil {
return nil, err
}
decoded, decErr := util.DecodeBase64Key(raw, 32)
if decErr == nil {
return decoded, nil
}
plain := []byte(strings.TrimSpace(raw))
if len(plain) < 32 {
return nil, errors.New("invalid peer secret encryption key")
}
plain = plain[:32]
encoded := base64.StdEncoding.EncodeToString(plain)
if err := UpdateConfig([]string{constant.PeerSecretEncryptionKey}, map[string]interface{}{"value": encoded}); err != nil {
return nil, err
}
return plain, nil
}
func encryptBootstrapPeerSecret(rawSecret string) (string, error) {
key, err := getOrCreatePeerSecretEncryptionKey()
if err != nil {
return "", err
}
return util.EncryptAESGCM(rawSecret, key)
}
func getOrCreateConfigKey(key string, size int) (string, error) {
cfg, err := GetConfig("key = ?", key)
if err == nil && cfg.Value != nil && strings.TrimSpace(*cfg.Value) != "" {
return strings.TrimSpace(*cfg.Value), nil
}
raw, genErr := util.RandomString(size)
if genErr != nil {
return "", genErr
}
value := raw
remark := key
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &value, Remark: &remark}); saveErr != nil {
if updErr := UpdateConfig([]string{key}, map[string]interface{}{"value": value}); updErr != nil {
return "", updErr
}
}
return value, nil
}
func runMigrations() error {
if tx := sqliteDB.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`); tx.Error != nil {
logrus.Errorf("sqlite migration init err: %v", tx.Error)
return errors.New("sqlite migration init err")
}
type migration struct {
version string
apply func() error
}
migrations := []migration{
{version: "000_base_config", apply: migrateBaseConfig},
{version: "001_admin_peer_split", apply: migrateAdminPeerSplit},
{version: "002_migrate_legacy_accounts", apply: migrateLegacyAccounts},
{version: "003_archive_legacy_account", apply: archiveLegacyAccount},
{version: "004_traffic_samples_and_aggregates", apply: migrateTrafficTables},
{version: "005_metric_sample", apply: migrateMetricSampleTable},
}
for _, m := range migrations {
if isApplied, err := migrationApplied(m.version); err != nil {
return err
} else if isApplied {
continue
}
if err := m.apply(); err != nil {
return err
}
if tx := sqliteDB.Exec("INSERT INTO schema_migrations(version) VALUES(?)", m.version); tx.Error != nil {
logrus.Errorf("sqlite migration mark err: %v", tx.Error)
return errors.New("sqlite migration mark err")
}
}
return nil
}
func seedBaseConfig() error {
// Сетевых настроек панели здесь нет: ими владеет оркестратор.
defaults := map[string]string{
constant.JwtSecret: "",
constant.Hysteria2Enable: "0",
constant.Hysteria2Config: "",
constant.Hysteria2TrafficTime: "10",
constant.Hysteria2ConfigRemark: "",
constant.ResetTrafficCron: "0 0 * * *",
constant.Hysteria2TrafficStatsSecret: "",
constant.PeerSecretKey: "",
constant.PeerSecretEncryptionKey: "",
}
for k, v := range defaults {
if tx := sqliteDB.Exec("INSERT OR IGNORE INTO config(key, value, remark) VALUES(?, ?, ?)", k, v, k); tx.Error != nil {
logrus.Errorf("sqlite seed config err: %v", tx.Error)
return errors.New("sqlite seed config err")
}
}
return nil
}
func migrateBaseConfig() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE DEFAULT '',
value TEXT NOT NULL DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS config_key_index ON config (key)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return seedBaseConfig()
}
func migrationApplied(version string) (bool, error) {
var count int64
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM schema_migrations WHERE version = ?", version).Scan(&count); tx.Error != nil {
logrus.Errorf("sqlite migration query err: %v", tx.Error)
return false, errors.New("sqlite migration query err")
}
return count > 0, nil
}
func migrateAdminPeerSplit() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS admin_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE DEFAULT '',
password_hash TEXT NOT NULL DEFAULT '',
status INTEGER NOT NULL DEFAULT 1,
force_password_change INTEGER NOT NULL DEFAULT 0,
last_login_at INTEGER NOT NULL DEFAULT 0,
password_changed_at INTEGER NOT NULL DEFAULT 0,
token_version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS admin_user_username_index ON admin_user (username)`,
`CREATE TABLE IF NOT EXISTS peer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
auth_id TEXT NOT NULL UNIQUE DEFAULT '',
secret_digest TEXT NOT NULL UNIQUE DEFAULT '',
secret_ciphertext TEXT NOT NULL DEFAULT '',
quota_bytes INTEGER NOT NULL DEFAULT 0,
download_bytes INTEGER NOT NULL DEFAULT 0,
upload_bytes INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER NOT NULL DEFAULT 0,
max_devices INTEGER NOT NULL DEFAULT 3,
disabled INTEGER NOT NULL DEFAULT 0,
banned_until INTEGER NOT NULL DEFAULT 0,
last_connection_at INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS peer_name_index ON peer (name)`,
`CREATE INDEX IF NOT EXISTS peer_auth_id_index ON peer (auth_id)`,
`CREATE INDEX IF NOT EXISTS peer_secret_digest_index ON peer (secret_digest)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func migrateLegacyAccounts() error {
if !tableExists("account") {
return nil
}
var accounts []entity.LegacyAccount
if tx := sqliteDB.Model(&entity.LegacyAccount{}).Order("id asc").Find(&accounts); tx.Error != nil {
logrus.Errorf("sqlite legacy account query err: %v", tx.Error)
return errors.New("sqlite legacy account query err")
}
nowMs := time.Now().UnixMilli()
for _, acc := range accounts {
if acc.Role != nil && *acc.Role == "admin" {
if acc.Username == nil || acc.Pass == nil {
continue
}
_, getErr := GetAdminUser("username = ?", *acc.Username)
if getErr == nil {
continue
}
status := int64(1)
if acc.Deleted != nil && *acc.Deleted == 1 {
status = 0
}
lastLogin := int64(0)
if acc.LoginAt != nil {
lastLogin = *acc.LoginAt
}
passwordChangedAt := nowMs
admin := entity.AdminUser{
Username: acc.Username,
PasswordHash: acc.Pass,
Status: &status,
ForcePasswordChange: acc.ForcePasswordChange,
LastLoginAt: &lastLogin,
PasswordChangedAt: &passwordChangedAt,
TokenVersion: func() *int64 { v := int64(1); return &v }(),
}
if _, saveErr := SaveAdminUser(admin); saveErr != nil {
return saveErr
}
continue
}
if acc.Username == nil || acc.ConPass == nil {
continue
}
_, getPeerErr := GetPeer("name = ?", *acc.Username)
if getPeerErr == nil {
continue
}
authId, authErr := util.RandomString(18)
if authErr != nil {
return authErr
}
peerSecretKey, keyErr := getOrCreatePeerSecretDigestKey()
if keyErr != nil {
return keyErr
}
secretDigest := util.HmacSHA256Hex(*acc.ConPass, peerSecretKey)
secretEncrypted := *acc.ConPass
quota := int64(0)
if acc.Quota != nil {
quota = *acc.Quota
}
download := int64(0)
if acc.Download != nil {
download = *acc.Download
}
upload := int64(0)
if acc.Upload != nil {
upload = *acc.Upload
}
expires := int64(0)
if acc.ExpireTime != nil {
expires = *acc.ExpireTime
}
maxDevices := int64(3)
if acc.DeviceNo != nil {
maxDevices = *acc.DeviceNo
}
disabled := int64(0)
if acc.Deleted != nil {
disabled = *acc.Deleted
}
bannedUntil := int64(0)
if acc.KickUtilTime != nil {
bannedUntil = *acc.KickUtilTime
}
lastConnection := int64(0)
if acc.ConAt != nil {
lastConnection = *acc.ConAt
}
remark := ""
if acc.Remark != nil {
remark = *acc.Remark
}
peer := entity.Peer{
Name: acc.Username,
Remark: &remark,
AuthId: &authId,
SecretDigest: &secretDigest,
SecretEncrypted: &secretEncrypted,
QuotaBytes: &quota,
DownloadBytes: &download,
UploadBytes: &upload,
ExpiresAt: &expires,
MaxDevices: &maxDevices,
Disabled: &disabled,
BannedUntil: &bannedUntil,
LastConnectionAt: &lastConnection,
}
if _, saveErr := SavePeer(peer); saveErr != nil {
return saveErr
}
}
return nil
}
func getOrCreateJwtSecret() (string, error) {
if existing, err := GetConfig("key = ?", constant.JwtSecret); err == nil {
if existing.Value != nil && strings.TrimSpace(*existing.Value) != "" {
return strings.TrimSpace(*existing.Value), nil
}
}
secret, secErr := util.RandomString(64)
if secErr != nil {
return "", secErr
}
if err := UpdateConfig([]string{constant.JwtSecret}, map[string]interface{}{"value": secret}); err != nil {
key := constant.JwtSecret
remark := "JWT signing secret"
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &secret, Remark: &remark}); saveErr != nil {
return "", saveErr
}
}
return secret, nil
}
func getOrCreatePeerSecretDigestKey() (string, error) {
if existing, err := GetConfig("key = ?", constant.PeerSecretKey); err == nil {
if existing.Value != nil && strings.TrimSpace(*existing.Value) != "" {
return strings.TrimSpace(*existing.Value), nil
}
}
keyValue, keyErr := util.RandomString(48)
if keyErr != nil {
return "", keyErr
}
if err := UpdateConfig([]string{constant.PeerSecretKey}, map[string]interface{}{"value": keyValue}); err != nil {
key := constant.PeerSecretKey
remark := "Peer secret digest key"
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &keyValue, Remark: &remark}); saveErr != nil {
return "", saveErr
}
}
return keyValue, nil
}
func archiveLegacyAccount() error {
if !tableExists("account") {
return nil
}
backupName := fmt.Sprintf("legacy_account_backup_%d", time.Now().Unix())
if tx := sqliteDB.Exec("ALTER TABLE account RENAME TO " + backupName); tx.Error != nil {
logrus.Errorf("sqlite legacy archive err: %v", tx.Error)
return errors.New("sqlite legacy archive err")
}
return nil
}
func migrateTrafficTables() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS traffic_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
peer_id INTEGER NOT NULL,
auth_id TEXT NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
sampled_at INTEGER NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_sample_time ON traffic_sample(sampled_at)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_sample_peer_time ON traffic_sample(peer_id, sampled_at)`,
`CREATE TABLE IF NOT EXISTS traffic_aggregate_hourly (
peer_id INTEGER NOT NULL,
hour_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(peer_id, hour_start)
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_hourly_hour_start ON traffic_aggregate_hourly(hour_start)`,
`CREATE TABLE IF NOT EXISTS traffic_aggregate_daily (
peer_id INTEGER NOT NULL,
day_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(peer_id, day_start)
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_daily_day_start ON traffic_aggregate_daily(day_start)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func migrateMetricSampleTable() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS metric_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sampled_at INTEGER NOT NULL,
cpu_percent REAL NOT NULL DEFAULT 0,
load1 REAL NOT NULL DEFAULT 0,
mem_used_bytes INTEGER NOT NULL DEFAULT 0,
mem_total_bytes INTEGER NOT NULL DEFAULT 0,
mem_percent REAL NOT NULL DEFAULT 0,
disk_path TEXT NOT NULL DEFAULT '/',
disk_used_bytes INTEGER NOT NULL DEFAULT 0,
disk_total_bytes INTEGER NOT NULL DEFAULT 0,
disk_percent REAL NOT NULL DEFAULT 0,
hysteria_running INTEGER NOT NULL DEFAULT 0,
online_peers INTEGER NOT NULL DEFAULT 0,
online_devices INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_metric_sample_time ON metric_sample(sampled_at)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func tableExists(tableName string) bool {
var count int64
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count); tx.Error != nil {
return false
}
return count > 0
}
func listSQLMigrationFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
files := make([]string, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasSuffix(strings.ToLower(name), ".sql") {
files = append(files, filepath.Join(dir, name))
}
}
sort.Strings(files)
return files, nil
}
func ensureTrafficStatsSecret() error {
envSecret := strings.TrimSpace(os.Getenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET"))
if envSecret != "" {
if existing, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret); err == nil {
if existing.Value != nil && strings.TrimSpace(*existing.Value) != envSecret {
return UpdateConfig([]string{constant.Hysteria2TrafficStatsSecret}, map[string]interface{}{"value": envSecret})
}
return nil
}
key := constant.Hysteria2TrafficStatsSecret
remark := "Hysteria2 trafficStats API secret"
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &envSecret, Remark: &remark}); saveErr != nil {
return saveErr
}
return nil
}
if existing, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret); err == nil {
if existing.Value != nil && strings.TrimSpace(*existing.Value) != "" {
return nil
}
secret, secErr := util.RandomString(32)
if secErr != nil {
return secErr
}
return UpdateConfig([]string{constant.Hysteria2TrafficStatsSecret}, map[string]interface{}{"value": secret})
}
secret, secErr := util.RandomString(32)
if secErr != nil {
return secErr
}
key := constant.Hysteria2TrafficStatsSecret
remark := "Hysteria2 trafficStats API secret"
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &secret, Remark: &remark}); saveErr != nil {
return saveErr
}
return nil
}
func CloseSqliteDB() error {
if sqliteDB != nil {
db, err := sqliteDB.DB()
if err != nil {
logrus.Errorf("sqlite err: %v", err)
return errors.New("sqlite err")
}
if err = db.Close(); err != nil {
logrus.Errorf("sqlite close err: %v", err)
return errors.New("sqlite close err")
}
}
return nil
}
func IsSqliteReady() bool {
if sqliteDB == nil {
return false
}
db, err := sqliteDB.DB()
if err != nil {
return false
}
if err := db.Ping(); err != nil {
return false
}
return true
}
func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
var num int64 = 1
var size int64 = 10
if pageNum != nil && *pageNum > 0 {
num = *pageNum
}
if pageSize != nil && *pageSize > 0 {
size = *pageSize
}
return func(db *gorm.DB) *gorm.DB {
return db.Offset(int((num - 1) * size)).Limit(int(size))
}
}