new tests
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user