From 08d4a5b62532f867d3af67d8b8ad72921d02412a Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 13 Feb 2017 00:44:44 +0100 Subject: [PATCH] implement new protocol / naming --- src/content_scripts/notify.ts | 246 +++++++++++++++++++--------------- src/renderHtml.tsx | 7 +- src/wallet.ts | 54 ++++---- src/wxBackend.ts | 80 ++++++----- web-common | 2 +- 5 files changed, 223 insertions(+), 166 deletions(-) diff --git a/src/content_scripts/notify.ts b/src/content_scripts/notify.ts index fda5d3fec..10b988c43 100644 --- a/src/content_scripts/notify.ts +++ b/src/content_scripts/notify.ts @@ -90,6 +90,19 @@ namespace TalerNotify { }); } + function queryPayment(query: any): Promise { + // current URL without fragment + const walletMsg = { + type: "query-payment", + detail: query, + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(resp); + }); + }); + } + function putHistory(historyEntry: any): Promise { const walletMsg = { type: "put-history-entry", @@ -109,16 +122,20 @@ namespace TalerNotify { type: "save-offer", detail: { offer: { - contract: offer.contract, - merchant_sig: offer.merchant_sig, - H_contract: offer.H_contract, + contract: offer.data, + merchant_sig: offer.sig, + H_contract: offer.hash, offer_time: new Date().getTime() / 1000 }, }, }; return new Promise((resolve, reject) => { chrome.runtime.sendMessage(walletMsg, (resp: any) => { - resolve(resp); + if (resp && resp.error) { + reject(resp); + } else { + resolve(resp); + } }); }); } @@ -141,17 +158,10 @@ namespace TalerNotify { } }); - if (resp && resp.type === "fetch") { - logVerbose && console.log("it's fetch"); - taler.internalOfferContractFrom(resp.contractUrl); + if (resp && resp.type == "pay") { + logVerbose && console.log("doing taler.pay with", resp.payDetail); + taler.internalPay(resp.payDetail); document.documentElement.style.visibility = "hidden"; - - } else if (resp && resp.type === "execute") { - logVerbose && console.log("it's execute"); - document.documentElement.style.visibility = "hidden"; - taler.internalExecutePayment(resp.contractHash, - resp.payUrl, - resp.offerUrl); } }); } @@ -163,6 +173,104 @@ namespace TalerNotify { (detail: any, sendResponse: (msg: any) => void): void; } + function downloadContract(url: string): Promise { + // FIXME: include and check nonce! + return new Promise((resolve, reject) => { + const contract_request = new XMLHttpRequest(); + console.log("downloading contract from '" + url + "'") + contract_request.open("GET", url, true); + contract_request.onload = function (e) { + if (contract_request.readyState == 4) { + if (contract_request.status == 200) { + console.log("response text:", + contract_request.responseText); + var contract_wrapper = JSON.parse(contract_request.responseText); + if (!contract_wrapper) { + console.error("response text was invalid json"); + let detail = {hint: "invalid json", status: contract_request.status, body: contract_request.responseText}; + reject(detail); + return; + } + resolve(contract_wrapper); + } else { + let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; + reject(detail); + return; + } + } + }; + contract_request.onerror = function (e) { + let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; + reject(detail); + return; + }; + contract_request.send(); + }); + } + + async function processProposal(proposal: any) { + if (!proposal.data) { + console.error("field proposal.data field missing"); + return; + } + + if (!proposal.hash) { + console.error("proposal.hash field missing"); + return; + } + + let contractHash = await hashContract(proposal.data); + + if (contractHash != proposal.hash) { + console.error("merchant-supplied contract hash is wrong"); + return; + } + + let resp = await checkRepurchase(proposal.data); + + if (resp.error) { + console.error("wallet backend error", resp); + return; + } + + if (resp.isRepurchase) { + logVerbose && console.log("doing repurchase"); + console.assert(resp.existingFulfillmentUrl); + console.assert(resp.existingContractHash); + window.location.href = subst(resp.existingFulfillmentUrl, + resp.existingContractHash); + + } else { + + let merchantName = "(unknown)"; + try { + merchantName = proposal.data.merchant.name; + } catch (e) { + // bad contract / name not included + } + + let historyEntry = { + timestamp: (new Date).getTime(), + subjectId: `contract-${contractHash}`, + type: "offer-contract", + detail: { + contractHash, + merchantName, + } + }; + await putHistory(historyEntry); + let offerId = await saveOffer(proposal); + + const uri = URI(chrome.extension.getURL( + "/src/pages/confirm-contract.html")); + const params = { + offerId: offerId.toString(), + }; + const target = uri.query(params).href(); + document.location.replace(target); + } + } + function registerHandlers() { /** * Add a handler for a DOM event, which automatically @@ -237,70 +345,28 @@ namespace TalerNotify { const proposal = msg.contract_wrapper; - if (!proposal.data) { - console.error("field proposal.data field missing"); + processProposal(proposal); + }); + + addHandler("taler-pay", async(msg: any, sendResponse: any) => { + let res = await queryPayment(msg.contract_query); + logVerbose && console.log("taler-pay: got response", res); + if (res && res.payReq) { + sendResponse(res); + return; + } + if (msg.contract_url) { + let proposal = await downloadContract(msg.contract_url); + await processProposal(proposal); return; } - if (!proposal.hash) { - console.error("proposal.hash field missing"); + if (msg.offer_url) { + document.location.href = msg.offer_url; return; } - let contractHash = await hashContract(proposal.data); - - if (contractHash != proposal.hash) { - console.error("merchant-supplied contract hash is wrong"); - return; - } - - let resp = await checkRepurchase(proposal.data); - - if (resp.error) { - console.error("wallet backend error", resp); - return; - } - - if (resp.isRepurchase) { - logVerbose && console.log("doing repurchase"); - console.assert(resp.existingFulfillmentUrl); - console.assert(resp.existingContractHash); - window.location.href = subst(resp.existingFulfillmentUrl, - resp.existingContractHash); - - } else { - - let merchantName = "(unknown)"; - try { - merchantName = proposal.data.merchant.name; - } catch (e) { - // bad contract / name not included - } - - let historyEntry = { - timestamp: (new Date).getTime(), - subjectId: `contract-${contractHash}`, - type: "offer-contract", - detail: { - contractHash, - merchantName, - } - }; - await putHistory(historyEntry); - let offerId = await saveOffer(proposal); - - const uri = URI(chrome.extension.getURL( - "/src/pages/confirm-contract.html")); - const params = { - offerId: offerId.toString(), - }; - const target = uri.query(params).href(); - if (msg.replace_navigation === true) { - document.location.replace(target); - } else { - document.location.href = target; - } - } + console.log("can't proceed with payment, no way to get contract specified"); }); addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { @@ -331,41 +397,5 @@ namespace TalerNotify { sendResponse(); }) }); - - addHandler("taler-get-payment", (msg: any, sendResponse: any) => { - const walletMsg = { - type: "execute-payment", - detail: { - H_contract: msg.H_contract, - }, - }; - - chrome.runtime.sendMessage(walletMsg, (resp) => { - if (resp.rateLimitExceeded) { - console.error("rate limit exceeded, check for redirect loops"); - } - - if (!resp.success) { - if (msg.offering_url) { - window.location.href = msg.offering_url; - } else { - console.error("execute-payment failed", resp); - } - return; - } - let contract = resp.contract; - if (!contract) { - throw Error("contract missing"); - } - - // We have the details for then payment, the merchant page - // is responsible to give it to the merchant. - sendResponse({ - H_contract: msg.H_contract, - contract: resp.contract, - payment: resp.payReq, - }); - }); - }); } } diff --git a/src/renderHtml.tsx b/src/renderHtml.tsx index 40b48094e..79e101b17 100644 --- a/src/renderHtml.tsx +++ b/src/renderHtml.tsx @@ -30,7 +30,12 @@ export function prettyAmount(amount: AmountJson) { } export function renderContract(contract: Contract): JSX.Element { - let merchantName = {contract.merchant.name}; + let merchantName; + if (contract.merchant && contract.merchant.name) { + merchantName = {contract.merchant.name}; + } else { + merchantName = (pub: {contract.merchant_pub}); + } let amount = {prettyAmount(contract.amount)}; return ( diff --git a/src/wallet.ts b/src/wallet.ts index 988ed32df..1c9de0170 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -164,20 +164,10 @@ export interface HistoryRecord { interface PayReq { - amount: AmountJson; coins: CoinPaySig[]; - H_contract: string; - max_fee: AmountJson; - merchant_sig: string; + merchant_pub: string; + order_id: string; exchange: string; - refund_deadline: string; - timestamp: string; - pay_deadline: string; - /** - * Merchant instance identifier that should receive the - * payment, if applicable. - */ - instance?: string; } interface TransactionRecord { @@ -352,6 +342,8 @@ export namespace Stores { "contract.merchant_pub", "contract.repurchase_correlation_id" ]); + fulfillmentUrlIndex = new Index(this, "fulfillment_url", "contract.fulfillment_url"); + orderIdIndex = new Index(this, "order_id", "contract.order_id"); } class DenominationsStore extends Store { @@ -552,16 +544,10 @@ export class Wallet { payCoinInfo: PayCoinInfo, chosenExchange: string): Promise { let payReq: PayReq = { - amount: offer.contract.amount, coins: payCoinInfo.map((x) => x.sig), - H_contract: offer.H_contract, - max_fee: offer.contract.max_fee, - merchant_sig: offer.merchant_sig, - exchange: URI(chosenExchange).href(), - refund_deadline: offer.contract.refund_deadline, - pay_deadline: offer.contract.pay_deadline, - timestamp: offer.contract.timestamp, - instance: offer.contract.merchant.instance + merchant_pub: offer.contract.merchant_pub, + order_id: offer.contract.order_id, + exchange: chosenExchange, }; let t: TransactionRecord = { contractHash: offer.H_contract, @@ -679,18 +665,36 @@ export class Wallet { * Retrieve all necessary information for looking up the contract * with the given hash. */ - async executePayment(H_contract: string): Promise { - let t = await this.q().get(Stores.transactions, - H_contract); + async queryPayment(query: any): Promise { + let t: TransactionRecord | undefined; + + console.log("query for payment", query); + + switch (query.type) { + case "fulfillment_url": + t = await this.q().getIndexed(Stores.transactions.fulfillmentUrlIndex, query.value); + break; + case "order_id": + t = await this.q().getIndexed(Stores.transactions.orderIdIndex, query.value); + break; + case "hash": + t = await this.q().get(Stores.transactions, query.value); + break; + default: + throw Error("invalid type"); + } + if (!t) { + console.log("query for payment failed"); return { success: false, - contractFound: false, } } + console.log("query for payment succeeded:", t); let resp = { success: true, payReq: t.payReq, + H_contract: t.contractHash, contract: t.contract, }; return resp; diff --git a/src/wxBackend.ts b/src/wxBackend.ts index 637ab5d0e..50e068946 100644 --- a/src/wxBackend.ts +++ b/src/wxBackend.ts @@ -139,20 +139,20 @@ function makeHandlers(db: IDBDatabase, } return wallet.checkPay(offer); }, - ["execute-payment"]: function (detail: any, sender: MessageSender) { + ["query-payment"]: function (detail: any, sender: MessageSender) { if (sender.tab && sender.tab.id) { rateLimitCache[sender.tab.id]++; if (rateLimitCache[sender.tab.id] > 10) { - console.warn("rate limit for execute payment exceeded"); + console.warn("rate limit for query-payment exceeded"); let msg = { - error: "rate limit exceeded for execute-payment", + error: "rate limit exceeded for query-payment", rateLimitExceeded: true, hint: "Check for redirect loops", }; return Promise.resolve(msg); } } - return wallet.executePayment(detail.H_contract); + return wallet.queryPayment(detail); }, ["exchange-info"]: function (detail) { if (!detail.baseUrl) { @@ -179,8 +179,10 @@ function makeHandlers(db: IDBDatabase, if (!offer) { return Promise.resolve({ error: "offer missing" }); } - console.log("handling safe-offer"); - return wallet.saveOffer(offer); + console.log("handling safe-offer", detail); + // FIXME: fully migrate to new terminology + let checkedOffer = OfferRecord.checked(offer); + return wallet.saveOffer(checkedOffer); }, ["reserve-creation-info"]: function (detail, sender) { if (!detail.baseUrl || typeof detail.baseUrl !== "string") { @@ -317,8 +319,7 @@ class ChromeNotifier implements Notifier { */ let paymentRequestCookies: { [n: number]: any } = {}; -function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { +function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any { const headers: { [s: string]: string } = {}; for (let kv of headerList) { if (kv.value) { @@ -326,35 +327,52 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], } } - const contractUrl = headers["x-taler-contract-url"]; - if (contractUrl !== undefined) { - paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; - return; + let fields = { + contract_url: headers["x-taler-contract-url"], + contract_query: headers["x-taler-contract-query"], + offer_url: headers["x-taler-offer-url"], + pay_url: headers["x-taler-pay-url"], } - const contractHash = headers["x-taler-contract-hash"]; + let n: number = 0; - if (contractHash !== undefined) { - const payUrl = headers["x-taler-pay-url"]; - if (payUrl === undefined) { - console.log("malformed 402, X-Taler-Pay-Url missing"); - return; + for (let key of Object.keys(fields)) { + if ((fields as any)[key]) { + n++; } - - // Offer URL is optional - const offerUrl = headers["x-taler-offer-url"]; - paymentRequestCookies[tabId] = { - type: "execute", - offerUrl, - payUrl, - contractHash - }; - return; } - // looks like it's not a taler request, it might be - // for a different payment system (or the shop is buggy) - console.log("ignoring non-taler 402 response"); + if (n == 0) { + // looks like it's not a taler request, it might be + // for a different payment system (or the shop is buggy) + console.log("ignoring non-taler 402 response"); + } + + let contract_query = undefined; + // parse " type [ ':' value ] " format + if (fields.contract_query) { + let res = /[-a-zA-Z0-9_.,]+(:.*)?/.exec(fields.contract_query); + if (res) { + contract_query = {type: res[0], value: res[1]}; + if (contract_query.type == "fulfillment_url" && !contract_query.value) { + contract_query.value = url; + } + } + } + + let payDetail = { + contract_query, + contract_url: fields.contract_url, + offer_url: fields.offer_url, + pay_url: fields.pay_url, + }; + + console.log("got pay detail", payDetail) + + paymentRequestCookies[tabId] = { + type: "pay", + payDetail, + }; } diff --git a/web-common b/web-common index d4de1c912..4831e664d 160000 --- a/web-common +++ b/web-common @@ -1 +1 @@ -Subproject commit d4de1c912ecaac7991067027b352de61b237c0c9 +Subproject commit 4831e664d69759da288625911c053d145aa1b68c