From defdfd769749354ec3aba9436a3bcb5cd39bdd83 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 3 Apr 2023 12:12:35 -0300 Subject: taleruri sync with the spec --- packages/taler-util/src/taleruri.ts | 555 ++++++++++++++++++++++++++++-------- 1 file changed, 438 insertions(+), 117 deletions(-) diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index 4d55d4c98..e641f1d27 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -14,11 +14,24 @@ GNU Taler; see the file COPYING. If not, see */ -import { BackupRecovery } from "./backup-types.js"; import { canonicalizeBaseUrl } from "./helpers.js"; import { URLSearchParams, URL } from "./url.js"; +export type TalerUri = + | PayUriResult + | PayTemplateUriResult + | DevExperimentUri + | PayPullUriResult + | PayPushUriResult + | BackupRestoreUri + | RefundUriResult + | TipUriResult + | WithdrawUriResult + | ExchangeUri + | AuditorUri; + export interface PayUriResult { + type: TalerUriAction.Pay; merchantBaseUrl: string; orderId: string; sessionId: string; @@ -27,40 +40,68 @@ export interface PayUriResult { } export interface PayTemplateUriResult { + type: TalerUriAction.PayTemplate; merchantBaseUrl: string; templateId: string; templateParams: Record; } export interface WithdrawUriResult { + type: TalerUriAction.Withdraw; bankIntegrationApiBaseUrl: string; withdrawalOperationId: string; } export interface RefundUriResult { + type: TalerUriAction.Refund; merchantBaseUrl: string; orderId: string; } export interface TipUriResult { - merchantTipId: string; + type: TalerUriAction.Tip; merchantBaseUrl: string; + merchantTipId: string; +} + +export interface ExchangeUri { + type: TalerUriAction.Exchange; + exchangeBaseUrl: string; + exchangePub: string; +} + +export interface AuditorUri { + type: TalerUriAction.Auditor; + auditorBaseUrl: string; + auditorPub: string; } export interface PayPushUriResult { + type: TalerUriAction.PayPush; exchangeBaseUrl: string; contractPriv: string; } export interface PayPullUriResult { + type: TalerUriAction.PayPull; exchangeBaseUrl: string; contractPriv: string; } export interface DevExperimentUri { + type: TalerUriAction.DevExperiment; devExperimentId: string; } +export interface BackupRestoreUri { + type: TalerUriAction.Restore; + walletRootPriv: string; + providers: { + name: string; + url: string; + }[]; +} + /** * Parse a taler[+http]://withdraw URI. * Return undefined if not passed a valid URI. @@ -89,11 +130,15 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { const p = [host, ...pathSegments].join("/"); return { + type: TalerUriAction.Withdraw, bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`), withdrawalOperationId: withdrawId, }; } +/** + * @deprecated use TalerUriAction + */ export enum TalerUriType { TalerPay = "taler-pay", TalerTemplate = "taler-template", @@ -112,15 +157,30 @@ const talerActionPayPull = "pay-pull"; const talerActionPayPush = "pay-push"; const talerActionPayTemplate = "pay-template"; +export enum TalerUriAction { + Pay = "pay", + Withdraw = "withdraw", + Refund = "refund", + Tip = "tip", + PayPull = "pay-pull", + PayPush = "pay-push", + PayTemplate = "pay-template", + Exchange = "exchange", + Auditor = "auditor", + Restore = "restore", + DevExperiment = "dev-experiment", +} + /** * Classify a taler:// URI. + * @deprecated use parseTalerUri */ export function classifyTalerUri(s: string): TalerUriType { const sl = s.toLowerCase(); - if (sl.startsWith("taler://recovery/")) { + if (sl.startsWith("taler://restore/")) { return TalerUriType.TalerRecovery; } - if (sl.startsWith("taler+http://recovery/")) { + if (sl.startsWith("taler+http://restore/")) { return TalerUriType.TalerRecovery; } if (sl.startsWith("taler://pay/")) { @@ -197,6 +257,71 @@ function parseProtoInfo( } } +type Parser = (s: string) => TalerUri | undefined; +const parsers: { [A in TalerUriAction]: Parser } = { + [TalerUriAction.Pay]: parsePayUri, + [TalerUriAction.PayPull]: parsePayPullUri, + [TalerUriAction.PayPush]: parsePayPushUri, + [TalerUriAction.PayTemplate]: parsePayTemplateUri, + [TalerUriAction.Restore]: parseRestoreUri, + [TalerUriAction.Refund]: parseRefundUri, + [TalerUriAction.Tip]: parseTipUri, + [TalerUriAction.Withdraw]: parseWithdrawUri, + [TalerUriAction.DevExperiment]: parseDevExperimentUri, + [TalerUriAction.Exchange]: parseExchangeUri, + [TalerUriAction.Auditor]: parseAuditorUri, +}; + +export function parseTalerUri(string: string): TalerUri | undefined { + const https = string.startsWith("taler://"); + const http = string.startsWith("taler+http://"); + if (!https && !http) return undefined; + const actionStart = https ? 8 : 13; + const actionEnd = string.indexOf("/", actionStart + 1); + const action = string.substring(actionStart, actionEnd); + const found = Object.values(TalerUriAction).find((x) => x === action); + if (!found) return undefined; + return parsers[found](string); +} + +export function stringifyTalerUri(uri: TalerUri): string { + switch (uri.type) { + case TalerUriAction.DevExperiment: { + return stringifyDevExperimentUri(uri); + } + case TalerUriAction.Pay: { + return stringifyPayUri(uri); + } + case TalerUriAction.PayPull: { + return stringifyPayPullUri(uri); + } + case TalerUriAction.PayPush: { + return stringifyPayPushUri(uri); + } + case TalerUriAction.PayTemplate: { + return stringifyPayTemplateUri(uri); + } + case TalerUriAction.Restore: { + return stringifyRestoreUri(uri); + } + case TalerUriAction.Refund: { + return stringifyRefundUri(uri); + } + case TalerUriAction.Tip: { + return stringifyTipUri(uri); + } + case TalerUriAction.Withdraw: { + return stringifyWithdrawUri(uri); + } + case TalerUriAction.Exchange: { + return stringifyExchangeUri(uri); + } + case TalerUriAction.Auditor: { + return stringifyAuditorUri(uri); + } + } +} + /** * Parse a taler[+http]://pay URI. * Return undefined if not passed a valid URI. @@ -222,76 +347,51 @@ export function parsePayUri(s: string): PayUriResult | undefined { const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); return { + type: TalerUriAction.Pay, merchantBaseUrl, orderId, - sessionId: sessionId, + sessionId, claimToken, noncePriv, }; } export function parsePayTemplateUri( - s: string, + uriString: string, ): PayTemplateUriResult | undefined { - const pi = parseProtoInfo(s, talerActionPayTemplate); + const pi = parseProtoInfo(uriString, talerActionPayTemplate); if (!pi) { return undefined; } - const c = pi?.rest.split("?"); - const q = new URLSearchParams(c[1] ?? ""); + const c = pi.rest.split("?"); + const parts = c[0].split("/"); if (parts.length < 2) { return undefined; } - const host = parts[0].toLowerCase(); - const templateId = parts[parts.length - 1]; - const pathSegments = parts.slice(1, parts.length - 1); - const p = [host, ...pathSegments].join("/"); - const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); + const q = new URLSearchParams(c[1] ?? ""); const params: Record = {}; - q.forEach((v, k) => { params[k] = v; }); + const host = parts[0].toLowerCase(); + const templateId = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const hostAndSegments = [host, ...pathSegments].join("/"); + const merchantBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); + return { + type: TalerUriAction.PayTemplate, merchantBaseUrl, templateId, templateParams: params, }; } -export function constructPayUri( - merchantBaseUrl: string, - orderId: string, - sessionId: string, - claimToken?: string, - noncePriv?: string, -): string { - const base = canonicalizeBaseUrl(merchantBaseUrl); - const url = new URL(base); - const isHttp = base.startsWith("http://"); - let result = isHttp ? `taler+http://pay/` : `taler://pay/`; - result += url.hostname; - if (url.port != "") { - result += `:${url.port}`; - } - result += `${url.pathname}${orderId}/${sessionId}`; - const qp = new URLSearchParams(); - if (claimToken) { - qp.append("c", claimToken); - } - if (noncePriv) { - qp.append("n", noncePriv); - } - const queryPart = qp.toString(); - if (queryPart) { - result += "?" + queryPart; - } - return result; -} - export function parsePayPushUri(s: string): PayPushUriResult | undefined { const pi = parseProtoInfo(s, talerActionPayPush); if (!pi) { @@ -305,10 +405,13 @@ export function parsePayPushUri(s: string): PayPushUriResult | 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}/`); + const hostAndSegments = [host, ...pathSegments].join("/"); + const exchangeBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); return { + type: TalerUriAction.PayPush, exchangeBaseUrl, contractPriv, }; @@ -327,10 +430,13 @@ export function parsePayPullUri(s: string): PayPullUriResult | 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}/`); + const hostAndSegments = [host, ...pathSegments].join("/"); + const exchangeBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); return { + type: TalerUriAction.PayPull, exchangeBaseUrl, contractPriv, }; @@ -353,15 +459,67 @@ export function parseTipUri(s: string): TipUriResult | 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}/`); + const hostAndSegments = [host, ...pathSegments].join("/"); + const merchantBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); return { + type: TalerUriAction.Tip, merchantBaseUrl, merchantTipId: tipId, }; } +export function parseExchangeUri(s: string): ExchangeUri | undefined { + const pi = parseProtoInfo(s, "exchange"); + 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 exchangePub = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const hostAndSegments = [host, ...pathSegments].join("/"); + const exchangeBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); + + return { + type: TalerUriAction.Exchange, + exchangeBaseUrl, + exchangePub, + }; +} +export function parseAuditorUri(s: string): AuditorUri | undefined { + const pi = parseProtoInfo(s, "auditor"); + 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 auditorPub = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const hostAndSegments = [host, ...pathSegments].join("/"); + const auditorBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); + + return { + type: TalerUriAction.Auditor, + auditorBaseUrl, + auditorPub, + }; +} + /** * Parse a taler[+http]://refund URI. * Return undefined if not passed a valid URI. @@ -377,13 +535,16 @@ export function parseRefundUri(s: string): RefundUriResult | undefined { 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}/`); + // const sessionId = parts[parts.length - 1]; + const orderId = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const hostAndSegments = [host, ...pathSegments].join("/"); + const merchantBaseUrl = canonicalizeBaseUrl( + `${pi.innerProto}://${hostAndSegments}/`, + ); return { + type: TalerUriAction.Refund, merchantBaseUrl, orderId, }; @@ -395,89 +556,249 @@ export function parseDevExperimentUri(s: string): DevExperimentUri | undefined { if (!c) { return undefined; } - // const q = new URLSearchParams(c[1] ?? ""); const parts = c[0].split("/"); return { + type: TalerUriAction.DevExperiment, 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}`); +export function parseRestoreUri(uri: string): BackupRestoreUri | undefined { + const pi = parseProtoInfo(uri, "restore"); + if (!pi) { + return undefined; } - if (!url.pathname.endsWith("/")) { - throw Error( - `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`, - ); + const c = pi.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 2) { + return undefined; } - return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`; + + const walletRootPriv = parts[0]; + if (!walletRootPriv) return undefined; + const providers = new Array<{ name: string; url: string }>(); + parts[1].split(",").map((name) => { + const url = canonicalizeBaseUrl(`${pi.innerProto}://${name}/`); + providers.push({ name, url }); + }); + return { + type: TalerUriAction.Restore, + walletRootPriv, + providers, + }; +} + +// ================================================ +// To string functions +// ================================================ + +/** + * @deprecated use stringifyRecoveryUri + */ +export function constructRecoveryUri(args: { + walletRootPriv: string; + providers: { + name: string; + url: string; + }[]; +}): string { + return stringifyRestoreUri(args); } +/** + * @deprecated stringifyPayPullUri + */ export function constructPayPullUri(args: { exchangeBaseUrl: string; contractPriv: string; }): string { - const url = new URL(args.exchangeBaseUrl); + return stringifyPayPullUri(args); +} + +/** + * @deprecated use stringifyPayPushUri + */ +export function constructPayPushUri(args: { + exchangeBaseUrl: string; + contractPriv: string; +}): string { + return stringifyPayPushUri(args); +} + +/** + * + * @deprecated use stringifyPayUri + */ +export function constructPayUri( + merchantBaseUrl: string, + orderId: string, + sessionId: string, + claimToken?: string, + noncePriv?: string, +): string { + return stringifyPayUri({ + merchantBaseUrl, + orderId, + sessionId, + claimToken, + noncePriv, + }); +} + +export function stringifyPayUri({ + merchantBaseUrl, + orderId, + sessionId, + claimToken, + noncePriv, +}: Omit): string { + // const base = canonicalizeBaseUrl(merchantBaseUrl); + // const url = new URL(base); + // const isHttp = base.startsWith("http://"); + // let result = isHttp ? `taler+http://pay/` : `taler://pay/`; + // result += url.hostname; + // if (url.port != "") { + // result += `:${url.port}`; + // } + // result += `${url.pathname}${orderId}/${sessionId}`; + // const qp = new URLSearchParams(); + // if (claimToken) { + // qp.append("c", claimToken); + // } + // if (noncePriv) { + // qp.append("n", noncePriv); + // } + // const queryPart = qp.toString(); + // if (queryPart) { + // result += "?" + queryPart; + // } + const { proto, path, query } = getUrlInfo(merchantBaseUrl, { + c: claimToken, + n: noncePriv, + }); + return `${proto}://pay/${path}${orderId}/${sessionId}${query}`; +} + +export function stringifyPayPullUri({ + contractPriv, + exchangeBaseUrl, +}: Omit): string { + const { proto, path } = getUrlInfo(exchangeBaseUrl); + return `${proto}://pay-pull/${path}${contractPriv}`; +} + +export function stringifyPayPushUri({ + contractPriv, + exchangeBaseUrl, +}: Omit): string { + const { proto, path } = getUrlInfo(exchangeBaseUrl); + + return `${proto}://pay-push/${path}${contractPriv}`; +} + +export function stringifyRestoreUri({ + providers, + walletRootPriv, +}: Omit): string { + const list = providers.map((p) => `${new URL(p.url).hostname}`).join("m"); + return `taler://restore/${walletRootPriv}/${list}`; +} + +export function stringifyDevExperimentUri({ + devExperimentId, +}: Omit): string { + return `taler://dev-experiment/${devExperimentId}`; +} + +export function stringifyPayTemplateUri({ + merchantBaseUrl, + templateId, + templateParams, +}: Omit): string { + const { proto, path, query } = getUrlInfo(merchantBaseUrl, templateParams); + return `${proto}://pay-template/${path}${templateId}${query}`; +} +export function stringifyRefundUri({ + merchantBaseUrl, + orderId, +}: Omit): string { + const { proto, path } = getUrlInfo(merchantBaseUrl); + return `${proto}://refund/${path}${orderId}`; +} +export function stringifyTipUri({ + merchantBaseUrl, + merchantTipId, +}: Omit): string { + const { proto, path } = getUrlInfo(merchantBaseUrl); + return `${proto}://tip/${path}${merchantTipId}`; +} + +export function stringifyExchangeUri({ + exchangeBaseUrl, + exchangePub, +}: Omit): string { + const { proto, path } = getUrlInfo(exchangeBaseUrl); + return `${proto}://exchange/${path}${exchangePub}`; +} + +export function stringifyAuditorUri({ + auditorBaseUrl, + auditorPub, +}: Omit): string { + const { proto, path } = getUrlInfo(auditorBaseUrl); + return `${proto}://auditor/${path}${auditorPub}`; +} + +export function stringifyWithdrawUri({ + bankIntegrationApiBaseUrl, + withdrawalOperationId, +}: Omit): string { + const { proto, path } = getUrlInfo(bankIntegrationApiBaseUrl); + return `${proto}://withdraw/${path}${withdrawalOperationId}`; +} + +/** + * Use baseUrl to defined http or https + * create path using host+port+pathname + * use params to create a query parameter string or empty + * + * @param baseUrl + * @param params + * @returns + */ +function getUrlInfo( + baseUrl: string, + params: Record = {}, +): { proto: string; path: string; query: string } { + const url = new URL(baseUrl); 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}`); + throw Error(`Unsupported URL protocol in ${baseUrl}`); } - if (!url.pathname.endsWith("/")) { - throw Error( - `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`, - ); + let path = url.hostname; + if (url.port) { + path = path + ":" + url.port; } - 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; + if (url.pathname) { + path = path + url.pathname; } - const idx = pi.rest.indexOf("?"); - if (idx === -1) { - return undefined; + if (!path.endsWith("/")) { + path = path + "/"; } - 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 }; + + const qp = new URLSearchParams(); + let withParams = false; + Object.entries(params).forEach(([name, value]) => { + if (value) { + withParams = true; + qp.append(name, value); + } + }); + const query = withParams ? "?" + qp.toString() : ""; + + return { proto, path, query }; } -- cgit v1.2.3