/* This file is part of GNU Taler (C) 2019 GNUnet e.V. 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. 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 GNU Taler; see the file COPYING. If not, see */ /** * Imports. */ import { Wallet, getDefaultNodeWallet, DefaultNodeWalletArgs, NodeHttpLib, makeErrorDetails, handleWorkerError, handleWorkerMessage, HttpRequestLibrary, OpenedPromise, HttpResponse, HttpRequestOptions, openPromise, Headers, WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION, } from "@gnu-taler/taler-wallet-core"; import fs from "fs"; import { WalletNotification } from "../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/notifications.js"; import { TalerErrorCode } from "../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/taler-error-codes.js"; import { CoreApiEnvelope, CoreApiResponse, CoreApiResponseSuccess, } from "../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/walletTypes.js"; export { handleWorkerError, handleWorkerMessage }; export class AndroidHttpLib implements HttpRequestLibrary { useNfcTunnel = false; private nodeHttpLib: HttpRequestLibrary = new NodeHttpLib(); private requestId = 1; private requestMap: { [id: number]: OpenedPromise; } = {}; constructor(private sendMessage: (m: string) => void) {} fetch(url: string, opt?: HttpRequestOptions): Promise { return this.nodeHttpLib.fetch(url, opt); } get(url: string, opt?: HttpRequestOptions): Promise { if (this.useNfcTunnel) { const myId = this.requestId++; const p = openPromise(); this.requestMap[myId] = p; const request = { method: "get", url, }; this.sendMessage( JSON.stringify({ type: "tunnelHttp", request, id: myId, }), ); return p.promise; } else { return this.nodeHttpLib.get(url, opt); } } postJson( url: string, body: any, opt?: HttpRequestOptions, ): Promise { if (this.useNfcTunnel) { const myId = this.requestId++; const p = openPromise(); this.requestMap[myId] = p; const request = { method: "postJson", url, body, }; this.sendMessage( JSON.stringify({ type: "tunnelHttp", request, id: myId }), ); return p.promise; } else { return this.nodeHttpLib.postJson(url, body, opt); } } handleTunnelResponse(msg: any): void { const myId = msg.id; const p = this.requestMap[myId]; if (!p) { console.error( `no matching request for tunneled HTTP response, id=${myId}`, ); } const headers = new Headers(); if (msg.status != 0) { const resp: HttpResponse = { // FIXME: pass through this URL requestUrl: "", headers, status: msg.status, requestMethod: "FIXME", json: async () => JSON.parse(msg.responseText), text: async () => msg.responseText, bytes: async () => { throw Error("bytes() not supported for tunnel response"); }, }; p.resolve(resp); } else { p.reject(new Error(`unexpected HTTP status code ${msg.status}`)); } delete this.requestMap[myId]; } } function sendAkonoMessage(ev: CoreApiEnvelope): void { // @ts-ignore const sendMessage = globalThis.__akono_sendMessage; if (typeof sendMessage !== "function") { const errMsg = "FATAL: cannot install android wallet listener: akono functions missing"; console.error(errMsg); throw new Error(errMsg); } const m = JSON.stringify(ev); // @ts-ignore sendMessage(m); } class AndroidWalletMessageHandler { walletArgs: DefaultNodeWalletArgs | undefined; maybeWallet: Wallet | undefined; wp = openPromise(); httpLib = new NodeHttpLib(); /** * Handle a request from the Android wallet. */ async handleMessage( operation: string, id: string, args: any, ): Promise { const wrapResponse = (result: unknown): CoreApiResponseSuccess => { return { type: "response", id, operation, result, }; }; switch (operation) { case "init": { this.walletArgs = { notifyHandler: async (notification: WalletNotification) => { sendAkonoMessage({ type: "notification", payload: notification }); }, persistentStoragePath: args.persistentStoragePath, httpLib: this.httpLib, }; const w = await getDefaultNodeWallet(this.walletArgs); this.maybeWallet = w; w.runRetryLoop().catch((e) => { console.error("Error during wallet retry loop", e); }); this.wp.resolve(w); return wrapResponse({ supported_protocol_versions: { exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION, }, }); } case "getHistory": { return wrapResponse({ history: [] }); } case "startTunnel": { // this.httpLib.useNfcTunnel = true; throw Error("not implemented"); } case "stopTunnel": { // this.httpLib.useNfcTunnel = false; throw Error("not implemented"); } case "tunnelResponse": { // httpLib.handleTunnelResponse(msg.args); throw Error("not implemented"); } case "reset": { const oldArgs = this.walletArgs; this.walletArgs = { ...oldArgs }; if (oldArgs && oldArgs.persistentStoragePath) { try { fs.unlinkSync(oldArgs.persistentStoragePath); } catch (e) { console.error("Error while deleting the wallet db:", e); } // Prevent further storage! this.walletArgs.persistentStoragePath = undefined; } const wallet = await this.wp.promise; wallet.stop(); this.wp = openPromise(); this.maybeWallet = undefined; const w = await getDefaultNodeWallet(this.walletArgs); this.maybeWallet = w; w.runRetryLoop().catch((e) => { console.error("Error during wallet retry loop", e); }); this.wp.resolve(w); return wrapResponse({}); } default: { const wallet = await this.wp.promise; return await wallet.handleCoreApiRequest(operation, id, args); } } } } export function installAndroidWalletListener(): void { const handler = new AndroidWalletMessageHandler(); const onMessage = async (msgStr: any): Promise => { if (typeof msgStr !== "string") { console.error("expected string as message"); return; } const msg = JSON.parse(msgStr); const operation = msg.operation; if (typeof operation !== "string") { console.error( "message to android wallet helper must contain operation of type string", ); return; } const id = msg.id; console.log(`android listener: got request for ${operation} (${id})`); try { const respMsg = await handler.handleMessage(operation, id, msg.args); console.log( `android listener: sending success response for ${operation} (${id})`, ); sendAkonoMessage(respMsg); } catch (e) { const respMsg: CoreApiResponse = { type: "error", id, operation, error: makeErrorDetails( TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, "unexpected exception", {}, ), }; sendAkonoMessage(respMsg); return; } }; // @ts-ignore globalThis.__akono_onMessage = onMessage; console.log("android wallet listener installed"); }