From ee1fc03ae82f2f4662041af3d4243113e92ffeaf Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 6 Dec 2019 12:47:28 +0100 Subject: case-insensitive URIs --- src/dbTypes.ts | 16 +++--- src/headless/taler-wallet-cli.ts | 53 +++++++++++------- src/util/taleruri-test.ts | 55 ++++++------------- src/util/taleruri.ts | 81 ++++++++++++++++++++++------ src/wallet-impl/pay.ts | 23 ++++---- src/wallet-impl/pending.ts | 3 +- src/walletTypes.ts | 1 + src/webex/wxBackend.ts | 114 ++++++++++++++++++++------------------- 8 files changed, 200 insertions(+), 146 deletions(-) (limited to 'src') diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 3625740e2..553040614 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -699,11 +699,11 @@ export class ProposalDownload { */ @Checkable.Class() export class ProposalRecord { - /** - * URL where the proposal was downloaded. - */ @Checkable.String() - url: string; + orderId: string; + + @Checkable.String() + merchantBaseUrl: string; /** * Downloaded data from the merchant. @@ -970,7 +970,6 @@ export interface WireFee { sig: string; } - /** * Record that stores status information about one purchase, starting from when * the customer accepts a proposal. Includes refund status if applicable. @@ -1058,7 +1057,7 @@ export interface PurchaseRecord { */ lastRefundStatusError: OperationError | undefined; - /** + /** * Retry information for querying the refund status with the merchant. */ refundApplyRetryInfo: RetryInfo; @@ -1242,7 +1241,10 @@ export namespace Stores { constructor() { super("proposals", { keyPath: "proposalId" }); } - urlIndex = new Index(this, "urlIndex", "url"); + urlAndOrderIdIndex = new Index(this, "urlIndex", [ + "merchantBaseUrl", + "orderId", + ]); } class PurchasesStore extends Store { diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 2073c2573..4cec984d5 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -28,6 +28,7 @@ import * as Amounts from "../util/amounts"; import { decodeCrock } from "../crypto/talerCrypto"; import { OperationFailedAndReportedError } from "../wallet-impl/errors"; import { Bank } from "./bank"; +import { classifyTalerUri, TalerUriType } from "../util/taleruri"; const logger = new Logger("taler-wallet-cli.ts"); @@ -212,25 +213,39 @@ walletCli .action(async args => { await withWallet(args, async wallet => { const uri: string = args.handleUri.uri; - if (uri.startsWith("taler://pay/")) { - await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); - } else if (uri.startsWith("taler://tip/")) { - const res = await wallet.getTipStatus(uri); - console.log("tip status", res); - await wallet.acceptTip(res.tipId); - } else if (uri.startsWith("taler://refund/")) { - await wallet.applyRefund(uri); - } else if (uri.startsWith("taler://withdraw/")) { - const withdrawInfo = await wallet.getWithdrawalInfo(uri); - const selectedExchange = withdrawInfo.suggestedExchange; - if (!selectedExchange) { - console.error("no suggested exchange!"); - process.exit(1); - return; - } - const res = await wallet.acceptWithdrawal(uri, selectedExchange); - await wallet.processReserve(res.reservePub); + const uriType = classifyTalerUri(uri); + switch (uriType) { + case TalerUriType.TalerPay: + await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); + break; + case TalerUriType.TalerTip: + { + const res = await wallet.getTipStatus(uri); + console.log("tip status", res); + await wallet.acceptTip(res.tipId); + } + break; + case TalerUriType.TalerRefund: + await wallet.applyRefund(uri); + break; + case TalerUriType.TalerWithdraw: + { + const withdrawInfo = await wallet.getWithdrawalInfo(uri); + const selectedExchange = withdrawInfo.suggestedExchange; + if (!selectedExchange) { + console.error("no suggested exchange!"); + process.exit(1); + return; + } + const res = await wallet.acceptWithdrawal(uri, selectedExchange); + await wallet.processReserve(res.reservePub); + } + break; + default: + console.log(`URI type (${uriType}) not handled`); + break; } + return; }); }); @@ -445,7 +460,7 @@ testCli .requiredOption("bank", ["-b", "--bank"], clk.STRING, { default: "https://bank.test.taler.net/", }) - .action(async (args) => { + .action(async args => { const b = new Bank(args.genWithdrawUri.bank); const user = await b.registerRandomUser(); const url = await b.generateWithdrawUri(user, args.genWithdrawUri.amount); diff --git a/src/util/taleruri-test.ts b/src/util/taleruri-test.ts index c687a6717..de4a90697 100644 --- a/src/util/taleruri-test.ts +++ b/src/util/taleruri-test.ts @@ -22,23 +22,6 @@ import { parseTipUri, } from "./taleruri"; -test("taler pay url parsing: http(s)", t => { - const url1 = "https://example.com/bar?spam=eggs"; - const r1 = parsePayUri(url1); - if (!r1) { - t.fail(); - return; - } - t.is(r1.downloadUrl, url1); - t.is(r1.sessionId, undefined); - const url2 = "http://example.com/bar?spam=eggs"; - const r2 = parsePayUri(url2); - if (!r2) { - t.fail(); - return; - } -}); - test("taler pay url parsing: wrong scheme", t => { const url1 = "talerfoo://"; const r1 = parsePayUri(url1); @@ -56,7 +39,7 @@ test("taler pay url parsing: defaults", t => { t.fail(); return; } - t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder"); + t.is(r1.merchantBaseUrl, "https://example.com/public/"); t.is(r1.sessionId, undefined); const url2 = "taler://pay/example.com/-/-/myorder/mysession"; @@ -65,7 +48,7 @@ test("taler pay url parsing: defaults", t => { t.fail(); return; } - t.is(r2.downloadUrl, "https://example.com/public/proposal?order_id=myorder"); + t.is(r2.merchantBaseUrl, "https://example.com/public/"); t.is(r2.sessionId, "mysession"); }); @@ -76,7 +59,7 @@ test("taler pay url parsing: trailing parts", t => { t.fail(); return; } - t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder"); + t.is(r1.merchantBaseUrl, "https://example.com/public/"); t.is(r1.sessionId, "mysession"); }); @@ -87,10 +70,8 @@ test("taler pay url parsing: instance", t => { t.fail(); return; } - t.is( - r1.downloadUrl, - "https://example.com/public/instances/myinst/proposal?order_id=myorder", - ); + t.is(r1.merchantBaseUrl, "https://example.com/public/instances/myinst/"); + t.is(r1.orderId, "myorder"); }); test("taler pay url parsing: path prefix and instance", t => { @@ -100,10 +81,7 @@ test("taler pay url parsing: path prefix and instance", t => { t.fail(); return; } - t.is( - r1.downloadUrl, - "https://example.com/mypfx/instances/myinst/proposal?order_id=myorder", - ); + t.is(r1.merchantBaseUrl, "https://example.com/mypfx/instances/myinst/"); }); test("taler pay url parsing: complex path prefix", t => { @@ -113,10 +91,9 @@ test("taler pay url parsing: complex path prefix", t => { t.fail(); return; } - t.is( - r1.downloadUrl, - "https://example.com/mypfx/public/proposal?order_id=myorder", - ); + t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/"); + t.is(r1.orderId, "myorder"); + t.is(r1.sessionId, undefined); }); test("taler pay url parsing: complex path prefix and instance", t => { @@ -126,10 +103,8 @@ test("taler pay url parsing: complex path prefix and instance", t => { t.fail(); return; } - t.is( - r1.downloadUrl, - "https://example.com/mypfx/public/instances/foo/proposal?order_id=myorder", - ); + t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/instances/foo/"); + t.is(r1.orderId, "myorder"); }); test("taler pay url parsing: non-https #1", t => { @@ -138,8 +113,9 @@ test("taler pay url parsing: non-https #1", t => { if (!r1) { t.fail(); return; - } - t.is(r1.downloadUrl, "http://example.com/public/proposal?order_id=myorder"); + } + t.is(r1.merchantBaseUrl, "http://example.com/public/"); + t.is(r1.orderId, "myorder") }); test("taler pay url parsing: non-https #2", t => { @@ -149,7 +125,8 @@ test("taler pay url parsing: non-https #2", t => { t.fail(); return; } - t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder"); + t.is(r1.merchantBaseUrl, "https://example.com/public/"); + t.is(r1.orderId, "myorder"); }); test("taler withdraw uri parsing", t => { diff --git a/src/util/taleruri.ts b/src/util/taleruri.ts index 50886a916..f34b82a59 100644 --- a/src/util/taleruri.ts +++ b/src/util/taleruri.ts @@ -15,7 +15,8 @@ */ export interface PayUriResult { - downloadUrl: string; + merchantBaseUrl: string; + orderId: string; sessionId?: string; } @@ -36,7 +37,7 @@ export interface TipUriResult { export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { const pfx = "taler://withdraw/"; - if (!s.startsWith(pfx)) { + if (!s.toLowerCase().startsWith(pfx)) { return undefined; } @@ -44,6 +45,20 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { let [host, path, withdrawId] = rest.split("/"); + if (!host) { + return undefined; + } + + host = host.toLowerCase(); + + if (!path) { + return undefined; + } + + if (!withdrawId) { + return undefined; + } + if (path === "-") { path = "api/withdraw-operation"; } @@ -53,15 +68,45 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { }; } -export function parsePayUri(s: string): PayUriResult | undefined { - if (s.startsWith("https://") || s.startsWith("http://")) { - return { - downloadUrl: s, - sessionId: undefined, - }; +export const enum TalerUriType { + TalerPay = "taler-pay", + TalerWithdraw = "taler-withdraw", + TalerTip = "taler-tip", + TalerRefund = "taler-refund", + TalerNotifyReserve = "taler-notify-reserve", + Unknown = "unknown", +} + +export function classifyTalerUri(s: string): TalerUriType { + const sl = s.toLowerCase(); + if (sl.startsWith("taler://pay/")) { + return TalerUriType.TalerPay; } + if (sl.startsWith("taler://tip/")) { + return TalerUriType.TalerTip; + } + if (sl.startsWith("taler://refund/")) { + return TalerUriType.TalerRefund; + } + if (sl.startsWith("taler://withdraw/")) { + return TalerUriType.TalerWithdraw; + } + if (sl.startsWith("taler://notify-reserve/")) { + return TalerUriType.TalerWithdraw; + } + return TalerUriType.Unknown; + +} + +export function getOrderDownloadUrl(merchantBaseUrl: string, orderId: string) { + const u = new URL("proposal", merchantBaseUrl); + u.searchParams.set("order_id", orderId); + return u.href +} + +export function parsePayUri(s: string): PayUriResult | undefined { const pfx = "taler://pay/"; - if (!s.startsWith(pfx)) { + if (!s.toLowerCase().startsWith(pfx)) { return undefined; } @@ -75,6 +120,8 @@ export function parsePayUri(s: string): PayUriResult | undefined { return undefined; } + host = host.toLowerCase(); + if (!maybePath) { return undefined; } @@ -99,21 +146,21 @@ export function parsePayUri(s: string): PayUriResult | undefined { protocol = "http"; } - const downloadUrl = + const merchantBaseUrl = `${protocol}://${host}/` + decodeURIComponent(maybePath) + - maybeInstancePath + - `proposal?order_id=${orderId}`; + maybeInstancePath; return { - downloadUrl, + merchantBaseUrl, + orderId, sessionId: maybeSessionid, }; } export function parseTipUri(s: string): TipUriResult | undefined { const pfx = "taler://tip/"; - if (!s.startsWith(pfx)) { + if (!s.toLowerCase().startsWith(pfx)) { return undefined; } @@ -125,6 +172,8 @@ export function parseTipUri(s: string): TipUriResult | undefined { return undefined; } + host = host.toLowerCase(); + if (!maybePath) { return undefined; } @@ -155,7 +204,7 @@ export function parseTipUri(s: string): TipUriResult | undefined { export function parseRefundUri(s: string): RefundUriResult | undefined { const pfx = "taler://refund/"; - if (!s.startsWith(pfx)) { + if (!s.toLowerCase().startsWith(pfx)) { return undefined; } @@ -167,6 +216,8 @@ export function parseRefundUri(s: string): RefundUriResult | undefined { return undefined; } + host = host.toLowerCase(); + if (!maybePath) { return undefined; } diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts index 4933098cd..7076f905d 100644 --- a/src/wallet-impl/pay.ts +++ b/src/wallet-impl/pay.ts @@ -65,7 +65,7 @@ import { } from "../util/helpers"; import { Logger } from "../util/logging"; import { InternalWalletState } from "./state"; -import { parsePayUri, parseRefundUri } from "../util/taleruri"; +import { parsePayUri, parseRefundUri, getOrderDownloadUrl } from "../util/taleruri"; import { getTotalRefreshCost, refresh } from "./refresh"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { guardOperationException } from "./errors"; @@ -557,9 +557,10 @@ async function processDownloadProposalImpl( if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) { return; } - const parsed_url = new URL(proposal.url); - parsed_url.searchParams.set("nonce", proposal.noncePub); - const urlWithNonce = parsed_url.href; + + const parsedUrl = new URL(getOrderDownloadUrl(proposal.merchantBaseUrl, proposal.orderId)); + parsedUrl.searchParams.set("nonce", proposal.noncePub); + const urlWithNonce = parsedUrl.href; console.log("downloading contract from '" + urlWithNonce + "'"); let resp; try { @@ -629,13 +630,14 @@ async function processDownloadProposalImpl( */ async function startDownloadProposal( ws: InternalWalletState, - url: string, + merchantBaseUrl: string, + orderId: string, sessionId?: string, ): Promise { const oldProposal = await oneShotGetIndexed( ws.db, - Stores.proposals.urlIndex, - url, + Stores.proposals.urlAndOrderIdIndex, + [merchantBaseUrl, orderId], ); if (oldProposal) { await processDownloadProposal(ws, oldProposal.proposalId); @@ -650,8 +652,8 @@ async function startDownloadProposal( noncePriv: priv, noncePub: pub, timestamp: getTimestampNow(), - url, - downloadSessionId: sessionId, + merchantBaseUrl, + orderId, proposalId: proposalId, proposalStatus: ProposalStatus.DOWNLOADING, repurchaseProposalId: undefined, @@ -763,7 +765,8 @@ export async function preparePay( let proposalId = await startDownloadProposal( ws, - uriResult.downloadUrl, + uriResult.merchantBaseUrl, + uriResult.orderId, uriResult.sessionId, ); diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts index 02f8d9ef9..5fb9ef5ce 100644 --- a/src/wallet-impl/pending.ts +++ b/src/wallet-impl/pending.ts @@ -312,7 +312,8 @@ async function gatherProposalPending( resp.pendingOperations.push({ type: "proposal-download", givesLifeness: true, - merchantBaseUrl: proposal.download?.contractTerms.merchant_base_url || "", + merchantBaseUrl: proposal.merchantBaseUrl, + orderId: proposal.orderId, proposalId: proposal.proposalId, proposalTimestamp: proposal.timestamp, lastError: proposal.lastError, diff --git a/src/walletTypes.ts b/src/walletTypes.ts index 407871666..e2be26b03 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -741,6 +741,7 @@ export interface PendingProposalDownloadOperation { merchantBaseUrl: string; proposalTimestamp: Timestamp; proposalId: string; + orderId: string; lastError?: OperationError; retryInfo: RetryInfo; } diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 4363890eb..27141247e 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -42,6 +42,7 @@ import Port = chrome.runtime.Port; import MessageSender = chrome.runtime.MessageSender; import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { OpenedPromise, openPromise } from "../util/promiseUtils"; +import { classifyTalerUri, TalerUriType } from "../util/taleruri"; const NeedsWallet = Symbol("NeedsWallet"); @@ -257,7 +258,11 @@ async function handleMessage( await walletInit.promise; } catch (e) { errors.push("Error during wallet initialization: " + e); - if (currentDatabase === undefined && outdatedDbVersion === undefined && isFirefox()) { + if ( + currentDatabase === undefined && + outdatedDbVersion === undefined && + isFirefox() + ) { firefoxIdbProblem = true; } } @@ -435,7 +440,7 @@ async function reinitWallet() { http, new BrowserCryptoWorkerFactory(), ); - wallet.runRetryLoop().catch((e) => { + wallet.runRetryLoop().catch(e => { console.log("error during wallet retry loop", e); }); // Useful for debugging in the background page. @@ -601,61 +606,60 @@ export async function wxMain() { for (let header of details.responseHeaders || []) { if (header.name.toLowerCase() === "taler") { const talerUri = header.value || ""; - if (!talerUri.startsWith("taler://")) { - console.warn( - "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", - ); - break; - } - if (talerUri.startsWith("taler://withdraw/")) { - return makeSyncWalletRedirect( - "withdraw.html", - details.tabId, - details.url, - { - talerWithdrawUri: talerUri, - }, - ); - } else if (talerUri.startsWith("taler://pay/")) { - return makeSyncWalletRedirect( - "pay.html", - details.tabId, - details.url, - { - talerPayUri: talerUri, - }, - ); - } else if (talerUri.startsWith("taler://tip/")) { - return makeSyncWalletRedirect( - "tip.html", - details.tabId, - details.url, - { - talerTipUri: talerUri, - }, - ); - } else if (talerUri.startsWith("taler://refund/")) { - return makeSyncWalletRedirect( - "refund.html", - details.tabId, - details.url, - { - talerRefundUri: talerUri, - }, - ); - } else if (talerUri.startsWith("taler://notify-reserve/")) { - Promise.resolve().then(() => { - const w = currentWallet; - if (!w) { - return; - } - w.handleNotifyReserve(); - }); + const uriType = classifyTalerUri(talerUri); + switch (uriType) { + case TalerUriType.TalerWithdraw: + return makeSyncWalletRedirect( + "withdraw.html", + details.tabId, + details.url, + { + talerWithdrawUri: talerUri, + }, + ); + case TalerUriType.TalerPay: + return makeSyncWalletRedirect( + "pay.html", + details.tabId, + details.url, + { + talerPayUri: talerUri, + }, + ); + case TalerUriType.TalerTip: + return makeSyncWalletRedirect( + "tip.html", + details.tabId, + details.url, + { + talerTipUri: talerUri, + }, + ); + case TalerUriType.TalerRefund: + return makeSyncWalletRedirect( + "refund.html", + details.tabId, + details.url, + { + talerRefundUri: talerUri, + }, + ); + case TalerUriType.TalerNotifyReserve: + Promise.resolve().then(() => { + const w = currentWallet; + if (!w) { + return; + } + w.handleNotifyReserve(); + }); + break; - } else { - console.warn("Unknown action in taler:// URI, ignoring."); + default: + console.warn( + "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + ); + break; } - break; } } } -- cgit v1.2.3