Files
OpenFlare/openflare_agent/internal/nginx/manager_test.go
T

807 lines
28 KiB
Go

package nginx
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"openflare-agent/internal/protocol"
)
type runCall struct {
name string
args []string
}
type fakeRunner struct {
calls []runCall
runFn func(name string, args ...string) ([]byte, error)
}
type fakeExecutor struct {
testErr error
reloadErr error
}
type scriptedExecutor struct {
reloadErrors []error
reloadCalls int
}
func (r *fakeRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
r.calls = append(r.calls, runCall{name: name, args: append([]string{}, args...)})
if r.runFn != nil {
return r.runFn(name, args...)
}
return nil, nil
}
func (e *fakeExecutor) Test(ctx context.Context) error {
return e.testErr
}
func (e *fakeExecutor) Reload(ctx context.Context) error {
return e.reloadErr
}
func (e *fakeExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
return nil
}
func (e *fakeExecutor) CheckHealth(ctx context.Context) error {
return e.testErr
}
func (e *fakeExecutor) Restart(ctx context.Context) error {
return e.reloadErr
}
func (e *scriptedExecutor) Test(ctx context.Context) error {
return nil
}
func (e *scriptedExecutor) Reload(ctx context.Context) error {
index := e.reloadCalls
e.reloadCalls++
if index >= len(e.reloadErrors) {
return nil
}
return e.reloadErrors[index]
}
func (e *scriptedExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
return nil
}
func (e *scriptedExecutor) CheckHealth(ctx context.Context) error {
return nil
}
func (e *scriptedExecutor) Restart(ctx context.Context) error {
return nil
}
func TestPathExecutorCommands(t *testing.T) {
runner := &fakeRunner{}
executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty",
ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
}
if err := executor.Test(context.Background()); err != nil {
t.Fatalf("Test failed: %v", err)
}
if err := executor.Reload(context.Background()); err != nil {
t.Fatalf("Reload failed: %v", err)
}
expected := []runCall{
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t", "-c", "/data/etc/nginx/nginx.conf"}},
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload", "-c", "/data/etc/nginx/nginx.conf"}},
}
if !reflect.DeepEqual(runner.calls, expected) {
t.Fatalf("unexpected calls: %#v", runner.calls)
}
}
func TestPathExecutorEnsureRuntimeNoop(t *testing.T) {
runner := &fakeRunner{}
executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty",
ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
}
if err := executor.EnsureRuntime(context.Background(), true); err != nil {
t.Fatalf("EnsureRuntime failed: %v", err)
}
if len(runner.calls) != 2 {
t.Fatalf("expected test and reload calls, got %d", len(runner.calls))
}
}
func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) {
runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) {
if len(args) == 2 && args[0] == "-s" && args[1] == "quit" {
return []byte("openresty: [error] invalid PID number \"\" in \"/usr/local/openresty/nginx/logs/nginx.pid\""), errors.New("exit status 1")
}
return []byte(""), nil
},
}
executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty",
ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
}
if err := executor.Restart(context.Background()); err != nil {
t.Fatalf("Restart failed: %v", err)
}
if len(runner.calls) != 2 {
t.Fatalf("expected 2 restart calls, got %d", len(runner.calls))
}
}
func TestPathExecutorReloadStartsWhenRuntimeIsNotRunning(t *testing.T) {
runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) {
if len(args) >= 2 && args[0] == "-s" && args[1] == "reload" {
return []byte("openresty: [error] invalid PID number \"\" in \"/usr/local/openresty/nginx/logs/nginx.pid\""), errors.New("exit status 1")
}
return []byte(""), nil
},
}
executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty",
ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
}
if err := executor.Reload(context.Background()); err != nil {
t.Fatalf("Reload failed: %v", err)
}
expected := []runCall{
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload", "-c", "/data/etc/nginx/nginx.conf"}},
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-c", "/data/etc/nginx/nginx.conf"}},
}
if !reflect.DeepEqual(runner.calls, expected) {
t.Fatalf("unexpected calls: %#v", runner.calls)
}
}
func TestDetectVersionFromBinary(t *testing.T) {
version, err := detectVersion(context.Background(), ExecutorOptions{
NginxPath: "/usr/local/openresty/nginx/sbin/openresty",
}, &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) {
return []byte("nginx version: openresty/1.27.1.2\n"), nil
},
})
if err != nil {
t.Fatalf("detectVersion failed: %v", err)
}
if version != "1.27.1.2" {
t.Fatalf("unexpected version: %s", version)
}
}
func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) {
tempDir := t.TempDir()
mainPath := filepath.Join(tempDir, "nginx.conf")
routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf")
certDir := filepath.Join(tempDir, "certs")
accessLogPath := filepath.Join(tempDir, "var", "log", "openflare", "access.log")
manager := &Manager{
MainConfigPath: mainPath,
RouteConfigPath: routePath,
AccessLogPath: accessLogPath,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &fakeExecutor{},
}
outcome := manager.Apply(
context.Background(),
"include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
"ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;\n",
[]protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
)
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
mainData, err := os.ReadFile(mainPath)
if err != nil {
t.Fatalf("failed to read main config: %v", err)
}
expectedMain := "include " + routePath + ";\naccess_log " + filepath.ToSlash(accessLogPath) + " openflare_json;\n"
if string(mainData) != expectedMain {
t.Fatalf("unexpected main config: %s", string(mainData))
}
routeData, err := os.ReadFile(routePath)
if err != nil {
t.Fatalf("failed to read route config: %v", err)
}
if string(routeData) != "ssl_certificate /etc/nginx/openflare-certs/1.crt;\n" {
t.Fatalf("unexpected route config: %s", string(routeData))
}
value, err := manager.CurrentChecksum()
if err != nil {
t.Fatalf("CurrentChecksum failed: %v", err)
}
expected := bundleChecksum(
"include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
"ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;\n",
[]protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
)
if value != expected {
t.Fatalf("unexpected checksum: got %s want %s", value, expected)
}
}
func TestParseNginxVersionIgnoresDockerEntrypointPaths(t *testing.T) {
output := strings.Join([]string{
"/docker-entrypoint.sh: /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)",
"nginx version: openresty/1.27.1.2",
}, "\n")
version := parseNginxVersion(output)
if version != "1.27.1.2" {
t.Fatalf("unexpected version: %s", version)
}
}
func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
tempDir := t.TempDir()
manager := &Manager{
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
CertDir: filepath.Join(tempDir, "certs"),
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
OpenrestyObservabilityListen: "18081",
OpenrestyResolverDirective: " resolver 127.0.0.11 valid=30s ipv6=off;\n resolver_timeout 5s;\n",
Executor: &fakeExecutor{},
}
outcome := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\n__OPENFLARE_RESOLVER_DIRECTIVE__server { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;", []protocol.SupportFile{
{Path: "1.crt", Content: "cert-data"},
{Path: "1.key", Content: "key-data"},
})
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
routeData, err := os.ReadFile(manager.RouteConfigPath)
if err != nil {
t.Fatalf("failed to read route config: %v", err)
}
if !strings.Contains(string(routeData), "/etc/nginx/openflare-certs/1.crt") {
t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData))
}
renderedRoute := manager.renderRouteConfig("access_by_lua_file __OPENFLARE_LUA_DIR__/pow/check.lua;\nlocation /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\n")
if !strings.Contains(renderedRoute, "access_by_lua_file /etc/nginx/openflare-lua/pow/check.lua;") {
t.Fatalf("expected lua dir placeholder replacement in route config, got %s", renderedRoute)
}
if !strings.Contains(renderedRoute, "alias /etc/nginx/openflare-lua/pow/static/;") {
t.Fatalf("expected pow static dir placeholder replacement in route config, got %s", renderedRoute)
}
mainData, err := os.ReadFile(manager.MainConfigPath)
if err != nil {
t.Fatalf("failed to read main config: %v", err)
}
if !strings.Contains(string(mainData), "listen 18081;") {
t.Fatalf("expected observability listen placeholder replacement in main config, got %s", string(mainData))
}
if !strings.Contains(string(mainData), "resolver 127.0.0.11 valid=30s ipv6=off;") {
t.Fatalf("expected resolver directive placeholder replacement in main config, got %s", string(mainData))
}
certData, err := os.ReadFile(filepath.Join(manager.CertDir, "1.crt"))
if err != nil {
t.Fatalf("failed to read cert file: %v", err)
}
if string(certData) != "cert-data" {
t.Fatalf("unexpected cert file content: %s", string(certData))
}
luaInfo, err := os.Stat(filepath.Join(manager.LuaDir, "log.lua"))
if err != nil {
t.Fatalf("expected managed lua file to exist, stat err = %v", err)
}
if runtime.GOOS != "windows" && luaInfo.Mode().Perm() != 0o644 {
t.Fatalf("unexpected lua mode: %o", luaInfo.Mode().Perm())
}
}
func TestResolverDirectiveUsesExplicitResolvers(t *testing.T) {
got := ResolverDirective("", []string{"10.0.0.2", "1.1.1.1"})
if !strings.Contains(got, "resolver 10.0.0.2 1.1.1.1") {
t.Fatalf("expected explicit resolver directive, got %q", got)
}
}
func TestParseResolverAddressesFiltersLoopbackForDocker(t *testing.T) {
content := strings.Join([]string{
"nameserver 127.0.0.53",
"nameserver 10.0.0.2",
"nameserver ::1",
"nameserver 1.1.1.1",
}, "\n")
got := parseResolverAddresses(content, true)
expected := []string{"10.0.0.2", "1.1.1.1"}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("unexpected docker resolvers: got %#v want %#v", got, expected)
}
}
func TestParseResolverAddressesKeepsLoopbackForLocalBinary(t *testing.T) {
content := strings.Join([]string{
"nameserver 127.0.0.53",
"nameserver 10.0.0.2",
}, "\n")
got := parseResolverAddresses(content, false)
expected := []string{"127.0.0.53", "10.0.0.2"}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("unexpected local resolvers: got %#v want %#v", got, expected)
}
}
func TestRequiresRuntimeResolver(t *testing.T) {
testCases := []struct {
name string
originURL string
want bool
}{
{name: "hostname", originURL: "https://origin.internal", want: true},
{name: "ipv4", originURL: "https://10.0.0.8", want: false},
{name: "ipv6", originURL: "https://[2001:db8::1]", want: false},
{name: "invalid", originURL: "://bad", want: false},
}
for _, testCase := range testCases {
if got := RequiresRuntimeResolver(testCase.originURL); got != testCase.want {
t.Fatalf("%s: got %v want %v", testCase.name, got, testCase.want)
}
}
}
func TestWriteCertFilesKeepsBaseDirAndRemovesStaleFiles(t *testing.T) {
tempDir := t.TempDir()
certDir := filepath.Join(tempDir, "certs")
if err := os.MkdirAll(filepath.Join(certDir, "stale"), 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
if err := os.WriteFile(filepath.Join(certDir, "stale", "old.crt"), []byte("old"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
manager := &Manager{CertDir: certDir}
if err := manager.writeCertFiles([]protocol.SupportFile{
{Path: "1.crt", Content: "cert"},
{Path: "1.key", Content: "key"},
}); err != nil {
t.Fatalf("writeCertFiles failed: %v", err)
}
if _, err := os.Stat(certDir); err != nil {
t.Fatalf("expected cert dir to persist, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(certDir, "stale", "old.crt")); !os.IsNotExist(err) {
t.Fatalf("expected stale cert file to be removed, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(certDir, "1.crt")); err != nil {
t.Fatalf("expected new cert file to exist, stat err = %v", err)
}
}
func TestEnsureLuaAssetsKeepsBaseDirAndRemovesStaleFiles(t *testing.T) {
tempDir := t.TempDir()
luaDir := filepath.Join(tempDir, "lua")
if err := os.MkdirAll(filepath.Join(luaDir, "stale"), 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
if err := os.WriteFile(filepath.Join(luaDir, "stale", "old.lua"), []byte("old"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
manager := &Manager{LuaDir: luaDir}
if err := manager.EnsureLuaAssets(); err != nil {
t.Fatalf("EnsureLuaAssets failed: %v", err)
}
if _, err := os.Stat(luaDir); err != nil {
t.Fatalf("expected lua dir to persist, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(luaDir, "stale", "old.lua")); !os.IsNotExist(err) {
t.Fatalf("expected stale lua file to be removed, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(luaDir, "log.lua")); err != nil {
t.Fatalf("expected managed lua file to exist, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(luaDir, "pow", "check.lua")); err != nil {
t.Fatalf("expected managed pow lua file to exist, stat err = %v", err)
}
if _, err := os.Stat(filepath.Join(luaDir, "pow", "static", "js", "main.mjs")); err != nil {
t.Fatalf("expected managed pow static asset to exist, stat err = %v", err)
}
}
func TestCertFileMode(t *testing.T) {
testCases := []struct {
path string
want os.FileMode
}{
{path: "1.crt", want: 0o644},
{path: "1.pem", want: 0o644},
{path: "1.key", want: 0o600},
{path: "misc.txt", want: 0o644},
}
for _, testCase := range testCases {
if got := certFileMode(testCase.path); got != testCase.want {
t.Fatalf("unexpected mode for %s: got %o want %o", testCase.path, got, testCase.want)
}
}
}
func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) {
tempDir := t.TempDir()
manager := &Manager{
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
RuntimeConfigDir: filepath.Join(tempDir, "runtime"),
}
err := manager.EnsureLuaAssets()
if err != nil {
t.Fatalf("EnsureLuaAssets failed: %v", err)
}
luaInfo, err := os.Stat(filepath.Join(manager.LuaDir, "log.lua"))
if err != nil {
t.Fatalf("failed to stat lua file: %v", err)
}
if luaInfo.Mode().Perm() != 0o644 {
t.Fatalf("unexpected lua mode: %o", luaInfo.Mode().Perm())
}
if _, err := os.Stat(filepath.Join(manager.LuaDir, "pow", "check.lua")); err != nil {
t.Fatalf("failed to stat pow lua file: %v", err)
}
data, err := os.ReadFile(filepath.Join(manager.LuaDir, "pow", "check.lua"))
if err != nil {
t.Fatalf("failed to read pow lua file: %v", err)
}
if !strings.Contains(string(data), filepath.ToSlash(manager.RuntimeConfigDir)+"/pow_config.json") {
t.Fatalf("expected pow lua to read runtime config dir, got %s", string(data))
}
}
func TestEnsureLuaAssetsLeavesRuntimePowConfigOutsideLuaDir(t *testing.T) {
tempDir := t.TempDir()
luaDir := filepath.Join(tempDir, "lua")
runtimeConfigDir := filepath.Join(tempDir, "runtime")
if err := os.MkdirAll(runtimeConfigDir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
powConfigPath := filepath.Join(runtimeConfigDir, "pow_config.json")
want := `[{"domains":["pow.example.com"],"enabled":true}]`
if err := os.WriteFile(powConfigPath, []byte(want), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
manager := &Manager{LuaDir: luaDir, RuntimeConfigDir: runtimeConfigDir}
if err := manager.EnsureLuaAssets(); err != nil {
t.Fatalf("EnsureLuaAssets failed: %v", err)
}
got, err := os.ReadFile(powConfigPath)
if err != nil {
t.Fatalf("expected pow_config.json to remain after EnsureLuaAssets: %v", err)
}
if string(got) != want {
t.Fatalf("unexpected pow_config.json content: got %s want %s", string(got), want)
}
if _, err := os.Stat(filepath.Join(luaDir, "pow_config.json")); !os.IsNotExist(err) {
t.Fatalf("expected lua pow_config.json to stay absent, stat err = %v", err)
}
}
func TestManagerApplyWritesPowConfigToRuntimeDirAndCleansLegacyCopies(t *testing.T) {
tempDir := t.TempDir()
certDir := filepath.Join(tempDir, "certs")
luaDir := filepath.Join(tempDir, "lua")
runtimeConfigDir := filepath.Join(tempDir, "runtime")
for _, dir := range []string{certDir, luaDir, runtimeConfigDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
}
for _, path := range []string{filepath.Join(certDir, "pow_config.json"), filepath.Join(luaDir, "pow_config.json")} {
if err := os.WriteFile(path, []byte("stale"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
}
manager := &Manager{
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
CertDir: certDir,
LuaDir: luaDir,
RuntimeConfigDir: runtimeConfigDir,
Executor: &fakeExecutor{},
}
outcome := manager.Apply(context.Background(), "main", "route", []protocol.SupportFile{
{Path: "pow_config.json", Content: "runtime"},
})
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
data, err := os.ReadFile(filepath.Join(runtimeConfigDir, "pow_config.json"))
if err != nil {
t.Fatalf("failed to read runtime pow config: %v", err)
}
if string(data) != "runtime" {
t.Fatalf("unexpected runtime pow config: %s", string(data))
}
for _, path := range []string{filepath.Join(certDir, "pow_config.json"), filepath.Join(luaDir, "pow_config.json")} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("expected legacy pow config to be removed from %s, stat err = %v", path, err)
}
}
}
func TestManagerCurrentChecksumIncludesPowConfig(t *testing.T) {
tempDir := t.TempDir()
mainPath := filepath.Join(tempDir, "nginx.conf")
routePath := filepath.Join(tempDir, "routes.conf")
luaDir := filepath.Join(tempDir, "lua")
runtimeConfigDir := filepath.Join(tempDir, "runtime")
manager := &Manager{
MainConfigPath: mainPath,
RouteConfigPath: routePath,
LuaDir: luaDir,
NginxLuaDir: "/etc/nginx/openflare-lua",
RuntimeConfigDir: runtimeConfigDir,
Executor: &fakeExecutor{},
}
outcome := manager.Apply(
context.Background(),
"access_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
"location /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\n",
[]protocol.SupportFile{{Path: "pow_config.json", Content: `[{"domains":["pow.example.com"],"enabled":true}]`}},
)
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
value, err := manager.CurrentChecksum()
if err != nil {
t.Fatalf("CurrentChecksum failed: %v", err)
}
expected := bundleChecksum(
"access_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
"location /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\n",
[]protocol.SupportFile{{Path: "pow_config.json", Content: `[{"domains":["pow.example.com"],"enabled":true}]`}},
)
if value != expected {
t.Fatalf("unexpected checksum with pow config: got %s want %s", value, expected)
}
}
func TestManagedPowLuaFilesUseInternalChallengeFlow(t *testing.T) {
if !strings.Contains(openRestyPowCheckLua, `return ngx.exec("/.within.website/x/cmd/anubis/api/make-challenge")`) {
t.Fatal("expected check.lua to internally execute make-challenge instead of issuing a 302 redirect")
}
if strings.Contains(openRestyPowCheckLua, "ngx.redirect(") {
t.Fatal("expected check.lua to avoid external redirects for challenge rendering")
}
if !strings.Contains(openRestyPowChallengeLua, `<h1 id="title" class="centered-div">`) {
t.Fatal("expected challenge html to include Anubis-compatible title node")
}
if !strings.Contains(openRestyPowChallengeLua, `<div id="progress" role="progressbar" aria-labelledby="status"><div class="bar-inner"></div></div>`) {
t.Fatal("expected challenge html to include Anubis-compatible progress markup")
}
if !strings.Contains(openRestyPowChallengeLua, `<script id="anubis_public_url" type="application/json">"__openflare_internal__"</script>`) {
t.Fatal("expected challenge html to force Anubis frontend to reuse the current URL as redir target")
}
if !strings.Contains(openRestyPowCheckLua, `pow_sessions:set(session_key, "1", session_ttl)`) {
t.Fatal("expected check.lua to refresh the PoW session TTL on each valid request")
}
if !strings.Contains(openRestyPowCheckLua, `ngx.header["Set-Cookie"] = session_cookie(cookie_val, session_ttl)`) {
t.Fatal("expected check.lua to refresh the browser session cookie on each valid request")
}
if !strings.Contains(openRestyPowChallengeLua, `local session_ttl = config.session_ttl or 600`) {
t.Fatal("expected challenge.lua to default session TTL to 10 minutes")
}
if !strings.Contains(openRestyPowVerifyLua, `local session_ttl = challenge_info.session_ttl or 600`) {
t.Fatal("expected verify.lua to default session TTL to 10 minutes")
}
if !strings.Contains(openRestyPowVerifyLua, `if ngx.var.scheme == "https" then`) {
t.Fatal("expected verify.lua to only mark the session cookie as Secure for HTTPS requests")
}
}
func TestManagerRollbackRestoresCertFiles(t *testing.T) {
tempDir := t.TempDir()
routePath := filepath.Join(tempDir, "routes.conf")
mainPath := filepath.Join(tempDir, "nginx.conf")
certDir := filepath.Join(tempDir, "certs")
if err := os.MkdirAll(certDir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
if err := os.WriteFile(mainPath, []byte("old-main"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
if err := os.WriteFile(filepath.Join(certDir, "1.crt"), []byte("old-cert"), 0o600); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
manager := &Manager{
MainConfigPath: mainPath,
RouteConfigPath: routePath,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &fakeExecutor{
reloadErr: errors.New("openresty reload failed"),
},
}
outcome := manager.Apply(context.Background(), "new-main", "new-route", []protocol.SupportFile{
{Path: "1.crt", Content: "new-cert"},
})
if outcome.Status != ApplyStatusFatal {
t.Fatalf("expected fatal apply outcome, got %#v", outcome)
}
mainData, err := os.ReadFile(mainPath)
if err != nil {
t.Fatalf("failed to read main config: %v", err)
}
if string(mainData) != "old-main" {
t.Fatalf("expected main rollback, got %s", string(mainData))
}
routeData, err := os.ReadFile(routePath)
if err != nil {
t.Fatalf("failed to read route config: %v", err)
}
if string(routeData) != "old-route" {
t.Fatalf("expected route rollback, got %s", string(routeData))
}
certData, err := os.ReadFile(filepath.Join(certDir, "1.crt"))
if err != nil {
t.Fatalf("failed to read cert file: %v", err)
}
if string(certData) != "old-cert" {
t.Fatalf("expected cert rollback, got %s", string(certData))
}
}
func TestManagerApplyReturnsWarningWhenRollbackRecoversRuntime(t *testing.T) {
tempDir := t.TempDir()
routePath := filepath.Join(tempDir, "routes.conf")
mainPath := filepath.Join(tempDir, "nginx.conf")
certDir := filepath.Join(tempDir, "certs")
if err := os.MkdirAll(certDir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
if err := os.WriteFile(mainPath, []byte("old-main"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
if err := os.WriteFile(filepath.Join(certDir, "1.crt"), []byte("old-cert"), 0o600); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
manager := &Manager{
MainConfigPath: mainPath,
RouteConfigPath: routePath,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &scriptedExecutor{
reloadErrors: []error{errors.New("target config failed"), nil},
},
}
outcome := manager.Apply(context.Background(), "new-main", "new-route", []protocol.SupportFile{
{Path: "1.crt", Content: "new-cert"},
})
if outcome.Status != ApplyStatusWarning {
t.Fatalf("expected warning apply outcome, got %#v", outcome)
}
mainData, err := os.ReadFile(mainPath)
if err != nil {
t.Fatalf("failed to read main config: %v", err)
}
if string(mainData) != "old-main" {
t.Fatalf("expected main rollback, got %s", string(mainData))
}
}
func TestManagerCertFileTargetPathRejectsEscapes(t *testing.T) {
manager := &Manager{CertDir: filepath.Join(t.TempDir(), "certs")}
if err := os.MkdirAll(manager.CertDir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
absolutePath := "/tmp/evil.crt"
if runtime.GOOS == "windows" {
absolutePath = `C:/tmp/evil.crt`
}
testCases := []struct {
path string
shouldErr bool
}{
{path: "nested/1.crt", shouldErr: false},
{path: "../escape.crt", shouldErr: true},
{path: "..\\escape.crt", shouldErr: true},
{path: absolutePath, shouldErr: true},
{path: "", shouldErr: true},
}
for _, testCase := range testCases {
targetPath, err := manager.certFileTargetPath(testCase.path)
if testCase.shouldErr {
if err == nil {
t.Fatalf("expected path %q to be rejected, got target %q", testCase.path, targetPath)
}
continue
}
if err != nil {
t.Fatalf("expected path %q to be accepted: %v", testCase.path, err)
}
if !strings.HasPrefix(targetPath, manager.CertDir) {
t.Fatalf("expected target path %q to stay under %q", targetPath, manager.CertDir)
}
}
}
func TestManagerApplyRejectsCertFilePathTraversal(t *testing.T) {
tempDir := t.TempDir()
manager := &Manager{
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
CertDir: filepath.Join(tempDir, "certs"),
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &fakeExecutor{},
}
outcome := manager.Apply(context.Background(), "main", "route", []protocol.SupportFile{
{Path: "../escape.crt", Content: "bad"},
})
if outcome.Status != ApplyStatusWarning {
t.Fatalf("expected warning apply outcome, got %#v", outcome)
}
if _, statErr := os.Stat(filepath.Join(tempDir, "escape.crt")); !os.IsNotExist(statErr) {
t.Fatalf("expected escaped file to not exist, stat err = %v", statErr)
}
}
func TestObservabilityListenAddress(t *testing.T) {
if got := ObservabilityListenAddress("", 18081); got != "127.0.0.1:18081" {
t.Fatalf("unexpected default observability listen address: %s", got)
}
if got := ObservabilityListenAddress("/usr/local/openresty/nginx/sbin/openresty", 18081); got != "127.0.0.1:18081" {
t.Fatalf("unexpected path observability listen address: %s", got)
}
}