mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be722aa170 |
@@ -51,8 +51,6 @@ class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
private var messageQueue: [PendingSend] = []
|
private var messageQueue: [PendingSend] = []
|
||||||
private let messageQueueLock = NSLock()
|
private let messageQueueLock = NSLock()
|
||||||
private let encoder = JSONEncoder()
|
|
||||||
private let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
// Exponential backoff configuration
|
// Exponential backoff configuration
|
||||||
private let initialBackoffInterval: TimeInterval = TransportConfig.nostrRelayInitialBackoffSeconds
|
private let initialBackoffInterval: TimeInterval = TransportConfig.nostrRelayInitialBackoffSeconds
|
||||||
@@ -66,8 +64,6 @@ class NostrRelayManager: ObservableObject {
|
|||||||
init() {
|
init() {
|
||||||
// Initialize with default relays
|
// Initialize with default relays
|
||||||
self.relays = Self.defaultRelays.map { Relay(url: $0) }
|
self.relays = Self.defaultRelays.map { Relay(url: $0) }
|
||||||
// Deterministic JSON shape for outbound requests
|
|
||||||
self.encoder.outputFormatting = .sortedKeys
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to all configured relays
|
/// Connect to all configured relays
|
||||||
@@ -174,6 +170,8 @@ class NostrRelayManager: ObservableObject {
|
|||||||
let req = NostrRequest.subscribe(id: id, filters: [filter])
|
let req = NostrRequest.subscribe(id: id, filters: [filter])
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = .sortedKeys // For consistent output
|
||||||
let message = try encoder.encode(req)
|
let message = try encoder.encode(req)
|
||||||
guard let messageString = String(data: message, encoding: .utf8) else {
|
guard let messageString = String(data: message, encoding: .utf8) else {
|
||||||
SecureLogger.log("❌ Failed to encode subscription request", category: SecureLogger.session, level: .error)
|
SecureLogger.log("❌ Failed to encode subscription request", category: SecureLogger.session, level: .error)
|
||||||
@@ -223,7 +221,7 @@ class NostrRelayManager: ObservableObject {
|
|||||||
messageHandlers.removeValue(forKey: id)
|
messageHandlers.removeValue(forKey: id)
|
||||||
|
|
||||||
let req = NostrRequest.close(id: id)
|
let req = NostrRequest.close(id: id)
|
||||||
let message = try? encoder.encode(req)
|
let message = try? JSONEncoder().encode(req)
|
||||||
|
|
||||||
guard let messageData = message,
|
guard let messageData = message,
|
||||||
let messageString = String(data: messageData, encoding: .utf8) else { return }
|
let messageString = String(data: messageData, encoding: .utf8) else { return }
|
||||||
@@ -290,20 +288,13 @@ class NostrRelayManager: ObservableObject {
|
|||||||
case .success(let message):
|
case .success(let message):
|
||||||
switch message {
|
switch message {
|
||||||
case .string(let text):
|
case .string(let text):
|
||||||
// Parse off-main to reduce UI jank, then hop back for state updates
|
Task { @MainActor in
|
||||||
Task.detached(priority: .utility) {
|
self.handleMessage(text, from: relayUrl)
|
||||||
guard let parsed = parseInboundMessage(text) else { return }
|
|
||||||
await MainActor.run {
|
|
||||||
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .data(let data):
|
case .data(let data):
|
||||||
if let text = String(data: data, encoding: .utf8) {
|
if let text = String(data: data, encoding: .utf8) {
|
||||||
Task.detached(priority: .utility) {
|
Task { @MainActor in
|
||||||
guard let parsed = parseInboundMessage(text) else { return }
|
self.handleMessage(text, from: relayUrl)
|
||||||
await MainActor.run {
|
|
||||||
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@unknown default:
|
@unknown default:
|
||||||
@@ -323,31 +314,57 @@ class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsed inbound message type (off-main)
|
private func handleMessage(_ message: String, from relayUrl: String) {
|
||||||
// Note: declared at file scope below to avoid MainActor isolation inside this class
|
guard let data = message.data(using: .utf8) else { return }
|
||||||
// and keep parsing off the main actor.
|
|
||||||
|
|
||||||
// Handle parsed message on MainActor (state updates and handlers)
|
do {
|
||||||
private func handleParsedMessage(_ parsed: ParsedInbound, from relayUrl: String) {
|
// Try to decode as an array first
|
||||||
switch parsed {
|
if let array = try JSONSerialization.jsonObject(with: data) as? [Any],
|
||||||
case .event(let subId, let event):
|
array.count >= 2,
|
||||||
|
let type = array[0] as? String {
|
||||||
|
|
||||||
|
// Received message from relay
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case "EVENT":
|
||||||
|
if array.count >= 3,
|
||||||
|
let subId = array[1] as? String,
|
||||||
|
let eventDict = array[2] as? [String: Any] {
|
||||||
|
|
||||||
|
let event = try NostrEvent(from: eventDict)
|
||||||
|
|
||||||
|
// Only log non-gift-wrap events to reduce noise
|
||||||
if event.kind != 1059 {
|
if event.kind != 1059 {
|
||||||
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// Update relay stats
|
||||||
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
|
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
|
||||||
self.relays[index].messagesReceived += 1
|
self.relays[index].messagesReceived += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call handler
|
||||||
if let handler = self.messageHandlers[subId] {
|
if let handler = self.messageHandlers[subId] {
|
||||||
handler(event)
|
handler(event)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ No handler for subscription \(subId)",
|
SecureLogger.log("⚠️ No handler for subscription \(subId)",
|
||||||
category: SecureLogger.session, level: .warning)
|
category: SecureLogger.session, level: .warning)
|
||||||
}
|
}
|
||||||
case .eose:
|
}
|
||||||
// No-op for now
|
}
|
||||||
break
|
|
||||||
case .ok(let eventId, let success, let reason):
|
case "EOSE":
|
||||||
|
if array.count >= 2 {
|
||||||
|
// End of stored events
|
||||||
|
}
|
||||||
|
|
||||||
|
case "OK":
|
||||||
|
if array.count >= 3,
|
||||||
|
let eventId = array[1] as? String,
|
||||||
|
let success = array[2] as? Bool {
|
||||||
|
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
||||||
if success {
|
if success {
|
||||||
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
||||||
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
|
||||||
@@ -357,8 +374,19 @@ class NostrRelayManager: ObservableObject {
|
|||||||
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
|
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
|
||||||
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
|
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
|
||||||
}
|
}
|
||||||
case .notice:
|
}
|
||||||
break
|
|
||||||
|
case "NOTICE":
|
||||||
|
if array.count >= 2 {
|
||||||
|
// Server notice received
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break // Unknown message type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
SecureLogger.log("Failed to parse Nostr message: \(error)", category: SecureLogger.session, level: .error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +394,8 @@ class NostrRelayManager: ObservableObject {
|
|||||||
let req = NostrRequest.event(event)
|
let req = NostrRequest.event(event)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = .sortedKeys
|
||||||
let data = try encoder.encode(req)
|
let data = try encoder.encode(req)
|
||||||
let message = String(data: data, encoding: .utf8) ?? ""
|
let message = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
|
||||||
@@ -514,51 +544,6 @@ class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Off-main inbound parsing helpers (file scope, non-isolated)
|
|
||||||
|
|
||||||
private enum ParsedInbound {
|
|
||||||
case event(subId: String, event: NostrEvent)
|
|
||||||
case ok(eventId: String, success: Bool, reason: String)
|
|
||||||
case eose(subscriptionId: String)
|
|
||||||
case notice(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Off-main JSON parse to avoid UI jank; pure function, not actor-isolated
|
|
||||||
private func parseInboundMessage(_ message: String) -> ParsedInbound? {
|
|
||||||
guard let data = message.data(using: .utf8) else { return nil }
|
|
||||||
do {
|
|
||||||
if let array = try JSONSerialization.jsonObject(with: data) as? [Any],
|
|
||||||
array.count >= 2,
|
|
||||||
let type = array[0] as? String {
|
|
||||||
switch type {
|
|
||||||
case "EVENT":
|
|
||||||
if array.count >= 3,
|
|
||||||
let subId = array[1] as? String,
|
|
||||||
let eventDict = array[2] as? [String: Any] {
|
|
||||||
let event = try NostrEvent(from: eventDict)
|
|
||||||
return .event(subId: subId, event: event)
|
|
||||||
}
|
|
||||||
case "EOSE":
|
|
||||||
if let subId = array[1] as? String { return .eose(subscriptionId: subId) }
|
|
||||||
case "OK":
|
|
||||||
if array.count >= 3,
|
|
||||||
let eventId = array[1] as? String,
|
|
||||||
let success = array[2] as? Bool {
|
|
||||||
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
|
||||||
return .ok(eventId: eventId, success: success, reason: reason)
|
|
||||||
}
|
|
||||||
case "NOTICE":
|
|
||||||
if array.count >= 2, let msg = array[1] as? String { return .notice(msg) }
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Nostr Protocol Types
|
// MARK: - Nostr Protocol Types
|
||||||
|
|
||||||
enum NostrRequest: Encodable {
|
enum NostrRequest: Encodable {
|
||||||
|
|||||||
@@ -40,23 +40,23 @@ extension Data {
|
|||||||
extension Data {
|
extension Data {
|
||||||
// MARK: Writing
|
// MARK: Writing
|
||||||
|
|
||||||
@inlinable mutating func appendUInt8(_ value: UInt8) {
|
mutating func appendUInt8(_ value: UInt8) {
|
||||||
self.append(value)
|
self.append(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable mutating func appendUInt16(_ value: UInt16) {
|
mutating func appendUInt16(_ value: UInt16) {
|
||||||
self.append(UInt8((value >> 8) & 0xFF))
|
self.append(UInt8((value >> 8) & 0xFF))
|
||||||
self.append(UInt8(value & 0xFF))
|
self.append(UInt8(value & 0xFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable mutating func appendUInt32(_ value: UInt32) {
|
mutating func appendUInt32(_ value: UInt32) {
|
||||||
self.append(UInt8((value >> 24) & 0xFF))
|
self.append(UInt8((value >> 24) & 0xFF))
|
||||||
self.append(UInt8((value >> 16) & 0xFF))
|
self.append(UInt8((value >> 16) & 0xFF))
|
||||||
self.append(UInt8((value >> 8) & 0xFF))
|
self.append(UInt8((value >> 8) & 0xFF))
|
||||||
self.append(UInt8(value & 0xFF))
|
self.append(UInt8(value & 0xFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable mutating func appendUInt64(_ value: UInt64) {
|
mutating func appendUInt64(_ value: UInt64) {
|
||||||
for i in (0..<8).reversed() {
|
for i in (0..<8).reversed() {
|
||||||
self.append(UInt8((value >> (i * 8)) & 0xFF))
|
self.append(UInt8((value >> (i * 8)) & 0xFF))
|
||||||
}
|
}
|
||||||
@@ -113,21 +113,21 @@ extension Data {
|
|||||||
|
|
||||||
// MARK: Reading
|
// MARK: Reading
|
||||||
|
|
||||||
@inlinable func readUInt8(at offset: inout Int) -> UInt8? {
|
func readUInt8(at offset: inout Int) -> UInt8? {
|
||||||
guard offset >= 0 && offset < self.count else { return nil }
|
guard offset >= 0 && offset < self.count else { return nil }
|
||||||
let value = self[offset]
|
let value = self[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable func readUInt16(at offset: inout Int) -> UInt16? {
|
func readUInt16(at offset: inout Int) -> UInt16? {
|
||||||
guard offset + 2 <= self.count else { return nil }
|
guard offset + 2 <= self.count else { return nil }
|
||||||
let value = UInt16(self[offset]) << 8 | UInt16(self[offset + 1])
|
let value = UInt16(self[offset]) << 8 | UInt16(self[offset + 1])
|
||||||
offset += 2
|
offset += 2
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable func readUInt32(at offset: inout Int) -> UInt32? {
|
func readUInt32(at offset: inout Int) -> UInt32? {
|
||||||
guard offset + 4 <= self.count else { return nil }
|
guard offset + 4 <= self.count else { return nil }
|
||||||
let value = UInt32(self[offset]) << 24 |
|
let value = UInt32(self[offset]) << 24 |
|
||||||
UInt32(self[offset + 1]) << 16 |
|
UInt32(self[offset + 1]) << 16 |
|
||||||
@@ -137,7 +137,7 @@ extension Data {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable func readUInt64(at offset: inout Int) -> UInt64? {
|
func readUInt64(at offset: inout Int) -> UInt64? {
|
||||||
guard offset + 8 <= self.count else { return nil }
|
guard offset + 8 <= self.count else { return nil }
|
||||||
var value: UInt64 = 0
|
var value: UInt64 = 0
|
||||||
for i in 0..<8 {
|
for i in 0..<8 {
|
||||||
@@ -220,3 +220,4 @@ extension Data {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,11 +139,6 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
// Reserve capacity to reduce reallocations. Estimate base size conservatively.
|
|
||||||
// header(13) + sender(8) + opt recipient(8) + opt originalSize(2) + payload + opt signature(64) + up to 255 pad
|
|
||||||
let estimatedPayload = payload.count + (isCompressed ? 2 : 0)
|
|
||||||
let estimated = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize) + estimatedPayload + (packet.signature == nil ? 0 : signatureSize) + 255
|
|
||||||
data.reserveCapacity(estimated)
|
|
||||||
data.append(packet.version)
|
data.append(packet.version)
|
||||||
data.append(packet.type)
|
data.append(packet.type)
|
||||||
data.append(packet.ttl)
|
data.append(packet.ttl)
|
||||||
@@ -227,107 +222,137 @@ struct BinaryProtocol {
|
|||||||
|
|
||||||
// Core decoding implementation used by decode(_:) with and without padding removal
|
// Core decoding implementation used by decode(_:) with and without padding removal
|
||||||
private static func decodeCore(_ raw: Data) -> BitchatPacket? {
|
private static func decodeCore(_ raw: Data) -> BitchatPacket? {
|
||||||
// Minimum size: header + senderID
|
// Minimum size check: header + senderID
|
||||||
guard raw.count >= headerSize + senderIDSize else { return nil }
|
guard raw.count >= headerSize + senderIDSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return raw.withUnsafeBytes { (buf: UnsafeRawBufferPointer) -> BitchatPacket? in
|
// Convert to array for safer indexed access
|
||||||
guard let base = buf.baseAddress else { return nil }
|
let dataArray = Array(raw)
|
||||||
var offset = 0
|
var offset = 0
|
||||||
func require(_ n: Int) -> Bool { offset + n <= buf.count }
|
|
||||||
// Read single byte
|
// Header parsing with bounds checks
|
||||||
func read8() -> UInt8? {
|
guard offset < dataArray.count else { return nil }
|
||||||
guard require(1) else { return nil }
|
let version = dataArray[offset]; offset += 1
|
||||||
let v = base.advanced(by: offset).assumingMemoryBound(to: UInt8.self).pointee
|
|
||||||
offset += 1
|
// Check if version is 1 (only supported version)
|
||||||
return v
|
guard version == 1 else {
|
||||||
}
|
return nil
|
||||||
// Read big-endian 16-bit
|
|
||||||
func read16() -> UInt16? {
|
|
||||||
guard require(2) else { return nil }
|
|
||||||
let p = base.advanced(by: offset).assumingMemoryBound(to: UInt8.self)
|
|
||||||
let v = (UInt16(p[0]) << 8) | UInt16(p[1])
|
|
||||||
offset += 2
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
// Copy N bytes into Data
|
|
||||||
func readData(_ n: Int) -> Data? {
|
|
||||||
guard require(n) else { return nil }
|
|
||||||
let ptr = base.advanced(by: offset)
|
|
||||||
let d = Data(bytes: ptr, count: n)
|
|
||||||
offset += n
|
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version
|
guard offset < dataArray.count else { return nil }
|
||||||
guard let version = read8(), version == 1 else { return nil }
|
let type = dataArray[offset]; offset += 1
|
||||||
guard let type = read8() else { return nil }
|
|
||||||
guard let ttl = read8() else { return nil }
|
|
||||||
|
|
||||||
// Timestamp 8 bytes BE
|
guard offset < dataArray.count else { return nil }
|
||||||
guard require(8) else { return nil }
|
let ttl = dataArray[offset]; offset += 1
|
||||||
var ts: UInt64 = 0
|
|
||||||
for _ in 0..<8 {
|
// Timestamp - need 8 bytes
|
||||||
guard let b = read8() else { return nil }
|
guard offset + 8 <= dataArray.count else { return nil }
|
||||||
ts = (ts << 8) | UInt64(b)
|
let timestampData = Data(dataArray[offset..<offset+8])
|
||||||
|
let timestamp = timestampData.reduce(0) { result, byte in
|
||||||
|
(result << 8) | UInt64(byte)
|
||||||
}
|
}
|
||||||
|
offset += 8
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
guard let flags = read8() else { return nil }
|
guard offset < dataArray.count else { return nil }
|
||||||
|
let flags = dataArray[offset]; offset += 1
|
||||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||||
|
|
||||||
// Payload length
|
// Payload length - need 2 bytes
|
||||||
guard let payloadLen = read16(), payloadLen <= 65535 else { return nil }
|
guard offset + 2 <= dataArray.count else { return nil }
|
||||||
|
let payloadLengthData = Data(dataArray[offset..<offset+2])
|
||||||
|
let payloadLength = payloadLengthData.reduce(0) { result, byte in
|
||||||
|
(result << 8) | UInt16(byte)
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
// SenderID
|
// Validate payloadLength is reasonable (prevent integer overflow)
|
||||||
guard let senderID = readData(senderIDSize) else { return nil }
|
guard payloadLength <= 65535 else { return nil }
|
||||||
|
|
||||||
// Recipient
|
// SenderID - need 8 bytes
|
||||||
var recipientID: Data? = nil
|
guard offset + senderIDSize <= dataArray.count else { return nil }
|
||||||
|
let senderID = Data(dataArray[offset..<offset+senderIDSize])
|
||||||
|
offset += senderIDSize
|
||||||
|
|
||||||
|
// RecipientID if present
|
||||||
|
var recipientID: Data?
|
||||||
if hasRecipient {
|
if hasRecipient {
|
||||||
recipientID = readData(recipientIDSize)
|
guard offset + recipientIDSize <= dataArray.count else { return nil }
|
||||||
if recipientID == nil { return nil }
|
recipientID = Data(dataArray[offset..<offset+recipientIDSize])
|
||||||
|
offset += recipientIDSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload
|
// Payload handling with comprehensive bounds checking
|
||||||
let payload: Data
|
let payload: Data
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
// Need original size (2 bytes)
|
// Compressed payload needs at least 2 bytes for original size
|
||||||
guard let origSize16 = read16() else { return nil }
|
guard Int(payloadLength) >= 2 else { return nil }
|
||||||
let originalSize = Int(origSize16)
|
|
||||||
guard originalSize >= 0 && originalSize <= 1_048_576 else { return nil }
|
// Check we have enough data for the original size prefix
|
||||||
let compSize = Int(payloadLen) - 2
|
guard offset + 2 <= dataArray.count else { return nil }
|
||||||
guard compSize >= 0, let compressed = readData(compSize) else { return nil }
|
let originalSizeData = Data(dataArray[offset..<offset+2])
|
||||||
guard let decompressed = CompressionUtil.decompress(compressed, originalSize: originalSize),
|
let originalSize = Int(originalSizeData.reduce(0) { result, byte in
|
||||||
decompressed.count == originalSize else { return nil }
|
(result << 8) | UInt16(byte)
|
||||||
payload = decompressed
|
})
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
// Validate original size is reasonable
|
||||||
|
guard originalSize >= 0 && originalSize <= 1048576 else { return nil } // Max 1MB
|
||||||
|
|
||||||
|
// Check we have enough data for the compressed payload
|
||||||
|
let compressedPayloadSize = Int(payloadLength) - 2
|
||||||
|
guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= dataArray.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let compressedPayload = Data(dataArray[offset..<offset+compressedPayloadSize])
|
||||||
|
offset += compressedPayloadSize
|
||||||
|
|
||||||
|
// Decompress with error handling
|
||||||
|
guard let decompressedPayload = CompressionUtil.decompress(compressedPayload, originalSize: originalSize) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify decompressed size matches expected
|
||||||
|
guard decompressedPayload.count == originalSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = decompressedPayload
|
||||||
} else {
|
} else {
|
||||||
guard let p = readData(Int(payloadLen)) else { return nil }
|
// Uncompressed payload
|
||||||
payload = p
|
guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= dataArray.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload = Data(dataArray[offset..<offset+Int(payloadLength)])
|
||||||
|
offset += Int(payloadLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature
|
// Signature if present
|
||||||
var signature: Data? = nil
|
var signature: Data?
|
||||||
if hasSignature {
|
if hasSignature {
|
||||||
signature = readData(signatureSize)
|
guard offset + signatureSize <= dataArray.count else { return nil }
|
||||||
if signature == nil { return nil }
|
signature = Data(dataArray[offset..<offset+signatureSize])
|
||||||
|
offset += signatureSize
|
||||||
}
|
}
|
||||||
|
|
||||||
guard offset <= buf.count else { return nil }
|
// Final validation: ensure we haven't gone past the end
|
||||||
|
guard offset <= dataArray.count else { return nil }
|
||||||
|
|
||||||
return BitchatPacket(
|
return BitchatPacket(
|
||||||
type: type,
|
type: type,
|
||||||
senderID: senderID,
|
senderID: senderID,
|
||||||
recipientID: recipientID,
|
recipientID: recipientID,
|
||||||
timestamp: ts,
|
timestamp: timestamp,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: signature,
|
signature: signature,
|
||||||
ttl: ttl
|
ttl: ttl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Binary encoding for BitchatMessage
|
// Binary encoding for BitchatMessage
|
||||||
extension BitchatMessage {
|
extension BitchatMessage {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ struct AnnouncementPacket {
|
|||||||
|
|
||||||
func encode() -> Data? {
|
func encode() -> Data? {
|
||||||
var data = Data()
|
var data = Data()
|
||||||
// Reserve: TLVs for nickname (2 + n), noise key (2 + 32), signing key (2 + 32)
|
|
||||||
data.reserveCapacity(2 + min(nickname.count, 255) + 2 + noisePublicKey.count + 2 + signingPublicKey.count)
|
|
||||||
|
|
||||||
// TLV for nickname
|
// TLV for nickname
|
||||||
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
|
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
|
||||||
@@ -90,7 +88,6 @@ struct PrivateMessagePacket {
|
|||||||
|
|
||||||
func encode() -> Data? {
|
func encode() -> Data? {
|
||||||
var data = Data()
|
var data = Data()
|
||||||
data.reserveCapacity(2 + min(messageID.count, 255) + 2 + min(content.count, 255))
|
|
||||||
|
|
||||||
// TLV for messageID
|
// TLV for messageID
|
||||||
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
|
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
|
||||||
|
|||||||
@@ -63,9 +63,8 @@ final class BLEService: NSObject {
|
|||||||
private let messageDeduplicator = MessageDeduplicator()
|
private let messageDeduplicator = MessageDeduplicator()
|
||||||
|
|
||||||
// 5. Fragment Reassembly (necessary for messages > MTU)
|
// 5. Fragment Reassembly (necessary for messages > MTU)
|
||||||
private struct FragmentKey: Hashable { let sender: UInt64; let id: UInt64 }
|
private var incomingFragments: [String: [Int: Data]] = [:]
|
||||||
private var incomingFragments: [FragmentKey: [Int: Data]] = [:]
|
private var fragmentMetadata: [String: (type: UInt8, total: Int, timestamp: Date)] = [:]
|
||||||
private var fragmentMetadata: [FragmentKey: (type: UInt8, total: Int, timestamp: Date)] = [:]
|
|
||||||
// Backoff for peripherals that recently timed out connecting
|
// Backoff for peripherals that recently timed out connecting
|
||||||
private var recentConnectTimeouts: [String: Date] = [:] // Peripheral UUID -> last timeout
|
private var recentConnectTimeouts: [String: Date] = [:] // Peripheral UUID -> last timeout
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ final class BLEService: NSObject {
|
|||||||
var myPeerID: String = ""
|
var myPeerID: String = ""
|
||||||
var myNickname: String = "anon"
|
var myNickname: String = "anon"
|
||||||
private let noiseService = NoiseEncryptionService()
|
private let noiseService = NoiseEncryptionService()
|
||||||
private var myPeerIDData: Data = Data()
|
|
||||||
|
|
||||||
// MARK: - Advertising Privacy
|
// MARK: - Advertising Privacy
|
||||||
// No Local Name by default for maximum privacy. No rotating alias.
|
// No Local Name by default for maximum privacy. No rotating alias.
|
||||||
@@ -333,7 +331,6 @@ final class BLEService: NSObject {
|
|||||||
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
||||||
let fingerprint = noiseService.getIdentityFingerprint() // 64 hex chars
|
let fingerprint = noiseService.getIdentityFingerprint() // 64 hex chars
|
||||||
self.myPeerID = String(fingerprint.prefix(16))
|
self.myPeerID = String(fingerprint.prefix(16))
|
||||||
self.myPeerIDData = Data(hexString: myPeerID) ?? Data()
|
|
||||||
|
|
||||||
// Set queue key for identification
|
// Set queue key for identification
|
||||||
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
||||||
@@ -400,7 +397,7 @@ final class BLEService: NSObject {
|
|||||||
maintenanceTimer = timer
|
maintenanceTimer = timer
|
||||||
|
|
||||||
// Publish initial empty state
|
// Publish initial empty state
|
||||||
requestPeerDataPublish()
|
publishFullPeerData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNickname(_ nickname: String) {
|
func setNickname(_ nickname: String) {
|
||||||
@@ -489,7 +486,7 @@ final class BLEService: NSObject {
|
|||||||
// Send leave message synchronously to ensure delivery
|
// Send leave message synchronously to ensure delivery
|
||||||
let leavePacket = BitchatPacket(
|
let leavePacket = BitchatPacket(
|
||||||
type: MessageType.leave.rawValue,
|
type: MessageType.leave.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: nil,
|
recipientID: nil,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: Data(),
|
payload: Data(),
|
||||||
@@ -617,7 +614,7 @@ final class BLEService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -669,7 +666,7 @@ final class BLEService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -826,7 +823,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: recipientData,
|
recipientID: recipientData,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -880,7 +877,7 @@ final class BLEService: NSObject {
|
|||||||
// Send handshake init
|
// Send handshake init
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: handshakeData,
|
payload: handshakeData,
|
||||||
@@ -930,7 +927,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -1273,11 +1270,8 @@ final class BLEService: NSObject {
|
|||||||
// Minimum header: 8 bytes ID + 2 index + 2 total + 1 type
|
// Minimum header: 8 bytes ID + 2 index + 2 total + 1 type
|
||||||
guard packet.payload.count >= 13 else { return }
|
guard packet.payload.count >= 13 else { return }
|
||||||
|
|
||||||
// Compute compact fragment key (sender: 8 bytes, id: 8 bytes), big-endian
|
let senderHex = packet.senderID.hexEncodedString()
|
||||||
var senderU64: UInt64 = 0
|
let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
||||||
for b in packet.senderID.prefix(8) { senderU64 = (senderU64 << 8) | UInt64(b) }
|
|
||||||
var fragU64: UInt64 = 0
|
|
||||||
for b in packet.payload.prefix(8) { fragU64 = (fragU64 << 8) | UInt64(b) }
|
|
||||||
// Parse big-endian UInt16 safely without alignment assumptions
|
// Parse big-endian UInt16 safely without alignment assumptions
|
||||||
let idxHi = UInt16(packet.payload[8])
|
let idxHi = UInt16(packet.payload[8])
|
||||||
let idxLo = UInt16(packet.payload[9])
|
let idxLo = UInt16(packet.payload[9])
|
||||||
@@ -1292,7 +1286,7 @@ final class BLEService: NSObject {
|
|||||||
guard total > 0 && index >= 0 && index < total else { return }
|
guard total > 0 && index >= 0 && index < total else { return }
|
||||||
|
|
||||||
// Store fragment
|
// Store fragment
|
||||||
let key = FragmentKey(sender: senderU64, id: fragU64)
|
let key = "\(senderHex):\(fragmentID)"
|
||||||
if incomingFragments[key] == nil {
|
if incomingFragments[key] == nil {
|
||||||
// Cap in-flight assemblies to prevent memory/battery blowups
|
// Cap in-flight assemblies to prevent memory/battery blowups
|
||||||
if incomingFragments.count >= maxInFlightAssemblies {
|
if incomingFragments.count >= maxInFlightAssemblies {
|
||||||
@@ -1465,20 +1459,6 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// Suppress announce logs to reduce noise
|
// Suppress announce logs to reduce noise
|
||||||
|
|
||||||
// Precompute signature verification outside barrier to reduce contention
|
|
||||||
let existingPeerForVerify = collectionsQueue.sync { peers[peerID] }
|
|
||||||
var verifiedAnnounce = false
|
|
||||||
if packet.signature != nil {
|
|
||||||
verifiedAnnounce = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey)
|
|
||||||
if !verifiedAnnounce {
|
|
||||||
SecureLogger.log("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let existingKey = existingPeerForVerify?.noisePublicKey, existingKey != announcement.noisePublicKey {
|
|
||||||
SecureLogger.log("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: SecureLogger.security, level: .warning)
|
|
||||||
verifiedAnnounce = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track if this is a new or reconnected peer
|
// Track if this is a new or reconnected peer
|
||||||
var isNewPeer = false
|
var isNewPeer = false
|
||||||
var isReconnectedPeer = false
|
var isReconnectedPeer = false
|
||||||
@@ -1503,8 +1483,21 @@ final class BLEService: NSObject {
|
|||||||
isNewPeer = (existingPeer == nil)
|
isNewPeer = (existingPeer == nil)
|
||||||
isReconnectedPeer = wasDisconnected
|
isReconnectedPeer = wasDisconnected
|
||||||
|
|
||||||
// Use precomputed verification result
|
// Verify packet signature using the announced signing public key
|
||||||
let verified = verifiedAnnounce
|
var verified = false
|
||||||
|
if packet.signature != nil {
|
||||||
|
// Verify that the packet was signed by the signing private key corresponding to the announced signing public key
|
||||||
|
verified = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey)
|
||||||
|
if !verified {
|
||||||
|
SecureLogger.log("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If existing peer has a different noise public key, do not consider this verified
|
||||||
|
if let existing = existingPeer, let existingKey = existing.noisePublicKey, existingKey != announcement.noisePublicKey {
|
||||||
|
SecureLogger.log("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: SecureLogger.security, level: .warning)
|
||||||
|
verified = false
|
||||||
|
}
|
||||||
|
|
||||||
// Require verified announce; ignore otherwise (no backward compatibility)
|
// Require verified announce; ignore otherwise (no backward compatibility)
|
||||||
if !verified {
|
if !verified {
|
||||||
@@ -1595,7 +1588,7 @@ final class BLEService: NSObject {
|
|||||||
self.delegate?.didConnectToPeer(peerID)
|
self.delegate?.didConnectToPeer(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.requestPeerDataPublish()
|
self.publishFullPeerData()
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1696,7 +1689,7 @@ final class BLEService: NSObject {
|
|||||||
// Send response
|
// Send response
|
||||||
let responsePacket = BitchatPacket(
|
let responsePacket = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: response,
|
payload: response,
|
||||||
@@ -1852,7 +1845,7 @@ final class BLEService: NSObject {
|
|||||||
// Create packet with signature using the noise private key
|
// Create packet with signature using the noise private key
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.announce.rawValue,
|
type: MessageType.announce.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: nil,
|
recipientID: nil,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -1886,7 +1879,7 @@ final class BLEService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -1923,7 +1916,7 @@ final class BLEService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: Data(hexString: peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
@@ -1990,28 +1983,6 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounced publish to coalesce rapid changes
|
|
||||||
private var lastPeerPublishAt: Date = .distantPast
|
|
||||||
private var peerPublishPending: Bool = false
|
|
||||||
private let peerPublishMinInterval: TimeInterval = 0.1
|
|
||||||
private func requestPeerDataPublish() {
|
|
||||||
let now = Date()
|
|
||||||
let elapsed = now.timeIntervalSince(lastPeerPublishAt)
|
|
||||||
if elapsed >= peerPublishMinInterval {
|
|
||||||
lastPeerPublishAt = now
|
|
||||||
publishFullPeerData()
|
|
||||||
} else if !peerPublishPending {
|
|
||||||
peerPublishPending = true
|
|
||||||
let delay = peerPublishMinInterval - elapsed
|
|
||||||
messageQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.lastPeerPublishAt = Date()
|
|
||||||
self.peerPublishPending = false
|
|
||||||
self.publishFullPeerData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Consolidated Maintenance
|
// MARK: - Consolidated Maintenance
|
||||||
|
|
||||||
private func performMaintenance() {
|
private func performMaintenance() {
|
||||||
@@ -2124,7 +2095,7 @@ final class BLEService: NSObject {
|
|||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(peerID)
|
||||||
}
|
}
|
||||||
// Publish snapshots so UnifiedPeerService updates connection/reachability icons
|
// Publish snapshots so UnifiedPeerService updates connection/reachability icons
|
||||||
self.requestPeerDataPublish()
|
self.publishFullPeerData()
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2506,7 +2477,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
if let peerID = peerID {
|
if let peerID = peerID {
|
||||||
self.notifyPeerDisconnectedDebounced(peerID)
|
self.notifyPeerDisconnectedDebounced(peerID)
|
||||||
}
|
}
|
||||||
self.requestPeerDataPublish()
|
self.publishFullPeerData()
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2907,7 +2878,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
self.notifyPeerDisconnectedDebounced(peerID)
|
self.notifyPeerDisconnectedDebounced(peerID)
|
||||||
// Publish snapshots so UnifiedPeerService can refresh icons promptly
|
// Publish snapshots so UnifiedPeerService can refresh icons promptly
|
||||||
self.requestPeerDataPublish()
|
self.publishFullPeerData()
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ final class MessageDeduplicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var entries: [Entry] = []
|
private var entries: [Entry] = []
|
||||||
private var head: Int = 0
|
|
||||||
private var lookup = Set<String>()
|
private var lookup = Set<String>()
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private let maxAge: TimeInterval = TransportConfig.messageDedupMaxAgeSeconds // 5 minutes
|
private let maxAge: TimeInterval = TransportConfig.messageDedupMaxAgeSeconds // 5 minutes
|
||||||
@@ -29,18 +28,10 @@ final class MessageDeduplicator {
|
|||||||
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
||||||
lookup.insert(messageID)
|
lookup.insert(messageID)
|
||||||
|
|
||||||
// Soft-cap and advance head by a chunk to avoid O(n) shifting
|
if entries.count > maxCount {
|
||||||
if (entries.count - head) > maxCount {
|
let toRemove = entries.prefix(100)
|
||||||
let removeCount = min(100, entries.count - head)
|
toRemove.forEach { lookup.remove($0.messageID) }
|
||||||
for i in head..<(head + removeCount) {
|
entries.removeFirst(100)
|
||||||
lookup.remove(entries[i].messageID)
|
|
||||||
}
|
|
||||||
head += removeCount
|
|
||||||
// Periodically compact to reclaim memory
|
|
||||||
if head > entries.count / 2 {
|
|
||||||
entries.removeFirst(head)
|
|
||||||
head = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -70,7 +61,6 @@ final class MessageDeduplicator {
|
|||||||
defer { lock.unlock() }
|
defer { lock.unlock() }
|
||||||
|
|
||||||
entries.removeAll()
|
entries.removeAll()
|
||||||
head = 0
|
|
||||||
lookup.removeAll()
|
lookup.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +78,9 @@ final class MessageDeduplicator {
|
|||||||
|
|
||||||
private func cleanupOldEntries() {
|
private func cleanupOldEntries() {
|
||||||
let cutoff = Date().addingTimeInterval(-maxAge)
|
let cutoff = Date().addingTimeInterval(-maxAge)
|
||||||
while head < entries.count, entries[head].timestamp < cutoff {
|
while let first = entries.first, first.timestamp < cutoff {
|
||||||
lookup.remove(entries[head].messageID)
|
lookup.remove(first.messageID)
|
||||||
head += 1
|
entries.removeFirst()
|
||||||
}
|
|
||||||
if head > 0 && head > entries.count / 2 {
|
|
||||||
entries.removeFirst(head)
|
|
||||||
head = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,11 +140,11 @@ class SecureLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Log general messages with automatic sensitive data filtering
|
/// Log general messages with automatic sensitive data filtering
|
||||||
static func log(_ message: @autoclosure () -> String, category: OSLog = noise, level: LogLevel = .debug,
|
static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug,
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
guard shouldLog(level) else { return }
|
guard shouldLog(level) else { return }
|
||||||
let location = formatLocation(file: file, line: line, function: function)
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
let sanitized = sanitize("\(location) \(message())")
|
let sanitized = sanitize("\(location) \(message)")
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
os_log("%{public}@", log: category, type: level.osLogType, sanitized)
|
os_log("%{public}@", log: category, type: level.osLogType, sanitized)
|
||||||
@@ -157,10 +157,10 @@ class SecureLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Log errors with context
|
/// Log errors with context
|
||||||
static func logError(_ error: Error, context: @autoclosure () -> String, category: OSLog = noise,
|
static func logError(_ error: Error, context: String, category: OSLog = noise,
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
let location = formatLocation(file: file, line: line, function: function)
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
let sanitized = sanitize(context())
|
let sanitized = sanitize(context)
|
||||||
let errorDesc = sanitize(error.localizedDescription)
|
let errorDesc = sanitize(error.localizedDescription)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
Reference in New Issue
Block a user