162 lines
5.8 KiB
Swift
162 lines
5.8 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import Logging
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
nonisolated enum RemoteLogging {
|
|
static func hostID(_ id: UUID) -> Logger.Metadata.Value {
|
|
"\(id.uuidString.lowercased())"
|
|
}
|
|
|
|
static func errorCode(_ error: any Error) -> Logger.Metadata.Value {
|
|
if error is CancellationError { return "cancelled" }
|
|
if error is SSHInteractionRequired { return "interaction_required" }
|
|
if let failure = error as? WebAuthnSSH.Failure {
|
|
switch failure {
|
|
case .invalidCredential: return "security_key_invalid_reference"
|
|
case .invalidResponse: return "security_key_invalid_response"
|
|
case .unsupportedResponse: return "security_key_unsupported_response"
|
|
}
|
|
}
|
|
guard let error = error as? TermindSSHError else { return "unexpected" }
|
|
switch error {
|
|
case .socket: return "socket"
|
|
case .proxy: return "proxy"
|
|
case .proxyAuthentication: return "proxy_authentication"
|
|
case .route: return "route"
|
|
case .routeHost(_, let authentication, _, let hostKey, _):
|
|
if authentication { return "route_authentication" }
|
|
if hostKey { return "route_host_key" }
|
|
return "route"
|
|
case .library: return "library"
|
|
case .handshake(let failure): return "handshake_\(failure.kind.rawValue)"
|
|
case .authenticationFailed: return "authentication_failed"
|
|
case .hostKeyChanged: return "host_key_changed"
|
|
case .missingCredential: return "missing_credential"
|
|
case .keyPassphraseRequired: return "passphrase_required"
|
|
case .missingUsername: return "missing_username"
|
|
case .untrustedHost: return "untrusted_host"
|
|
case .mismatchedHostKey: return "mismatched_host_key"
|
|
case .disconnected: return "disconnected"
|
|
}
|
|
}
|
|
|
|
static func forwardingKind(_ kind: PortForwardingRule.Kind) -> Logger.Metadata.Value {
|
|
switch kind {
|
|
case .local: "local"
|
|
case .remote: "remote"
|
|
case .dynamic: "dynamic"
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated struct RemoteShellIntegrationLifecycleLogger: Sendable {
|
|
private let logger: Logger
|
|
private var operationID: String?
|
|
|
|
init(logger: Logger) {
|
|
self.logger = logger
|
|
}
|
|
|
|
mutating func begin(hostID: UUID, enabled: Bool) {
|
|
let operationID = UUID().uuidString.lowercased()
|
|
if enabled {
|
|
self.operationID = operationID
|
|
logger.info(
|
|
"ssh.shell_integration.started",
|
|
metadata: metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "probing", outcome: "pending"
|
|
)
|
|
)
|
|
} else {
|
|
logger.info(
|
|
"ssh.shell_integration.skipped",
|
|
metadata: metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "configuration", outcome: "skipped",
|
|
reason: "disabled"
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
mutating func record(
|
|
_ diagnostic: RemoteShellIntegrationBootstrap.Diagnostic,
|
|
hostID: UUID
|
|
) {
|
|
guard let operationID else { return }
|
|
switch diagnostic {
|
|
case .probeDelayed:
|
|
logger.warning(
|
|
"ssh.shell_integration.delayed",
|
|
metadata: metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "probing", outcome: "pending",
|
|
reason: "probe_timeout"
|
|
)
|
|
)
|
|
case .installed(let late, let homeResolved, let systemInfoAvailable):
|
|
var fields = metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "completed", outcome: "success"
|
|
)
|
|
fields["home_resolution"] = homeResolved ? "resolved" : "unavailable"
|
|
fields["late"] = "\(late)"
|
|
fields["system_info"] = systemInfoAvailable ? "available" : "unavailable"
|
|
logger.info("ssh.shell_integration.completed", metadata: fields)
|
|
self.operationID = nil
|
|
case .skipped(let reason):
|
|
logger.info(
|
|
"ssh.shell_integration.skipped",
|
|
metadata: metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "completed", outcome: "skipped",
|
|
reason: reason.rawValue
|
|
)
|
|
)
|
|
self.operationID = nil
|
|
case .timedOut(let stage, let late):
|
|
var fields = metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: stage.rawValue, outcome: "failure",
|
|
reason: "timeout"
|
|
)
|
|
fields["late"] = "\(late)"
|
|
logger.warning("ssh.shell_integration.timed_out", metadata: fields)
|
|
self.operationID = nil
|
|
}
|
|
}
|
|
|
|
mutating func sessionEnded(hostID: UUID) {
|
|
guard let operationID else { return }
|
|
logger.info(
|
|
"ssh.shell_integration.skipped",
|
|
metadata: metadata(
|
|
hostID: hostID, operationID: operationID,
|
|
stage: "completed", outcome: "skipped",
|
|
reason: "session_ended"
|
|
)
|
|
)
|
|
self.operationID = nil
|
|
}
|
|
|
|
private func metadata(
|
|
hostID: UUID, operationID: String, stage: String,
|
|
outcome: String, reason: String? = nil
|
|
) -> Logger.Metadata {
|
|
var fields: Logger.Metadata = [
|
|
"host_id": RemoteLogging.hostID(hostID),
|
|
"operation_id": "\(operationID)",
|
|
"outcome": "\(outcome)",
|
|
"protocol": "ssh",
|
|
"stage": "\(stage)",
|
|
]
|
|
if let reason { fields["reason"] = "\(reason)" }
|
|
return fields
|
|
}
|
|
}
|