Skip to content

Instantly share code, notes, and snippets.

@mac-cain13
Last active March 29, 2024 17:12
Show Gist options
  • Save mac-cain13/fa684f54a7ae1bba8669e78d28611784 to your computer and use it in GitHub Desktop.
Save mac-cain13/fa684f54a7ae1bba8669e78d28611784 to your computer and use it in GitHub Desktop.
Function to request and check Local Network permission on iOS, iPadOS & visionOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_preflight_check._tcp</string>
</array>
</dict>
</plist>
//
// RequestLocalNetworkAuthorization.swift
// Nonstrict
//
// Created by Nonstrict on 10/01/2024.
//
import Foundation
import Network
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.nonstrict.localNetworkAuthCheck", category: #fileID)
private let type = "_preflight_check._tcp"
/// Checks whether Local Network permission has been granted, if the authorization state for Local Network usage isn't yet determined it will request the user for permission.
///
/// - Throws: When a network error occurs or a `CancellationError` when cancelled.
/// - Returns: A boolean indicating whether Local Network permission is granted.
func requestLocalNetworkAuthorization() async throws -> Bool {
let queue = DispatchQueue(label: "com.nonstrict.localNetworkAuthCheck")
logger.info("Setup listener.")
let listener = try NWListener(using: NWParameters(tls: .none, tcp: NWProtocolTCP.Options()))
listener.service = NWListener.Service(name: UUID().uuidString, type: type)
listener.newConnectionHandler = { _ in } // Must be set or else the listener will error with POSIX error 22
logger.info("Setup browser.")
let parameters = NWParameters()
parameters.includePeerToPeer = true
let browser = NWBrowser(for: .bonjour(type: type, domain: nil), using: parameters)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Bool, Error>) in
class LocalState {
var didResume = false
}
let local = LocalState()
@Sendable func resume(with result: Result<Bool, Error>) {
if local.didResume {
logger.warning("Already resumed, ignoring subsequent result.")
return
}
local.didResume = true
// Teardown listener and browser
listener.stateUpdateHandler = { _ in }
browser.stateUpdateHandler = { _ in }
browser.browseResultsChangedHandler = { _, _ in }
listener.cancel()
browser.cancel()
continuation.resume(with: result)
}
// Do not setup listener/browser is we're already cancelled, it does work but logs a lot of very ugly errors
if Task.isCancelled {
logger.notice("Task cancelled before listener & browser started.")
resume(with: .failure(CancellationError()))
return
}
listener.stateUpdateHandler = { newState in
switch newState {
case .setup:
logger.debug("Listener performing setup.")
case .ready:
logger.notice("Listener ready to be discovered.")
case .cancelled:
logger.notice("Listener cancelled.")
resume(with: .failure(CancellationError()))
case .failed(let error):
logger.error("Listener failed, stopping. \(error, privacy: .public)")
resume(with: .failure(error))
case .waiting(let error):
logger.warning("Listener waiting, stopping. \(error, privacy: .public)")
resume(with: .failure(error))
@unknown default:
logger.warning("Ignoring unknown listener state: \(String(describing: newState), privacy: .public)")
}
}
listener.start(queue: queue)
browser.stateUpdateHandler = { newState in
switch newState {
case .setup:
logger.debug("Browser performing setup.")
return
case .ready:
logger.notice("Browser ready to discover listeners.")
return
case .cancelled:
logger.notice("Browser cancelled.")
resume(with: .failure(CancellationError()))
case .failed(let error):
logger.error("Browser failed, stopping. \(error, privacy: .public)")
resume(with: .failure(error))
case let .waiting(error):
switch error {
case .dns(DNSServiceErrorType(kDNSServiceErr_PolicyDenied)):
logger.notice("Browser permission denied, reporting failure.")
resume(with: .success(false))
default:
logger.error("Browser waiting, stopping. \(error, privacy: .public)")
resume(with: .failure(error))
}
@unknown default:
logger.warning("Ignoring unknown browser state: \(String(describing: newState), privacy: .public)")
return
}
}
browser.browseResultsChangedHandler = { results, changes in
if results.isEmpty {
logger.warning("Got empty result set from browser, ignoring.")
return
}
logger.notice("Discovered \(results.count) listeners, reporting success.")
resume(with: .success(true))
}
browser.start(queue: queue)
// Task cancelled while setting up listener & browser, tear down immediatly
if Task.isCancelled {
logger.notice("Task cancelled during listener & browser start. (Some warnings might be logged by the listener or browser.)")
resume(with: .failure(CancellationError()))
return
}
}
} onCancel: {
listener.cancel()
browser.cancel()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment