190 lines
5.9 KiB
Go
190 lines
5.9 KiB
Go
package nginxactivation
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/processlock"
|
||
"golang.org/x/sys/unix"
|
||
)
|
||
|
||
func initializationFixture(t *testing.T) (*rotationFixture, string, string) {
|
||
t.Helper()
|
||
f := newRotationFixture(t)
|
||
f.rotation.Directory = t.TempDir()
|
||
if err := os.Chmod(f.rotation.Directory, 0o700); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
material := t.TempDir()
|
||
chain, key := filepath.Join(material, "fullchain.pem"), filepath.Join(material, "key.pem")
|
||
writeTestFile(t, chain, f.old.chain)
|
||
writeTestFile(t, key, f.old.key)
|
||
return f, chain, key
|
||
}
|
||
|
||
func TestInitializeCreatesPrivateBaselineWithoutReload(t *testing.T) {
|
||
f, chain, key := initializationFixture(t)
|
||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||
defer cancel()
|
||
id, err := f.rotation.Initialize(ctx, chain, key)
|
||
if err != nil || !validID(id) {
|
||
t.Fatal("初始化失败或版本编号无效", err)
|
||
}
|
||
if f.reloader.reloadCount != 0 || f.reloader.checkCount != 1 {
|
||
t.Fatal("初始化只能检查既有配置,不能重载")
|
||
}
|
||
for path, mode := range map[string]os.FileMode{
|
||
id: 0o700, filepath.Join(id, "fullchain.pem"): 0o600,
|
||
filepath.Join(id, "key.pem"): 0o600, includeName: 0o600,
|
||
} {
|
||
info, err := os.Lstat(filepath.Join(f.rotation.Directory, path))
|
||
if err != nil || info.Mode().Perm() != mode {
|
||
t.Fatalf("初始化材料权限错误:%s,%v", path, err)
|
||
}
|
||
}
|
||
root, err := os.OpenRoot(f.rotation.Directory)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer root.Close()
|
||
include, err := readRegular(root, includeName, 4096)
|
||
if err != nil || !bytes.Equal(include, renderInclude(f.rotation.Directory, id)) {
|
||
t.Fatal("专用引用格式错误", err)
|
||
}
|
||
if err := verifyIncludeMaterial(root, include, f.old.pair.Certificate[0]); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(f.rotation.Directory, journalName)); !errors.Is(err, os.ErrNotExist) {
|
||
t.Fatal("初始化不应创建轮换事务", err)
|
||
}
|
||
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
|
||
t.Fatal("不能重复初始化已有状态")
|
||
}
|
||
if err := requireInclude(root, include); err != nil {
|
||
t.Fatal("重复初始化改变了原引用", err)
|
||
}
|
||
}
|
||
|
||
func TestInitializeRejectsUnsafeMaterial(t *testing.T) {
|
||
for _, scenario := range []string{
|
||
"key-readable", "key-writable", "parent-writable", "symlink", "hardlink", "fifo",
|
||
"large", "mismatch", "different-leaf", "upstream-mismatch", "relative", "missing",
|
||
} {
|
||
t.Run(scenario, func(t *testing.T) {
|
||
f, chain, key := initializationFixture(t)
|
||
switch scenario {
|
||
case "key-readable", "key-writable", "parent-writable":
|
||
path, mode := key, os.FileMode(0o644)
|
||
if scenario == "key-writable" {
|
||
mode = 0o666
|
||
}
|
||
if scenario == "parent-writable" {
|
||
path, mode = filepath.Dir(key), 0o777
|
||
}
|
||
if err := os.Chmod(path, mode); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
case "symlink":
|
||
if err := os.Symlink(key, key+".link"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
key += ".link"
|
||
case "hardlink":
|
||
if err := os.Link(key, key+".link"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
case "fifo":
|
||
key += ".fifo"
|
||
if err := unix.Mkfifo(key, 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
case "large":
|
||
writeTestFile(t, chain, make([]byte, (1<<20)+1))
|
||
case "mismatch":
|
||
writeTestFile(t, key, f.candidate.key)
|
||
case "different-leaf":
|
||
writeTestFile(t, chain, f.candidate.chain)
|
||
writeTestFile(t, key, f.candidate.key)
|
||
case "upstream-mismatch":
|
||
f.upstream.active.Store(&f.candidate.pair)
|
||
case "relative":
|
||
key = "key.pem"
|
||
case "missing":
|
||
key += ".missing"
|
||
}
|
||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||
defer cancel()
|
||
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
|
||
t.Fatal("不应接受不可信或不一致的初始化材料")
|
||
}
|
||
if f.reloader.checkCount != 0 || f.reloader.reloadCount != 0 {
|
||
t.Fatal("材料预检失败不应执行 Nginx 命令")
|
||
}
|
||
assertInitializationEmpty(t, f.rotation.Directory)
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestInitializePreservesExistingState(t *testing.T) {
|
||
for _, scenario := range []string{"include", "transaction", "partial", "external-change", "config-failure", "cancel", "locked"} {
|
||
t.Run(scenario, func(t *testing.T) {
|
||
f, chain, key := initializationFixture(t)
|
||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||
defer cancel()
|
||
name, content := "", []byte("保留原有状态\n")
|
||
switch scenario {
|
||
case "include", "transaction", "partial":
|
||
name = map[string]string{"include": includeName, "transaction": journalName, "partial": ".pending-old"}[scenario]
|
||
writeTestFile(t, filepath.Join(f.rotation.Directory, name), content)
|
||
case "external-change":
|
||
name = includeName
|
||
f.reloader.check = func(int) error {
|
||
writeTestFile(t, filepath.Join(f.rotation.Directory, name), content)
|
||
return nil
|
||
}
|
||
case "config-failure":
|
||
f.reloader.check = func(int) error { return errors.New("注入配置检查失败") }
|
||
case "cancel":
|
||
cancel()
|
||
case "locked":
|
||
lock, err := processlock.Acquire(filepath.Join(f.rotation.Directory, ".activation.lock"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer lock.Close()
|
||
}
|
||
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
|
||
t.Fatal("已有状态、取消或故障不应初始化成功")
|
||
}
|
||
if f.reloader.reloadCount != 0 {
|
||
t.Fatal("初始化不能重载 Nginx")
|
||
}
|
||
if name == "" {
|
||
assertInitializationEmpty(t, f.rotation.Directory)
|
||
return
|
||
}
|
||
actual, err := os.ReadFile(filepath.Join(f.rotation.Directory, name))
|
||
if err != nil || !bytes.Equal(actual, content) {
|
||
t.Fatal("初始化不能覆盖已有状态", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func assertInitializationEmpty(t *testing.T, directory string) {
|
||
t.Helper()
|
||
root, err := os.OpenRoot(directory)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer root.Close()
|
||
if err := requireUninitialized(root); err != nil {
|
||
t.Fatal("预检失败不应留下证书或引用", err)
|
||
}
|
||
}
|