wallet-core/packages/taler-util/src/taleruri.ts

412 lines
11 KiB
TypeScript
Raw Normal View History

2019-08-27 02:49:38 +02:00
/*
This file is part of GNU Taler
2020-07-27 13:39:52 +02:00
(C) 2019-2020 Taler Systems S.A.
2019-08-27 02:49:38 +02:00
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
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
2022-10-20 19:53:29 +02:00
import { BackupRecovery } from "./backup-types.js";
import { canonicalizeBaseUrl } from "./helpers.js";
2022-10-20 19:53:29 +02:00
import { initNodePrng } from "./prng-node.js";
2022-08-09 15:00:45 +02:00
import { URLSearchParams, URL } from "./url.js";
2019-08-27 02:49:38 +02:00
export interface PayUriResult {
2019-12-06 12:47:28 +01:00
merchantBaseUrl: string;
orderId: string;
2020-07-27 13:39:52 +02:00
sessionId: string;
2020-07-30 13:58:09 +02:00
claimToken: string | undefined;
2021-09-17 20:48:33 +02:00
noncePriv: string | undefined;
2019-08-27 02:49:38 +02:00
}
2019-08-28 02:49:27 +02:00
export interface WithdrawUriResult {
2020-07-27 13:39:52 +02:00
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
2019-08-28 02:49:27 +02:00
}
2019-08-31 11:49:36 +02:00
export interface RefundUriResult {
2019-12-05 19:38:19 +01:00
merchantBaseUrl: string;
orderId: string;
2019-08-31 11:49:36 +02:00
}
2019-08-30 17:27:59 +02:00
export interface TipUriResult {
merchantTipId: string;
merchantBaseUrl: string;
2019-08-30 17:27:59 +02:00
}
2022-08-09 15:00:45 +02:00
export interface PayPushUriResult {
exchangeBaseUrl: string;
contractPriv: string;
}
export interface PayPullUriResult {
exchangeBaseUrl: string;
contractPriv: string;
}
export interface DevExperimentUri {
devExperimentId: string;
}
2020-07-27 13:39:52 +02:00
/**
* Parse a taler[+http]://withdraw URI.
* Return undefined if not passed a valid URI.
*/
2019-08-28 02:49:27 +02:00
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
2020-07-27 19:57:32 +02:00
const pi = parseProtoInfo(s, "withdraw");
if (!pi) {
2019-08-28 02:49:27 +02:00
return undefined;
}
2020-07-27 19:57:32 +02:00
const parts = pi.rest.split("/");
2020-07-27 13:39:52 +02:00
if (parts.length < 2) {
2019-12-06 12:47:28 +01:00
return undefined;
}
2020-07-27 13:39:52 +02:00
const host = parts[0].toLowerCase();
const pathSegments = parts.slice(1, parts.length - 1);
/**
* The statement below does not tolerate a slash-ended URI.
* This results in (1) the withdrawalId being passed as the
* empty string, and (2) the bankIntegrationApi ending with the
* actual withdrawal operation ID. That can be fixed by
* trimming the parts-list. FIXME
*/
2020-07-27 13:39:52 +02:00
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
2019-08-28 02:49:27 +02:00
return {
2020-11-16 14:12:37 +01:00
bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
2020-07-27 13:39:52 +02:00
withdrawalOperationId: withdrawId,
2019-08-28 02:49:27 +02:00
};
}
export enum TalerUriType {
2019-12-06 12:47:28 +01:00
TalerPay = "taler-pay",
TalerWithdraw = "taler-withdraw",
TalerTip = "taler-tip",
TalerRefund = "taler-refund",
TalerNotifyReserve = "taler-notify-reserve",
TalerPayPush = "taler-pay-push",
TalerPayPull = "taler-pay-pull",
2022-10-20 19:53:29 +02:00
TalerRecovery = "taler-recovery",
TalerDevExperiment = "taler-dev-experiment",
2019-12-06 12:47:28 +01:00
Unknown = "unknown",
}
const talerActionPayPull = "pay-pull";
const talerActionPayPush = "pay-push";
2020-07-27 13:39:52 +02:00
/**
* Classify a taler:// URI.
*/
2019-12-06 12:47:28 +01:00
export function classifyTalerUri(s: string): TalerUriType {
const sl = s.toLowerCase();
2022-10-20 19:53:29 +02:00
if (sl.startsWith("taler://recovery/")) {
return TalerUriType.TalerRecovery;
}
if (sl.startsWith("taler+http://recovery/")) {
return TalerUriType.TalerRecovery;
}
2019-12-06 12:47:28 +01:00
if (sl.startsWith("taler://pay/")) {
return TalerUriType.TalerPay;
2019-08-27 02:49:38 +02:00
}
2020-07-27 13:39:52 +02:00
if (sl.startsWith("taler+http://pay/")) {
return TalerUriType.TalerPay;
}
2019-12-06 12:47:28 +01:00
if (sl.startsWith("taler://tip/")) {
return TalerUriType.TalerTip;
}
2020-07-27 13:39:52 +02:00
if (sl.startsWith("taler+http://tip/")) {
return TalerUriType.TalerTip;
}
2019-12-06 12:47:28 +01:00
if (sl.startsWith("taler://refund/")) {
return TalerUriType.TalerRefund;
}
2020-07-27 13:39:52 +02:00
if (sl.startsWith("taler+http://refund/")) {
return TalerUriType.TalerRefund;
}
2019-12-06 12:47:28 +01:00
if (sl.startsWith("taler://withdraw/")) {
return TalerUriType.TalerWithdraw;
}
2020-10-02 17:16:19 +02:00
if (sl.startsWith("taler+http://withdraw/")) {
return TalerUriType.TalerWithdraw;
}
if (sl.startsWith(`taler://${talerActionPayPush}/`)) {
2022-08-09 15:00:45 +02:00
return TalerUriType.TalerPayPush;
}
if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {
2022-08-09 15:00:45 +02:00
return TalerUriType.TalerPayPush;
}
if (sl.startsWith(`taler://${talerActionPayPull}/`)) {
return TalerUriType.TalerPayPull;
}
if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) {
return TalerUriType.TalerPayPull;
}
2019-12-06 12:47:28 +01:00
if (sl.startsWith("taler://notify-reserve/")) {
2019-12-20 11:35:51 +01:00
return TalerUriType.TalerNotifyReserve;
2019-12-06 12:47:28 +01:00
}
if (sl.startsWith("taler://dev-experiment/")) {
return TalerUriType.TalerDevExperiment;
}
2019-12-06 12:47:28 +01:00
return TalerUriType.Unknown;
}
2020-07-27 13:39:52 +02:00
interface TalerUriProtoInfo {
innerProto: "http" | "https";
2020-07-27 13:39:52 +02:00
rest: string;
}
function parseProtoInfo(
s: string,
action: string,
): TalerUriProtoInfo | undefined {
2020-07-27 13:39:52 +02:00
const pfxPlain = `taler://${action}/`;
const pfxHttp = `taler+http://${action}/`;
if (s.toLowerCase().startsWith(pfxPlain)) {
return {
innerProto: "https",
rest: s.substring(pfxPlain.length),
};
2020-07-27 13:39:52 +02:00
} else if (s.toLowerCase().startsWith(pfxHttp)) {
return {
innerProto: "http",
rest: s.substring(pfxHttp.length),
};
2020-07-27 13:39:52 +02:00
} else {
2019-08-27 02:49:38 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
}
2019-08-27 02:49:38 +02:00
2020-07-27 13:39:52 +02:00
/**
* 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) {
2019-08-27 02:49:38 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
const c = pi?.rest.split("?");
2020-07-30 13:58:09 +02:00
const q = new URLSearchParams(c[1] ?? "");
const claimToken = q.get("c") ?? undefined;
2021-09-17 20:48:33 +02:00
const noncePriv = q.get("n") ?? undefined;
2020-07-27 13:39:52 +02:00
const parts = c[0].split("/");
if (parts.length < 3) {
2019-08-27 02:49:38 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
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("/");
2020-11-16 14:12:37 +01:00
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
2019-08-27 02:49:38 +02:00
return {
2019-12-06 12:47:28 +01:00
merchantBaseUrl,
orderId,
2020-07-27 13:39:52 +02:00
sessionId: sessionId,
2020-07-30 13:58:09 +02:00
claimToken,
2021-09-17 20:48:33 +02:00
noncePriv,
2019-08-28 02:49:27 +02:00
};
2019-08-27 02:49:38 +02:00
}
2019-08-30 17:27:59 +02:00
2022-08-09 15:00:45 +02:00
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
const pi = parseProtoInfo(s, talerActionPayPush);
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 2) {
return undefined;
}
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
const p = [host, ...pathSegments].join("/");
const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
exchangeBaseUrl,
contractPriv,
};
}
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
const pi = parseProtoInfo(s, talerActionPayPull);
2022-08-09 15:00:45 +02:00
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 2) {
return undefined;
}
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
const p = [host, ...pathSegments].join("/");
const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
exchangeBaseUrl,
contractPriv,
};
}
2020-07-27 13:39:52 +02:00
/**
* Parse a taler[+http]://tip URI.
* Return undefined if not passed a valid URI.
*/
2019-08-30 17:27:59 +02:00
export function parseTipUri(s: string): TipUriResult | undefined {
2020-07-27 13:39:52 +02:00
const pi = parseProtoInfo(s, "tip");
if (!pi) {
2019-08-30 17:27:59 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 2) {
2019-08-30 17:27:59 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
const host = parts[0].toLowerCase();
const tipId = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
const p = [host, ...pathSegments].join("/");
2020-11-16 14:12:37 +01:00
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
2019-08-30 17:27:59 +02:00
return {
merchantBaseUrl,
2020-07-27 13:39:52 +02:00
merchantTipId: tipId,
2019-08-30 17:27:59 +02:00
};
}
2019-08-31 11:49:36 +02:00
2020-07-27 13:39:52 +02:00
/**
* Parse a taler[+http]://refund URI.
* Return undefined if not passed a valid URI.
*/
2019-08-31 11:49:36 +02:00
export function parseRefundUri(s: string): RefundUriResult | undefined {
2020-07-27 13:39:52 +02:00
const pi = parseProtoInfo(s, "refund");
if (!pi) {
2019-08-31 11:49:36 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 3) {
2019-08-31 11:49:36 +02:00
return undefined;
}
2020-07-27 13:39:52 +02:00
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);
2020-07-27 13:39:52 +02:00
const p = [host, ...pathSegments].join("/");
2020-11-16 14:12:37 +01:00
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
2019-08-31 11:49:36 +02:00
return {
2019-12-05 19:38:19 +01:00
merchantBaseUrl,
orderId,
2019-08-31 11:49:36 +02:00
};
2019-10-01 20:45:36 +02:00
}
2022-08-09 15:00:45 +02:00
export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
const pi = parseProtoInfo(s, "dev-experiment");
const c = pi?.rest.split("?");
if (!c) {
return undefined;
}
// const q = new URLSearchParams(c[1] ?? "");
const parts = c[0].split("/");
return {
devExperimentId: parts[0],
};
}
2022-08-09 15:00:45 +02:00
export function constructPayPushUri(args: {
exchangeBaseUrl: string;
contractPriv: string;
}): string {
const url = new URL(args.exchangeBaseUrl);
let proto: string;
if (url.protocol === "https:") {
proto = "taler";
} else if (url.protocol === "http:") {
proto = "taler+http";
} else {
throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
}
if (!url.pathname.endsWith("/")) {
throw Error(
`exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
);
}
return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
}
export function constructPayPullUri(args: {
exchangeBaseUrl: string;
contractPriv: string;
}): string {
const url = new URL(args.exchangeBaseUrl);
let proto: string;
if (url.protocol === "https:") {
proto = "taler";
} else if (url.protocol === "http:") {
proto = "taler+http";
} else {
throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
}
if (!url.pathname.endsWith("/")) {
throw Error(
`exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
);
}
return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`;
}
2022-10-20 19:53:29 +02:00
export function constructRecoveryUri(args: BackupRecovery): string {
2022-10-21 17:12:35 +02:00
const key = args.walletRootPriv;
//FIXME: name may contain non valid characters
const urls = args.providers
.map((p) => `${p.name}=${canonicalizeBaseUrl(p.url)}`)
.join("&");
2022-10-20 19:53:29 +02:00
2022-10-21 17:12:35 +02:00
return `taler://recovery/${key}?${urls}`;
2022-10-20 19:53:29 +02:00
}
export function parseRecoveryUri(uri: string): BackupRecovery | undefined {
const pi = parseProtoInfo(uri, "recovery");
if (!pi) {
return undefined;
}
const idx = pi.rest.indexOf("?");
if (idx === -1) {
2022-10-21 17:12:35 +02:00
return undefined;
2022-10-20 19:53:29 +02:00
}
2022-10-21 17:12:35 +02:00
const path = pi.rest.slice(0, idx);
const params = pi.rest.slice(idx + 1);
2022-10-20 19:53:29 +02:00
if (!path || !params) {
return undefined;
}
const parts = path.split("/");
const walletRootPriv = parts[0];
2022-10-21 17:12:35 +02:00
if (!walletRootPriv) return undefined;
const providers = new Array<{ name: string; url: string }>();
const args = params.split("&");
2022-10-20 19:53:29 +02:00
for (const param in args) {
2022-10-21 17:12:35 +02:00
const eq = args[param].indexOf("=");
2022-10-20 19:53:29 +02:00
if (eq === -1) return undefined;
2022-10-21 17:12:35 +02:00
const name = args[param].slice(0, eq);
const url = args[param].slice(eq + 1);
providers.push({ name, url });
2022-10-20 19:53:29 +02:00
}
2022-10-21 17:12:35 +02:00
return { walletRootPriv, providers };
2022-10-20 19:53:29 +02:00
}