v0.165.0: Indítópult megosztása — guest launcher via capability URL (+ optional password, QR)
Mint a 160-bit capability URL (/s/<token>) serving a standalone read-only guest launcher: same tiles, opens apps in new tabs, no account, no admin session. Information only, zero control — every privilege stays behind each app's own auth. - /s/ pre-auth pass-through (after the claim gate) + session-CSRF exemption; guest password POST carries its own pre-auth HMAC CSRF. - Constant-time token match; empty stored token = disabled = byte-identical mux 404. - Optional per-share password: separate bcrypt hash + own attempt map; signed cookie = HMAC(token|passwordHash) keyed with web.session_secret, so rotate/change invalidates. - Guest labels ride the v0.164.0 ruling; never expose internal state vocabulary. - Token redacted in logs (/s/<redacted>); never in CHANGELOG/REPORT/CONTEXT. - Admin modal: copy-link, QR (go-qrcode), set/clear password, rotate, disable. - Tests: Groups A-G (14) + 3 red-proofs verified red.
This commit is contained in:
@@ -50,6 +50,12 @@ type Server struct {
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
|
||||
// Guest launcher share (v0.165.0): its OWN per-IP brute-force limiter for the optional share
|
||||
// password gate — deliberately separate from loginAttempts (the admin login), so a guest and the
|
||||
// owner never share a counter. Lazily initialized (struct-literal test servers skip NewServer).
|
||||
shareAttempts map[string]*loginAttempt
|
||||
shareAttemptMu sync.Mutex
|
||||
|
||||
// Customer-claim arc (v0.122.0, F-4): the claim/reset code brute-force limiter. Per-source
|
||||
// (IP) + a global counter; both must be clear. claimClock is the test clock seam (nil → time.Now).
|
||||
claimMu sync.Mutex
|
||||
@@ -179,6 +185,7 @@ func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *syste
|
||||
version: version,
|
||||
sessions: make(map[string]*session),
|
||||
loginAttempts: make(map[string]*loginAttempt),
|
||||
shareAttempts: make(map[string]*loginAttempt),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
s.classifyFSPath = system.ClassifyPathFSTimeout
|
||||
@@ -331,7 +338,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] ServeHTTP: %s %s from %s", r.Method, path, r.RemoteAddr)
|
||||
// The guest launcher share token (v0.165.0) is a secret — redact it from the request log so
|
||||
// debug logging never leaks a live capability URL (Scenario G). Method + IP stay intact.
|
||||
logPath := path
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
logPath = "/s/<redacted>"
|
||||
}
|
||||
s.logger.Printf("[DEBUG] [web] ServeHTTP: %s %s from %s", r.Method, logPath, r.RemoteAddr)
|
||||
}
|
||||
|
||||
switch {
|
||||
@@ -347,6 +360,25 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.dashboardHandler(w, r)
|
||||
case path == "/launcher":
|
||||
s.launcherHandler(w, r)
|
||||
// Guest launcher share (v0.165.0). /s/<token> is the pre-auth capability URL (RequireAuth lets
|
||||
// the /s/ prefix through after the claim gate). A GET renders the guest launcher (or the password
|
||||
// gate); a POST submits the optional share password. An unknown/disabled token falls through to a
|
||||
// byte-identical 404 (share404), so nothing distinguishes a wrong token from an unknown route.
|
||||
case strings.HasPrefix(path, "/s/") && r.Method == http.MethodGet:
|
||||
s.shareGuestHandler(w, r)
|
||||
case strings.HasPrefix(path, "/s/") && r.Method == http.MethodPost:
|
||||
s.shareGuestPasswordHandler(w, r)
|
||||
// Admin share management (session-authed via RequireAuth + session CSRF via CsrfProtect).
|
||||
case path == "/launcher/share/qr.png" && r.Method == http.MethodGet:
|
||||
s.launcherShareQRHandler(w, r)
|
||||
case path == "/launcher/share/enable" && r.Method == http.MethodPost:
|
||||
s.launcherShareEnableHandler(w, r)
|
||||
case path == "/launcher/share/rotate" && r.Method == http.MethodPost:
|
||||
s.launcherShareRotateHandler(w, r)
|
||||
case path == "/launcher/share/disable" && r.Method == http.MethodPost:
|
||||
s.launcherShareDisableHandler(w, r)
|
||||
case path == "/launcher/share/password" && r.Method == http.MethodPost:
|
||||
s.launcherSharePasswordHandler(w, r)
|
||||
case path == "/stacks":
|
||||
s.stacksHandler(w, r)
|
||||
case path == "/backups":
|
||||
|
||||
Reference in New Issue
Block a user