check / check (push) In progress
`smallwebwaf rproxy` reads the first 64 KiB of each body, asks servewaf, and passes what servewaf allows to the app within its own size and time limits. It reports each request servewaf answered. SWRP_DEFAULT_DISPOSITION decides when servewaf gives no answer. Duplication: internal/rproxy copies passing-on code from internal/proxy until #172 removes the originals. Rule suppressed: gosec G704 on servewaf's URL, a setting. Rule suppressed: gosec G705 on the answer written to the visitor. Judgement call: the SWRP_ size and time limits take off; SWRP_TIMEOUT_MS does not. Model: opus-5-5
166 lines
4.1 KiB
Go
166 lines
4.1 KiB
Go
package requestlog_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
err := requestlog.Write(&out, &requestlog.Line{
|
|
Time: requestlog.FormatTime(time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)),
|
|
ClientIP: "203.0.113.9",
|
|
Status: 200,
|
|
Action: requestlog.ActionForward,
|
|
DurationTotal: requestlog.Milliseconds(1500 * time.Microsecond),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
text := out.String()
|
|
if strings.Count(text, "\n") != 1 || !strings.HasSuffix(text, "\n") {
|
|
t.Fatalf("wrote %q, want one line", text)
|
|
}
|
|
|
|
var fields map[string]any
|
|
|
|
err = json.Unmarshal(out.Bytes(), &fields)
|
|
if err != nil {
|
|
t.Fatalf("decode %q: %v", text, err)
|
|
}
|
|
|
|
want := map[string]any{
|
|
"type": "request", "time": "2026-10-03T12:00:00.000Z",
|
|
"client_ip": "203.0.113.9", "status": 200.0, "action": "forward",
|
|
"duration_total": 1.5,
|
|
}
|
|
for name, value := range want {
|
|
if fields[name] != value {
|
|
t.Errorf("%s is %v, want %v", name, fields[name], value)
|
|
}
|
|
}
|
|
|
|
unset := []string{
|
|
"forwarded_for", "content_type", "content_length", "request_headers",
|
|
"has_authorization", "has_cookie", "websocket", "response_content_type",
|
|
"upstream_status", "cache_control", "location", "aborted", "counts",
|
|
"limit_hit", "offence", "ban_expires", "duration_checks",
|
|
"duration_upstream_connect", "duration_upstream_first_byte",
|
|
"duration_upstream_total",
|
|
}
|
|
for _, name := range unset {
|
|
_, present := fields[name]
|
|
if present {
|
|
t.Errorf("%s is there with no value to give", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteRProxyLineWritesOneJSONLineWithOnlyItsFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
err := requestlog.WriteRProxyLine(&out, &requestlog.RProxyLine{
|
|
RequestID: "an-id",
|
|
Disposition: "allow",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
const want = `{"type":"request","time":"","request_id":"an-id","client_ip":"",` +
|
|
`"host":"","method":"","path":"","query":"","status":0,"request_bytes":0,` +
|
|
`"response_bytes":0,"duration_request":0,"duration_response":0,` +
|
|
`"disposition":"allow"}` + "\n"
|
|
if out.String() != want {
|
|
t.Errorf("wrote %s, want %s", out.String(), want)
|
|
}
|
|
}
|
|
|
|
func TestProcessLinesAreMarkedProcessAndGiveTheInstance(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
requestlog.NewProcessLogger(&out, "fsn1app1/gitea", slog.LevelInfo).Info("starting",
|
|
"version", "v1")
|
|
|
|
var fields map[string]any
|
|
|
|
err := json.Unmarshal(out.Bytes(), &fields)
|
|
if err != nil {
|
|
t.Fatalf("decode %q: %v", out.String(), err)
|
|
}
|
|
|
|
if fields["type"] != "process" || fields["instance"] != "fsn1app1/gitea" ||
|
|
fields["msg"] != "starting" || fields["level"] != "INFO" ||
|
|
fields["version"] != "v1" {
|
|
t.Errorf("process line %v", fields)
|
|
}
|
|
|
|
timeText, _ := fields["time"].(string)
|
|
|
|
logged, err := time.Parse(time.RFC3339, timeText)
|
|
if err != nil || !strings.HasSuffix(timeText, "Z") ||
|
|
len(timeText) != len("2006-01-02T15:04:05.000Z") ||
|
|
time.Since(logged) > time.Minute {
|
|
t.Errorf("process line time %q, want now in UTC with milliseconds", timeText)
|
|
}
|
|
}
|
|
|
|
func TestProcessLoggerWritesTheMessagesAtItsLevelOrMoreSevere(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
levels := []slog.Level{
|
|
slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError,
|
|
}
|
|
|
|
for i, level := range levels {
|
|
t.Run(level.String(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
processLog := requestlog.NewProcessLogger(&out, "fsn1app1/gitea", level)
|
|
for _, at := range levels {
|
|
processLog.Log(t.Context(), at, "message")
|
|
}
|
|
|
|
var got, want []string
|
|
|
|
for line := range strings.Lines(out.String()) {
|
|
var fields struct {
|
|
Level string `json:"level"`
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(line), &fields)
|
|
if err != nil {
|
|
t.Fatalf("decode %q: %v", line, err)
|
|
}
|
|
|
|
got = append(got, fields.Level)
|
|
}
|
|
|
|
for _, written := range levels[i:] {
|
|
want = append(want, written.String())
|
|
}
|
|
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("lines at %v, want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|