- 首次启动落试用起点(DATA_DIR/.trial-start 0600 + 库内 settings 双写,取最早防分次续命);
窗口内 IsPro() 等效 Pro——日志分析/告警/AI 助手/审计全解锁,到期自动回免费版
- 防刷三层:时间戳双写(删任一不重发)+ 机器级指纹(machine-id / hostname+cpu,
故意不含 DATA_DIR 特征——目录特征会因删除重建漂移等于后门);指纹命中则落已过期起点
- LicenseStatusPayload 返回 trial{active,daysLeft} 与 plan=trial;
设置页授权区显示「Pro 试用中 · 剩余 N 天」提示条
- 单测:全新安装 14 天/到期回落/删时间戳重装不重发(指纹防刷)三用例
- 滥用评估:删库重装同机器被指纹拦截;换机器/VM 必然可重试(接受,Pro 功能零边际成本,
追杀到硬件级得不偿失);正式授权随时激活覆盖试用
238 lines
5.8 KiB
Go
238 lines
5.8 KiB
Go
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_<hash> = 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
|
|
}
|