mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 13:25:20 +00:00
BCH-01-002: Switch from pending files to disk quota management
The original PR introduced a PendingFileManager that held incoming files in memory until user acceptance. However, this approach had issues: 1. No UI was implemented for users to accept/decline files 2. Files would expire after 5 minutes, losing legitimate transfers 3. The app only allows specific media types (photos, audio) that users explicitly choose to send, so manual acceptance adds friction This commit replaces the pending file system with disk quota management: - Auto-save files immediately (preserving original UX) - Enforce 100 MB storage quota for incoming files - Auto-delete oldest files when quota is exceeded - Logs cleanup activity for visibility This directly addresses the DoS vulnerability (disk exhaustion from file spam) while maintaining good UX for legitimate transfers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,25 +134,18 @@ struct FragmentationTests {
|
||||
}
|
||||
}
|
||||
|
||||
// BCH-01-002: Files are now held as pending until user accepts (no auto-save to disk)
|
||||
try await capture.waitForPendingFiles(count: 1, timeout: .seconds(2))
|
||||
try await sleep(1.0)
|
||||
|
||||
let pending = try #require(capture.pendingFiles.first, "Expected pending file transfer")
|
||||
#expect(pending.mimeType == "application/octet-stream")
|
||||
#expect(pending.fileSize == FileTransferLimits.maxPayloadBytes)
|
||||
#expect(pending.fileName == "limit.bin")
|
||||
let message = try #require(capture.receivedMessages.first, "Expected file transfer message")
|
||||
#expect(message.content.hasPrefix("[file]"))
|
||||
|
||||
// Verify file was NOT written to disk (the whole point of BCH-01-002 fix)
|
||||
let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let filesRoot = base.appendingPathComponent("files", isDirectory: true)
|
||||
let incoming = filesRoot.appendingPathComponent("files/incoming", isDirectory: true)
|
||||
if FileManager.default.fileExists(atPath: incoming.path) {
|
||||
let files = try FileManager.default.contentsOfDirectory(atPath: incoming.path)
|
||||
#expect(files.isEmpty, "Files should NOT be auto-saved to disk before user acceptance")
|
||||
if let fileName = message.content.split(separator: " ").last {
|
||||
let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let filesRoot = base.appendingPathComponent("files", isDirectory: true)
|
||||
let incoming = filesRoot.appendingPathComponent("files/incoming", isDirectory: true)
|
||||
let url = incoming.appendingPathComponent(String(fileName))
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
|
||||
// Clean up pending files
|
||||
PendingFileManager.shared.clearAll()
|
||||
}
|
||||
|
||||
@Test("Invalid fragment header is ignored")
|
||||
@@ -207,13 +200,10 @@ extension FragmentationTests {
|
||||
private let lock = NSLock()
|
||||
private var _publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||
private var _receivedMessages: [BitchatMessage] = []
|
||||
private var _pendingFiles: [PendingFileTransfer] = []
|
||||
private var publicMessageContinuation: CheckedContinuation<Void, Never>?
|
||||
private var receivedMessageContinuation: CheckedContinuation<Void, Never>?
|
||||
private var pendingFileContinuation: CheckedContinuation<Void, Never>?
|
||||
private var expectedPublicMessageCount: Int = 0
|
||||
private var expectedReceivedMessageCount: Int = 0
|
||||
private var expectedPendingFileCount: Int = 0
|
||||
|
||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] {
|
||||
lock.lock()
|
||||
@@ -227,12 +217,6 @@ extension FragmentationTests {
|
||||
return _receivedMessages
|
||||
}
|
||||
|
||||
var pendingFiles: [PendingFileTransfer] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return _pendingFiles
|
||||
}
|
||||
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
lock.lock()
|
||||
_receivedMessages.append(message)
|
||||
@@ -249,22 +233,6 @@ extension FragmentationTests {
|
||||
}
|
||||
}
|
||||
|
||||
func didReceivePendingFileTransfer(_ pending: PendingFileTransfer) {
|
||||
lock.lock()
|
||||
_pendingFiles.append(pending)
|
||||
let count = _pendingFiles.count
|
||||
let expected = expectedPendingFileCount
|
||||
let continuation = pendingFileContinuation
|
||||
lock.unlock()
|
||||
|
||||
if count >= expected, let cont = continuation {
|
||||
lock.lock()
|
||||
pendingFileContinuation = nil
|
||||
lock.unlock()
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
|
||||
lock.lock()
|
||||
_publicMessages.append((peerID, nickname, content))
|
||||
@@ -349,38 +317,6 @@ extension FragmentationTests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the specified number of pending files to be received
|
||||
func waitForPendingFiles(count: Int, timeout: Duration = .seconds(2)) async throws {
|
||||
lock.lock()
|
||||
if _pendingFiles.count >= count {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
expectedPendingFileCount = count
|
||||
lock.unlock()
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await withCheckedContinuation { continuation in
|
||||
self.lock.lock()
|
||||
if self._pendingFiles.count >= count {
|
||||
self.lock.unlock()
|
||||
continuation.resume()
|
||||
return
|
||||
}
|
||||
self.pendingFileContinuation = continuation
|
||||
self.lock.unlock()
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(for: timeout)
|
||||
throw CancellationError()
|
||||
}
|
||||
try await group.next()
|
||||
group.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
func didConnectToPeer(_ peerID: PeerID) {}
|
||||
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||
|
||||
Reference in New Issue
Block a user