diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index 0c6cdbd..2f5e79e 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -2,24 +2,63 @@ import Foundation import OSLog import SecretKit +public struct Session: Sendable { + + public let messages: AsyncStream + public let provenance: SigningRequestProvenance + + private let fileHandle: FileHandle + private let continuation: AsyncStream.Continuation + private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Session") + + init(fileHandle: FileHandle) { + self.fileHandle = fileHandle + provenance = SigningRequestTracer().provenance(from: fileHandle) + (messages, continuation) = AsyncStream.makeStream() + Task { [continuation, logger] in + await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { + let data = fileHandle.availableData + guard !data.isEmpty else { + logger.debug("Socket controller received empty data, ending continuation.") + continuation.finish() + return + } + continuation.yield(data) + logger.debug("Socket controller yielded data.") + } + } + } + + public func write(_ data: Data) async throws { + try fileHandle.write(contentsOf: data) + await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + } + + public func close() throws { + logger.debug("Session closed.") + try fileHandle.close() + } + +} + /// A controller that manages socket configuration and request dispatching. -public final class SocketController { +public struct SocketController { /// The active SocketPort. - private var port: SocketPort? - /// A handler that will be notified when a new read/write handle is available. - /// False if no data could be read - public var handler: OSAllocatedUnfairLock<(@Sendable (Data, SigningRequestProvenance) async throws -> Data)?> = .init(initialState: nil) - /// Logger. + private let port: SocketPort + /// The FileHandle for the main socket. + private let fileHandle: FileHandle private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "SocketController") - private let requestTracer = SigningRequestTracer() - // Async sequence of message? + public let sessions: AsyncStream + public let continuation: AsyncStream.Continuation /// Initializes a socket controller with a specified path. /// - Parameter path: The path to use as a socket. public init(path: String) { + (sessions, continuation) = AsyncStream.makeStream() logger.debug("Socket controller setting up at \(path)") if let _ = try? FileManager.default.removeItem(atPath: path) { logger.debug("Socket controller removed existing socket") @@ -27,76 +66,19 @@ public final class SocketController { let exists = FileManager.default.fileExists(atPath: path) assert(!exists) logger.debug("Socket controller path is clear") - port = socketPort(at: path) - configureSocket(at: path) - logger.debug("Socket listening at \(path)") - } - - /// Configures the socket and a corresponding FileHandle. - /// - Parameter path: The path to use as a socket. - func configureSocket(at path: String) { - guard let port = port else { return } - let fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) - fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common]) - Task { [handler, logger] in + port = SocketPort(path: path) + fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) + Task { [fileHandle, continuation, logger] in for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) { logger.debug("Socket controller accepted connection") guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue } - let provenance = SigningRequestTracer().provenance(from: new) - guard let handler = handler.withLock({ $0 }) else { - // FIXME: THIS - fatalError() - } - let response = try await handler(Data(fileHandle.availableData), provenance) - try fileHandle.write(contentsOf: response) - await new.waitForDataInBackgroundAndNotifyOnMainActor() + let session = Session(fileHandle: new) + continuation.yield(session) await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor() } } - Task { [logger, handler] in - for await notification in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable) { - logger.debug("Socket controller has new data available") - guard let new = notification.object as? FileHandle else { return } - logger.debug("Socket controller received new file handle") - guard let handler = handler.withLock({ $0 }) else { - // FIXME: THIS - fatalError() - } - do { - let provenance = SigningRequestTracer().provenance(from: new) - let response = try await handler(Data(fileHandle.availableData), provenance) - try fileHandle.write(contentsOf: response) - logger.debug("Socket controller handled data, wait for more data") - await new.waitForDataInBackgroundAndNotifyOnMainActor() - } catch { - logger.debug("Socket controller called with empty data, socked closed") - } - } - } - } - - /// Creates a SocketPort for a path. - /// - Parameter path: The path to use as a socket. - /// - Returns: A configured SocketPort. - func socketPort(at path: String) -> SocketPort { - var addr = sockaddr_un() - addr.sun_family = sa_family_t(AF_UNIX) - - var len: Int = 0 - withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in - path.withCString { cstring in - len = strlen(cstring) - strncpy(pointer, cstring, len) - } - } - addr.sun_len = UInt8(len+2) - - var data: Data! - withUnsafePointer(to: &addr) { pointer in - data = Data(bytes: pointer, count: MemoryLayout.size) - } - - return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)! + fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common]) + logger.debug("Socket listening at \(path)") } } @@ -116,3 +98,28 @@ extension FileHandle { } } + +extension SocketPort { + + convenience init(path: String) { + var addr = sockaddr_un() + addr.sun_family = sa_family_t(AF_UNIX) + + var len: Int = 0 + withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in + path.withCString { cstring in + len = strlen(cstring) + strncpy(pointer, cstring, len) + } + } + addr.sun_len = UInt8(len+2) + + var data: Data! + withUnsafePointer(to: &addr) { pointer in + data = Data(bytes: pointer, count: MemoryLayout.size) + } + + self.init(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)! + } + +} diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 4dfce2c..5800c75 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -33,12 +33,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { logger.debug("SecretAgent finished launching") - socketController.handler.withLock { [agent] in $0 = agent.handle } -// Task { -// for await (message, response) in socketController.messages { -// let handled = -// } -// } + Task { + for await session in socketController.sessions { + Task { + do { + for await message in session.messages { + let agentResponse = try await agent.handle(data: message, provenance: session.provenance) + try await session.write(agentResponse) + } + } catch { + try session.close() + } + } + } + } Task { for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)