Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import Foundation

public enum MSSQLTLSFailureKind: Sendable {
case serverRejectedPlaintext
case serverRequiresPlaintext
case untrustedCertificate
case hostnameMismatch
case clientCertRequired
case cipherMismatch
}

public enum MSSQLCoreError: LocalizedError, Sendable {
case connectionFailed(String)
case notConnected
case queryFailed(String)
case cancelled
case tlsHandshakeFailed(String)
case tlsHandshakeFailed(kind: MSSQLTLSFailureKind, serverMessage: String)

public var errorDescription: String? {
switch self {
Expand All @@ -17,8 +26,8 @@ public enum MSSQLCoreError: LocalizedError, Sendable {
return String(format: String(localized: "Query failed: %@"), detail)
case .cancelled:
return String(localized: "Query was cancelled")
case .tlsHandshakeFailed(let detail):
return String(format: String(localized: "TLS handshake failed: %@"), detail)
case .tlsHandshakeFailed(_, let serverMessage):
return String(format: String(localized: "TLS handshake failed: %@"), serverMessage)
}
}
}
17 changes: 8 additions & 9 deletions Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import Foundation
import os
import TableProMSSQLCore
import TableProPluginKit

private let freetdsLogger = Logger(subsystem: "com.TablePro", category: "FreeTDSConnection")

Expand Down Expand Up @@ -134,7 +133,7 @@
init(options: MSSQLConnectionOptions) {
self.options = options
self.queue = DispatchQueue(label: "com.TablePro.freetds.\(options.host).\(options.port)", qos: .userInitiated)
_ = freetdsInitOnce

Check warning on line 136 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

main actor-isolated let 'freetdsInitOnce' can not be referenced from a nonisolated context

Check warning on line 136 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

main actor-isolated let 'freetdsInitOnce' can not be referenced from a nonisolated context
}

func connect() async throws {
Expand Down Expand Up @@ -162,13 +161,13 @@
// the brief window (cleared at function exit) keeps the cost acceptable for interactive use.
_ = dbsetlogintime(Int32(options.loginTimeoutSeconds))

freetdsClearError(for: nil)

Check warning on line 164 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsClearError(for:)' in a synchronous nonisolated context
let serverName = "\(options.host):\(options.port)"
guard let proc = dbopen(login, serverName) else {
let detail = freetdsGetError(for: nil)

Check warning on line 167 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsGetError(for:)' in a synchronous nonisolated context
let msg = detail.isEmpty ? "Check host, port, credentials, and TLS settings" : detail
if let sslError = FreeTDSConnection.classifySSLError(detail) {
throw sslError
if let kind = FreeTDSConnection.classifySSLError(detail) {
throw MSSQLCoreError.tlsHandshakeFailed(kind: kind, serverMessage: detail)
}
throw MSSQLCoreError.connectionFailed("Failed to connect to \(options.host):\(options.port): \(msg)")
}
Expand Down Expand Up @@ -206,9 +205,9 @@
lock.unlock()

if let handle {
freetdsUnregister(handle)

Check warning on line 208 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsUnregister' in a synchronous nonisolated context
queue.async {
_ = dbclose(handle)

Check warning on line 210 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

capture of 'handle' with non-Sendable type 'UnsafeMutablePointer<DBPROCESS>' (aka 'UnsafeMutablePointer<dbprocess>') in a '@sendable' closure
}
}
}
Expand Down Expand Up @@ -245,12 +244,12 @@
_isCancelled = false
lock.unlock()

freetdsClearError(for: proc)

Check warning on line 247 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsClearError(for:)' in a synchronous nonisolated context
if dbcmd(proc, query) == FAIL {
throw MSSQLCoreError.queryFailed("Failed to prepare query")
}
if dbsqlexec(proc) == FAIL {
let detail = freetdsGetError(for: proc)

Check warning on line 252 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsGetError(for:)' in a synchronous nonisolated context
let msg = detail.isEmpty ? "Query execution failed" : detail
throw MSSQLCoreError.queryFailed(msg)
}
Expand Down Expand Up @@ -369,12 +368,12 @@
_isCancelled = false
lock.unlock()

freetdsClearError(for: proc)

Check warning on line 371 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsClearError(for:)' in a synchronous nonisolated context
if dbcmd(proc, query) == FAIL {
throw MSSQLCoreError.queryFailed("Failed to prepare query")
}
if dbsqlexec(proc) == FAIL {
let detail = freetdsGetError(for: proc)

Check warning on line 376 in Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift

View workflow job for this annotation

GitHub Actions / Run iOS Tests

call to main actor-isolated global function 'freetdsGetError(for:)' in a synchronous nonisolated context
let msg = detail.isEmpty ? "Query execution failed" : detail
throw MSSQLCoreError.queryFailed(msg)
}
Expand Down Expand Up @@ -532,22 +531,22 @@
return raw
}

static func classifySSLError(_ message: String) -> SSLHandshakeError? {
static func classifySSLError(_ message: String) -> MSSQLTLSFailureKind? {
let lower = message.lowercased()
if lower.contains("encryption is required") || lower.contains("server requires encryption") {
return .serverRejectedPlaintext(serverMessage: message)
return .serverRejectedPlaintext
}
if lower.contains("encryption not supported") || lower.contains("server does not support encryption") {
return .serverRequiresPlaintext(serverMessage: message)
return .serverRequiresPlaintext
}
if lower.contains("certificate verify failed") || lower.contains("certificate is not trusted") {
return .untrustedCertificate(serverMessage: message)
return .untrustedCertificate
}
if lower.contains("does not match host") {
return .hostnameMismatch(serverMessage: message)
return .hostnameMismatch
}
if lower.contains("ssl handshake") || lower.contains("tls handshake") || lower.contains("openssl error") {
return .cipherMismatch(serverMessage: message)
return .cipherMismatch
}
return nil
}
Expand Down
20 changes: 18 additions & 2 deletions Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,21 @@ private extension MSSQLPluginError {
self = .queryFailed(m)
case .cancelled:
self = .queryFailed(String(localized: "Query was cancelled"))
case .tlsHandshakeFailed(let m):
self = .connectionFailed(String(format: String(localized: "TLS: %@"), m))
case .tlsHandshakeFailed(_, let serverMessage):
self = .connectionFailed(String(format: String(localized: "TLS: %@"), serverMessage))
}
}
}

