From 7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 15 Jul 2026 02:24:58 -0500 Subject: fix: harden HTTP transport, server timeouts and panic paths Correctness and security findings surfaced by golangci-lint, plus two latent panics found alongside them. - router: http.ListenAndServe has no timeouts at all (gosec G114), so a slow client could hold a connection and its handler open indefinitely. Replace it with an explicit http.Server carrying read/write/idle timeouts. - httpclient: InstallDAThrottle asserted http.DefaultTransport was a *http.Transport and would panic outright if anything had already wrapped it -- which is precisely what that function does. Check the assertion and fall back to a fresh transport. Expose ProxiedTransport so a configured download-proxy can inherit the same throttle and timeouts instead of silently bypassing them. - cache: the Sys() assertion to *syscall.Stat_t is only valid on unix and would panic elsewhere; skip rotation instead. Indexing Headers["Content-Type"][0] panics when the header is absent; use Headers.Get. Cache files are written 0600 rather than 0700, as they are never executed. - cli, api: check error returns, and exit rather than nil-dereference a file handle that failed to open. SHA-1 and math/rand keep //nolint:gosec with reasons: they are cache-key hashes and random-artwork picks, not security primitives. --- app/router.go | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) (limited to 'app/router.go') diff --git a/app/router.go b/app/router.go index 13e8415..e29005d 100755 --- a/app/router.go +++ b/app/router.go @@ -7,10 +7,15 @@ import ( "skunkyart/static" "strconv" "strings" + "time" ) +// Host is the scheme and host that generated links are built from. It is set per +// request from the Host header and X-Forwarded-Proto. var Host string +// Router registers the single catch-all handler that dispatches every path, then +// serves until the process exits. It does not return on success. func Router() { parsepath := func(path string) map[int]string { if l := len(CFG.URI); len(path) > l { @@ -33,22 +38,30 @@ func Router() { return parsedpath } - next := func(path map[int]string, from int) (out string) { + next := func(path map[int]string, from int) string { + var out strings.Builder for x, l := from, len(path)-1; x <= l; x++ { - out += path[x] + out.WriteString(path[x]) if x != l { - out += "/" + out.WriteString("/") } } - return + return out.String() } open := func(name string) []byte { file, err := static.Templates.Open(name) - try(err) - fileReaded, err := io.ReadAll(file) - try(err) + if err != nil { + try(err) + return nil + } + defer func() { try(file.Close()) }() + fileReaded, err := io.ReadAll(file) + if err != nil { + try(err) + return nil + } return fileReaded } @@ -119,10 +132,10 @@ func Router() { skunky.Emojitar(path[3]) } case "stylesheet": - w.Header().Add("content-type", "text/css") - w.Write(open("css/skunky.css")) + w.Header().Add("Content-Type", "text/css") + _, _ = w.Write(open("css/skunky.css")) case "favicon.ico": - w.Write(open("images/logo.png")) + _, _ = w.Write(open("images/logo.png")) // API case "api": @@ -145,5 +158,15 @@ func Router() { http.HandleFunc("/", handle) println("SkunkyArt is listening on", CFG.Listen) - tryWithExitStatus(http.ListenAndServe(CFG.Listen, nil), 1) + // Explicit timeouts: the bare http.ListenAndServe has none, so a slow client + // can hold a connection (and its handler) open indefinitely. WriteTimeout is + // generous because media proxying streams large files through a handler. + srv := &http.Server{ + Addr: CFG.Listen, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 120 * time.Second, + } + tryWithExitStatus(srv.ListenAndServe(), 1) } -- cgit v1.2.3