From ff8da93a5a830783868911e3227f549f3c98e310 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 28 Nov 2025 10:44:12 +1300 Subject: [PATCH 1/4] Use ephemeral URLSession instead of the shared URLSession in unit tests --- native/swift/Tests/wordpress-api/LoginTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index 546303c15..17eae4cd2 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -11,7 +11,7 @@ import FoundationNetworking @Suite("Login Tests", .enabled(if: !isLinux())) class LoginTests { - let client = WordPressLoginClient(urlSession: .shared) + let client = WordPressLoginClient(urlSession: .init(configuration: .ephemeral)) @Test("Login Spec Example 1: Valid URL") func testValidURL() async throws { @@ -172,7 +172,7 @@ class LoginTests { await #expect(performing: { _ = try await WordPressLoginClient( - urlSession: .shared, + urlSession: .init(configuration: .ephemeral), middleware: MiddlewarePipeline(middlewares: invalid) ).findLoginUrl(forSite: "https://basic-auth.wpmt.co") }, throws: { error in @@ -196,7 +196,7 @@ class LoginTests { let valid = ApiDiscoveryAuthenticationMiddleware(username: "test@example.com", password: "str0ngp4ssw0rd!") let parsedUrl = try await WordPressLoginClient( - urlSession: .shared, + urlSession: .init(configuration: .ephemeral), middleware: MiddlewarePipeline(middlewares: valid) ).findLoginUrl(forSite: "https://basic-auth.wpmt.co") From 78e3c83c6e40dab4251548df00eedb899226ff68 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 28 Nov 2025 10:50:53 +1300 Subject: [PATCH 2/4] Support additional SSL certificate alternative names --- .../wordpress-api/SafeRequestExecutor.swift | 40 +++++++++++++++++ .../Tests/wordpress-api/LoginTests.swift | 44 +++++-------------- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift index df2a0715d..d42ec1877 100644 --- a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -71,6 +71,10 @@ public final class WpRequestExecutor: SafeRequestExecutor { } } + public func allowSSL(altNames: [String], forCommonName cn: String) { + executorDelegate.allowSSL(altNames: altNames, forCommonName: cn) + } + func perform(_ request: NetworkRequestContent) async -> Result { do { let (data, response) = try await request.perform( @@ -272,6 +276,11 @@ private final class RequestExecutorDelegate: NSObject, URLSessionTaskDelegate, @ private let lock = NSLock() private var redirects: [String: [WpRedirect]] = [:] + // When a site's domain is not included in its SSL certificate's common name and alternative names, + // URLSession rejects the HTTPs connection. Here we allow the consumers to add additional + // alternative names. + // The key is SSL certificate common name. + private var additionalAlternativeNames: [String: Set] = [:] init(redirects: [String: [WpRedirect]] = [:]) { self.redirects = redirects @@ -283,6 +292,37 @@ private final class RequestExecutorDelegate: NSObject, URLSessionTaskDelegate, @ } } + func allowSSL(altNames: [String], forCommonName cn: String) { + lock.withLock { + additionalAlternativeNames[cn, default: []].formUnion(altNames) + } + } + + private func alternateNames(forCertificate cert: SslCertificateInfo) -> Set { + lock.withLock { + additionalAlternativeNames[cert.commonName()] ?? [] + } + } + + #if !os(Linux) + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust, + let certificateChain = SecTrustCopyCertificateChain(trust) as? [SecCertificate], + let cert = certificateChain.first, + let cert = parseCertificate(data: SecCertificateCopyData(cert) as Data), + alternateNames(forCertificate: cert).contains(challenge.protectionSpace.host) { + return (.useCredential, URLCredential(trust: trust)) + } + + return (.performDefaultHandling, nil) + } + #endif + func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) { NotificationCenter.default.post(name: RequestExecutorDelegate.didCreateTaskNotification, object: task) } diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index 17eae4cd2..4d7faebd6 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -285,46 +285,22 @@ class LoginTests { } /// This test is unavailable in Linux until https://github.com/swiftlang/swift-corelibs-foundation/pull/4937 lands - @Test("Login Spec Example 17: Invalid SSL Certificate with explicit exception", .enabled(if: !isLinux())) + @Test("Login Spec Example 18: Invalid SSL Certificate with explicit exception", .enabled(if: !isLinux())) func testInvalidHttpsWithExceptionWorks() async throws { - let session = URLSession( - configuration: .default, - delegate: HTTPSSessionDelegate( - allowedDomains: ["wordpress-1315525-4803651.cloudwaysapps.com"] - ), - delegateQueue: nil - ) - let client = WordPressLoginClient(urlSession: session) + let executor = WpRequestExecutor(urlSession: .init(configuration: .ephemeral)) + executor.allowSSL(altNames: ["wordpress-1315525-4803651.cloudwaysapps.com"], forCommonName: "vanilla.wpmt.co") + let client = WordPressLoginClient(requestExecutor: executor) _ = try await client.findLoginUrl(forSite: "https://wordpress-1315525-4803651.cloudwaysapps.com") } - final class HTTPSSessionDelegate: NSObject, URLSessionDelegate { - - let allowedDomains: [String] - - init(allowedDomains: [String] = []) { - self.allowedDomains = allowedDomains - } - - #if !os(Linux) - // There's no ability to support self-signed (or otherwise invalid) SSL certificates in Linux until - // https://github.com/swiftlang/swift-corelibs-foundation/pull/4937 lands. - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - guard allowedDomains.contains(challenge.protectionSpace.host), - let trust = challenge.protectionSpace.serverTrust else { - completionHandler(.useCredential, nil) - return - } - - completionHandler(.useCredential, URLCredential(trust: trust)) - } - #endif + /// This test is unavailable in Linux until https://github.com/swiftlang/swift-corelibs-foundation/pull/4937 lands + @Test("Login Spec Example 19: Alternative name in SSL Certificate", .enabled(if: !isLinux())) + func testAlternameWorks() async throws { + // "vanilla1.wpmt.co" is one of the alternative names in vanilla1.wpmt.co certificate. + _ = try await self.client.findLoginUrl(forSite: "https://vanilla1.wpmt.co") } + private func getApplicationPasswordsNotSupportedReason( from error: any Error ) throws -> ApplicationPasswordsNotSupportedReason? { From 2dcac48b494b6f9ffb7662f1cdba94d380a5b269 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 28 Nov 2025 10:54:15 +1300 Subject: [PATCH 3/4] Fix a code comment --- native/swift/Tests/wordpress-api/LoginTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index 4d7faebd6..f2ccc6ef7 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -296,7 +296,7 @@ class LoginTests { /// This test is unavailable in Linux until https://github.com/swiftlang/swift-corelibs-foundation/pull/4937 lands @Test("Login Spec Example 19: Alternative name in SSL Certificate", .enabled(if: !isLinux())) func testAlternameWorks() async throws { - // "vanilla1.wpmt.co" is one of the alternative names in vanilla1.wpmt.co certificate. + // "vanilla1.wpmt.co" is one of the alternative names in vanilla.wpmt.co certificate. _ = try await self.client.findLoginUrl(forSite: "https://vanilla1.wpmt.co") } From a5e97d2aec0e620dca8159d8d4b3c114d5f37c52 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 28 Nov 2025 13:26:39 +1300 Subject: [PATCH 4/4] Fix swiftlint issues --- .../swift/Sources/wordpress-api/SafeRequestExecutor.swift | 8 ++++---- native/swift/Tests/wordpress-api/LoginTests.swift | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift index d42ec1877..a9c6f5d3b 100644 --- a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -71,8 +71,8 @@ public final class WpRequestExecutor: SafeRequestExecutor { } } - public func allowSSL(altNames: [String], forCommonName cn: String) { - executorDelegate.allowSSL(altNames: altNames, forCommonName: cn) + public func allowSSL(altNames: [String], forCommonName name: String) { + executorDelegate.allowSSL(altNames: altNames, forCommonName: name) } func perform(_ request: NetworkRequestContent) async -> Result { @@ -292,9 +292,9 @@ private final class RequestExecutorDelegate: NSObject, URLSessionTaskDelegate, @ } } - func allowSSL(altNames: [String], forCommonName cn: String) { + func allowSSL(altNames: [String], forCommonName name: String) { lock.withLock { - additionalAlternativeNames[cn, default: []].formUnion(altNames) + additionalAlternativeNames[name, default: []].formUnion(altNames) } } diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index f2ccc6ef7..02cac16d9 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -300,7 +300,6 @@ class LoginTests { _ = try await self.client.findLoginUrl(forSite: "https://vanilla1.wpmt.co") } - private func getApplicationPasswordsNotSupportedReason( from error: any Error ) throws -> ApplicationPasswordsNotSupportedReason? {