More web interface pages

This commit is contained in:
Daniel Redetzke
2026-10-04 20:47:12 +03:00
parent 6afef85b2b
commit 82228faeba
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))); } }, ['debug', 'info', 'warn', 'error'].map((l) => h('option', { value: l, selected: s.log.level === l }, l)));
// decoy // 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 decoyBox = h('input', { type: 'checkbox', id: 'dc', checked: s.decoy.enabled, onChange: async (e) => {
const on = e.target.checked; const on = e.target.checked;
if (on) { if (on) {
+41
View File
@@ -13,6 +13,7 @@ import (
type decoyPage struct { type decoyPage struct {
server string // Server header, "" for none server string // Server header, "" for none
index func(host string) string // the front page 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 error func(code int, r *http.Request) string // body for 404 and 405
} }
@@ -20,6 +21,10 @@ var decoyPages = map[string]decoyPage{
"nginx": {server: nginxServer, index: func(string) string { return nginxIndex }, error: nginxError}, "nginx": {server: nginxServer, index: func(string) string { return nginxIndex }, error: nginxError},
"apache": {server: apacheServer, index: func(string) string { return apacheIndex }, error: apacheError}, "apache": {server: apacheServer, index: func(string) string { return apacheIndex }, error: apacheError},
"soon": {index: soonIndex, error: soonError}, "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 // 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) code, body = http.StatusMethodNotAllowed, d.error(http.StatusMethodNotAllowed, r)
case r.URL.Path == "/" || r.URL.Path == "/index.html": case r.URL.Path == "/" || r.URL.Path == "/index.html":
body = d.index(hostOnly(r.Host)) body = d.index(hostOnly(r.Host))
if d.indexCode != 0 {
code = d.indexCode
}
default: default:
code, body = http.StatusNotFound, d.error(http.StatusNotFound, r) 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> </body>
</html> </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") != "" { 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) 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 { if err := store.Update(func(c *Config) error { c.Decoy.Page = "iis"; return nil }); err == nil {
t.Fatal("unknown decoy page accepted") t.Fatal("unknown decoy page accepted")
} }