Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclusively path based routes - closes #20 #28

Merged
merged 4 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Sources/SecureXPC/Routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import Foundation
/// Consistent framework internal implementation of routes that can be sent over XPC (because its Codable) and used as a dictionary key (because its Hashable).
struct XPCRoute: Codable, Hashable {
let pathComponents: [String]

// These are intentionally excluded when computing equality and hash values as routes are uniqued only on path
let messageType: String?
let replyType: String?

init(pathComponents: [String], messageType: Any.Type?, replyType: Any.Type?) {
fileprivate init(pathComponents: [String], messageType: Any.Type?, replyType: Any.Type?) {
self.pathComponents = pathComponents

if let messageType = messageType {
Expand All @@ -28,8 +30,17 @@ struct XPCRoute: Codable, Hashable {
self.replyType = nil
}
}

public func hash(into hasher: inout Hasher) {
hasher.combine(pathComponents)
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.pathComponents == rhs.pathComponents
}
}


/// A route that can't receive a message and is expected to reply.
public struct XPCRouteWithoutMessageWithReply<R: Codable> {
let route: XPCRoute
Expand Down
177 changes: 96 additions & 81 deletions Sources/SecureXPC/Server/XPCServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
/// #### XPC Mach services
///
/// Launch Agents, Launch Daemons, and helper tools installed with
/// [ `SMJobBless`](https://developer.apple.com/documentation/servicemanagement/1431078-smjobbless) can optionally communicate
/// [`SMJobBless`](https://developer.apple.com/documentation/servicemanagement/1431078-smjobbless) can optionally communicate
/// over XPC by using Mach services.
///
/// In most cases, a server can be auto-configured for a helper tool installed with `SMJobBless`:
Expand Down Expand Up @@ -172,61 +172,66 @@ public class XPCServer {
public var errorHandler: ((XPCError) -> Void)?

// Routes
private var routesWithoutMessageWithReply = [XPCRoute : XPCHandlerWithoutMessageWithReply]()
private var routesWithMessageWithReply = [XPCRoute : XPCHandlerWithMessageWithReply]()
private var routesWithoutMessageWithoutReply = [XPCRoute : XPCHandlerWithoutMessageWithoutReply]()
private var routesWithMessageWithoutReply = [XPCRoute : XPCHandlerWithMessageWithoutReply]()
private var routes = [XPCRoute : XPCHandler]()

/// Registers a route that has no message and can't receive a reply.
///
/// If this route has already been registered, calling this function will overwrite the existing registration. Routes are unique based on their paths and types.
///
/// - Parameters:
/// - route: A route that has no message and can't receive a reply.
/// - handler: Will be called when the server receives an incoming request for this route if the request is accepted.
/// - Throws: If this route has already been registered.
public func registerRoute(_ route: XPCRouteWithoutMessageWithoutReply,
handler: @escaping () throws -> Void) {
let handlerWrapper = ConstrainedXPCHandlerWithoutMessageWithoutReply(handler: handler)
self.routesWithoutMessageWithoutReply[route.route] = handlerWrapper
handler: @escaping () throws -> Void) throws {
if self.routes.keys.contains(route.route) {
throw XPCError.routeAlreadyRegistered(route.route.pathComponents)
}

self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithoutReply(handler: handler)
}

/// Registers a route that has a message and can't receive a reply.
///
/// If this route has already been registered, calling this function will overwrite the existing registration. Routes are unique based on their paths and types.
///
/// - Parameters:
/// - route: A route that has a message and can't receive a reply.
/// - handler: Will be called when the server receives an incoming request for this route if the request is accepted.
/// - Throws: If this route has already been registered.
public func registerRoute<M: Decodable>(_ route: XPCRouteWithMessageWithoutReply<M>,
handler: @escaping (M) throws -> Void) {
let handlerWrapper = ConstrainedXPCHandlerWithMessageWithoutReply(handler: handler)
self.routesWithMessageWithoutReply[route.route] = handlerWrapper
handler: @escaping (M) throws -> Void) throws {
if self.routes.keys.contains(route.route) {
throw XPCError.routeAlreadyRegistered(route.route.pathComponents)
}

self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithoutReply(handler: handler)
}

/// Registers a route that has no message and expects a reply.
///
/// If this route has already been registered, calling this function will overwrite the existing registration. Routes are unique based on their paths and types.
///
/// - Parameters:
/// - route: A route that has no message and expects a reply.
/// - handler: Will be called when the server receives an incoming request for this route if the request is accepted.
/// - Throws: If this route has already been registered.
public func registerRoute<R: Decodable>(_ route: XPCRouteWithoutMessageWithReply<R>,
handler: @escaping () throws -> R) {
let handlerWrapper = ConstrainedXPCHandlerWithoutMessageWithReply(handler: handler)
self.routesWithoutMessageWithReply[route.route] = handlerWrapper
handler: @escaping () throws -> R) throws {
if self.routes.keys.contains(route.route) {
throw XPCError.routeAlreadyRegistered(route.route.pathComponents)
}

self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithReply(handler: handler)
}

/// Registers a route that has a message and expects a reply.
///
/// If this route has already been registered, calling this function will overwrite the existing registration. Routes are unique based on their paths and types.
///
/// - Parameters:
/// - route: A route that has a message and expects a reply.
/// - handler: Will be called when the server receives an incoming request for this route if the request is accepted.
/// - Throws: If this route has already been registered.
public func registerRoute<M: Decodable, R: Encodable>(_ route: XPCRouteWithMessageWithReply<M, R>,
handler: @escaping (M) throws -> R) {
let handlerWrapper = ConstrainedXPCHandlerWithMessageWithReply(handler: handler)
self.routesWithMessageWithReply[route.route] = handlerWrapper
handler: @escaping (M) throws -> R) throws {
if self.routes.keys.contains(route.route) {
throw XPCError.routeAlreadyRegistered(route.route.pathComponents)
}

self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithReply(handler: handler)
}

internal func handleEvent(connection: xpc_connection_t, event: xpc_object_t) {
Expand Down Expand Up @@ -267,38 +272,14 @@ public class XPCServer {

private func handleMessage(connection: xpc_connection_t, message: xpc_object_t, reply: inout xpc_object_t?) throws {
let request = try Request(dictionary: message)

// If a dictionary reply exists, then the message expects a reply
if var reply = reply {
if request.containsPayload {
if let handler = self.routesWithMessageWithReply[request.route] {
try handler.handle(request: request, reply: &reply)
xpc_connection_send_message(connection, reply)
} else {
throw XPCError.routeNotRegistered(String(describing: request.route))
}
} else {
if let handler = self.routesWithoutMessageWithReply[request.route] {
try handler.handle(reply: &reply)
xpc_connection_send_message(connection, reply)
} else {
throw XPCError.routeNotRegistered(String(describing: request.route))
}
}
} else { // Otherwise the message can't receive a reply
if request.containsPayload {
if let handler = self.routesWithMessageWithoutReply[request.route] {
try handler.handle(request: request)
} else {
throw XPCError.routeNotRegistered(String(describing: request.route))
}
} else {
if let handler = self.routesWithoutMessageWithoutReply[request.route] {
try handler.handle()
} else {
throw XPCError.routeNotRegistered(String(describing: request.route))
}
}
guard let handler = self.routes[request.route] else {
throw XPCError.routeNotRegistered(request.route.pathComponents)
}
try handler.handle(request: request, reply: &reply)

// If a dictionary reply exists, then the message expects a reply to be sent back
if let reply = reply {
xpc_connection_send_message(connection, reply)
}
}

Expand Down Expand Up @@ -340,54 +321,88 @@ public class XPCServer {
// These wrappers perform type erasure via their implemented protocols while internally maintaining type constraints
// This makes it possible to create heterogenous collections of them

fileprivate protocol XPCHandlerWithoutMessageWithoutReply {
func handle() throws -> Void
fileprivate protocol XPCHandler {
func handle(request: Request, reply: inout xpc_object_t?) throws
}

fileprivate struct ConstrainedXPCHandlerWithoutMessageWithoutReply: XPCHandlerWithoutMessageWithoutReply {
let handler: () throws -> Void
fileprivate extension XPCHandler {

func handle() throws {
try self.handler()
/// Validates that the incoming request matches the handler in terms of the presence of a message and reply.
///
/// The actual validation of the types themselves is performed as part of encoding/decoding and is intentionally not checked by this function.
///
/// - Parameters:
/// - request: The incoming request.
/// - reply: The XPC reply object, if one exists.
/// - messageType: The parameter type of the registered handler, if applicable.
/// - replyType: The return type of the registered handler, if applicable.
/// - Throws: If the check fails.
func checkMatchesRequest(_ request: Request,
reply: inout xpc_object_t?,
messageType: Any.Type?,
replyType: Any.Type?) throws {
var errorMessages = [String]()

// Message
if messageType == nil, request.containsPayload {
errorMessages.append("Request had a message of type \(String(describing: request.route.messageType)), " +
"but the handler registered with the server does not have a message parameter.")
} else if let messageType = messageType, !request.containsPayload {
errorMessages.append("Request did not contain a message, but the handler registered with the server has " +
"a message parameter of type \(messageType).")
}

// Reply
if replyType == nil, reply != nil {
errorMessages.append("Request expects a reply of type \(String(describing: request.route.replyType)), " +
"but the handler registered with the server has no return value.")
} else if let replyType = replyType, reply == nil {
errorMessages.append("Request does not expect a reply, but the handler registered with the server has a " +
"return value of type \(replyType).")
}

if !errorMessages.isEmpty {
throw XPCError.routeMismatch(request.route.pathComponents, errorMessages.joined(separator: "\n"))
}
}
}

fileprivate protocol XPCHandlerWithMessageWithoutReply {
func handle(request: Request) throws -> Void
fileprivate struct ConstrainedXPCHandlerWithoutMessageWithoutReply: XPCHandler {
let handler: () throws -> Void

func handle(request: Request, reply: inout xpc_object_t?) throws {
try checkMatchesRequest(request, reply: &reply, messageType: nil, replyType: nil)
try self.handler()
}
}

fileprivate struct ConstrainedXPCHandlerWithMessageWithoutReply<M: Decodable>: XPCHandlerWithMessageWithoutReply {
fileprivate struct ConstrainedXPCHandlerWithMessageWithoutReply<M: Decodable>: XPCHandler {
let handler: (M) throws -> Void

func handle(request: Request) throws {
func handle(request: Request, reply: inout xpc_object_t?) throws {
try checkMatchesRequest(request, reply: &reply, messageType: M.self, replyType: nil)
let decodedMessage = try request.decodePayload(asType: M.self)
try self.handler(decodedMessage)
}
}

fileprivate protocol XPCHandlerWithoutMessageWithReply {
func handle(reply: inout xpc_object_t) throws
}

fileprivate struct ConstrainedXPCHandlerWithoutMessageWithReply<R: Encodable>: XPCHandlerWithoutMessageWithReply {
fileprivate struct ConstrainedXPCHandlerWithoutMessageWithReply<R: Encodable>: XPCHandler {
let handler: () throws -> R

func handle(reply: inout xpc_object_t) throws {
func handle(request: Request, reply: inout xpc_object_t?) throws {
try checkMatchesRequest(request, reply: &reply, messageType: nil, replyType: R.self)
let payload = try self.handler()
try Response.encodePayload(payload, intoReply: &reply)
try Response.encodePayload(payload, intoReply: &reply!)
}
}

fileprivate protocol XPCHandlerWithMessageWithReply {
func handle(request: Request, reply: inout xpc_object_t) throws
}

fileprivate struct ConstrainedXPCHandlerWithMessageWithReply<M: Decodable, R: Encodable>: XPCHandlerWithMessageWithReply {
fileprivate struct ConstrainedXPCHandlerWithMessageWithReply<M: Decodable, R: Encodable>: XPCHandler {
let handler: (M) throws -> R

func handle(request: Request, reply: inout xpc_object_t) throws {
func handle(request: Request, reply: inout xpc_object_t?) throws {
try checkMatchesRequest(request, reply: &reply, messageType: M.self, replyType: R.self)
let decodedMessage = try request.decodePayload(asType: M.self)
let payload = try self.handler(decodedMessage)
try Response.encodePayload(payload, intoReply: &reply)
try Response.encodePayload(payload, intoReply: &reply!)
}
}
11 changes: 9 additions & 2 deletions Sources/SecureXPC/XPCError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@ public enum XPCError: Error, Codable {
///
/// The associated value describes this decoding error.
case decodingError(String)
/// The route associated with the incoming XPC request is not registed with the ``XPCServer``.
case routeNotRegistered(String)
/// The route can't be registered because a route with this path already exists.
case routeAlreadyRegistered([String])
/// The route associated with the incoming XPC request is not registered with the ``XPCServer``.
case routeNotRegistered([String])
/// While the route associated with the incoming XPC request is registered with the ``XPCServer``, the message and/or reply does not match the handler
/// registered with the server.
///
/// The first associated value is the route's path components. The second is a descriptive error message.
case routeMismatch([String], String)
/// The caller is not a blessed helper tool or its property list configuration is not compatible with ``XPCServer/forThisBlessedHelperTool()``.
case misconfiguredBlessedHelperTool(String)
/// A server already exists for this named XPC Mach service and therefore another server can't be returned with different client requirements.
Expand Down