mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 09:25:20 +00:00
* Bound public rate-limit buckets against attacker-keyed growth. Keep the NIP-13 PoW sender bypass, skip content-bucket minting on sender reject, and evict idle/oldest entries at a hard cap. Co-authored-by: Cursor <cursoragent@cursor.com> * Stop Cashu-looking text from skipping long-message guards. Oversized public content always collapses and takes the plain formatting path so remote tokens cannot force layout/regex DoS. Co-authored-by: Cursor <cursoragent@cursor.com> * Cap teleported geohash participant markers. Bound the set with FIFO eviction, clear it on channel switch, and prune markers that leave the visible participant list. Co-authored-by: Cursor <cursoragent@cursor.com> * Bound untrusted Nostr relay frames and event tags. Reject oversized inbound messages before JSON parse, cap tag arrays/values at decode, and stop logging raw tag contents. Co-authored-by: Cursor <cursoragent@cursor.com> * Fail soft when Noise handshake state is unexpectedly missing. Replace the initiator startHandshake force unwrap with a guard that throws invalidState instead of crashing. Co-authored-by: Cursor <cursoragent@cursor.com> * Cap geohash nickname cache from remote Nostr events. FIFO-evict at capacity, clear on channel switch, and prune nicknames that leave the visible participant list. Co-authored-by: Cursor <cursoragent@cursor.com> * Avoid overlapping exclusive access in the rate limiter. Make bucket helpers static so inout dictionary updates do not conflict with a mutating call on self. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix rate-limiter tests for mutating allow under #expect. Call allow outside the macro so Swift Testing does not capture an immutable copy of the struct. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop unused WebSocket data helper; reset rate limiter on panic wipe. dataWithinInboundLimit replaced the unbounded path, and panic clear should not leave public intake buckets behind. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.5 KiB
Swift
90 lines
3.5 KiB
Swift
//
|
|
// MessageTextHelpers.swift
|
|
// Shared text parsing helpers for message rendering.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension String {
|
|
// Detect if there is an extremely long token (no whitespace/newlines) that could break layout
|
|
func hasVeryLongToken(threshold: Int) -> Bool {
|
|
var current = 0
|
|
for ch in self {
|
|
if ch.isWhitespace || ch.isNewline {
|
|
if current >= threshold { return true }
|
|
current = 0
|
|
} else {
|
|
current += 1
|
|
if current >= threshold { return true }
|
|
}
|
|
}
|
|
return current >= threshold
|
|
}
|
|
|
|
/// True when the message should collapse behind Show more in the UI.
|
|
/// Length alone decides this — embedding a Cashu-looking token must not
|
|
/// disable the guard (remote DoS via unbounded layout).
|
|
func isLongForDisplay(
|
|
lengthThreshold: Int = TransportConfig.uiLongMessageLengthThreshold,
|
|
tokenThreshold: Int = TransportConfig.uiVeryLongTokenThreshold
|
|
) -> Bool {
|
|
count > lengthThreshold || hasVeryLongToken(threshold: tokenThreshold)
|
|
}
|
|
|
|
/// True when rich formatting (regex / link detectors) should be skipped.
|
|
/// Cashu presence used to exempt oversized content from the plain path;
|
|
/// that let untrusted input force expensive formatting work.
|
|
func isOversizedForRichFormatting(
|
|
lengthThreshold: Int = 4000,
|
|
tokenThreshold: Int = 1024
|
|
) -> Bool {
|
|
count > lengthThreshold || hasVeryLongToken(threshold: tokenThreshold)
|
|
}
|
|
|
|
// Extract up to `max` distinct Cashu tokens (cashuA/cashuB), as the bare
|
|
// bearer strings. Allow dot '.' and shorter lengths. The `cashu:` URI
|
|
// form matches too — the token embedded after the scheme is the match.
|
|
func extractCashuLinks(max: Int = 3) -> [String] {
|
|
let regex = MessageFormattingEngine.Patterns.cashu
|
|
let ns = self as NSString
|
|
let range = NSRange(location: 0, length: ns.length)
|
|
var found: [String] = []
|
|
for m in regex.matches(in: self, range: range) where m.numberOfRanges > 0 {
|
|
let token = ns.substring(with: m.range(at: 0))
|
|
// Dedup: repeated tokens are one bearer instrument (and duplicate
|
|
// ForEach IDs) — one chip is enough.
|
|
if !found.contains(token) {
|
|
found.append(token)
|
|
if found.count >= max { break }
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
// Extract Lightning payloads (scheme, BOLT11, LNURL). Returned as lightning:<payload>
|
|
func extractLightningLinks(max: Int = 3) -> [String] {
|
|
var results: [String] = []
|
|
let ns = self as NSString
|
|
let full = NSRange(location: 0, length: ns.length)
|
|
// lightning: scheme
|
|
for m in MessageFormattingEngine.Patterns.lightningScheme.matches(in: self, range: full) {
|
|
let s = ns.substring(with: m.range(at: 0))
|
|
results.append(s)
|
|
if results.count >= max { return results }
|
|
}
|
|
// BOLT11
|
|
for m in MessageFormattingEngine.Patterns.bolt11.matches(in: self, range: full) {
|
|
let s = ns.substring(with: m.range(at: 0))
|
|
results.append("lightning:\(s)")
|
|
if results.count >= max { return results }
|
|
}
|
|
// LNURL bech32
|
|
for m in MessageFormattingEngine.Patterns.lnurl.matches(in: self, range: full) {
|
|
let s = ns.substring(with: m.range(at: 0))
|
|
results.append("lightning:\(s)")
|
|
if results.count >= max { return results }
|
|
}
|
|
return results
|
|
}
|
|
}
|