217 lines
6.8 KiB
Go
217 lines
6.8 KiB
Go
package nginxactivation
|
||
|
||
import (
|
||
"context"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"net"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"syscall"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 实机验收显式指定 Nginx 路径;仅启动独立进程,不使用系统站点、PID 或服务单元。
|
||
func TestRotationWithRealNginx(t *testing.T) {
|
||
f := newRotationFixture(t)
|
||
useRealNginx(t, f)
|
||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
defer cancel()
|
||
id, err := f.rotation.Stage(ctx, f.candidate.chain, f.candidate.key)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f.upstream.active.Store(&f.candidate.pair)
|
||
if err := f.rotation.Commit(ctx, id); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f.assertStatus(t, "committed")
|
||
next := f.authority.issue(t, testHostname, time.Now().Add(-time.Hour), time.Now().Add(72*time.Hour))
|
||
if _, err := f.rotation.Stage(ctx, next.chain, next.key); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 模拟调用方退出后由新组件恢复,真实入口必须重新提供上一张已提交证书。
|
||
restarted := f.rotation
|
||
if err := restarted.Recover(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f.assertStatus(t, "rolled_back")
|
||
if _, err := restarted.Proxy.Check(ctx, f.candidate.pair.Certificate[0]); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func useRealNginx(t *testing.T, f *rotationFixture) {
|
||
t.Helper()
|
||
binary := os.Getenv("SCERT_TEST_NGINX_BINARY")
|
||
if binary == "" {
|
||
t.Skip("未指定隔离验收使用的 Nginx 可执行文件")
|
||
}
|
||
if !filepath.IsAbs(binary) {
|
||
t.Fatal("测试 Nginx 路径必须是绝对路径")
|
||
}
|
||
prefix := t.TempDir()
|
||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
address := listener.Addr().String()
|
||
if err := listener.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rootPath := filepath.Join(prefix, "ca.pem")
|
||
writeTestFile(t, rootPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: f.authority.certificate.Raw}))
|
||
configPath := filepath.Join(prefix, "nginx.conf")
|
||
configuration := fmt.Sprintf(`worker_processes 1;
|
||
pid %s/nginx.pid;
|
||
error_log stderr warn;
|
||
events { worker_connections 64; }
|
||
http {
|
||
access_log off;
|
||
client_body_temp_path %s/client;
|
||
proxy_temp_path %s/proxy;
|
||
fastcgi_temp_path %s/fastcgi;
|
||
uwsgi_temp_path %s/uwsgi;
|
||
scgi_temp_path %s/scgi;
|
||
server {
|
||
listen %s ssl;
|
||
server_name %s;
|
||
include %s/active.conf;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
location / {
|
||
proxy_pass https://%s;
|
||
proxy_set_header Host %s;
|
||
proxy_ssl_server_name on;
|
||
proxy_ssl_name %s;
|
||
proxy_ssl_verify on;
|
||
proxy_ssl_trusted_certificate %s;
|
||
}
|
||
}
|
||
}
|
||
`, prefix, prefix, prefix, prefix, prefix, prefix, address, testHostname,
|
||
f.rotation.Directory, f.upstream.Address, net.JoinHostPort(testHostname, endpointPort(t, f.upstream.Address)), testHostname, rootPath)
|
||
writeTestFile(t, configPath, []byte(configuration))
|
||
reloader := CommandReloader{Binary: binary, Config: configPath, Prefix: prefix + "/"}
|
||
if err := reloader.Check(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
logFile, err := os.OpenFile(filepath.Join(prefix, "process.log"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
command := exec.Command(binary, "-p", prefix+"/", "-c", configPath, "-g", "daemon off;")
|
||
command.Stdout, command.Stderr = logFile, logFile
|
||
command.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C"}
|
||
if err := command.Start(); err != nil {
|
||
_ = logFile.Close()
|
||
t.Fatal(err)
|
||
}
|
||
finished := make(chan error, 1)
|
||
go func() { finished <- command.Wait() }()
|
||
t.Cleanup(func() {
|
||
_ = command.Process.Signal(syscall.SIGQUIT)
|
||
select {
|
||
case err := <-finished:
|
||
if err != nil {
|
||
t.Errorf("隔离 Nginx 退出失败:%v", err)
|
||
}
|
||
case <-time.After(5 * time.Second):
|
||
_ = command.Process.Kill()
|
||
<-finished
|
||
t.Error("隔离 Nginx 未在时限内退出")
|
||
}
|
||
_ = logFile.Close()
|
||
if t.Failed() {
|
||
content, _ := os.ReadFile(filepath.Join(prefix, "process.log"))
|
||
t.Logf("隔离 Nginx 日志:\n%s", content)
|
||
}
|
||
})
|
||
f.rotation.Proxy = Endpoint{Address: address, Hostname: testHostname, Roots: f.authority.roots}
|
||
f.rotation.Reloader = reloader
|
||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
defer cancel()
|
||
if err := f.rotation.Proxy.Wait(ctx, f.old.pair.Certificate[0]); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestCoordinatorWithRealNginxAndIPC(t *testing.T) {
|
||
for _, failure := range []string{"none", "stage_response", "commit_response"} {
|
||
t.Run(failure, func(t *testing.T) {
|
||
f := newCoordinatorFixture(t)
|
||
useRealNginx(t, f.rotationFixture)
|
||
f.previous.ProxyAddress = f.rotation.Proxy.Address
|
||
f.candidate.ProxyAddress = f.rotation.Proxy.Address
|
||
f.session.current = f.previous
|
||
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
||
defer stopLocalService(t, cancel, done)
|
||
proxy := coordinatedTestProxy{ProxyClient: client}
|
||
if failure == "stage_response" {
|
||
proxy.stage = func(ctx context.Context, id string, chain, key []byte) (string, error) {
|
||
if _, err := client.StageRequest(ctx, id, chain, key); err != nil {
|
||
return "", err
|
||
}
|
||
return "", fmt.Errorf("注入暂存应答丢失")
|
||
}
|
||
}
|
||
if failure == "commit_response" {
|
||
proxy.commit = func(ctx context.Context, id string) error {
|
||
if err := client.Commit(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
return fmt.Errorf("注入提交应答丢失")
|
||
}
|
||
}
|
||
f.coordinator.Proxy = proxy
|
||
ctx, stop := context.WithTimeout(t.Context(), 20*time.Second)
|
||
defer stop()
|
||
err := f.coordinator.Activate(ctx, f.candidate.CertificateFiles)
|
||
want := "committed"
|
||
leaf := f.rotationFixture.candidate.pair.Certificate[0]
|
||
if failure == "stage_response" {
|
||
want, leaf = "rolled_back", f.old.pair.Certificate[0]
|
||
if err == nil {
|
||
t.Fatal("暂存应答丢失应回退并报告失败")
|
||
}
|
||
} else if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if record := f.record(t); record.Phase != want || record.RequestID != record.ProxyID {
|
||
t.Fatal("真实 Nginx 协调终态错误", record)
|
||
}
|
||
if _, err := f.rotation.Proxy.Check(ctx, leaf); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := f.rotation.Upstream.Check(ctx, leaf); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := f.coordinator.Ready(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func endpointPort(t *testing.T, address string) string {
|
||
t.Helper()
|
||
_, port, err := net.SplitHostPort(address)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return port
|
||
}
|
||
|
||
func TestCommandReloaderRejectsRelativePolicy(t *testing.T) {
|
||
for _, policy := range []CommandReloader{
|
||
{Binary: "nginx", Config: "/etc/nginx/nginx.conf", Prefix: "/etc/nginx"},
|
||
{Binary: "/usr/sbin/nginx", Config: "nginx.conf", Prefix: "/etc/nginx"},
|
||
{Binary: "/usr/sbin/nginx", Config: "/etc/nginx/nginx.conf", Prefix: "."},
|
||
} {
|
||
if err := policy.Check(context.Background()); err == nil {
|
||
t.Fatal("不应执行相对路径策略")
|
||
}
|
||
}
|
||
}
|