From 3247a45d9709d787ac56e3148a0c8edc9a5e30a3 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 24 Aug 2019 19:31:24 +0200 Subject: [PATCH] always decode 'Taler-' headers --- src/webex/wxBackend.ts | 300 ++++++++++++++++++++++++++--------------- 1 file changed, 195 insertions(+), 105 deletions(-) diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index b9c0db873..69ba8bad1 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -20,7 +20,6 @@ * logic as possible. */ - /** * Imports. */ @@ -36,20 +35,14 @@ import { ReturnCoinsRequest, } from "../walletTypes"; -import { - Wallet, -} from "../wallet"; +import { Wallet } from "../wallet"; import { isFirefox } from "./compat"; -import { - PurchaseRecord, - WALLET_DB_VERSION, -} from "../dbTypes"; +import { PurchaseRecord, WALLET_DB_VERSION } from "../dbTypes"; import { openTalerDb, exportDb, importDb, deleteDb } from "../db"; - import { ChromeBadge } from "./chromeBadge"; import { MessageType } from "./messages"; import * as wxApi from "./wxApi"; @@ -62,8 +55,11 @@ import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi"; const NeedsWallet = Symbol("NeedsWallet"); -function handleMessage(sender: MessageSender, - type: MessageType, detail: any): any { +function handleMessage( + sender: MessageSender, + type: MessageType, + detail: any, +): any { function assertNotFound(t: never): never { console.error(`Request type ${t as string} unknown`); console.error(`Request detail was ${detail}`); @@ -133,7 +129,10 @@ function handleMessage(sender: MessageSender, if (typeof detail.contractTermsHash !== "string") { throw Error("contractTermsHash must be a string"); } - return needsWallet().submitPay(detail.contractTermsHash, detail.sessionId); + return needsWallet().submitPay( + detail.contractTermsHash, + detail.sessionId, + ); } case "check-pay": { if (typeof detail.proposalId !== "number") { @@ -172,9 +171,11 @@ function handleMessage(sender: MessageSender, if (!detail.contract) { return Promise.resolve({ error: "contract missing" }); } - return needsWallet().hashContract(detail.contract).then((hash) => { - return hash; - }); + return needsWallet() + .hashContract(detail.contract) + .then(hash => { + return hash; + }); } case "reserve-creation-info": { if (!detail.baseUrl || typeof detail.baseUrl !== "string") { @@ -269,8 +270,10 @@ function handleMessage(sender: MessageSender, return resp; } case "log-and-display-error": - logging.storeReport(detail).then((reportUid) => { - const url = chrome.extension.getURL(`/src/webex/pages/error.html?reportUid=${reportUid}`); + logging.storeReport(detail).then(reportUid => { + const url = chrome.extension.getURL( + `/src/webex/pages/error.html?reportUid=${reportUid}`, + ); if (detail.sameTab && sender && sender.tab && sender.tab.id) { chrome.tabs.update(detail.tabId, { url }); } else { @@ -343,7 +346,11 @@ function handleMessage(sender: MessageSender, } } -async function dispatch(req: any, sender: any, sendResponse: any): Promise { +async function dispatch( + req: any, + sender: any, + sendResponse: any, +): Promise { try { const p = handleMessage(sender, req.type, req.detail); const r = await p; @@ -375,12 +382,11 @@ async function dispatch(req: any, sender: any, sendResponse: any): Promise } } - class ChromeNotifier implements Notifier { private ports: Port[] = []; constructor() { - chrome.runtime.onConnect.addListener((port) => { + chrome.runtime.onConnect.addListener(port => { console.log("got connect!"); this.ports.push(port); port.onDisconnect.addListener(() => { @@ -401,8 +407,11 @@ class ChromeNotifier implements Notifier { } } - -async function talerPay(fields: any, url: string, tabId: number): Promise { +async function talerPay( + fields: any, + url: string, + tabId: number, +): Promise { if (!currentWallet) { console.log("can't handle payment, no wallet"); return undefined; @@ -422,13 +431,18 @@ async function talerPay(fields: any, url: string, tabId: number): Promise { return new Promise((resolve, reject) => { chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab)); }); } - function setBadgeText(options: chrome.browserAction.BadgeTextDetails) { // not supported by all browsers ... if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) { @@ -468,18 +482,20 @@ function setBadgeText(options: chrome.browserAction.BadgeTextDetails) { } } - function waitMs(timeoutMs: number): Promise { return new Promise((resolve, reject) => { - chrome.extension.getBackgroundPage()!.setTimeout(() => resolve(), timeoutMs); + chrome.extension + .getBackgroundPage()! + .setTimeout(() => resolve(), timeoutMs); }); } - -function makeSyncWalletRedirect(url: string, - tabId: number, - oldUrl: string, - params?: {[name: string]: string | undefined}): object { +function makeSyncWalletRedirect( + url: string, + tabId: number, + oldUrl: string, + params?: { [name: string]: string | undefined }, +): object { const innerUrl = new URI(chrome.extension.getURL("/src/webex/pages/" + url)); if (params) { for (const key in params) { @@ -488,12 +504,14 @@ function makeSyncWalletRedirect(url: string, } } } - const outerUrl = new URI(chrome.extension.getURL("/src/webex/pages/redirect.html")); + const outerUrl = new URI( + chrome.extension.getURL("/src/webex/pages/redirect.html"), + ); outerUrl.addSearch("url", innerUrl); if (isFirefox()) { // Some platforms don't support the sync redirect (yet), so fall back to // async redirect after a timeout. - const doit = async() => { + const doit = async () => { await waitMs(150); const tab = await getTab(tabId); if (tab.url === oldUrl) { @@ -505,14 +523,17 @@ function makeSyncWalletRedirect(url: string, return { redirectUrl: outerUrl.href() }; } - /** * Handle a HTTP response that has the "402 Payment Required" status. * In this callback we don't have access to the body, and must communicate via * shared state with the content script that will later be run later * in this tab. */ -function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any { +function handleHttpPayment( + headerList: chrome.webRequest.HttpHeader[], + url: string, + tabId: number, +): any { if (!currentWallet) { console.log("can't handle payment, no wallet"); return; @@ -525,16 +546,30 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri } } + const decodeIfDefined = (url?: string) => + url ? decodeURIComponent(url) : undefined; + const fields = { - contract_url: headers["x-taler-contract-url"] || headers["taler-contract-url"], - offer_url: headers["x-taler-offer-url"] || headers["taler-offer-url"], - refund_url: headers["x-taler-refund-url"] || headers["taler-refund-url"], - resource_url: headers["x-taler-resource-url"] || headers["taler-resource-url"], - session_id: headers["x-taler-session-id"] || headers["taler-session-id"], - tip: headers["x-taler-tip"] || headers["taler-tip"], + contract_url: decodeIfDefined( + headers["x-taler-contract-url"] || headers["taler-contract-url"], + ), + offer_url: decodeIfDefined( + headers["x-taler-offer-url"] || headers["taler-offer-url"], + ), + refund_url: decodeIfDefined( + headers["x-taler-refund-url"] || headers["taler-refund-url"], + ), + resource_url: decodeIfDefined( + headers["x-taler-resource-url"] || headers["taler-resource-url"], + ), + session_id: decodeIfDefined( + headers["x-taler-session-id"] || headers["taler-session-id"], + ), + tip: decodeIfDefined(headers["x-taler-tip"] || headers["taler-tip"]), }; - const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; + const talerHeaderFound = + Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; if (!talerHeaderFound) { // looks like it's not a taler request, it might be @@ -548,7 +583,11 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri // Synchronous fast path for existing payment if (fields.resource_url) { const result = currentWallet.getNextUrlFromResourceUrl(fields.resource_url); - if (result && (fields.session_id === undefined || fields.session_id === result.lastSessionId)) { + if ( + result && + (fields.session_id === undefined || + fields.session_id === result.lastSessionId) + ) { return { redirectUrl: result.nextUrl }; } } @@ -563,23 +602,29 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri // Synchronous fast path for tip if (fields.tip) { - return makeSyncWalletRedirect("tip.html", tabId, url, { tip_token: fields.tip }); + return makeSyncWalletRedirect("tip.html", tabId, url, { + tip_token: fields.tip, + }); } // Synchronous fast path for refund if (fields.refund_url) { console.log("processing refund"); - return makeSyncWalletRedirect("refund.html", tabId, url, { refundUrl: fields.refund_url }); + return makeSyncWalletRedirect("refund.html", tabId, url, { + refundUrl: fields.refund_url, + }); } // We need to do some asynchronous operation, we can't directly redirect - talerPay(fields, url, tabId).then((nextUrl) => { + talerPay(fields, url, tabId).then(nextUrl => { if (nextUrl) { // We use chrome.tabs.executeScript instead of chrome.tabs.update // because the latter is buggy when it does not execute in the same // (micro-?)task as the header callback. chrome.tabs.executeScript({ - code: `document.location.href = decodeURIComponent("${encodeURI(nextUrl)}");`, + code: `document.location.href = decodeURIComponent("${encodeURI( + nextUrl, + )}");`, runAt: "document_start", }); } @@ -588,9 +633,12 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri return; } - -function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { +function handleBankRequest( + wallet: Wallet, + headerList: chrome.webRequest.HttpHeader[], + url: string, + tabId: number, +): any { const headers: { [s: string]: string } = {}; for (const kv of headerList) { if (kv.value) { @@ -609,9 +657,11 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea const reservePub = headers["x-taler-reserve-pub"]; if (reservePub !== undefined) { console.log(`confirming reserve ${reservePub} via 201`); - wallet.confirmReserve({reservePub}); + wallet.confirmReserve({ reservePub }); } else { - console.warn("got 'X-Taler-Operation: confirm-reserve' without 'X-Taler-Reserve-Pub'"); + console.warn( + "got 'X-Taler-Operation: confirm-reserve' without 'X-Taler-Reserve-Pub'", + ); } return; } @@ -622,7 +672,8 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea console.log("202 not understood (X-Taler-Amount missing)"); return; } - const callbackUrl = headers["x-taler-callback-url"] || headers["taler-callback-url"]; + const callbackUrl = + headers["x-taler-callback-url"] || headers["taler-callback-url"]; if (!callbackUrl) { console.log("202 not understood (X-Taler-Callback-Url missing)"); return; @@ -630,13 +681,15 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea try { JSON.parse(amount); } catch (e) { - const errUri = new URI(chrome.extension.getURL("/src/webex/pages/error.html")); + const errUri = new URI( + chrome.extension.getURL("/src/webex/pages/error.html"), + ); const p = { message: `Can't parse amount ("${amount}"): ${e.message}`, }; const errRedirectUrl = errUri.query(p).href(); // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed - chrome.tabs.update(tabId, {url: errRedirectUrl}); + chrome.tabs.update(tabId, { url: errRedirectUrl }); return; } const wtTypes = headers["x-taler-wt-types"] || headers["taler-wt-types"]; @@ -647,12 +700,17 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea const params = { amount, bank_url: url, - callback_url: new URI(callbackUrl) .absoluteTo(url), - sender_wire: headers["x-taler-sender-wire"] || headers["taler-sender-wire"], - suggested_exchange_url: headers["x-taler-suggested-exchange"] || headers["taler-suggested-exchange"], + callback_url: new URI(callbackUrl).absoluteTo(url), + sender_wire: + headers["x-taler-sender-wire"] || headers["taler-sender-wire"], + suggested_exchange_url: + headers["x-taler-suggested-exchange"] || + headers["taler-suggested-exchange"], wt_types: wtTypes, }; - const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html")); + const uri = new URI( + chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"), + ); const redirectUrl = uri.query(params).href(); console.log("redirecting to", redirectUrl); // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed @@ -663,7 +721,6 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea console.log("Ignoring unknown (X-)Taler-Operation:", operation); } - // Rate limit cache for executePayment operations, to break redirect loops let rateLimitCache: { [n: number]: number } = {}; @@ -671,30 +728,26 @@ function clearRateLimitCache() { rateLimitCache = {}; } - /** * Currently active wallet instance. Might be unloaded and * re-instantiated when the database is reset. */ -let currentWallet: Wallet|undefined; +let currentWallet: Wallet | undefined; /** * Last version if an outdated DB, if applicable. */ -let oldDbVersion: number|undefined; +let oldDbVersion: number | undefined; function handleUpgradeUnsupported(oldDbVersion: number, newDbVersion: number) { console.log("DB migration not supported"); chrome.tabs.create({ - url: chrome.extension.getURL( - "/src/webex/pages/reset-required.html", - ), + url: chrome.extension.getURL("/src/webex/pages/reset-required.html"), }); setBadgeText({ text: "err" }); chrome.browserAction.setBadgeBackgroundColor({ color: "#F00" }); } - async function reinitWallet() { if (currentWallet) { currentWallet.stop(); @@ -712,31 +765,43 @@ async function reinitWallet() { const http = new BrowserHttpLib(); const notifier = new ChromeNotifier(); console.log("setting wallet"); - const wallet = new Wallet(db, http, badge, notifier, new BrowserCryptoWorkerFactory()); + const wallet = new Wallet( + db, + http, + badge, + notifier, + new BrowserCryptoWorkerFactory(), + ); // Useful for debugging in the background page. (window as any).talerWallet = wallet; currentWallet = wallet; } - /** * Inject a script into a tab. Gracefully logs errors * and works around a bug where the tab's URL does not match the internal URL, * making the injection fail in a confusing way. */ -function injectScript(tabId: number, details: chrome.tabs.InjectDetails, actualUrl: string): void { - chrome.tabs.executeScript(tabId, details, () => { +function injectScript( + tabId: number, + details: chrome.tabs.InjectDetails, + actualUrl: string, +): void { + chrome.tabs.executeScript(tabId, details, () => { // Required to squelch chrome's "unchecked lastError" warning. // Sometimes chrome reports the URL of a tab as http/https but // injection fails. This can happen when a page is unloaded or // shows a "no internet" page etc. if (chrome.runtime.lastError) { - console.warn("injection failed on page", actualUrl, chrome.runtime.lastError.message); + console.warn( + "injection failed on page", + actualUrl, + chrome.runtime.lastError.message, + ); } }); } - /** * Main function to run for the WebExtension backend. * @@ -745,16 +810,23 @@ function injectScript(tabId: number, details: chrome.tabs.InjectDetails, actualU export async function wxMain() { // Explicitly unload the extension page as soon as an update is available, // so the update gets installed as soon as possible. - chrome.runtime.onUpdateAvailable.addListener((details) => { + chrome.runtime.onUpdateAvailable.addListener(details => { console.log("update available:", details); chrome.runtime.reload(); }); window.onerror = (m, source, lineno, colno, error) => { - logging.record("error", "".concat(m as any, error as any), undefined, source || "(unknown)", lineno || 0, colno || 0); + logging.record( + "error", + "".concat(m as any, error as any), + undefined, + source || "(unknown)", + lineno || 0, + colno || 0, + ); }; - chrome.tabs.query({}, (tabs) => { + chrome.tabs.query({}, tabs => { console.log("got tabs", tabs); for (const tab of tabs) { if (!tab.url || !tab.id) { @@ -764,8 +836,19 @@ export async function wxMain() { if (uri.protocol() !== "http" && uri.protocol() !== "https") { continue; } - console.log("injecting into existing tab", tab.id, "with url", uri.href(), "protocol", uri.protocol()); - injectScript(tab.id, { file: "/dist/contentScript-bundle.js", runAt: "document_start" }, uri.href()); + console.log( + "injecting into existing tab", + tab.id, + "with url", + uri.href(), + "protocol", + uri.protocol(), + ); + injectScript( + tab.id, + { file: "/dist/contentScript-bundle.js", runAt: "document_start" }, + uri.href(), + ); const code = ` if (("taler" in window) || document.documentElement.getAttribute("data-taler-nojs")) { document.dispatchEvent(new Event("taler-probe-result")); @@ -775,7 +858,7 @@ export async function wxMain() { } }); - const tabTimers: {[n: number]: number[]} = {}; + const tabTimers: { [n: number]: number[] } = {}; chrome.tabs.onRemoved.addListener((tabId, changeInfo) => { const tt = tabTimers[tabId] || []; @@ -796,7 +879,7 @@ export async function wxMain() { const run = () => { timers.shift(); - chrome.tabs.get(tabId, (tab) => { + chrome.tabs.get(tabId, tab => { if (chrome.runtime.lastError) { return; } @@ -838,38 +921,45 @@ export async function wxMain() { return true; }); - // Clear notifications both when the popop opens, // as well when it closes. - chrome.runtime.onConnect.addListener((port) => { + chrome.runtime.onConnect.addListener(port => { if (port.name === "popup") { if (currentWallet) { currentWallet.clearNotification(); } port.onDisconnect.addListener(() => { - if (currentWallet) { - currentWallet.clearNotification(); - } + if (currentWallet) { + currentWallet.clearNotification(); + } }); } }); - // Handlers for catching HTTP requests - chrome.webRequest.onHeadersReceived.addListener((details) => { - const wallet = currentWallet; - if (!wallet) { - console.warn("wallet not available while handling header"); - } - if (details.statusCode === 402) { - console.log(`got 402 from ${details.url}`); - return handleHttpPayment(details.responseHeaders || [], - details.url, - details.tabId); - } else if (details.statusCode === 202) { - return handleBankRequest(wallet!, details.responseHeaders || [], - details.url, - details.tabId); - } - }, { urls: [""] }, ["responseHeaders", "blocking"]); + chrome.webRequest.onHeadersReceived.addListener( + details => { + const wallet = currentWallet; + if (!wallet) { + console.warn("wallet not available while handling header"); + } + if (details.statusCode === 402) { + console.log(`got 402 from ${details.url}`); + return handleHttpPayment( + details.responseHeaders || [], + details.url, + details.tabId, + ); + } else if (details.statusCode === 202) { + return handleBankRequest( + wallet!, + details.responseHeaders || [], + details.url, + details.tabId, + ); + } + }, + { urls: [""] }, + ["responseHeaders", "blocking"], + ); }