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) { // Check if signing key is configured if s.sessMgr == nil { s.respondError(w, "signing key not configured", http.StatusServiceUnavailable) return } 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) { if err := r.ParseForm(); 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 if err := s.sessMgr.CreateSession(w); 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) { if s.sessMgr != nil { 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 == nil || !s.sessMgr.IsAuthenticated(r) { http.Redirect(w, r, "/", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { s.renderGenerator(w, &generatorData{Error: "Invalid form data"}) return } // Parse form values sourceURL := r.FormValue("url") widthStr := r.FormValue("width") heightStr := r.FormValue("height") format := r.FormValue("format") qualityStr := r.FormValue("quality") fit := r.FormValue("fit") ttlStr := r.FormValue("ttl") // Validate source URL parsed, err := url.Parse(sourceURL) if err != nil || parsed.Host == "" { s.renderGeneratorWithForm(w, "Invalid source URL", r.Form) return } // Parse dimensions width, _ := strconv.Atoi(widthStr) height, _ := strconv.Atoi(heightStr) quality, _ := strconv.Atoi(qualityStr) ttl, _ := strconv.Atoi(ttlStr) if quality <= 0 { quality = 85 } if ttl <= 0 { ttl = 2592000 // 30 days default } // Create payload expiresAt := time.Now().Add(time.Duration(ttl) * time.Second) payload := &encurl.Payload{ SourceHost: parsed.Host, SourcePath: parsed.Path, SourceQuery: parsed.RawQuery, Width: width, Height: height, Format: imgcache.ImageFormat(format), Quality: quality, FitMode: imgcache.FitMode(fit), ExpiresAt: expiresAt.Unix(), } // 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 } // Build full URL scheme := "https" if s.config.Debug { scheme = "http" } host := r.Host generatedURL := scheme + "://" + host + "/v1/e/" + token s.renderGenerator(w, &generatorData{ GeneratedURL: generatedURL, ExpiresAt: expiresAt.Format(time.RFC3339), FormURL: sourceURL, FormWidth: widthStr, FormHeight: heightStr, FormFormat: format, FormQuality: qualityStr, FormFit: fit, FormTTL: ttlStr, }) } } // 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, } if err := templates.Render(w, "login.html", data); 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{} } if err := templates.Render(w, "generator.html", data); 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"), }) }