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(() => {
async function getValue(): Promise<void> {
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);

View File

@ -35,7 +35,10 @@ export function useClipboardPermissions(): ToggleHandler {
useEffect(() => {
async function getValue(): Promise<void> {
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);

View File

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

View File

@ -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<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 {
version_name?: string | undefined;
version: string;
@ -183,10 +209,9 @@ export interface PlatformAPI {
*
* @return response from the backend
*/
sendMessageToWalletBackground(
operation: string,
payload: any,
): Promise<CoreApiResponse>;
sendMessageToBackground<Op extends WalletOperations | BackgroundOperations>(
message: MessageFromFrontend<Op>,
): Promise<MessageResponse>;
/**
* 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: <Op extends WalletOperations | BackgroundOperations>(
message: MessageFromFrontend<Op> & { id: string },
) => Promise<MessageResponse>,
): void;
/**

View File

@ -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<any> {
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,
);
async function sendMessageToBackground<
Op extends WalletOperations | BackgroundOperations,
>(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
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<any>((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: <Op extends WalletOperations | BackgroundOperations>(
message: MessageFromFrontend<Op> & { id: string },
) => Promise<MessageResponse>,
): 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;

View File

@ -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<Op>,
): Promise<MessageResponse> => {
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<string, never>;
@ -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<any>;
}
export default api;

View File

@ -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),
)
}
>

View File

@ -34,70 +34,72 @@ 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;
};
}
/**
* @deprecated Use {@link WxWalletCoreApiClient} instead.
*/
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>(
export interface BackgroundApiClient {
call<Op extends keyof BackgroundOperations>(
operation: Op,
payload: WalletCoreRequestType<Op>,
): Promise<WalletCoreResponseType<Op>> {
payload: BackgroundOperations[Op]["request"],
): 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;
const message: MessageFromFrontendBackground<Op> = {
channel: "background",
operation,
payload,
};
try {
response = await platform.sendMessageToWalletBackground(
operation,
payload,
);
response = await platform.sendMessageToBackground(message);
} catch (e) {
console.log("Error calling backend");
throw new Error(`Error contacting backend: ${e}`);
@ -110,29 +112,34 @@ export class WxWalletCoreApiClient implements WalletCoreApiClient {
}
}
export class BackgroundApiClient {
public resetDb(): Promise<void> {
return callBackend("reset-db", {});
}
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", {});
/**
* WalletCoreApiClient integration with browser platform
*/
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;
}
}
function onUpdateNotification(
messageTypes: Array<NotificationType>,
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,
},

View File

@ -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<WalletDiagnostics> {
return diagnostics;
}
async function dispatch(
req: any,
sender: any,
sendResponse: any,
): Promise<void> {
let r: CoreApiResponse;
type BackendHandlerType = {
[Op in keyof BackgroundOperations]: (
req: BackgroundOperations[Op]["request"],
) => Promise<BackgroundOperations[Op]["response"]>;
};
const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
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 };
}
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`),
),
};
}
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<void> {
@ -328,12 +356,11 @@ export async function wxMain(): Promise<void> {
// 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();