mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
427 lines
12 KiB
Swift
427 lines
12 KiB
Swift
//
|
|
// TAKServerManager.swift
|
|
// Meshtastic
|
|
//
|
|
// Created by niccellular 12/26/25
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import OSLog
|
|
import Combine
|
|
import SwiftUI
|
|
|
|
/// Manages the TAK Server lifecycle, TLS connections, and client management
|
|
/// Runs on MainActor for thread safety, following the AccessoryManager pattern
|
|
@MainActor
|
|
final class TAKServerManager: ObservableObject {
|
|
|
|
static let shared = TAKServerManager()
|
|
|
|
// MARK: - Published State
|
|
|
|
@Published private(set) var isRunning = false
|
|
@Published private(set) var connectedClients: [TAKClientInfo] = []
|
|
@Published var lastError: String?
|
|
|
|
// MARK: - Configuration (persisted via AppStorage)
|
|
|
|
@AppStorage("takServerEnabled") var enabled = false {
|
|
didSet {
|
|
Task {
|
|
if enabled && !isRunning {
|
|
try? await start()
|
|
} else if !enabled && isRunning {
|
|
stop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fixed port - always use TLS port 8089
|
|
static let defaultTLSPort = 8089
|
|
static let defaultTCPPort = 8087 // Legacy, not used
|
|
|
|
/// Port is fixed to 8089 (mTLS)
|
|
var port: Int { Self.defaultTLSPort }
|
|
|
|
/// Always use TLS/mTLS
|
|
var useTLS: Bool { true }
|
|
|
|
// MARK: - Bridge
|
|
|
|
/// Bridge for converting between CoT and Meshtastic formats
|
|
var bridge: TAKMeshtasticBridge?
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private var listener: NWListener?
|
|
private var connections: [ObjectIdentifier: TAKConnection] = [:]
|
|
private var connectionTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
|
|
private let queue = DispatchQueue(label: "tak.server", qos: .userInitiated)
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Initialize the TAK server on app startup
|
|
/// Call this from app initialization to restore server state
|
|
func initializeOnStartup() {
|
|
guard enabled else {
|
|
Logger.tak.debug("TAK Server not enabled, skipping startup")
|
|
return
|
|
}
|
|
|
|
guard !isRunning else {
|
|
Logger.tak.debug("TAK Server already running")
|
|
return
|
|
}
|
|
|
|
Logger.tak.info("TAK Server enabled, starting on app launch")
|
|
Task {
|
|
do {
|
|
try await start()
|
|
} catch {
|
|
Logger.tak.error("Failed to start TAK Server on startup: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Server Lifecycle
|
|
|
|
/// Start the TAK server (TLS or TCP based on configuration)
|
|
func start() async throws {
|
|
guard !isRunning else {
|
|
Logger.tak.info("TAK Server already running")
|
|
return
|
|
}
|
|
|
|
let mode = useTLS ? "TLS" : "TCP"
|
|
Logger.tak.info("Starting TAK Server on port \(self.port) (\(mode) mode)")
|
|
|
|
let parameters: NWParameters
|
|
|
|
if useTLS {
|
|
// Validate we have a server certificate for TLS mode
|
|
guard let identity = TAKCertificateManager.shared.getServerIdentity() else {
|
|
let error = TAKServerError.noServerCertificate
|
|
lastError = error.localizedDescription
|
|
enabled = false
|
|
throw error
|
|
}
|
|
|
|
// Create TLS options
|
|
let tlsOptions = NWProtocolTLS.Options()
|
|
|
|
// Set server identity (certificate + private key)
|
|
let secIdentity = sec_identity_create(identity)!
|
|
sec_protocol_options_set_local_identity(
|
|
tlsOptions.securityProtocolOptions,
|
|
secIdentity
|
|
)
|
|
|
|
// Set minimum TLS version to 1.2 (TAK standard)
|
|
sec_protocol_options_set_min_tls_protocol_version(
|
|
tlsOptions.securityProtocolOptions,
|
|
.TLSv12
|
|
)
|
|
|
|
// Configure mTLS - always require client certificate for TLS mode
|
|
sec_protocol_options_set_peer_authentication_required(
|
|
tlsOptions.securityProtocolOptions,
|
|
true
|
|
)
|
|
|
|
// Set up client certificate validation
|
|
let clientCAs = TAKCertificateManager.shared.getClientCACertificates()
|
|
Logger.tak.info("Loaded \(clientCAs.count) CA certificate(s) for client validation")
|
|
if !clientCAs.isEmpty {
|
|
for (index, ca) in clientCAs.enumerated() {
|
|
if let summary = SecCertificateCopySubjectSummary(ca) as String? {
|
|
Logger.tak.info("CA[\(index)]: \(summary)")
|
|
}
|
|
}
|
|
let trustRoots = clientCAs as CFArray
|
|
sec_protocol_options_set_verify_block(
|
|
tlsOptions.securityProtocolOptions,
|
|
{ _, secTrust, completion in
|
|
// Convert sec_trust_t to SecTrust
|
|
let trust = sec_trust_copy_ref(secTrust).takeRetainedValue()
|
|
|
|
// Set policy for client certificate validation
|
|
// Use SSL policy with server=false to validate client certificates
|
|
// This properly accepts clientAuth ExtendedKeyUsage
|
|
let clientPolicy = SecPolicyCreateSSL(false, nil)
|
|
SecTrustSetPolicies(trust, clientPolicy)
|
|
|
|
SecTrustSetAnchorCertificates(trust, trustRoots)
|
|
SecTrustSetAnchorCertificatesOnly(trust, true)
|
|
var error: CFError?
|
|
let isValid = SecTrustEvaluateWithError(trust, &error)
|
|
if let error = error {
|
|
Logger.tak.error("Client cert validation error: \(error.localizedDescription)")
|
|
}
|
|
Logger.tak.info("Client certificate validation: \(isValid ? "passed" : "failed")")
|
|
completion(isValid)
|
|
},
|
|
queue
|
|
)
|
|
} else {
|
|
Logger.tak.warning("mTLS enabled but no CA certificates configured for client validation")
|
|
}
|
|
|
|
// TCP options
|
|
let tcpOptions = NWProtocolTCP.Options()
|
|
tcpOptions.enableKeepalive = true
|
|
tcpOptions.keepaliveIdle = 60
|
|
|
|
parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions)
|
|
} else {
|
|
// Plain TCP mode (no TLS)
|
|
let tcpOptions = NWProtocolTCP.Options()
|
|
tcpOptions.enableKeepalive = true
|
|
tcpOptions.keepaliveIdle = 60
|
|
|
|
parameters = NWParameters(tls: nil, tcp: tcpOptions)
|
|
}
|
|
|
|
parameters.allowLocalEndpointReuse = true
|
|
|
|
// Bind to localhost only - only allow TAK clients on the same device
|
|
parameters.requiredLocalEndpoint = NWEndpoint.hostPort(
|
|
host: NWEndpoint.Host("127.0.0.1"),
|
|
port: NWEndpoint.Port(integerLiteral: UInt16(port))
|
|
)
|
|
|
|
// Create and configure listener
|
|
do {
|
|
listener = try NWListener(using: parameters)
|
|
} catch {
|
|
lastError = "Failed to create listener: \(error.localizedDescription)"
|
|
Logger.tak.error("Failed to create TAK listener: \(error.localizedDescription)")
|
|
enabled = false
|
|
throw error
|
|
}
|
|
|
|
// Set up state handler
|
|
listener?.stateUpdateHandler = { [weak self] state in
|
|
Task { @MainActor in
|
|
self?.handleListenerState(state)
|
|
}
|
|
}
|
|
|
|
// Set up new connection handler
|
|
listener?.newConnectionHandler = { [weak self] connection in
|
|
Task { @MainActor in
|
|
await self?.handleNewConnection(connection)
|
|
}
|
|
}
|
|
|
|
// Start listening
|
|
listener?.start(queue: queue)
|
|
}
|
|
|
|
/// Stop the TAK server
|
|
func stop() {
|
|
Logger.tak.info("Stopping TAK Server")
|
|
|
|
listener?.cancel()
|
|
listener = nil
|
|
|
|
// Cancel all connection tasks
|
|
for (_, task) in connectionTasks {
|
|
task.cancel()
|
|
}
|
|
connectionTasks.removeAll()
|
|
|
|
// Disconnect all clients
|
|
for (_, connection) in connections {
|
|
Task {
|
|
await connection.disconnect()
|
|
}
|
|
}
|
|
connections.removeAll()
|
|
connectedClients.removeAll()
|
|
|
|
isRunning = false
|
|
lastError = nil
|
|
|
|
Logger.tak.info("TAK Server stopped")
|
|
}
|
|
|
|
/// Restart the server (useful after configuration changes)
|
|
func restart() async throws {
|
|
stop()
|
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay
|
|
try await start()
|
|
}
|
|
|
|
// MARK: - State Handling
|
|
|
|
private func handleListenerState(_ state: NWListener.State) {
|
|
switch state {
|
|
case .ready:
|
|
isRunning = true
|
|
lastError = nil
|
|
Logger.tak.info("TAK Server listening on port \(self.port)")
|
|
|
|
case .failed(let error):
|
|
isRunning = false
|
|
lastError = error.localizedDescription
|
|
enabled = false
|
|
Logger.tak.error("TAK Server failed: \(error.localizedDescription)")
|
|
|
|
case .cancelled:
|
|
isRunning = false
|
|
Logger.tak.info("TAK Server cancelled")
|
|
|
|
case .waiting(let error):
|
|
Logger.tak.warning("TAK Server waiting: \(error.localizedDescription)")
|
|
|
|
case .setup:
|
|
Logger.tak.debug("TAK Server setup")
|
|
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - Connection Management
|
|
|
|
private func handleNewConnection(_ nwConnection: NWConnection) async {
|
|
let connectionId = ObjectIdentifier(nwConnection)
|
|
let connection = TAKConnection(connection: nwConnection)
|
|
|
|
connections[connectionId] = connection
|
|
|
|
Logger.tak.info("New TAK client connecting: \(nwConnection.endpoint.debugDescription)")
|
|
|
|
// Start handling the connection
|
|
let eventStream = await connection.start()
|
|
|
|
// Create task to handle connection events
|
|
let task = Task {
|
|
for await event in eventStream {
|
|
await handleConnectionEvent(event, connectionId: connectionId)
|
|
}
|
|
// Connection ended
|
|
await removeConnection(connectionId)
|
|
}
|
|
|
|
connectionTasks[connectionId] = task
|
|
}
|
|
|
|
private func handleConnectionEvent(_ event: TAKConnectionEvent, connectionId: ObjectIdentifier) async {
|
|
switch event {
|
|
case .connected(let clientInfo):
|
|
connectedClients.append(clientInfo)
|
|
Logger.tak.info("TAK client connected: \(clientInfo.displayName)")
|
|
|
|
case .clientInfoUpdated(let clientInfo):
|
|
// Update the client info in our list
|
|
if let index = connectedClients.firstIndex(where: { $0.id == clientInfo.id }) {
|
|
connectedClients[index] = clientInfo
|
|
}
|
|
|
|
case .message(let cotMessage):
|
|
Logger.tak.info("Received CoT from TAK client: \(cotMessage.type)")
|
|
// Forward to Meshtastic mesh via bridge
|
|
await bridge?.sendToMesh(cotMessage)
|
|
|
|
case .disconnected:
|
|
await removeConnection(connectionId)
|
|
|
|
case .error(let error):
|
|
Logger.tak.error("TAK client error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func removeConnection(_ connectionId: ObjectIdentifier) async {
|
|
connectionTasks[connectionId]?.cancel()
|
|
connectionTasks.removeValue(forKey: connectionId)
|
|
|
|
if let connection = connections.removeValue(forKey: connectionId) {
|
|
let endpoint = await connection.endpoint
|
|
connectedClients.removeAll { $0.endpoint.debugDescription == endpoint.debugDescription }
|
|
Logger.tak.info("TAK client disconnected")
|
|
}
|
|
}
|
|
|
|
// MARK: - Message Distribution
|
|
|
|
/// Broadcast a CoT message to all connected TAK clients
|
|
func broadcast(_ cotMessage: CoTMessage) async {
|
|
guard !connections.isEmpty else { return }
|
|
|
|
Logger.tak.info("Broadcasting CoT to \(self.connections.count) TAK client(s): \(cotMessage.type)")
|
|
|
|
for (connectionId, connection) in connections {
|
|
do {
|
|
try await connection.send(cotMessage)
|
|
} catch {
|
|
Logger.tak.error("Failed to send to TAK client: \(error.localizedDescription)")
|
|
// Remove failed connection
|
|
await removeConnection(connectionId)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Send a CoT message to a specific client
|
|
func send(_ cotMessage: CoTMessage, to clientId: UUID) async throws {
|
|
guard let clientInfo = connectedClients.first(where: { $0.id == clientId }) else {
|
|
throw TAKServerError.clientNotFound
|
|
}
|
|
|
|
for (_, connection) in connections {
|
|
let endpoint = await connection.endpoint
|
|
if endpoint.debugDescription == clientInfo.endpoint.debugDescription {
|
|
try await connection.send(cotMessage)
|
|
return
|
|
}
|
|
}
|
|
|
|
throw TAKServerError.clientNotFound
|
|
}
|
|
|
|
// MARK: - Status
|
|
|
|
/// Get server status description
|
|
var statusDescription: String {
|
|
if isRunning {
|
|
let mode = useTLS ? "TLS" : "TCP"
|
|
return "Running on port \(port) (\(mode))"
|
|
} else if let error = lastError {
|
|
return "Error: \(error)"
|
|
} else {
|
|
return "Stopped"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Server Errors
|
|
|
|
enum TAKServerError: LocalizedError {
|
|
case noServerCertificate
|
|
case noClientCACertificate
|
|
case tlsConfigurationFailed
|
|
case listenerFailed(String)
|
|
case clientNotFound
|
|
case notRunning
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noServerCertificate:
|
|
return "No server certificate configured. Import a .p12 file with the server certificate and private key."
|
|
case .noClientCACertificate:
|
|
return "No client CA certificate configured. Import the CA certificate (.pem) used to sign client certificates."
|
|
case .tlsConfigurationFailed:
|
|
return "Failed to configure TLS settings."
|
|
case .listenerFailed(let reason):
|
|
return "Failed to start listener: \(reason)"
|
|
case .clientNotFound:
|
|
return "Client not found"
|
|
case .notRunning:
|
|
return "TAK Server is not running"
|
|
}
|
|
}
|
|
}
|