The command is now `sww waf`, and the package is `internal/waf`. Types, functions, files, tests, comments, docs, the Dockerfile, the runit script, `script/`, compose examples, the log field `waf_line` and the `X-From-Waf` test header follow. No behaviour changes, and no setting changes name: rproxy already finds the waf through `SWRP_WAF_BACKEND`. Done with one `sed -i` (Discriminator and DISCRIMINATOR to WAF, discriminator to waf) over the files `git grep -i discriminator` listed, as sneak allowed for this rename, then `git mv` for the package and files. By hand afterwards: the header's spelling, which the linter requires, and the padding after `waf` in `sww`'s usage text. Model: opus-5-5
This commit was merged in pull request #319.
This commit is contained in:
1 parent
ad36dd1775
commit
f7e3190654
89 files changed
+1454
-1489
No files matched your search
@@ -18,7 +18,7 @@ const (
|
||||
)
|
||||
|
||||
// peerAddress is the address of the request's TCP peer, normally traefik, without
|
||||
// any IPv6 zone, which discriminator refuses.
|
||||
// any IPv6 zone, which waf refuses.
|
||||
func peerAddress(r *http.Request) netip.Addr {
|
||||
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
@@ -34,7 +34,7 @@ func peerAddress(r *http.Request) netip.Addr {
|
||||
// address outside them is the visitor; if every address in it is inside, the
|
||||
// leftmost is, and with no header, the peer. An entry that is not an address
|
||||
// ends the reading, since nothing to its left can be believed. The visitor's
|
||||
// address never has an IPv6 zone, which discriminator refuses.
|
||||
// address never has an IPv6 zone, which waf refuses.
|
||||
func clientAddress(
|
||||
peer netip.Addr, forwardedFor []string, trusted []netip.Prefix,
|
||||
) netip.Addr {
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
// post is the start of a POST whose headers end with what follows it.
|
||||
@@ -59,7 +59,7 @@ func TestServerHasTheFixedLimits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing.T) {
|
||||
func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
@@ -72,7 +72,7 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||
t.Error("the app was asked")
|
||||
})
|
||||
@@ -101,7 +101,7 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing
|
||||
func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
maxRequestSize: "1K",
|
||||
@@ -110,7 +110,7 @@ func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
||||
got := visit(t, newRequest(t, http.MethodPost, addr, "/",
|
||||
bytes.NewReader(make([]byte, 1025))))
|
||||
if got.status != http.StatusForbidden {
|
||||
t.Errorf("answered %+v, want discriminator's 403", got)
|
||||
t.Errorf("answered %+v, want waf's 403", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
||||
|
||||
var asked atomic.Bool
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
asked.Store(true)
|
||||
greet(w, r)
|
||||
@@ -166,7 +166,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
|
||||
t.Run("announced, is refused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(contentLengthHeader, strconv.Itoa(len(large)))
|
||||
_, _ = io.WriteString(w, large)
|
||||
@@ -188,7 +188,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
|
||||
t.Run("not announced, is cut off", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, large[:1])
|
||||
_ = http.NewResponseController(w).Flush()
|
||||
@@ -218,7 +218,7 @@ func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(t *testing.
|
||||
|
||||
const announced = "2048"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(contentLengthHeader, announced)
|
||||
})
|
||||
@@ -241,7 +241,7 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
|
||||
t.Run(setting, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
setting: "off",
|
||||
@@ -252,7 +252,7 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
|
||||
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
|
||||
strings.NewReader("a body"))))
|
||||
|
||||
if got := get(t, addr, discriminator.HealthPath); got.status != http.StatusOK {
|
||||
if got := get(t, addr, waf.HealthPath); got.status != http.StatusOK {
|
||||
t.Errorf("answered %+v, want rproxy's own 200", got)
|
||||
}
|
||||
})
|
||||
@@ -271,7 +271,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
maxRequestTime: "500",
|
||||
@@ -290,7 +290,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked")
|
||||
t.Error("waf was asked")
|
||||
}
|
||||
|
||||
standIn.wantNoReport(t)
|
||||
@@ -301,7 +301,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
||||
func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -315,7 +315,7 @@ func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked")
|
||||
t.Error("waf was asked")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -366,7 +366,7 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked")
|
||||
t.Error("waf was asked")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -412,7 +412,7 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
|
||||
announced = 100 * questionBodyMaxBytes
|
||||
)
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
@@ -456,13 +456,13 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
|
||||
name, target string
|
||||
status int
|
||||
}{
|
||||
{"discriminator's deny", "/", http.StatusForbidden},
|
||||
{"waf's deny", "/", http.StatusForbidden},
|
||||
{"rproxy's own 400", "x:admin", http.StatusBadRequest},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
||||
@@ -494,7 +494,7 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
|
||||
|
||||
const least = 10 * time.Second
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||
maxResponseTime: "300",
|
||||
@@ -516,7 +516,7 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
|
||||
func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
maxRequestTime: "3000",
|
||||
@@ -585,7 +585,7 @@ func TestAnAppTooSlowToTakeTheRequestIsAnswered504(t *testing.T) {
|
||||
writeSize = 32 << 10
|
||||
)
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
addr, _ := startRProxy(t, standIn.URL, startAppThatTakesNothing(t),
|
||||
map[string]string{maxRequestTime: "3000"})
|
||||
|
||||
@@ -612,7 +612,7 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
release := make(chan struct{})
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||
<-release
|
||||
})
|
||||
@@ -658,7 +658,7 @@ func visitAsTheTimeRunsOut(t *testing.T) {
|
||||
// answerAfter is how long the app waits before it answers.
|
||||
var answerAfter atomic.Int64
|
||||
|
||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
||||
backend := startWAFAnsweringReports(t, allow,
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
@@ -701,7 +701,7 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
|
||||
const start = "the start"
|
||||
|
||||
release := make(chan struct{})
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, start)
|
||||
_ = http.NewResponseController(w).Flush()
|
||||
@@ -750,7 +750,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
|
||||
writeSize = 32 << 10
|
||||
)
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
part := make([]byte, writeSize)
|
||||
|
||||
@@ -787,7 +787,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
|
||||
func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
||||
|
||||
got := get(t, addr, "/")
|
||||
@@ -803,7 +803,7 @@ func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
|
||||
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, greeting)
|
||||
})
|
||||
@@ -867,7 +867,7 @@ func readAnswer(t *testing.T, conn net.Conn) answered {
|
||||
func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, answerBeforeTheBody)
|
||||
// The request time limit is longer than the 10 seconds the status and headers
|
||||
// of an answer have to be sent, and than dial's deadline.
|
||||
@@ -887,7 +887,7 @@ func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing.
|
||||
|
||||
const limit = 2 * time.Second
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, answerBeforeTheBody)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
maxRequestTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
||||
@@ -914,7 +914,7 @@ func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
working := make(chan struct{})
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
close(working)
|
||||
<-r.Context().Done()
|
||||
@@ -955,7 +955,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
|
||||
|
||||
const answer = "the end of a long answer"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
_ = http.NewResponseController(w).Flush()
|
||||
@@ -984,14 +984,14 @@ func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T
|
||||
{"a deny", post, http.StatusForbidden},
|
||||
{
|
||||
"the health check",
|
||||
"GET " + discriminator.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n",
|
||||
"GET " + waf.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n",
|
||||
http.StatusOK,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
// The request time limit is longer than the 10 seconds an answer rproxy
|
||||
// gives itself has to be sent, and than dial's deadline.
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
// questionBodyMaxBytes is the most of a body a question carries.
|
||||
const questionBodyMaxBytes = discriminator.QuestionBodyMaxBytes
|
||||
const questionBodyMaxBytes = waf.QuestionBodyMaxBytes
|
||||
|
||||
// chunked is the encoding of a body whose length the visitor did not announce.
|
||||
const chunked = "chunked"
|
||||
@@ -29,7 +29,7 @@ const pathGoReencodes = "/some%2Fpath/with%20space|pipe"
|
||||
func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -42,9 +42,9 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
||||
req.Header.Set(forwardedForHeader, "198.51.100.1")
|
||||
req.Header.Set(forwardedProtoHeader, "gopher")
|
||||
req.Header.Set(requestIDHeader, "the-visitor's")
|
||||
req.Header.Set(discriminator.ClientHeader, "198.51.100.2")
|
||||
req.Header.Set(discriminator.PeerHeader, "198.51.100.3")
|
||||
req.Header.Set(discriminator.ContentLengthHeader, "12345")
|
||||
req.Header.Set(waf.ClientHeader, "198.51.100.2")
|
||||
req.Header.Set(waf.PeerHeader, "198.51.100.3")
|
||||
req.Header.Set(waf.ContentLengthHeader, "12345")
|
||||
// Go's client then sends no User-Agent, so that one in the question would be
|
||||
// Go's own.
|
||||
req.Header["User-Agent"] = nil
|
||||
@@ -68,8 +68,8 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
||||
want.Set(forwardedForHeader, "198.51.100.1")
|
||||
want.Set(forwardedProtoHeader, "http")
|
||||
want.Set(requestIDHeader, asked.header.Get(requestIDHeader))
|
||||
want.Set(discriminator.ClientHeader, localhost)
|
||||
want.Set(discriminator.PeerHeader, localhost)
|
||||
want.Set(waf.ClientHeader, localhost)
|
||||
want.Set(waf.PeerHeader, localhost)
|
||||
|
||||
if !maps.EqualFunc(asked.header, want, slices.Equal) {
|
||||
t.Errorf("the question's headers are %v, want %v", asked.header, want)
|
||||
@@ -93,7 +93,7 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
forwarded := make(chan http.Header, 1)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
forwarded <- r.Header.Clone()
|
||||
@@ -110,8 +110,8 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
|
||||
wantGreeting(t, visit(t, req))
|
||||
|
||||
asked := standIn.question(t)
|
||||
if asked.header.Get(discriminator.ClientHeader) != tc.client ||
|
||||
asked.header.Get(discriminator.PeerHeader) != localhost ||
|
||||
if asked.header.Get(waf.ClientHeader) != tc.client ||
|
||||
asked.header.Get(waf.PeerHeader) != localhost ||
|
||||
asked.header.Get(forwardedForHeader) != visitor ||
|
||||
asked.header.Get(forwardedProtoHeader) != tc.proto {
|
||||
t.Errorf("the question's headers are %v", asked.header)
|
||||
@@ -157,7 +157,7 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
trustedProxies: localhost + "/32,10.0.0.0/8",
|
||||
@@ -167,7 +167,7 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
|
||||
req.Header[forwardedForHeader] = tc.header
|
||||
wantGreeting(t, visit(t, req))
|
||||
|
||||
if got := standIn.question(t).header.Get(discriminator.ClientHeader); got !=
|
||||
if got := standIn.question(t).header.Get(waf.ClientHeader); got !=
|
||||
tc.client {
|
||||
t.Errorf("the question names %s, want %s", got, tc.client)
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func randomBytes(n int) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
func TestFirst64KiBReachDiscriminatorAndTheWholeBodyTheApp(t *testing.T) {
|
||||
func TestFirst64KiBReachWAFAndTheWholeBodyTheApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, size := range []int{
|
||||
@@ -198,7 +198,7 @@ func TestFirst64KiBReachDiscriminatorAndTheWholeBodyTheApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := randomBytes(size)
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
received := make(chan []byte, 1)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
whole, _ := io.ReadAll(r.Body)
|
||||
@@ -244,7 +244,7 @@ func wantQuestionBody(
|
||||
len(want))
|
||||
}
|
||||
|
||||
values := asked.header.Values(discriminator.ContentLengthHeader)
|
||||
values := asked.header.Values(waf.ContentLengthHeader)
|
||||
|
||||
if announced {
|
||||
if !slices.Equal(values, []string{strconv.Itoa(size)}) ||
|
||||
@@ -265,7 +265,7 @@ func wantQuestionBody(
|
||||
func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
|
||||
strings.Repeat("x", questionBodyMaxBytes))
|
||||
|
||||
if status := readStatus(t, conn); status != http.StatusForbidden {
|
||||
t.Errorf("status %d, want discriminator's 403", status)
|
||||
t.Errorf("status %d, want waf's 403", status)
|
||||
}
|
||||
|
||||
if report := standIn.report(t); report.RequestBytes != questionBodyMaxBytes {
|
||||
@@ -307,14 +307,14 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
|
||||
t.Run(method+" "+tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
||||
|
||||
conn := dial(t, addr)
|
||||
send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing)
|
||||
|
||||
if status := readStatus(t, conn); status != http.StatusForbidden {
|
||||
t.Errorf("status %d, want discriminator's 403", status)
|
||||
t.Errorf("status %d, want waf's 403", status)
|
||||
}
|
||||
|
||||
asked := standIn.question(t)
|
||||
@@ -335,7 +335,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||
t.Error("the app was asked")
|
||||
})
|
||||
@@ -356,7 +356,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked")
|
||||
t.Error("waf was asked")
|
||||
}
|
||||
|
||||
standIn.wantNoReport(t)
|
||||
@@ -364,7 +364,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
||||
func TestEachMethodPassedOnReachesWAFAndTheApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, method := range []string{
|
||||
@@ -377,7 +377,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reached := make(chan string, 1)
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
reached <- r.Method
|
||||
})
|
||||
@@ -389,7 +389,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
||||
}
|
||||
|
||||
if asked := standIn.question(t); asked.method != method {
|
||||
t.Errorf("discriminator was asked about %s", asked.method)
|
||||
t.Errorf("waf was asked about %s", asked.method)
|
||||
}
|
||||
|
||||
if atApp := <-reached; atApp != method {
|
||||
@@ -399,7 +399,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
||||
func TestARequestThatCannotGoToWAFAsSentIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const host = "Host: site.example\r\n\r\n"
|
||||
@@ -421,7 +421,7 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -439,7 +439,7 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked")
|
||||
t.Error("waf was asked")
|
||||
}
|
||||
|
||||
standIn.wantNoReport(t)
|
||||
@@ -447,12 +447,12 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHostWithAPercentSignButNoBracketGoesToDiscriminator(t *testing.T) {
|
||||
func TestAHostWithAPercentSignButNoBracketGoesToWAF(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const host = "a%b"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -473,7 +473,7 @@ func TestATrustedPeerWithNoForwardedProtoIsAskedAboutAsHTTP(t *testing.T) {
|
||||
|
||||
const want = "http"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
trustedProxies: localhost + "/32",
|
||||
@@ -502,7 +502,7 @@ func TestAQuestionNamesNoZone(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
_, out, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||
trustedProxies: localhost + "/32",
|
||||
@@ -515,8 +515,8 @@ func TestAQuestionNamesNoZone(t *testing.T) {
|
||||
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
asked := standIn.question(t)
|
||||
if asked.header.Get(discriminator.ClientHeader) != tc.client ||
|
||||
asked.header.Get(discriminator.PeerHeader) != tc.peer {
|
||||
if asked.header.Get(waf.ClientHeader) != tc.client ||
|
||||
asked.header.Get(waf.PeerHeader) != tc.peer {
|
||||
t.Errorf("the question's headers are %v", asked.header)
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -16,16 +16,16 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
// flushAfterEachWrite has ReverseProxy pass on each part of the app's answer as
|
||||
// soon as it arrives.
|
||||
const flushAfterEachWrite time.Duration = -1
|
||||
|
||||
// reportStatusMax is the highest status discriminator takes in a report. An app can
|
||||
// reportStatusMax is the highest status waf takes in a report. An app can
|
||||
// send a higher one, up to 999, and the visitor gets that as it is.
|
||||
const reportStatusMax = 599
|
||||
|
||||
@@ -60,14 +60,14 @@ type request struct {
|
||||
peerTrusted bool
|
||||
start time.Time
|
||||
|
||||
// disposition is what is done with the request: discriminator's valid disposition,
|
||||
// disposition is what is done with the request: waf's valid disposition,
|
||||
// or SWRP_DEFAULT_DISPOSITION. It is "" when neither gave one.
|
||||
disposition string
|
||||
// answered is true when discriminator gave disposition, which makes a report due,
|
||||
// answered is true when waf gave disposition, which makes a report due,
|
||||
// and byDefault when SWRP_DEFAULT_DISPOSITION did.
|
||||
answered bool
|
||||
byDefault bool
|
||||
// asn and country are discriminator's X-Client-ASN and X-Client-Country, for the
|
||||
// asn and country are waf's X-Client-ASN and X-Client-Country, for the
|
||||
// app.
|
||||
asn, country string
|
||||
|
||||
@@ -131,14 +131,14 @@ func (rq *request) answerHealth() {
|
||||
// visitor too slow to send it is answered 408. For a malformed chunk, or a
|
||||
// visitor that went away or stopped its sending side, the connection is closed
|
||||
// without an answer. A body over SWRP_MAX_REQUEST_SIZE goes on with the part
|
||||
// read, so that discriminator is asked first; passToApp then refuses it.
|
||||
// read, so that waf is asked first; passToApp then refuses it.
|
||||
func (rq *request) readFirstPartOfBody() bool {
|
||||
if rq.body == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
_ = rq.rc.SetReadDeadline(rq.requestDeadline())
|
||||
held, err := io.ReadAll(io.LimitReader(rq.body, discriminator.QuestionBodyMaxBytes))
|
||||
held, err := io.ReadAll(io.LimitReader(rq.body, waf.QuestionBodyMaxBytes))
|
||||
// From here on, the request timer ends a read of the rest of the body, so that
|
||||
// the answer can name the side rproxy was waiting on.
|
||||
_ = rq.rc.SetReadDeadline(time.Time{})
|
||||
@@ -200,7 +200,7 @@ func (rq *request) forward(ctx context.Context) {
|
||||
// rewrite makes the request the app receives: the visitor's request, unchanged,
|
||||
// sent to SWRP_UPSTREAM_URL, with the forwarded headers and the request's id set,
|
||||
// without any X-Client-ASN or X-Client-Country the visitor sent, and with
|
||||
// discriminator's.
|
||||
// waf's.
|
||||
func (rq *request) rewrite(pr *httputil.ProxyRequest) {
|
||||
upstream := rq.h.config.UpstreamURL
|
||||
pr.Out.URL.Scheme = upstream.Scheme
|
||||
@@ -333,7 +333,7 @@ func (rq *request) answer(status int) {
|
||||
// handler has returned, after finish has written the log line and sent the report.
|
||||
// Without Content-Length, the flush would send the body in chunks, the last of
|
||||
// which would still wait for the handler. A HEAD's answer has no body, so any
|
||||
// Content-Length it has stays as discriminator gave it.
|
||||
// Content-Length it has stays as waf gave it.
|
||||
func (rq *request) send(status int, body []byte) {
|
||||
if rq.in.Method != http.MethodHead {
|
||||
rq.out.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
@@ -379,7 +379,7 @@ func (rq *request) refuse(status int) {
|
||||
}
|
||||
}
|
||||
|
||||
// finish ends the request's time limits, writes its log line and, if discriminator
|
||||
// finish ends the request's time limits, writes its log line and, if waf
|
||||
// answered the question, sends the report. ctx is the request's own context.
|
||||
func (rq *request) finish(ctx context.Context) {
|
||||
rq.stopTimers()
|
||||
@@ -416,7 +416,7 @@ func (rq *request) finish(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
report := &discriminator.Report{
|
||||
report := &waf.Report{
|
||||
RequestID: rq.id,
|
||||
Status: min(rq.out.status, reportStatusMax),
|
||||
RequestBytes: requestBytes,
|
||||
|
||||
+28
-28
@@ -1,10 +1,10 @@
|
||||
// Package rproxy is the reverse proxy in front of one app that asks discriminator
|
||||
// about each request. It passes a request discriminator allows to the app and the
|
||||
// Package rproxy is the reverse proxy in front of one app that asks waf
|
||||
// about each request. It passes a request waf allows to the app and the
|
||||
// app's answer back, within the size and time limits. Any other answer from
|
||||
// discriminator goes to the visitor as discriminator gave it. SWRP_DEFAULT_DISPOSITION
|
||||
// decides only when discriminator cannot be connected to, does not answer at all, or
|
||||
// waf goes to the visitor as waf gave it. SWRP_DEFAULT_DISPOSITION
|
||||
// decides only when waf cannot be connected to, does not answer at all, or
|
||||
// does not answer within SWRP_TIMEOUT_MS. rproxy itself refuses a request with a
|
||||
// method it does not pass on, or one it cannot put to discriminator as the visitor
|
||||
// method it does not pass on, or one it cannot put to waf as the visitor
|
||||
// sent it. It keeps nothing between requests and never touches disk. "rproxy" in
|
||||
// SPEC.md says what it does.
|
||||
package rproxy
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
// How rproxy keeps connections to the app and to discriminator open between
|
||||
// How rproxy keeps connections to the app and to waf open between
|
||||
// requests.
|
||||
const (
|
||||
idleConns = 100
|
||||
@@ -80,14 +80,14 @@ func New(params Params) *Server {
|
||||
backend := params.Config.WAFBackend
|
||||
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
|
||||
h := &handler{
|
||||
config: params.Config,
|
||||
requestLog: params.RequestLog,
|
||||
processLog: params.ProcessLog,
|
||||
errorLog: errorLog,
|
||||
transport: newTransport(),
|
||||
discriminatorTransport: newDiscriminatorTransport(),
|
||||
config: params.Config,
|
||||
requestLog: params.RequestLog,
|
||||
processLog: params.ProcessLog,
|
||||
errorLog: errorLog,
|
||||
transport: newTransport(),
|
||||
wafTransport: newWAFTransport(),
|
||||
reportURL: (&url.URL{
|
||||
Scheme: backend.Scheme, Host: backend.Host, Path: discriminator.ReportPath,
|
||||
Scheme: backend.Scheme, Host: backend.Host, Path: waf.ReportPath,
|
||||
}).String(),
|
||||
}
|
||||
|
||||
@@ -122,10 +122,10 @@ type handler struct {
|
||||
requestLog io.Writer
|
||||
processLog *slog.Logger
|
||||
errorLog *log.Logger
|
||||
// transport carries requests to the app, and discriminatorTransport questions and
|
||||
// reports to discriminator.
|
||||
transport http.RoundTripper
|
||||
discriminatorTransport http.RoundTripper
|
||||
// transport carries requests to the app, and wafTransport questions and
|
||||
// reports to waf.
|
||||
transport http.RoundTripper
|
||||
wafTransport http.RoundTripper
|
||||
// reportURL is where the reports go.
|
||||
reportURL string
|
||||
// reports waits for the reports being sent. reportsMu guards starting one
|
||||
@@ -147,11 +147,11 @@ func newTransport() *http.Transport {
|
||||
}
|
||||
}
|
||||
|
||||
// newDiscriminatorTransport returns what carries questions and reports to
|
||||
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
||||
// newWAFTransport returns what carries questions and reports to
|
||||
// waf. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
||||
// a visitor's Upgrade header, or a Connection header other than close or
|
||||
// keep-alive, and that would read as discriminator giving no answer.
|
||||
func newDiscriminatorTransport() *http.Transport {
|
||||
// keep-alive, and that would read as waf giving no answer.
|
||||
func newWAFTransport() *http.Transport {
|
||||
transport := newTransport()
|
||||
transport.Protocols = new(http.Protocols)
|
||||
transport.Protocols.SetHTTP1(true)
|
||||
@@ -160,15 +160,15 @@ func newDiscriminatorTransport() *http.Transport {
|
||||
}
|
||||
|
||||
// ServeHTTP handles one request: it answers the health check itself, refuses a
|
||||
// request with a method it does not pass on or one it cannot put to discriminator as
|
||||
// request with a method it does not pass on or one it cannot put to waf as
|
||||
// the visitor sent it, and for any other request reads the first part of the
|
||||
// body, asks discriminator, and does what the answer says. Once the answer has ended,
|
||||
// body, asks waf, and does what the answer says. Once the answer has ended,
|
||||
// it writes the request's log line and sends its report.
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rq := h.newRequest(w, r)
|
||||
defer rq.finish(r.Context())
|
||||
|
||||
if r.Method == http.MethodGet && r.URL.Path == discriminator.HealthPath {
|
||||
if r.Method == http.MethodGet && r.URL.Path == waf.HealthPath {
|
||||
rq.answerHealth()
|
||||
|
||||
return
|
||||
@@ -201,9 +201,9 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rq.noteAnswer(answer)
|
||||
|
||||
switch rq.disposition {
|
||||
case discriminator.DispositionAllow:
|
||||
case waf.DispositionAllow:
|
||||
rq.passToApp(r.Context())
|
||||
case discriminator.DispositionClose:
|
||||
case waf.DispositionClose:
|
||||
// This panic has Go's server close the connection without an answer,
|
||||
// and log nothing; the deferred finish still runs.
|
||||
panic(http.ErrAbortHandler)
|
||||
@@ -213,7 +213,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// passToApp passes the request on to the app, unless a limit or its path keeps
|
||||
// it from the app. discriminator answers a path under ownEndpointPrefix itself, so
|
||||
// it from the app. waf answers a path under ownEndpointPrefix itself, so
|
||||
// such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
|
||||
// request's own context.
|
||||
func (rq *request) passToApp(ctx context.Context) {
|
||||
|
||||
+101
-101
@@ -19,9 +19,9 @@ import (
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,27 +54,27 @@ const (
|
||||
|
||||
// The headers the tests set and look at.
|
||||
const (
|
||||
dispositionHeader = discriminator.DispositionHeader
|
||||
dispositionHeader = waf.DispositionHeader
|
||||
forwardedForHeader = "X-Forwarded-For"
|
||||
forwardedProtoHeader = "X-Forwarded-Proto"
|
||||
requestIDHeader = "X-Request-ID"
|
||||
asnHeader = "X-Client-Asn"
|
||||
countryHeader = "X-Client-Country"
|
||||
contentLengthHeader = "Content-Length"
|
||||
// fromDiscriminatorHeader is a header the stand-in for discriminator adds to a deny.
|
||||
fromDiscriminatorHeader = "X-From-Discriminator"
|
||||
// fromWAFHeader is a header the stand-in for waf adds to a deny.
|
||||
fromWAFHeader = "X-From-Waf"
|
||||
// addedHeaderValue is the value of a header a test adds to an answer, to see it
|
||||
// reach the visitor.
|
||||
addedHeaderValue = "yes"
|
||||
)
|
||||
|
||||
// What the stand-in for discriminator answers.
|
||||
// What the stand-in for waf answers.
|
||||
const (
|
||||
dispositionAllow = discriminator.DispositionAllow
|
||||
dispositionDeny = discriminator.DispositionDeny
|
||||
dispositionClose = discriminator.DispositionClose
|
||||
dispositionAllow = waf.DispositionAllow
|
||||
dispositionDeny = waf.DispositionDeny
|
||||
dispositionClose = waf.DispositionClose
|
||||
// deniedBody is the body of a deny.
|
||||
deniedBody = "refused by discriminator"
|
||||
deniedBody = "refused by waf"
|
||||
)
|
||||
|
||||
// output collects what rproxy writes on stdout.
|
||||
@@ -158,7 +158,7 @@ func (l heldLog) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// question is what the stand-in for discriminator received as a question.
|
||||
// question is what the stand-in for waf received as a question.
|
||||
type question struct {
|
||||
method, target, host string
|
||||
header http.Header
|
||||
@@ -167,26 +167,26 @@ type question struct {
|
||||
transferEncoding []string
|
||||
}
|
||||
|
||||
// Discriminator is a stand-in for discriminator that answers each question as
|
||||
// WAF is a stand-in for waf that answers each question as
|
||||
// answer does, and keeps the questions and the reports it gets.
|
||||
type Discriminator struct {
|
||||
type WAF struct {
|
||||
*httptest.Server
|
||||
|
||||
questions chan question
|
||||
reports chan discriminator.Report
|
||||
reports chan waf.Report
|
||||
}
|
||||
|
||||
// startDiscriminator starts a stand-in for discriminator.
|
||||
func startDiscriminator(t *testing.T, answer http.HandlerFunc) *Discriminator {
|
||||
// startWAF starts a stand-in for waf.
|
||||
func startWAF(t *testing.T, answer http.HandlerFunc) *WAF {
|
||||
t.Helper()
|
||||
|
||||
s := &Discriminator{
|
||||
s := &WAF{
|
||||
questions: make(chan question, 10),
|
||||
reports: make(chan discriminator.Report, 10),
|
||||
reports: make(chan waf.Report, 10),
|
||||
}
|
||||
s.Server = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(discriminator.ClientHeader) == "" {
|
||||
if r.Header.Get(waf.ClientHeader) == "" {
|
||||
s.takeReport(t, w, r)
|
||||
|
||||
return
|
||||
@@ -209,17 +209,17 @@ func startDiscriminator(t *testing.T, answer http.HandlerFunc) *Discriminator {
|
||||
return s
|
||||
}
|
||||
|
||||
// takeReport keeps a report and answers it as discriminator does.
|
||||
func (s *Discriminator) takeReport(
|
||||
// takeReport keeps a report and answers it as waf does.
|
||||
func (s *WAF) takeReport(
|
||||
t *testing.T, w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var report discriminator.Report
|
||||
var report waf.Report
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&report)
|
||||
if err != nil || r.Method != http.MethodPost ||
|
||||
r.URL.Path != discriminator.ReportPath {
|
||||
r.URL.Path != waf.ReportPath {
|
||||
t.Errorf("%s %s is no report: %v", r.Method, r.URL.Path, err)
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ func (s *Discriminator) takeReport(
|
||||
}
|
||||
|
||||
// wantNoReport checks that no report comes for a moment.
|
||||
func (s *Discriminator) wantNoReport(t *testing.T) {
|
||||
func (s *WAF) wantNoReport(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
@@ -240,7 +240,7 @@ func (s *Discriminator) wantNoReport(t *testing.T) {
|
||||
}
|
||||
|
||||
// report waits for a report.
|
||||
func (s *Discriminator) report(t *testing.T) discriminator.Report {
|
||||
func (s *WAF) report(t *testing.T) waf.Report {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
@@ -249,12 +249,12 @@ func (s *Discriminator) report(t *testing.T) discriminator.Report {
|
||||
case <-time.After(waitLimit):
|
||||
t.Fatal("no report")
|
||||
|
||||
return discriminator.Report{}
|
||||
return waf.Report{}
|
||||
}
|
||||
}
|
||||
|
||||
// question waits for a question.
|
||||
func (s *Discriminator) question(t *testing.T) question {
|
||||
func (s *WAF) question(t *testing.T) question {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
@@ -267,17 +267,17 @@ func (s *Discriminator) question(t *testing.T) question {
|
||||
}
|
||||
}
|
||||
|
||||
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
|
||||
// startWAFAnsweringReports starts a stand-in for waf that
|
||||
// answers each question as answer does and each report as report does, and
|
||||
// returns its URL.
|
||||
func startDiscriminatorAnsweringReports(
|
||||
func startWAFAnsweringReports(
|
||||
t *testing.T, answer, report http.HandlerFunc,
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
backend := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
||||
if r.Header.Get(waf.ClientHeader) != "" {
|
||||
answer(w, r)
|
||||
|
||||
return
|
||||
@@ -299,7 +299,7 @@ func allow(w http.ResponseWriter, _ *http.Request) {
|
||||
// deny answers a question with deny, status 403 and a body of its own.
|
||||
func deny(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(dispositionHeader, dispositionDeny)
|
||||
w.Header().Set(fromDiscriminatorHeader, addedHeaderValue)
|
||||
w.Header().Set(fromWAFHeader, addedHeaderValue)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = io.WriteString(w, deniedBody)
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func greet(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, greeting)
|
||||
}
|
||||
|
||||
// startRProxy starts rproxy with discriminator at backend and the app at appURL, and
|
||||
// startRProxy starts rproxy with waf at backend and the app at appURL, and
|
||||
// the settings in env on top of the defaults. It returns where rproxy listens
|
||||
// and what it writes.
|
||||
func startRProxy(
|
||||
@@ -502,7 +502,7 @@ func readStatus(t *testing.T, conn net.Conn) int {
|
||||
func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(asnHeader, "AS64496")
|
||||
w.Header().Set(countryHeader, "DE")
|
||||
allow(w, r)
|
||||
@@ -561,7 +561,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
|
||||
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
|
||||
}
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
seen := make(chan http.Header, 1)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen <- r.Header.Clone()
|
||||
@@ -606,7 +606,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
|
||||
func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
seen := make(chan http.Header, 1)
|
||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen <- r.Header.Clone()
|
||||
@@ -645,7 +645,7 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
|
||||
func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header()["Content-Type"] = nil
|
||||
_, _ = io.WriteString(w, "<html>")
|
||||
@@ -667,19 +667,19 @@ func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovesTheVisitorsLookupHeadersWhenDiscriminatorGivesNone(t *testing.T) {
|
||||
func TestRemovesTheVisitorsLookupHeadersWhenWAFGivesNone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
backend func(t *testing.T) string
|
||||
}{
|
||||
{"discriminator allows", func(t *testing.T) string {
|
||||
{"waf allows", func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
return startDiscriminator(t, allow).URL
|
||||
return startWAF(t, allow).URL
|
||||
}},
|
||||
{"discriminator is down", func(*testing.T) string { return nowhere }},
|
||||
{"waf is down", func(*testing.T) string { return nowhere }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -705,10 +705,10 @@ func TestRemovesTheVisitorsLookupHeadersWhenDiscriminatorGivesNone(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
||||
func TestDenyGivesTheVisitorWAFsAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||
t.Error("the app was asked")
|
||||
})
|
||||
@@ -716,8 +716,8 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
||||
|
||||
got := get(t, addr, "/")
|
||||
if got.status != http.StatusForbidden || got.body != deniedBody ||
|
||||
got.header.Get(fromDiscriminatorHeader) != addedHeaderValue {
|
||||
t.Errorf("answered %+v, want discriminator's answer", got)
|
||||
got.header.Get(fromWAFHeader) != addedHeaderValue {
|
||||
t.Errorf("answered %+v, want waf's answer", got)
|
||||
}
|
||||
|
||||
if len(got.header.Values(dispositionHeader)) != 0 {
|
||||
@@ -733,21 +733,21 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
||||
|
||||
line := out.requestLine(t)
|
||||
if line["disposition"] != dispositionDeny || line["default_disposition"] != nil {
|
||||
t.Errorf("line %v, want discriminator's deny", line)
|
||||
t.Errorf("line %v, want waf's deny", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADeniedHeadKeepsDiscriminatorsContentLength(t *testing.T) {
|
||||
func TestADeniedHeadKeepsWAFsContentLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
|
||||
if got.status != http.StatusForbidden ||
|
||||
got.header.Get(contentLengthHeader) != strconv.Itoa(len(deniedBody)) {
|
||||
t.Errorf("answered %+v, want discriminator's 403 and its length", got)
|
||||
t.Errorf("answered %+v, want waf's 403 and its length", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,13 +762,13 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
|
||||
}{
|
||||
{"a deny", "GET /" + host, deniedBody, http.StatusForbidden},
|
||||
{"rproxy's own answer", "GET x:admin" + host, "Bad Request\n", http.StatusBadRequest},
|
||||
{"the health check", "GET " + discriminator.HealthPath + host, "ok\n", http.StatusOK},
|
||||
{"the health check", "GET " + waf.HealthPath + host, "ok\n", http.StatusOK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := heldLog{release: make(chan struct{})}
|
||||
standIn := startDiscriminator(t, deny)
|
||||
standIn := startWAF(t, deny)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startServerLogging(t, standIn.URL, app.URL, nil, log, io.Discard)
|
||||
|
||||
@@ -798,7 +798,7 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
|
||||
func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -821,7 +821,7 @@ func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
|
||||
func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(dispositionHeader, dispositionClose)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
@@ -853,7 +853,7 @@ func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
||||
func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend := startDiscriminatorAnsweringReports(t,
|
||||
backend := startWAFAnsweringReports(t,
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(dispositionHeader, dispositionClose)
|
||||
},
|
||||
@@ -878,17 +878,17 @@ func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// answeringWith returns what starts a stand-in for discriminator that answers each
|
||||
// answeringWith returns what starts a stand-in for waf that answers each
|
||||
// question as answer does, and returns its URL.
|
||||
func answeringWith(answer http.HandlerFunc) func(t *testing.T) string {
|
||||
return func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
return startDiscriminator(t, answer).URL
|
||||
return startWAF(t, answer).URL
|
||||
}
|
||||
}
|
||||
|
||||
// noAnswers are the ways discriminator can leave the decision to the default.
|
||||
// noAnswers are the ways waf can leave the decision to the default.
|
||||
func noAnswers() map[string]func(t *testing.T) string {
|
||||
return map[string]func(t *testing.T) string{
|
||||
"no connection": func(*testing.T) string { return nowhere },
|
||||
@@ -920,7 +920,7 @@ func noAnswers() map[string]func(t *testing.T) string {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.T) {
|
||||
func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
@@ -929,7 +929,7 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
||||
}{
|
||||
{"a refusal", dispositionDeny, deniedBody, http.StatusForbidden},
|
||||
{"a refusal, 429", dispositionDeny, deniedBody, http.StatusTooManyRequests},
|
||||
{"discriminator's 400", "", "refused\n", http.StatusBadRequest},
|
||||
{"waf's 400", "", "refused\n", http.StatusBadRequest},
|
||||
{"a 500", "", "broken\n", http.StatusInternalServerError},
|
||||
{"a 503 denied", dispositionDeny, "busy\n", http.StatusServiceUnavailable},
|
||||
{"no header", "", "who knows\n", http.StatusOK},
|
||||
@@ -939,12 +939,12 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
||||
t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
if tc.disposition != "" {
|
||||
w.Header().Set(dispositionHeader, tc.disposition)
|
||||
}
|
||||
|
||||
w.Header().Set(fromDiscriminatorHeader, addedHeaderValue)
|
||||
w.Header().Set(fromWAFHeader, addedHeaderValue)
|
||||
w.WriteHeader(tc.status)
|
||||
_, _ = io.WriteString(w, tc.body)
|
||||
})
|
||||
@@ -957,8 +957,8 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
||||
|
||||
got := get(t, addr, "/")
|
||||
if got.status != tc.status || got.body != tc.body ||
|
||||
got.header.Get(fromDiscriminatorHeader) != addedHeaderValue {
|
||||
t.Errorf("answered %+v, want discriminator's answer", got)
|
||||
got.header.Get(fromWAFHeader) != addedHeaderValue {
|
||||
t.Errorf("answered %+v, want waf's answer", got)
|
||||
}
|
||||
|
||||
if len(got.header.Values(dispositionHeader)) != 0 {
|
||||
@@ -970,8 +970,8 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
||||
t.Errorf("line %v, want status %d and no default", line, tc.status)
|
||||
}
|
||||
|
||||
if out.has("discriminator gave no answer") {
|
||||
t.Error("rproxy warned that discriminator gave no answer")
|
||||
if out.has("waf gave no answer") {
|
||||
t.Error("rproxy warned that waf gave no answer")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -981,14 +981,14 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
||||
func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, "refused", http.StatusBadRequest)
|
||||
})
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
if got := get(t, addr, "/"); got.status != http.StatusBadRequest {
|
||||
t.Errorf("answered %+v, want discriminator's 400", got)
|
||||
t.Errorf("answered %+v, want waf's 400", got)
|
||||
}
|
||||
|
||||
if line := out.requestLine(t); line["disposition"] != nil {
|
||||
@@ -1001,7 +1001,7 @@ func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
|
||||
func TestAnAnswerWithoutAContentTypeGoesToTheVisitorWithNone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header()["Content-Type"] = nil
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = io.WriteString(w, "<html>broken")
|
||||
@@ -1019,7 +1019,7 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
||||
|
||||
const size = 1 << 20
|
||||
|
||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(make([]byte, size))
|
||||
})
|
||||
@@ -1027,15 +1027,15 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
||||
|
||||
got := get(t, addr, "/")
|
||||
if got.status != http.StatusInternalServerError || len(got.body) != size {
|
||||
t.Errorf("answered %d with %d bytes, want discriminator's 500 with %d",
|
||||
t.Errorf("answered %d with %d bytes, want waf's 500 with %d",
|
||||
got.status, len(got.body), size)
|
||||
}
|
||||
}
|
||||
|
||||
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
||||
// startWAFWriting starts a stand-in for waf that reads
|
||||
// each question and answers it with text, as it is, and returns its URL. It
|
||||
// can send what Go's server would not.
|
||||
func startDiscriminatorWriting(t *testing.T, text string) string {
|
||||
func startWAFWriting(t *testing.T, text string) string {
|
||||
t.Helper()
|
||||
|
||||
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||
@@ -1105,7 +1105,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
||||
"a status below 100": func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
return startDiscriminatorWriting(t, "HTTP/1.1 099 Odd\r\nContent-Length: 0\r\n\r\n")
|
||||
return startWAFWriting(t, "HTTP/1.1 099 Odd\r\nContent-Length: 0\r\n\r\n")
|
||||
},
|
||||
"a 101 that holds its connection open": answeringWith(switchAndHold),
|
||||
} {
|
||||
@@ -1130,7 +1130,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
||||
t.Errorf("line %v, want status 502 and no disposition", line)
|
||||
}
|
||||
|
||||
out.waitFor(t, "discriminator's answer is dropped")
|
||||
out.waitFor(t, "waf's answer is dropped")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1139,7 +1139,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
||||
func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, overOneMiB(dispositionDeny))
|
||||
standIn := startWAF(t, overOneMiB(dispositionDeny))
|
||||
addr, out := startRProxy(t, standIn.URL, nowhere, nil)
|
||||
|
||||
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
||||
@@ -1196,14 +1196,14 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAVisitorThatGoesAwayWhileDiscriminatorIsAskedGetsNoDefault(t *testing.T) {
|
||||
func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, disposition := range []string{dispositionAllow, dispositionDeny} {
|
||||
t.Run("with the default "+disposition, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
standIn := startWAF(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
<-r.Context().Done()
|
||||
})
|
||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||
@@ -1226,8 +1226,8 @@ func TestAVisitorThatGoesAwayWhileDiscriminatorIsAskedGetsNoDefault(t *testing.T
|
||||
t.Errorf("line %v, want status 0 and no disposition", line)
|
||||
}
|
||||
|
||||
if out.has("discriminator gave no answer") {
|
||||
t.Error("rproxy warned that discriminator gave no answer")
|
||||
if out.has("waf gave no answer") {
|
||||
t.Error("rproxy warned that waf gave no answer")
|
||||
}
|
||||
|
||||
standIn.wantNoReport(t)
|
||||
@@ -1242,13 +1242,13 @@ func TestRProxyWarnsWhenTheDefaultDecides(t *testing.T) {
|
||||
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
||||
|
||||
wantGreeting(t, get(t, addr, "/"))
|
||||
out.waitFor(t, "discriminator gave no answer")
|
||||
out.waitFor(t, "waf gave no answer")
|
||||
}
|
||||
|
||||
func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
standIn := startWAF(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||
<-r.Context().Done()
|
||||
})
|
||||
app := startApp(t, greet)
|
||||
@@ -1275,29 +1275,29 @@ func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthCheckIsAnsweredWhileDiscriminatorIsDown(t *testing.T) {
|
||||
func TestHealthCheckIsAnsweredWhileWAFIsDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
||||
|
||||
got := get(t, addr, discriminator.HealthPath)
|
||||
got := get(t, addr, waf.HealthPath)
|
||||
if got.status != http.StatusOK || got.body != "ok\n" {
|
||||
t.Errorf("answered %+v, want ok", got)
|
||||
}
|
||||
|
||||
line := out.requestLine(t)
|
||||
if line["path"] != discriminator.HealthPath || line["disposition"] != nil {
|
||||
if line["path"] != waf.HealthPath || line["disposition"] != nil {
|
||||
t.Errorf("line %v, want one with no disposition", line)
|
||||
}
|
||||
|
||||
// With discriminator up too, it is not asked.
|
||||
// With waf up too, it is not asked.
|
||||
addr, _ = startRProxy(t, standIn.URL, app.URL, nil)
|
||||
get(t, addr, discriminator.HealthPath)
|
||||
get(t, addr, waf.HealthPath)
|
||||
|
||||
if len(standIn.questions) != 0 {
|
||||
t.Error("discriminator was asked about the health check")
|
||||
t.Error("waf was asked about the health check")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1305,7 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
release := make(chan struct{})
|
||||
backend := startDiscriminatorAnsweringReports(t, deny,
|
||||
backend := startWAFAnsweringReports(t, deny,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-release:
|
||||
@@ -1340,16 +1340,16 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
|
||||
_ = res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("status %d, want discriminator's 403", res.StatusCode)
|
||||
t.Errorf("status %d, want waf's 403", res.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAReportDiscriminatorLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
|
||||
func TestAReportWAFLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
release := make(chan struct{})
|
||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
||||
backend := startWAFAnsweringReports(t, allow,
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
<-release
|
||||
|
||||
@@ -1369,10 +1369,10 @@ func TestAReportDiscriminatorLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.
|
||||
out.waitFor(t, "a report is dropped")
|
||||
}
|
||||
|
||||
func TestAReportDiscriminatorRefusesIsDroppedWithAWarning(t *testing.T) {
|
||||
func TestAReportWAFRefusesIsDroppedWithAWarning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
||||
backend := startWAFAnsweringReports(t, allow,
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
@@ -1387,7 +1387,7 @@ func TestAReportDiscriminatorRefusesIsDroppedWithAWarning(t *testing.T) {
|
||||
func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend := startDiscriminatorAnsweringReports(t, allow, switchAndHold)
|
||||
backend := startWAFAnsweringReports(t, allow, switchAndHold)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, backend, app.URL, map[string]string{
|
||||
timeoutSetting: "60000",
|
||||
@@ -1400,7 +1400,7 @@ func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
||||
func TestAReportDueAfterWaitForReportsIsDropped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out, server := startServer(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -1416,7 +1416,7 @@ func TestReportGivesWhatHappened(t *testing.T) {
|
||||
|
||||
const body = "the visitor's body"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
@@ -1446,7 +1446,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
backend := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
||||
if r.Header.Get(waf.ClientHeader) != "" {
|
||||
allow(w, r)
|
||||
|
||||
return
|
||||
@@ -1501,7 +1501,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
|
||||
func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, echoAfterUpgrade)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
maxRequestTime: "2000",
|
||||
@@ -1556,7 +1556,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
|
||||
|
||||
const goodbye = "sent after the visitor stopped sending\n"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
conn, buffered, err := http.NewResponseController(w).Hijack()
|
||||
if err != nil {
|
||||
@@ -1614,7 +1614,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
|
||||
//nolint:paralleltest // see above
|
||||
func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) {
|
||||
closed := make(chan error, 1)
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
conn, buffered, err := http.NewResponseController(w).Hijack()
|
||||
if err != nil {
|
||||
@@ -1685,7 +1685,7 @@ func TestEachRequestGetsAFreshID(t *testing.T) {
|
||||
|
||||
const fromTraefik = "an-id-from-traefik"
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||
trustedProxies: localhost + "/32",
|
||||
@@ -1715,7 +1715,7 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
|
||||
body = "the visitor's body"
|
||||
)
|
||||
|
||||
standIn := startDiscriminator(t, allow)
|
||||
standIn := startWAF(t, allow)
|
||||
app := startApp(t, greet)
|
||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
// answerBodyMaxBytes is the most of the body of discriminator's answer that is read.
|
||||
// answerBodyMaxBytes is the most of the body of waf's answer that is read.
|
||||
// It is far above any answer's.
|
||||
const answerBodyMaxBytes = 1 << 20
|
||||
|
||||
// reportAnswerMaxBytes is the most of the answer to a report that is read,
|
||||
// which has no body when discriminator takes the report.
|
||||
// which has no body when waf takes the report.
|
||||
const reportAnswerMaxBytes = 4 << 10
|
||||
|
||||
var (
|
||||
@@ -27,22 +27,22 @@ var (
|
||||
errReportRefused = errors.New("the report was answered")
|
||||
)
|
||||
|
||||
// discriminatorAnswer is what discriminator answered to a question.
|
||||
type discriminatorAnswer struct {
|
||||
// wafAnswer is what waf answered to a question.
|
||||
type wafAnswer struct {
|
||||
status int
|
||||
header http.Header
|
||||
body []byte
|
||||
}
|
||||
|
||||
// noteAnswer notes what discriminator answered. An answer without a valid disposition
|
||||
// notes nothing, and no report is due for it: discriminator holds no decision to report
|
||||
// noteAnswer notes what waf answered. An answer without a valid disposition
|
||||
// notes nothing, and no report is due for it: waf holds no decision to report
|
||||
// on.
|
||||
func (rq *request) noteAnswer(answer *discriminatorAnswer) {
|
||||
disposition := answer.header.Get(discriminator.DispositionHeader)
|
||||
func (rq *request) noteAnswer(answer *wafAnswer) {
|
||||
disposition := answer.header.Get(waf.DispositionHeader)
|
||||
|
||||
switch disposition {
|
||||
case discriminator.DispositionAllow, discriminator.DispositionDeny,
|
||||
discriminator.DispositionClose:
|
||||
case waf.DispositionAllow, waf.DispositionDeny,
|
||||
waf.DispositionClose:
|
||||
rq.disposition = disposition
|
||||
rq.answered = true
|
||||
rq.asn = answer.header.Get(asnHeader)
|
||||
@@ -55,27 +55,27 @@ func (rq *request) noteAnswer(answer *discriminatorAnswer) {
|
||||
func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
||||
switch {
|
||||
case ctx.Err() != nil:
|
||||
// The visitor went away while discriminator was being asked. discriminator has not
|
||||
// The visitor went away while waf was being asked. waf has not
|
||||
// failed, and nothing is noted. This panic has Go's server close the
|
||||
// connection without an answer, and log nothing; the deferred finish still
|
||||
// runs.
|
||||
panic(http.ErrAbortHandler)
|
||||
case errors.Is(err, errCannotPassOn):
|
||||
rq.h.processLog.Warn("discriminator's answer is dropped", "error", err.Error())
|
||||
rq.h.processLog.Warn("waf's answer is dropped", "error", err.Error())
|
||||
rq.answer(http.StatusBadGateway)
|
||||
default:
|
||||
// discriminator cannot be connected to, does not answer at all, or does not answer
|
||||
// within SWRP_TIMEOUT_MS. A request that keeps discriminator busy past
|
||||
// SWRP_TIMEOUT_MS, like any request while discriminator is overloaded, gets
|
||||
// waf cannot be connected to, does not answer at all, or does not answer
|
||||
// within SWRP_TIMEOUT_MS. A request that keeps waf busy past
|
||||
// SWRP_TIMEOUT_MS, like any request while waf is overloaded, gets
|
||||
// SWRP_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
|
||||
// whenever discriminator is slow. Making such requests hard to send is
|
||||
// whenever waf is slow. Making such requests hard to send is
|
||||
// https://git.eeqj.de/sneak/smallwebwaf/issues/182.
|
||||
rq.h.processLog.Warn("discriminator gave no answer", "error", err.Error())
|
||||
rq.h.processLog.Warn("waf gave no answer", "error", err.Error())
|
||||
|
||||
rq.disposition = rq.h.config.DefaultDisposition
|
||||
rq.byDefault = true
|
||||
|
||||
if rq.disposition == discriminator.DispositionAllow {
|
||||
if rq.disposition == waf.DispositionAllow {
|
||||
rq.passToApp(ctx)
|
||||
} else {
|
||||
rq.answer(http.StatusServiceUnavailable)
|
||||
@@ -83,10 +83,10 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ask sends discriminator the question, and returns its answer, which must have come
|
||||
// ask sends waf the question, and returns its answer, which must have come
|
||||
// whole within SWRP_TIMEOUT_MS. It ends when ctx does, as when the visitor goes
|
||||
// away.
|
||||
func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
||||
func (rq *request) ask(ctx context.Context) (*wafAnswer, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, rq.h.config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -95,7 +95,7 @@ func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := rq.h.discriminatorTransport.RoundTrip(question)
|
||||
res, err := rq.h.wafTransport.RoundTrip(question)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send the question: %w", err)
|
||||
}
|
||||
@@ -120,17 +120,17 @@ func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
||||
return nil, fmt.Errorf("%w: its body is over 1 MiB", errCannotPassOn)
|
||||
}
|
||||
|
||||
return &discriminatorAnswer{res.StatusCode, res.Header, body}, nil
|
||||
return &wafAnswer{res.StatusCode, res.Header, body}, nil
|
||||
}
|
||||
|
||||
// canBeAsked reports whether Go's client can send discriminator r as the visitor sent
|
||||
// canBeAsked reports whether Go's client can send waf r as the visitor sent
|
||||
// it. It cannot send these as they came:
|
||||
// - a target that does not start with /, or that starts with //;
|
||||
// - an empty Host, for which it sends discriminator's own address;
|
||||
// - an empty Host, for which it sends waf's own address;
|
||||
// - a Host holding an IPv6 zone, which it removes;
|
||||
// - more than one User-Agent, of which it sends the first;
|
||||
// - an empty User-Agent, which it leaves out, the one case that still goes to
|
||||
// discriminator.
|
||||
// waf.
|
||||
func canBeAsked(r *http.Request) bool {
|
||||
target, host := r.RequestURI, r.Host
|
||||
hostHasZone := strings.HasPrefix(host, "[") && strings.Contains(host, "%")
|
||||
@@ -170,13 +170,13 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
|
||||
question.TransferEncoding = in.TransferEncoding
|
||||
|
||||
question.Header = in.Header.Clone()
|
||||
question.Header.Set(discriminator.ClientHeader, rq.client.String())
|
||||
question.Header.Set(discriminator.PeerHeader, rq.peer.String())
|
||||
question.Header.Del(discriminator.ContentLengthHeader)
|
||||
question.Header.Set(waf.ClientHeader, rq.client.String())
|
||||
question.Header.Set(waf.PeerHeader, rq.peer.String())
|
||||
question.Header.Del(waf.ContentLengthHeader)
|
||||
|
||||
announced := in.Header.Get("Content-Length")
|
||||
if announced != "" {
|
||||
question.Header.Set(discriminator.ContentLengthHeader, announced)
|
||||
question.Header.Set(waf.ContentLengthHeader, announced)
|
||||
}
|
||||
|
||||
question.Header.Set(requestIDHeader, rq.id)
|
||||
@@ -191,13 +191,13 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
|
||||
return question, nil
|
||||
}
|
||||
|
||||
// answerWith gives the visitor discriminator's answer, less its disposition.
|
||||
func (rq *request) answerWith(answer *discriminatorAnswer) {
|
||||
// answerWith gives the visitor waf's answer, less its disposition.
|
||||
func (rq *request) answerWith(answer *wafAnswer) {
|
||||
rq.prepareOwnAnswer(rq.requestDeadline())
|
||||
|
||||
header := rq.out.Header()
|
||||
maps.Copy(header, answer.header)
|
||||
header.Del(discriminator.DispositionHeader)
|
||||
header.Del(waf.DispositionHeader)
|
||||
|
||||
// Go's server adds a Content-Type it guesses from the body to a response whose
|
||||
// headers have no Content-Type key. The key with no value sends none.
|
||||
@@ -208,10 +208,10 @@ func (rq *request) answerWith(answer *discriminatorAnswer) {
|
||||
rq.send(answer.status, answer.body)
|
||||
}
|
||||
|
||||
// sendReport sends discriminator report on a goroutine of its own, so that the
|
||||
// sendReport sends waf report on a goroutine of its own, so that the
|
||||
// visitor does not wait for it. The report goes on after ctx, the request's own
|
||||
// context, has ended.
|
||||
func (h *handler) sendReport(ctx context.Context, report *discriminator.Report) {
|
||||
func (h *handler) sendReport(ctx context.Context, report *waf.Report) {
|
||||
h.reportsMu.Lock()
|
||||
defer h.reportsMu.Unlock()
|
||||
|
||||
@@ -227,9 +227,9 @@ func (h *handler) sendReport(ctx context.Context, report *discriminator.Report)
|
||||
})
|
||||
}
|
||||
|
||||
// postReport posts report to discriminator, and waits for its answer for up to
|
||||
// postReport posts report to waf, and waits for its answer for up to
|
||||
// SWRP_TIMEOUT_MS.
|
||||
func (h *handler) postReport(ctx context.Context, report *discriminator.Report) error {
|
||||
func (h *handler) postReport(ctx context.Context, report *waf.Report) error {
|
||||
body, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode the report: %w", err)
|
||||
@@ -246,7 +246,7 @@ func (h *handler) postReport(ctx context.Context, report *discriminator.Report)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := h.discriminatorTransport.RoundTrip(req)
|
||||
res, err := h.wafTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send the report: %w", err)
|
||||
}
|
||||
Reference in new issue
Block a user