wallet-core/packages/taler-util/src/taleruri.ts
2022-10-21 12:12:35 -03:00

412 lines
11 KiB
TypeScript

/*
This file is part of GNU Taler
(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
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/>
*/
import { BackupRecovery } from "./backup-types.js";
import { canonicalizeBaseUrl } from "./helpers.js";
import { initNodePrng } from "./prng-node.js";
import { URLSearchParams, URL } from "./url.js";
export interface PayUriResult {
merchantBaseUrl: string;
orderId: string;
sessionId: string;
claimToken: string | undefined;
noncePriv: string | undefined;
}
export interface WithdrawUriResult {
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
}
export interface RefundUriResult {
merchantBaseUrl: string;
orderId: string;
}
export interface TipUriResult {
merchantTipId: string;
merchantBaseUrl: string;
}
export interface PayPushUriResult {
exchangeBaseUrl: string;
contractPriv: string;
}
export interface PayPullUriResult {
exchangeBaseUrl: string;
contractPriv: string;
}
export interface DevExperimentUri {
devExperimentId: string;
}
/**
* Parse a taler[+http]://withdraw URI.
* Return undefined if not passed a valid URI.
*/
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const pi = parseProtoInfo(s, "withdraw");
if (!pi) {
return undefined;
}
const parts = pi.rest.split("/");
if (parts.length < 2) {
return undefined;
}
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
*/
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
return {
bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
withdrawalOperationId: withdrawId,
};
}
export enum TalerUriType {
TalerPay = "taler-pay",
TalerWithdraw = "taler-withdraw",
TalerTip = "taler-tip",
TalerRefund = "taler-refund",
TalerNotifyReserve = "taler-notify-reserve",
TalerPayPush = "taler-pay-push",
TalerPayPull = "taler-pay-pull",
TalerRecovery = "taler-recovery",
TalerDevExperiment = "taler-dev-experiment",
Unknown = "unknown",
}
const talerActionPayPull = "pay-pull";
const talerActionPayPush = "pay-push";
/**
* Classify a taler:// URI.
*/
export function classifyTalerUri(s: string): TalerUriType {
const sl = s.toLowerCase();
if (sl.startsWith("taler://recovery/")) {
return TalerUriType.TalerRecovery;
}
if (sl.startsWith("taler+http://recovery/")) {
return TalerUriType.TalerRecovery;
}
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;
}
if (sl.startsWith("taler+http://withdraw/")) {
return TalerUriType.TalerWithdraw;
}
if (sl.startsWith(`taler://${talerActionPayPush}/`)) {
return TalerUriType.TalerPayPush;
}
if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {
return TalerUriType.TalerPayPush;
}
if (sl.startsWith(`taler://${talerActionPayPull}/`)) {
return TalerUriType.TalerPayPull;
}
if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) {
return TalerUriType.TalerPayPull;
}
if (sl.startsWith("taler://notify-reserve/")) {
return TalerUriType.TalerNotifyReserve;
}
if (sl.startsWith("taler://dev-experiment/")) {
return TalerUriType.TalerDevExperiment;
}
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 pi = parseProtoInfo(s, "pay");
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const q = new URLSearchParams(c[1] ?? "");
const claimToken = q.get("c") ?? undefined;
const noncePriv = q.get("n") ?? undefined;
const parts = c[0].split("/");
if (parts.length < 3) {
return undefined;
}
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 = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
merchantBaseUrl,
orderId,
sessionId: sessionId,
claimToken,
noncePriv,
};
}
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);
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,
};
}
/**
* Parse a taler[+http]://tip URI.
* Return undefined if not passed a valid URI.
*/
export function parseTipUri(s: string): TipUriResult | undefined {
const pi = parseProtoInfo(s, "tip");
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 tipId = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
const p = [host, ...pathSegments].join("/");
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
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 pi = parseProtoInfo(s, "refund");
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const parts = c[0].split("/");
if (parts.length < 3) {
return undefined;
}
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 = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
merchantBaseUrl,
orderId,
};
}
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],
};
}
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}`;
}
export function constructRecoveryUri(args: BackupRecovery): string {
const key = args.walletRootPriv;
//FIXME: name may contain non valid characters
const urls = args.providers
.map((p) => `${p.name}=${canonicalizeBaseUrl(p.url)}`)
.join("&");
return `taler://recovery/${key}?${urls}`;
}
export function parseRecoveryUri(uri: string): BackupRecovery | undefined {
const pi = parseProtoInfo(uri, "recovery");
if (!pi) {
return undefined;
}
const idx = pi.rest.indexOf("?");
if (idx === -1) {
return undefined;
}
const path = pi.rest.slice(0, idx);
const params = pi.rest.slice(idx + 1);
if (!path || !params) {
return undefined;
}
const parts = path.split("/");
const walletRootPriv = parts[0];
if (!walletRootPriv) return undefined;
const providers = new Array<{ name: string; url: string }>();
const args = params.split("&");
for (const param in args) {
const eq = args[param].indexOf("=");
if (eq === -1) return undefined;
const name = args[param].slice(0, eq);
const url = args[param].slice(eq + 1);
providers.push({ name, url });
}
return { walletRootPriv, providers };
}