Handle concurrent requests to socket (#495)

* Socket updates

* Sendable.

* Update tests.
This commit is contained in:
Max Goedjen 2023-12-11 00:59:30 -08:00 committed by GitHub
parent 7b0ccbcc16
commit cf58630065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 48 deletions

View File

@ -12,7 +12,7 @@ public class Agent {
private let writer = OpenSSHKeyWriter() private let writer = OpenSSHKeyWriter()
private let requestTracer = SigningRequestTracer() private let requestTracer = SigningRequestTracer()
private let certificateHandler = OpenSSHCertificateHandler() private let certificateHandler = OpenSSHCertificateHandler()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.agent", category: "") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
/// Initializes an agent with a store list and a witness. /// Initializes an agent with a store list and a witness.
/// - Parameters: /// - Parameters:
@ -35,7 +35,7 @@ extension Agent {
/// - writer: A ``FileHandleWriter`` to write the response to. /// - writer: A ``FileHandleWriter`` to write the response to.
/// - Return value: /// - Return value:
/// - Boolean if data could be read /// - Boolean if data could be read
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool { @discardableResult @Sendable public func handle(reader: FileHandleReader, writer: FileHandleWriter) async -> Bool {
logger.debug("Agent handling new data") logger.debug("Agent handling new data")
let data = Data(reader.availableData) let data = Data(reader.availableData)
guard data.count > 4 else { return false} guard data.count > 4 else { return false}
@ -47,12 +47,12 @@ extension Agent {
} }
logger.debug("Agent handling request of type \(requestType.debugDescription)") logger.debug("Agent handling request of type \(requestType.debugDescription)")
let subData = Data(data[5...]) let subData = Data(data[5...])
let response = handle(requestType: requestType, data: subData, reader: reader) let response = await handle(requestType: requestType, data: subData, reader: reader)
writer.write(response) writer.write(response)
return true return true
} }
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data { func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) async -> Data {
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
reloadSecretsIfNeccessary() reloadSecretsIfNeccessary()
var response = Data() var response = Data()

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// Protocol abstraction of the reading aspects of FileHandle. /// Protocol abstraction of the reading aspects of FileHandle.
public protocol FileHandleReader { public protocol FileHandleReader: Sendable {
/// Gets data that is available for reading. /// Gets data that is available for reading.
var availableData: Data { get } var availableData: Data { get }
@ -13,7 +13,7 @@ public protocol FileHandleReader {
} }
/// Protocol abstraction of the writing aspects of FileHandle. /// Protocol abstraction of the writing aspects of FileHandle.
public protocol FileHandleWriter { public protocol FileHandleWriter: Sendable {
/// Writes data to the handle. /// Writes data to the handle.
func write(_ data: Data) func write(_ data: Data)

View File

@ -10,22 +10,24 @@ public class SocketController {
private var port: SocketPort? private var port: SocketPort?
/// A handler that will be notified when a new read/write handle is available. /// A handler that will be notified when a new read/write handle is available.
/// False if no data could be read /// False if no data could be read
public var handler: ((FileHandleReader, FileHandleWriter) -> Bool)? public var handler: ((FileHandleReader, FileHandleWriter) async -> Bool)?
/// Logger.
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "SocketController")
/// Initializes a socket controller with a specified path. /// Initializes a socket controller with a specified path.
/// - Parameter path: The path to use as a socket. /// - Parameter path: The path to use as a socket.
public init(path: String) { public init(path: String) {
Logger().debug("Socket controller setting up at \(path)") logger.debug("Socket controller setting up at \(path)")
if let _ = try? FileManager.default.removeItem(atPath: path) { if let _ = try? FileManager.default.removeItem(atPath: path) {
Logger().debug("Socket controller removed existing socket") logger.debug("Socket controller removed existing socket")
} }
let exists = FileManager.default.fileExists(atPath: path) let exists = FileManager.default.fileExists(atPath: path)
assert(!exists) assert(!exists)
Logger().debug("Socket controller path is clear") logger.debug("Socket controller path is clear")
port = socketPort(at: path) port = socketPort(at: path)
configureSocket(at: path) configureSocket(at: path)
Logger().debug("Socket listening at \(path)") logger.debug("Socket listening at \(path)")
} }
/// Configures the socket and a corresponding FileHandle. /// Configures the socket and a corresponding FileHandle.
@ -35,7 +37,7 @@ public class SocketController {
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!]) fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common])
} }
/// Creates a SocketPort for a path. /// Creates a SocketPort for a path.
@ -65,25 +67,45 @@ public class SocketController {
/// Handles a new connection being accepted, invokes the handler, and prepares to accept new connections. /// Handles a new connection being accepted, invokes the handler, and prepares to accept new connections.
/// - Parameter notification: A `Notification` that triggered the call. /// - Parameter notification: A `Notification` that triggered the call.
@objc func handleConnectionAccept(notification: Notification) { @objc func handleConnectionAccept(notification: Notification) {
Logger().debug("Socket controller accepted connection") logger.debug("Socket controller accepted connection")
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return } guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
_ = handler?(new, new) Task {
new.waitForDataInBackgroundAndNotify() _ = await handler?(new, new)
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!]) await new.waitForDataInBackgroundAndNotifyOnMainActor()
await fileHandle?.acceptConnectionInBackgroundAndNotifyOnMainActor()
}
} }
/// Handles a new connection providing data and invokes the handler callback. /// Handles a new connection providing data and invokes the handler callback.
/// - Parameter notification: A `Notification` that triggered the call. /// - Parameter notification: A `Notification` that triggered the call.
@objc func handleConnectionDataAvailable(notification: Notification) { @objc func handleConnectionDataAvailable(notification: Notification) {
Logger().debug("Socket controller has new data available") logger.debug("Socket controller has new data available")
guard let new = notification.object as? FileHandle else { return } guard let new = notification.object as? FileHandle else { return }
Logger().debug("Socket controller received new file handle") logger.debug("Socket controller received new file handle")
if((handler?(new, new)) == true) { Task {
Logger().debug("Socket controller handled data, wait for more data") if((await handler?(new, new)) == true) {
new.waitForDataInBackgroundAndNotify() logger.debug("Socket controller handled data, wait for more data")
await new.waitForDataInBackgroundAndNotifyOnMainActor()
} else { } else {
Logger().debug("Socket controller called with empty data, socked closed") logger.debug("Socket controller called with empty data, socked closed")
}
} }
} }
} }
extension FileHandle {
/// Ensures waitForDataInBackgroundAndNotify will be called on the main actor.
@MainActor func waitForDataInBackgroundAndNotifyOnMainActor() {
waitForDataInBackgroundAndNotify()
}
/// Ensures acceptConnectionInBackgroundAndNotify will be called on the main actor.
/// - Parameter modes: the runloop modes to use.
@MainActor func acceptConnectionInBackgroundAndNotifyOnMainActor(forModes modes: [RunLoop.Mode]? = [RunLoop.Mode.common]) {
acceptConnectionInBackgroundAndNotify(forModes: modes)
}
}

