implement new protocol / naming

This commit is contained in:
Florian Dold 2017-02-13 00:44:44 +01:00
parent dd5b679791
commit 08d4a5b625
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 223 additions and 166 deletions

View File

@ -90,6 +90,19 @@ namespace TalerNotify {
}); });
} }
function queryPayment(query: any): Promise<any> {
// 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<void> { function putHistory(historyEntry: any): Promise<void> {
const walletMsg = { const walletMsg = {
type: "put-history-entry", type: "put-history-entry",
@ -109,16 +122,20 @@ namespace TalerNotify {
type: "save-offer", type: "save-offer",
detail: { detail: {
offer: { offer: {
contract: offer.contract, contract: offer.data,
merchant_sig: offer.merchant_sig, merchant_sig: offer.sig,
H_contract: offer.H_contract, H_contract: offer.hash,
offer_time: new Date().getTime() / 1000 offer_time: new Date().getTime() / 1000
}, },
}, },
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(walletMsg, (resp: any) => { chrome.runtime.sendMessage(walletMsg, (resp: any) => {
if (resp && resp.error) {
reject(resp);
} else {
resolve(resp); resolve(resp);
}
}); });
}); });
} }
@ -141,17 +158,10 @@ namespace TalerNotify {
} }
}); });
if (resp && resp.type === "fetch") { if (resp && resp.type == "pay") {
logVerbose && console.log("it's fetch"); logVerbose && console.log("doing taler.pay with", resp.payDetail);
taler.internalOfferContractFrom(resp.contractUrl); taler.internalPay(resp.payDetail);
document.documentElement.style.visibility = "hidden"; 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; (detail: any, sendResponse: (msg: any) => void): void;
} }
function downloadContract(url: string): Promise<any> {
// 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() { function registerHandlers() {
/** /**
* Add a handler for a DOM event, which automatically * Add a handler for a DOM event, which automatically
@ -237,70 +345,28 @@ namespace TalerNotify {
const proposal = msg.contract_wrapper; const proposal = msg.contract_wrapper;
if (!proposal.data) { processProposal(proposal);
console.error("field proposal.data field missing"); });
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; return;
} }
if (!proposal.hash) { if (msg.offer_url) {
console.error("proposal.hash field missing"); document.location.href = msg.offer_url;
return; return;
} }
let contractHash = await hashContract(proposal.data); console.log("can't proceed with payment, no way to get contract specified");
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;
}
}
}); });
addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { addHandler("taler-payment-failed", (msg: any, sendResponse: any) => {
@ -331,41 +397,5 @@ namespace TalerNotify {
sendResponse(); 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,
});
});
});
} }
} }

View File

@ -30,7 +30,12 @@ export function prettyAmount(amount: AmountJson) {
} }
export function renderContract(contract: Contract): JSX.Element { export function renderContract(contract: Contract): JSX.Element {
let merchantName = <strong>{contract.merchant.name}</strong>; let merchantName;
if (contract.merchant && contract.merchant.name) {
merchantName = <strong>{contract.merchant.name}</strong>;
} else {
merchantName = <strong>(pub: {contract.merchant_pub})</strong>;
}
let amount = <strong>{prettyAmount(contract.amount)}</strong>; let amount = <strong>{prettyAmount(contract.amount)}</strong>;
return ( return (

View File

@ -164,20 +164,10 @@ export interface HistoryRecord {
interface PayReq { interface PayReq {
amount: AmountJson;
coins: CoinPaySig[]; coins: CoinPaySig[];
H_contract: string; merchant_pub: string;
max_fee: AmountJson; order_id: string;
merchant_sig: string;
exchange: 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 { interface TransactionRecord {
@ -352,6 +342,8 @@ export namespace Stores {
"contract.merchant_pub", "contract.merchant_pub",
"contract.repurchase_correlation_id" "contract.repurchase_correlation_id"
]); ]);
fulfillmentUrlIndex = new Index<string,TransactionRecord>(this, "fulfillment_url", "contract.fulfillment_url");
orderIdIndex = new Index<string,TransactionRecord>(this, "order_id", "contract.order_id");
} }
class DenominationsStore extends Store<DenominationRecord> { class DenominationsStore extends Store<DenominationRecord> {
@ -552,16 +544,10 @@ export class Wallet {
payCoinInfo: PayCoinInfo, payCoinInfo: PayCoinInfo,
chosenExchange: string): Promise<void> { chosenExchange: string): Promise<void> {
let payReq: PayReq = { let payReq: PayReq = {
amount: offer.contract.amount,
coins: payCoinInfo.map((x) => x.sig), coins: payCoinInfo.map((x) => x.sig),
H_contract: offer.H_contract, merchant_pub: offer.contract.merchant_pub,
max_fee: offer.contract.max_fee, order_id: offer.contract.order_id,
merchant_sig: offer.merchant_sig, exchange: chosenExchange,
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
}; };
let t: TransactionRecord = { let t: TransactionRecord = {
contractHash: offer.H_contract, contractHash: offer.H_contract,
@ -679,18 +665,36 @@ export class Wallet {
* Retrieve all necessary information for looking up the contract * Retrieve all necessary information for looking up the contract
* with the given hash. * with the given hash.
*/ */
async executePayment(H_contract: string): Promise<any> { async queryPayment(query: any): Promise<any> {
let t = await this.q().get<TransactionRecord>(Stores.transactions, let t: TransactionRecord | undefined;
H_contract);
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<TransactionRecord>(Stores.transactions, query.value);
break;
default:
throw Error("invalid type");
}
if (!t) { if (!t) {
console.log("query for payment failed");
return { return {
success: false, success: false,
contractFound: false,
} }
} }
console.log("query for payment succeeded:", t);
let resp = { let resp = {
success: true, success: true,
payReq: t.payReq, payReq: t.payReq,
H_contract: t.contractHash,
contract: t.contract, contract: t.contract,
}; };
return resp; return resp;

View File

@ -139,20 +139,20 @@ function makeHandlers(db: IDBDatabase,
} }
return wallet.checkPay(offer); return wallet.checkPay(offer);
}, },
["execute-payment"]: function (detail: any, sender: MessageSender) { ["query-payment"]: function (detail: any, sender: MessageSender) {
if (sender.tab && sender.tab.id) { if (sender.tab && sender.tab.id) {
rateLimitCache[sender.tab.id]++; rateLimitCache[sender.tab.id]++;
if (rateLimitCache[sender.tab.id] > 10) { if (rateLimitCache[sender.tab.id] > 10) {
console.warn("rate limit for execute payment exceeded"); console.warn("rate limit for query-payment exceeded");
let msg = { let msg = {
error: "rate limit exceeded for execute-payment", error: "rate limit exceeded for query-payment",
rateLimitExceeded: true, rateLimitExceeded: true,
hint: "Check for redirect loops", hint: "Check for redirect loops",
}; };
return Promise.resolve(msg); return Promise.resolve(msg);
} }
} }
return wallet.executePayment(detail.H_contract); return wallet.queryPayment(detail);
}, },
["exchange-info"]: function (detail) { ["exchange-info"]: function (detail) {
if (!detail.baseUrl) { if (!detail.baseUrl) {
@ -179,8 +179,10 @@ function makeHandlers(db: IDBDatabase,
if (!offer) { if (!offer) {
return Promise.resolve({ error: "offer missing" }); return Promise.resolve({ error: "offer missing" });
} }
console.log("handling safe-offer"); console.log("handling safe-offer", detail);
return wallet.saveOffer(offer); // FIXME: fully migrate to new terminology
let checkedOffer = OfferRecord.checked(offer);
return wallet.saveOffer(checkedOffer);
}, },
["reserve-creation-info"]: function (detail, sender) { ["reserve-creation-info"]: function (detail, sender) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") { if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
@ -317,8 +319,7 @@ class ChromeNotifier implements Notifier {
*/ */
let paymentRequestCookies: { [n: number]: any } = {}; let paymentRequestCookies: { [n: number]: any } = {};
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any {
url: string, tabId: number): any {
const headers: { [s: string]: string } = {}; const headers: { [s: string]: string } = {};
for (let kv of headerList) { for (let kv of headerList) {
if (kv.value) { if (kv.value) {
@ -326,35 +327,52 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[],
} }
} }
const contractUrl = headers["x-taler-contract-url"]; let fields = {
if (contractUrl !== undefined) { contract_url: headers["x-taler-contract-url"],
paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; contract_query: headers["x-taler-contract-query"],
return; 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) { for (let key of Object.keys(fields)) {
const payUrl = headers["x-taler-pay-url"]; if ((fields as any)[key]) {
if (payUrl === undefined) { n++;
console.log("malformed 402, X-Taler-Pay-Url missing"); }
return;
}
// Offer URL is optional
const offerUrl = headers["x-taler-offer-url"];
paymentRequestCookies[tabId] = {
type: "execute",
offerUrl,
payUrl,
contractHash
};
return;
} }
if (n == 0) {
// looks like it's not a taler request, it might be // looks like it's not a taler request, it might be
// for a different payment system (or the shop is buggy) // for a different payment system (or the shop is buggy)
console.log("ignoring non-taler 402 response"); 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,
};
} }

@ -1 +1 @@
Subproject commit d4de1c912ecaac7991067027b352de61b237c0c9 Subproject commit 4831e664d69759da288625911c053d145aa1b68c