diff options
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/modules/utils/calAuthUtils.jsm | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/comm/calendar/base/modules/utils/calAuthUtils.jsm b/comm/calendar/base/modules/utils/calAuthUtils.jsm new file mode 100644 index 0000000000..1f14d1c6cd --- /dev/null +++ b/comm/calendar/base/modules/utils/calAuthUtils.jsm @@ -0,0 +1,564 @@ +/* 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/. */ + +/** + * Authentication tools and prompts, mostly for providers + */ + +// NOTE: This module should not be loaded directly, it is available when including +// calUtils.jsm under the cal.auth namespace. + +const EXPORTED_SYMBOLS = ["calauth"]; + +var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + cal: "resource:///modules/calendar/calUtils.jsm", + MsgAuthPrompt: "resource:///modules/MsgAsyncPrompter.jsm", +}); + +/** + * The userContextId of nsIHttpChannel is currently implemented as a uint32, so + * the ContainerMap defined below must not return Ids greater then the allowed + * range of a uint32. + */ +const MAX_CONTAINER_ID = Math.pow(2, 32) - 1; + +/** + * A map that handles userContextIds and usernames and provides unique Ids for + * different usernames. + */ +class ContainerMap extends Map { + /** + * Create a container map with a given range of userContextIds. + * + * @param {number} min - The lower range limit of userContextIds to be + * used. + * @param {number} max - The upper range limit of userContextIds to be + * used. + * @param {?object} iterable - Optional parameter which is passed to the + * constructor of Map. See definition of Map + * for more details. + */ + constructor(min = 0, max = MAX_CONTAINER_ID, iterable) { + super(iterable); + this.order = []; + this.inverted = {}; + this.min = min; + // The userConextId is a uint32, limit accordingly. + this.max = Math.max(max, MAX_CONTAINER_ID); + if (this.min > this.max) { + throw new RangeError( + "[ContainerMap] The provided min value " + + "(" + + this.min + + ") must not be greater than the provided " + + "max value (" + + this.max + + ")" + ); + } + } + + /** + * Check if the allowed userContextId range is fully used. + */ + get full() { + return this.size > this.max - this.min; + } + + /** + * Add a new username to the map. + * + * @param {string} username - The username to be added. + * @returns {number} The userContextId assigned to the given username. + */ + _add(username) { + let nextUserContextId; + if (this.full) { + let oldestUsernameEntry = this.order.shift(); + nextUserContextId = this.get(oldestUsernameEntry); + this.delete(oldestUsernameEntry); + } else { + nextUserContextId = this.min + this.size; + } + + Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: nextUserContextId }); + this.order.push(username); + this.set(username, nextUserContextId); + this.inverted[nextUserContextId] = username; + return nextUserContextId; + } + + /** + * Look up the userContextId for the given username. Create a new one, + * if the username is not yet known. + * + * @param {string} username - The username for which the userContextId + * is to be looked up. + * @returns {number} The userContextId which is assigned to + * the provided username. + */ + getUserContextIdForUsername(username) { + if (this.has(username)) { + return this.get(username); + } + return this._add(username); + } + + /** + * Look up the username for the given userContextId. Return empty string + * if not found. + * + * @param {number} userContextId - The userContextId for which the + * username is to be to looked up. + * @returns {string} The username mapped to the given + * userContextId. + */ + getUsernameForUserContextId(userContextId) { + if (this.inverted.hasOwnProperty(userContextId)) { + return this.inverted[userContextId]; + } + return ""; + } +} + +var calauth = { + /** + * Calendar Auth prompt implementation. This instance of the auth prompt should + * be used by providers and other components that handle authentication using + * nsIAuthPrompt2 and friends. + * + * This implementation guarantees there are no request loops when an invalid + * password is stored in the login-manager. + * + * There is one instance of that object per calendar provider. + */ + Prompt: class { + constructor() { + this.mWindow = lazy.cal.window.getCalendarWindow(); + this.mReturnedLogins = {}; + this.mProvider = null; + } + + /** + * @typedef {object} PasswordInfo + * @property {boolean} found True, if the password was found + * @property {?string} username The found username + * @property {?string} password The found password + */ + + /** + * Retrieve password information from the login manager + * + * @param {string} aPasswordRealm - The realm to retrieve password info for + * @param {string} aRequestedUser - The username to look up. + * @returns {PasswordInfo} The retrieved password information + */ + getPasswordInfo(aPasswordRealm, aRequestedUser) { + // Prefill aRequestedUser, so it will be used in the prompter. + let username = aRequestedUser; + let password; + let found = false; + + let logins = Services.logins.findLogins(aPasswordRealm.prePath, null, aPasswordRealm.realm); + for (let login of logins) { + if (!aRequestedUser || aRequestedUser == login.username) { + username = login.username; + password = login.password; + found = true; + break; + } + } + if (found) { + let keyStr = aPasswordRealm.prePath + ":" + aPasswordRealm.realm + ":" + aRequestedUser; + let now = new Date(); + // Remove the saved password if it was already returned less + // than 60 seconds ago. The reason for the timestamp check is that + // nsIHttpChannel can call the nsIAuthPrompt2 interface + // again in some situation. ie: When using Digest auth token + // expires. + if ( + this.mReturnedLogins[keyStr] && + now.getTime() - this.mReturnedLogins[keyStr].getTime() < 60000 + ) { + lazy.cal.LOG( + "Credentials removed for: user=" + + username + + ", host=" + + aPasswordRealm.prePath + + ", realm=" + + aPasswordRealm.realm + ); + + delete this.mReturnedLogins[keyStr]; + calauth.passwordManagerRemove(username, aPasswordRealm.prePath, aPasswordRealm.realm); + return { found: false, username }; + } + this.mReturnedLogins[keyStr] = now; + } + return { found, username, password }; + } + + // boolean promptAuth(in nsIChannel aChannel, + // in uint32_t level, + // in nsIAuthInformation authInfo) + promptAuth(aChannel, aLevel, aAuthInfo) { + let hostRealm = {}; + hostRealm.prePath = aChannel.URI.prePath; + hostRealm.realm = aAuthInfo.realm; + let port = aChannel.URI.port; + if (port == -1) { + let handler = Services.io + .getProtocolHandler(aChannel.URI.scheme) + .QueryInterface(Ci.nsIProtocolHandler); + port = handler.defaultPort; + } + hostRealm.passwordRealm = aChannel.URI.host + ":" + port + " (" + aAuthInfo.realm + ")"; + + let requestedUser = lazy.cal.auth.containerMap.getUsernameForUserContextId( + aChannel.loadInfo.originAttributes.userContextId + ); + let pwInfo = this.getPasswordInfo(hostRealm, requestedUser); + aAuthInfo.username = pwInfo.username; + if (pwInfo && pwInfo.found) { + aAuthInfo.password = pwInfo.password; + return true; + } + let savePasswordLabel = null; + if (Services.prefs.getBoolPref("signon.rememberSignons", true)) { + savePasswordLabel = lazy.cal.l10n.getAnyString( + "passwordmgr", + "passwordmgr", + "rememberPassword" + ); + } + let savePassword = {}; + let returnValue = new lazy.MsgAuthPrompt().promptAuth( + aChannel, + aLevel, + aAuthInfo, + savePasswordLabel, + savePassword + ); + if (savePassword.value) { + calauth.passwordManagerSave( + aAuthInfo.username, + aAuthInfo.password, + hostRealm.prePath, + aAuthInfo.realm + ); + } + return returnValue; + } + + // nsICancelable asyncPromptAuth(in nsIChannel aChannel, + // in nsIAuthPromptCallback aCallback, + // in nsISupports aContext, + // in uint32_t level, + // in nsIAuthInformation authInfo); + asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) { + let self = this; + let promptlistener = { + onPromptStartAsync(callback) { + callback.onAuthResult(this.onPromptStart()); + }, + + onPromptStart() { + let res = self.promptAuth(aChannel, aLevel, aAuthInfo); + if (res) { + gAuthCache.setAuthInfo(hostKey, aAuthInfo); + this.onPromptAuthAvailable(); + return true; + } + + this.onPromptCanceled(); + return false; + }, + + onPromptAuthAvailable() { + let authInfo = gAuthCache.retrieveAuthInfo(hostKey); + if (authInfo) { + aAuthInfo.username = authInfo.username; + aAuthInfo.password = authInfo.password; + } + aCallback.onAuthAvailable(aContext, aAuthInfo); + }, + + onPromptCanceled() { + gAuthCache.retrieveAuthInfo(hostKey); + aCallback.onAuthCancelled(aContext, true); + }, + }; + + let requestedUser = lazy.cal.auth.containerMap.getUsernameForUserContextId( + aChannel.loadInfo.originAttributes.userContextId + ); + let hostKey = aChannel.URI.prePath + ":" + aAuthInfo.realm + ":" + requestedUser; + gAuthCache.planForAuthInfo(hostKey); + + let queuePrompt = function () { + let asyncprompter = Cc["@mozilla.org/messenger/msgAsyncPrompter;1"].getService( + Ci.nsIMsgAsyncPrompter + ); + asyncprompter.queueAsyncAuthPrompt(hostKey, false, promptlistener); + }; + + let finalSteps = function () { + // the prompt will fail if we are too early + if (self.mWindow.document.readyState == "complete") { + queuePrompt(); + } else { + self.mWindow.addEventListener("load", queuePrompt, true); + } + }; + + let tryUntilReady = function () { + self.mWindow = lazy.cal.window.getCalendarWindow(); + if (!self.mWindow) { + lazy.setTimeout(tryUntilReady, 1000); + return; + } + + finalSteps(); + }; + + // We might reach this code when cal.window.getCalendarWindow() + // returns null, which means the window obviously isn't yet + // in readyState complete, and we also cannot yet queue a prompt. + // It may happen if startup shows a blocking primary password + // prompt, which delays starting up the application windows. + // Use a timer to retry until we can access the calendar window. + + tryUntilReady(); + } + }, + + /** + * Tries to get the username/password combination of a specific calendar name from the password + * manager or asks the user. + * + * @param {string} aTitle - The dialog title. + * @param {string} aCalendarName - The calendar name or url to look up. Can be null. + * @param {{value: string}} aUsername The username that belongs to the calendar. + * @param {{value: string}} aPassword The password that belongs to the calendar. + * @param {{value: string}} aSavePassword Should the password be saved? + * @param {boolean} aFixedUsername - Whether the user name is fixed or editable + * @returns {boolean} Could a password be retrieved? + */ + getCredentials(aTitle, aCalendarName, aUsername, aPassword, aSavePassword, aFixedUsername) { + if ( + typeof aUsername != "object" || + typeof aPassword != "object" || + typeof aSavePassword != "object" + ) { + throw new Components.Exception("", Cr.NS_ERROR_XPC_NEED_OUT_OBJECT); + } + + let prompter = new lazy.MsgAuthPrompt(); + + // Only show the save password box if we are supposed to. + let savepassword = null; + if (Services.prefs.getBoolPref("signon.rememberSignons", true)) { + savepassword = lazy.cal.l10n.getAnyString("passwordmgr", "passwordmgr", "rememberPassword"); + } + + let aText; + if (aFixedUsername) { + aText = lazy.cal.l10n.getAnyString("global", "commonDialogs", "EnterPasswordFor", [ + aUsername.value, + aCalendarName, + ]); + return prompter.promptPassword(aTitle, aText, aPassword, savepassword, aSavePassword); + } + aText = lazy.cal.l10n.getAnyString("global", "commonDialogs", "EnterUserPasswordFor2", [ + aCalendarName, + ]); + return prompter.promptUsernameAndPassword( + aTitle, + aText, + aUsername, + aPassword, + savepassword, + aSavePassword + ); + }, + + /** + * Make sure the passed origin is actually an uri string, because password manager functions + * require it. This is a fallback for compatibility only and should be removed a few versions + * after Lightning 6.2 + * + * @param {string} aOrigin - The hostname or origin to check + * @returns {string} The origin uri + */ + _ensureOrigin(aOrigin) { + try { + let { prePath, spec } = Services.io.newURI(aOrigin); + if (prePath == "oauth:") { + return spec; + } + return prePath; + } catch (e) { + return "https://" + aOrigin; + } + }, + + /** + * Helper to insert/update an entry to the password manager. + * + * @param {string} aUsername - The username to insert + * @param {string} aPassword - The corresponding password + * @param {string} aOrigin - The corresponding origin + * @param {string} aRealm - The password realm (unused on branch) + */ + passwordManagerSave(aUsername, aPassword, aOrigin, aRealm) { + lazy.cal.ASSERT(aUsername); + lazy.cal.ASSERT(aPassword); + + let origin = this._ensureOrigin(aOrigin); + + if (!Services.logins.getLoginSavingEnabled(origin)) { + throw new Components.Exception( + "Password saving is disabled for " + origin, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + try { + let logins = Services.logins.findLogins(origin, null, aRealm); + + let newLoginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + newLoginInfo.init(origin, null, aRealm, aUsername, aPassword, "", ""); + for (let login of logins) { + if (aUsername == login.username) { + Services.logins.modifyLogin(login, newLoginInfo); + return; + } + } + Services.logins.addLogin(newLoginInfo); + } catch (exc) { + // Only show the message if its not an abort, which can happen if + // the user canceled the primary password dialog + lazy.cal.ASSERT(exc.result == Cr.NS_ERROR_ABORT, exc); + } + }, + + /** + * Helper to retrieve an entry from the password manager. + * + * @param {string} aUsername - The username to search + * @param {string} aPassword - The corresponding password + * @param {string} aOrigin - The corresponding origin + * @param {string} aRealm - The password realm (unused on branch) + * @returns {boolean} True, if an entry exists in the password manager + */ + passwordManagerGet(aUsername, aPassword, aOrigin, aRealm) { + lazy.cal.ASSERT(aUsername); + + if (typeof aPassword != "object") { + throw new Components.Exception("", Cr.NS_ERROR_XPC_NEED_OUT_OBJECT); + } + + let origin = this._ensureOrigin(aOrigin); + + try { + let logins = Services.logins.findLogins(origin, null, ""); + for (let loginInfo of logins) { + if ( + loginInfo.username == aUsername && + (loginInfo.httpRealm == aRealm || loginInfo.httpRealm.split(" ").includes(aRealm)) + ) { + aPassword.value = loginInfo.password; + return true; + } + } + } catch (exc) { + lazy.cal.ASSERT(false, exc); + } + return false; + }, + + /** + * Helper to remove an entry from the password manager + * + * @param {string} aUsername - The username to remove + * @param {string} aOrigin - The corresponding origin + * @param {string} aRealm - The password realm (unused on branch) + * @returns {boolean} Could the user be removed? + */ + passwordManagerRemove(aUsername, aOrigin, aRealm) { + lazy.cal.ASSERT(aUsername); + + let origin = this._ensureOrigin(aOrigin); + + try { + let logins = Services.logins.findLogins(origin, null, aRealm); + for (let loginInfo of logins) { + if (loginInfo.username == aUsername) { + Services.logins.removeLogin(loginInfo); + return true; + } + } + } catch (exc) { + // If no logins are found, fall through to the return statement below. + } + return false; + }, + + /** + * A map which maps usernames to userContextIds, reserving a range + * of 20000 - 29999 for userContextIds to be used within calendar. + * + * @param {number} min - The lower range limit of userContextIds to be + * used. + * @param {number} max - The upper range limit of userContextIds to be + * used. + */ + containerMap: new ContainerMap(20000, 29999), +}; + +// Cache for authentication information since onAuthInformation in the prompt +// listener is called without further information. If the password is not +// saved, there is no way to retrieve it. We use ref counting to avoid keeping +// the password in memory longer than needed. +var gAuthCache = { + _authInfoCache: new Map(), + planForAuthInfo(hostKey) { + let authInfo = this._authInfoCache.get(hostKey); + if (authInfo) { + authInfo.refCnt++; + } else { + this._authInfoCache.set(hostKey, { refCnt: 1 }); + } + }, + + setAuthInfo(hostKey, aAuthInfo) { + let authInfo = this._authInfoCache.get(hostKey); + if (authInfo) { + authInfo.username = aAuthInfo.username; + authInfo.password = aAuthInfo.password; + } + }, + + retrieveAuthInfo(hostKey) { + let authInfo = this._authInfoCache.get(hostKey); + if (authInfo) { + authInfo.refCnt--; + + if (authInfo.refCnt == 0) { + this._authInfoCache.delete(hostKey); + } + } + return authInfo; + }, +}; |