fix(web,agent): P1 正确性与鉴权加固,P2 抽 fsutil 并补测试(v1.2.8)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateRoute(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
req routeRequest
|
||||
ok bool
|
||||
}{
|
||||
{"正常", routeRequest{Domain: "a.example.com", Upstream: "127.0.0.1:8080"}, true},
|
||||
{"非法域名", routeRequest{Domain: "bad domain", Upstream: "127.0.0.1:8080"}, false},
|
||||
{"空上游", routeRequest{Domain: "a.example.com"}, false},
|
||||
{"上游带分号", routeRequest{Domain: "a.example.com", Upstream: "127.0.0.1:80; rm -rf /"}, false},
|
||||
{"相对证书目录", routeRequest{Domain: "a.example.com", Upstream: "127.0.0.1:80", TLS: true, CertDir: "certs"}, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err := validateRoute(c.req)
|
||||
if c.ok && err != nil {
|
||||
t.Errorf("%s:期望通过,实际 %v", c.name, err)
|
||||
}
|
||||
if !c.ok && err == nil {
|
||||
t.Errorf("%s:期望报错", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNginxRouteRendersUpstream:vhost 模板的关键行(标记、server_name、proxy_pass)。
|
||||
// 不构造 Basic Auth 场景——那条路径会往 /var/lib/fonu-agent 写 htpasswd,不适合在单测里触发。
|
||||
func TestNginxRouteRendersUpstream(t *testing.T) {
|
||||
out := nginxRoute(routeRequest{Domain: "a.example.com", Upstream: "127.0.0.1:8080"}, "")
|
||||
if !strings.HasPrefix(out, "# Managed by fonu-agent.") {
|
||||
t.Errorf("缺少托管标记,其它工具无法识别该文件归属:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "server_name a.example.com;") {
|
||||
t.Errorf("缺少 server_name:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "proxy_pass http://127.0.0.1:8080;") {
|
||||
t.Errorf("缺少 proxy_pass 回源:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "listen 443") {
|
||||
t.Errorf("未启用 TLS 时不应监听 443:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileSHA256 覆盖 frps 安装包的校验路径。
|
||||
func TestFileSHA256(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "payload.txt")
|
||||
if err := os.WriteFile(path, []byte("hello"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := fileSHA256(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
if got != want {
|
||||
t.Fatalf("sha256 不符:got %s", got)
|
||||
}
|
||||
}
|
||||
+25
-31
@@ -25,6 +25,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fonu/fonu/internal/fsutil"
|
||||
"github.com/fonu/fonu/internal/sysinfo"
|
||||
)
|
||||
|
||||
@@ -38,7 +39,8 @@ import (
|
||||
// 0.11.0:新增 /api/v1/certs 证书清单与 /api/v1/nginx/reload 显式重载(回传校验/重载失败原因)。
|
||||
// 0.12.0:frps 安装包的 proxy 参数改白名单 + 下载后校验 SHA-256;Basic Auth 自动生成的密码不再写进
|
||||
// nginx 配置注释(改为只在 PUT 响应里回一次)。
|
||||
const AgentVersion = "0.12.0"
|
||||
// 0.12.1:frps 启停失败如实回报(不再无条件写「已启动」);HTTP 服务补 ReadTimeout/IdleTimeout。
|
||||
const AgentVersion = "0.12.1"
|
||||
|
||||
// Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。
|
||||
type Config struct {
|
||||
@@ -181,7 +183,7 @@ func (s *Server) Handler() http.Handler {
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
server := &http.Server{Addr: s.cfg.ListenAddr, Handler: s.Handler(), ReadHeaderTimeout: 10 * time.Second}
|
||||
server := &http.Server{Addr: s.cfg.ListenAddr, Handler: s.Handler(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 60 * time.Second, IdleTimeout: 2 * time.Minute}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -341,18 +343,27 @@ func (s *Server) installFRPS(w http.ResponseWriter, r *http.Request) {
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
}
|
||||
// nohup 手工跑的 frps 让位给 systemd;已在 systemd 下运行则重启以加载新二进制
|
||||
if exec.Command("systemctl", "is-active", "--quiet", "frps").Run() != nil {
|
||||
if exec.Command("pgrep", "-x", "frps").Run() == nil {
|
||||
_ = exec.Command("pkill", "-x", "frps").Run()
|
||||
time.Sleep(time.Second)
|
||||
notes = append(notes, "已停止 nohup 方式运行的旧 frps 进程")
|
||||
}
|
||||
_ = exec.Command("systemctl", "enable", "--now", "frps").Run()
|
||||
notes = append(notes, "frps.service 已启动")
|
||||
} else {
|
||||
_ = exec.Command("systemctl", "restart", "frps").Run()
|
||||
notes = append(notes, "frps.service 已重启以加载新版本")
|
||||
active := exec.Command("systemctl", "is-active", "--quiet", "frps").Run() == nil
|
||||
if !active && exec.Command("pgrep", "-x", "frps").Run() == nil {
|
||||
_ = exec.Command("pkill", "-x", "frps").Run()
|
||||
time.Sleep(time.Second)
|
||||
notes = append(notes, "已停止 nohup 方式运行的旧 frps 进程")
|
||||
}
|
||||
args := []string{"restart", "frps"}
|
||||
label := "已重启以加载新版本"
|
||||
if !active {
|
||||
args = []string{"enable", "--now", "frps"}
|
||||
label = "已启动"
|
||||
}
|
||||
// 启动失败必须如实回报:之前忽略 err 后无条件写「已启动」,运维会以为服务在跑
|
||||
if out, err := exec.Command("systemctl", args...).CombinedOutput(); err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": false, "version": version, "binary": installPath, "notes": notes,
|
||||
"error": fmt.Sprintf("frps 二进制已安装,但 systemctl %s 失败:%v:%s", strings.Join(args, " "), err, tailText(string(out), 400)),
|
||||
})
|
||||
return
|
||||
}
|
||||
notes = append(notes, "frps.service "+label)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": version, "binary": installPath, "notes": notes})
|
||||
}
|
||||
|
||||
@@ -1511,24 +1522,7 @@ func atomicWrite(path string, data []byte, mode os.FileMode) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".fonu-agent-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, path)
|
||||
return fsutil.WriteFileAtomic(path, data, mode)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
|
||||
Reference in New Issue
Block a user