From 7873571d225347aa2174b6d8943d9df820f8817c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 21 Dec 2022 16:21:25 -0300 Subject: add typecheck to background operations --- .../src/hooks/useAutoOpenPermissions.ts | 9 +- .../src/hooks/useClipboardPermissions.ts | 7 +- .../src/hooks/useDiagnostics.ts | 2 +- .../taler-wallet-webextension/src/platform/api.ts | 41 ++++- .../src/platform/chrome.ts | 80 ++++---- .../taler-wallet-webextension/src/platform/dev.ts | 27 ++- .../src/wallet/DeveloperPage.tsx | 4 +- packages/taler-wallet-webextension/src/wxApi.ts | 135 +++++++------- .../taler-wallet-webextension/src/wxBackend.ts | 201 ++++++++++++--------- 9 files changed, 290 insertions(+), 216 deletions(-) (limited to 'packages') diff --git a/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts index cb90ec853..e375f4958 100644 --- a/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts +++ b/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts @@ -34,7 +34,10 @@ export function useAutoOpenPermissions(): ToggleHandler { useEffect(() => { async function getValue(): Promise { - const res = await api.background.containsHeaderListener(); + const res = await api.background.call( + "containsHeaderListener", + undefined, + ); setEnabled(res.newValue); } getValue(); @@ -63,12 +66,12 @@ async function handleAutoOpenPerm( onChange(false); throw lastError; } - const res = await background.toggleHeaderListener(granted); + const res = await background.call("toggleHeaderListener", granted); onChange(res.newValue); } else { try { await background - .toggleHeaderListener(false) + .call("toggleHeaderListener", false) .then((r) => onChange(r.newValue)); } catch (e) { console.log(e); diff --git a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts index eda2afd8d..3f2824d6b 100644 --- a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts +++ b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts @@ -35,7 +35,10 @@ export function useClipboardPermissions(): ToggleHandler { useEffect(() => { async function getValue(): Promise { - const res = await api.background.containsHeaderListener(); + const res = await api.background.call( + "containsHeaderListener", + undefined, + ); setEnabled(res.newValue); } getValue(); @@ -71,7 +74,7 @@ async function handleClipboardPerm( } else { try { await background - .toggleHeaderListener(false) + .call("toggleHeaderListener", false) .then((r) => onChange(r.newValue)); } catch (e) { console.log(e); diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts index 1f36ca6c0..fcd31b3c6 100644 --- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts +++ b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts @@ -34,7 +34,7 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] { } }, 1000); const doFetch = async (): Promise => { - const d = await api.background.getDiagnostics(); + const d = await api.background.call("getDiagnostics", undefined); gotDiagnostics = true; setDiagnostics(d); }; diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts index 37546d2df..7df190303 100644 --- a/packages/taler-wallet-webextension/src/platform/api.ts +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -15,6 +15,8 @@ */ import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util"; +import { WalletOperations } from "@gnu-taler/taler-wallet-core"; +import { BackgroundOperations } from "../wxApi.js"; export interface Permissions { /** @@ -50,6 +52,30 @@ export type MessageFromBackend = { type: NotificationType; }; +export type MessageFromFrontend< + Op extends BackgroundOperations | WalletOperations, +> = Op extends BackgroundOperations + ? MessageFromFrontendBackground + : Op extends WalletOperations + ? MessageFromFrontendWallet + : never; + +export type MessageFromFrontendBackground< + Op extends keyof BackgroundOperations, +> = { + channel: "background"; + operation: Op; + payload: BackgroundOperations[Op]["request"]; +}; + +export type MessageFromFrontendWallet = { + channel: "wallet"; + operation: Op; + payload: WalletOperations[Op]["request"]; +}; + +export type MessageResponse = CoreApiResponse; + export interface WalletWebExVersion { version_name?: string | undefined; version: string; @@ -183,10 +209,9 @@ export interface PlatformAPI { * * @return response from the backend */ - sendMessageToWalletBackground( - operation: string, - payload: any, - ): Promise; + sendMessageToBackground( + message: MessageFromFrontend, + ): Promise; /** * Used from the frontend to receive notifications about new information @@ -204,11 +229,9 @@ export interface PlatformAPI { * @param onNewMessage */ listenToAllChannels( - onNewMessage: ( - message: any, - sender: any, - sendResponse: (r: CoreApiResponse) => void, - ) => void, + notifyNewMessage: ( + message: MessageFromFrontend & { id: string }, + ) => Promise, ): void; /** diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index 7785e19ef..f951685d2 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -20,9 +20,13 @@ import { Logger, TalerUriType, } from "@gnu-taler/taler-util"; +import { WalletOperations } from "@gnu-taler/taler-wallet-core"; +import { BackgroundOperations } from "../wxApi.js"; import { CrossBrowserPermissionsApi, MessageFromBackend, + MessageFromFrontend, + MessageResponse, Permissions, PlatformAPI, } from "./api.js"; @@ -41,11 +45,11 @@ const api: PlatformAPI = { redirectTabToWalletPage, registerAllIncomingConnections, registerOnInstalled, - listenToAllChannels, + listenToAllChannels: listenToAllChannels as any, registerReloadOnNewVersion, registerTalerHeaderListener, sendMessageToAllChannels, - sendMessageToWalletBackground, + sendMessageToBackground, useServiceWorkerAsBackgroundProcess, containsTalerHeaderListener, keepAlive, @@ -302,30 +306,22 @@ function openWalletPageFromPopup(page: string): void { let i = 0; -async function sendMessageToWalletBackground( - operation: string, - payload: any, -): Promise { - return new Promise((resolve, reject) => { - logger.trace("send operation to the wallet background", operation); - chrome.runtime.sendMessage( - { operation, payload, id: `id_${i++ % 1000}` }, - (backgroundResponse) => { - logger.trace( - "BUG: got response from background", - backgroundResponse, - chrome.runtime.lastError, - ); +async function sendMessageToBackground< + Op extends WalletOperations | BackgroundOperations, +>(message: MessageFromFrontend): Promise { + const messageWithId = { ...message, id: `id_${i++ % 1000}` }; - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message); - } else { - resolve(backgroundResponse); - } - // return true to keep the channel open - return true; - }, - ); + return new Promise((resolve, reject) => { + logger.trace("send operation to the wallet background", message); + chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } else { + resolve(backgroundResponse); + } + // return true to keep the channel open + return true; + }); }); } @@ -377,21 +373,27 @@ function registerAllIncomingConnections(): void { } function listenToAllChannels( - cb: ( - message: any, - sender: any, - callback: (r: CoreApiResponse) => void, - ) => void, + notifyNewMessage: ( + message: MessageFromFrontend & { id: string }, + ) => Promise, ): void { - chrome.runtime.onMessage.addListener((m, s, c) => { - cb(m, s, (apiResponse) => { - logger.trace("BUG: sending response to client", apiResponse); - try { - c(apiResponse); - } catch (e) { - logger.error("wallet operation ended with error", e); - } - }); + chrome.runtime.onMessage.addListener((message, sender, reply) => { + notifyNewMessage(message) + .then((apiResponse) => { + try { + reply(apiResponse); + } catch (e) { + logger.error( + "sending response to frontend failed", + message, + apiResponse, + e, + ); + } + }) + .catch((e) => { + logger.error("notify to background failed", e); + }); // keep the connection open return true; diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts index 8a410b062..df40b29e7 100644 --- a/packages/taler-wallet-webextension/src/platform/dev.ts +++ b/packages/taler-wallet-webextension/src/platform/dev.ts @@ -15,7 +15,14 @@ */ import { CoreApiResponse } from "@gnu-taler/taler-util"; -import { MessageFromBackend, PlatformAPI } from "./api.js"; +import { WalletOperations } from "@gnu-taler/taler-wallet-core"; +import { BackgroundOperations } from "../wxApi.js"; +import { + MessageFromBackend, + MessageFromFrontend, + MessageResponse, + PlatformAPI, +} from "./api.js"; const frames = ["popup", "wallet"]; @@ -121,12 +128,17 @@ const api: PlatformAPI = { window.parent.removeEventListener("message", listener); }; }, - sendMessageToWalletBackground: async (operation: string, payload: any) => { + + sendMessageToBackground: async < + Op extends WalletOperations | BackgroundOperations, + >( + payload: MessageFromFrontend, + ): Promise => { const replyMe = `reply-${Math.floor(Math.random() * 100000)}`; const message: IframeMessageCommand = { type: "command", header: { replyMe }, - body: { operation, payload, id: "(none)" }, + body: payload, }; window.parent.postMessage(message); @@ -150,6 +162,7 @@ type IframeMessageType = | IframeMessageNotification | IframeMessageResponse | IframeMessageCommand; + interface IframeMessageNotification { type: "notification"; header: Record; @@ -160,7 +173,7 @@ interface IframeMessageResponse { header: { responseId: string; }; - body: CoreApiResponse; + body: MessageResponse; } interface IframeMessageCommand { @@ -168,11 +181,7 @@ interface IframeMessageCommand { header: { replyMe: string; }; - body: { - operation: any; - id: string; - payload: any; - }; + body: MessageFromFrontend; } export default api; diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index c42798c8f..cbf6b1c0a 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -177,7 +177,7 @@ export function View({ onClick={() => confirmReset( i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`, - () => api.background.resetDb(), + () => api.background.call("resetDb", undefined), ) } > @@ -190,7 +190,7 @@ export function View({ onClick={() => confirmReset( i18n.str`TESTING: This may delete all your coin, proceed with caution`, - () => api.background.runGarbageCollector(), + () => api.background.call("runGarbageCollector", undefined), ) } > diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 991cbb5fe..ea087fc5d 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -34,70 +34,100 @@ import { WalletCoreRequestType, WalletCoreResponseType, } from "@gnu-taler/taler-wallet-core"; -import { MessageFromBackend, platform } from "./platform/api.js"; -import { nullFunction } from "./test-utils.js"; +import { + MessageFromBackend, + MessageFromFrontendBackground, + MessageFromFrontendWallet, + platform, +} from "./platform/api.js"; /** * - * @author Florian Dold * @author sebasjm */ +const logger = new Logger("wxApi"); + export interface ExtendedPermissionsResponse { newValue: boolean; } -const logger = new Logger("wxApi"); -/** - * Response with information about available version upgrades. - */ -export interface UpgradeResponse { - /** - * Is a reset required because of a new DB version - * that can't be automatically upgraded? - */ - dbResetRequired: boolean; - - /** - * Current database version. - */ - currentDbVersion: string; - - /** - * Old db version (if applicable). - */ - oldDbVersion: string; +export interface BackgroundOperations { + resetDb: { + request: void; + response: void; + }; + containsHeaderListener: { + request: void; + response: ExtendedPermissionsResponse; + }; + getDiagnostics: { + request: void; + response: WalletDiagnostics; + }; + toggleHeaderListener: { + request: boolean; + response: ExtendedPermissionsResponse; + }; + runGarbageCollector: { + request: void; + response: void; + }; +} + +export interface BackgroundApiClient { + call( + operation: Op, + payload: BackgroundOperations[Op]["request"], + ): Promise; } /** - * @deprecated Use {@link WxWalletCoreApiClient} instead. + * BackgroundApiClient integration with browser platform */ -async function callBackend(operation: string, payload: any): Promise { - let response: CoreApiResponse; - try { - response = await platform.sendMessageToWalletBackground(operation, payload); - } catch (e) { - console.log("Error calling backend"); - throw new Error(`Error contacting backend: ${e}`); - } - logger.info("got response", response); - if (response.type === "error") { - throw TalerError.fromUncheckedDetail(response.error); +class BackgroundApiClientImpl implements BackgroundApiClient { + async call( + operation: Op, + payload: BackgroundOperations[Op]["request"], + ): Promise { + let response: CoreApiResponse; + + const message: MessageFromFrontendBackground = { + channel: "background", + operation, + payload, + }; + + try { + response = await platform.sendMessageToBackground(message); + } catch (e) { + console.log("Error calling backend"); + throw new Error(`Error contacting backend: ${e}`); + } + logger.info("got response", response); + if (response.type === "error") { + throw TalerError.fromUncheckedDetail(response.error); + } + return response.result as any; } - return response.result; } -export class WxWalletCoreApiClient implements WalletCoreApiClient { +/** + * WalletCoreApiClient integration with browser platform + */ +class WalletApiClientImpl implements WalletCoreApiClient { async call( operation: Op, payload: WalletCoreRequestType, ): Promise> { let response: CoreApiResponse; try { - response = await platform.sendMessageToWalletBackground( + const message: MessageFromFrontendWallet = { + channel: "wallet", operation, payload, - ); + }; + response = await platform.sendMessageToBackground(message); } catch (e) { console.log("Error calling backend"); throw new Error(`Error contacting backend: ${e}`); @@ -110,29 +140,6 @@ export class WxWalletCoreApiClient implements WalletCoreApiClient { } } -export class BackgroundApiClient { - public resetDb(): Promise { - return callBackend("reset-db", {}); - } - - public containsHeaderListener(): Promise { - return callBackend("containsHeaderListener", {}); - } - - public getDiagnostics(): Promise { - return callBackend("wxGetDiagnostics", {}); - } - - public toggleHeaderListener( - value: boolean, - ): Promise { - return callBackend("toggleHeaderListener", { value }); - } - - public runGarbageCollector(): Promise { - return callBackend("run-gc", {}); - } -} function onUpdateNotification( messageTypes: Array, doCallback: undefined | (() => void), @@ -160,8 +167,8 @@ export type WxApiType = { }; export const wxApi = { - wallet: new WxWalletCoreApiClient(), - background: new BackgroundApiClient(), + wallet: new WalletApiClientImpl(), + background: new BackgroundApiClientImpl(), listener: { onUpdateNotification, }, diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 28adfa037..c94b390ff 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -25,8 +25,6 @@ */ import { classifyTalerUri, - CoreApiResponse, - CoreApiResponseSuccess, Logger, TalerErrorCode, TalerUriType, @@ -36,20 +34,27 @@ import { DbAccess, deleteTalerDatabase, exportDb, + getErrorDetailFromException, importDb, makeErrorDetail, OpenedPromise, openPromise, openTalerDatabase, + SetTimeoutTimerAPI, Wallet, + WalletOperations, WalletStoresV1, } from "@gnu-taler/taler-wallet-core"; -import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core"; -import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js"; import { BrowserHttpLib } from "./browserHttpLib.js"; -import { MessageFromBackend, platform } from "./platform/api.js"; +import { + MessageFromBackend, + MessageFromFrontend, + MessageResponse, + platform, +} from "./platform/api.js"; import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js"; import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js"; +import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js"; /** * Currently active wallet instance. Might be unloaded and @@ -107,92 +112,115 @@ async function getDiagnostics(): Promise { return diagnostics; } -async function dispatch( - req: any, - sender: any, - sendResponse: any, -): Promise { - let r: CoreApiResponse; +type BackendHandlerType = { + [Op in keyof BackgroundOperations]: ( + req: BackgroundOperations[Op]["request"], + ) => Promise; +}; + +async function containsHeaderListener(): Promise { + const result = await platform.containsTalerHeaderListener(); + return { newValue: result }; +} + +async function resetDb(): Promise { + await deleteTalerDatabase(indexedDB as any); + await reinitWallet(); +} + +async function runGarbageCollector(): Promise { + 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 { + 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 }; +} - const wrapResponse = (result: unknown): CoreApiResponseSuccess => { +const backendHandlers: BackendHandlerType = { + containsHeaderListener, + getDiagnostics, + resetDb, + runGarbageCollector, + toggleHeaderListener, +}; + +async function dispatch( + req: MessageFromFrontend & { id: string }, +): Promise { + 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`), + ), + }; + } + const result = await handler(req.payload); return { type: "response", id: req.id, - operation: req.operation, + operation: String(req.operation), result, }; - }; + } - try { - switch (req.operation) { - case "wxGetDiagnostics": { - r = wrapResponse(await getDiagnostics()); - break; - } - case "reset-db": { - await deleteTalerDatabase(indexedDB as any); - r = wrapResponse(await reinitWallet()); - break; - } - case "run-gc": { - logger.info("gc"); - const dump = await exportDb(currentDatabase!.idbHandle()); - await deleteTalerDatabase(indexedDB as any); - logger.info("cleaned"); - await reinitWallet(); - logger.info("init"); - await importDb(currentDatabase!.idbHandle(), dump); - logger.info("imported"); - r = wrapResponse({ result: true }); - break; - } - case "containsHeaderListener": { - const res = await platform.containsTalerHeaderListener(); - r = wrapResponse({ newValue: res }); - break; - } - //FIXME: implement type checked api like WalletCoreApi - case "toggleHeaderListener": { - const newVal = req.payload.value; - logger.trace("new extended permissions value", newVal); - if (newVal) { - platform.registerTalerHeaderListener(parseTalerUriAndRedirect); - r = wrapResponse({ newValue: true }); - } else { - const rem = await platform - .getPermissionsApi() - .removeHostPermissions(); - logger.trace("permissions removed:", rem); - r = wrapResponse({ newVal: false }); - } - break; - } - default: { - const w = currentWallet; - if (!w) { - r = { - type: "error", - id: req.id, - operation: req.operation, - error: makeErrorDetail( - TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, - {}, - "wallet core not available", - ), - }; - break; - } - r = await w.handleCoreApiRequest(req.operation, req.id, req.payload); - console.log("response received from wallet", r); - break; - } + 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", + ), + }; } - sendResponse(r); - } catch (e) { - logger.error(`Error sending operation: ${req.operation}`, e); - // might fail if tab disconnected + return await w.handleCoreApiRequest(req.operation, req.id, req.payload); } + + const anyReq = req as any; + return { + type: "error", + id: anyReq.id, + operation: String(anyReq.operation), + error: getErrorDetailFromException( + Error(`unknown channel ${anyReq.channel}`), + ), + }; } async function reinitWallet(): Promise { @@ -328,12 +356,11 @@ export async function wxMain(): Promise { // Handlers for messages coming directly from the content // script on the page - platform.listenToAllChannels((message, sender, callback) => { - afterWalletIsInitialized.then(() => { - dispatch(message, sender, (response: CoreApiResponse) => { - callback(response); - }); - }); + platform.listenToAllChannels(async (message) => { + //wait until wallet is initialized + await afterWalletIsInitialized; + const result = await dispatch(message); + return result; }); platform.registerAllIncomingConnections(); -- cgit v1.2.3