Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Magic Switch/AppDelegate/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
switch result {
case .success:
self.bluetoothStore.peripherals.forEach { peripheral in
self.bluetoothStore.connectPeripheral(peripheral)
self.bluetoothStore.connectPeripheralFromPeer(peripheral)
}
self.endTransfer()
case .failure(let err):
Expand Down
63 changes: 45 additions & 18 deletions Magic Switch/Manager/IncomingConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ extension Notification.Name {
final class IncomingConnection {
// MARK: - Constants

/// Cuts off slow-talkers. Reset on every successful frame.
private static let idleTimeout: TimeInterval = 30
/// Cuts off slow-talkers. Reset on every successful frame. Held above the
/// peer's worst-case handoff connect time (its pair watchdog is 60s) because
/// a `CONNECT_ALL`/`CONNECT_ONE` receiver only acks after pairing finishes —
/// no frames are exchanged meanwhile — so a tighter idle cap would kill the
/// connection before it could reply. Must stay >= `NetworkDeviceStore`'s
/// `handoffBodyTimeout` (the sender's matching wait).
private static let idleTimeout: TimeInterval = 75
/// Hard cap on a single connection regardless of idle activity. Without it,
/// a well-behaved-looking attacker could keep `idleTimer` happy with
/// well-formed sealed frames indefinitely and pin a listener slot forever.
Expand Down Expand Up @@ -189,21 +194,34 @@ final class IncomingConnection {
break
case .connectAll:
let store = bluetoothStore
DispatchQueue.main.async {
lastReceivedCommand = nil
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
NotificationCenter.default.post(name: .magicSwitchReceivedConnectAll, object: nil)
store.peripherals.forEach { peripheral in
store.connectPeripheral(peripheral)
let peripherals = store.peripherals
guard !peripherals.isEmpty else {
self.queue.async {
self.sendString(DeviceCommand.operationSuccess.rawValue)
}
return
}
var remaining = peripherals.count
var allSucceeded = true
peripherals.forEach { peripheral in
store.connectPeripheralFromPeer(peripheral) { success in
self.queue.async {
allSucceeded = allSucceeded && success
remaining -= 1
if remaining == 0 {
self.sendString(
(allSucceeded ? DeviceCommand.operationSuccess : DeviceCommand.operationFailed)
.rawValue
)
}
}
}
}
}
// Best-effort ack: OP_SUCCESS here means "command received and
// dispatched," not "all peripherals successfully connected." Local
// pair work is async and may still fail (out of range, peer never
// released, etc.). The peer's goal ("you now hold these") is
// satisfied as long as we attempt — tracking per-peripheral results
// and aggregating would require holding the connection open until
// every IOBluetooth callback lands, which isn't worth the complexity.
sendString(DeviceCommand.operationSuccess.rawValue)
lastReceivedCommand = nil
case .unregisterAll:
let store = bluetoothStore
DispatchQueue.main.async {
Expand Down Expand Up @@ -292,14 +310,23 @@ final class IncomingConnection {
}
let store = bluetoothStore
let address = message
DispatchQueue.main.async {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Peripheral is arriving at this Mac — flash the receiving arrow.
NotificationCenter.default.post(name: .magicSwitchPeripheralIncoming, object: nil)
if let peripheral = store.peripherals.first(where: { $0.id == address }) {
store.connectPeripheral(peripheral)
guard let peripheral = store.peripherals.first(where: { $0.id == address }) else {
self.queue.async {
self.sendString(DeviceCommand.operationFailed.rawValue)
}
return
}
store.connectPeripheralFromPeer(peripheral) { success in
self.queue.async {
self.sendString(
(success ? DeviceCommand.operationSuccess : DeviceCommand.operationFailed).rawValue)
}
}
}
sendString(DeviceCommand.operationSuccess.rawValue)
case .holdsOne:
guard Self.isValidMACAddress(message) else {
print("holdsOne: invalid MAC address: \(message)")
Expand Down
7 changes: 5 additions & 2 deletions Magic Switch/Manager/OutgoingConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final class OutgoingConnection {
/// opcode and don't reply OP_FAILED — i.e. anything older than the
/// commit that added the default-case ack) would otherwise hang here
/// until the peer's own idle timer (~30s) closes the socket.
private static let bodyTimeout: TimeInterval = 5
private static let defaultBodyTimeout: TimeInterval = 5

// MARK: - State

Expand All @@ -119,6 +119,7 @@ final class OutgoingConnection {
/// probes can't trip the limiter — and thereby block a user-initiated switch —
/// when a peer is down.
private let countsTowardRateLimit: Bool
private let bodyTimeout: TimeInterval
private let queue: DispatchQueue
private var channel: SecureChannel?
private var selfRef: OutgoingConnection?
Expand All @@ -134,6 +135,7 @@ final class OutgoingConnection {
pairingStore: PairingStore = .shared,
rateLimiter: OutboundRateLimiter = .shared,
countsTowardRateLimit: Bool = true,
bodyTimeout: TimeInterval = OutgoingConnection.defaultBodyTimeout,
queue: DispatchQueue = DispatchQueue(label: "com.magicswitch.outgoing", qos: .userInitiated)
) {
self.connection = NWConnection(
Expand All @@ -145,6 +147,7 @@ final class OutgoingConnection {
self.pairingStore = pairingStore
self.rateLimiter = rateLimiter
self.countsTowardRateLimit = countsTowardRateLimit
self.bodyTimeout = bodyTimeout
self.queue = queue
}

Expand Down Expand Up @@ -258,7 +261,7 @@ final class OutgoingConnection {
completion: @escaping (Result<Void, OutgoingFailure>) -> Void
) {
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + Self.bodyTimeout)
timer.schedule(deadline: .now() + bodyTimeout)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.finish(.failure(.bodyFailed), completion: completion)
Expand Down
Loading
Loading