diff --git a/server/internal_api.go b/server/internal_api.go index ff6a7dc..4938778 100644 --- a/server/internal_api.go +++ b/server/internal_api.go @@ -185,7 +185,8 @@ func authorizeInternalAPI(wrt http.ResponseWriter, req *http.Request, capability writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "invalid internal signature", nil) return false } - if !claimInternalNonce(serviceName, nonce, time.Now().Add(window)) { + requestClaim := internalRequestClaimValue(signingText) + if !claimInternalNonce(serviceName, nonce, requestClaim, time.Now().Add(window)) { writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "internal nonce replayed", nil) return false } @@ -572,23 +573,36 @@ func verifyInternalSignature(secret, signingText, signature string) bool { return false } -func claimInternalNonce(serviceName, nonce string, expiresAt time.Time) bool { +func claimInternalNonce(serviceName, nonce, requestClaim string, expiresAt time.Time) bool { ttl := time.Until(expiresAt) - if ok, enabled, err := store.ClaimShortState("internal_api_nonce", fmt.Sprintf("%s:%s", serviceName, nonce), ttl); enabled { + nonceKey := fmt.Sprintf("%s:%s", serviceName, nonce) + if claimed, sameValue, enabled, err := store.ClaimShortStateValue("internal_api_nonce", nonceKey, requestClaim, ttl); enabled { if err != nil { logs.Warn.Println("internal api: failed to claim nonce in redis", err) return false } - return ok + return claimed || sameValue } now := time.Now() internalAPI.nonces.Range(func(key, value any) bool { - if expires, ok := value.(time.Time); ok && expires.Before(now) { + if claim, ok := value.(internalNonceClaim); ok && claim.expiresAt.Before(now) { internalAPI.nonces.Delete(key) } return true }) - nonceKey := fmt.Sprintf("%s:%s", serviceName, nonce) - _, loaded := internalAPI.nonces.LoadOrStore(nonceKey, expiresAt) - return !loaded + if existing, loaded := internalAPI.nonces.LoadOrStore(nonceKey, internalNonceClaim{value: requestClaim, expiresAt: expiresAt}); loaded { + claim, ok := existing.(internalNonceClaim) + return ok && claim.value == requestClaim + } + return true +} + +type internalNonceClaim struct { + value string + expiresAt time.Time +} + +func internalRequestClaimValue(signingText string) string { + sum := sha256.Sum256([]byte(signingText)) + return hex.EncodeToString(sum[:]) } diff --git a/server/store/group_message_runtime.go b/server/store/group_message_runtime.go index 4e8c1eb..ad1a4e3 100644 --- a/server/store/group_message_runtime.go +++ b/server/store/group_message_runtime.go @@ -70,6 +70,7 @@ type groupMessageRuntime interface { CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error) ClaimShortState(namespace, id string, ttl time.Duration) (bool, error) + ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool, bool, error) } var groupMsgRuntime groupMessageRuntime @@ -97,6 +98,14 @@ func ClaimShortState(namespace, id string, ttl time.Duration) (bool, bool, error return ok, true, err } +func ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool, bool, bool, error) { + if groupMsgRuntime == nil || namespace == "" || id == "" || value == "" { + return false, false, false, nil + } + claimed, sameValue, err := groupMsgRuntime.ClaimShortStateValue(namespace, id, value, ttl) + return claimed, sameValue, true, err +} + func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error { if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" { return nil @@ -753,6 +762,23 @@ func (r *redisGroupMessageRuntime) ClaimShortState(namespace, id string, ttl tim return r.client.SetNX(context.Background(), r.shortStateKey(namespace, id), "1", ttl).Result() } +func (r *redisGroupMessageRuntime) ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool, bool, error) { + if ttl <= 0 { + ttl = time.Duration(r.cfg.ShortTTL) * time.Second + } + key := r.shortStateKey(namespace, id) + ctx := context.Background() + claimed, err := r.client.SetNX(ctx, key, value, ttl).Result() + if err != nil || claimed { + return claimed, false, err + } + existing, err := r.client.Get(ctx, key).Result() + if err != nil { + return false, false, err + } + return false, existing == value, nil +} + func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic string, opts *types.QueryOpt, limit int) ([]int64, bool, error) { if opts != nil && len(opts.IdRanges) > 0 { seqs := expandRanges(opts.IdRanges, limit)