mirror of
https://github.com/apple/container.git
synced 2026-07-12 20:47:04 +00:00
1794afc449
- Part of #1318. - Part of #1378. - `RouteHandler` now accepts an additional `XPCServerSession` which include a disconnect handler. - `XPCServer` configures a session for each client connection and passes it to every route handler. - `XPCServer.route(fn:)` can be used to wrap existing route handlers that don't care about sessions. - `XPCClient` adds functions to support client-side session handling. - Update all existing routes.
289 lines
10 KiB
Swift
289 lines
10 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if os(macOS)
|
|
import CAuditToken
|
|
import ContainerizationError
|
|
import Foundation
|
|
import Logging
|
|
import os
|
|
import Synchronization
|
|
|
|
public struct XPCServer: Sendable {
|
|
public typealias RouteHandler = @Sendable (XPCMessage, XPCServerSession) async throws -> XPCMessage
|
|
|
|
/// Wraps a session-unaware handler for use in a route table.
|
|
public static func route(
|
|
_ fn: @Sendable @escaping (XPCMessage) async throws -> XPCMessage
|
|
) -> RouteHandler {
|
|
{ message, _ in try await fn(message) }
|
|
}
|
|
|
|
private let routes: [String: RouteHandler]
|
|
// Access to `connection` is protected by a lock.
|
|
private nonisolated(unsafe) let connection: xpc_connection_t
|
|
private let lock = NSLock()
|
|
|
|
let log: Logging.Logger
|
|
|
|
public init(identifier: String, routes: [String: RouteHandler], log: Logging.Logger) {
|
|
let connection = xpc_connection_create_mach_service(
|
|
identifier,
|
|
nil,
|
|
UInt64(XPC_CONNECTION_MACH_SERVICE_LISTENER)
|
|
)
|
|
|
|
self.routes = routes
|
|
self.connection = connection
|
|
self.log = log
|
|
}
|
|
|
|
public init(connection: xpc_connection_t, routes: [String: RouteHandler], log: Logging.Logger) {
|
|
self.routes = routes
|
|
self.connection = connection
|
|
self.log = log
|
|
}
|
|
|
|
public func listen() async throws {
|
|
let connections = AsyncStream<xpc_connection_t> { cont in
|
|
lock.withLock {
|
|
xpc_connection_set_event_handler(self.connection) { object in
|
|
switch xpc_get_type(object) {
|
|
case XPC_TYPE_CONNECTION:
|
|
// `object` isn't used concurrently.
|
|
nonisolated(unsafe) let object = object
|
|
cont.yield(object)
|
|
case XPC_TYPE_ERROR:
|
|
if object.connectionError {
|
|
cont.finish()
|
|
}
|
|
default:
|
|
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
defer {
|
|
lock.withLock {
|
|
xpc_connection_cancel(self.connection)
|
|
}
|
|
}
|
|
|
|
lock.withLock {
|
|
xpc_connection_activate(self.connection)
|
|
}
|
|
|
|
try await withThrowingDiscardingTaskGroup { group in
|
|
for await conn in connections {
|
|
// `conn` isn't used concurrently.
|
|
nonisolated(unsafe) let conn = conn
|
|
let added = group.addTaskUnlessCancelled { @Sendable in
|
|
try await self.handleClientConnection(connection: conn)
|
|
xpc_connection_cancel(conn)
|
|
}
|
|
|
|
if !added {
|
|
break
|
|
}
|
|
}
|
|
|
|
group.cancelAll()
|
|
}
|
|
}
|
|
|
|
func handleClientConnection(connection: xpc_connection_t) async throws {
|
|
let replySent = Mutex(false)
|
|
let session = XPCServerSession()
|
|
|
|
let objects = AsyncStream<xpc_object_t> { cont in
|
|
xpc_connection_set_event_handler(connection) { object in
|
|
switch xpc_get_type(object) {
|
|
case XPC_TYPE_DICTIONARY:
|
|
// `object` isn't used concurrently.
|
|
nonisolated(unsafe) let object = object
|
|
cont.yield(object)
|
|
case XPC_TYPE_ERROR:
|
|
if object.connectionError {
|
|
cont.finish()
|
|
}
|
|
if !(replySent.withLock({ $0 }) && object.connectionClosed) {
|
|
// When a xpc connection is closed, the framework sends
|
|
// a final XPC_ERROR_CONNECTION_INVALID message.
|
|
// We can ignore this if we know we have already handled
|
|
// the request.
|
|
self.log.error(
|
|
"xpc client handler connection error",
|
|
metadata: [
|
|
"error": "\(object.errorDescription ?? "no description")"
|
|
])
|
|
}
|
|
default:
|
|
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
|
|
}
|
|
}
|
|
}
|
|
defer {
|
|
xpc_connection_cancel(connection)
|
|
}
|
|
|
|
xpc_connection_activate(connection)
|
|
try await withThrowingDiscardingTaskGroup { group in
|
|
// `connection` isn't used concurrently.
|
|
nonisolated(unsafe) let connection = connection
|
|
for await object in objects {
|
|
// `object` isn't used concurrently.
|
|
nonisolated(unsafe) let object = object
|
|
let added = group.addTaskUnlessCancelled { @Sendable in
|
|
try await self.handleMessage(connection: connection, object: object, session: session)
|
|
replySent.withLock { $0 = true }
|
|
}
|
|
if !added {
|
|
break
|
|
}
|
|
}
|
|
group.cancelAll()
|
|
}
|
|
await session.fireDisconnect()
|
|
}
|
|
|
|
func handleMessage(connection: xpc_connection_t, object: xpc_object_t, session: XPCServerSession) async throws {
|
|
// All requests are dictionary-valued.
|
|
guard xpc_get_type(object) == XPC_TYPE_DICTIONARY else {
|
|
log.error("invalid request - not a dictionary")
|
|
Self.replyWithError(
|
|
connection: connection,
|
|
object: object,
|
|
err: ContainerizationError(.invalidArgument, message: "invalid request")
|
|
)
|
|
return
|
|
}
|
|
|
|
// Ensure that the client has our EUID
|
|
var token = audit_token_t()
|
|
xpc_dictionary_get_audit_token(object, &token)
|
|
let serverEuid = geteuid()
|
|
let clientEuid = audit_token_to_euid(token)
|
|
guard clientEuid == serverEuid else {
|
|
log.error(
|
|
"unauthorized request - uid mismatch",
|
|
metadata: [
|
|
"server_euid": "\(serverEuid)",
|
|
"client_euid": "\(clientEuid)",
|
|
])
|
|
Self.replyWithError(
|
|
connection: connection,
|
|
object: object,
|
|
err: ContainerizationError(.invalidState, message: "unauthorized request")
|
|
)
|
|
return
|
|
}
|
|
|
|
guard let route = object.route else {
|
|
log.error("invalid request - empty route")
|
|
Self.replyWithError(
|
|
connection: connection,
|
|
object: object,
|
|
err: ContainerizationError(.invalidArgument, message: "invalid request")
|
|
)
|
|
return
|
|
}
|
|
|
|
if let handler = routes[route] {
|
|
do {
|
|
let message = XPCMessage(object: object)
|
|
let response = try await handler(message, session)
|
|
xpc_connection_send_message(connection, response.underlying)
|
|
} catch let error as ContainerizationError {
|
|
log.error(
|
|
"route handler threw an error",
|
|
metadata: [
|
|
"route": "\(route)",
|
|
"error": "\(error)",
|
|
])
|
|
Self.replyWithError(
|
|
connection: connection,
|
|
object: object,
|
|
err: error
|
|
)
|
|
} catch {
|
|
log.error(
|
|
"route handler threw an error",
|
|
metadata: [
|
|
"route": "\(route)",
|
|
"error": "\(error)",
|
|
])
|
|
let message = XPCMessage(object: object)
|
|
let reply = message.reply()
|
|
|
|
// Check if this is a VolumeError by looking at the error description
|
|
let errorMessage = error.localizedDescription
|
|
let errorTypeString = String(describing: type(of: error))
|
|
if errorTypeString.contains("VolumeError") || errorMessage.contains("Volume") {
|
|
let err = ContainerizationError(.invalidArgument, message: errorMessage)
|
|
reply.set(error: err)
|
|
} else {
|
|
let err = ContainerizationError(.unknown, message: String(describing: error))
|
|
reply.set(error: err)
|
|
}
|
|
xpc_connection_send_message(connection, reply.underlying)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func replyWithError(connection: xpc_connection_t, object: xpc_object_t, err: ContainerizationError) {
|
|
let message = XPCMessage(object: object)
|
|
let reply = message.reply()
|
|
reply.set(error: err)
|
|
xpc_connection_send_message(connection, reply.underlying)
|
|
}
|
|
}
|
|
|
|
extension xpc_object_t {
|
|
var route: String? {
|
|
let croute = xpc_dictionary_get_string(self, XPCMessage.routeKey)
|
|
guard let croute else {
|
|
return nil
|
|
}
|
|
return String(cString: croute)
|
|
}
|
|
|
|
var connectionError: Bool {
|
|
precondition(isError, "not an error")
|
|
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID) || xpc_equal(self, XPC_ERROR_CONNECTION_INTERRUPTED)
|
|
}
|
|
|
|
var connectionClosed: Bool {
|
|
precondition(isError, "not an error")
|
|
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID)
|
|
}
|
|
|
|
var isError: Bool {
|
|
xpc_get_type(self) == XPC_TYPE_ERROR
|
|
}
|
|
|
|
var errorDescription: String? {
|
|
precondition(isError, "not an error")
|
|
let cstring = xpc_dictionary_get_string(self, XPC_ERROR_KEY_DESCRIPTION)
|
|
guard let cstring else {
|
|
return nil
|
|
}
|
|
return String(cString: cstring)
|
|
}
|
|
}
|
|
|
|
#endif
|