diff options
| author | Christian Cleberg <[email protected]> | 2026-07-15 02:24:58 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-15 02:24:58 -0500 |
| commit | 7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b (patch) | |
| tree | 85e30ad9bf89809e3f2179123239d52bda81f63c /app/router.go | |
| parent | 40d405f318a109aad5e7de7ad71fedcc04bb0e86 (diff) | |
| download | skunky-art-7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b.tar.gz skunky-art-7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b.tar.bz2 skunky-art-7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b.zip | |
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.
Diffstat (limited to 'app/router.go')
| -rwxr-xr-x | app/router.go | 45 |
1 files changed, 34 insertions, 11 deletions
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) } |
