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> {
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<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() {
/**
* 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,
});
});
});
}
}

View File

@ -30,7 +30,12 @@ export function prettyAmount(amount: AmountJson) {
}
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>;
return (

View File

@ -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<string,TransactionRecord>(this, "fulfillment_url", "contract.fulfillment_url");
orderIdIndex = new Index<string,TransactionRecord>(this, "order_id", "contract.order_id");
}
class DenominationsStore extends Store<DenominationRecord> {
@ -552,16 +544,10 @@ export class Wallet {
payCoinInfo: PayCoinInfo,
chosenExchange: string): Promise<void> {
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<any> {
let t = await this.q().get<TransactionRecord>(Stores.transactions,
H_contract);
async queryPayment(query: any): Promise<any> {
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<TransactionRecord>(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;

View File

@ -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,
};
}

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