Add trailing filename to encrypted URLs for better browser compatibility. The filename is ignored by the server but helps browsers identify content type.
240 lines
5.8 KiB
Go
240 lines
5.8 KiB
Go
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) {
|
|
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) {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Create payload
|
|
// ttl=0 means never expires
|
|
var expiresAt time.Time
|
|
var expiresAtUnix int64
|
|
|
|
if ttl > 0 {
|
|
expiresAt = time.Now().Add(time.Duration(ttl) * time.Second)
|
|
expiresAtUnix = expiresAt.Unix()
|
|
}
|
|
// else expiresAtUnix stays 0 (never expires)
|
|
|
|
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: expiresAtUnix,
|
|
}
|
|
|
|
// 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 (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
|
|
}
|
|
|
|
host := r.Host
|
|
generatedURL := scheme + "://" + host + "/v1/e/" + url.PathEscape(token) + "/img." + ext
|
|
|
|
// 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: 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"),
|
|
})
|
|
}
|