diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wxBackend.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/wxBackend.ts | 685 |
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(); + } } |