One caller in modifyLogin is switched to synchronous searchLogins, we can switch this later to add an asyncronous version of modifyLogin Differential Revision: https://phabricator.services.mozilla.com/D184840
1125 lines
33 KiB
JavaScript
1125 lines
33 KiB
JavaScript
/* 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 { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
|
|
import { PromptUtils } from "resource://gre/modules/PromptUtils.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gPrompterService",
|
|
"@mozilla.org/login-manager/prompter;1",
|
|
Ci.nsILoginManagerPrompter
|
|
);
|
|
|
|
/* eslint-disable block-scoped-var, no-var */
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
|
});
|
|
|
|
const LoginInfo = Components.Constructor(
|
|
"@mozilla.org/login-manager/loginInfo;1",
|
|
"nsILoginInfo",
|
|
"init"
|
|
);
|
|
|
|
/**
|
|
* A helper module to prevent modal auth prompt abuse.
|
|
*/
|
|
const PromptAbuseHelper = {
|
|
getBaseDomainOrFallback(hostname) {
|
|
try {
|
|
return Services.eTLD.getBaseDomainFromHost(hostname);
|
|
} catch (e) {
|
|
return hostname;
|
|
}
|
|
},
|
|
|
|
incrementPromptAbuseCounter(baseDomain, browser) {
|
|
if (!browser) {
|
|
return;
|
|
}
|
|
|
|
if (!browser.authPromptAbuseCounter) {
|
|
browser.authPromptAbuseCounter = {};
|
|
}
|
|
|
|
if (!browser.authPromptAbuseCounter[baseDomain]) {
|
|
browser.authPromptAbuseCounter[baseDomain] = 0;
|
|
}
|
|
|
|
browser.authPromptAbuseCounter[baseDomain] += 1;
|
|
},
|
|
|
|
resetPromptAbuseCounter(baseDomain, browser) {
|
|
if (!browser || !browser.authPromptAbuseCounter) {
|
|
return;
|
|
}
|
|
|
|
browser.authPromptAbuseCounter[baseDomain] = 0;
|
|
},
|
|
|
|
hasReachedAbuseLimit(baseDomain, browser) {
|
|
if (!browser || !browser.authPromptAbuseCounter) {
|
|
return false;
|
|
}
|
|
|
|
let abuseCounter = browser.authPromptAbuseCounter[baseDomain];
|
|
// Allow for setting -1 to turn the feature off.
|
|
if (this.abuseLimit < 0) {
|
|
return false;
|
|
}
|
|
return !!abuseCounter && abuseCounter >= this.abuseLimit;
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
PromptAbuseHelper,
|
|
"abuseLimit",
|
|
"prompts.authentication_dialog_abuse_limit"
|
|
);
|
|
|
|
/**
|
|
* Implements nsIPromptFactory
|
|
*
|
|
* Invoked by [toolkit/components/prompts/src/Prompter.jsm]
|
|
*/
|
|
export function LoginManagerAuthPromptFactory() {
|
|
Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
|
|
}
|
|
|
|
LoginManagerAuthPromptFactory.prototype = {
|
|
classID: Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIPromptFactory",
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
|
|
// Tracks pending auth prompts per top level browser and hash key.
|
|
// browser -> hashkey -> prompt
|
|
// This enables us to consolidate auth prompts with the same browser and
|
|
// hashkey (level, origin, realm).
|
|
_pendingPrompts: new WeakMap(),
|
|
_pendingSavePrompts: new WeakMap(),
|
|
// We use a separate bucket for when we don't have a browser.
|
|
// _noBrowser -> hashkey -> prompt
|
|
_noBrowser: {},
|
|
// Promise used to defer prompts if the password manager isn't ready when
|
|
// they're called.
|
|
_uiBusyPromise: null,
|
|
_uiBusyResolve: null,
|
|
|
|
observe(subject, topic, data) {
|
|
this.log(`Observed topic: ${topic}.`);
|
|
if (topic == "passwordmgr-crypto-login") {
|
|
// Show the deferred prompters.
|
|
this._uiBusyResolve?.();
|
|
}
|
|
},
|
|
|
|
getPrompt(aWindow, aIID) {
|
|
var prompt = new LoginManagerAuthPrompter().QueryInterface(aIID);
|
|
prompt.init(aWindow, this);
|
|
return prompt;
|
|
},
|
|
|
|
getPendingPrompt(browser, hashKey) {
|
|
// If there is already a matching auth prompt which has no browser
|
|
// associated we can reuse it. This way we avoid showing tab level prompts
|
|
// when there is already a pending window prompt.
|
|
let pendingNoBrowserPrompt = this._pendingPrompts
|
|
.get(this._noBrowser)
|
|
?.get(hashKey);
|
|
if (pendingNoBrowserPrompt) {
|
|
return pendingNoBrowserPrompt;
|
|
}
|
|
return this._pendingPrompts.get(browser)?.get(hashKey);
|
|
},
|
|
|
|
_dismissPendingSavePrompt(browser) {
|
|
this._pendingSavePrompts.get(browser)?.dismiss();
|
|
this._pendingSavePrompts.delete(browser);
|
|
},
|
|
|
|
_setPendingSavePrompt(browser, prompt) {
|
|
this._pendingSavePrompts.set(browser, prompt);
|
|
},
|
|
|
|
_setPendingPrompt(prompt, hashKey) {
|
|
let browser = prompt.prompter.browser || this._noBrowser;
|
|
let hashToPrompt = this._pendingPrompts.get(browser);
|
|
if (!hashToPrompt) {
|
|
hashToPrompt = new Map();
|
|
this._pendingPrompts.set(browser, hashToPrompt);
|
|
}
|
|
hashToPrompt.set(hashKey, prompt);
|
|
},
|
|
|
|
_removePendingPrompt(prompt, hashKey) {
|
|
let browser = prompt.prompter.browser || this._noBrowser;
|
|
let hashToPrompt = this._pendingPrompts.get(browser);
|
|
if (!hashToPrompt) {
|
|
return;
|
|
}
|
|
hashToPrompt.delete(hashKey);
|
|
if (!hashToPrompt.size) {
|
|
this._pendingPrompts.delete(browser);
|
|
}
|
|
},
|
|
|
|
async _waitForLoginsUI(prompt) {
|
|
await this._uiBusyPromise;
|
|
|
|
let [origin, httpRealm] = prompt.prompter._getAuthTarget(
|
|
prompt.channel,
|
|
prompt.authInfo
|
|
);
|
|
|
|
// No UI to wait for.
|
|
if (!Services.logins.uiBusy) {
|
|
return;
|
|
}
|
|
|
|
let hasLogins = Services.logins.countLogins(origin, null, httpRealm) > 0;
|
|
if (
|
|
!hasLogins &&
|
|
lazy.LoginHelper.schemeUpgrades &&
|
|
origin.startsWith("https://")
|
|
) {
|
|
let httpOrigin = origin.replace(/^https:\/\//, "http://");
|
|
hasLogins = Services.logins.countLogins(httpOrigin, null, httpRealm) > 0;
|
|
}
|
|
// We don't depend on saved logins.
|
|
if (!hasLogins) {
|
|
return;
|
|
}
|
|
|
|
this.log("Waiting for primary password UI.");
|
|
|
|
this._uiBusyPromise = new Promise(resolve => {
|
|
this._uiBusyResolve = resolve;
|
|
});
|
|
await this._uiBusyPromise;
|
|
},
|
|
|
|
async _doAsyncPrompt(prompt, hashKey) {
|
|
this._setPendingPrompt(prompt, hashKey);
|
|
|
|
// UI might be busy due to the primary password dialog. Wait for it to close.
|
|
await this._waitForLoginsUI(prompt);
|
|
|
|
let ok = false;
|
|
let promptAborted = false;
|
|
try {
|
|
this.log(`Performing the prompt for ${hashKey}.`);
|
|
ok = await prompt.prompter.promptAuthInternal(
|
|
prompt.channel,
|
|
prompt.level,
|
|
prompt.authInfo
|
|
);
|
|
} catch (e) {
|
|
if (
|
|
e instanceof Components.Exception &&
|
|
e.result == Cr.NS_ERROR_NOT_AVAILABLE
|
|
) {
|
|
this.log("Bypassed, UI is not available in this context.");
|
|
// Prompts throw NS_ERROR_NOT_AVAILABLE if they're aborted.
|
|
promptAborted = true;
|
|
} else {
|
|
console.error("LoginManagerAuthPrompter: _doAsyncPrompt", e);
|
|
}
|
|
}
|
|
|
|
this._removePendingPrompt(prompt, hashKey);
|
|
|
|
// Handle callbacks
|
|
for (var consumer of prompt.consumers) {
|
|
if (!consumer.callback) {
|
|
// Not having a callback means that consumer didn't provide it
|
|
// or canceled the notification
|
|
continue;
|
|
}
|
|
|
|
this.log(`Calling back to callback: ${consumer.callback} ok: ${ok}.`);
|
|
try {
|
|
if (ok) {
|
|
consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
|
|
} else {
|
|
consumer.callback.onAuthCancelled(consumer.context, !promptAborted);
|
|
}
|
|
} catch (e) {
|
|
/* Throw away exceptions caused by callback */
|
|
}
|
|
}
|
|
},
|
|
}; // end of LoginManagerAuthPromptFactory implementation
|
|
|
|
ChromeUtils.defineLazyGetter(
|
|
LoginManagerAuthPromptFactory.prototype,
|
|
"log",
|
|
() => {
|
|
let logger = lazy.LoginHelper.createLogger("LoginManagerAuthPromptFactory");
|
|
return logger.log.bind(logger);
|
|
}
|
|
);
|
|
|
|
/* ==================== LoginManagerAuthPrompter ==================== */
|
|
|
|
/**
|
|
* Implements interfaces for prompting the user to enter/save/change auth info.
|
|
*
|
|
* nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
|
|
*
|
|
* Note this implementation no longer provides `nsIAuthPrompt.promptPassword()`
|
|
* and `nsIAuthPrompt.promptUsernameAndPassword()`. Use their async
|
|
* counterparts `asyncPromptPassword` and `asyncPromptUsernameAndPassword`
|
|
* instead.
|
|
*
|
|
* nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
|
|
* (eg HTTP Authenticate, FTP login).
|
|
*
|
|
* nsILoginManagerAuthPrompter: Used by consumers to indicate which tab/window a
|
|
* prompt should appear on.
|
|
*/
|
|
export function LoginManagerAuthPrompter() {}
|
|
|
|
LoginManagerAuthPrompter.prototype = {
|
|
classID: Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIAuthPrompt",
|
|
"nsIAuthPrompt2",
|
|
"nsILoginManagerAuthPrompter",
|
|
]),
|
|
|
|
_factory: null,
|
|
_chromeWindow: null,
|
|
_browser: null,
|
|
|
|
__strBundle: null, // String bundle for L10N
|
|
get _strBundle() {
|
|
if (!this.__strBundle) {
|
|
this.__strBundle = Services.strings.createBundle(
|
|
"chrome://passwordmgr/locale/passwordmgr.properties"
|
|
);
|
|
if (!this.__strBundle) {
|
|
throw new Error("String bundle for Login Manager not present!");
|
|
}
|
|
}
|
|
|
|
return this.__strBundle;
|
|
},
|
|
|
|
__ellipsis: null,
|
|
get _ellipsis() {
|
|
if (!this.__ellipsis) {
|
|
this.__ellipsis = "\u2026";
|
|
try {
|
|
this.__ellipsis = Services.prefs.getComplexValue(
|
|
"intl.ellipsis",
|
|
Ci.nsIPrefLocalizedString
|
|
).data;
|
|
} catch (e) {}
|
|
}
|
|
return this.__ellipsis;
|
|
},
|
|
|
|
// Whether we are in private browsing mode
|
|
get _inPrivateBrowsing() {
|
|
if (this._chromeWindow) {
|
|
return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
|
|
}
|
|
// If we don't that we're in private browsing mode if the caller did
|
|
// not provide a window. The callers which really care about this
|
|
// will indeed pass down a window to us, and for those who don't,
|
|
// we can just assume that we don't want to save the entered login
|
|
// information.
|
|
this.log("We have no chromeWindow so assume we're in a private context.");
|
|
return true;
|
|
},
|
|
|
|
get _allowRememberLogin() {
|
|
if (!this._inPrivateBrowsing) {
|
|
return true;
|
|
}
|
|
return lazy.LoginHelper.privateBrowsingCaptureEnabled;
|
|
},
|
|
|
|
/* ---------- nsIAuthPrompt prompts ---------- */
|
|
|
|
/**
|
|
* Wrapper around the prompt service prompt. Saving random fields here
|
|
* doesn't really make sense and therefore isn't implemented.
|
|
*/
|
|
prompt(
|
|
aDialogTitle,
|
|
aText,
|
|
aPasswordRealm,
|
|
aSavePassword,
|
|
aDefaultText,
|
|
aResult
|
|
) {
|
|
if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
|
|
throw new Components.Exception(
|
|
"prompt only supports SAVE_PASSWORD_NEVER",
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
if (aDefaultText) {
|
|
aResult.value = aDefaultText;
|
|
}
|
|
|
|
return Services.prompt.prompt(
|
|
this._chromeWindow,
|
|
aDialogTitle,
|
|
aText,
|
|
aResult,
|
|
null,
|
|
{}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Looks up a username and password in the database. Will prompt the user
|
|
* with a dialog, even if a username and password are found.
|
|
*/
|
|
async asyncPromptUsernameAndPassword(
|
|
aDialogTitle,
|
|
aText,
|
|
aPasswordRealm,
|
|
aSavePassword,
|
|
aUsername,
|
|
aPassword
|
|
) {
|
|
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
|
|
throw new Components.Exception(
|
|
"asyncPromptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
let foundLogins = null;
|
|
let canRememberLogin = false;
|
|
var selectedLogin = null;
|
|
var [origin, realm] = this._getRealmInfo(aPasswordRealm);
|
|
|
|
// If origin is null, we can't save this login.
|
|
if (origin) {
|
|
if (this._allowRememberLogin) {
|
|
canRememberLogin =
|
|
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
|
|
Services.logins.getLoginSavingEnabled(origin);
|
|
}
|
|
|
|
// Look for existing logins.
|
|
// We don't use searchLoginsAsync here and in asyncPromptPassword
|
|
// because of bug 1848682
|
|
let matchData = lazy.LoginHelper.newPropertyBag({
|
|
origin,
|
|
httpRealm: realm,
|
|
});
|
|
foundLogins = Services.logins.searchLogins(matchData);
|
|
|
|
// XXX Like the original code, we can't deal with multiple
|
|
// account selection. (bug 227632)
|
|
if (foundLogins.length) {
|
|
selectedLogin = foundLogins[0];
|
|
|
|
// If the caller provided a username, try to use it. If they
|
|
// provided only a password, this will try to find a password-only
|
|
// login (or return null if none exists).
|
|
if (aUsername.value) {
|
|
selectedLogin = this._repickSelectedLogin(
|
|
foundLogins,
|
|
aUsername.value
|
|
);
|
|
}
|
|
|
|
if (selectedLogin) {
|
|
aUsername.value = selectedLogin.username;
|
|
// If the caller provided a password, prefer it.
|
|
if (!aPassword.value) {
|
|
aPassword.value = selectedLogin.password;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let autofilled = !!aPassword.value;
|
|
var ok = Services.prompt.promptUsernameAndPassword(
|
|
this._chromeWindow,
|
|
aDialogTitle,
|
|
aText,
|
|
aUsername,
|
|
aPassword
|
|
);
|
|
|
|
if (!ok || !canRememberLogin) {
|
|
return ok;
|
|
}
|
|
|
|
if (!aPassword.value) {
|
|
this.log("No password entered, so won't offer to save.");
|
|
return ok;
|
|
}
|
|
|
|
// XXX We can't prompt with multiple logins yet (bug 227632), so
|
|
// the entered login might correspond to an existing login
|
|
// other than the one we originally selected.
|
|
selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
|
|
|
|
// If we didn't find an existing login, or if the username
|
|
// changed, save as a new login.
|
|
let newLogin = new LoginInfo(
|
|
origin,
|
|
null,
|
|
realm,
|
|
aUsername.value,
|
|
aPassword.value
|
|
);
|
|
if (!selectedLogin) {
|
|
// add as new
|
|
this.log(`New login seen for: ${realm}.`);
|
|
await Services.logins.addLoginAsync(newLogin);
|
|
} else if (aPassword.value != selectedLogin.password) {
|
|
// update password
|
|
this.log(`Updating password for ${realm}.`);
|
|
this._updateLogin(selectedLogin, newLogin);
|
|
} else {
|
|
this.log("Login unchanged, no further action needed.");
|
|
Services.logins.recordPasswordUse(
|
|
selectedLogin,
|
|
this._inPrivateBrowsing,
|
|
"prompt_login",
|
|
autofilled
|
|
);
|
|
}
|
|
|
|
return ok;
|
|
},
|
|
|
|
/**
|
|
* If a password is found in the database for the password realm, it is
|
|
* returned straight away without displaying a dialog.
|
|
*
|
|
* If a password is not found in the database, the user will be prompted
|
|
* with a dialog with a text field and ok/cancel buttons. If the user
|
|
* allows it, then the password will be saved in the database.
|
|
*/
|
|
async asyncPromptPassword(
|
|
aDialogTitle,
|
|
aText,
|
|
aPasswordRealm,
|
|
aSavePassword,
|
|
aPassword
|
|
) {
|
|
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
|
|
throw new Components.Exception(
|
|
"promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
|
|
|
|
username = decodeURIComponent(username);
|
|
|
|
let canRememberLogin = false;
|
|
// If origin is null, we can't save this login.
|
|
if (origin && !this._inPrivateBrowsing) {
|
|
canRememberLogin =
|
|
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
|
|
Services.logins.getLoginSavingEnabled(origin);
|
|
if (!aPassword.value) {
|
|
// Look for existing logins.
|
|
let matchData = lazy.LoginHelper.newPropertyBag({
|
|
origin,
|
|
httpRealm: realm,
|
|
});
|
|
let foundLogins = Services.logins.searchLogins(matchData);
|
|
|
|
// XXX Like the original code, we can't deal with multiple
|
|
// account selection (bug 227632). We can deal with finding the
|
|
// account based on the supplied username - but in this case we'll
|
|
// just return the first match.
|
|
for (var i = 0; i < foundLogins.length; ++i) {
|
|
if (foundLogins[i].username == username) {
|
|
aPassword.value = foundLogins[i].password;
|
|
// wallet returned straight away, so this mimics that code
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var ok = Services.prompt.promptPassword(
|
|
this._chromeWindow,
|
|
aDialogTitle,
|
|
aText,
|
|
aPassword
|
|
);
|
|
|
|
if (ok && canRememberLogin && aPassword.value) {
|
|
let newLogin = new LoginInfo(
|
|
origin,
|
|
null,
|
|
realm,
|
|
username,
|
|
aPassword.value
|
|
);
|
|
|
|
this.log(`New login seen for ${realm}.`);
|
|
|
|
await Services.logins.addLoginAsync(newLogin);
|
|
}
|
|
|
|
return ok;
|
|
},
|
|
|
|
/* ---------- nsIAuthPrompt helpers ---------- */
|
|
|
|
/**
|
|
* Given aRealmString, such as "http://user@example.com/foo", returns an
|
|
* array of:
|
|
* - the formatted origin
|
|
* - the realm (origin + path)
|
|
* - the username, if present
|
|
*
|
|
* If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
|
|
* channels, e.g. "example.com:80 (httprealm)", null is returned for all
|
|
* arguments to let callers know the login can't be saved because we don't
|
|
* know whether it's http or https.
|
|
*/
|
|
_getRealmInfo(aRealmString) {
|
|
var httpRealm = /^.+ \(.+\)$/;
|
|
if (httpRealm.test(aRealmString)) {
|
|
return [null, null, null];
|
|
}
|
|
|
|
var uri = Services.io.newURI(aRealmString);
|
|
var pathname = "";
|
|
|
|
if (uri.pathQueryRef != "/") {
|
|
pathname = uri.pathQueryRef;
|
|
}
|
|
|
|
var formattedOrigin = this._getFormattedOrigin(uri);
|
|
|
|
return [formattedOrigin, formattedOrigin + pathname, uri.username];
|
|
},
|
|
|
|
async promptAuthInternal(aChannel, aLevel, aAuthInfo) {
|
|
var selectedLogin = null;
|
|
var epicfail = false;
|
|
var canAutologin = false;
|
|
var foundLogins;
|
|
let autofilled = false;
|
|
|
|
try {
|
|
// If the user submits a login but it fails, we need to remove the
|
|
// notification prompt that was displayed. Conveniently, the user will
|
|
// be prompted for authentication again, which brings us here.
|
|
this._factory._dismissPendingSavePrompt(this._browser);
|
|
|
|
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
|
|
|
|
// Looks for existing logins to prefill the prompt with.
|
|
foundLogins = await Services.logins.searchLoginsAsync({
|
|
origin,
|
|
httpRealm,
|
|
schemeUpgrades: lazy.LoginHelper.schemeUpgrades,
|
|
});
|
|
this.log(`Found ${foundLogins.length} matching logins.`);
|
|
let resolveBy = ["scheme", "timePasswordChanged"];
|
|
foundLogins = lazy.LoginHelper.dedupeLogins(
|
|
foundLogins,
|
|
["username"],
|
|
resolveBy,
|
|
origin
|
|
);
|
|
this.log(`${foundLogins.length} matching logins remain after deduping.`);
|
|
|
|
// XXX Can't select from multiple accounts yet. (bug 227632)
|
|
if (foundLogins.length) {
|
|
selectedLogin = foundLogins[0];
|
|
this._SetAuthInfo(
|
|
aAuthInfo,
|
|
selectedLogin.username,
|
|
selectedLogin.password
|
|
);
|
|
autofilled = true;
|
|
|
|
// Allow automatic proxy login
|
|
if (
|
|
aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
|
|
!(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
|
|
Services.prefs.getBoolPref("signon.autologin.proxy") &&
|
|
!PrivateBrowsingUtils.permanentPrivateBrowsing
|
|
) {
|
|
this.log("Autologin enabled, skipping auth prompt.");
|
|
canAutologin = true;
|
|
}
|
|
}
|
|
|
|
var canRememberLogin = Services.logins.getLoginSavingEnabled(origin);
|
|
if (!this._allowRememberLogin) {
|
|
canRememberLogin = false;
|
|
}
|
|
} catch (e) {
|
|
// Ignore any errors and display the prompt anyway.
|
|
epicfail = true;
|
|
console.error("LoginManagerAuthPrompter: Epic fail in promptAuth:", e);
|
|
}
|
|
|
|
var ok = canAutologin;
|
|
let browser = this._browser;
|
|
let baseDomain;
|
|
|
|
// We might not have a browser or browser.currentURI.host could fail
|
|
// (e.g. on about:blank). Fall back to the subresource hostname in that case.
|
|
try {
|
|
let topLevelHost = browser.currentURI.host;
|
|
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(topLevelHost);
|
|
} catch (e) {
|
|
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(origin);
|
|
}
|
|
|
|
if (!ok) {
|
|
if (PromptAbuseHelper.hasReachedAbuseLimit(baseDomain, browser)) {
|
|
this.log("Blocking auth dialog, due to exceeding dialog bloat limit.");
|
|
return false;
|
|
}
|
|
|
|
// Set up a counter for ensuring that the basic auth prompt can not
|
|
// be abused for DOS-style attacks. With this counter, each eTLD+1
|
|
// per browser will get a limited number of times a user can
|
|
// cancel the prompt until we stop showing it.
|
|
PromptAbuseHelper.incrementPromptAbuseCounter(baseDomain, browser);
|
|
|
|
if (this._chromeWindow) {
|
|
PromptUtils.fireDialogEvent(
|
|
this._chromeWindow,
|
|
"DOMWillOpenModalDialog",
|
|
this._browser
|
|
);
|
|
}
|
|
|
|
ok = await Services.prompt.asyncPromptAuth(
|
|
this._browser?.browsingContext,
|
|
LoginManagerAuthPrompter.promptAuthModalType,
|
|
aChannel,
|
|
aLevel,
|
|
aAuthInfo
|
|
);
|
|
}
|
|
|
|
let [username, password] = this._GetAuthInfo(aAuthInfo);
|
|
|
|
// Reset the counter state if the user replied to a prompt and actually
|
|
// tried to login (vs. simply clicking any button to get out).
|
|
if (ok && (username || password)) {
|
|
PromptAbuseHelper.resetPromptAbuseCounter(baseDomain, browser);
|
|
}
|
|
|
|
if (!ok || !canRememberLogin || epicfail) {
|
|
return ok;
|
|
}
|
|
|
|
try {
|
|
if (!password) {
|
|
this.log("No password entered, so won't offer to save.");
|
|
return ok;
|
|
}
|
|
|
|
// XXX We can't prompt with multiple logins yet (bug 227632), so
|
|
// the entered login might correspond to an existing login
|
|
// other than the one we originally selected.
|
|
selectedLogin = this._repickSelectedLogin(foundLogins, username);
|
|
|
|
// If we didn't find an existing login, or if the username
|
|
// changed, save as a new login.
|
|
let newLogin = new LoginInfo(origin, null, httpRealm, username, password);
|
|
if (!selectedLogin) {
|
|
this.log(`New login seen for origin: ${origin}.`);
|
|
|
|
let promptBrowser = lazy.LoginHelper.getBrowserForPrompt(browser);
|
|
let savePrompt = lazy.gPrompterService.promptToSavePassword(
|
|
promptBrowser,
|
|
newLogin
|
|
);
|
|
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
|
|
} else if (password != selectedLogin.password) {
|
|
this.log(`Updating password for origin: ${origin}.`);
|
|
|
|
let promptBrowser = lazy.LoginHelper.getBrowserForPrompt(browser);
|
|
let savePrompt = lazy.gPrompterService.promptToChangePassword(
|
|
promptBrowser,
|
|
selectedLogin,
|
|
newLogin
|
|
);
|
|
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
|
|
} else {
|
|
this.log("Login unchanged, no further action needed.");
|
|
Services.logins.recordPasswordUse(
|
|
selectedLogin,
|
|
this._inPrivateBrowsing,
|
|
"auth_login",
|
|
autofilled
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error("LoginManagerAuthPrompter: Fail2 in promptAuth:", e);
|
|
}
|
|
|
|
return ok;
|
|
},
|
|
|
|
/* ---------- nsIAuthPrompt2 prompts ---------- */
|
|
|
|
/**
|
|
* Implementation of nsIAuthPrompt2.
|
|
*
|
|
* @param {nsIChannel} aChannel
|
|
* @param {int} aLevel
|
|
* @param {nsIAuthInformation} aAuthInfo
|
|
*/
|
|
promptAuth(aChannel, aLevel, aAuthInfo) {
|
|
let closed = false;
|
|
let result = false;
|
|
this.promptAuthInternal(aChannel, aLevel, aAuthInfo)
|
|
.then(ok => (result = ok))
|
|
.finally(() => (closed = true));
|
|
Services.tm.spinEventLoopUntilOrQuit(
|
|
"LoginManagerAuthPrompter.jsm:promptAuth",
|
|
() => closed
|
|
);
|
|
return result;
|
|
},
|
|
|
|
asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
|
|
var cancelable = null;
|
|
|
|
try {
|
|
// If the user submits a login but it fails, we need to remove the
|
|
// notification prompt that was displayed. Conveniently, the user will
|
|
// be prompted for authentication again, which brings us here.
|
|
this._factory._dismissPendingSavePrompt(this._browser);
|
|
|
|
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
|
|
|
|
let [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
|
|
|
|
let hashKey = aLevel + "|" + origin + "|" + httpRealm;
|
|
let pendingPrompt = this._factory.getPendingPrompt(
|
|
this._browser,
|
|
hashKey
|
|
);
|
|
if (pendingPrompt) {
|
|
this.log(
|
|
`Prompt bound to an existing one in the queue, callback: ${aCallback}.`
|
|
);
|
|
pendingPrompt.consumers.push(cancelable);
|
|
return cancelable;
|
|
}
|
|
|
|
this.log(`Adding new async prompt, callback: ${aCallback}.`);
|
|
let asyncPrompt = {
|
|
consumers: [cancelable],
|
|
channel: aChannel,
|
|
authInfo: aAuthInfo,
|
|
level: aLevel,
|
|
prompter: this,
|
|
};
|
|
|
|
this._factory._doAsyncPrompt(asyncPrompt, hashKey);
|
|
} catch (e) {
|
|
console.error("LoginManagerAuthPrompter: asyncPromptAuth:", e);
|
|
console.error("Falling back to promptAuth");
|
|
// Fail the prompt operation to let the consumer fall back
|
|
// to synchronous promptAuth method
|
|
throw e;
|
|
}
|
|
|
|
return cancelable;
|
|
},
|
|
|
|
/* ---------- nsILoginManagerAuthPrompter prompts ---------- */
|
|
|
|
init(aWindow = null, aFactory = null) {
|
|
if (!aWindow) {
|
|
// There may be no applicable window e.g. in a Sandbox or JSM.
|
|
this._chromeWindow = null;
|
|
this._browser = null;
|
|
} else if (aWindow.isChromeWindow) {
|
|
this._chromeWindow = aWindow;
|
|
// needs to be set explicitly using setBrowser
|
|
this._browser = null;
|
|
} else {
|
|
let { win, browser } = this._getChromeWindow(aWindow);
|
|
this._chromeWindow = win;
|
|
this._browser = browser;
|
|
}
|
|
this._factory = aFactory || null;
|
|
},
|
|
|
|
set browser(aBrowser) {
|
|
this._browser = aBrowser;
|
|
},
|
|
|
|
get browser() {
|
|
return this._browser;
|
|
},
|
|
|
|
/* ---------- Internal Methods ---------- */
|
|
|
|
_updateLogin(login, aNewLogin) {
|
|
var now = Date.now();
|
|
var propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
|
|
Ci.nsIWritablePropertyBag
|
|
);
|
|
propBag.setProperty("formActionOrigin", aNewLogin.formActionOrigin);
|
|
propBag.setProperty("origin", aNewLogin.origin);
|
|
propBag.setProperty("password", aNewLogin.password);
|
|
propBag.setProperty("username", aNewLogin.username);
|
|
// Explicitly set the password change time here (even though it would
|
|
// be changed automatically), to ensure that it's exactly the same
|
|
// value as timeLastUsed.
|
|
propBag.setProperty("timePasswordChanged", now);
|
|
propBag.setProperty("timeLastUsed", now);
|
|
propBag.setProperty("timesUsedIncrement", 1);
|
|
// Note that we don't call `recordPasswordUse` so we won't potentially record
|
|
// both a use and a save/update. See bug 1640096.
|
|
Services.logins.modifyLogin(login, propBag);
|
|
},
|
|
|
|
/**
|
|
* Given a content DOM window, returns the chrome window and browser it's in.
|
|
*/
|
|
_getChromeWindow(aWindow) {
|
|
let browser = aWindow.docShell.chromeEventHandler;
|
|
if (!browser) {
|
|
return null;
|
|
}
|
|
|
|
let chromeWin = browser.ownerGlobal;
|
|
if (!chromeWin) {
|
|
return null;
|
|
}
|
|
|
|
return { win: chromeWin, browser };
|
|
},
|
|
|
|
/**
|
|
* The user might enter a login that isn't the one we prefilled, but
|
|
* is the same as some other existing login. So, pick a login with a
|
|
* matching username, or return null.
|
|
*/
|
|
_repickSelectedLogin(foundLogins, username) {
|
|
for (var i = 0; i < foundLogins.length; i++) {
|
|
if (foundLogins[i].username == username) {
|
|
return foundLogins[i];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Can be called as:
|
|
* _getLocalizedString("key1");
|
|
* _getLocalizedString("key2", ["arg1"]);
|
|
* _getLocalizedString("key3", ["arg1", "arg2"]);
|
|
* (etc)
|
|
*
|
|
* Returns the localized string for the specified key,
|
|
* formatted if required.
|
|
*
|
|
*/
|
|
_getLocalizedString(key, formatArgs) {
|
|
if (formatArgs) {
|
|
return this._strBundle.formatStringFromName(key, formatArgs);
|
|
}
|
|
return this._strBundle.GetStringFromName(key);
|
|
},
|
|
|
|
/**
|
|
* Sanitizes the specified username, by stripping quotes and truncating if
|
|
* it's too long. This helps prevent an evil site from messing with the
|
|
* "save password?" prompt too much.
|
|
*/
|
|
_sanitizeUsername(username) {
|
|
if (username.length > 30) {
|
|
username = username.substring(0, 30);
|
|
username += this._ellipsis;
|
|
}
|
|
return username.replace(/['"]/g, "");
|
|
},
|
|
|
|
/**
|
|
* The aURI parameter may either be a string uri, or an nsIURI instance.
|
|
*
|
|
* Returns the origin to use in a nsILoginInfo object (for example,
|
|
* "http://example.com").
|
|
*/
|
|
_getFormattedOrigin(aURI) {
|
|
let uri;
|
|
if (aURI instanceof Ci.nsIURI) {
|
|
uri = aURI;
|
|
} else {
|
|
uri = Services.io.newURI(aURI);
|
|
}
|
|
|
|
return uri.scheme + "://" + uri.displayHostPort;
|
|
},
|
|
|
|
/**
|
|
* Converts a login's origin field (a URL) to a short string for
|
|
* prompting purposes. Eg, "http://foo.com" --> "foo.com", or
|
|
* "ftp://www.site.co.uk" --> "site.co.uk".
|
|
*/
|
|
_getShortDisplayHost(aURIString) {
|
|
var displayHost;
|
|
|
|
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
|
|
Ci.nsIIDNService
|
|
);
|
|
try {
|
|
var uri = Services.io.newURI(aURIString);
|
|
var baseDomain = Services.eTLD.getBaseDomain(uri);
|
|
displayHost = idnService.convertToDisplayIDN(baseDomain, {});
|
|
} catch (e) {
|
|
this.log(`Couldn't process supplied URIString ${aURIString}.`);
|
|
}
|
|
|
|
if (!displayHost) {
|
|
displayHost = aURIString;
|
|
}
|
|
|
|
return displayHost;
|
|
},
|
|
|
|
/**
|
|
* Returns the origin and realm for which authentication is being
|
|
* requested, in the format expected to be used with nsILoginInfo.
|
|
*/
|
|
_getAuthTarget(aChannel, aAuthInfo) {
|
|
var origin, realm;
|
|
|
|
// If our proxy is demanding authentication, don't use the
|
|
// channel's actual destination.
|
|
if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
|
|
this.log("getAuthTarget is for proxy auth.");
|
|
if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
|
|
throw new Error("proxy auth needs nsIProxiedChannel");
|
|
}
|
|
|
|
var info = aChannel.proxyInfo;
|
|
if (!info) {
|
|
throw new Error("proxy auth needs nsIProxyInfo");
|
|
}
|
|
|
|
// Proxies don't have a scheme, but we'll use "moz-proxy://"
|
|
// so that it's more obvious what the login is for.
|
|
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
|
|
Ci.nsIIDNService
|
|
);
|
|
origin =
|
|
"moz-proxy://" +
|
|
idnService.convertUTF8toACE(info.host) +
|
|
":" +
|
|
info.port;
|
|
realm = aAuthInfo.realm;
|
|
if (!realm) {
|
|
realm = origin;
|
|
}
|
|
|
|
return [origin, realm];
|
|
}
|
|
|
|
origin = this._getFormattedOrigin(aChannel.URI);
|
|
|
|
// If a HTTP WWW-Authenticate header specified a realm, that value
|
|
// will be available here. If it wasn't set or wasn't HTTP, we'll use
|
|
// the formatted origin instead.
|
|
realm = aAuthInfo.realm;
|
|
if (!realm) {
|
|
realm = origin;
|
|
}
|
|
|
|
return [origin, realm];
|
|
},
|
|
|
|
/**
|
|
* Returns [username, password] as extracted from aAuthInfo (which
|
|
* holds this info after having prompted the user).
|
|
*
|
|
* If the authentication was for a Windows domain, we'll prepend the
|
|
* return username with the domain. (eg, "domain\user")
|
|
*/
|
|
_GetAuthInfo(aAuthInfo) {
|
|
var username, password;
|
|
|
|
var flags = aAuthInfo.flags;
|
|
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) {
|
|
username = aAuthInfo.domain + "\\" + aAuthInfo.username;
|
|
} else {
|
|
username = aAuthInfo.username;
|
|
}
|
|
|
|
password = aAuthInfo.password;
|
|
|
|
return [username, password];
|
|
},
|
|
|
|
/**
|
|
* Given a username (possibly in DOMAIN\user form) and password, parses the
|
|
* domain out of the username if necessary and sets domain, username and
|
|
* password on the auth information object.
|
|
*/
|
|
_SetAuthInfo(aAuthInfo, username, password) {
|
|
var flags = aAuthInfo.flags;
|
|
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
|
|
// Domain is separated from username by a backslash
|
|
var idx = username.indexOf("\\");
|
|
if (idx == -1) {
|
|
aAuthInfo.username = username;
|
|
} else {
|
|
aAuthInfo.domain = username.substring(0, idx);
|
|
aAuthInfo.username = username.substring(idx + 1);
|
|
}
|
|
} else {
|
|
aAuthInfo.username = username;
|
|
}
|
|
aAuthInfo.password = password;
|
|
},
|
|
|
|
_newAsyncPromptConsumer(aCallback, aContext) {
|
|
return {
|
|
QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
|
|
callback: aCallback,
|
|
context: aContext,
|
|
cancel() {
|
|
this.callback.onAuthCancelled(this.context, false);
|
|
this.callback = null;
|
|
this.context = null;
|
|
},
|
|
};
|
|
},
|
|
}; // end of LoginManagerAuthPrompter implementation
|
|
|
|
ChromeUtils.defineLazyGetter(LoginManagerAuthPrompter.prototype, "log", () => {
|
|
let logger = lazy.LoginHelper.createLogger("LoginManagerAuthPrompter");
|
|
return logger.log.bind(logger);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
LoginManagerAuthPrompter,
|
|
"promptAuthModalType",
|
|
"prompts.modalType.httpAuth",
|
|
Services.prompt.MODAL_TYPE_WINDOW
|
|
);
|