package services // 14 天 Pro 试用:首次启动落一个安装时间戳,窗口内等效 Pro(无需授权码)。 // 滥用防护(多层,删库重装只能重置最外层): // 1. DATA_DIR/.trial-start(0600)——用户可删,快但易防 // 2. 库内 settings.trial_started_at——随 lokistack.json 走,删库重装会重置 // 3. 主机指纹(机器级):/etc/machine-id 优先(容器内不稳定)、回退 os.Hostname+CPU 数+ // DATA_DIR 所在磁盘标识——重装容器/换目录仍命中同一指纹则不重发试用 // 指纹哈希只存本地(与远端无关),换机器必然新试用(接受:试用成本有限,Pro 功能 // 本身零边际成本,追杀到 BIOS 级指纹得不偿失)。 import ( "crypto/sha256" "encoding/hex" "fmt" "os" "runtime" "strconv" "strings" "time" "lokistack/config" "lokistack/models" "lokistack/repository" ) const trialDays = 14 // TrialRemainingDays 剩余试用天数(0=无试用;>0=试用中)。trialStart==0 表示从未启动过计时。 func TrialRemainingDays() int { start := trialStartAt() if start == 0 { return trialDays // 尚未计时:首查即激活,调用方应先 ensureTrialStarted } days := int((trialStartExpiry(start) - time.Now().Unix()) / 86400) if days < 0 { days = 0 } return days } // TrialActive 试用是否生效(Pro 等效) func TrialActive() bool { start := trialStartAt() if start == 0 { return true // 未计时视为可用(首次调用会落时间戳) } return time.Now().Unix() < trialStartExpiry(start) } func trialStartExpiry(start int64) int64 { return start + int64(trialDays)*86400 } // EnsureTrialStarted 首次启动时落试用起点(幂等)。三层防线:文件 > 库 > 指纹。 // 指纹命中"该机器已用过试用"则不重发(防删库重装刷试用)。 func EnsureTrialStarted() { if trialStartAt() != 0 { return } now := time.Now().Unix() // 库内 + 文件双写(任一存在即以最早的为准——防先删文件再删库分次续命) if start := earliestKnownStart(now); start < now { persistTrialStart(start) return } // 本机指纹库:命中则这台机器已消费过试用,直接落一个已过期起点(不重发) fp := hostTrialFingerprint() if fp != "" && fingerprintConsumed(fp) { persistTrialStart(now - int64(trialDays)*86400 - 1) return } persistTrialStart(now) if fp != "" { recordFingerprint(fp) } } func trialStartAt() int64 { if start := trialStartFromFile(); start > 0 { return start } return trialStartFromDB() } func trialStartFromFile() int64 { cfg := trialDataDir() raw, err := os.ReadFile(cfg + "/.trial-start") if err != nil { return 0 } v, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64) if err != nil || v <= 0 { return 0 } return v } func trialStartFromDB() int64 { db := repository.GetDB() if db == nil { return 0 } db.RLock() defer db.RUnlock() for _, s := range db.Settings { if s.Key == "trial_started_at" { v, err := strconv.ParseInt(s.Value, 10, 64) if err == nil && v > 0 { return v } } } return 0 } // earliestKnownStart 文件与库中较早的起点(都无则返回 now) func earliestKnownStart(now int64) int64 { file := trialStartFromFile() dbv := trialStartFromDB() switch { case file > 0 && dbv > 0: return min64(file, dbv) case file > 0: return file case dbv > 0: return dbv default: return now } } func persistTrialStart(start int64) { // 库 db := repository.GetDB() if db != nil { db.Lock() exists := false for i := range db.Settings { if db.Settings[i].Key == "trial_started_at" { v, _ := strconv.ParseInt(db.Settings[i].Value, 10, 64) if v == 0 || start < v { db.Settings[i].Value = strconv.FormatInt(start, 10) } exists = true break } } if !exists { db.Settings = append(db.Settings, newSetting("trial_started_at", strconv.FormatInt(start, 10))) } db.RequestSave() db.Unlock() } // 文件(0600) dir := trialDataDir() _ = os.MkdirAll(dir, 0o700) _ = os.WriteFile(dir+"/.trial-start", []byte(strconv.FormatInt(start, 10)), 0o600) } // hostTrialFingerprint 机器级指纹(与 DATA_DIR 完全无关,删目录/换挂载不变): // machine-id > hostname+cpu。故意不含任何 DATA_DIR 特征——目录 mtime 会因删除/重建漂移, // 含它等于给刷试用留后门。 func hostTrialFingerprint() string { if raw, err := os.ReadFile("/etc/machine-id"); err == nil { if id := strings.TrimSpace(string(raw)); len(id) >= 8 { return hashPart(id, "mid") } } host, err := os.Hostname() if err != nil || host == "" { return "" } return hashPart(fmt.Sprintf("%s|%d", host, runtime.NumCPU()), "host") } func hashPart(seed, prefix string) string { sum := sha256.Sum256([]byte("lokistack-trial|" + seed)) return prefix + "-" + hex.EncodeToString(sum[:10]) } // 指纹记录:库内 settings.trial_fp_ = 1 func fingerprintConsumed(fp string) bool { db := repository.GetDB() if db == nil { return false } key := "trial_fp_" + fp db.RLock() defer db.RUnlock() for _, s := range db.Settings { if s.Key == key && s.Value == "1" { return true } } return false } func recordFingerprint(fp string) { db := repository.GetDB() if db == nil { return } key := "trial_fp_" + fp db.Lock() defer db.Unlock() for _, s := range db.Settings { if s.Key == key { return } } db.Settings = append(db.Settings, newSetting(key, "1")) db.RequestSave() } func trialDataDir() string { if trialDataDirOverride != "" { return trialDataDirOverride } return config.GetConfig().DataDir } // trialDataDirOverride 测试注入(空=用 config.DataDir) var trialDataDirOverride string func newSetting(key, value string) models.SystemSetting { db := repository.GetDB() id := uint(0) if db != nil { id = db.NextID("settings") } return models.SystemSetting{ID: id, Key: key, Value: value} } func min64(a, b int64) int64 { if a < b { return a } return b }