@@ -100,15 +100,16 @@ enum SiteCredentialLoginError: LocalizedError {
100100/// This use case handles site credential login without the need to use XMLRPC API.
101101/// Steps for login:
102102/// - Make a request to the site wp-login.php with a redirect to the nonce retrieval URL.
103- /// - Upon redirect, cancel the request and verify if the redirect URL is the nonce retrieval URL.
104- /// - If it is, make a request to retrieve nonce at that URL, the login succeeds if this is successful.
103+ /// - If the redirect succeeds with a nonce in the response, login is successful.
104+ /// - If the request does not redirect or the redirect fails, login fails.
105+ /// Ref: pe5sF9-1iQ-p2
105106///
106107final class SiteCredentialLoginUseCase : NSObject , SiteCredentialLoginProtocol {
107108 private let siteURL : String
108109 private let cookieJar : HTTPCookieStorage
109110 private var successHandler : ( ( ) -> Void ) ?
110111 private var errorHandler : ( ( SiteCredentialLoginError ) -> Void ) ?
111- private lazy var session = URLSession ( configuration: . default, delegate : self , delegateQueue : nil )
112+ private lazy var session = URLSession ( configuration: . default)
112113
113114 init ( siteURL: String ,
114115 cookieJar: HTTPCookieStorage = HTTPCookieStorage . shared) {
@@ -134,10 +135,9 @@ final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol {
134135 Task { @MainActor in
135136 do {
136137 try await startLogin ( with: loginRequest)
138+ successHandler ? ( )
137139 } catch let error as SiteCredentialLoginError {
138140 errorHandler ? ( error)
139- } catch let nsError as NSError where nsError. domain == NSURLErrorDomain && nsError. code == - 999 {
140- /// login request is cancelled upon redirect, ignore this error
141141 } catch {
142142 errorHandler ? ( . genericFailure( underlyingError: error as NSError ) )
143143 }
@@ -160,10 +160,26 @@ private extension SiteCredentialLoginUseCase {
160160 throw SiteCredentialLoginError . invalidLoginResponse
161161 }
162162
163- switch response. statusCode {
164- case 404 :
163+ /// The login request comes with a redirect header to nonce retrieval URL.
164+ /// If we get a response from this URL, that means the redirect is successful.
165+ /// We need to check the result of this redirect first to determine if login is successful.
166+ let isNonceUrl = response. url? . absoluteString. hasSuffix ( Constants . wporgNoncePath) == true
167+
168+ switch ( isNonceUrl, response. statusCode) {
169+ case ( true , 200 ) :
170+ if let nonceString = String ( data: data, encoding: . utf8) ,
171+ nonceString. isValidNonce ( ) {
172+ // success!
173+ return
174+ } else {
175+ throw SiteCredentialLoginError . invalidLoginResponse
176+ }
177+ case ( true , 404 ) :
178+ throw SiteCredentialLoginError . inaccessibleAdminPage
179+ case ( false , 404 ) :
165180 throw SiteCredentialLoginError . inaccessibleLoginPage
166- case 200 :
181+ case ( false , 200 ) :
182+ // 200 for the login URL, which means a failure
167183 guard let html = String ( data: data, encoding: . utf8) else {
168184 throw SiteCredentialLoginError . invalidLoginResponse
169185 }
@@ -180,39 +196,6 @@ private extension SiteCredentialLoginUseCase {
180196 }
181197 }
182198
183- @MainActor
184- func checkRedirect( url: URL ? ) async {
185- guard let url, url. absoluteString. hasSuffix ( Constants . wporgNoncePath) ,
186- let nonceRetrievalURL = URL ( string: siteURL + Constants. adminPath + Constants. wporgNoncePath) else {
187- errorHandler ? ( . invalidLoginResponse)
188- return
189- }
190- do {
191- let nonceRequest = try URLRequest ( url: nonceRetrievalURL, method: . get)
192- try await checkAdminPageAccess ( with: nonceRequest)
193- successHandler ? ( )
194- } catch let error as SiteCredentialLoginError {
195- errorHandler ? ( error)
196- } catch {
197- errorHandler ? ( . genericFailure( underlyingError: error as NSError ) )
198- }
199- }
200-
201- func checkAdminPageAccess( with nonceRequest: URLRequest ) async throws {
202- let ( _, response) = try await session. data ( for: nonceRequest)
203- guard let response = response as? HTTPURLResponse else {
204- throw SiteCredentialLoginError . invalidLoginResponse
205- }
206- switch response. statusCode {
207- case 200 :
208- return // success 🎉
209- case 404 :
210- throw SiteCredentialLoginError . inaccessibleAdminPage
211- default :
212- throw SiteCredentialLoginError . unacceptableStatusCode ( code: response. statusCode)
213- }
214- }
215-
216199 func buildLoginRequest( username: String , password: String ) -> URLRequest ? {
217200 guard let loginURL = URL ( string: siteURL + Constants. loginPath) else {
218201 return nil
@@ -239,18 +222,6 @@ private extension SiteCredentialLoginUseCase {
239222 }
240223}
241224
242- extension SiteCredentialLoginUseCase : URLSessionDataDelegate {
243- func urlSession( _ session: URLSession ,
244- task: URLSessionTask ,
245- willPerformHTTPRedirection response: HTTPURLResponse ,
246- newRequest request: URLRequest ) async -> URLRequest ? {
247- // Disables redirect and check if the redirect is correct
248- task. cancel ( )
249- await checkRedirect ( url: request. url)
250- return nil
251- }
252- }
253-
254225extension SiteCredentialLoginUseCase {
255226 enum Constants {
256227 static let loginPath = " /wp-login.php "
@@ -310,4 +281,15 @@ private extension String {
310281 func hasInvalidCredentialsPattern( ) -> Bool {
311282 contains ( " document.querySelector('form').classList.add('shake') " )
312283 }
284+
285+ /// Validates if the string matches the expected nonce format.
286+ /// A valid nonce should contain at least 2 alphanumeric characters.
287+ ///
288+ func isValidNonce( ) -> Bool {
289+ guard let regex = try ? Regex ( " ^[0-9a-zA-Z]{2,}$ " ) else {
290+ DDLogError ( " ⚠️ Invalid regex pattern " )
291+ return false
292+ }
293+ return wholeMatch ( of: regex) != nil
294+ }
313295}
0 commit comments