226 lines
6.8 KiB
Go
226 lines
6.8 KiB
Go
package nginxactivation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func startLocalService(t *testing.T, rotation Rotation, allowed uint32) (Client, context.CancelFunc, <-chan error) {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "activation.sock")
|
|
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener.SetUnlinkOnClose(false)
|
|
if err := os.Chmod(path, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
service := LocalService{Rotation: rotation, AllowedUID: allowed}
|
|
go func() { done <- service.Serve(ctx, listener) }()
|
|
t.Cleanup(cancel)
|
|
return Client{SocketPath: path, ServiceUID: uint32(os.Geteuid())}, cancel, done
|
|
}
|
|
|
|
func stopLocalService(t *testing.T, cancel context.CancelFunc, done <-chan error) {
|
|
t.Helper()
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("本地激活服务未及时退出")
|
|
}
|
|
}
|
|
|
|
func TestLocalServiceRoundTrip(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
|
defer stopLocalService(t, cancel, done)
|
|
ctx := context.Background()
|
|
if ready, err := client.Ready(ctx); err != nil || ready.Hostname != testHostname {
|
|
t.Fatal("就绪查询失败", ready, err)
|
|
}
|
|
id, err := client.Stage(ctx, f.candidate.chain, f.candidate.key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err := client.State(ctx)
|
|
if err != nil || state.ID != id || state.Status != "staged" {
|
|
t.Fatalf("事务查询失败:%+v, %v", state, err)
|
|
}
|
|
f.upstream.active.Store(&f.candidate.pair)
|
|
if err := client.Commit(ctx, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err = client.State(ctx)
|
|
if err != nil || state.Status != "committed" {
|
|
t.Fatal(state, err)
|
|
}
|
|
if state, err := client.Reconcile(ctx, id); err != nil || state.Status != "committed" {
|
|
t.Fatal(state, err)
|
|
}
|
|
if err := client.Commit(ctx, id); !errors.Is(err, ErrLocalOperation) {
|
|
t.Fatal("重复提交应要求查询已知终态", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalServiceRejectsUnauthorizedPeer(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid())+1)
|
|
defer stopLocalService(t, cancel, done)
|
|
ctx, stop := context.WithTimeout(context.Background(), time.Second)
|
|
defer stop()
|
|
if _, err := client.Stage(ctx, f.candidate.chain, f.candidate.key); err == nil {
|
|
t.Fatal("未授权 UID 不应调用激活器")
|
|
}
|
|
if state, err := f.rotation.State(context.Background()); err != nil || state.Status != "idle" {
|
|
t.Fatal("未授权调用改变了事务", state, err)
|
|
}
|
|
}
|
|
|
|
func TestLocalServiceRejectsUnsafeMessages(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
|
defer stopLocalService(t, cancel, done)
|
|
for _, message := range []request{
|
|
{Version: 1, Action: "exec"},
|
|
{Version: 2, Action: "state"},
|
|
{Version: 1, Action: "commit", ID: "../../config"},
|
|
{Version: 1, Action: "state", Key: []byte("secret")},
|
|
{Version: 1, Action: "stage", Chain: []byte("pem")},
|
|
} {
|
|
connection, err := net.Dial("unix", client.SocketPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = connection.SetDeadline(time.Now().Add(time.Second))
|
|
if err := writeFrame(connection, message); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var result response
|
|
err = readFrame(connection, &result)
|
|
_ = connection.Close()
|
|
if err != nil || result.Code != "invalid_request" {
|
|
t.Fatal("无效请求没有被拒绝", result, err)
|
|
}
|
|
}
|
|
connection, err := net.Dial("unix", client.SocketPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
_ = connection.SetDeadline(time.Now().Add(time.Second))
|
|
var header [4]byte
|
|
binary.BigEndian.PutUint32(header[:], maxFrameSize+1)
|
|
if _, err := connection.Write(header[:]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var result response
|
|
if err := readFrame(connection, &result); err != nil || result.Code != "invalid_request" {
|
|
t.Fatal("超限帧没有立即拒绝", result, err)
|
|
}
|
|
state, err := f.rotation.State(context.Background())
|
|
if err != nil || state.Status != "idle" {
|
|
t.Fatal(state, err)
|
|
}
|
|
}
|
|
|
|
func TestLocalServiceLostStageResponseCanReconcile(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
started := make(chan struct{})
|
|
f.reloader.check = func(number int) error {
|
|
if number == 1 {
|
|
close(started)
|
|
}
|
|
return nil
|
|
}
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
|
defer stopLocalService(t, cancel, done)
|
|
connection, err := net.Dial("unix", client.SocketPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeFrame(connection, request{Version: 1, Action: "stage", Chain: f.candidate.chain, Key: f.candidate.key}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 先确认暂存取得事务锁,避免状态查询抢锁使请求在开始前就失败。
|
|
select {
|
|
case <-started:
|
|
case <-time.After(3 * time.Second):
|
|
_ = connection.Close()
|
|
t.Fatal("暂存请求未开始执行")
|
|
}
|
|
_ = connection.Close()
|
|
ctx, stop := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer stop()
|
|
var state State
|
|
for {
|
|
state, err = client.State(ctx)
|
|
if err == nil && state.Status == "staged" {
|
|
break
|
|
}
|
|
if ctx.Err() != nil {
|
|
t.Fatal("丢失响应后未找到暂存事务", state, err)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
state, err = client.Reconcile(ctx, state.ID)
|
|
if err != nil || state.Status != "rolled_back" {
|
|
t.Fatal(state, err)
|
|
}
|
|
}
|
|
|
|
func TestLocalServiceShutdownClosesStalledClients(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
|
connection, err := net.Dial("unix", client.SocketPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
if _, err := connection.Write([]byte{0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stopLocalService(t, cancel, done)
|
|
if _, err := os.Lstat(client.SocketPath); err != nil {
|
|
t.Fatal("服务停止不应删除托管 socket", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalClientRejectsUntrustedSocket(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
client, cancel, done := startLocalService(t, f.rotation, uint32(os.Geteuid()))
|
|
defer stopLocalService(t, cancel, done)
|
|
wrong := client
|
|
wrong.ServiceUID++
|
|
if _, err := wrong.State(context.Background()); err == nil {
|
|
t.Fatal("不应信任错误所有者")
|
|
}
|
|
link := filepath.Join(filepath.Dir(client.SocketPath), "link.sock")
|
|
if err := os.Symlink(client.SocketPath, link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wrong = client
|
|
wrong.SocketPath = link
|
|
if _, err := wrong.State(context.Background()); err == nil {
|
|
t.Fatal("不应连接符号链接")
|
|
}
|
|
if err := os.Chmod(client.SocketPath, 0o666); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := client.State(context.Background()); err == nil {
|
|
t.Fatal("不应连接公开可写 socket")
|
|
}
|
|
}
|