View File

@ -5,7 +5,7 @@ import OSLog
public class OpenSSHCertificateHandler { public class OpenSSHCertificateHandler {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let logger = Logger() private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHKeyWriter() private let writer = OpenSSHKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]

View File

@ -4,7 +4,7 @@ import OSLog
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts. /// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
public class PublicKeyFileStoreController { public class PublicKeyFileStoreController {
private let logger = Logger() private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: String private let directory: String
private let keyWriter = OpenSSHKeyWriter() private let keyWriter = OpenSSHKeyWriter()

View File

@ -10,39 +10,39 @@ class AgentTests: XCTestCase {
// MARK: Identity Listing // MARK: Identity Listing
func testEmptyStores() { func testEmptyStores() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
let agent = Agent(storeList: SecretStoreList()) let agent = Agent(storeList: SecretStoreList())
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesEmpty) XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesEmpty)
} }
func testIdentitiesList() { func testIdentitiesList() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list) let agent = Agent(storeList: list)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesMultiple) XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesMultiple)
} }
// MARK: Signatures // MARK: Signatures
func testNoMatchingIdentities() { func testNoMatchingIdentities() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignatureWithNoneMatching) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignatureWithNoneMatching)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list) let agent = Agent(storeList: list)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
// XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) // XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
} }
func testSignature() { func testSignature() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...]) let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...])
_ = requestReader.readNextChunk() _ = requestReader.readNextChunk()
let dataToSign = requestReader.readNextChunk() let dataToSign = requestReader.readNextChunk()
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list) let agent = Agent(storeList: list)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
let outer = OpenSSHReader(data: stubWriter.data[5...]) let outer = OpenSSHReader(data: stubWriter.data[5...])
let payload = outer.readNextChunk() let payload = outer.readNextChunk()
let inner = OpenSSHReader(data: payload) let inner = OpenSSHReader(data: payload)
@ -76,18 +76,18 @@ class AgentTests: XCTestCase {
// MARK: Witness protocol // MARK: Witness protocol
func testWitnessObjectionStopsRequest() { func testWitnessObjectionStopsRequest() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
let witness = StubWitness(speakNow: { _,_ in let witness = StubWitness(speakNow: { _,_ in
return true return true
}, witness: { _, _ in }) }, witness: { _, _ in })
let agent = Agent(storeList: list, witness: witness) let agent = Agent(storeList: list, witness: witness)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
} }
func testWitnessSignature() { func testWitnessSignature() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
var witnessed = false var witnessed = false
@ -97,11 +97,11 @@ class AgentTests: XCTestCase {
witnessed = true witnessed = true
}) })
let agent = Agent(storeList: list, witness: witness) let agent = Agent(storeList: list, witness: witness)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertTrue(witnessed) XCTAssertTrue(witnessed)
} }
func testRequestTracing() { func testRequestTracing() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
var speakNowTrace: SigningRequestProvenance! = nil var speakNowTrace: SigningRequestProvenance! = nil
@ -113,7 +113,7 @@ class AgentTests: XCTestCase {
witnessTrace = trace witnessTrace = trace
}) })
let agent = Agent(storeList: list, witness: witness) let agent = Agent(storeList: list, witness: witness)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(witnessTrace, speakNowTrace) XCTAssertEqual(witnessTrace, speakNowTrace)
XCTAssertEqual(witnessTrace.origin.displayName, "Finder") XCTAssertEqual(witnessTrace.origin.displayName, "Finder")
XCTAssertEqual(witnessTrace.origin.validSignature, true) XCTAssertEqual(witnessTrace.origin.validSignature, true)
@ -122,22 +122,22 @@ class AgentTests: XCTestCase {
// MARK: Exception Handling // MARK: Exception Handling
func testSignatureException() { func testSignatureException() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let store = list.stores.first?.base as! Stub.Store let store = list.stores.first?.base as! Stub.Store
store.shouldThrow = true store.shouldThrow = true
let agent = Agent(storeList: list) let agent = Agent(storeList: list)
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
} }
// MARK: Unsupported // MARK: Unsupported
func testUnhandledAdd() { func testUnhandledAdd() async {
let stubReader = StubFileHandleReader(availableData: Constants.Requests.addIdentity) let stubReader = StubFileHandleReader(availableData: Constants.Requests.addIdentity)
let agent = Agent(storeList: SecretStoreList()) let agent = Agent(storeList: SecretStoreList())
agent.handle(reader: stubReader, writer: stubWriter) await agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
} }

