diff --git a/Sources/SecureXPC/SecureXPC.docc/SecureXPC.md b/Sources/SecureXPC/SecureXPC.docc/SecureXPC.md index 4040192..199dfab 100644 --- a/Sources/SecureXPC/SecureXPC.docc/SecureXPC.md +++ b/Sources/SecureXPC/SecureXPC.docc/SecureXPC.md @@ -91,4 +91,5 @@ See ``XPCClient`` for more on how to retrieve a client and send requests. - ``SequentialResultProvider`` ### Other +- ``XPCFileDescriptorContainer`` - ``XPCRequestContext`` diff --git a/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCDecoderImpl.swift b/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCDecoderImpl.swift index b31e772..3c2313f 100644 --- a/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCDecoderImpl.swift +++ b/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCDecoderImpl.swift @@ -37,9 +37,15 @@ internal class XPCDecoderImpl: Decoder { userInfo: self.userInfo) } - func singleValueContainer() throws -> SingleValueDecodingContainer { + // Internal implementation so that XPC-specific functions of the container can be accessed + func xpcSingleValueContainer() -> XPCSingleValueDecodingContainer { XPCSingleValueDecodingContainer(value: self.value, codingPath: self.codingPath, userInfo: self.userInfo) + } + + + func singleValueContainer() throws -> SingleValueDecodingContainer { + xpcSingleValueContainer() } } diff --git a/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCSingleValueDecodingContainer.swift b/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCSingleValueDecodingContainer.swift index e933ca7..7476b07 100644 --- a/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCSingleValueDecodingContainer.swift +++ b/Sources/SecureXPC/XPC Coders/XPCDecoder/XPCSingleValueDecodingContainer.swift @@ -102,4 +102,10 @@ internal class XPCSingleValueDecodingContainer: SingleValueDecodingContainer { codingPath: self.codingPath, userInfo: self.userInfo)) } + + // MARK: XPC specific decoding + + func accessAsEncodedValue(xpcType: xpc_type_t) throws -> xpc_object_t { + return try decode(xpcType: xpcType, transform: {$0}) + } } diff --git a/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCEncoderImpl.swift b/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCEncoderImpl.swift index aee0df7..003fa91 100644 --- a/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCEncoderImpl.swift +++ b/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCEncoderImpl.swift @@ -40,13 +40,18 @@ internal class XPCEncoderImpl: Encoder, XPCContainer { return container } + + // Internal implementation so that XPC-specific functions of the container can be accessed + func xpcSingleValueContainer() -> XPCSingleValueEncodingContainer { + let container = XPCSingleValueEncodingContainer(codingPath: self.codingPath) + self.container = container - func singleValueContainer() -> SingleValueEncodingContainer { - let container = XPCSingleValueEncodingContainer(codingPath: self.codingPath) - self.container = container + return container + } - return container - } + func singleValueContainer() -> SingleValueEncodingContainer { + return xpcSingleValueContainer() + } internal func encodedValue() throws -> xpc_object_t? { return try container?.encodedValue() diff --git a/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCSingleValueEncodingContainer.swift b/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCSingleValueEncodingContainer.swift index 603acda..d632dab 100644 --- a/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCSingleValueEncodingContainer.swift +++ b/Sources/SecureXPC/XPC Coders/XPCEncoder/XPCSingleValueEncodingContainer.swift @@ -100,4 +100,10 @@ internal class XPCSingleValueEncodingContainer: SingleValueEncodingContainer, XP try value.encode(to: encoder) } + + // MARK: XPC specific encoding + + func setAlreadyEncodedValue(_ value: xpc_object_t) { + self.setValue(value) + } } diff --git a/Sources/SecureXPC/XPCCommon.swift b/Sources/SecureXPC/XPCCommon.swift index e7ee1d1..b583a0c 100644 --- a/Sources/SecureXPC/XPCCommon.swift +++ b/Sources/SecureXPC/XPCCommon.swift @@ -20,6 +20,15 @@ func const(_ input: UnsafePointer!) -> UnsafePointer! { return UnsafePointer(mutableCopy) // The result should never actually be mutated } +/// Thrown by a "fake" `Codable` instance such as ``XPCServerEndpoint`` or ``XPCFileDescriptorContainer`` which are only capable of being +/// encoded or decoded by the XPC coders, not an arbitrary coder. +/// +/// This error is intentionally internal to the framework as we don't want API users to be trying to explicitly handle this specific case. +enum XPCCoderError: Error { + case onlyDecodableBySecureXPCFramework + case onlyEncodableBySecureXPCFramework +} + /// Determines the `SecCode` corresponding to an XPC connection and/or message. /// /// Uses undocumented functionality prior to macOS 11. diff --git a/Sources/SecureXPC/XPCFileDescriptorContainer.swift b/Sources/SecureXPC/XPCFileDescriptorContainer.swift new file mode 100644 index 0000000..41091b5 --- /dev/null +++ b/Sources/SecureXPC/XPCFileDescriptorContainer.swift @@ -0,0 +1,162 @@ +// +// XPCFileDescriptorContainer.swift +// SecureXPC +// +// Created by Josh Kaplan on 2022-04-12 +// + +import Foundation +import System + +/// A container for a file descriptor which can be sent over an XPC connection. +/// +/// Any initializer may be used in combination with any `duplicate` function. For example, it is valid to create a container using +/// ``init(descriptor:closeDescriptor:)-5b3wo``, send it over an XPC connection, and then on the receiving side call +/// ``duplicateAsFileHandle()``. +/// +/// > Warning: While ``XPCFileDescriptorContainer`` conforms to `Codable` it can only be encoded and decoded by the `SecureXPC` framework. +/// +/// ## Topics +/// ### Creating a Container +/// - ``init(descriptor:closeDescriptor:)-5b3wo`` +/// - ``init(handle:closeHandle:)`` +/// - ``init(descriptor:closeDescriptor:)-8kiiv`` +/// +/// ### Duplicating +/// - ``duplicateAsNativeDescriptor()`` +/// - ``duplicateAsFileHandle()`` +/// - ``duplicateAsFileDescriptor()`` +public struct XPCFileDescriptorContainer { + + /// Errors related to boxing or duplicating file descriptors. + public enum XPCFileDescriptorContainerError: Error { + /// The provided value is not a valid file descriptor, including because it has already been closed. + case invalidFileDescriptor + } + + private let xpcEncodedForm: xpc_object_t + + /// Boxes the provided native file descriptor. + /// + /// - Parameters: + /// - descriptor: The descriptor to be boxed. + /// - closeDescriptor: If set to `true`, `descriptor` will be closed if this initializer succesfully completes. + public init(descriptor: Int32, closeDescriptor: Bool) throws { + guard let xpcEncodedForm = xpc_fd_create(descriptor) else { + throw XPCFileDescriptorContainerError.invalidFileDescriptor + } + self.xpcEncodedForm = xpcEncodedForm + + if closeDescriptor { + close(descriptor) + } + } + + /// Boxes the provided [`FileHandle`](https://developer.apple.com/documentation/foundation/filehandle). + /// + /// - Parameters: + /// - handle: The file handle to be boxed. + /// - closeHandle: If set to `true`, `handler` will be closed if this initializer succesfully completes. + public init(handle: FileHandle, closeHandle: Bool) throws { + guard let xpcEncodedForm = xpc_fd_create(handle.fileDescriptor) else { + throw XPCFileDescriptorContainerError.invalidFileDescriptor + } + self.xpcEncodedForm = xpcEncodedForm + + if closeHandle { + if #available(macOS 10.15, *) { + try handle.close() + } else { + handle.closeFile() + } + } + } + + /// Boxes the provided [`FileDescriptor`](https://developer.apple.com/documentation/system/filedescriptor). + /// + /// - Parameters: + /// - descriptor: The descriptor to be boxed. + /// - closeDescriptor: If set to `true`, `descriptor` will be closed if this initializer succesfully completes. + @available(macOS 11.0, *) + public init(descriptor: FileDescriptor, closeDescriptor: Bool) throws { + guard let xpcEncodedForm = xpc_fd_create(descriptor.rawValue) else { + throw XPCFileDescriptorContainerError.invalidFileDescriptor + } + self.xpcEncodedForm = xpcEncodedForm + + if closeDescriptor { + try descriptor.close() + } + } + + /// Returns a file descriptor equivalent to the one originally boxed. + /// + /// This function may be called multiple times, but each time it will return a different file descriptor. The returned descriptors will be equivalent, as though they + /// had been created by + /// [dup(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/dup2.2.html). + /// + /// > Important: The caller is responsible for calling + /// [close(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/close.2.html#//apple_ref/doc/man/2/close) + /// on the returned descriptor. + public func duplicateAsNativeDescriptor() throws -> Int32 { + let fd = xpc_fd_dup(self.xpcEncodedForm) + // From xpc_fd_dup documentation: + // If the descriptor could not be created or if the given object was not an XPC file descriptor, -1 is + // returned. + if fd == -1 { + throw XPCFileDescriptorContainerError.invalidFileDescriptor + } + + return fd + } + + /// Returns a [`FileHandle`](https://developer.apple.com/documentation/foundation/filehandle) equivalent to the one originally boxed. + /// + /// This function may be called multiple times, but each time it will return a different file handle that is equivalent. + /// + /// > Important: The caller is responsible for calling + /// [`close()`](https://developer.apple.com/documentation/foundation/filehandle/3172525-close) or + /// [`closeFile()`](https://developer.apple.com/documentation/foundation/filehandle/1413393-closefile). + public func duplicateAsFileHandle() throws -> FileHandle { + FileHandle(fileDescriptor: try duplicateAsNativeDescriptor()) + } + + /// Returns a [`FileDescriptor`](https://developer.apple.com/documentation/system/filedescriptor) equivalent to the one originally + /// boxed. + /// + /// This function may be called multiple times, but each time it will return a different file descriptor. The returned descriptors will be equivalent, as though they + /// had been created by + /// [`duplicate(as:retryoninterrupt:)`](https://developer.apple.com/documentation/system/filedescriptor/duplicate(as:retryoninterrupt:)). + /// + /// > Important: The caller is responsible for calling + /// [`close()`](https://developer.apple.com/documentation/system/filedescriptor/close()) or + /// [`closeAfter(_:)`](https://developer.apple.com/documentation/system/filedescriptor/closeafter(_:)) on the returned descriptor. + @available(macOS 11.0, *) + public func duplicateAsFileDescriptor() throws -> FileDescriptor { + FileDescriptor(rawValue: try duplicateAsNativeDescriptor()) + } +} + +// MARK: Codable + +extension XPCFileDescriptorContainer: Encodable { + public func encode(to encoder: Encoder) throws { + guard let xpcEncoder = encoder as? XPCEncoderImpl else { + throw XPCCoderError.onlyEncodableBySecureXPCFramework + } + + let container = xpcEncoder.xpcSingleValueContainer() + container.setAlreadyEncodedValue(self.xpcEncodedForm) + } +} + +extension XPCFileDescriptorContainer: Decodable { + public init(from decoder: Decoder) throws { + guard let xpcDecoder = decoder as? XPCDecoderImpl else { + throw XPCCoderError.onlyDecodableBySecureXPCFramework + } + + let container = xpcDecoder.xpcSingleValueContainer() + self.xpcEncodedForm = try container.accessAsEncodedValue(xpcType: XPC_TYPE_FD) + } +} diff --git a/Sources/SecureXPC/XPCServerEndpoint.swift b/Sources/SecureXPC/XPCServerEndpoint.swift index 55247af..44c1cc3 100644 --- a/Sources/SecureXPC/XPCServerEndpoint.swift +++ b/Sources/SecureXPC/XPCServerEndpoint.swift @@ -48,7 +48,7 @@ private enum CodingKeys: String, CodingKey { extension XPCServerEndpoint: Encodable { public func encode(to encoder: Encoder) throws { guard let xpcEncoder = encoder as? XPCEncoderImpl else { - throw XPCServerEndpointError.onlyEncodableBySecureXPCFramework + throw XPCCoderError.onlyEncodableBySecureXPCFramework } let container = xpcEncoder.xpcContainer(keyedBy: CodingKeys.self) @@ -60,7 +60,7 @@ extension XPCServerEndpoint: Encodable { extension XPCServerEndpoint: Decodable { public init(from decoder: Decoder) throws { guard let xpcDecoder = decoder as? XPCDecoderImpl else { - throw XPCServerEndpointError.onlyDecodableBySecureXPCFramework + throw XPCCoderError.onlyDecodableBySecureXPCFramework } let container = try xpcDecoder.xpcContainer(keyedBy: CodingKeys.self) @@ -68,8 +68,3 @@ extension XPCServerEndpoint: Decodable { self.serviceDescriptor = try container.decode(XPCServiceDescriptor.self, forKey: CodingKeys.serviceDescriptor) } } - -private enum XPCServerEndpointError: Error { - case onlyDecodableBySecureXPCFramework - case onlyEncodableBySecureXPCFramework -} diff --git a/Tests/SecureXPCTests/Encoder & Decoder/Round Trip/XPCFileDescriptorContainer Tests.swift b/Tests/SecureXPCTests/Encoder & Decoder/Round Trip/XPCFileDescriptorContainer Tests.swift new file mode 100644 index 0000000..6683712 --- /dev/null +++ b/Tests/SecureXPCTests/Encoder & Decoder/Round Trip/XPCFileDescriptorContainer Tests.swift @@ -0,0 +1,186 @@ +// +// XPCFileDescriptorContainer Tests.swift +// SecurXPC +// +// Created by Josh Kaplan on 2022-04-12 +// + +import XCTest +import System +@testable import SecureXPC + +final class XPCFileDescriptorContainerTests: XCTestCase { + + // MARK: helper functions + + func currentPath(filePath: String = #filePath) -> String { filePath } + + func pathForFileDescriptor(fileDescriptor: Int32) -> String { + var buffer = [CChar](repeating: 0, count: Int(PATH_MAX)) + _ = fcntl(fileDescriptor, F_GETPATH, &buffer) + + return String(cString: buffer) + } + + // MARK: tests + + func testNativeFD_NativeFD() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: open(self.currentPath(), O_RDONLY), closeDescriptor: true) + } + server.start() + + let descriptorContainer = try await client.send(to: route) + let nativeDescriptor = try descriptorContainer.duplicateAsNativeDescriptor() + defer { close(nativeDescriptor) } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: nativeDescriptor), currentPath()) + } + + func testNativeFD_FileHandle() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: open(self.currentPath(), O_RDONLY), closeDescriptor: true) + } + server.start() + + let descriptorContainer = try await client.send(to: route) + let handle = try descriptorContainer.duplicateAsFileHandle() + defer { try! handle.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: handle.fileDescriptor), currentPath()) + } + + func testNativeFD_FileDescriptor() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: open(self.currentPath(), O_RDONLY), closeDescriptor: true) + } + server.start() + + let descriptorContainer = try await client.send(to: route) + let descriptor = try descriptorContainer.duplicateAsFileDescriptor() + defer { try! descriptor.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: descriptor.rawValue), currentPath()) + } + + func testFileHandle_NativeFD() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(handle: FileHandle(forReadingAtPath: self.currentPath())!, + closeHandle: true) + } + server.start() + + let container = try await client.send(to: route) + let nativeDescriptor = try container.duplicateAsNativeDescriptor() + defer { close(nativeDescriptor) } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: nativeDescriptor), currentPath()) + } + + func testFileHandle_FileHandle() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(handle: FileHandle(forReadingAtPath: self.currentPath())!, + closeHandle: true) + } + server.start() + + let container = try await client.send(to: route) + let handle = try container.duplicateAsFileHandle() + defer { try! handle.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: handle.fileDescriptor), currentPath()) + } + + func testFileHandle_FileDescriptor() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(handle: FileHandle(forReadingAtPath: self.currentPath())!, + closeHandle: true) + } + server.start() + + let container = try await client.send(to: route) + let descriptor = try container.duplicateAsFileDescriptor() + defer { try! descriptor.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: descriptor.rawValue), currentPath()) + } + + func testFileDescriptor_NativeFD() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: FileDescriptor(rawValue: open(self.currentPath(), O_RDONLY)), + closeDescriptor: true) + } + server.start() + + let container = try await client.send(to: route) + let nativeDescriptor = try container.duplicateAsNativeDescriptor() + defer { close(nativeDescriptor) } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: nativeDescriptor), currentPath()) + } + + func testFileDescriptor_FileHandle() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: FileDescriptor(rawValue: open(self.currentPath(), O_RDONLY)), + closeDescriptor: true) + } + server.start() + + let container = try await client.send(to: route) + let handle = try container.duplicateAsFileHandle() + defer { try! handle.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: handle.fileDescriptor), currentPath()) + } + + func testFileDescriptor_FileDescriptor() async throws { + let server = XPCServer.makeAnonymous() + let client = XPCClient.forEndpoint(server.endpoint) + let route = XPCRoute.named("fd", "provider") + .withReplyType(XPCFileDescriptorContainer.self) + server.registerRoute(route) { + try XPCFileDescriptorContainer(descriptor: FileDescriptor(rawValue: open(self.currentPath(), O_RDONLY)), + closeDescriptor: true) + } + server.start() + + let container = try await client.send(to: route) + let descriptor = try container.duplicateAsFileDescriptor() + defer { try! descriptor.close() } + + XCTAssertEqual(pathForFileDescriptor(fileDescriptor: descriptor.rawValue), currentPath()) + } +} +