new taler:// URI syntax

This commit is contained in:
Florian Dold 2020-07-27 17:09:52 +05:30
parent 694d913d1f
commit ae111663f4
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 136 additions and 209 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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");
});

View File

@ -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) {
if (parts.length < 2) {
return undefined;
}
host = host.toLowerCase();
if (!path) {
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;
}
interface TalerUriProtoInfo {
innerProto: "http" | "https";
rest: string;
}
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;
}
}
/**
* Parse a taler[+http]://pay URI.
* Return undefined if not passed a valid URI.
*/
export function parsePayUri(s: string): PayUriResult | undefined {
const pfx = "taler://pay/";
if (!s.toLowerCase().startsWith(pfx)) {
const pi = parseProtoInfo(s, "pay");
if (!pi) {
return undefined;
}
const [path, search] = s.slice(pfx.length).split("?");
let [host, maybePath, maybeInstance, orderId, maybeSessionid] = path.split(
"/",
);
if (!host) {
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 3) {
return undefined;
}
host = host.toLowerCase();
if (!maybePath) {
return undefined;
}
if (!orderId) {
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) {
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 2) {
return undefined;
}
host = host.toLowerCase();
if (!maybePath) {
return undefined;
}
if (!tipId) {
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)) {
const pi = parseProtoInfo(s, "refund");
if (!pi) {
return undefined;
}
const [path, search] = s.slice(pfx.length).split("?");
let [host, maybePath, maybeInstance, orderId] = path.split("/");
if (!host) {
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 2) {
return undefined;
}
host = host.toLowerCase();
if (!maybePath) {
return undefined;
}
if (!orderId) {
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,