Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ func NewHandler(opts *Options) (http.Handler, error) {
ServeMux: http.NewServeMux(),
}
s.Handle("/favicon.ico", http.FileServer(http.FS(fs)))
s.HandleFunc("/", logged(s.flood()))
// {$} matches the root path and nothing else. A plain "/" pattern is a
// catch-all, which meant every scanner probing /wp-login.php and the like
// ran the full handler: fetching the road alert feed, and potentially
// spending a Gemini call. Those requests now get a 404 from the mux.
s.HandleFunc("/{$}", logged(s.flood()))

return s, nil
}
Expand Down
74 changes: 74 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -169,6 +170,79 @@ func TestOpen(t *testing.T) {
}
}

// countingHandler records how many requests reached the wrapped handler.
type countingHandler struct {
h http.Handler
n atomic.Int64
}

func (c *countingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.n.Add(1)
c.h.ServeHTTP(w, r)
}

// Only the root path is served. Anything else -- overwhelmingly vulnerability
// scanners probing for PHP and WordPress -- must 404 from the mux without
// reaching the handler, because the handler fetches the road alert feed and
// may spend a Gemini call.
func TestNonRootPathsAreNotServed(t *testing.T) {
feed := &countingHandler{h: newFeedGenerator(t, nil)}
feedURL := startTestServer(t, feed)
h, err := NewHandler(&Options{FeedURL: feedURL, Road: "124th"})
if err != nil {
t.Fatalf("NewHandler failed: %v", err)
}
server := startTestServer(t, h)

probes := []string{
"/wp-login.php",
"/admin/config.php",
"/666.php",
"/wp-content/plugins/core-plugin/include.php",
"/backup/",
"/.well-known/acme-challenge/index.php",
"/some/deep/nested/path",
}
for _, p := range probes {
resp, err := http.Get(server + p)
if err != nil {
t.Fatalf("http.Get(%s) failed: %v", p, err)
}
resp.Body.Close()
if sc := resp.StatusCode; sc != http.StatusNotFound {
t.Errorf("GET %s returned %d, want 404", p, sc)
}
}

// The point of the 404 is avoiding work, not just the status code.
if n := feed.n.Load(); n != 0 {
t.Errorf("scanner probes fetched the road alert feed %d times, want 0", n)
}
}

// The root path must still be served, and it does fetch the feed.
func TestRootIsStillServed(t *testing.T) {
feed := &countingHandler{h: newFeedGenerator(t, nil)}
feedURL := startTestServer(t, feed)
h, err := NewHandler(&Options{FeedURL: feedURL, Road: "124th"})
if err != nil {
t.Fatalf("NewHandler failed: %v", err)
}
server := startTestServer(t, h)

resp, err := http.Get(server + "/")
if err != nil {
t.Fatalf("http.Get(/) failed: %v", err)
}
defer resp.Body.Close()
if sc := resp.StatusCode; sc != http.StatusOK {
t.Fatalf("GET / returned %d, want 200", sc)
}
if n := feed.n.Load(); n != 1 {
t.Errorf("GET / fetched the road alert feed %d times, want 1", n)
}
}

func TestFavicon(t *testing.T) {
h, err := NewHandler(&Options{})
if err != nil {
Expand Down
Loading