diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6583019 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +uploads +podstalk.db +podstalk diff --git a/handler/import.go b/handler/import.go index 2b3cd83..0fc4fb2 100644 --- a/handler/import.go +++ b/handler/import.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/xml" "fmt" "io" @@ -126,8 +127,20 @@ func (h *ImportHandler) ServeImport(w http.ResponseWriter, r *http.Request) { }) } +var rssHTTPClient = &http.Client{ + Timeout: 30 * time.Second, +} + func (h *ImportHandler) fetchRSS(rssURL string) ([]ImportEpisodeView, error) { - resp, err := http.Get(rssURL) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rssURL, nil) + if err != nil { + return nil, err + } + + resp, err := rssHTTPClient.Do(req) if err != nil { return nil, err } @@ -180,7 +193,16 @@ func (h *ImportHandler) importSelected(w http.ResponseWriter, r *http.Request, r } } - resp, err := http.Get(rssURL) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rssURL, nil) + if err != nil { + http.Redirect(w, r, "/import?error=failed+to+fetch+rss", http.StatusSeeOther) + return + } + + resp, err := rssHTTPClient.Do(req) if err != nil { http.Redirect(w, r, "/import?error=failed+to+fetch+rss", http.StatusSeeOther) return @@ -255,8 +277,20 @@ func (h *ImportHandler) episodeImageURL(item *importItem, channel *importChannel return "" } +var importHTTPClient = &http.Client{ + Timeout: 5 * time.Minute, +} + func (h *ImportHandler) downloadFile(url, category string) (string, error) { - resp, err := http.Get(url) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + + resp, err := importHTTPClient.Do(req) if err != nil { return "", err } diff --git a/handler/template.go b/handler/template.go index 51bc45e..e03e3fe 100644 --- a/handler/template.go +++ b/handler/template.go @@ -1,9 +1,10 @@ package handler import ( + "bytes" "html/template" - "io" "log" + "net/http" ) type Templates struct { @@ -13,8 +14,12 @@ type Templates struct { func NewTemplates(tmpl *template.Template) *Templates { return &Templates{tmpl: tmpl} } -func (t *Templates) Render(w io.Writer, name string, data any) { - if err := t.tmpl.ExecuteTemplate(w, name+".html", data); err != nil { +func (t *Templates) Render(w http.ResponseWriter, name string, data any) { + var buf bytes.Buffer + if err := t.tmpl.ExecuteTemplate(&buf, name+".html", data); err != nil { log.Printf("template error: %s: %v", name, err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return } + w.Write(buf.Bytes()) } diff --git a/main.go b/main.go index b98d512..f50f9d2 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "runtime/debug" "golang.org/x/crypto/bcrypt" @@ -83,13 +84,13 @@ func main() { http.HandleFunc("/signin", authMid.RedirectIfAuthed(authH.ServeSignin)) http.HandleFunc("/signout", authH.ServeSignout) - // Protected routes. - http.HandleFunc("/", authMid.RequireAuth(epH.ServeDashboard)) - http.HandleFunc("/episodes/new", authMid.RequireAuth(epH.ServeNew)) - http.HandleFunc("/episodes/edit", authMid.RequireAuth(epH.ServeEdit)) - http.HandleFunc("/episodes/delete", authMid.RequireAuth(epH.ServeDelete)) - http.HandleFunc("/settings", authMid.RequireAuth(setH.ServeSettings)) - http.HandleFunc("/import", authMid.RequireAuth(importH.ServeImport)) + // Protected routes with panic recovery. + http.HandleFunc("/", recovery(authMid.RequireAuth(epH.ServeDashboard))) + http.HandleFunc("/episodes/new", recovery(authMid.RequireAuth(epH.ServeNew))) + http.HandleFunc("/episodes/edit", recovery(authMid.RequireAuth(epH.ServeEdit))) + http.HandleFunc("/episodes/delete", recovery(authMid.RequireAuth(epH.ServeDelete))) + http.HandleFunc("/settings", recovery(authMid.RequireAuth(setH.ServeSettings))) + http.HandleFunc("/import", recovery(authMid.RequireAuth(importH.ServeImport))) // Public RSS: /rss/my-slug or /rss?slug=my-slug or /rss?user=N (legacy) http.HandleFunc("/rss", rssH.ServeRSS) @@ -106,6 +107,18 @@ func main() { log.Fatal(http.ListenAndServe(addr, nil)) } +func recovery(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("panic: %v\n%s", err, debug.Stack()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + }() + next(w, r) + } +} + func runAddUser() { if len(os.Args) != 4 { log.Fatalf("Usage: podstalk adduser ") diff --git a/podstalk b/podstalk deleted file mode 100755 index 0e05a1a..0000000 Binary files a/podstalk and /dev/null differ diff --git a/podstalk.db b/podstalk.db deleted file mode 100644 index a99c55e..0000000 Binary files a/podstalk.db and /dev/null differ