new tests

This commit is contained in:
Mark Gregory
2026-10-19 10:40:04 +01:00
parent f46f598baf
commit 3307bd90e4
4 changed files with 128 additions and 52 deletions
+47 -2
View File
@@ -10,6 +10,49 @@ struct MySQLWireError: LocalizedError {
var errorDescription: String? { message }
}
// SSH transport uses the system OpenSSH client and key-based authentication.
// No remote MySQL password is passed to ssh. SSH host-key verification remains enabled.
private final class DBSynSSHTunnels {
static let shared = DBSynSSHTunnels()
private let lock = NSLock()
private var running: [UUID: (Process, ConnectionProfile, Int)] = [:]
func endpoint(for profile: ConnectionProfile) throws -> (String, Int) {
guard profile.useSSHTunnel else { return (profile.host, profile.port) }
lock.lock(); defer { lock.unlock() }
if let current = running[profile.id] {
if current.0.isRunning && current.1 == profile { return ("127.0.0.1", current.2) }
if current.0.isRunning { current.0.terminate() }
running.removeValue(forKey: profile.id)
}
guard !profile.sshHost.isEmpty, !profile.sshUser.isEmpty else {
throw MySQLWireError(message: "Configure SSH hostname and SSH username in Connection Settings")
}
// A stable local port per connection; ExitOnForwardFailure rejects conflicts.
let bytes = Array(profile.id.uuidString.utf8)
let checksum = bytes.reduce(0) { ($0 &* 31 &+ Int($1)) % 10000 }
let localPort = 45000 + checksum
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
var args = ["-N", "-T", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "ConnectTimeout=8", "-o", "StrictHostKeyChecking=yes", "-p", String(profile.sshPort)]
if !profile.sshIdentityFile.isEmpty { args += ["-i", (profile.sshIdentityFile as NSString).expandingTildeInPath] }
args += ["-L", "127.0.0.1:\(localPort):\(profile.host):\(profile.port)", "\(profile.sshUser)@\(profile.sshHost)"]
process.arguments = args
process.standardInput = FileHandle.nullDevice
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
do { try process.run() } catch { throw MySQLWireError(message: "Cannot launch SSH: \(error.localizedDescription)") }
Thread.sleep(forTimeInterval: 0.6)
guard process.isRunning else { throw MySQLWireError(message: "SSH tunnel failed. Check SSH key, known_hosts, target and port \(localPort). Try ssh manually for details.") }
running[profile.id] = (process, profile, localPort)
return ("127.0.0.1", localPort)
}
func stop(_ id: UUID) {
lock.lock(); defer { lock.unlock() }
if let existing = running.removeValue(forKey: id), existing.0.isRunning { existing.0.terminate() }
}
}
private struct ByteCursor {
let bytes: [UInt8]
var offset = 0
@@ -371,8 +414,9 @@ private final class PersistentMySQLSessions {
}
if connections[profile.id] == nil {
let connection = try MySQLWireConnection(host: profile.host, port: profile.port)
connection.markHost(profile.host)
let endpoint = try DBSynSSHTunnels.shared.endpoint(for: profile)
let connection = try MySQLWireConnection(host: endpoint.0, port: endpoint.1)
connection.markHost(endpoint.0)
try connection.authenticate(user: profile.username,
password: password, database: database)
connections[profile.id] = Entry(profile: profile,
@@ -415,6 +459,7 @@ private final class PersistentMySQLSessions {
func disconnect(profileID: UUID) {
queue.sync { _ = connections.removeValue(forKey: profileID) }
DBSynSSHTunnels.shared.stop(profileID)
}
}