case-insensitive URIs

This commit is contained in:
Florian Dold 2019-12-06 12:47:28 +01:00
parent b4d36fca18
commit ee1fc03ae8
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 200 additions and 146 deletions

View File

@ -699,11 +699,11 @@ export class ProposalDownload {
*/ */
@Checkable.Class() @Checkable.Class()
export class ProposalRecord { export class ProposalRecord {
/**
* URL where the proposal was downloaded.
*/
@Checkable.String() @Checkable.String()
url: string; orderId: string;
@Checkable.String()
merchantBaseUrl: string;
/** /**
* Downloaded data from the merchant. * Downloaded data from the merchant.
@ -970,7 +970,6 @@ export interface WireFee {
sig: string; sig: string;
} }
/** /**
* Record that stores status information about one purchase, starting from when * Record that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable. * the customer accepts a proposal. Includes refund status if applicable.
@ -1242,7 +1241,10 @@ export namespace Stores {
constructor() { constructor() {
super("proposals", { keyPath: "proposalId" }); super("proposals", { keyPath: "proposalId" });
} }
urlIndex = new Index<string, ProposalRecord>(this, "urlIndex", "url"); urlAndOrderIdIndex = new Index<string, ProposalRecord>(this, "urlIndex", [
"merchantBaseUrl",
"orderId",
]);
} }
class PurchasesStore extends Store<PurchaseRecord> { class PurchasesStore extends Store<PurchaseRecord> {

View File

@ -28,6 +28,7 @@ import * as Amounts from "../util/amounts";
import { decodeCrock } from "../crypto/talerCrypto"; import { decodeCrock } from "../crypto/talerCrypto";
import { OperationFailedAndReportedError } from "../wallet-impl/errors"; import { OperationFailedAndReportedError } from "../wallet-impl/errors";
import { Bank } from "./bank"; import { Bank } from "./bank";
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
const logger = new Logger("taler-wallet-cli.ts"); const logger = new Logger("taler-wallet-cli.ts");
@ -212,15 +213,23 @@ walletCli
.action(async args => { .action(async args => {
await withWallet(args, async wallet => { await withWallet(args, async wallet => {
const uri: string = args.handleUri.uri; const uri: string = args.handleUri.uri;
if (uri.startsWith("taler://pay/")) { const uriType = classifyTalerUri(uri);
switch (uriType) {
case TalerUriType.TalerPay:
await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
} else if (uri.startsWith("taler://tip/")) { break;
case TalerUriType.TalerTip:
{
const res = await wallet.getTipStatus(uri); const res = await wallet.getTipStatus(uri);
console.log("tip status", res); console.log("tip status", res);
await wallet.acceptTip(res.tipId); await wallet.acceptTip(res.tipId);
} else if (uri.startsWith("taler://refund/")) { }
break;
case TalerUriType.TalerRefund:
await wallet.applyRefund(uri); await wallet.applyRefund(uri);
} else if (uri.startsWith("taler://withdraw/")) { break;
case TalerUriType.TalerWithdraw:
{
const withdrawInfo = await wallet.getWithdrawalInfo(uri); const withdrawInfo = await wallet.getWithdrawalInfo(uri);
const selectedExchange = withdrawInfo.suggestedExchange; const selectedExchange = withdrawInfo.suggestedExchange;
if (!selectedExchange) { if (!selectedExchange) {
@ -231,6 +240,12 @@ walletCli
const res = await wallet.acceptWithdrawal(uri, selectedExchange); const res = await wallet.acceptWithdrawal(uri, selectedExchange);
await wallet.processReserve(res.reservePub); 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, { .requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/", default: "https://bank.test.taler.net/",
}) })
.action(async (args) => { .action(async args => {
const b = new Bank(args.genWithdrawUri.bank); const b = new Bank(args.genWithdrawUri.bank);
const user = await b.registerRandomUser(); const user = await b.registerRandomUser();
const url = await b.generateWithdrawUri(user, args.genWithdrawUri.amount); const url = await b.generateWithdrawUri(user, args.genWithdrawUri.amount);

View File

@ -22,23 +22,6 @@ import {
parseTipUri, parseTipUri,
} from "./taleruri"; } 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 => { test("taler pay url parsing: wrong scheme", t => {
const url1 = "talerfoo://"; const url1 = "talerfoo://";
const r1 = parsePayUri(url1); const r1 = parsePayUri(url1);
@ -56,7 +39,7 @@ test("taler pay url parsing: defaults", t => {
t.fail(); t.fail();
return; 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); t.is(r1.sessionId, undefined);
const url2 = "taler://pay/example.com/-/-/myorder/mysession"; const url2 = "taler://pay/example.com/-/-/myorder/mysession";
@ -65,7 +48,7 @@ test("taler pay url parsing: defaults", t => {
t.fail(); t.fail();
return; 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"); t.is(r2.sessionId, "mysession");
}); });
@ -76,7 +59,7 @@ test("taler pay url parsing: trailing parts", t => {
t.fail(); t.fail();
return; 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"); t.is(r1.sessionId, "mysession");
}); });
@ -87,10 +70,8 @@ test("taler pay url parsing: instance", t => {
t.fail(); t.fail();
return; return;
} }
t.is( t.is(r1.merchantBaseUrl, "https://example.com/public/instances/myinst/");
r1.downloadUrl, t.is(r1.orderId, "myorder");
"https://example.com/public/instances/myinst/proposal?order_id=myorder",
);
}); });
test("taler pay url parsing: path prefix and instance", t => { 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(); t.fail();
return; return;
} }
t.is( t.is(r1.merchantBaseUrl, "https://example.com/mypfx/instances/myinst/");
r1.downloadUrl,
"https://example.com/mypfx/instances/myinst/proposal?order_id=myorder",
);
}); });
test("taler pay url parsing: complex path prefix", t => { test("taler pay url parsing: complex path prefix", t => {
@ -113,10 +91,9 @@ test("taler pay url parsing: complex path prefix", t => {
t.fail(); t.fail();
return; return;
} }
t.is( t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/");
r1.downloadUrl, t.is(r1.orderId, "myorder");
"https://example.com/mypfx/public/proposal?order_id=myorder", t.is(r1.sessionId, undefined);
);
}); });
test("taler pay url parsing: complex path prefix and instance", t => { 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(); t.fail();
return; return;
} }
t.is( t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/instances/foo/");
r1.downloadUrl, t.is(r1.orderId, "myorder");
"https://example.com/mypfx/public/instances/foo/proposal?order_id=myorder",
);
}); });
test("taler pay url parsing: non-https #1", t => { test("taler pay url parsing: non-https #1", t => {
@ -139,7 +114,8 @@ test("taler pay url parsing: non-https #1", t => {
t.fail(); t.fail();
return; 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 => { test("taler pay url parsing: non-https #2", t => {
@ -149,7 +125,8 @@ test("taler pay url parsing: non-https #2", t => {
t.fail(); t.fail();
return; 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 => { test("taler withdraw uri parsing", t => {

View File

@ -15,7 +15,8 @@
*/ */
export interface PayUriResult { export interface PayUriResult {
downloadUrl: string; merchantBaseUrl: string;
orderId: string;
sessionId?: string; sessionId?: string;
} }
@ -36,7 +37,7 @@ export interface TipUriResult {
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const pfx = "taler://withdraw/"; const pfx = "taler://withdraw/";
if (!s.startsWith(pfx)) { if (!s.toLowerCase().startsWith(pfx)) {
return undefined; return undefined;
} }
@ -44,6 +45,20 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
let [host, path, withdrawId] = rest.split("/"); let [host, path, withdrawId] = rest.split("/");
if (!host) {
return undefined;
}
host = host.toLowerCase();
if (!path) {
return undefined;
}
if (!withdrawId) {
return undefined;
}
if (path === "-") { if (path === "-") {
path = "api/withdraw-operation"; path = "api/withdraw-operation";
} }
@ -53,15 +68,45 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
}; };
} }
export function parsePayUri(s: string): PayUriResult | undefined { export const enum TalerUriType {
if (s.startsWith("https://") || s.startsWith("http://")) { TalerPay = "taler-pay",
return { TalerWithdraw = "taler-withdraw",
downloadUrl: s, TalerTip = "taler-tip",
sessionId: undefined, 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/"; const pfx = "taler://pay/";
if (!s.startsWith(pfx)) { if (!s.toLowerCase().startsWith(pfx)) {
return undefined; return undefined;
} }
@ -75,6 +120,8 @@ export function parsePayUri(s: string): PayUriResult | undefined {
return undefined; return undefined;
} }
host = host.toLowerCase();
if (!maybePath) { if (!maybePath) {
return undefined; return undefined;
} }
@ -99,21 +146,21 @@ export function parsePayUri(s: string): PayUriResult | undefined {
protocol = "http"; protocol = "http";
} }
const downloadUrl = const merchantBaseUrl =
`${protocol}://${host}/` + `${protocol}://${host}/` +
decodeURIComponent(maybePath) + decodeURIComponent(maybePath) +
maybeInstancePath + maybeInstancePath;
`proposal?order_id=${orderId}`;
return { return {
downloadUrl, merchantBaseUrl,
orderId,
sessionId: maybeSessionid, sessionId: maybeSessionid,
}; };
} }
export function parseTipUri(s: string): TipUriResult | undefined { export function parseTipUri(s: string): TipUriResult | undefined {
const pfx = "taler://tip/"; const pfx = "taler://tip/";
if (!s.startsWith(pfx)) { if (!s.toLowerCase().startsWith(pfx)) {
return undefined; return undefined;
} }
@ -125,6 +172,8 @@ export function parseTipUri(s: string): TipUriResult | undefined {
return undefined; return undefined;
} }
host = host.toLowerCase();
if (!maybePath) { if (!maybePath) {
return undefined; return undefined;
} }
@ -155,7 +204,7 @@ export function parseTipUri(s: string): TipUriResult | undefined {
export function parseRefundUri(s: string): RefundUriResult | undefined { export function parseRefundUri(s: string): RefundUriResult | undefined {
const pfx = "taler://refund/"; const pfx = "taler://refund/";
if (!s.startsWith(pfx)) { if (!s.toLowerCase().startsWith(pfx)) {
return undefined; return undefined;
} }
@ -167,6 +216,8 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
return undefined; return undefined;
} }
host = host.toLowerCase();
if (!maybePath) { if (!maybePath) {
return undefined; return undefined;
} }

View File

@ -65,7 +65,7 @@ import {
} from "../util/helpers"; } from "../util/helpers";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { parsePayUri, parseRefundUri } from "../util/taleruri"; import { parsePayUri, parseRefundUri, getOrderDownloadUrl } from "../util/taleruri";
import { getTotalRefreshCost, refresh } from "./refresh"; import { getTotalRefreshCost, refresh } from "./refresh";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
@ -557,9 +557,10 @@ async function processDownloadProposalImpl(
if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) { if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
return; return;
} }
const parsed_url = new URL(proposal.url);
parsed_url.searchParams.set("nonce", proposal.noncePub); const parsedUrl = new URL(getOrderDownloadUrl(proposal.merchantBaseUrl, proposal.orderId));
const urlWithNonce = parsed_url.href; parsedUrl.searchParams.set("nonce", proposal.noncePub);
const urlWithNonce = parsedUrl.href;
console.log("downloading contract from '" + urlWithNonce + "'"); console.log("downloading contract from '" + urlWithNonce + "'");
let resp; let resp;
try { try {
@ -629,13 +630,14 @@ async function processDownloadProposalImpl(
*/ */
async function startDownloadProposal( async function startDownloadProposal(
ws: InternalWalletState, ws: InternalWalletState,
url: string, merchantBaseUrl: string,
orderId: string,
sessionId?: string, sessionId?: string,
): Promise<string> { ): Promise<string> {
const oldProposal = await oneShotGetIndexed( const oldProposal = await oneShotGetIndexed(
ws.db, ws.db,
Stores.proposals.urlIndex, Stores.proposals.urlAndOrderIdIndex,
url, [merchantBaseUrl, orderId],
); );
if (oldProposal) { if (oldProposal) {
await processDownloadProposal(ws, oldProposal.proposalId); await processDownloadProposal(ws, oldProposal.proposalId);
@ -650,8 +652,8 @@ async function startDownloadProposal(
noncePriv: priv, noncePriv: priv,
noncePub: pub, noncePub: pub,
timestamp: getTimestampNow(), timestamp: getTimestampNow(),
url, merchantBaseUrl,
downloadSessionId: sessionId, orderId,
proposalId: proposalId, proposalId: proposalId,
proposalStatus: ProposalStatus.DOWNLOADING, proposalStatus: ProposalStatus.DOWNLOADING,
repurchaseProposalId: undefined, repurchaseProposalId: undefined,
@ -763,7 +765,8 @@ export async function preparePay(
let proposalId = await startDownloadProposal( let proposalId = await startDownloadProposal(
ws, ws,
uriResult.downloadUrl, uriResult.merchantBaseUrl,
uriResult.orderId,
uriResult.sessionId, uriResult.sessionId,
); );

View File

@ -312,7 +312,8 @@ async function gatherProposalPending(
resp.pendingOperations.push({ resp.pendingOperations.push({
type: "proposal-download", type: "proposal-download",
givesLifeness: true, givesLifeness: true,
merchantBaseUrl: proposal.download?.contractTerms.merchant_base_url || "", merchantBaseUrl: proposal.merchantBaseUrl,
orderId: proposal.orderId,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp, proposalTimestamp: proposal.timestamp,
lastError: proposal.lastError, lastError: proposal.lastError,

View File

@ -741,6 +741,7 @@ export interface PendingProposalDownloadOperation {
merchantBaseUrl: string; merchantBaseUrl: string;
proposalTimestamp: Timestamp; proposalTimestamp: Timestamp;
proposalId: string; proposalId: string;
orderId: string;
lastError?: OperationError; lastError?: OperationError;
retryInfo: RetryInfo; retryInfo: RetryInfo;
} }

View File

@ -42,6 +42,7 @@ import Port = chrome.runtime.Port;
import MessageSender = chrome.runtime.MessageSender; import MessageSender = chrome.runtime.MessageSender;
import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
import { OpenedPromise, openPromise } from "../util/promiseUtils"; import { OpenedPromise, openPromise } from "../util/promiseUtils";
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
const NeedsWallet = Symbol("NeedsWallet"); const NeedsWallet = Symbol("NeedsWallet");
@ -257,7 +258,11 @@ async function handleMessage(
await walletInit.promise; await walletInit.promise;
} catch (e) { } catch (e) {
errors.push("Error during wallet initialization: " + e); errors.push("Error during wallet initialization: " + e);
if (currentDatabase === undefined && outdatedDbVersion === undefined && isFirefox()) { if (
currentDatabase === undefined &&
outdatedDbVersion === undefined &&
isFirefox()
) {
firefoxIdbProblem = true; firefoxIdbProblem = true;
} }
} }
@ -435,7 +440,7 @@ async function reinitWallet() {
http, http,
new BrowserCryptoWorkerFactory(), new BrowserCryptoWorkerFactory(),
); );
wallet.runRetryLoop().catch((e) => { wallet.runRetryLoop().catch(e => {
console.log("error during wallet retry loop", e); console.log("error during wallet retry loop", e);
}); });
// Useful for debugging in the background page. // Useful for debugging in the background page.
@ -601,13 +606,9 @@ export async function wxMain() {
for (let header of details.responseHeaders || []) { for (let header of details.responseHeaders || []) {
if (header.name.toLowerCase() === "taler") { if (header.name.toLowerCase() === "taler") {
const talerUri = header.value || ""; const talerUri = header.value || "";
if (!talerUri.startsWith("taler://")) { const uriType = classifyTalerUri(talerUri);
console.warn( switch (uriType) {
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", case TalerUriType.TalerWithdraw:
);
break;
}
if (talerUri.startsWith("taler://withdraw/")) {
return makeSyncWalletRedirect( return makeSyncWalletRedirect(
"withdraw.html", "withdraw.html",
details.tabId, details.tabId,
@ -616,7 +617,7 @@ export async function wxMain() {
talerWithdrawUri: talerUri, talerWithdrawUri: talerUri,
}, },
); );
} else if (talerUri.startsWith("taler://pay/")) { case TalerUriType.TalerPay:
return makeSyncWalletRedirect( return makeSyncWalletRedirect(
"pay.html", "pay.html",
details.tabId, details.tabId,
@ -625,7 +626,7 @@ export async function wxMain() {
talerPayUri: talerUri, talerPayUri: talerUri,
}, },
); );
} else if (talerUri.startsWith("taler://tip/")) { case TalerUriType.TalerTip:
return makeSyncWalletRedirect( return makeSyncWalletRedirect(
"tip.html", "tip.html",
details.tabId, details.tabId,
@ -634,7 +635,7 @@ export async function wxMain() {
talerTipUri: talerUri, talerTipUri: talerUri,
}, },
); );
} else if (talerUri.startsWith("taler://refund/")) { case TalerUriType.TalerRefund:
return makeSyncWalletRedirect( return makeSyncWalletRedirect(
"refund.html", "refund.html",
details.tabId, details.tabId,
@ -643,7 +644,7 @@ export async function wxMain() {
talerRefundUri: talerUri, talerRefundUri: talerUri,
}, },
); );
} else if (talerUri.startsWith("taler://notify-reserve/")) { case TalerUriType.TalerNotifyReserve:
Promise.resolve().then(() => { Promise.resolve().then(() => {
const w = currentWallet; const w = currentWallet;
if (!w) { if (!w) {
@ -651,11 +652,14 @@ export async function wxMain() {
} }
w.handleNotifyReserve(); w.handleNotifyReserve();
}); });
} else {
console.warn("Unknown action in taler:// URI, ignoring.");
}
break; break;
default:
console.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
);
break;
}
} }
} }
} }