From ae111663f412ad7bee9029110e3ab1594ec14576 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 27 Jul 2020 17:09:52 +0530 Subject: new taler:// URI syntax --- src/operations/tip.ts | 6 +- src/operations/withdraw.ts | 8 +- src/util/taleruri-test.ts | 101 ++++++--------------- src/util/taleruri.ts | 222 +++++++++++++++++++-------------------------- 4 files changed, 132 insertions(+), 205 deletions(-) diff --git a/src/operations/tip.ts b/src/operations/tip.ts index d121b1cbb..17f7ee90d 100644 --- a/src/operations/tip.ts +++ b/src/operations/tip.ts @@ -66,9 +66,11 @@ export async function getTipStatus( const amount = Amounts.parseOrThrow(tipPickupStatus.amount); + const merchantOrigin = new URL(res.merchantBaseUrl).origin; + let tipRecord = await ws.db.get(Stores.tips, [ res.merchantTipId, - res.merchantOrigin, + merchantOrigin, ]); if (!tipRecord) { @@ -117,7 +119,7 @@ export async function getTipStatus( amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left), exchangeUrl: tipPickupStatus.exchange_url, nextUrl: tipPickupStatus.extra.next_url, - merchantOrigin: res.merchantOrigin, + merchantOrigin: merchantOrigin, merchantTipId: res.merchantTipId, expirationTimestamp: tipPickupStatus.stamp_expire, timestamp: tipPickupStatus.stamp_created, diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index f7879dfec..9f6804b2b 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -141,7 +141,11 @@ export async function getBankWithdrawalInfo( if (!uriResult) { throw Error(`can't parse URL ${talerWithdrawUri}`); } - const resp = await ws.http.get(uriResult.statusUrl); + const reqUrl = new URL( + `api/withdraw-operations/${uriResult.withdrawalOperationId}`, + uriResult.bankIntegrationApiBaseUrl, + ); + const resp = await ws.http.get(reqUrl.href); const status = await readSuccessResponseJsonOrThrow( resp, codecForWithdrawOperationStatusResponse(), @@ -150,7 +154,7 @@ export async function getBankWithdrawalInfo( return { amount: Amounts.parseOrThrow(status.amount), confirmTransferUrl: status.confirm_transfer_url, - extractedStatusUrl: uriResult.statusUrl, + extractedStatusUrl: uriResult.bankIntegrationApiBaseUrl, selectionDone: status.selection_done, senderWire: status.sender_wire, suggestedExchange: status.suggested_exchange, diff --git a/src/util/taleruri-test.ts b/src/util/taleruri-test.ts index 1510880c5..40a30bf7f 100644 --- a/src/util/taleruri-test.ts +++ b/src/util/taleruri-test.ts @@ -33,136 +33,93 @@ test("taler pay url parsing: wrong scheme", (t) => { }); test("taler pay url parsing: defaults", (t) => { - const url1 = "taler://pay/example.com/-/-/myorder"; + const url1 = "taler://pay/example.com/myorder/"; const r1 = parsePayUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "https://example.com/public/"); - t.is(r1.sessionId, undefined); + t.is(r1.merchantBaseUrl, "https://example.com/"); + t.is(r1.sessionId, ""); - const url2 = "taler://pay/example.com/-/-/myorder/mysession"; + const url2 = "taler://pay/example.com/myorder/mysession"; const r2 = parsePayUri(url2); if (!r2) { t.fail(); return; } - t.is(r2.merchantBaseUrl, "https://example.com/public/"); + t.is(r2.merchantBaseUrl, "https://example.com/"); t.is(r2.sessionId, "mysession"); }); -test("taler pay url parsing: trailing parts", (t) => { - const url1 = "taler://pay/example.com/-/-/myorder/mysession/spam/eggs"; - const r1 = parsePayUri(url1); - if (!r1) { - t.fail(); - return; - } - t.is(r1.merchantBaseUrl, "https://example.com/public/"); - t.is(r1.sessionId, "mysession"); -}); - test("taler pay url parsing: instance", (t) => { - const url1 = "taler://pay/example.com/-/myinst/myorder"; + const url1 = "taler://pay/example.com/instances/myinst/myorder/"; const r1 = parsePayUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "https://example.com/public/instances/myinst/"); + t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/"); t.is(r1.orderId, "myorder"); }); -test("taler pay url parsing: path prefix and instance", (t) => { - const url1 = "taler://pay/example.com/mypfx/myinst/myorder"; - const r1 = parsePayUri(url1); - if (!r1) { - t.fail(); - return; - } - t.is(r1.merchantBaseUrl, "https://example.com/mypfx/instances/myinst/"); -}); - -test("taler pay url parsing: complex path prefix", (t) => { - const url1 = "taler://pay/example.com/mypfx%2Fpublic/-/myorder"; - const r1 = parsePayUri(url1); - if (!r1) { - t.fail(); - return; - } - t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/"); - t.is(r1.orderId, "myorder"); - t.is(r1.sessionId, undefined); -}); - -test("taler pay uri parsing: complex path prefix and instance", (t) => { - const url1 = "taler://pay/example.com/mypfx%2Fpublic/foo/myorder"; - const r1 = parsePayUri(url1); - if (!r1) { - t.fail(); - return; - } - t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/instances/foo/"); - t.is(r1.orderId, "myorder"); -}); test("taler refund uri parsing: non-https #1", (t) => { - const url1 = "taler://refund/example.com/-/-/myorder?insecure=1"; + const url1 = "taler+http://refund/example.com/myorder"; const r1 = parseRefundUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "http://example.com/public/"); + t.is(r1.merchantBaseUrl, "http://example.com/"); t.is(r1.orderId, "myorder"); }); -test("taler pay uri parsing: non-https #1", (t) => { - const url1 = "taler://pay/example.com/-/-/myorder?insecure=1"; +test("taler pay uri parsing: non-https", (t) => { + const url1 = "taler+http://pay/example.com/myorder/"; const r1 = parsePayUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "http://example.com/public/"); + t.is(r1.merchantBaseUrl, "http://example.com/"); t.is(r1.orderId, "myorder"); }); -test("taler pay url parsing: non-https #2", (t) => { - const url1 = "taler://pay/example.com/-/-/myorder?insecure=2"; +test("taler pay uri parsing: missing session component", (t) => { + const url1 = "taler+http://pay/example.com/myorder"; const r1 = parsePayUri(url1); - if (!r1) { + if (r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "https://example.com/public/"); - t.is(r1.orderId, "myorder"); + t.pass(); }); test("taler withdraw uri parsing", (t) => { - const url1 = "taler://withdraw/bank.example.com/-/12345"; + const url1 = "taler://withdraw/bank.example.com/12345"; const r1 = parseWithdrawUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.statusUrl, "https://bank.example.com/api/withdraw-operation/12345"); + t.is(r1.withdrawalOperationId, "12345"); + t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/"); }); test("taler refund uri parsing", (t) => { - const url1 = "taler://refund/merchant.example.com/-/-/1234"; + const url1 = "taler://refund/merchant.example.com/1234"; const r1 = parseRefundUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/"); + t.is(r1.merchantBaseUrl, "https://merchant.example.com/"); t.is(r1.orderId, "1234"); }); test("taler refund uri parsing with instance", (t) => { - const url1 = "taler://refund/merchant.example.com/-/myinst/1234"; + const url1 = "taler://refund/merchant.example.com/instances/myinst/1234"; const r1 = parseRefundUri(url1); if (!r1) { t.fail(); @@ -171,22 +128,22 @@ test("taler refund uri parsing with instance", (t) => { t.is(r1.orderId, "1234"); t.is( r1.merchantBaseUrl, - "https://merchant.example.com/public/instances/myinst/", + "https://merchant.example.com/instances/myinst/", ); }); test("taler tip pickup uri", (t) => { - const url1 = "taler://tip/merchant.example.com/-/-/tipid"; + const url1 = "taler://tip/merchant.example.com/tipid"; const r1 = parseTipUri(url1); if (!r1) { t.fail(); return; } - t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/"); + t.is(r1.merchantBaseUrl, "https://merchant.example.com/"); }); test("taler tip pickup uri with instance", (t) => { - const url1 = "taler://tip/merchant.example.com/-/tipm/tipid"; + const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; const r1 = parseTipUri(url1); if (!r1) { t.fail(); @@ -194,13 +151,13 @@ test("taler tip pickup uri with instance", (t) => { } t.is( r1.merchantBaseUrl, - "https://merchant.example.com/public/instances/tipm/", + "https://merchant.example.com/instances/tipm/", ); t.is(r1.merchantTipId, "tipid"); }); test("taler tip pickup uri with instance and prefix", (t) => { - const url1 = "taler://tip/merchant.example.com/my%2fpfx/tipm/tipid"; + const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; const r1 = parseTipUri(url1); if (!r1) { t.fail(); @@ -208,7 +165,7 @@ test("taler tip pickup uri with instance and prefix", (t) => { } t.is( r1.merchantBaseUrl, - "https://merchant.example.com/my/pfx/instances/tipm/", + "https://merchant.example.com/my/pfx/tipm/", ); t.is(r1.merchantTipId, "tipid"); }); diff --git a/src/util/taleruri.ts b/src/util/taleruri.ts index 73280b6c8..7e64dd4ca 100644 --- a/src/util/taleruri.ts +++ b/src/util/taleruri.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019 GNUnet e.V. + (C) 2019-2020 Taler Systems S.A. 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 @@ -17,11 +17,12 @@ export interface PayUriResult { merchantBaseUrl: string; orderId: string; - sessionId?: string; + sessionId: string; } export interface WithdrawUriResult { - statusUrl: string; + bankIntegrationApiBaseUrl: string; + withdrawalOperationId: string; } export interface RefundUriResult { @@ -31,10 +32,13 @@ export interface RefundUriResult { export interface TipUriResult { merchantTipId: string; - merchantOrigin: string; merchantBaseUrl: string; } +/** + * Parse a taler[+http]://withdraw URI. + * Return undefined if not passed a valid URI. + */ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { const pfx = "taler://withdraw/"; if (!s.toLowerCase().startsWith(pfx)) { @@ -42,29 +46,20 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { } const rest = s.substring(pfx.length); + const parts = rest.split("/"); - let [host, path, withdrawId] = rest.split("/"); - - if (!host) { - return undefined; - } - - host = host.toLowerCase(); - - if (!path) { + if (parts.length < 2) { return undefined; } - if (!withdrawId) { - return undefined; - } - - if (path === "-") { - path = "api/withdraw-operation"; - } + const host = parts[0].toLowerCase(); + const pathSegments = parts.slice(1, parts.length - 1); + const withdrawId = parts[parts.length - 1]; + const p = [host, ...pathSegments].join("/"); return { - statusUrl: `https://${host}/${path}/${withdrawId}`, + bankIntegrationApiBaseUrl: `https://${p}/`, + withdrawalOperationId: withdrawId, }; } @@ -77,17 +72,29 @@ export const enum TalerUriType { Unknown = "unknown", } +/** + * Classify a taler:// URI. + */ export function classifyTalerUri(s: string): TalerUriType { const sl = s.toLowerCase(); if (sl.startsWith("taler://pay/")) { return TalerUriType.TalerPay; } + if (sl.startsWith("taler+http://pay/")) { + return TalerUriType.TalerPay; + } if (sl.startsWith("taler://tip/")) { return TalerUriType.TalerTip; } + if (sl.startsWith("taler+http://tip/")) { + return TalerUriType.TalerTip; + } if (sl.startsWith("taler://refund/")) { return TalerUriType.TalerRefund; } + if (sl.startsWith("taler+http://refund/")) { + return TalerUriType.TalerRefund; + } if (sl.startsWith("taler://withdraw/")) { return TalerUriType.TalerWithdraw; } @@ -97,146 +104,103 @@ export function classifyTalerUri(s: string): TalerUriType { return TalerUriType.Unknown; } -export function parsePayUri(s: string): PayUriResult | undefined { - const pfx = "taler://pay/"; - if (!s.toLowerCase().startsWith(pfx)) { - return undefined; - } - - const [path, search] = s.slice(pfx.length).split("?"); +interface TalerUriProtoInfo { + innerProto: "http" | "https"; + rest: string; +} - let [host, maybePath, maybeInstance, orderId, maybeSessionid] = path.split( - "/", - ); - if (!host) { +function parseProtoInfo(s: string, action: string): TalerUriProtoInfo | undefined { + const pfxPlain = `taler://${action}/`; + const pfxHttp = `taler+http://${action}/`; + if (s.toLowerCase().startsWith(pfxPlain)) { + return { + innerProto: "https", + rest: s.substring(pfxPlain.length), + } + } else if (s.toLowerCase().startsWith(pfxHttp)) { + return { + innerProto: "http", + rest: s.substring(pfxHttp.length), + } + } else { return undefined; } +} - host = host.toLowerCase(); - - if (!maybePath) { +/** + * Parse a taler[+http]://pay URI. + * Return undefined if not passed a valid URI. + */ +export function parsePayUri(s: string): PayUriResult | undefined { + const pi = parseProtoInfo(s, "pay"); + if (!pi) { return undefined; } - - if (!orderId) { + const c = pi?.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 3) { return undefined; } - - if (maybePath === "-") { - maybePath = ""; - } else { - maybePath = decodeURIComponent(maybePath) + "/"; - } - let maybeInstancePath = ""; - if (maybeInstance !== "-") { - maybeInstancePath = `instances/${maybeInstance}/`; - } - - let protocol = "https"; - const searchParams = new URLSearchParams(search); - if (searchParams.get("insecure") === "1") { - protocol = "http"; - } - - const merchantBaseUrl = - `${protocol}://${host}/` + - decodeURIComponent(maybePath) + - maybeInstancePath; + const host = parts[0].toLowerCase(); + const sessionId = parts[parts.length - 1]; + const orderId = parts[parts.length - 2]; + const pathSegments = parts.slice(1, parts.length - 2); + const p = [host, ...pathSegments].join("/"); + const merchantBaseUrl = `${pi.innerProto}://${p}/`; return { merchantBaseUrl, orderId, - sessionId: maybeSessionid, + sessionId: sessionId, }; } +/** + * Parse a taler[+http]://tip URI. + * Return undefined if not passed a valid URI. + */ export function parseTipUri(s: string): TipUriResult | undefined { - const pfx = "taler://tip/"; - if (!s.toLowerCase().startsWith(pfx)) { + const pi = parseProtoInfo(s, "tip"); + if (!pi) { return undefined; } - - const path = s.slice(pfx.length); - - let [host, maybePath, maybeInstance, tipId] = path.split("/"); - - if (!host) { - return undefined; - } - - host = host.toLowerCase(); - - if (!maybePath) { - return undefined; - } - - if (!tipId) { + const c = pi?.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 2) { return undefined; } - - if (maybePath === "-") { - maybePath = "public/"; - } else { - maybePath = decodeURIComponent(maybePath) + "/"; - } - let maybeInstancePath = ""; - if (maybeInstance !== "-") { - maybeInstancePath = `instances/${maybeInstance}/`; - } - - const merchantBaseUrl = `https://${host}/${maybePath}${maybeInstancePath}`; + const host = parts[0].toLowerCase(); + const tipId = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const p = [host, ...pathSegments].join("/"); + const merchantBaseUrl = `${pi.innerProto}://${p}/`; return { - merchantTipId: tipId, - merchantOrigin: new URL(merchantBaseUrl).origin, merchantBaseUrl, + merchantTipId: tipId, }; } +/** + * Parse a taler[+http]://refund URI. + * Return undefined if not passed a valid URI. + */ export function parseRefundUri(s: string): RefundUriResult | undefined { - const pfx = "taler://refund/"; - - if (!s.toLowerCase().startsWith(pfx)) { - return undefined; - } - - const [path, search] = s.slice(pfx.length).split("?"); - - let [host, maybePath, maybeInstance, orderId] = path.split("/"); - - if (!host) { - return undefined; - } - - host = host.toLowerCase(); - - if (!maybePath) { + const pi = parseProtoInfo(s, "refund"); + if (!pi) { return undefined; } - - if (!orderId) { + const c = pi?.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 2) { return undefined; } - - if (maybePath === "-") { - maybePath = ""; - } else { - maybePath = decodeURIComponent(maybePath) + "/"; - } - let maybeInstancePath = ""; - if (maybeInstance !== "-") { - maybeInstancePath = `instances/${maybeInstance}/`; - } - - let protocol = "https"; - const searchParams = new URLSearchParams(search); - if (searchParams.get("insecure") === "1") { - protocol = "http"; - } - - const merchantBaseUrl = - `${protocol}://${host}/` + maybePath + maybeInstancePath; + const host = parts[0].toLowerCase(); + const orderId = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const p = [host, ...pathSegments].join("/"); + const merchantBaseUrl = `${pi.innerProto}://${p}/`; return { merchantBaseUrl, -- cgit v1.2.3