From 82228faeba53ab35fd243cc5363853a8ce14150d Mon Sep 17 00:00:00 2001 From: Daniel Redetzke Date: Sun, 4 Oct 2026 20:47:12 +0300 Subject: [PATCH] More web interface pages --- app.js | 2 +- decoy.go | 47 ++++++++++++++++++++++++++++++++++++++++++++--- main_test.go | 15 +++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index d20c908..4972b72 100644 --- a/app.js +++ b/app.js @@ -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) { diff --git a/decoy.go b/decoy.go index e7f7894..828f8c0 100644 --- a/decoy.go +++ b/decoy.go @@ -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 = ` ` + +const forbiddenIndex = ` + +403 Forbidden + +

Forbidden

+

You don't have permission to access this resource.

+ + +` + +const privateIndex = ` + + + + +Private + + + +
+

Private server

+

Nothing to see here.

+
+ + +` diff --git a/main_test.go b/main_test.go index e964f77..a9450ba 100644 --- a/main_test.go +++ b/main_test.go @@ -954,6 +954,21 @@ func TestDecoy(t *testing.T) { if b, h := get("/", 200); !strings.Contains(b, "

127.0.0.1

") || 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") }