package handlers import ( "crypto/subtle" "net/http" "net/url" "strconv" "time" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/templates" ) // HandleRoot serves the login page or generator page based on authentication state. func (s *Handlers) HandleRoot() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { s.handleLoginPost(w, r) return } // Check if authenticated if s.sessMgr.IsAuthenticated(r) { s.renderGenerator(w, nil) return } // Show login page s.renderLogin(w, "") } } // handleLoginPost handles login form submission. func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { s.renderLogin(w, "Invalid form data") return } submittedKey := r.FormValue("key") // Constant-time comparison to prevent timing attacks if subtle.ConstantTimeCompare([]byte(submittedKey), []byte(s.config.SigningKey)) != 1 { s.log.Warn("failed login attempt", "remote_addr", r.RemoteAddr) s.renderLogin(w, "Invalid signing key") return } // Create session err = s.sessMgr.CreateSession(w) if err != nil { s.log.Error("failed to create session", "error", err) s.renderLogin(w, "Failed to create session") return } s.log.Info("successful login", "remote_addr", r.RemoteAddr) // Redirect to generator page http.Redirect(w, r, "/", http.StatusSeeOther) } // HandleLogout clears the session and redirects to login. func (s *Handlers) HandleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { s.sessMgr.ClearSession(w) http.Redirect(w, r, "/", http.StatusSeeOther) } } // HandleGenerateURL handles the URL generation form submission. func (s *Handlers) HandleGenerateURL() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Check authentication if !s.sessMgr.IsAuthenticated(r) { http.Redirect(w, r, "/", http.StatusSeeOther) return } err := r.ParseForm() if err != nil { s.renderGenerator(w, &generatorData{Error: "Invalid form data"}) return } sourceURL := r.FormValue("url") // Validate source URL parsed, err := url.Parse(sourceURL) if err != nil || parsed.Host == "" { s.renderGeneratorWithForm(w, "Invalid source URL", r.Form) return } payload, expiresAt, ttl := buildGeneratePayload(parsed, r.Form) // Generate encrypted token token, err := s.encGen.Generate(payload) if err != nil { s.log.Error("failed to generate encrypted URL", "error", err) s.renderGeneratorWithForm(w, "Failed to generate URL", r.Form) return } generatedURL := s.buildGeneratedURL(r, token, r.FormValue("format")) // Format expiry for display expiresAtStr := "Never" if ttl > 0 { expiresAtStr = expiresAt.Format(time.RFC3339) } s.renderGenerator(w, &generatorData{ GeneratedURL: generatedURL, ExpiresAt: expiresAtStr, FormURL: sourceURL, FormWidth: r.FormValue("width"), FormHeight: r.FormValue("height"), FormFormat: r.FormValue("format"), FormQuality: r.FormValue("quality"), FormFit: r.FormValue("fit"), FormTTL: r.FormValue("ttl"), }) } } // buildGeneratePayload parses the numeric form fields and assembles the // encrypted URL payload. ttl=0 means never expires (ExpiresAt stays 0). func buildGeneratePayload( parsed *url.URL, form url.Values, ) (*encurl.Payload, time.Time, int) { width, _ := strconv.Atoi(form.Get("width")) height, _ := strconv.Atoi(form.Get("height")) quality, _ := strconv.Atoi(form.Get("quality")) ttl, _ := strconv.Atoi(form.Get("ttl")) if quality <= 0 { quality = 85 } var ( expiresAt time.Time expiresAtUnix int64 ) if ttl > 0 { expiresAt = time.Now().Add(time.Duration(ttl) * time.Second) expiresAtUnix = expiresAt.Unix() } payload := &encurl.Payload{ SourceHost: parsed.Host, SourcePath: parsed.Path, SourceQuery: parsed.RawQuery, Width: width, Height: height, Format: imgcache.ImageFormat(form.Get("format")), Quality: quality, FitMode: imgcache.FitMode(form.Get("fit")), ExpiresAt: expiresAtUnix, } return payload, expiresAt, ttl } // generatorData holds template data for the generator page. type generatorData struct { GeneratedURL string ExpiresAt string Error string FormURL string FormWidth string FormHeight string FormFormat string FormQuality string FormFit string FormTTL string } func (s *Handlers) renderLogin(w http.ResponseWriter, errorMsg string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") data := struct { Error string }{ Error: errorMsg, } err := templates.Render(w, "login.html", data) if err != nil { s.log.Error("failed to render login template", "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func (s *Handlers) renderGenerator(w http.ResponseWriter, data *generatorData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if data == nil { data = &generatorData{} } err := templates.Render(w, "generator.html", data) if err != nil { s.log.Error("failed to render generator template", "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func (s *Handlers) renderGeneratorWithForm( w http.ResponseWriter, errorMsg string, form url.Values, ) { s.renderGenerator(w, &generatorData{ Error: errorMsg, FormURL: form.Get("url"), FormWidth: form.Get("width"), FormHeight: form.Get("height"), FormFormat: form.Get("format"), FormQuality: form.Get("quality"), FormFit: form.Get("fit"), FormTTL: form.Get("ttl"), }) } func (s *Handlers) buildGeneratedURL(r *http.Request, token, format string) string { // Build full URL (URL-encode the token for safety) scheme := "https" if s.config.Debug { scheme = "http" } // Determine file extension for the trailing filename ext := format if ext == "" || ext == "orig" { ext = "jpg" // Default extension } return scheme + "://" + r.Host + "/v1/e/" + url.PathEscape(token) + "/img." + ext }