From 82f9ca028c6e4d8677add9da5578aee560875ceb Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 21 Jun 2026 20:08:58 +0700 Subject: [PATCH] refactor(plugins): move SSL classifiers into PluginKit and drop the test mirror --- .../MSSQLTLSClassifier.swift | 23 +++ .../MSSQLTLSClassifierTests.swift | 50 +++++ .../CassandraConnection.swift | 16 +- .../ClickHousePlugin.swift | 26 +-- .../MSSQLDriverPlugin/FreeTDSConnection.swift | 22 +- .../MongoDBConnection.swift | 38 +--- .../MariaDBPluginConnection.swift | 26 +-- .../OracleDriverPlugin/OracleConnection.swift | 24 +-- .../LibPQPluginConnection.swift | 28 +-- .../RedisPluginConnection.swift | 22 +- .../CassandraClientKeyClassifier.swift | 17 ++ .../ClickHouseSSLClassifier.swift | 27 +++ .../LibPQSSLClassifier.swift | 29 +++ .../MariaDBSSLClassifier.swift | 19 ++ .../MongoDBSSLClassifier.swift | 39 ++++ .../OracleSSLClassifier.swift | 20 ++ .../RedisSSLClassifier.swift | 23 +++ .../PluginSSLClassifiers.swift | 191 ------------------ .../Plugins/PluginSSLClassifierTests.swift | 85 +++----- 19 files changed, 292 insertions(+), 433 deletions(-) create mode 100644 Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLTLSClassifier.swift create mode 100644 Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLTLSClassifierTests.swift create mode 100644 Plugins/TableProPluginKit/CassandraClientKeyClassifier.swift create mode 100644 Plugins/TableProPluginKit/ClickHouseSSLClassifier.swift create mode 100644 Plugins/TableProPluginKit/LibPQSSLClassifier.swift create mode 100644 Plugins/TableProPluginKit/MariaDBSSLClassifier.swift create mode 100644 Plugins/TableProPluginKit/MongoDBSSLClassifier.swift create mode 100644 Plugins/TableProPluginKit/OracleSSLClassifier.swift create mode 100644 Plugins/TableProPluginKit/RedisSSLClassifier.swift delete mode 100644 TableProTests/PluginTestSources/PluginSSLClassifiers.swift diff --git a/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLTLSClassifier.swift b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLTLSClassifier.swift new file mode 100644 index 000000000..b97f31a24 --- /dev/null +++ b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLTLSClassifier.swift @@ -0,0 +1,23 @@ +import Foundation + +public enum MSSQLTLSClassifier { + public static func classifySSLError(_ message: String) -> MSSQLTLSFailureKind? { + let lower = message.lowercased() + if lower.contains("encryption is required") || lower.contains("server requires encryption") { + return .serverRejectedPlaintext + } + if lower.contains("encryption not supported") || lower.contains("server does not support encryption") { + return .serverRequiresPlaintext + } + if lower.contains("certificate verify failed") || lower.contains("certificate is not trusted") { + return .untrustedCertificate + } + if lower.contains("does not match host") { + return .hostnameMismatch + } + if lower.contains("ssl handshake") || lower.contains("tls handshake") || lower.contains("openssl error") { + return .cipherMismatch + } + return nil + } +} diff --git a/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLTLSClassifierTests.swift b/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLTLSClassifierTests.swift new file mode 100644 index 000000000..87fc52349 --- /dev/null +++ b/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLTLSClassifierTests.swift @@ -0,0 +1,50 @@ +import Testing +@testable import TableProMSSQLCore + +@Suite("MSSQL TLS Classifier") +struct MSSQLTLSClassifierTests { + @Test("Server requires encryption → serverRejectedPlaintext") + func testServerRequires() { + guard case .serverRejectedPlaintext = MSSQLTLSClassifier.classifySSLError("Server requires encryption") else { + Issue.record("Expected serverRejectedPlaintext") + return + } + } + + @Test("Server does not support encryption → serverRequiresPlaintext") + func testServerNoSupport() { + guard case .serverRequiresPlaintext = MSSQLTLSClassifier.classifySSLError("encryption not supported by server") else { + Issue.record("Expected serverRequiresPlaintext") + return + } + } + + @Test("Certificate verify failed → untrustedCertificate") + func testUntrustedCertificate() { + guard case .untrustedCertificate = MSSQLTLSClassifier.classifySSLError("certificate verify failed") else { + Issue.record("Expected untrustedCertificate") + return + } + } + + @Test("Hostname mismatch → hostnameMismatch") + func testHostnameMismatch() { + guard case .hostnameMismatch = MSSQLTLSClassifier.classifySSLError("certificate does not match host name") else { + Issue.record("Expected hostnameMismatch") + return + } + } + + @Test("OpenSSL handshake → cipherMismatch") + func testOpenSSL() { + guard case .cipherMismatch = MSSQLTLSClassifier.classifySSLError("OpenSSL error during SSL handshake") else { + Issue.record("Expected cipherMismatch") + return + } + } + + @Test("Non-TLS error returns nil") + func testNonTLS() { + #expect(MSSQLTLSClassifier.classifySSLError("Login failed for user 'sa'") == nil) + } +} diff --git a/Plugins/CassandraDriverPlugin/CassandraConnection.swift b/Plugins/CassandraDriverPlugin/CassandraConnection.swift index 7fdf64d5b..d7800e75d 100644 --- a/Plugins/CassandraDriverPlugin/CassandraConnection.swift +++ b/Plugins/CassandraDriverPlugin/CassandraConnection.swift @@ -197,24 +197,10 @@ actor CassandraConnectionActor { let keyResult = cass_ssl_set_private_key(ssl, keyString, passphrase) if keyResult != CASS_OK { cleanup() - throw Self.privateKeyLoadError(keyPEM: keyString, hasPassphrase: passphrase != nil, keyPath: keyPath) + throw CassandraClientKeyClassifier.privateKeyLoadError(keyPEM: keyString, hasPassphrase: passphrase != nil, keyPath: keyPath) } } - static func isEncryptedPrivateKey(_ pem: String) -> Bool { - pem.contains("ENCRYPTED PRIVATE KEY") || (pem.contains("Proc-Type:") && pem.contains("ENCRYPTED")) - } - - static func privateKeyLoadError(keyPEM: String, hasPassphrase: Bool, keyPath: String) -> SSLHandshakeError { - guard isEncryptedPrivateKey(keyPEM) else { - return .clientKeyInvalid(serverMessage: "The client key at \(keyPath) is not a valid private key") - } - if hasPassphrase { - return .clientKeyPassphraseIncorrect(serverMessage: "The passphrase for the client key at \(keyPath) is incorrect") - } - return .clientKeyPassphraseRequired(serverMessage: "The client key at \(keyPath) is encrypted. Enter its passphrase.") - } - func close() { if let session { let closeFuture = cass_session_close(session) diff --git a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift index 3dac36eaf..772dd89ba 100644 --- a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift +++ b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift @@ -216,7 +216,7 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable { session = nil lock.unlock() Self.logger.error("Connection test failed: \(error.localizedDescription)") - if let sslError = Self.classifySSLError(error) { + if let sslError = ClickHouseSSLClassifier.classifySSLError(error) { throw sslError } throw ClickHouseError.connectionFailed @@ -709,30 +709,6 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable { func generateDropIndexSQL(table: String, indexName: String) -> String? { "ALTER TABLE \(quoteIdentifier(table)) DROP INDEX \(quoteIdentifier(indexName))" } - - static func classifySSLError(_ error: Error) -> SSLHandshakeError? { - let urlError = error as? URLError ?? (error as NSError).underlyingErrors.compactMap { $0 as? URLError }.first - if let urlError { - switch urlError.code { - case .serverCertificateUntrusted, .serverCertificateNotYetValid, .serverCertificateHasUnknownRoot, .serverCertificateHasBadDate: - return .untrustedCertificate(serverMessage: urlError.localizedDescription) - case .clientCertificateRequired, .clientCertificateRejected: - return .clientCertRequired(serverMessage: urlError.localizedDescription) - case .secureConnectionFailed: - return .cipherMismatch(serverMessage: urlError.localizedDescription) - default: - break - } - } - let message = error.localizedDescription.lowercased() - if message.contains("certificate") && (message.contains("untrusted") || message.contains("verify failed")) { - return .untrustedCertificate(serverMessage: error.localizedDescription) - } - if message.contains("hostname") { - return .hostnameMismatch(serverMessage: error.localizedDescription) - } - return nil - } } // MARK: - TLS Delegate diff --git a/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift b/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift index 183d21c6a..cd29197e1 100644 --- a/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift +++ b/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift @@ -169,7 +169,7 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable { guard let proc = dbopen(login, serverName) else { let detail = freetdsGetError(for: nil) let msg = detail.isEmpty ? "Check host, port, credentials, and TLS settings" : detail - if let kind = FreeTDSConnection.classifySSLError(detail) { + if let kind = MSSQLTLSClassifier.classifySSLError(detail) { throw MSSQLCoreError.tlsHandshakeFailed(kind: kind, serverMessage: detail) } throw MSSQLCoreError.connectionFailed("Failed to connect to \(options.host):\(options.port): \(msg)") @@ -526,26 +526,6 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable { } return raw } - - static func classifySSLError(_ message: String) -> MSSQLTLSFailureKind? { - let lower = message.lowercased() - if lower.contains("encryption is required") || lower.contains("server requires encryption") { - return .serverRejectedPlaintext - } - if lower.contains("encryption not supported") || lower.contains("server does not support encryption") { - return .serverRequiresPlaintext - } - if lower.contains("certificate verify failed") || lower.contains("certificate is not trusted") { - return .untrustedCertificate - } - if lower.contains("does not match host") { - return .hostnameMismatch - } - if lower.contains("ssl handshake") || lower.contains("tls handshake") || lower.contains("openssl error") { - return .cipherMismatch - } - return nil - } } private extension MSSQLLoginField { diff --git a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift index f2cf334ed..4fbf3cb2c 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift @@ -300,7 +300,7 @@ final class MongoDBConnection: @unchecked Sendable { let errorMsg = bsonErrorMessage(&error) mongoc_client_destroy(newClient) logger.error("MongoDB ping failed: \(errorMsg)") - if let sslError = Self.classifySSLError(errorMsg) { + if let sslError = MongoDBSSLClassifier.classifySSLError(errorMsg) { throw sslError } throw MongoDBError(code: error.code, message: errorMsg) @@ -805,42 +805,6 @@ extension MongoDBConnection { return nil #endif } - - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("certificate verify failed") || lower.contains("ssl certificate") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("hostname") && lower.contains("verification") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("tls required") || lower.contains("ssl required") { - return .serverRejectedPlaintext(serverMessage: message) - } - if lower.contains("client certificate required") || lower.contains("peer did not return a certificate") { - return .clientCertRequired(serverMessage: message) - } - if isCipherOrProtocolMismatch(lower) { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ssl handshake failed") || lower.contains("tls handshake failed") { - return .unknown(serverMessage: message) - } - return nil - } - - static func isCipherOrProtocolMismatch(_ lower: String) -> Bool { - let signatures = [ - "no shared cipher", - "sslv3 alert handshake failure", - "wrong version number", - "unsupported protocol", - "no protocols available", - "alert protocol version", - "protocol version", - ] - return signatures.contains { lower.contains($0) } - } } // bsonToDict and bsonToJson take bson_t parameters (a CLibMongoc type), diff --git a/Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift b/Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift index b7ca34cb2..98592e76b 100644 --- a/Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift +++ b/Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift @@ -216,30 +216,24 @@ final class MariaDBPluginConnection: @unchecked Sendable { // MARK: - Connection Management - private static let sslOnlyErrorCodes: Set = [ - 2_026, - 2_012, - 1_043 - ] - func connect() async throws { try await pluginDispatchAsync(on: queue) { [self] in let mode = self.sslConfig.mode let handle: UnsafeMutablePointer do { handle = try self.attemptConnect(enforceSSL: mode != .disabled) - } catch let error as MariaDBPluginError where mode == .preferred && Self.sslOnlyErrorCodes.contains(error.code) { + } catch let error as MariaDBPluginError where mode == .preferred && MariaDBSSLClassifier.sslOnlyErrorCodes.contains(error.code) { logger.notice("MySQL SSL handshake failed (code \(error.code)); falling back to plaintext for .preferred mode") do { handle = try self.attemptConnect(enforceSSL: false) } catch let fallbackError as MariaDBPluginError { - if let sslError = Self.classifySSLError(fallbackError) { + if let sslError = MariaDBSSLClassifier.classifySSLError(code: fallbackError.code, message: fallbackError.message) { throw sslError } throw fallbackError } } catch let error as MariaDBPluginError { - if let sslError = Self.classifySSLError(error) { + if let sslError = MariaDBSSLClassifier.classifySSLError(code: error.code, message: error.message) { throw sslError } throw error @@ -256,20 +250,6 @@ final class MariaDBPluginConnection: @unchecked Sendable { } } - static func classifySSLError(_ error: MariaDBPluginError) -> SSLHandshakeError? { - let lower = error.message.lowercased() - if lower.contains("insecure transport") || lower.contains("require_secure_transport") { - return .serverRejectedPlaintext(serverMessage: error.message) - } - if Self.sslOnlyErrorCodes.contains(error.code) { - if lower.contains("certificate") { - return .untrustedCertificate(serverMessage: error.message) - } - return .cipherMismatch(serverMessage: error.message) - } - return nil - } - private func attemptConnect(enforceSSL: Bool) throws -> UnsafeMutablePointer { guard let mysql = mysql_init(nil) else { throw MariaDBPluginError.initFailed diff --git a/Plugins/OracleDriverPlugin/OracleConnection.swift b/Plugins/OracleDriverPlugin/OracleConnection.swift index 129445b20..e94a21b30 100644 --- a/Plugins/OracleDriverPlugin/OracleConnection.swift +++ b/Plugins/OracleDriverPlugin/OracleConnection.swift @@ -192,7 +192,7 @@ final class OracleConnectionWrapper: @unchecked Sendable { } catch let sqlError as OracleSQLError { let detail = Self.connectFailureDetail(sqlError) osLogger.error("Oracle connection failed: \(detail)") - if let sslError = Self.classifySSLError(detail) { + if let sslError = OracleSSLClassifier.classifySSLError(detail) { throw sslError } let category = classifyConnectError(sqlError) @@ -203,35 +203,17 @@ final class OracleConnectionWrapper: @unchecked Sendable { } catch let nioSslError as NIOSSLError { let detail = String(describing: nioSslError) osLogger.error("Oracle TLS error: \(detail)") - throw Self.classifySSLError(detail) ?? SSLHandshakeError.unknown(serverMessage: detail) + throw OracleSSLClassifier.classifySSLError(detail) ?? SSLHandshakeError.unknown(serverMessage: detail) } catch { let detail = String(describing: error) osLogger.error("Oracle connection failed: \(detail)") - if let sslError = Self.classifySSLError(detail) { + if let sslError = OracleSSLClassifier.classifySSLError(detail) { throw sslError } throw OracleError(message: detail, category: .connectionFailed) } } - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("ora-28759") || lower.contains("failure to open file") && lower.contains("wallet") { - return .clientCertRequired(serverMessage: message) - } - if lower.contains("ora-29024") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ora-28860") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("certificate") && (lower.contains("verify") || lower.contains("untrusted")) { - return .untrustedCertificate(serverMessage: message) - } - return nil - } - - private func classifyConnectError(_ error: OracleSQLError) -> OracleError.Category { let codeDescription = error.code.description if codeDescription.hasPrefix("unsupportedVerifierType") { diff --git a/Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift b/Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift index 83fadd793..82e3df1e9 100644 --- a/Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift +++ b/Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift @@ -197,7 +197,7 @@ final class LibPQPluginConnection: @unchecked Sendable { if PQstatus(connection) != CONNECTION_OK { let error = self.getError(from: connection) PQfinish(connection) - if let sslError = Self.classifySSLError(error.message) { + if let sslError = LibPQSSLClassifier.classifySSLError(error.message) { throw sslError } throw error @@ -865,32 +865,6 @@ final class LibPQPluginConnection: @unchecked Sendable { // MARK: - Private Helpers - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("no pg_hba.conf entry") && lower.contains("no encryption") { - return .serverRejectedPlaintext(serverMessage: message) - } - if lower.contains("no pg_hba.conf entry") && lower.contains("ssl") { - return .serverRequiresPlaintext(serverMessage: message) - } - if lower.contains("server does not support ssl") || lower.contains("ssl is not enabled on the server") { - return .serverRequiresPlaintext(serverMessage: message) - } - if lower.contains("certificate verify failed") || lower.contains("self-signed certificate") || lower.contains("unable to get local issuer certificate") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("server certificate") && lower.contains("does not match host name") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("certificate required") || lower.contains("connection requires a valid client certificate") { - return .clientCertRequired(serverMessage: message) - } - if lower.contains("ssl error") || lower.contains("tls handshake") || lower.contains("ssl handshake") { - return .cipherMismatch(serverMessage: message) - } - return nil - } - private func getError(from conn: OpaquePointer) -> LibPQPluginError { var message = "Unknown error" if let msgPtr = PQerrorMessage(conn) { diff --git a/Plugins/RedisDriverPlugin/RedisPluginConnection.swift b/Plugins/RedisDriverPlugin/RedisPluginConnection.swift index e2ca02c80..36271acfe 100644 --- a/Plugins/RedisDriverPlugin/RedisPluginConnection.swift +++ b/Plugins/RedisDriverPlugin/RedisPluginConnection.swift @@ -340,26 +340,6 @@ final class RedisPluginConnection: @unchecked Sendable { throw RedisPluginError.hiredisUnavailable #endif } - - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("certificate verify failed") || lower.contains("unable to get local issuer") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("hostname") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("sslv3") || lower.contains("unsupported protocol") || lower.contains("no shared cipher") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ssl handshake failed") || lower.contains("tlsv1") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("client certificate") { - return .clientCertRequired(serverMessage: message) - } - return nil - } } // MARK: - Synchronous Helpers (must be called on the serial queue) @@ -405,7 +385,7 @@ private extension RedisPluginConnection { if result != REDIS_OK { redisFreeSSLContext(ssl) let errMsg = Self.contextErrorMessage(ctx) - if let sslError = Self.classifySSLError(errMsg) { + if let sslError = RedisSSLClassifier.classifySSLError(errMsg) { throw sslError } throw RedisPluginError(code: Int(result), message: "SSL handshake failed: \(errMsg)") diff --git a/Plugins/TableProPluginKit/CassandraClientKeyClassifier.swift b/Plugins/TableProPluginKit/CassandraClientKeyClassifier.swift new file mode 100644 index 000000000..0f7b51c8d --- /dev/null +++ b/Plugins/TableProPluginKit/CassandraClientKeyClassifier.swift @@ -0,0 +1,17 @@ +import Foundation + +public enum CassandraClientKeyClassifier { + public static func isEncryptedPrivateKey(_ pem: String) -> Bool { + pem.contains("ENCRYPTED PRIVATE KEY") || (pem.contains("Proc-Type:") && pem.contains("ENCRYPTED")) + } + + public static func privateKeyLoadError(keyPEM: String, hasPassphrase: Bool, keyPath: String) -> SSLHandshakeError { + guard isEncryptedPrivateKey(keyPEM) else { + return .clientKeyInvalid(serverMessage: "The client key at \(keyPath) is not a valid private key") + } + if hasPassphrase { + return .clientKeyPassphraseIncorrect(serverMessage: "The passphrase for the client key at \(keyPath) is incorrect") + } + return .clientKeyPassphraseRequired(serverMessage: "The client key at \(keyPath) is encrypted. Enter its passphrase.") + } +} diff --git a/Plugins/TableProPluginKit/ClickHouseSSLClassifier.swift b/Plugins/TableProPluginKit/ClickHouseSSLClassifier.swift new file mode 100644 index 000000000..ca74b293e --- /dev/null +++ b/Plugins/TableProPluginKit/ClickHouseSSLClassifier.swift @@ -0,0 +1,27 @@ +import Foundation + +public enum ClickHouseSSLClassifier { + public static func classifySSLError(_ error: Error) -> SSLHandshakeError? { + let urlError = error as? URLError ?? (error as NSError).underlyingErrors.compactMap { $0 as? URLError }.first + if let urlError { + switch urlError.code { + case .serverCertificateUntrusted, .serverCertificateNotYetValid, .serverCertificateHasUnknownRoot, .serverCertificateHasBadDate: + return .untrustedCertificate(serverMessage: urlError.localizedDescription) + case .clientCertificateRequired, .clientCertificateRejected: + return .clientCertRequired(serverMessage: urlError.localizedDescription) + case .secureConnectionFailed: + return .cipherMismatch(serverMessage: urlError.localizedDescription) + default: + break + } + } + let message = error.localizedDescription.lowercased() + if message.contains("certificate") && (message.contains("untrusted") || message.contains("verify failed")) { + return .untrustedCertificate(serverMessage: error.localizedDescription) + } + if message.contains("hostname") { + return .hostnameMismatch(serverMessage: error.localizedDescription) + } + return nil + } +} diff --git a/Plugins/TableProPluginKit/LibPQSSLClassifier.swift b/Plugins/TableProPluginKit/LibPQSSLClassifier.swift new file mode 100644 index 000000000..e8d60e0a5 --- /dev/null +++ b/Plugins/TableProPluginKit/LibPQSSLClassifier.swift @@ -0,0 +1,29 @@ +import Foundation + +public enum LibPQSSLClassifier { + public static func classifySSLError(_ message: String) -> SSLHandshakeError? { + let lower = message.lowercased() + if lower.contains("no pg_hba.conf entry") && lower.contains("no encryption") { + return .serverRejectedPlaintext(serverMessage: message) + } + if lower.contains("no pg_hba.conf entry") && lower.contains("ssl") { + return .serverRequiresPlaintext(serverMessage: message) + } + if lower.contains("server does not support ssl") || lower.contains("ssl is not enabled on the server") { + return .serverRequiresPlaintext(serverMessage: message) + } + if lower.contains("certificate verify failed") || lower.contains("self-signed certificate") || lower.contains("unable to get local issuer certificate") { + return .untrustedCertificate(serverMessage: message) + } + if lower.contains("server certificate") && lower.contains("does not match host name") { + return .hostnameMismatch(serverMessage: message) + } + if lower.contains("certificate required") || lower.contains("connection requires a valid client certificate") { + return .clientCertRequired(serverMessage: message) + } + if lower.contains("ssl error") || lower.contains("tls handshake") || lower.contains("ssl handshake") { + return .cipherMismatch(serverMessage: message) + } + return nil + } +} diff --git a/Plugins/TableProPluginKit/MariaDBSSLClassifier.swift b/Plugins/TableProPluginKit/MariaDBSSLClassifier.swift new file mode 100644 index 000000000..9c8333fea --- /dev/null +++ b/Plugins/TableProPluginKit/MariaDBSSLClassifier.swift @@ -0,0 +1,19 @@ +import Foundation + +public enum MariaDBSSLClassifier { + public static let sslOnlyErrorCodes: Set = [2_026, 2_012, 1_043] + + public static func classifySSLError(code: UInt32, message: String) -> SSLHandshakeError? { + let lower = message.lowercased() + if lower.contains("insecure transport") || lower.contains("require_secure_transport") { + return .serverRejectedPlaintext(serverMessage: message) + } + if sslOnlyErrorCodes.contains(code) { + if lower.contains("certificate") { + return .untrustedCertificate(serverMessage: message) + } + return .cipherMismatch(serverMessage: message) + } + return nil + } +} diff --git a/Plugins/TableProPluginKit/MongoDBSSLClassifier.swift b/Plugins/TableProPluginKit/MongoDBSSLClassifier.swift new file mode 100644 index 000000000..ce8f61cb3 --- /dev/null +++ b/Plugins/TableProPluginKit/MongoDBSSLClassifier.swift @@ -0,0 +1,39 @@ +import Foundation + +public enum MongoDBSSLClassifier { + public static func classifySSLError(_ message: String) -> SSLHandshakeError? { + let lower = message.lowercased() + if lower.contains("certificate verify failed") || lower.contains("ssl certificate") { + return .untrustedCertificate(serverMessage: message) + } + if lower.contains("hostname") && lower.contains("verification") { + return .hostnameMismatch(serverMessage: message) + } + if lower.contains("tls required") || lower.contains("ssl required") { + return .serverRejectedPlaintext(serverMessage: message) + } + if lower.contains("client certificate required") || lower.contains("peer did not return a certificate") { + return .clientCertRequired(serverMessage: message) + } + if isCipherOrProtocolMismatch(lower) { + return .cipherMismatch(serverMessage: message) + } + if lower.contains("ssl handshake failed") || lower.contains("tls handshake failed") { + return .unknown(serverMessage: message) + } + return nil + } + + public static func isCipherOrProtocolMismatch(_ lower: String) -> Bool { + let signatures = [ + "no shared cipher", + "sslv3 alert handshake failure", + "wrong version number", + "unsupported protocol", + "no protocols available", + "alert protocol version", + "protocol version", + ] + return signatures.contains { lower.contains($0) } + } +} diff --git a/Plugins/TableProPluginKit/OracleSSLClassifier.swift b/Plugins/TableProPluginKit/OracleSSLClassifier.swift new file mode 100644 index 000000000..7cfe58c55 --- /dev/null +++ b/Plugins/TableProPluginKit/OracleSSLClassifier.swift @@ -0,0 +1,20 @@ +import Foundation + +public enum OracleSSLClassifier { + public static func classifySSLError(_ message: String) -> SSLHandshakeError? { + let lower = message.lowercased() + if lower.contains("ora-28759") || lower.contains("failure to open file") && lower.contains("wallet") { + return .clientCertRequired(serverMessage: message) + } + if lower.contains("ora-29024") { + return .cipherMismatch(serverMessage: message) + } + if lower.contains("ora-28860") { + return .cipherMismatch(serverMessage: message) + } + if lower.contains("certificate") && (lower.contains("verify") || lower.contains("untrusted")) { + return .untrustedCertificate(serverMessage: message) + } + return nil + } +} diff --git a/Plugins/TableProPluginKit/RedisSSLClassifier.swift b/Plugins/TableProPluginKit/RedisSSLClassifier.swift new file mode 100644 index 000000000..b17e9976b --- /dev/null +++ b/Plugins/TableProPluginKit/RedisSSLClassifier.swift @@ -0,0 +1,23 @@ +import Foundation + +public enum RedisSSLClassifier { + public static func classifySSLError(_ message: String) -> SSLHandshakeError? { + let lower = message.lowercased() + if lower.contains("certificate verify failed") || lower.contains("unable to get local issuer") { + return .untrustedCertificate(serverMessage: message) + } + if lower.contains("hostname") { + return .hostnameMismatch(serverMessage: message) + } + if lower.contains("sslv3") || lower.contains("unsupported protocol") || lower.contains("no shared cipher") { + return .cipherMismatch(serverMessage: message) + } + if lower.contains("ssl handshake failed") || lower.contains("tlsv1") { + return .cipherMismatch(serverMessage: message) + } + if lower.contains("client certificate") { + return .clientCertRequired(serverMessage: message) + } + return nil + } +} diff --git a/TableProTests/PluginTestSources/PluginSSLClassifiers.swift b/TableProTests/PluginTestSources/PluginSSLClassifiers.swift deleted file mode 100644 index 154eb0f08..000000000 --- a/TableProTests/PluginTestSources/PluginSSLClassifiers.swift +++ /dev/null @@ -1,191 +0,0 @@ -import Foundation -import TableProPluginKit - -enum LibPQClassifier { - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("no pg_hba.conf entry") && lower.contains("no encryption") { - return .serverRejectedPlaintext(serverMessage: message) - } - if lower.contains("no pg_hba.conf entry") && lower.contains("ssl") { - return .serverRequiresPlaintext(serverMessage: message) - } - if lower.contains("server does not support ssl") || lower.contains("ssl is not enabled on the server") { - return .serverRequiresPlaintext(serverMessage: message) - } - if lower.contains("certificate verify failed") || lower.contains("self-signed certificate") || lower.contains("unable to get local issuer certificate") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("server certificate") && lower.contains("does not match host name") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("certificate required") || lower.contains("connection requires a valid client certificate") { - return .clientCertRequired(serverMessage: message) - } - if lower.contains("ssl error") || lower.contains("tls handshake") || lower.contains("ssl handshake") { - return .cipherMismatch(serverMessage: message) - } - return nil - } -} - -enum MariaDBClassifier { - static let sslOnlyErrorCodes: Set = [2_026, 2_012, 1_043] - - static func classifySSLError(code: UInt32, message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("insecure transport") || lower.contains("require_secure_transport") { - return .serverRejectedPlaintext(serverMessage: message) - } - if sslOnlyErrorCodes.contains(code) { - if lower.contains("certificate") { - return .untrustedCertificate(serverMessage: message) - } - return .cipherMismatch(serverMessage: message) - } - return nil - } -} - -enum FreeTDSClassifier { - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("encryption is required") || lower.contains("server requires encryption") { - return .serverRejectedPlaintext(serverMessage: message) - } - if lower.contains("encryption not supported") || lower.contains("server does not support encryption") { - return .serverRequiresPlaintext(serverMessage: message) - } - if lower.contains("certificate verify failed") || lower.contains("certificate is not trusted") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("does not match host") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("ssl handshake") || lower.contains("tls handshake") || lower.contains("openssl error") { - return .cipherMismatch(serverMessage: message) - } - return nil - } -} - -enum MongoDBClassifier { - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("certificate verify failed") || lower.contains("ssl certificate") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("hostname") && lower.contains("verification") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("tls required") || lower.contains("ssl required") { - return .serverRejectedPlaintext(serverMessage: message) - } - if lower.contains("client certificate required") || lower.contains("peer did not return a certificate") { - return .clientCertRequired(serverMessage: message) - } - if isCipherOrProtocolMismatch(lower) { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ssl handshake failed") || lower.contains("tls handshake failed") { - return .unknown(serverMessage: message) - } - return nil - } - - static func isCipherOrProtocolMismatch(_ lower: String) -> Bool { - let signatures = [ - "no shared cipher", - "sslv3 alert handshake failure", - "wrong version number", - "unsupported protocol", - "no protocols available", - "alert protocol version", - "protocol version", - ] - return signatures.contains { lower.contains($0) } - } -} - -enum RedisClassifier { - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("certificate verify failed") || lower.contains("unable to get local issuer") { - return .untrustedCertificate(serverMessage: message) - } - if lower.contains("hostname") { - return .hostnameMismatch(serverMessage: message) - } - if lower.contains("sslv3") || lower.contains("unsupported protocol") || lower.contains("no shared cipher") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ssl handshake failed") || lower.contains("tlsv1") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("client certificate") { - return .clientCertRequired(serverMessage: message) - } - return nil - } -} - -enum OracleClassifier { - static func classifySSLError(_ message: String) -> SSLHandshakeError? { - let lower = message.lowercased() - if lower.contains("ora-28759") || lower.contains("failure to open file") && lower.contains("wallet") { - return .clientCertRequired(serverMessage: message) - } - if lower.contains("ora-29024") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("ora-28860") { - return .cipherMismatch(serverMessage: message) - } - if lower.contains("certificate") && (lower.contains("verify") || lower.contains("untrusted")) { - return .untrustedCertificate(serverMessage: message) - } - return nil - } -} - -enum ClickHouseClassifier { - static func classifySSLError(_ error: Error) -> SSLHandshakeError? { - let urlError = error as? URLError ?? (error as NSError).underlyingErrors.compactMap { $0 as? URLError }.first - if let urlError { - switch urlError.code { - case .serverCertificateUntrusted, .serverCertificateNotYetValid, .serverCertificateHasUnknownRoot, .serverCertificateHasBadDate: - return .untrustedCertificate(serverMessage: urlError.localizedDescription) - case .clientCertificateRequired, .clientCertificateRejected: - return .clientCertRequired(serverMessage: urlError.localizedDescription) - case .secureConnectionFailed: - return .cipherMismatch(serverMessage: urlError.localizedDescription) - default: - break - } - } - let message = error.localizedDescription.lowercased() - if message.contains("certificate") && (message.contains("untrusted") || message.contains("verify failed")) { - return .untrustedCertificate(serverMessage: error.localizedDescription) - } - if message.contains("hostname") { - return .hostnameMismatch(serverMessage: error.localizedDescription) - } - return nil - } -} - -enum CassandraClassifier { - static func isEncryptedPrivateKey(_ pem: String) -> Bool { - pem.contains("ENCRYPTED PRIVATE KEY") || (pem.contains("Proc-Type:") && pem.contains("ENCRYPTED")) - } - - static func privateKeyLoadError(keyPEM: String, hasPassphrase: Bool, keyPath: String) -> SSLHandshakeError { - guard isEncryptedPrivateKey(keyPEM) else { - return .clientKeyInvalid(serverMessage: "The client key at \(keyPath) is not a valid private key") - } - if hasPassphrase { - return .clientKeyPassphraseIncorrect(serverMessage: "The passphrase for the client key at \(keyPath) is incorrect") - } - return .clientKeyPassphraseRequired(serverMessage: "The client key at \(keyPath) is encrypted. Enter its passphrase.") - } -} diff --git a/TableProTests/Plugins/PluginSSLClassifierTests.swift b/TableProTests/Plugins/PluginSSLClassifierTests.swift index 41a33812c..e7f5b3f78 100644 --- a/TableProTests/Plugins/PluginSSLClassifierTests.swift +++ b/TableProTests/Plugins/PluginSSLClassifierTests.swift @@ -1,5 +1,4 @@ import Foundation -@testable import TablePro import TableProPluginKit import Testing @@ -8,7 +7,7 @@ struct LibPQClassifierTests { @Test("Classifies the AWS RDS rejection in #1298 as serverRejectedPlaintext") func testRDSPattern() { let msg = "FATAL: no pg_hba.conf entry for host \"1.2.3.4\", user \"u\", database \"d\", no encryption" - guard case .serverRejectedPlaintext = LibPQClassifier.classifySSLError(msg) else { + guard case .serverRejectedPlaintext = LibPQSSLClassifier.classifySSLError(msg) else { Issue.record("Expected serverRejectedPlaintext") return } @@ -17,7 +16,7 @@ struct LibPQClassifierTests { @Test("Classifies SSL-required as serverRequiresPlaintext") func testSSLRequired() { let msg = "FATAL: no pg_hba.conf entry for host \"1.2.3.4\", user \"u\", database \"d\", SSL on" - guard case .serverRequiresPlaintext = LibPQClassifier.classifySSLError(msg) else { + guard case .serverRequiresPlaintext = LibPQSSLClassifier.classifySSLError(msg) else { Issue.record("Expected serverRequiresPlaintext") return } @@ -26,7 +25,7 @@ struct LibPQClassifierTests { @Test("Classifies server-no-ssl-support as serverRequiresPlaintext") func testServerNoSSL() { let msg = "server does not support SSL, but SSL was required" - guard case .serverRequiresPlaintext = LibPQClassifier.classifySSLError(msg) else { + guard case .serverRequiresPlaintext = LibPQSSLClassifier.classifySSLError(msg) else { Issue.record("Expected serverRequiresPlaintext") return } @@ -35,7 +34,7 @@ struct LibPQClassifierTests { @Test("Classifies cert verify failure as untrustedCertificate") func testCertVerify() { let msg = "SSL error: certificate verify failed" - guard case .untrustedCertificate = LibPQClassifier.classifySSLError(msg) else { + guard case .untrustedCertificate = LibPQSSLClassifier.classifySSLError(msg) else { Issue.record("Expected untrustedCertificate") return } @@ -44,7 +43,7 @@ struct LibPQClassifierTests { @Test("Classifies hostname mismatch") func testHostnameMismatch() { let msg = "server certificate for \"foo\" does not match host name \"bar\"" - guard case .hostnameMismatch = LibPQClassifier.classifySSLError(msg) else { + guard case .hostnameMismatch = LibPQSSLClassifier.classifySSLError(msg) else { Issue.record("Expected hostnameMismatch") return } @@ -52,8 +51,8 @@ struct LibPQClassifierTests { @Test("Non-SSL error returns nil") func testNonSSL() { - #expect(LibPQClassifier.classifySSLError("FATAL: password authentication failed") == nil) - #expect(LibPQClassifier.classifySSLError("connection refused") == nil) + #expect(LibPQSSLClassifier.classifySSLError("FATAL: password authentication failed") == nil) + #expect(LibPQSSLClassifier.classifySSLError("connection refused") == nil) } } @@ -61,7 +60,7 @@ struct LibPQClassifierTests { struct MariaDBClassifierTests { @Test("CR_SSL_CONNECTION_ERROR with cipher message → cipherMismatch") func testSSLConnectionError() { - guard case .cipherMismatch = MariaDBClassifier.classifySSLError(code: 2_026, message: "SSL connection error: no shared cipher") else { + guard case .cipherMismatch = MariaDBSSLClassifier.classifySSLError(code: 2_026, message: "SSL connection error: no shared cipher") else { Issue.record("Expected cipherMismatch") return } @@ -69,7 +68,7 @@ struct MariaDBClassifierTests { @Test("CR_SSL_CONNECTION_ERROR with certificate keyword → untrustedCertificate") func testSSLCertError() { - guard case .untrustedCertificate = MariaDBClassifier.classifySSLError(code: 2_026, message: "SSL certificate not trusted") else { + guard case .untrustedCertificate = MariaDBSSLClassifier.classifySSLError(code: 2_026, message: "SSL certificate not trusted") else { Issue.record("Expected untrustedCertificate") return } @@ -77,7 +76,8 @@ struct MariaDBClassifierTests { @Test("require_secure_transport → serverRejectedPlaintext") func testRequireSecureTransport() { - guard case .serverRejectedPlaintext = MariaDBClassifier.classifySSLError(code: 1_045, message: "Connections using insecure transport are prohibited while --require_secure_transport=ON") else { + let message = "Connections using insecure transport are prohibited while --require_secure_transport=ON" + guard case .serverRejectedPlaintext = MariaDBSSLClassifier.classifySSLError(code: 1_045, message: message) else { Issue.record("Expected serverRejectedPlaintext") return } @@ -85,31 +85,12 @@ struct MariaDBClassifierTests { @Test("Auth error 1045 not retried (returns nil)") func testAuthError() { - #expect(MariaDBClassifier.classifySSLError(code: 1_045, message: "Access denied for user 'foo'@'bar'") == nil) + #expect(MariaDBSSLClassifier.classifySSLError(code: 1_045, message: "Access denied for user 'foo'@'bar'") == nil) } @Test("Network error 2002 not retried") func testNetworkError() { - #expect(MariaDBClassifier.classifySSLError(code: 2_002, message: "Can't connect to MySQL server") == nil) - } -} - -@Suite("FreeTDS SSL Classifier") -struct FreeTDSClassifierTests { - @Test("Server requires encryption → serverRejectedPlaintext") - func testServerRequires() { - guard case .serverRejectedPlaintext = FreeTDSClassifier.classifySSLError("Server requires encryption") else { - Issue.record("Expected serverRejectedPlaintext") - return - } - } - - @Test("OpenSSL handshake → cipherMismatch") - func testOpenSSL() { - guard case .cipherMismatch = FreeTDSClassifier.classifySSLError("OpenSSL: SSL_connect failed") else { - Issue.record("Expected cipherMismatch") - return - } + #expect(MariaDBSSLClassifier.classifySSLError(code: 2_002, message: "Can't connect to MySQL server") == nil) } } @@ -119,7 +100,7 @@ struct MongoDBClassifierTests { func testAtlasInternalErrorHandshake() { let message = "No suitable servers found: [TLS handshake failed: internal error (-9838) " + "calling hello on 'ac-zmho1ul-shard-00-00.dsllzcf.mongodb.net:27017']" - guard case .unknown = MongoDBClassifier.classifySSLError(message) else { + guard case .unknown = MongoDBSSLClassifier.classifySSLError(message) else { Issue.record("Expected unknown for a generic handshake failure") return } @@ -127,7 +108,7 @@ struct MongoDBClassifierTests { @Test("Genuine cipher/protocol failure → cipherMismatch") func testGenuineCipherMismatch() { - guard case .cipherMismatch = MongoDBClassifier.classifySSLError("TLS handshake failed: sslv3 alert handshake failure: no shared cipher") else { + guard case .cipherMismatch = MongoDBSSLClassifier.classifySSLError("TLS handshake failed: sslv3 alert handshake failure: no shared cipher") else { Issue.record("Expected cipherMismatch") return } @@ -135,7 +116,7 @@ struct MongoDBClassifierTests { @Test("Certificate verify failure → untrustedCertificate") func testCertificateVerifyFailed() { - guard case .untrustedCertificate = MongoDBClassifier.classifySSLError("TLS handshake failed: certificate verify failed") else { + guard case .untrustedCertificate = MongoDBSSLClassifier.classifySSLError("TLS handshake failed: certificate verify failed") else { Issue.record("Expected untrustedCertificate") return } @@ -143,7 +124,7 @@ struct MongoDBClassifierTests { @Test("Hostname verification failure → hostnameMismatch") func testHostnameVerification() { - guard case .hostnameMismatch = MongoDBClassifier.classifySSLError("hostname verification failed") else { + guard case .hostnameMismatch = MongoDBSSLClassifier.classifySSLError("hostname verification failed") else { Issue.record("Expected hostnameMismatch") return } @@ -151,7 +132,7 @@ struct MongoDBClassifierTests { @Test("TLS required → serverRejectedPlaintext") func testTLSRequired() { - guard case .serverRejectedPlaintext = MongoDBClassifier.classifySSLError("TLS required by Atlas cluster") else { + guard case .serverRejectedPlaintext = MongoDBSSLClassifier.classifySSLError("TLS required by Atlas cluster") else { Issue.record("Expected serverRejectedPlaintext") return } @@ -162,7 +143,7 @@ struct MongoDBClassifierTests { struct RedisClassifierTests { @Test("No shared cipher → cipherMismatch") func testNoSharedCipher() { - guard case .cipherMismatch = RedisClassifier.classifySSLError("SSL_connect: no shared cipher") else { + guard case .cipherMismatch = RedisSSLClassifier.classifySSLError("SSL_connect: no shared cipher") else { Issue.record("Expected cipherMismatch") return } @@ -170,7 +151,7 @@ struct RedisClassifierTests { @Test("Cert verify failed → untrustedCertificate") func testCertVerify() { - guard case .untrustedCertificate = RedisClassifier.classifySSLError("certificate verify failed (self-signed)") else { + guard case .untrustedCertificate = RedisSSLClassifier.classifySSLError("certificate verify failed (self-signed)") else { Issue.record("Expected untrustedCertificate") return } @@ -181,7 +162,7 @@ struct RedisClassifierTests { struct OracleClassifierTests { @Test("ORA-29024 → cipherMismatch") func testORA29024() { - guard case .cipherMismatch = OracleClassifier.classifySSLError("ORA-29024: Certificate validation failure") else { + guard case .cipherMismatch = OracleSSLClassifier.classifySSLError("ORA-29024: Certificate validation failure") else { Issue.record("Expected cipherMismatch") return } @@ -189,12 +170,12 @@ struct OracleClassifierTests { @Test("Network timeout (ORA-12606) is not classified as SSL") func testTimeoutNotSSL() { - #expect(OracleClassifier.classifySSLError("ORA-12606: TNS: Application timeout occurred") == nil) + #expect(OracleSSLClassifier.classifySSLError("ORA-12606: TNS: Application timeout occurred") == nil) } @Test("ORA-28759 → clientCertRequired") func testORA28759() { - guard case .clientCertRequired = OracleClassifier.classifySSLError("ORA-28759: failure to open file") else { + guard case .clientCertRequired = OracleSSLClassifier.classifySSLError("ORA-28759: failure to open file") else { Issue.record("Expected clientCertRequired") return } @@ -206,7 +187,7 @@ struct ClickHouseClassifierTests { @Test("URLError.secureConnectionFailed → cipherMismatch") func testSecureConnectionFailed() { let error = URLError(.secureConnectionFailed) - guard case .cipherMismatch = ClickHouseClassifier.classifySSLError(error) else { + guard case .cipherMismatch = ClickHouseSSLClassifier.classifySSLError(error) else { Issue.record("Expected cipherMismatch") return } @@ -215,7 +196,7 @@ struct ClickHouseClassifierTests { @Test("URLError.serverCertificateUntrusted → untrustedCertificate") func testCertUntrusted() { let error = URLError(.serverCertificateUntrusted) - guard case .untrustedCertificate = ClickHouseClassifier.classifySSLError(error) else { + guard case .untrustedCertificate = ClickHouseSSLClassifier.classifySSLError(error) else { Issue.record("Expected untrustedCertificate") return } @@ -224,7 +205,7 @@ struct ClickHouseClassifierTests { @Test("Non-SSL error returns nil") func testNonSSL() { let error = URLError(.notConnectedToInternet) - #expect(ClickHouseClassifier.classifySSLError(error) == nil) + #expect(ClickHouseSSLClassifier.classifySSLError(error) == nil) } } @@ -243,14 +224,14 @@ struct CassandraClassifierTests { @Test("Detects PKCS#8 and PKCS#1 encrypted keys, not unencrypted ones") func testEncryptionDetection() { - #expect(CassandraClassifier.isEncryptedPrivateKey(encryptedPkcs8)) - #expect(CassandraClassifier.isEncryptedPrivateKey(encryptedPkcs1)) - #expect(!CassandraClassifier.isEncryptedPrivateKey(unencryptedPkcs8)) + #expect(CassandraClientKeyClassifier.isEncryptedPrivateKey(encryptedPkcs8)) + #expect(CassandraClientKeyClassifier.isEncryptedPrivateKey(encryptedPkcs1)) + #expect(!CassandraClientKeyClassifier.isEncryptedPrivateKey(unencryptedPkcs8)) } @Test("Encrypted key with no passphrase → clientKeyPassphraseRequired") func testEncryptedNoPassphrase() { - let error = CassandraClassifier.privateKeyLoadError( + let error = CassandraClientKeyClassifier.privateKeyLoadError( keyPEM: encryptedPkcs8, hasPassphrase: false, keyPath: "/k.pem") guard case .clientKeyPassphraseRequired = error else { Issue.record("Expected clientKeyPassphraseRequired") @@ -260,7 +241,7 @@ struct CassandraClassifierTests { @Test("Encrypted key with wrong passphrase → clientKeyPassphraseIncorrect") func testEncryptedWrongPassphrase() { - let error = CassandraClassifier.privateKeyLoadError( + let error = CassandraClientKeyClassifier.privateKeyLoadError( keyPEM: encryptedPkcs1, hasPassphrase: true, keyPath: "/k.pem") guard case .clientKeyPassphraseIncorrect = error else { Issue.record("Expected clientKeyPassphraseIncorrect") @@ -270,9 +251,9 @@ struct CassandraClassifierTests { @Test("Unencrypted but unreadable key → clientKeyInvalid, never a passphrase error") func testUnencryptedInvalid() { - let withoutPassphrase = CassandraClassifier.privateKeyLoadError( + let withoutPassphrase = CassandraClientKeyClassifier.privateKeyLoadError( keyPEM: unencryptedPkcs8, hasPassphrase: false, keyPath: "/k.pem") - let withPassphrase = CassandraClassifier.privateKeyLoadError( + let withPassphrase = CassandraClientKeyClassifier.privateKeyLoadError( keyPEM: unencryptedPkcs8, hasPassphrase: true, keyPath: "/k.pem") guard case .clientKeyInvalid = withoutPassphrase else { Issue.record("Expected clientKeyInvalid without passphrase")