View File

@ -27,9 +27,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return SocketController(path: path) return SocketController(path: path)
}() }()
private var updateSink: AnyCancellable? private var updateSink: AnyCancellable?
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger().debug("SecretAgent finished launching") logger.debug("SecretAgent finished launching")
DispatchQueue.main.async { DispatchQueue.main.async {
self.socketController.handler = self.agent.handle(reader:writer:) self.socketController.handler = self.agent.handle(reader:writer:)
} }

View File

@ -6,8 +6,10 @@ import SecretKit
struct LaunchAgentController { struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install(completion: (() -> Void)? = nil) { func install(completion: (() -> Void)? = nil) {
Logger().debug("Installing agent") logger.debug("Installing agent")
_ = setEnabled(false) _ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but: // This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
@ -20,7 +22,7 @@ struct LaunchAgentController {
} }
func forceLaunch(completion: ((Bool) -> Void)?) { func forceLaunch(completion: ((Bool) -> Void)?) {
Logger().debug("Agent is not running, attempting to force launch") logger.debug("Agent is not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app") let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
config.activates = false config.activates = false
@ -29,9 +31,9 @@ struct LaunchAgentController {
completion?(error == nil) completion?(error == nil)
} }
if let error = error { if let error = error {
Logger().error("Error force launching \(error.localizedDescription)") logger.error("Error force launching \(error.localizedDescription)")
} else { } else {
Logger().debug("Agent force launched") logger.debug("Agent force launched")
} }
} }
} }