<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0

import Foundation
import Shared
import Storage

private let log = Logger.browserLogger

class Authenticator {
    fileprivate static let MaxAuthenticationAttempts = 3

    static func handleAuthRequest(_ viewController: UIViewController, challenge: URLAuthenticationChallenge, loginsHelper: LoginsHelper?) -> Deferred<Maybe<LoginEntry>> {
        // If there have already been too many login attempts, we'll just fail.
        if challenge.previousFailureCount >= Authenticator.MaxAuthenticationAttempts {
            return deferMaybe(LoginRecordError(description: "Too many attempts to open site"))
        }

        var credential = challenge.proposedCredential

        // If we were passed an initial set of credentials from iOS, try and use them.
        if let proposed = credential {
            if !(proposed.user?.isEmpty ?? true) {
                if challenge.previousFailureCount == 0 {
                    return deferMaybe(LoginEntry(credentials: proposed, protectionSpace: challenge.protectionSpace))
                }
            } else {
                credential = nil
            }
        }

        // If we have some credentials, we'll show a prompt with them.
        if let credential = credential {
            return promptForUsernamePassword(viewController, credentials: credential, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper)
        }

        // Otherwise, try to look them up and show the prompt.
        if let loginsHelper = loginsHelper {
            return findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: loginsHelper.logins).bindQueue(.main) { result in
                guard let credentials = result.successValue else {
                    return deferMaybe(result.failureValue ?? LoginRecordError(description: "Unknown error when finding credentials"))
                }
                return self.promptForUsernamePassword(viewController, credentials: credentials, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper)
            }
        }

