summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wxBackend.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wxBackend.ts')
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts685
1 files changed, 328 insertions, 357 deletions
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 4004f04f6..008f80c57 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -1,17 +1,17 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
@@ -23,28 +23,43 @@
/**
* Imports.
*/
-import { isFirefox, getPermissionsApi } from "./compat";
-import { extendedPermissions } from "./permissions";
import {
+ AbsoluteTime,
+ BalanceFlag,
+ LogLevel,
+ Logger,
+ NotificationType,
OpenedPromise,
+ SetTimeoutTimerAPI,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TransactionMajorState,
+ TransactionMinorState,
+ WalletNotification,
+ getErrorDetailFromException,
+ makeErrorDetail,
openPromise,
- openTalerDatabase,
- makeErrorDetails,
- deleteTalerDatabase,
+ setGlobalLogLevelFromString,
+ setLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
+import {
DbAccess,
- WalletStoresV1,
+ SynchronousCryptoWorkerFactoryPlain,
Wallet,
+ WalletApiOperation,
+ WalletOperations,
+ WalletStoresV1,
+ deleteTalerDatabase,
+ exportDb,
+ importDb,
} from "@gnu-taler/taler-wallet-core";
-import {
- classifyTalerUri,
- CoreApiResponse,
- CoreApiResponseSuccess,
- TalerErrorCode,
- TalerUriType,
- WalletDiagnostics,
-} from "@gnu-taler/taler-util";
-import { BrowserHttpLib } from "./browserHttpLib";
-import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+import { MessageFromFrontend, MessageResponse } from "./platform/api.js";
+import { platform } from "./platform/background.js";
+import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
+import { BackgroundOperations, WalletEvent } from "./wxApi.js";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
/**
* Currently active wallet instance. Might be unloaded and
@@ -56,357 +71,283 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version if an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
-const notificationPorts: chrome.runtime.Port[] = [];
+const logger = new Logger("wxBackend.ts");
-async function getDiagnostics(): Promise<WalletDiagnostics> {
- const manifestData = chrome.runtime.getManifest();
- const errors: string[] = [];
- let firefoxIdbProblem = false;
- let dbOutdated = false;
- try {
- await walletInit.promise;
- } catch (e) {
- errors.push("Error during wallet initialization: " + e);
- if (
- currentDatabase === undefined &&
- outdatedDbVersion === undefined &&
- isFirefox()
- ) {
- firefoxIdbProblem = true;
- }
- }
- if (!currentWallet) {
- errors.push("Could not create wallet backend.");
+type BackendHandlerType = {
+ [Op in keyof BackgroundOperations]: (
+ req: BackgroundOperations[Op]["request"],
+ ) => Promise<BackgroundOperations[Op]["response"]>;
+};
+
+type ExtensionHandlerType = {
+ [Op in keyof ExtensionOperations]: (
+ req: ExtensionOperations[Op]["request"],
+ ) => Promise<ExtensionOperations[Op]["response"]>;
+};
+
+async function resetDb(): Promise<void> {
+ await deleteTalerDatabase(indexedDB as any);
+ await reinitWallet();
+}
+
+//FIXME: maybe circular buffer
+const notifications: WalletEvent[] = [];
+async function getNotifications(): Promise<WalletEvent[]> {
+ return notifications;
+}
+
+async function clearNotifications(): Promise<void> {
+ notifications.splice(0, notifications.length);
+}
+
+async function runGarbageCollector(): Promise<void> {
+ const dbBeforeGc = currentDatabase;
+ if (!dbBeforeGc) {
+ throw Error("no current db before running gc");
}
- if (!currentDatabase) {
- errors.push("Could not open database");
+ const dump = await exportDb(indexedDB as any);
+
+ await deleteTalerDatabase(indexedDB as any);
+ logger.info("cleaned");
+ await reinitWallet();
+ logger.info("init");
+
+ const dbAfterGc = currentDatabase;
+ if (!dbAfterGc) {
+ throw Error("no current db before running gc");
}
- if (outdatedDbVersion !== undefined) {
- errors.push(`Outdated DB version: ${outdatedDbVersion}`);
- dbOutdated = true;
+ await importDb(dbAfterGc.idbHandle(), dump);
+ logger.info("imported");
+}
+
+const extensionHandlers: ExtensionHandlerType = {
+ isAutoOpenEnabled,
+ isDomainTrusted,
+};
+
+async function isAutoOpenEnabled(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.autoOpen === true;
+}
+
+async function isDomainTrusted(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.injectTalerSupport === true;
+}
+
+const backendHandlers: BackendHandlerType = {
+ resetDb,
+ runGarbageCollector,
+ getNotifications,
+ clearNotifications,
+ reinitWallet,
+ setLoggingLevel,
+};
+
+async function setLoggingLevel({
+ tag,
+ level,
+}: {
+ tag?: string;
+ level: LogLevel;
+}): Promise<void> {
+ logger.info(`setting ${tag} to ${level}`);
+ if (!tag) {
+ setGlobalLogLevelFromString(level);
+ } else {
+ setLogLevelFromString(tag, level);
}
- const diagnostics: WalletDiagnostics = {
- walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
- walletManifestVersion: manifestData.version,
- errors,
- firefoxIdbProblem,
- dbOutdated,
- };
- return diagnostics;
}
+let nextMessageIndex = 0;
-async function dispatch(
- req: any,
- sender: any,
- sendResponse: any,
-): Promise<void> {
- let r: CoreApiResponse;
-
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
- return {
- type: "response",
- id: req.id,
- operation: req.operation,
- result,
- };
- };
+async function dispatch<
+ Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
+>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
- switch (req.operation) {
- case "wxGetDiagnostics": {
- r = wrapResponse(await getDiagnostics());
- break;
- }
- case "reset-db": {
- await deleteTalerDatabase(indexedDB);
- r = wrapResponse(await reinitWallet());
- break;
- }
- case "wxGetExtendedPermissions": {
- const res = await new Promise((resolve, reject) => {
- getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
- resolve(result);
- });
- });
- r = wrapResponse({ newValue: res });
- break;
+ switch (req.channel) {
+ case "background": {
+ const handler = backendHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown background operation`),
+ ),
+ };
+ }
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
+ }
}
- case "wxSetExtendedPermissions": {
- const newVal = req.payload.value;
- console.log("new extended permissions value", newVal);
- if (newVal) {
- setupHeaderListener();
- r = wrapResponse({ newValue: true });
- } else {
- await new Promise<void>((resolve, reject) => {
- getPermissionsApi().remove(extendedPermissions, (rem) => {
- console.log("permissions removed:", rem);
- resolve();
- });
- });
- r = wrapResponse({ newVal: false });
+ case "extension": {
+ const handler = extensionHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown extension operation`),
+ ),
+ };
+ }
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
}
- break;
}
- default: {
+ case "wallet": {
const w = currentWallet;
if (!w) {
- r = {
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
+
+ return {
type: "error",
id: req.id,
operation: req.operation,
- error: makeErrorDetails(
+ error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
- "wallet core not available",
- {},
+ { lastError },
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
),
};
- break;
}
- r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
- break;
+ //multiple client can create the same id, send the wallet an unique key
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
+ //return to the client the original id
+ resp.id = req.id;
+ return resp;
}
}
- try {
- sendResponse(r);
- } catch (e) {
- // might fail if tab disconnected
- }
-}
-
-function getTab(tabId: number): Promise<chrome.tabs.Tab> {
- return new Promise((resolve, reject) => {
- chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
- });
-}
-
-function setBadgeText(options: chrome.browserAction.BadgeTextDetails): void {
- // not supported by all browsers ...
- if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) {
- chrome.browserAction.setBadgeText(options);
- } else {
- console.warn("can't set badge text, not supported", options);
- }
-}
-
-function waitMs(timeoutMs: number): Promise<void> {
- return new Promise((resolve, reject) => {
- const bgPage = chrome.extension.getBackgroundPage();
- if (!bgPage) {
- reject("fatal: no background page");
- return;
- }
- bgPage.setTimeout(() => resolve(), timeoutMs);
- });
-}
-
-function makeSyncWalletRedirect(
- url: string,
- tabId: number,
- oldUrl: string,
- params?: { [name: string]: string | undefined },
-): Record<string, unknown> {
- const innerUrl = new URL(chrome.extension.getURL(url));
- if (params) {
- const hParams = Object.keys(params)
- .map((k) => `${k}=${params[k]}`)
- .join("&");
- innerUrl.hash = innerUrl.hash + "?" + hParams;
- }
- if (isFirefox()) {
- // Some platforms don't support the sync redirect (yet), so fall back to
- // async redirect after a timeout.
- const doit = async (): Promise<void> => {
- await waitMs(150);
- const tab = await getTab(tabId);
- if (tab.url === oldUrl) {
- chrome.tabs.update(tabId, { url: innerUrl.href });
- }
- };
- doit();
- }
- console.log("redirecting to", innerUrl.href);
- chrome.tabs.update(tabId, { url: innerUrl.href });
- return { redirectUrl: innerUrl.href };
+ const anyReq = req as any;
+ return {
+ type: "error",
+ id: anyReq.id,
+ operation: String(anyReq.operation),
+ error: getErrorDetailFromException(
+ Error(
+ `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
+ ),
+ ),
+ };
}
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
- setBadgeText({ text: "" });
- try {
- currentDatabase = await openTalerDatabase(indexedDB, reinitWallet);
- } catch (e) {
- console.error("could not open database", e);
- walletInit.reject(e);
- return;
+ // setBadgeText({ text: "" });
+ let cryptoWorker;
+ let timer;
+
+ const httpFactory = (): HttpRequestLibrary => {
+ return new BrowserFetchHttpLib({
+ // enableThrottling: false,
+ });
+ };
+
+ if (platform.useServiceWorkerAsBackgroundProcess()) {
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
+ timer = new SetTimeoutTimerAPI();
+ } else {
+ // We could (should?) use the BrowserCryptoWorkerFactory here,
+ // but right now we don't, to have less platform differences.
+ // cryptoWorker = new BrowserCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
+ timer = new SetTimeoutTimerAPI();
}
- const http = new BrowserHttpLib();
- console.log("setting wallet");
+
+ const settings = await platform.getSettingsFromStorage();
+ logger.info("Setting up wallet");
const wallet = await Wallet.create(
- currentDatabase,
- http,
- new BrowserCryptoWorkerFactory(),
+ indexedDB as any,
+ httpFactory as any,
+ timer,
+ cryptoWorker,
);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {});
+ await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ testing: {
+ emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
+ },
+ features: {
+ allowHttp: settings.walletAllowHttp,
+ },
+ },
+ });
} catch (e) {
- console.error("could not initialize wallet", e);
+ logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
- wallet.addNotificationListener((x) => {
- for (const x of notificationPorts) {
- try {
- x.postMessage({ type: "notification" });
- } catch (e) {
- console.error(e);
- }
+ wallet.addNotificationListener((message) => {
+ if (settings.showWalletActivity) {
+ notifications.push({
+ notification: message,
+ when: AbsoluteTime.now(),
+ });
}
- });
- wallet.runTaskLoop().catch((e) => {
- console.log("error during wallet task loop", e);
- });
- // Useful for debugging in the background page.
- (window as any).talerWallet = wallet;
- currentWallet = wallet;
- walletInit.resolve();
-}
-try {
- // This needs to be outside of main, as Firefox won't fire the event if
- // the listener isn't created synchronously on loading the backend.
- chrome.runtime.onInstalled.addListener((details) => {
- console.log("onInstalled with reason", details.reason);
- if (details.reason === "install") {
- const url = chrome.extension.getURL("/static/wallet.html#/welcome");
- chrome.tabs.create({ active: true, url: url });
- }
+ processWalletNotification(message);
+
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message,
+ });
});
-} catch (e) {
- console.error(e);
-}
-function headerListener(
- details: chrome.webRequest.WebResponseHeadersDetails,
-): chrome.webRequest.BlockingResponse | undefined {
- console.log("header listener");
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- return;
- }
- const wallet = currentWallet;
- if (!wallet) {
- console.warn("wallet not available while handling header");
- return;
- }
- console.log("in header listener");
- if (
- details.statusCode === 402 ||
- details.statusCode === 202 ||
- details.statusCode === 200
- ) {
- console.log(`got 402/202 from ${details.url}`);
- for (const header of details.responseHeaders || []) {
- if (header.name.toLowerCase() === "taler") {
- const talerUri = header.value || "";
- const uriType = classifyTalerUri(talerUri);
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/withdraw",
- details.tabId,
- details.url,
- {
- talerWithdrawUri: talerUri,
- },
- );
- case TalerUriType.TalerPay:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/pay",
- details.tabId,
- details.url,
- {
- talerPayUri: talerUri,
- },
- );
- case TalerUriType.TalerTip:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/tip",
- details.tabId,
- details.url,
- {
- talerTipUri: talerUri,
- },
- );
- case TalerUriType.TalerRefund:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/refund",
- details.tabId,
- details.url,
- {
- talerRefundUri: talerUri,
- },
- );
- case TalerUriType.TalerNotifyReserve:
- Promise.resolve().then(() => {
- const w = currentWallet;
- if (!w) {
- return;
- }
- // FIXME: Is this still useful?
- // handleNotifyReserve(w);
- });
- break;
- default:
- console.warn(
- "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
- );
- break;
- }
- }
- }
+ // Useful for debugging in the background page.
+ if (typeof window !== "undefined") {
+ (window as any).talerWallet = wallet;
}
- return;
-}
-
-function setupHeaderListener(): void {
- console.log("setting up header listener");
- // Handlers for catching HTTP requests
- getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
- if (
- "webRequest" in chrome &&
- "onHeadersReceived" in chrome.webRequest &&
- chrome.webRequest.onHeadersReceived.hasListener(headerListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(headerListener);
- }
- if (result) {
- console.log("actually adding listener");
- chrome.webRequest.onHeadersReceived.addListener(
- headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders", "blocking"],
- );
- }
- if ("webRequest" in chrome) {
- chrome.webRequest.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- }
- });
- }
- });
+ currentWallet = wallet;
+ updateIconBasedOnBalance();
+ return walletInit.resolve();
}
/**
@@ -415,44 +356,74 @@ function setupHeaderListener(): void {
* Sets up all event handlers and other machinery.
*/
export async function wxMain(): Promise<void> {
- // Explicitly unload the extension page as soon as an update is available,
- // so the update gets installed as soon as possible.
- chrome.runtime.onUpdateAvailable.addListener((details) => {
- console.log("update available:", details);
- chrome.runtime.reload();
- });
- reinitWallet();
+ logger.trace("starting");
+ const afterWalletIsInitialized = reinitWallet();
+
+ logger.trace("reload on new version");
+ platform.registerReloadOnNewVersion();
// Handlers for messages coming directly from the content
// script on the page
- chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
- dispatch(req, sender, sendResponse);
- return true;
+ logger.trace("listen all channels");
+ platform.listenToAllChannels(async (message) => {
+ //wait until wallet is initialized
+ await afterWalletIsInitialized;
+ const result = await dispatch(message);
+ return result;
});
- chrome.runtime.onConnect.addListener((port) => {
- notificationPorts.push(port);
- port.onDisconnect.addListener((discoPort) => {
- const idx = notificationPorts.indexOf(discoPort);
- if (idx >= 0) {
- notificationPorts.splice(idx, 1);
- }
- });
- });
+ logger.trace("register all incoming connections");
+ platform.registerAllIncomingConnections();
+ logger.trace("redirect if first start");
try {
- setupHeaderListener();
+ platform.registerOnInstalled(() => {
+ platform.openWalletPage("/welcome");
+ });
} catch (e) {
- console.log(e);
+ console.error(e);
}
+}
- // On platforms that support it, also listen to external
- // modification of permissions.
- getPermissionsApi().addPermissionsListener((perm) => {
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- return;
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags))
+ showAlert = true;
+ break;
+ }
}
- setupHeaderListener();
- });
+
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
+ }
+ }
+}
+
+/**
+ * All the actions triggered by notification that need to be
+ * run in the background.
+ *
+ * @param message
+ */
+async function processWalletNotification(message: WalletNotification) {
+ if (
+ message.type === NotificationType.TransactionStateTransition &&
+ (message.newTxState.minor === TransactionMinorState.KycRequired ||
+ message.oldTxState.minor === TransactionMinorState.KycRequired ||
+ message.newTxState.minor === TransactionMinorState.AmlRequired ||
+ message.oldTxState.minor === TransactionMinorState.AmlRequired ||
+ message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
+ message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
+ ) {
+ await updateIconBasedOnBalance();
+ }
}