This commit is contained in:
@@ -52,7 +52,9 @@ import (
|
||||
// 0.12.8:Agent Bearer Token 改为常量时间比较,避免认证路径上的长度/内容时序泄漏。
|
||||
// 0.12.9:限流状态文件损坏时拒绝覆盖,避免保存单条规则时清空其他域名的限流配置。
|
||||
// 0.13.0:默认只监听回环地址,非回环监听必须显式允许;状态接口暴露监听范围与传输安全。
|
||||
const AgentVersion = "0.13.0"
|
||||
// 0.13.1:nginx 版本备份目录改为隐藏目录(.<name>.versions)并自动迁移旧目录,
|
||||
// 修掉旧 <name>.versions 目录被 include * 当配置文件读取、nginx -t 报 pread() "Is a directory" 的问题。
|
||||
const AgentVersion = "0.13.1"
|
||||
|
||||
// Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。
|
||||
type Config struct {
|
||||
@@ -760,6 +762,7 @@ func detectNginxConfDir(binary string) (string, error) {
|
||||
})
|
||||
var lastReason string
|
||||
for _, dir := range dirs {
|
||||
migrateLegacyVersionDirs(dir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
lastReason = fmt.Sprintf("%s: %v", dir, err)
|
||||
continue
|
||||
@@ -1275,9 +1278,30 @@ func (s *Server) routePath(domain string) string {
|
||||
}
|
||||
|
||||
func (s *Server) validateNginx() error {
|
||||
migrateLegacyVersionDirs(s.cfg.NginxConfDir)
|
||||
return runCommand(s.nginxPath(), "-t")
|
||||
}
|
||||
|
||||
// migrateLegacyVersionDirs 把 conf 目录下的 <name>.versions 目录迁移为隐藏目录。
|
||||
// 旧布局会被 nginx 的 include * 当成配置文件读取,报 pread() "Is a directory" 导致 nginx -t 失败。
|
||||
func migrateLegacyVersionDirs(dir string) {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
return
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || entry.Name() == ".versions" || !strings.HasSuffix(entry.Name(), ".versions") {
|
||||
continue
|
||||
}
|
||||
base := strings.TrimSuffix(filepath.Join(dir, entry.Name()), ".versions")
|
||||
_ = fsutil.MigrateLegacyVersions(base)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) reloadNginx() error {
|
||||
nginx := s.nginxPath()
|
||||
if nginx == "" {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrateLegacyVersionDirs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "fonuagent-https.conf")
|
||||
legacy := path + ".versions"
|
||||
if err := os.MkdirAll(legacy, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := "20260101T000000.000-abcd.conf"
|
||||
if err := os.WriteFile(filepath.Join(legacy, name), []byte("old"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
migrateLegacyVersionDirs(dir)
|
||||
|
||||
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
|
||||
t.Fatalf("旧版本目录应被移出 nginx include 范围:%v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dir, ".fonuagent-https.conf.versions"))
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("隐藏版本目录应保留 1 个版本:%v %v", entries, err)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -61,15 +61,62 @@ func Backup(path string) error {
|
||||
return WriteFileAtomic(path+".bak", data, 0o600)
|
||||
}
|
||||
|
||||
// VersionsDir 返回某文件的版本目录(<path>.versions),供列表 / 回滚读取。
|
||||
// VersionsDir 返回某文件的版本目录(.<name>.versions),供列表 / 回滚读取。
|
||||
// 目录名以点号开头,避免被 nginx 的 include <dir>/* 当成配置文件读取。
|
||||
func VersionsDir(path string) string {
|
||||
return filepath.Join(filepath.Dir(path), "."+filepath.Base(path)+".versions")
|
||||
}
|
||||
|
||||
// legacyVersionsDir 是旧布局(<path>.versions)。它会被 nginx 的 include * 误读为配置文件,
|
||||
// 因此读取 / 备份前统一迁移到隐藏目录。
|
||||
func legacyVersionsDir(path string) string {
|
||||
return path + ".versions"
|
||||
}
|
||||
|
||||
// BackupVersioned 把现有文件留存为 <path>.versions/<时间戳>-<随机>.conf,并只保留最近 keep 份。
|
||||
// MigrateLegacyVersions 把旧版本目录迁移到隐藏目录。只移动内容,不删除历史版本;
|
||||
// 迁移后旧目录已空,删除它是为了让 nginx 的 include * 不再命中该目录。
|
||||
func MigrateLegacyVersions(path string) error {
|
||||
legacy := legacyVersionsDir(path)
|
||||
info, err := os.Stat(legacy)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
target := VersionsDir(path)
|
||||
if _, err := os.Stat(target); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(legacy, target); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(target, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(legacy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
src := filepath.Join(legacy, entry.Name())
|
||||
dst := filepath.Join(target, entry.Name())
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Remove(legacy)
|
||||
}
|
||||
|
||||
// BackupVersioned 把现有文件留存为 .<name>.versions/<时间戳>-<随机>.conf,并只保留最近 keep 份。
|
||||
// 文件名前缀是 UTC 时间戳(同一秒内用随机后缀避开冲突),因此目录内按文件名倒序就是版本新旧顺序。
|
||||
// 文件不存在视为无需备份,返回空串;keep <= 0 表示不剪枝。
|
||||
func BackupVersioned(path string, keep int) (string, error) {
|
||||
if err := MigrateLegacyVersions(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
@@ -107,6 +154,9 @@ type VersionEntry struct {
|
||||
|
||||
// ListVersions 按新的在前列出版本;版本目录不存在时返回空列表(首次备份前是正常状态)。
|
||||
func ListVersions(path string) ([]VersionEntry, error) {
|
||||
if err := MigrateLegacyVersions(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(VersionsDir(path))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -135,6 +185,9 @@ func ReadVersion(path, name string) ([]byte, error) {
|
||||
if name == "" || name != filepath.Base(name) || name == "." || name == ".." {
|
||||
return nil, fmt.Errorf("版本名无效")
|
||||
}
|
||||
if err := MigrateLegacyVersions(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(VersionsDir(path), name))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
@@ -198,3 +198,34 @@ func TestReadVersionRejectsTraversal(t *testing.T) {
|
||||
t.Fatalf("版本目录不存在时应返回空列表且无错误:%v %v", versions, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionsDirIsHidden(t *testing.T) {
|
||||
path := filepath.Join("/etc/nginx/sites-enabled", "fonuagent-https.conf")
|
||||
base := filepath.Base(VersionsDir(path))
|
||||
if !strings.HasPrefix(base, ".") {
|
||||
t.Fatalf("版本目录应以点号开头,避免被 nginx 的 include * 加载:%s", base)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateLegacyVersions(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "fonuagent-https.conf")
|
||||
legacy := path + ".versions"
|
||||
if err := os.MkdirAll(legacy, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := "20260101T000000.000-abcd.conf"
|
||||
if err := os.WriteFile(filepath.Join(legacy, name), []byte("old"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
versions, err := ListVersions(path)
|
||||
if err != nil || len(versions) != 1 {
|
||||
t.Fatalf("迁移后应保留 1 个版本:%v %v", versions, err)
|
||||
}
|
||||
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
|
||||
t.Fatalf("旧版本目录应被移走,避免 nginx include * 命中:%v", err)
|
||||
}
|
||||
data, err := ReadVersion(path, versions[0].Name)
|
||||
if err != nil || string(data) != "old" {
|
||||
t.Fatalf("迁移后版本内容应可读:%q %v", data, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func (m *Manager) Apply(ctx context.Context, rules []proxy.Rule, certs []CertSou
|
||||
}
|
||||
|
||||
currentPath := m.cfg.NginxConfigPath()
|
||||
// 覆盖前留退路:.bak 保留最近一次(兼容既有读取方),.versions/ 保留最近 10 份(供版本对比与回滚)
|
||||
// 覆盖前留退路:.bak 保留最近一次(兼容既有读取方),隐藏的 .versions 目录保留最近 10 份(供版本对比与回滚)
|
||||
if err := fsutil.Backup(currentPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return ApplyResult{}, err
|
||||
|
||||
Reference in New Issue
Block a user