925b00edf5
Animierte GIFs (mehr als 1 Frame) werden jetzt unverändert durchgereicht statt via Decode+Re-Encode auf das erste Frame reduziert. Da die Bytes dabei nicht mehr sanitisiert werden, zwei Guardrails: - gif.DecodeAll läuft über einen io.LimitReader (16 MB) und prüft danach ein Frame-Budget (1000) sowie ein Gesamtpixel-Budget (100 MP) -> bremst den DoS-Vektor (GIF-Bombe mit vielen Frames). copyUpload schreibt die Bytes unter serverseitig erzeugtem Namen und deckelt hart bei 16 MB. - noSniff-Middleware setzt X-Content-Type-Options: nosniff auf /static/*, damit der Browser eine durchgereichte Datei nicht als HTML/JS deutet (Polyglot/Stored-XSS). Standbilder (JPEG/PNG/Einzelbild-GIF) behalten den skalierenden Re-Encode-Pfad und damit die Sanitisierung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
2.9 KiB
Go
97 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/httprate"
|
|
)
|
|
|
|
func main() {
|
|
if err := initDB("kver.db"); err != nil {
|
|
log.Fatalf("db init: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
const addr = ":8080"
|
|
log.Printf("kver listening on %s", addr)
|
|
log.Fatal(http.ListenAndServe(addr, routes()))
|
|
}
|
|
|
|
// routes baut den HTTP-Handler mit allen Endpunkten. In main() und in den
|
|
// Tests identisch verwendet.
|
|
func routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
// --- Auth (öffentlich) ---
|
|
// Login und Registrierung pro IP drosseln (Brute-Force-Bremse). bcrypt bremst
|
|
// zusätzlich, aber das Limit kappt automatisiertes Durchprobieren früh.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(httprate.LimitByIP(20, time.Minute))
|
|
r.Post("/auth/login", handleLogin)
|
|
r.Post("/auth/newuser", handleNewUser)
|
|
})
|
|
r.Get("/auth/logout", handleLogout)
|
|
r.Get("/auth/sessioninfo", handleSessionInfo)
|
|
r.Get("/auth/headerbar", handleHeaderbar)
|
|
|
|
// --- Entries ---
|
|
r.Get("/entry/feed/{page}", handleFeed)
|
|
r.Get("/entry/{pid}/votes", handleVotes)
|
|
r.Get("/entry/{pid}/thread", handleThread)
|
|
r.Get("/e/{pid}", serveEntryPage)
|
|
|
|
// --- User (öffentlich) ---
|
|
r.Get("/u/{username}", serveUserPage)
|
|
r.Get("/u/{username}/info", handleUserPage)
|
|
r.Get("/u/{username}/feed/{page}", handleUserFeed)
|
|
|
|
// --- Geschützt (Session erforderlich) ---
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(requireAuth)
|
|
r.Post("/entry/create", handleCreateEntry)
|
|
r.Post("/entry/{pid}/vote", handleVote)
|
|
r.Post("/entry/{pid}/edit", handleEditEntry)
|
|
r.Post("/entry/{pid}/delete", handleDeleteEntry)
|
|
r.Get("/user/info", handleUserInfo)
|
|
r.Post("/user/rename", handleUserRename)
|
|
r.Post("/user/avatar", handleSetAvatar)
|
|
r.Post("/user/delete", handleUserDelete)
|
|
})
|
|
|
|
// Hochgeladene Medien + alte statische Assets. nosniff verhindert, dass der
|
|
// Browser eine durchgereichte (nicht re-kodierte) Datei als HTML/JS
|
|
// interpretiert -> schließt Polyglot/Stored-XSS über Bild-Uploads.
|
|
r.Handle("/static/*", http.StripPrefix("/static/", noSniff(http.FileServer(http.Dir("static")))))
|
|
|
|
// Neues JS-Frontend
|
|
r.Handle("/*", http.FileServer(http.Dir("web")))
|
|
|
|
return r
|
|
}
|
|
|
|
// noSniff setzt X-Content-Type-Options: nosniff, damit der Browser den
|
|
// deklarierten Content-Type nicht überstimmt (Schutz vor Content-Sniffing).
|
|
func noSniff(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|