check / check (push) Failing after 14m44s
`smallwebwaf` with no command now prints the commands and exits with status 2. The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The passing-on code, its settings and its log fields and metric go. internal/proxy keeps the checks, answers, reports and own endpoints; its tests run through the real rproxy. The documents describe the pair. Judgement call: clients.json keeps token_refused, since state files refuse unknown fields. Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests. Partially verified: make example-app and make gitea-requests never built the image, as Ubuntu's snapshot service answered 5xx. Model: opus-5-5
205 lines
4.9 KiB
Go
205 lines
4.9 KiB
Go
package smallwebwaf_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/proxy"
|
|
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
|
|
)
|
|
|
|
// commandForMain, while set in the environment, has this test binary run Main
|
|
// with the command it gives, in place of the tests, so that a test can run
|
|
// Main in a process of its own.
|
|
const commandForMain = "SMALLWEBWAF_TEST_COMMAND"
|
|
|
|
func TestMain(m *testing.M) {
|
|
command, set := os.LookupEnv(commandForMain)
|
|
if !set {
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
os.Args = strings.Fields("smallwebwaf " + command)
|
|
|
|
os.Exit(smallwebwaf.Main(testVersion))
|
|
}
|
|
|
|
func TestMainRunsServeWAF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
binary, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatalf("find the test binary: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), 3*waitLimit)
|
|
defer cancel()
|
|
|
|
out := &output{}
|
|
cmd := exec.CommandContext(ctx, binary) //nolint:gosec // this test binary
|
|
cmd.Stdout = out
|
|
|
|
cmd.Env = append(os.Environ(),
|
|
commandForMain+"="+serveWAFCommand,
|
|
listenAddr+"="+localhost+":0",
|
|
stateDir+"="+t.TempDir(),
|
|
rulesDir+"="+t.TempDir(),
|
|
lookupSource+"=off")
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
_ = cmd.Process.Kill()
|
|
_ = cmd.Wait()
|
|
})
|
|
|
|
addr, _ := out.line(t, "msg", "starting")["address"].(string)
|
|
ask(t, addr, "/", http.StatusOK, proxy.DispositionAllow)
|
|
|
|
err = cmd.Process.Signal(syscall.SIGTERM)
|
|
if err != nil {
|
|
t.Fatalf("signal: %v", err)
|
|
}
|
|
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
t.Errorf("exit: %v; output:\n%s", err, out.text())
|
|
}
|
|
}
|
|
|
|
func TestMainWithoutACommandItKnowsPrintsTheCommands(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
binary, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatalf("find the test binary: %v", err)
|
|
}
|
|
|
|
for _, command := range []string{"", "serve"} {
|
|
t.Run("command "+command, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), waitLimit)
|
|
defer cancel()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd := exec.CommandContext(ctx, binary)
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
|
|
cmd.Env = append(os.Environ(), commandForMain+"="+command)
|
|
|
|
err := cmd.Run()
|
|
|
|
if code := cmd.ProcessState.ExitCode(); code != 2 {
|
|
t.Errorf("exit status %d (%v), want 2", code, err)
|
|
}
|
|
|
|
for _, name := range []string{"rproxy", serveWAFCommand, "healthcheck"} {
|
|
if !strings.Contains(stderr.String(), name) {
|
|
t.Errorf("stderr %q does not name %s", stderr.String(), name)
|
|
}
|
|
}
|
|
|
|
if stdout.Len() != 0 {
|
|
t.Errorf("stdout %q, want nothing", stdout.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ask sends servewaf at addr rproxy's question about a request for path from
|
|
// placed, checks the answer's status and disposition, and closes it.
|
|
func ask(t *testing.T, addr, path string, status int, disposition string) {
|
|
t.Helper()
|
|
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
|
|
"http://"+addr+path, http.NoBody)
|
|
if err != nil {
|
|
t.Fatalf("new request: %v", err)
|
|
}
|
|
|
|
req.Header.Set(proxy.ClientHeader, placed)
|
|
req.Header.Set("X-Request-ID", rand.Text())
|
|
|
|
transport := &http.Transport{}
|
|
defer transport.CloseIdleConnections()
|
|
|
|
res, err := (&http.Client{Transport: transport}).Do(req)
|
|
if err != nil {
|
|
t.Fatalf("question: %v", err)
|
|
}
|
|
|
|
_, _ = io.Copy(io.Discard, res.Body)
|
|
_ = res.Body.Close()
|
|
|
|
if res.StatusCode != status ||
|
|
res.Header.Get(proxy.DispositionHeader) != disposition {
|
|
t.Errorf("answered %d with disposition %q, want %d with %q", res.StatusCode,
|
|
res.Header.Get(proxy.DispositionHeader), status, disposition)
|
|
}
|
|
}
|
|
|
|
// runServeWAF runs `smallwebwaf servewaf` with the settings in env, has use
|
|
// send it requests at the address it listens on, stops it as SIGTERM does, and
|
|
// checks that it stopped in order.
|
|
func runServeWAF(t *testing.T, env map[string]string, use func(addr string)) {
|
|
t.Helper()
|
|
|
|
ctx, stop := context.WithCancel(t.Context())
|
|
out := &output{}
|
|
exited := make(chan int, 1)
|
|
|
|
go func() {
|
|
exited <- run(ctx, env, out)
|
|
}()
|
|
|
|
addr, _ := out.line(t, "msg", "starting")["address"].(string)
|
|
use(addr)
|
|
stop()
|
|
|
|
select {
|
|
case status := <-exited:
|
|
if status != 0 {
|
|
t.Fatalf("exit status %d, want 0; output:\n%s", status, out.text())
|
|
}
|
|
case <-time.After(waitLimit):
|
|
t.Fatal("still running after being told to stop")
|
|
}
|
|
|
|
out.line(t, "msg", "stopped")
|
|
}
|
|
|
|
func TestServeWAFKeepsItsBansAcrossARestartAndNeedsNoApp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := map[string]string{
|
|
listenAddr: localhost + ":0",
|
|
stateDir: t.TempDir(),
|
|
rulesDir: filepath.Join("..", "..", "share", "rules.d"),
|
|
}
|
|
|
|
runServeWAF(t, env, func(addr string) {
|
|
ask(t, addr, "/", http.StatusOK, proxy.DispositionAllow)
|
|
// The default rule file bans a probe for this.
|
|
ask(t, addr, "/.env", http.StatusForbidden, proxy.DispositionDeny)
|
|
})
|
|
|
|
runServeWAF(t, env, func(addr string) {
|
|
ask(t, addr, "/", http.StatusForbidden, proxy.DispositionDeny)
|
|
})
|
|
}
|