Serves a page explaining why a domain was blocked instead of leaving a sinkholed client with a dead connection. Binds its own HTTP/HTTPS listeners with self-signed, per-hostname TLS certs generated on the fly, re-evaluates the requesting client against the policy engine per request, and renders an HTML template editable from Settings with a live preview. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTKpGMQzpfDsvedu1hvSUf
92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package blockpage
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func discardLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
|
|
func TestRenderFillsPlaceholders(t *testing.T) {
|
|
out, err := Render(`<h1>{{.Domain}} blocked by {{.ListName}} ({{.PolicyName}})</h1>`)
|
|
if err != nil {
|
|
t.Fatalf("render: %v", err)
|
|
}
|
|
if !strings.Contains(out, "ads.example-tracker.com blocked by Advertising (Default Protection)") {
|
|
t.Errorf("unexpected output: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderRejectsInvalidTemplate(t *testing.T) {
|
|
if _, err := Render(`{{.Domain`); err == nil {
|
|
t.Fatal("expected a parse error for unterminated action")
|
|
}
|
|
}
|
|
|
|
func TestRenderEscapesAttackerControlledDomain(t *testing.T) {
|
|
// html/template must escape a hostile-looking hostname; a browser-facing
|
|
// error page has no business ever emitting attacker HTML unescaped.
|
|
out, err := Render(`<p>{{.Domain}}</p>`)
|
|
if err != nil {
|
|
t.Fatalf("render: %v", err)
|
|
}
|
|
if strings.Contains(out, "<script>") {
|
|
t.Errorf("output contains an unescaped tag: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestCertStoreGeneratesMatchingSAN(t *testing.T) {
|
|
store := newCertStore()
|
|
cert, err := store.get(&tls.ClientHelloInfo{ServerName: "blocked.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("get certificate: %v", err)
|
|
}
|
|
leaf, err := parseLeaf(cert)
|
|
if err != nil {
|
|
t.Fatalf("parse leaf: %v", err)
|
|
}
|
|
if len(leaf.DNSNames) != 1 || leaf.DNSNames[0] != "blocked.example.com" {
|
|
t.Errorf("DNSNames = %v, want [blocked.example.com]", leaf.DNSNames)
|
|
}
|
|
}
|
|
|
|
func TestCertStoreCachesByHostname(t *testing.T) {
|
|
store := newCertStore()
|
|
a, err := store.get(&tls.ClientHelloInfo{ServerName: "a.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
b, err := store.get(&tls.ClientHelloInfo{ServerName: "a.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if a != b {
|
|
t.Error("expected the same cached certificate for a repeated hostname")
|
|
}
|
|
|
|
c, err := store.get(&tls.ClientHelloInfo{ServerName: "b.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if a == c {
|
|
t.Error("expected a different certificate for a different hostname")
|
|
}
|
|
}
|
|
|
|
func TestServerFallsBackOnBrokenTemplate(t *testing.T) {
|
|
s := &Server{log: discardLogger()}
|
|
tmpl := s.compiled(`{{.Domain`)
|
|
var b strings.Builder
|
|
if err := tmpl.Execute(&b, pageData{}); err != nil {
|
|
t.Fatalf("execute fallback: %v", err)
|
|
}
|
|
if !strings.Contains(b.String(), "misconfigured") {
|
|
t.Errorf("expected the fallback page, got: %s", b.String())
|
|
}
|
|
}
|