v0.324.0: the database password comes back with the data (R-1106, R-127b, R-926); restic 0.19.1 (R-1107); R-1108, R-1065, R-1017; dashboard hardening (R-983, R-976)
gates / gates (push) Successful in 44s
gates / gates (push) Successful in 44s
This commit is contained in:
@@ -397,7 +397,7 @@ func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
s.invalidateAllSessions() // reset: kill old sessions; first-claim: none exist
|
||||
s.endEverySession("claim/reset") // reset: kill old sessions — dashboard, family, gate passes (R-976); first-claim: none exist
|
||||
|
||||
action := "claimed"
|
||||
if wasReset {
|
||||
@@ -428,10 +428,59 @@ func (s *Server) handleClaimRequestNewCode(w http.ResponseWriter, r *http.Reques
|
||||
s.handleClaimPage(w, r, s.msg(r, "claim.msg.invalid_form"), "")
|
||||
return
|
||||
}
|
||||
go s.requestHubResetCode() // fire-and-forget; the neutral response never reveals the outcome
|
||||
// R-983 (v0.324.0): a request reaches the hub only inside the box's own limit. Each call mints a fresh code that
|
||||
// replaces the one the household may be typing, and mails the household — so without a limit a stranger could do
|
||||
// both at will. The answer stays the same neutral page either way (it never revealed the outcome).
|
||||
if ok, why := s.allowResetRequest(rateKey(r)); !ok {
|
||||
s.logger.Printf("[WARN] [web] claim: a reset-code request was not passed to the hub (%s) — visitor %s", why, rateKey(r))
|
||||
} else if s.resetRequestFn != nil {
|
||||
s.resetRequestFn()
|
||||
} else {
|
||||
go s.requestHubResetCode() // fire-and-forget; the neutral response never reveals the outcome
|
||||
}
|
||||
s.handleClaimPage(w, r, "", s.msg(r, "claim.msg.code_sent"))
|
||||
}
|
||||
|
||||
// The reset-code request's limit (R-983): one hub call per visitor per resetReqVisitorEvery, and at most
|
||||
// resetReqGlobalMax for the whole box in any resetReqGlobalWindow. The worst a stranger can do is hold the box's
|
||||
// budget for one window — the household's code already sent stays valid meanwhile, because nothing replaces it.
|
||||
const (
|
||||
resetReqVisitorEvery = time.Minute
|
||||
resetReqGlobalMax = 3
|
||||
resetReqGlobalWindow = 15 * time.Minute
|
||||
)
|
||||
|
||||
// allowResetRequest decides and counts in one step (under claimMu), so parallel requests cannot all pass.
|
||||
func (s *Server) allowResetRequest(visitor string) (bool, string) {
|
||||
s.claimMu.Lock()
|
||||
defer s.claimMu.Unlock()
|
||||
now := s.claimNow()
|
||||
if s.resetReqVisitor == nil {
|
||||
s.resetReqVisitor = map[string]time.Time{}
|
||||
}
|
||||
for k, at := range s.resetReqVisitor {
|
||||
if now.Sub(at) >= resetReqVisitorEvery {
|
||||
delete(s.resetReqVisitor, k)
|
||||
}
|
||||
}
|
||||
keep := s.resetReqGlobal[:0]
|
||||
for _, at := range s.resetReqGlobal {
|
||||
if now.Sub(at) < resetReqGlobalWindow {
|
||||
keep = append(keep, at)
|
||||
}
|
||||
}
|
||||
s.resetReqGlobal = keep
|
||||
if _, seen := s.resetReqVisitor[visitor]; seen {
|
||||
return false, "this visitor asked less than a minute ago"
|
||||
}
|
||||
if len(s.resetReqGlobal) >= resetReqGlobalMax {
|
||||
return false, "the box's limit for this quarter hour is used"
|
||||
}
|
||||
s.resetReqVisitor[visitor] = now
|
||||
s.resetReqGlobal = append(s.resetReqGlobal, now)
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// requestHubResetCode calls POST /api/v1/claim/reset-request with the box's own report key.
|
||||
// v0.123.0 (take-two F-15): a hub ≥0.52.0 returns the freshly rotated code state in the response;
|
||||
// it is applied through the SAME generation-guarded consumer as the report ACK (ClaimSync), so the
|
||||
|
||||
Reference in New Issue
Block a user