wallet-core/packages/taler-wallet-webextension/src/wxBackend.ts

447 lines
12 KiB
TypeScript
Raw Normal View History

2016-01-05 14:20:13 +01:00
/*
2022-06-06 17:05:26 +02:00
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
2016-01-05 14:20:13 +01:00
2022-06-06 17:05:26 +02:00
GNU Taler is free software; you can redistribute it and/or modify it under the
2016-01-05 14:20:13 +01:00
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
2022-06-06 17:05:26 +02:00
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
2016-01-05 14:20:13 +01:00
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
2022-06-06 17:05:26 +02:00
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
2016-01-05 14:20:13 +01:00
*/
/**
* Messaging for the WebExtensions wallet. Should contain
* parts that are specific for WebExtensions, but as little business
* logic as possible.
*/
/**
* Imports.
*/
2021-03-27 14:35:58 +01:00
import {
classifyTalerUri,
2022-06-06 05:09:25 +02:00
Logger,
LogLevel,
setGlobalLogLevelFromString,
setLogLevelFromString,
2022-06-06 05:09:25 +02:00
TalerErrorCode,
2021-03-27 14:35:58 +01:00
TalerUriType,
2022-06-06 05:09:25 +02:00
WalletDiagnostics,
2023-02-15 23:32:42 +01:00
makeErrorDetail,
getErrorDetailFromException,
2021-03-27 14:35:58 +01:00
} from "@gnu-taler/taler-util";
import {
2022-02-03 14:36:37 +01:00
DbAccess,
deleteTalerDatabase,
2022-04-29 18:16:29 +02:00
exportDb,
importDb,
2022-02-03 14:36:37 +01:00
OpenedPromise,
openPromise,
2022-02-03 14:36:37 +01:00
openTalerDatabase,
2022-12-21 20:21:25 +01:00
SetTimeoutTimerAPI,
2022-02-03 14:36:37 +01:00
Wallet,
2022-12-21 20:21:25 +01:00
WalletOperations,
2022-06-06 05:09:25 +02:00
WalletStoresV1,
} from "@gnu-taler/taler-wallet-core";
2022-03-29 04:41:07 +02:00
import { BrowserHttpLib } from "./browserHttpLib.js";
2023-01-09 12:38:48 +01:00
import { platform } from "./platform/background.js";
2022-12-21 20:21:25 +01:00
import {
MessageFromBackend,
MessageFromFrontend,
MessageResponse,
} from "./platform/api.js";
2022-03-29 04:41:07 +02:00
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
2022-12-21 20:21:25 +01:00
import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js";
2020-04-06 20:02:01 +02:00
/**
* Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset.
*
2021-07-14 14:34:58 +02:00
* FIXME: Maybe move the wallet resetting into the Wallet class?
2020-04-06 20:02:01 +02:00
*/
let currentWallet: Wallet | undefined;
2020-04-06 20:02:01 +02:00
2021-06-09 15:14:17 +02:00
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
2020-04-06 20:02:01 +02:00
/**
2022-02-03 14:36:37 +01:00
* Last version of an outdated DB, if applicable.
2020-04-06 20:02:01 +02:00
*/
let outdatedDbVersion: number | undefined;
2020-08-12 09:11:00 +02:00
const walletInit: OpenedPromise<void> = openPromise<void>();
2020-04-06 20:02:01 +02:00
2022-04-21 20:39:30 +02:00
const logger = new Logger("wxBackend.ts");
async function getDiagnostics(): Promise<WalletDiagnostics> {
const manifestData = platform.getWalletWebExVersion();
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 &&
platform.isFirefox()
) {
firefoxIdbProblem = true;
}
}
if (!currentWallet) {
errors.push("Could not create wallet backend.");
}
if (!currentDatabase) {
errors.push("Could not open database");
}
if (outdatedDbVersion !== undefined) {
errors.push(`Outdated DB version: ${outdatedDbVersion}`);
dbOutdated = true;
}
const diagnostics: WalletDiagnostics = {
walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
walletManifestVersion: manifestData.version,
errors,
firefoxIdbProblem,
dbOutdated,
};
return diagnostics;
}
2022-12-21 20:21:25 +01:00
type BackendHandlerType = {
[Op in keyof BackgroundOperations]: (
req: BackgroundOperations[Op]["request"],
) => Promise<BackgroundOperations[Op]["response"]>;
};
async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
const result = await platform.containsTalerHeaderListener();
return { newValue: result };
}
async function resetDb(): Promise<void> {
await deleteTalerDatabase(indexedDB as any);
await reinitWallet();
}
async function runGarbageCollector(): Promise<void> {
const dbBeforeGc = currentDatabase;
if (!dbBeforeGc) {
throw Error("no current db before running gc");
}
const dump = await exportDb(dbBeforeGc.idbHandle());
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");
}
await importDb(dbAfterGc.idbHandle(), dump);
logger.info("imported");
}
async function toggleHeaderListener(
newVal: boolean,
): Promise<ExtendedPermissionsResponse> {
logger.trace("new extended permissions value", newVal);
if (newVal) {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
return { newValue: true };
}
const rem = await platform.getPermissionsApi().removeHostPermissions();
logger.trace("permissions removed:", rem);
return { newValue: false };
}
2022-12-21 20:36:24 +01:00
function freeze(time: number): Promise<void> {
return new Promise((res, rej) => {
setTimeout(res, time);
});
2022-12-21 20:36:24 +01:00
}
async function sum(ns: Array<number>): Promise<number> {
return ns.reduce((prev, cur) => prev + cur, 0);
2022-12-21 20:36:24 +01:00
}
2022-12-21 20:21:25 +01:00
const backendHandlers: BackendHandlerType = {
2022-12-21 20:36:24 +01:00
freeze,
sum,
2022-12-21 20:21:25 +01:00
containsHeaderListener,
getDiagnostics,
resetDb,
runGarbageCollector,
toggleHeaderListener,
setLoggingLevel,
2022-12-21 20:21:25 +01:00
};
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);
}
}
2022-12-21 20:21:25 +01:00
async function dispatch<Op extends WalletOperations | BackgroundOperations>(
req: MessageFromFrontend<Op> & { id: string },
): Promise<MessageResponse> {
if (req.channel === "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`),
),
};
}
2023-01-09 12:38:48 +01:00
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),
};
}
2022-12-21 20:21:25 +01:00
}
2022-12-21 20:21:25 +01:00
if (req.channel === "wallet") {
const w = currentWallet;
if (!w) {
return {
type: "error",
id: req.id,
operation: req.operation,
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
{},
"wallet core not available",
),
};
2021-05-07 15:38:28 +02:00
}
2020-08-13 20:43:51 +02:00
2022-12-21 20:21:25 +01:00
return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
}
2022-12-21 20:21:25 +01:00
const anyReq = req as any;
return {
type: "error",
id: anyReq.id,
operation: String(anyReq.operation),
error: getErrorDetailFromException(
2023-01-09 12:38:48 +01:00
Error(
`unknown channel ${anyReq.channel}, should be "background" or "wallet"`,
),
2022-12-21 20:21:25 +01:00
),
};
}
2020-04-06 20:02:01 +02:00
async function reinitWallet(): Promise<void> {
2017-06-05 02:00:03 +02:00
if (currentWallet) {
currentWallet.stop();
currentWallet = undefined;
}
currentDatabase = undefined;
// setBadgeText({ text: "" });
2017-06-05 02:00:03 +02:00
try {
currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
2017-06-05 02:00:03 +02:00
} catch (e) {
2022-04-21 20:39:30 +02:00
logger.error("could not open database", e);
walletInit.reject(e);
2017-06-05 02:00:03 +02:00
return;
}
let httpLib;
let cryptoWorker;
let timer;
if (platform.useServiceWorkerAsBackgroundProcess()) {
2022-02-03 14:36:37 +01:00
httpLib = new ServiceWorkerHttpLib();
cryptoWorker = new SynchronousCryptoWorkerFactory();
2022-04-28 18:26:29 +02:00
timer = new SetTimeoutTimerAPI();
} else {
2022-02-03 14:36:37 +01:00
httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
2022-09-30 13:11:17 +02:00
cryptoWorker = new SynchronousCryptoWorkerFactory();
timer = new SetTimeoutTimerAPI();
}
2022-04-21 20:39:30 +02:00
logger.info("Setting up wallet");
2022-06-06 05:09:25 +02:00
const wallet = await Wallet.create(
currentDatabase,
httpLib,
timer,
cryptoWorker,
);
2021-08-09 15:42:56 +02:00
try {
await wallet.handleCoreApiRequest("initWallet", "native-init", {});
} catch (e) {
2022-04-21 20:39:30 +02:00
logger.error("could not initialize wallet", e);
2021-08-09 15:42:56 +02:00
walletInit.reject(e);
return;
}
2020-05-04 14:11:22 +02:00
wallet.addNotificationListener((x) => {
const message: MessageFromBackend = { type: x.type };
2022-06-06 05:09:25 +02:00
platform.sendMessageToAllChannels(message);
2020-05-04 14:11:22 +02:00
});
2022-04-28 18:26:29 +02:00
platform.keepAlive(() => {
2022-05-06 22:29:42 +02:00
return wallet.runTaskLoop().catch((e) => {
2022-04-28 18:26:29 +02:00
logger.error("error during wallet task loop", e);
});
2022-06-06 05:09:25 +02:00
});
2017-06-05 02:00:03 +02:00
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
2017-06-05 02:00:03 +02:00
currentWallet = wallet;
return walletInit.resolve();
2017-06-05 02:00:03 +02:00
}
function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
const talerUri = maybeTalerUri.startsWith("ext+")
? maybeTalerUri.substring(4)
: maybeTalerUri;
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return platform.redirectTabToWalletPage(
tabId,
`/cta/withdraw?talerWithdrawUri=${talerUri}`,
);
case TalerUriType.TalerPay:
return platform.redirectTabToWalletPage(
tabId,
`/cta/pay?talerPayUri=${talerUri}`,
);
case TalerUriType.TalerTip:
return platform.redirectTabToWalletPage(
tabId,
`/cta/tip?talerTipUri=${talerUri}`,
);
case TalerUriType.TalerRefund:
return platform.redirectTabToWalletPage(
tabId,
`/cta/refund?talerRefundUri=${talerUri}`,
);
2022-08-31 05:20:35 +02:00
case TalerUriType.TalerPayPull:
return platform.redirectTabToWalletPage(
tabId,
`/cta/invoice/pay?talerPayPullUri=${talerUri}`,
);
case TalerUriType.TalerPayPush:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
);
case TalerUriType.TalerRecovery:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/recovery?talerBackupUri=${talerUri}`,
);
2022-08-31 05:20:35 +02:00
case TalerUriType.Unknown:
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
);
return;
2022-10-13 14:14:41 +02:00
case TalerUriType.TalerDevExperiment:
// FIXME: Implement!
logger.warn("not implemented");
return;
case TalerUriType.TalerTemplate:
return platform.redirectTabToWalletPage(
tabId,
`/cta/template?talerTemplateUri=${talerUri}`,
);
return;
2022-08-31 05:20:35 +02:00
default: {
const error: never = uriType;
logger.warn(
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
);
return;
}
}
}
2019-11-02 00:46:57 +01:00
/**
* Main function to run for the WebExtension backend.
*
* Sets up all event handlers and other machinery.
*/
2020-04-06 20:02:01 +02:00
export async function wxMain(): Promise<void> {
2022-06-06 05:09:25 +02:00
logger.trace("starting");
const afterWalletIsInitialized = reinitWallet();
2016-11-20 08:58:04 +01:00
platform.registerReloadOnNewVersion();
2016-11-20 08:58:04 +01:00
// Handlers for messages coming directly from the content
// script on the page
2022-12-21 20:21:25 +01:00
platform.listenToAllChannels(async (message) => {
//wait until wallet is initialized
await afterWalletIsInitialized;
const result = await dispatch(message);
return result;
2022-06-06 05:09:25 +02:00
});
2016-10-12 02:55:53 +02:00
2022-06-06 05:09:25 +02:00
platform.registerAllIncomingConnections();
2020-05-04 14:11:22 +02:00
2020-06-03 12:51:09 +02:00
try {
2022-09-12 19:28:53 +02:00
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
//
try {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
} catch (e) {
logger.error("could not register header listener", e);
}
});
2020-06-03 12:51:09 +02:00
} catch (e) {
2022-09-12 19:28:53 +02:00
console.error(e);
2020-06-03 12:51:09 +02:00
}
2020-05-04 14:11:22 +02:00
2020-06-03 12:51:09 +02:00
// On platforms that support it, also listen to external
// modification of permissions.
2022-03-25 20:57:27 +01:00
platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
if (lastError) {
2022-06-06 05:09:25 +02:00
logger.error(
`there was a problem trying to get permission ${perm}`,
lastError,
);
2020-05-01 11:34:12 +02:00
return;
}
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
2020-05-01 11:34:12 +02:00
});
2016-10-11 20:26:37 +02:00
}