Source code

Revision control

Copy as Markdown

Other Tools

/* 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 { Log } from "resource://gre/modules/Log.sys.mjs";
import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
const fxAccounts = getFxAccountsSingleton();
import { FxAccountsClient } from "resource://gre/modules/FxAccountsClient.sys.mjs";
import { FxAccountsConfig } from "resource://gre/modules/FxAccountsConfig.sys.mjs";
import { Logger } from "resource://tps/logger.sys.mjs";
/**
* Helper object for Firefox Accounts authentication
*/
export var Authentication = {
/**
* Check if an user has been logged in
*/
async isLoggedIn() {
return !!(await this.getSignedInUser());
},
async isReady() {
let user = await this.getSignedInUser();
return user && user.verified;
},
_getRestmailUsername(user) {
const restmailSuffix = "@restmail.net";
if (user.toLowerCase().endsWith(restmailSuffix)) {
return user.slice(0, -restmailSuffix.length);
}
return null;
},
async shortWaitForVerification(ms) {
let userData = await this.getSignedInUser();
let timeoutID;
let timeoutPromise = new Promise(resolve => {
timeoutID = setTimeout(() => {
Logger.logInfo(`Warning: no verification after ${ms}ms.`);
resolve();
}, ms);
});
await Promise.race([
fxAccounts.whenVerified(userData).finally(() => clearTimeout(timeoutID)),
timeoutPromise,
]);
userData = await this.getSignedInUser();
return userData && userData.verified;
},
async _openVerificationPage(uri) {
let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
let newtab = mainWindow.gBrowser.addWebTab(uri);
let win = mainWindow.gBrowser.getBrowserForTab(newtab);
await new Promise(resolve => {
win.addEventListener("loadend", resolve, { once: true });
});
let didVerify = await this.shortWaitForVerification(10000);
mainWindow.gBrowser.removeTab(newtab);
return didVerify;
},
async _completeVerification(user) {
let username = this._getRestmailUsername(user);
if (!username) {
Logger.logInfo(
`Username "${user}" isn't a restmail username so can't complete verification`
);
return false;
}
Logger.logInfo("Fetching mail (from restmail) for user " + username);
let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
username
)}`;
let triedAlready = new Set();
const tries = 10;
const normalWait = 2000;
for (let i = 0; i < tries; ++i) {
let resp = await fetch(restmailURI);
let messages = await resp.json();
// Sort so that the most recent emails are first.
messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
for (let m of messages) {
// We look for a link that has a x-link that we haven't yet tried.
if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
continue;
}
let confirmLink = m.headers["x-link"];
triedAlready.add(confirmLink);
Logger.logInfo("Trying confirmation link " + confirmLink);
try {
if (await this._openVerificationPage(confirmLink)) {
return true;
}
} catch (e) {
Logger.logInfo(
"Warning: Failed to follow confirmation link: " +
Log.exceptionStr(e)
);
}
}
if (i === 0) {
// first time through after failing we'll do this.
await fxAccounts.resendVerificationEmail();
}
if (await this.shortWaitForVerification(normalWait)) {
return true;
}
}
// One last try.
return this.shortWaitForVerification(normalWait);
},
async deleteEmail(user) {
let username = this._getRestmailUsername(user);
if (!username) {
Logger.logInfo("Not a restmail username, can't delete");
return false;
}
Logger.logInfo("Deleting mail (from restmail) for user " + username);
let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
username
)}`;
try {
// Clean up after ourselves.
let deleteResult = await fetch(restmailURI, { method: "DELETE" });
if (!deleteResult.ok) {
Logger.logInfo(
`Warning: Got non-success status ${deleteResult.status} when deleting emails`
);
return false;
}
} catch (e) {
Logger.logInfo(
"Warning: Failed to delete old emails: " + Log.exceptionStr(e)
);
return false;
}
return true;
},
/**
* Wrapper to retrieve the currently signed in user
*
* @returns Information about the currently signed in user
*/
async getSignedInUser() {
try {
return await fxAccounts.getSignedInUser();
} catch (error) {
Logger.logError(
"getSignedInUser() failed with: " + JSON.stringify(error)
);
throw error;
}
},
/**
* Wrapper to synchronize the login of a user
*
* @param account
* Account information of the user to login
* @param account.username
* The username for the account (utf8)
* @param account.password
* The user's password
*/
async signIn(account) {
Logger.AssertTrue(account.username, "Username has been found");
Logger.AssertTrue(account.password, "Password has been found");
Logger.logInfo("Login user: " + account.username);
try {
// Required here since we don't go through the real login page
await FxAccountsConfig.ensureConfigured();
let client = new FxAccountsClient();
let credentials = await client.signIn(
account.username,
account.password,
true
);
await fxAccounts._internal.setSignedInUser(credentials);
if (!credentials.verified) {
await this._completeVerification(account.username);
}
return true;
} catch (error) {
throw new Error("signIn() failed with: " + error.message);
}
},
/**
* Sign out of Firefox Accounts.
*/
async signOut() {
if (await Authentication.isLoggedIn()) {
// Note: This will clean up the device ID.
await fxAccounts.signOut();
}
},
};