add typecheck to background operations

This commit is contained in:
Sebastian 2022-12-21 16:21:25 -03:00
parent 8a98a5f880
commit 7873571d22
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
9 changed files with 298 additions and 224 deletions

View File

@ -34,7 +34,10 @@ export function useAutoOpenPermissions(): ToggleHandler {
useEffect(() => { useEffect(() => {
async function getValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await api.background.containsHeaderListener(); const res = await api.background.call(
"containsHeaderListener",
undefined,
);
setEnabled(res.newValue); setEnabled(res.newValue);
} }
getValue(); getValue();
@ -63,12 +66,12 @@ async function handleAutoOpenPerm(
onChange(false); onChange(false);
throw lastError; throw lastError;
} }
const res = await background.toggleHeaderListener(granted); const res = await background.call("toggleHeaderListener", granted);
onChange(res.newValue); onChange(res.newValue);
} else { } else {
try { try {
await background await background
.toggleHeaderListener(false) .call("toggleHeaderListener", false)
.then((r) => onChange(r.newValue)); .then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View File

@ -35,7 +35,10 @@ export function useClipboardPermissions(): ToggleHandler {
useEffect(() => { useEffect(() => {
async function getValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await api.background.containsHeaderListener(); const res = await api.background.call(
"containsHeaderListener",
undefined,
);
setEnabled(res.newValue); setEnabled(res.newValue);
} }
getValue(); getValue();
@ -71,7 +74,7 @@ async function handleClipboardPerm(
} else { } else {
try { try {
await background await background
.toggleHeaderListener(false) .call("toggleHeaderListener", false)
.then((r) => onChange(r.newValue)); .then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View File

@ -34,7 +34,7 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
} }
}, 1000); }, 1000);
const doFetch = async (): Promise<void> => { const doFetch = async (): Promise<void> => {
const d = await api.background.getDiagnostics(); const d = await api.background.call("getDiagnostics", undefined);
gotDiagnostics = true; gotDiagnostics = true;
setDiagnostics(d); setDiagnostics(d);
}; };

View File

@ -15,6 +15,8 @@
*/ */
import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util"; import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util";
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import { BackgroundOperations } from "../wxApi.js";
export interface Permissions { export interface Permissions {
/** /**
@ -50,6 +52,30 @@ export type MessageFromBackend = {
type: NotificationType; type: NotificationType;
}; };
export type MessageFromFrontend<
Op extends BackgroundOperations | WalletOperations,
> = Op extends BackgroundOperations
? MessageFromFrontendBackground<keyof BackgroundOperations>
: Op extends WalletOperations
? MessageFromFrontendWallet<keyof WalletOperations>
: never;
export type MessageFromFrontendBackground<
Op extends keyof BackgroundOperations,
> = {
channel: "background";
operation: Op;
payload: BackgroundOperations[Op]["request"];
};
export type MessageFromFrontendWallet<Op extends keyof WalletOperations> = {
channel: "wallet";
operation: Op;
payload: WalletOperations[Op]["request"];
};
export type MessageResponse = CoreApiResponse;
export interface WalletWebExVersion { export interface WalletWebExVersion {
version_name?: string | undefined; version_name?: string | undefined;
version: string; version: string;
@ -183,10 +209,9 @@ export interface PlatformAPI {
* *
* @return response from the backend * @return response from the backend
*/ */
sendMessageToWalletBackground( sendMessageToBackground<Op extends WalletOperations | BackgroundOperations>(
operation: string, message: MessageFromFrontend<Op>,
payload: any, ): Promise<MessageResponse>;
): Promise<CoreApiResponse>;
/** /**
* Used from the frontend to receive notifications about new information * Used from the frontend to receive notifications about new information
@ -204,11 +229,9 @@ export interface PlatformAPI {
* @param onNewMessage * @param onNewMessage
*/ */
listenToAllChannels( listenToAllChannels(
onNewMessage: ( notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
message: any, message: MessageFromFrontend<Op> & { id: string },
sender: any, ) => Promise<MessageResponse>,
sendResponse: (r: CoreApiResponse) => void,
) => void,
): void; ): void;
/** /**

View File

@ -20,9 +20,13 @@ import {
Logger, Logger,
TalerUriType, TalerUriType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import { BackgroundOperations } from "../wxApi.js";
import { import {
CrossBrowserPermissionsApi, CrossBrowserPermissionsApi,
MessageFromBackend, MessageFromBackend,
MessageFromFrontend,
MessageResponse,
Permissions, Permissions,
PlatformAPI, PlatformAPI,
} from "./api.js"; } from "./api.js";
@ -41,11 +45,11 @@ const api: PlatformAPI = {
redirectTabToWalletPage, redirectTabToWalletPage,
registerAllIncomingConnections, registerAllIncomingConnections,
registerOnInstalled, registerOnInstalled,
listenToAllChannels, listenToAllChannels: listenToAllChannels as any,
registerReloadOnNewVersion, registerReloadOnNewVersion,
registerTalerHeaderListener, registerTalerHeaderListener,
sendMessageToAllChannels, sendMessageToAllChannels,
sendMessageToWalletBackground, sendMessageToBackground,
useServiceWorkerAsBackgroundProcess, useServiceWorkerAsBackgroundProcess,
containsTalerHeaderListener, containsTalerHeaderListener,
keepAlive, keepAlive,
@ -302,21 +306,14 @@ function openWalletPageFromPopup(page: string): void {
let i = 0; let i = 0;
async function sendMessageToWalletBackground( async function sendMessageToBackground<
operation: string, Op extends WalletOperations | BackgroundOperations,
payload: any, >(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
): Promise<any> { const messageWithId = { ...message, id: `id_${i++ % 1000}` };
return new Promise<any>((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,
);
return new Promise<any>((resolve, reject) => {
logger.trace("send operation to the wallet background", message);
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError.message); reject(chrome.runtime.lastError.message);
} else { } else {
@ -324,8 +321,7 @@ async function sendMessageToWalletBackground(
} }
// return true to keep the channel open // return true to keep the channel open
return true; return true;
}, });
);
}); });
} }
@ -377,20 +373,26 @@ function registerAllIncomingConnections(): void {
} }
function listenToAllChannels( function listenToAllChannels(
cb: ( notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
message: any, message: MessageFromFrontend<Op> & { id: string },
sender: any, ) => Promise<MessageResponse>,
callback: (r: CoreApiResponse) => void,
) => void,
): void { ): void {
chrome.runtime.onMessage.addListener((m, s, c) => { chrome.runtime.onMessage.addListener((message, sender, reply) => {
cb(m, s, (apiResponse) => { notifyNewMessage(message)
logger.trace("BUG: sending response to client", apiResponse); .then((apiResponse) => {
try { try {
c(apiResponse); reply(apiResponse);
} catch (e) { } catch (e) {
logger.error("wallet operation ended with error", e); logger.error(
"sending response to frontend failed",
message,
apiResponse,
e,
);
} }
})
.catch((e) => {
logger.error("notify to background failed", e);
}); });
// keep the connection open // keep the connection open

View File

@ -15,7 +15,14 @@
*/ */
import { CoreApiResponse } from "@gnu-taler/taler-util"; 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"]; const frames = ["popup", "wallet"];
@ -121,12 +128,17 @@ const api: PlatformAPI = {
window.parent.removeEventListener("message", listener); window.parent.removeEventListener("message", listener);
}; };
}, },
sendMessageToWalletBackground: async (operation: string, payload: any) => {
sendMessageToBackground: async <
Op extends WalletOperations | BackgroundOperations,
>(
payload: MessageFromFrontend<Op>,
): Promise<MessageResponse> => {
const replyMe = `reply-${Math.floor(Math.random() * 100000)}`; const replyMe = `reply-${Math.floor(Math.random() * 100000)}`;
const message: IframeMessageCommand = { const message: IframeMessageCommand = {
type: "command", type: "command",
header: { replyMe }, header: { replyMe },
body: { operation, payload, id: "(none)" }, body: payload,
}; };
window.parent.postMessage(message); window.parent.postMessage(message);
@ -150,6 +162,7 @@ type IframeMessageType =
| IframeMessageNotification | IframeMessageNotification
| IframeMessageResponse | IframeMessageResponse
| IframeMessageCommand; | IframeMessageCommand;
interface IframeMessageNotification { interface IframeMessageNotification {
type: "notification"; type: "notification";
header: Record<string, never>; header: Record<string, never>;
@ -160,7 +173,7 @@ interface IframeMessageResponse {
header: { header: {
responseId: string; responseId: string;
}; };
body: CoreApiResponse; body: MessageResponse;
} }
interface IframeMessageCommand { interface IframeMessageCommand {
@ -168,11 +181,7 @@ interface IframeMessageCommand {
header: { header: {
replyMe: string; replyMe: string;
}; };
body: { body: MessageFromFrontend<any>;
operation: any;
id: string;
payload: any;
};
} }
export default api; export default api;

View File

@ -177,7 +177,7 @@ export function View({
onClick={() => onClick={() =>
confirmReset( confirmReset(
i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`, 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={() => onClick={() =>
confirmReset( confirmReset(
i18n.str`TESTING: This may delete all your coin, proceed with caution`, i18n.str`TESTING: This may delete all your coin, proceed with caution`,
() => api.background.runGarbageCollector(), () => api.background.call("runGarbageCollector", undefined),
) )
} }
> >

View File

@ -34,70 +34,72 @@ import {
WalletCoreRequestType, WalletCoreRequestType,
WalletCoreResponseType, WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { MessageFromBackend, platform } from "./platform/api.js"; import {
import { nullFunction } from "./test-utils.js"; MessageFromBackend,
MessageFromFrontendBackground,
MessageFromFrontendWallet,
platform,
} from "./platform/api.js";
/** /**
* *
* @author Florian Dold
* @author sebasjm * @author sebasjm
*/ */
const logger = new Logger("wxApi");
export interface ExtendedPermissionsResponse { export interface ExtendedPermissionsResponse {
newValue: boolean; newValue: boolean;
} }
const logger = new Logger("wxApi");
/** export interface BackgroundOperations {
* Response with information about available version upgrades. resetDb: {
*/ request: void;
export interface UpgradeResponse { response: void;
/** };
* Is a reset required because of a new DB version containsHeaderListener: {
* that can't be automatically upgraded? request: void;
*/ response: ExtendedPermissionsResponse;
dbResetRequired: boolean; };
getDiagnostics: {
/** request: void;
* Current database version. response: WalletDiagnostics;
*/ };
currentDbVersion: string; toggleHeaderListener: {
request: boolean;
/** response: ExtendedPermissionsResponse;
* Old db version (if applicable). };
*/ runGarbageCollector: {
oldDbVersion: string; request: void;
response: void;
};
} }
/** export interface BackgroundApiClient {
* @deprecated Use {@link WxWalletCoreApiClient} instead. call<Op extends keyof BackgroundOperations>(
*/
async function callBackend(operation: string, payload: any): Promise<any> {
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);
}
return response.result;
}
export class WxWalletCoreApiClient implements WalletCoreApiClient {
async call<Op extends WalletCoreOpKeys>(
operation: Op, operation: Op,
payload: WalletCoreRequestType<Op>, payload: BackgroundOperations[Op]["request"],
): Promise<WalletCoreResponseType<Op>> { ): Promise<BackgroundOperations[Op]["response"]>;
}
/**
* BackgroundApiClient integration with browser platform
*/
class BackgroundApiClientImpl implements BackgroundApiClient {
async call<Op extends keyof BackgroundOperations>(
operation: Op,
payload: BackgroundOperations[Op]["request"],
): Promise<BackgroundOperations[Op]["response"]> {
let response: CoreApiResponse; let response: CoreApiResponse;
try {
response = await platform.sendMessageToWalletBackground( const message: MessageFromFrontendBackground<Op> = {
channel: "background",
operation, operation,
payload, payload,
); };
try {
response = await platform.sendMessageToBackground(message);
} catch (e) { } catch (e) {
console.log("Error calling backend"); console.log("Error calling backend");
throw new Error(`Error contacting backend: ${e}`); throw new Error(`Error contacting backend: ${e}`);
@ -110,29 +112,34 @@ export class WxWalletCoreApiClient implements WalletCoreApiClient {
} }
} }
export class BackgroundApiClient { /**
public resetDb(): Promise<void> { * WalletCoreApiClient integration with browser platform
return callBackend("reset-db", {}); */
class WalletApiClientImpl implements WalletCoreApiClient {
async call<Op extends WalletCoreOpKeys>(
operation: Op,
payload: WalletCoreRequestType<Op>,
): Promise<WalletCoreResponseType<Op>> {
let response: CoreApiResponse;
try {
const message: MessageFromFrontendWallet<Op> = {
channel: "wallet",
operation,
payload,
};
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;
}
} }
public containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
return callBackend("containsHeaderListener", {});
}
public getDiagnostics(): Promise<WalletDiagnostics> {
return callBackend("wxGetDiagnostics", {});
}
public toggleHeaderListener(
value: boolean,
): Promise<ExtendedPermissionsResponse> {
return callBackend("toggleHeaderListener", { value });
}
public runGarbageCollector(): Promise<void> {
return callBackend("run-gc", {});
}
}
function onUpdateNotification( function onUpdateNotification(
messageTypes: Array<NotificationType>, messageTypes: Array<NotificationType>,
doCallback: undefined | (() => void), doCallback: undefined | (() => void),
@ -160,8 +167,8 @@ export type WxApiType = {
}; };
export const wxApi = { export const wxApi = {
wallet: new WxWalletCoreApiClient(), wallet: new WalletApiClientImpl(),
background: new BackgroundApiClient(), background: new BackgroundApiClientImpl(),
listener: { listener: {
onUpdateNotification, onUpdateNotification,
}, },

View File

@ -25,8 +25,6 @@
*/ */
import { import {
classifyTalerUri, classifyTalerUri,
CoreApiResponse,
CoreApiResponseSuccess,
Logger, Logger,
TalerErrorCode, TalerErrorCode,
TalerUriType, TalerUriType,
@ -36,20 +34,27 @@ import {
DbAccess, DbAccess,
deleteTalerDatabase, deleteTalerDatabase,
exportDb, exportDb,
getErrorDetailFromException,
importDb, importDb,
makeErrorDetail, makeErrorDetail,
OpenedPromise, OpenedPromise,
openPromise, openPromise,
openTalerDatabase, openTalerDatabase,
SetTimeoutTimerAPI,
Wallet, Wallet,
WalletOperations,
WalletStoresV1, WalletStoresV1,
} from "@gnu-taler/taler-wallet-core"; } 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 { 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 { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js"; import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js";
/** /**
* Currently active wallet instance. Might be unloaded and * Currently active wallet instance. Might be unloaded and
@ -107,70 +112,92 @@ async function getDiagnostics(): Promise<WalletDiagnostics> {
return diagnostics; return diagnostics;
} }
async function dispatch( type BackendHandlerType = {
req: any, [Op in keyof BackgroundOperations]: (
sender: any, req: BackgroundOperations[Op]["request"],
sendResponse: any, ) => Promise<BackgroundOperations[Op]["response"]>;
): Promise<void> {
let r: CoreApiResponse;
const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id: req.id,
operation: req.operation,
result,
};
}; };
try { async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
switch (req.operation) { const result = await platform.containsTalerHeaderListener();
case "wxGetDiagnostics": { return { newValue: result };
r = wrapResponse(await getDiagnostics());
break;
} }
case "reset-db": {
async function resetDb(): Promise<void> {
await deleteTalerDatabase(indexedDB as any); await deleteTalerDatabase(indexedDB as any);
r = wrapResponse(await reinitWallet()); await reinitWallet();
break;
} }
case "run-gc": {
logger.info("gc"); async function runGarbageCollector(): Promise<void> {
const dump = await exportDb(currentDatabase!.idbHandle()); const dbBeforeGc = currentDatabase;
if (!dbBeforeGc) {
throw Error("no current db before running gc");
}
const dump = await exportDb(dbBeforeGc.idbHandle());
await deleteTalerDatabase(indexedDB as any); await deleteTalerDatabase(indexedDB as any);
logger.info("cleaned"); logger.info("cleaned");
await reinitWallet(); await reinitWallet();
logger.info("init"); logger.info("init");
await importDb(currentDatabase!.idbHandle(), dump);
const dbAfterGc = currentDatabase;
if (!dbAfterGc) {
throw Error("no current db before running gc");
}
await importDb(dbAfterGc.idbHandle(), dump);
logger.info("imported"); logger.info("imported");
r = wrapResponse({ result: true });
break;
} }
case "containsHeaderListener": {
const res = await platform.containsTalerHeaderListener(); async function toggleHeaderListener(
r = wrapResponse({ newValue: res }); newVal: boolean,
break; ): Promise<ExtendedPermissionsResponse> {
}
//FIXME: implement type checked api like WalletCoreApi
case "toggleHeaderListener": {
const newVal = req.payload.value;
logger.trace("new extended permissions value", newVal); logger.trace("new extended permissions value", newVal);
if (newVal) { if (newVal) {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect); platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
r = wrapResponse({ newValue: true }); return { newValue: true };
} else { }
const rem = await platform
.getPermissionsApi() const rem = await platform.getPermissionsApi().removeHostPermissions();
.removeHostPermissions();
logger.trace("permissions removed:", rem); logger.trace("permissions removed:", rem);
r = wrapResponse({ newVal: false }); return { newValue: false };
} }
break;
const backendHandlers: BackendHandlerType = {
containsHeaderListener,
getDiagnostics,
resetDb,
runGarbageCollector,
toggleHeaderListener,
};
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`),
),
};
} }
default: { const result = await handler(req.payload);
return {
type: "response",
id: req.id,
operation: String(req.operation),
result,
};
}
if (req.channel === "wallet") {
const w = currentWallet; const w = currentWallet;
if (!w) { if (!w) {
r = { return {
type: "error", type: "error",
id: req.id, id: req.id,
operation: req.operation, operation: req.operation,
@ -180,19 +207,20 @@ async function dispatch(
"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;
}
} }
sendResponse(r); return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
} catch (e) {
logger.error(`Error sending operation: ${req.operation}`, e);
// might fail if tab disconnected
} }
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<void> { async function reinitWallet(): Promise<void> {
@ -328,12 +356,11 @@ export async function wxMain(): Promise<void> {
// Handlers for messages coming directly from the content // Handlers for messages coming directly from the content
// script on the page // script on the page
platform.listenToAllChannels((message, sender, callback) => { platform.listenToAllChannels(async (message) => {
afterWalletIsInitialized.then(() => { //wait until wallet is initialized
dispatch(message, sender, (response: CoreApiResponse) => { await afterWalletIsInitialized;
callback(response); const result = await dispatch(message);
}); return result;
});
}); });
platform.registerAllIncomingConnections(); platform.registerAllIncomingConnections();