More decoy pages

This commit is contained in:
Daniel Redetzke
2026-10-04 20:47:12 +03:00
parent 04a1d1ab85
commit 60426577a5
3 changed files with 60 additions and 4 deletions
+1 -1
View File
@@ -1547,7 +1547,7 @@
} }, ['debug', 'info', 'warn', 'error'].map((l) => h('option', { value: l, selected: s.log.level === l }, l)));
// decoy
const decoyPages = [['nginx', 'nginx welcome page'], ['apache', 'Apache "It works!" page'], ['soon', '"Coming soon" page']];
const decoyPages = [['nginx', 'nginx welcome page'], ['apache', 'Apache "It works!" page'], ['soon', '"Coming soon" page'], ['blank', 'Blank page'], ['forbidden', '"Forbidden" page'], ['private', '"Private server" page']];
const decoyBox = h('input', { type: 'checkbox', id: 'dc', checked: s.decoy.enabled, onChange: async (e) => {
const on = e.target.checked;
if (on) {
+44 -3
View File
@@ -11,15 +11,20 @@ import (
// front page is its stock welcome page and everything else is its stock
// error page. Only /api/v1 and live setup links get past it.
type decoyPage struct {
server string // Server header, "" for none
index func(host string) string // the front page
error func(code int, r *http.Request) string // body for 404 and 405
server string // Server header, "" for none
index func(host string) string // the front page
indexCode int // status of the front page, 0 for 200
error func(code int, r *http.Request) string // body for 404 and 405
}
var decoyPages = map[string]decoyPage{
"nginx": {server: nginxServer, index: func(string) string { return nginxIndex }, error: nginxError},
"apache": {server: apacheServer, index: func(string) string { return apacheIndex }, error: apacheError},
"soon": {index: soonIndex, error: soonError},
// Generic pages that name no server software.
"blank": {index: func(string) string { return "" }, error: func(int, *http.Request) string { return "" }},
"forbidden": {index: func(string) string { return forbiddenIndex }, indexCode: http.StatusForbidden, error: soonError},
"private": {index: func(string) string { return privateIndex }, error: soonError},
}
// serveDecoy writes the decoy's answer for r. It drops the headers the web
@@ -43,6 +48,9 @@ func serveDecoy(w http.ResponseWriter, r *http.Request, name string) {
code, body = http.StatusMethodNotAllowed, d.error(http.StatusMethodNotAllowed, r)
case r.URL.Path == "/" || r.URL.Path == "/index.html":
body = d.index(hostOnly(r.Host))
if d.indexCode != 0 {
code = d.indexCode
}
default:
code, body = http.StatusNotFound, d.error(http.StatusNotFound, r)
}
@@ -537,3 +545,36 @@ const apacheIndex = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//
</body>
</html>
`
const forbiddenIndex = `<!DOCTYPE html>
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body>
</html>
`
const privateIndex = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Private</title>
<style>
html, body { height: 100%; margin: 0; }
body { display: flex; align-items: center; justify-content: center; background: #111; color: #999;
font-family: Georgia, serif; text-align: center; }
h1 { font-size: 28px; font-weight: normal; letter-spacing: 0.04em; color: #fff; margin: 0 0 10px; }
p { margin: 0; font-size: 15px; }
</style>
</head>
<body>
<main>
<h1>Private server</h1>
<p>Nothing to see here.</p>
</main>
</body>
</html>
`
+15
View File
@@ -954,6 +954,21 @@ func TestDecoy(t *testing.T) {
if b, h := get("/", 200); !strings.Contains(b, "<p class=\"host\">127.0.0.1</p>") || h.Get("Server") != "" {
t.Fatalf("soon decoy: %q", b)
}
set(func(c *Config) { c.Decoy.Page = "blank" })
if b, _ := get("/", 200); b != "" {
t.Fatalf("blank decoy: %q", b)
}
if b, _ := get("/app.js", 404); b != "" {
t.Fatalf("blank 404: %q", b)
}
set(func(c *Config) { c.Decoy.Page = "forbidden" })
if b, _ := get("/", 403); !strings.Contains(b, "Forbidden") {
t.Fatalf("forbidden decoy: %q", b)
}
set(func(c *Config) { c.Decoy.Page = "private" })
if b, _ := get("/", 200); !strings.Contains(b, "Private server") {
t.Fatalf("private decoy: %q", b)
}
if err := store.Update(func(c *Config) error { c.Decoy.Page = "iis"; return nil }); err == nil {
t.Fatal("unknown decoy page accepted")
}