Improve installer extraction, login failures, and upload handling

This commit is contained in:
2026-07-27 00:17:30 +08:00
parent 97ea6fb7aa
commit 73555cd04c
14 changed files with 199 additions and 31 deletions
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"strings"
"time"
@@ -32,7 +33,7 @@ type router struct {
publicSnapshots *publicSnapshotService
}
const loginRequestTimeout = 5 * time.Second
const loginRequestTimeout = 8 * time.Second
func NewRouter(cfg *config.Config, store *db.Store, authService *auth.Service, feedbackService *feedback.Service, releaseService *releases.Service, sourceService *sources.Service, legacyService *legacy.Service, optional ...any) http.Handler {
r := &router{
@@ -189,6 +190,11 @@ func (r *router) handleAuthBootstrap(w http.ResponseWriter, req *http.Request) {
}
func (r *router) handleCaptcha(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", errors.New("GET required"))
return
}
w.Header().Set("Cache-Control", "no-store")
captcha, err := r.auth.NewCaptcha()
if err != nil {
writeError(w, http.StatusInternalServerError, "CAPTCHA_FAILED", err)
@@ -219,7 +225,8 @@ func (r *router) handleLogin(w http.ResponseWriter, req *http.Request) {
}
ctx, cancel := context.WithTimeout(req.Context(), loginRequestTimeout)
defer cancel()
sessionID, csrf, ok, err := r.auth.Login(ctx, body.Username, body.Password, body.CaptchaID, body.Captcha, req.RemoteAddr)
clientAddress := remoteHost(req.RemoteAddr)
sessionID, csrf, failure, err := r.auth.LoginDetailed(ctx, body.Username, body.Password, body.CaptchaID, body.Captcha, clientAddress)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
writeError(w, http.StatusGatewayTimeout, "LOGIN_TIMEOUT", errors.New("login verification timed out"))
@@ -228,13 +235,33 @@ func (r *router) handleLogin(w http.ResponseWriter, req *http.Request) {
writeError(w, http.StatusInternalServerError, "LOGIN_FAILED", err)
return
}
if !ok {
writeError(w, http.StatusOK, "LOGIN_FAILED", errors.New("invalid password or captcha"))
if failure != auth.LoginFailureNone {
code := "LOGIN_FAILED"
message := "invalid username or password"
switch failure {
case auth.LoginFailureLocked:
code = "LOGIN_LOCKED"
message = "too many login attempts; try again later"
case auth.LoginFailureCaptcha:
code = "CAPTCHA_INVALID"
message = "captcha is invalid or expired"
case auth.LoginFailureCredentials:
code = "CREDENTIALS_INVALID"
}
writeError(w, http.StatusOK, code, errors.New(message))
return
}
auth.SetSessionCookieForRequest(w, req, sessionID)
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "csrfToken": csrf, "user": map[string]any{"username": body.Username}})
go r.recordLoginAudit(body.Username, req.RemoteAddr, req.UserAgent())
go r.recordLoginAudit(body.Username, clientAddress, req.UserAgent())
}
func remoteHost(remoteAddress string) string {
host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddress))
if err == nil && host != "" {
return host
}
return strings.TrimSpace(remoteAddress)
}
func (r *router) recordLoginAudit(username, remoteAddr, userAgent string) {