summaryrefslogtreecommitdiff
path: root/app/router.go
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 02:24:58 -0500
committerChristian Cleberg <[email protected]>2026-07-15 02:24:58 -0500
commit7eb5e5e2230b6fb5b1bed6f0eaa03279aa61555b (patch)
tree85e30ad9bf89809e3f2179123239d52bda81f63c /app/router.go
parent40d405f318a109aad5e7de7ad71fedcc04bb0e86 (diff)
downloadskunky-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-xapp/router.go45
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)
}