        // No credentials, so show an empty prompt.
        return self.promptForUsernamePassword(viewController, credentials: nil, protectionSpace: challenge.protectionSpace, loginsHelper: nil)
    }

    static func findMatchingCredentialsForChallenge(_ challenge: URLAuthenticationChallenge, fromLoginsProvider loginsProvider: RustLogins) -> Deferred<Maybe<URLCredential?>> {
        return loginsProvider.getLoginsForProtectionSpace(challenge.protectionSpace) >>== { cursor in
            guard cursor.count >= 1 else { return deferMaybe(nil) }

            let logins = cursor.compactMap {
                // HTTP Auth must have nil formSubmitUrl and a non-nil httpRealm.
                return $0?.formSubmitUrl == nil && $0?.httpRealm != nil ? $0 : nil
            }
            var credentials: URLCredential?

            // It is possible that we might have duplicate entries since we match against host and scheme://host.
            // This is a side effect of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103.
            if logins.count > 1 {
                credentials = (logins.find { login in
                    (login.protectionSpace.`protocol` == challenge.protectionSpace.`protocol`) && !login.hasMalformedHostname
                })?.credentials

                let malformedGUIDs: [GUID] = logins.compactMap { login in
                    if login.hasMalformedHostname {
                        return login.id
                    }
                    return nil
                }
                loginsProvider.deleteLogins(ids: malformedGUIDs).upon { _ in log.debug("Removed malformed logins.") }
            }

            // Found a single entry but the schemes don't match. This is a result of a schemeless entry that we
            // saved in a previous iteration of the app so we need to migrate it. We only care about the
            // the username/password so we can rewrite the scheme to be correct.
            else if logins.count == 1 && logins[0].protectionSpace.`protocol` != challenge.protectionSpace.`protocol` {
                let login = logins[0]
                credentials = login.credentials
                let new = LoginEntry(credentials: login.credentials, protectionSpace: challenge.protectionSpace)
                return loginsProvider.updateLogin(id: login.id, login: new)
                    >>> { deferMaybe(credentials) }
            }

            // Found a single entry that matches the scheme and host - good to go.
            else {
                credentials = logins[0].credentials
            }

            return deferMaybe(credentials)
        }
    }

    fileprivate static func promptForUsernamePassword(
        _ viewController: UIViewController,
        credentials: URLCredential?,
        protectionSpace: URLProtectionSpace,
        loginsHelper: LoginsHelper?
    ) -> Deferred<Maybe<LoginEntry>> {
        if protectionSpace.host.isEmpty {
            print("Unable to show a password prompt without a hostname")
            return deferMaybe(LoginRecordError(description: "Unable to show a password prompt without a hostname"))
        }

        let deferred = Deferred<Maybe<LoginEntry>>()
        let alert: AlertController
        let title: String = .AuthenticatorPromptTitle
        if !(protectionSpace.realm?.isEmpty ?? true) {
            let msg: String = .AuthenticatorPromptRealmMessage
            let formatted = NSString(format: msg as NSString, protectionSpace.host, protectionSpace.realm ?? "") as String
            alert = AlertController(title: title, message: formatted, preferredStyle: .alert)
        } else {
            let msg: String = .AuthenticatorPromptEmptyRealmMessage
            let formatted = NSString(format: msg as NSString, protectionSpace.host) as String
            alert = AlertController(title: title, message: formatted, preferredStyle: .alert)
        }<commit_msg>Bugfix FXIOS-5245 [v107.1] Add a safety check and track warning in Authenticator (#12373)<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0

import Foundation
import Shared
import Storage

private let log = Logger.browserLogger

class Authenticator {
    fileprivate static let MaxAuthenticationAttempts = 3

    static func handleAuthRequest(_ viewController: UIViewController, challenge: URLAuthenticationChallenge, loginsHelper: LoginsHelper?) -> Deferred<Maybe<LoginEntry>> {
        // If there have already been too many login attempts, we'll just fail.
        if challenge.previousFailureCount >= Authenticator.MaxAuthenticationAttempts {
            return deferMaybe(LoginRecordError(description: "Too many attempts to open site"))
        }

        var credential = challenge.proposedCredential

        // If we were passed an initial set of credentials from iOS, try and use them.
        if let proposed = credential {
            if !(proposed.user?.isEmpty ?? true) {
                if challenge.previousFailureCount == 0 {
                    return deferMaybe(LoginEntry(credentials: proposed, protectionSpace: challenge.protectionSpace))
                }
            } else {
                credential = nil
            }
        }

        // If we have some credentials, we'll show a prompt with them.
        if let credential = credential {
            return promptForUsernamePassword(viewController, credentials: credential, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper)
        }

        // Otherwise, try to look them up and show the prompt.
        if let loginsHelper = loginsHelper {
            return findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: loginsHelper.logins).bindQueue(.main) { result in
                guard let credentials = result.successValue else {
                    return deferMaybe(result.failureValue ?? LoginRecordError(description: "Unknown error when finding credentials"))
                }
                return self.promptForUsernamePassword(viewController, credentials: credentials, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper)
            }
        }

        // No credentials, so show an empty prompt.
        return self.promptForUsernamePassword(viewController, credentials: nil, protectionSpace: challenge.protectionSpace, loginsHelper: nil)
    }

    static func findMatchingCredentialsForChallenge(_ challenge: URLAuthenticationChallenge, fromLoginsProvider loginsProvider: RustLogins) -> Deferred<Maybe<URLCredential?>> {
        return loginsProvider.getLoginsForProtectionSpace(challenge.protectionSpace) >>== { cursor in
            guard cursor.count >= 1 else { return deferMaybe(nil) }

            let logins = cursor.compactMap {
                // HTTP Auth must have nil formSubmitUrl and a non-nil httpRealm.
                return $0?.formSubmitUrl == nil && $0?.httpRealm != nil ? $0 : nil
            }
            var credentials: URLCredential?

            // It is possible that we might have duplicate entries since we match against host and scheme://host.
            // This is a side effect of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103.
            if logins.count > 1 {
                credentials = (logins.find { login in
                    (login.protectionSpace.`protocol` == challenge.protectionSpace.`protocol`) && !login.hasMalformedHostname
                })?.credentials

                let malformedGUIDs: [GUID] = logins.compactMap { login in
                    if login.hasMalformedHostname {
                        return login.id
                    }
                    return nil
                }
                loginsProvider.deleteLogins(ids: malformedGUIDs).upon { _ in log.debug("Removed malformed logins.") }
            }

            // Found a single entry but the schemes don't match. This is a result of a schemeless entry that we
            // saved in a previous iteration of the app so we need to migrate it. We only care about the
            // the username/password so we can rewrite the scheme to be correct.
            else if logins.count == 1 && logins[0].protectionSpace.`protocol` != challenge.protectionSpace.`protocol` {
                let login = logins[0]
                credentials = login.credentials
                let new = LoginEntry(credentials: login.credentials, protectionSpace: challenge.protectionSpace)
                return loginsProvider.updateLogin(id: login.id, login: new)
                    >>> { deferMaybe(credentials) }
            }

            // Found a single entry that matches the scheme and host - good to go.
            else if logins.count == 1 {
                credentials = logins[0].credentials
            } else {
                SentryIntegration.shared.send(message: "No logins found for Authenticator", severity: .warning)
            }

            return deferMaybe(credentials)
        }
    }

    fileprivate static func promptForUsernamePassword(
        _ viewController: UIViewController,
        credentials: URLCredential?,
        protectionSpace: URLProtectionSpace,
        loginsHelper: LoginsHelper?
    ) -> Deferred<Maybe<LoginEntry>> {
        if protectionSpace.host.isEmpty {
            print("Unable to show a password prompt without a hostname")
            return deferMaybe(LoginRecordError(description: "Unable to show a password prompt without a hostname"))
        }

        let deferred = Deferred<Maybe<LoginEntry>>()
        let alert: AlertController
        let title: String = .AuthenticatorPromptTitle
        if !(protectionSpace.realm?.isEmpty ?? true) {
            let msg: String = .AuthenticatorPromptRealmMessage
            let formatted = NSString(format: msg as NSString, protectionSpace.host, protectionSpace.realm ?? "") as String
            alert = AlertController(title: title, message: formatted, preferredStyle: .alert)
        } else {
            let msg: String = .AuthenticatorPromptEmptyRealmMessage
            let formatted = NSString(format: msg as NSString, protectionSpace.host) as String
            alert = AlertController(title: title, message: formatted, preferredStyle: .alert)
        }
{'fraction_non_alphanumeric': 0.07494033412887828, 'fraction_numerical': 0.004614160700079554, 'mean_word_length': 2.377485222998388, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 41, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2639323', 'n_tokens_mistral': 3142, 'n_tokens_neox': 2932, 'n_words': 972}