private extension MSSQLTLSFailureKind {
func sslHandshakeError(serverMessage: String) -> SSLHandshakeError {
switch self {
case .serverRejectedPlaintext: return .serverRejectedPlaintext(serverMessage: serverMessage)
case .serverRequiresPlaintext: return .serverRequiresPlaintext(serverMessage: serverMessage)
case .untrustedCertificate: return .untrustedCertificate(serverMessage: serverMessage)
case .hostnameMismatch: return .hostnameMismatch(serverMessage: serverMessage)
case .clientCertRequired: return .clientCertRequired(serverMessage: serverMessage)
case .cipherMismatch: return .cipherMismatch(serverMessage: serverMessage)
}
}
}
Expand Down Expand Up @@ -229,6 +242,9 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
do {
try await conn.connect()
} catch let error as MSSQLCoreError {
if case let .tlsHandshakeFailed(kind, serverMessage) = error {
throw kind.sslHandshakeError(serverMessage: serverMessage)
}
throw MSSQLPluginError(coreError: error)
}
self.freeTDSConn = conn
Expand Down
4 changes: 2 additions & 2 deletions TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ final class MSSQLDriver: DatabaseDriver, @unchecked Sendable {
return ConnectionError.notConnected
case .connectionFailed(let msg):
return DatabaseError(message: msg)
case .tlsHandshakeFailed(let msg):
return DatabaseError(message: "TLS handshake failed: \(msg)")
case .tlsHandshakeFailed(_, let serverMessage):
return DatabaseError(message: "TLS handshake failed: \(serverMessage)")
case .queryFailed(let msg):
return DatabaseError(message: msg)
case .cancelled:
Expand Down
Loading