Skip to content
Open
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
40 changes: 40 additions & 0 deletions native/swift/Sources/wordpress-api/SafeRequestExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public final class WpRequestExecutor: SafeRequestExecutor {
}
}

public func allowSSL(altNames: [String], forCommonName name: String) {
executorDelegate.allowSSL(altNames: altNames, forCommonName: name)
}

func perform(_ request: NetworkRequestContent) async -> Result<WpNetworkResponse, RequestExecutionError> {
do {
let (data, response) = try await request.perform(
Expand Down Expand Up @@ -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<String>] = [:]

init(redirects: [String: [WpRedirect]] = [:]) {
self.redirects = redirects
Expand All @@ -283,6 +292,37 @@ private final class RequestExecutorDelegate: NSObject, URLSessionTaskDelegate, @
}
}

func allowSSL(altNames: [String], forCommonName name: String) {
lock.withLock {
additionalAlternativeNames[name, default: []].formUnion(altNames)
}
}

private func alternateNames(forCertificate cert: SslCertificateInfo) -> Set<String> {
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)
}
Expand Down
49 changes: 12 additions & 37 deletions native/swift/Tests/wordpress-api/LoginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you manually trust a certificate, it's trusted for the entire lifetime of the URLSession, which means that using the .shared URLSession would cause unit tests to interfere with each other.


@Test("Login Spec Example 1: Valid URL")
func testValidURL() async throws {
Expand Down Expand Up @@ -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
Expand All @@ -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")

Expand Down Expand Up @@ -285,44 +285,19 @@ 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 vanilla.wpmt.co certificate.
_ = try await self.client.findLoginUrl(forSite: "https://vanilla1.wpmt.co")
}

private func getApplicationPasswordsNotSupportedReason(
Expand Down