drastically reduce permissions for Web integration

The old web integration with more permissions is still available on an
opt-in basis.
This commit is contained in:
Florian Dold 2020-05-01 14:16:56 +05:30
parent 3f52d293be
commit 609397d95a
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
14 changed files with 365 additions and 115 deletions

View File

@ -475,3 +475,8 @@ export interface DepositInfo {
denomPub: string; denomPub: string;
denomSig: string; denomSig: string;
} }
export interface ExtendedPermissionsResponse {
newValue: boolean;
}

View File

@ -164,6 +164,14 @@ export interface MessageMap {
request: {}; request: {};
response: walletTypes.WalletDiagnostics; response: walletTypes.WalletDiagnostics;
}; };
"set-extended-permissions": {
request: { value: boolean };
response: walletTypes.ExtendedPermissionsResponse;
};
"get-extended-permissions": {
request: { };
response: walletTypes.ExtendedPermissionsResponse;
};
} }
/** /**

View File

@ -24,6 +24,7 @@ import ReactDOM from "react-dom";
import { createPopup } from "./pages/popup"; import { createPopup } from "./pages/popup";
import { createWithdrawPage } from "./pages/withdraw"; import { createWithdrawPage } from "./pages/withdraw";
import { createWelcomePage } from "./pages/welcome"; import { createWelcomePage } from "./pages/welcome";
import { createPayPage } from "./pages/pay";
function main(): void { function main(): void {
try { try {
@ -43,6 +44,9 @@ function main(): void {
case "welcome.html": case "welcome.html":
mainElement = createWelcomePage(); mainElement = createWelcomePage();
break; break;
case "pay.html":
mainElement = createPayPage();
break;
default: default:
throw Error(`page '${page}' not implemented`); throw Error(`page '${page}' not implemented`);
} }

View File

@ -178,7 +178,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
); );
} }
export function makePayPage(): JSX.Element { export function createPayPage(): JSX.Element {
const url = new URL(document.location.href); const url = new URL(document.location.href);
const talerPayUri = url.searchParams.get("talerPayUri"); const talerPayUri = url.searchParams.get("talerPayUri");
if (!talerPayUri) { if (!talerPayUri) {

View File

@ -34,11 +34,12 @@ import { WalletBalance, WalletBalanceEntry } from "../../types/walletTypes";
import { abbrev, renderAmount, PageLink } from "../renderHtml"; import { abbrev, renderAmount, PageLink } from "../renderHtml";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
import React, { Fragment } from "react"; import React, { Fragment, useState, useEffect } from "react";
import { HistoryEvent } from "../../types/history"; import { HistoryEvent } from "../../types/history";
import moment from "moment"; import moment from "moment";
import { Timestamp } from "../../util/time"; import { Timestamp } from "../../util/time";
import { classifyTalerUri, TalerUriType } from "../../util/taleruri";
// FIXME: move to newer react functions // FIXME: move to newer react functions
/* eslint-disable react/no-deprecated */ /* eslint-disable react/no-deprecated */
@ -761,7 +762,113 @@ function openTab(page: string) {
}; };
} }
function makeExtensionUrlWithParams(
url: string,
params?: { [name: string]: string | undefined },
): string {
const innerUrl = new URL(chrome.extension.getURL("/" + url));
if (params) {
for (const key in params) {
const p = params[key];
if (p) {
innerUrl.searchParams.set(key, p);
}
}
}
return innerUrl.href;
}
function actionForTalerUri(talerUri: string): string | undefined {
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeExtensionUrlWithParams("withdraw.html", {
talerWithdrawUri: talerUri,
});
case TalerUriType.TalerPay:
return makeExtensionUrlWithParams("pay.html", {
talerPayUri: talerUri,
});
case TalerUriType.TalerTip:
return makeExtensionUrlWithParams("tip.html", {
talerTipUri: talerUri,
});
case TalerUriType.TalerRefund:
return makeExtensionUrlWithParams("refund.html", {
talerRefundUri: talerUri,
});
case TalerUriType.TalerNotifyReserve:
// FIXME: implement
break;
default:
console.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
);
break;
}
return undefined;
}
async function findTalerUriInActiveTab(): Promise<string | undefined> {
return new Promise((resolve, reject) => {
chrome.tabs.executeScript(
{
code: `
(() => {
let x = document.querySelector("a[href^='taler://'");
return x ? x.href.toString() : null;
})();
`,
allFrames: false,
},
(result) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
resolve(undefined);
return;
}
console.log("got result", result);
resolve(result[0]);
},
);
});
}
function WalletPopup(): JSX.Element { function WalletPopup(): JSX.Element {
const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
undefined,
);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
async function check(): Promise<void> {
const talerUri = await findTalerUriInActiveTab();
if (talerUri) {
const actionUrl = actionForTalerUri(talerUri);
setTalerActionUrl(actionUrl);
}
}
check();
});
if (talerActionUrl && !dismissed) {
return (
<div style={{ padding: "1em" }}>
<h1>Taler Action</h1>
<p>This page has a Taler action. </p>
<p>
<button
onClick={() => {
window.open(talerActionUrl, "_blank");
}}
>
Open
</button>
</p>
<p>
<button onClick={() => setDismissed(true)}>Dismiss</button>
</p>
</div>
);
}
return ( return (
<div> <div>
<WalletNavBar /> <WalletNavBar />
@ -777,6 +884,6 @@ function WalletPopup(): JSX.Element {
} }
export function createPopup(): JSX.Element { export function createPopup(): JSX.Element {
chrome.runtime.connect({ name: "popup" }); //chrome.runtime.connect({ name: "popup" });
return <WalletPopup />; return <WalletPopup />;
} }

View File

@ -24,8 +24,9 @@ import React, { useState, useEffect } from "react";
import { getDiagnostics } from "../wxApi"; import { getDiagnostics } from "../wxApi";
import { PageLink } from "../renderHtml"; import { PageLink } from "../renderHtml";
import { WalletDiagnostics } from "../../types/walletTypes"; import { WalletDiagnostics } from "../../types/walletTypes";
import * as wxApi from "../wxApi";
function Diagnostics(): JSX.Element { function Diagnostics(): JSX.Element | null {
const [timedOut, setTimedOut] = useState(false); const [timedOut, setTimedOut] = useState(false);
const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
undefined, undefined,
@ -55,7 +56,7 @@ function Diagnostics(): JSX.Element {
if (diagnostics) { if (diagnostics) {
if (diagnostics.errors.length === 0) { if (diagnostics.errors.length === 0) {
return <p>Running diagnostics ... everything looks fine.</p>; return null;
} else { } else {
return ( return (
<div <div
@ -96,16 +97,56 @@ function Diagnostics(): JSX.Element {
} }
function Welcome(): JSX.Element { function Welcome(): JSX.Element {
const [extendedPermissions, setExtendedPermissions] = useState(false);
async function handleExtendedPerm(newVal: boolean): Promise<void> {
const res = await wxApi.setExtendedPermissions(newVal);
setExtendedPermissions(res.newValue);
}
useEffect(() => {
async function getExtendedPermValue(): Promise<void> {
const res = await wxApi.getExtendedPermissions()
setExtendedPermissions(res.newValue);
}
getExtendedPermValue();
});
return ( return (
<> <>
<p>Thank you for installing the wallet.</p> <p>Thank you for installing the wallet.</p>
<h2>First Steps</h2>
<p>
Check out <a href="https://demo.taler.net/">demo.taler.net</a> for a
demo.
</p>
<h2>Troubleshooting</h2>
<Diagnostics /> <Diagnostics />
<h2>Permissions</h2>
<div>
<input
checked={extendedPermissions}
onChange={(x) => handleExtendedPerm(x.target.checked)}
type="checkbox"
id="checkbox-perm"
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
/>
<label
htmlFor="checkbox-perm"
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
>
Automatically open wallet based on page content
</label>
<span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
(Enabling this option below will make using the wallet faster, but
requires more permissions from your browser.)
</span>
</div>
<h2>Next Steps</h2>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Try the demo »
</a>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Learn how to top up your wallet balance »
</a>
</> </>
); );
} }

View File

@ -160,11 +160,18 @@ function NewExchangeSelection(props: {
return ( return (
<div> <div>
<h1>Digital Cash Withdrawal</h1>
<i18n.Translate wrap="p"> <i18n.Translate wrap="p">
You are about to withdraw{" "} You are about to withdraw{" "}
<strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from <strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from
your bank account into your wallet. your bank account into your wallet.
</i18n.Translate> </i18n.Translate>
{ selectedExchange ?
<p>
The exchange <strong>{selectedExchange}</strong> will be used as the Taler payment service provider.
</p> : null
}
<div> <div>
<button <button
className="pure-button button-success" className="pure-button button-success"

View File

@ -109,6 +109,7 @@ export class Collapsible extends React.Component<
return ( return (
<h2> <h2>
<a className="opener opener-collapsed" href="#" onClick={doOpen}> <a className="opener opener-collapsed" href="#" onClick={doOpen}>
{" "}
{this.props.title} {this.props.title}
</a> </a>
</h2> </h2>
@ -118,6 +119,7 @@ export class Collapsible extends React.Component<
<div> <div>
<h2> <h2>
<a className="opener opener-open" href="#" onClick={doClose}> <a className="opener opener-open" href="#" onClick={doClose}>
{" "}
{this.props.title} {this.props.title}
</a> </a>
</h2> </h2>
@ -143,7 +145,6 @@ function WireFee(props: {
<th>Closing Fee</th> <th>Closing Fee</th>
</tr> </tr>
</thead> </thead>
,
<tbody> <tbody>
{props.rci.wireFees.feesForType[props.s].map((f) => ( {props.rci.wireFees.feesForType[props.s].map((f) => (
<tr key={f.sig}> <tr key={f.sig}>
@ -153,7 +154,6 @@ function WireFee(props: {
</tr> </tr>
))} ))}
</tbody> </tbody>
,
</> </>
); );
} }

View File

@ -41,6 +41,7 @@ import {
WithdrawDetails, WithdrawDetails,
PreparePayResult, PreparePayResult,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
ExtendedPermissionsResponse,
} from "../types/walletTypes"; } from "../types/walletTypes";
import { MessageMap, MessageType } from "./messages"; import { MessageMap, MessageType } from "./messages";
@ -324,3 +325,17 @@ export function acceptWithdrawal(
export function getDiagnostics(): Promise<WalletDiagnostics> { export function getDiagnostics(): Promise<WalletDiagnostics> {
return callBackend("get-diagnostics", {}); return callBackend("get-diagnostics", {});
} }
/**
* Get diagnostics information
*/
export function setExtendedPermissions(value: boolean): Promise<ExtendedPermissionsResponse> {
return callBackend("set-extended-permissions", { value });
}
/**
* Get diagnostics information
*/
export function getExtendedPermissions(): Promise<ExtendedPermissionsResponse> {
return callBackend("get-extended-permissions", {});
}

View File

@ -63,6 +63,11 @@ let outdatedDbVersion: number | undefined;
const walletInit: OpenedPromise<void> = openPromise<void>(); const walletInit: OpenedPromise<void> = openPromise<void>();
const extendedPermissions = {
permissions: ["webRequest", "webRequestBlocking", "tabs"],
origins: ["http://*/*", "https://*/*"],
};
async function handleMessage( async function handleMessage(
sender: MessageSender, sender: MessageSender,
type: MessageType, type: MessageType,
@ -282,6 +287,43 @@ async function handleMessage(
} }
case "prepare-pay": case "prepare-pay":
return needsWallet().preparePayForUri(detail.talerPayUri); return needsWallet().preparePayForUri(detail.talerPayUri);
case "set-extended-permissions": {
const newVal = detail.value;
if (newVal) {
const res = await new Promise((resolve, reject) => {
chrome.permissions.request(
extendedPermissions,
(granted: boolean) => {
console.log("permissions granted:", granted);
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
resolve(granted);
},
);
});
if (res) {
setupHeaderListener();
}
return { newValue: res };
} else {
await new Promise((resolve, reject) => {
chrome.permissions.remove(extendedPermissions, (rem) => {
console.log("permissions removed:", rem);
resolve();
});
});
return { newVal: false };
}
}
case "get-extended-permissions": {
const res = await new Promise((resolve, reject) => {
chrome.permissions.contains(extendedPermissions, (result: boolean) => {
resolve(result);
});
});
return { newValue: res };
}
default: default:
// Exhaustiveness check. // Exhaustiveness check.
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html // See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@ -453,6 +495,91 @@ try {
console.error(e); console.error(e);
} }
function headerListener(
details: chrome.webRequest.WebResponseHeadersDetails,
): chrome.webRequest.BlockingResponse | undefined {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
const wallet = currentWallet;
if (!wallet) {
console.warn("wallet not available while handling header");
return;
}
if (details.statusCode === 402 || details.statusCode === 202) {
console.log(`got 402/202 from ${details.url}`);
for (const header of details.responseHeaders || []) {
if (header.name.toLowerCase() === "taler") {
const talerUri = header.value || "";
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeSyncWalletRedirect(
"withdraw.html",
details.tabId,
details.url,
{
talerWithdrawUri: talerUri,
},
);
case TalerUriType.TalerPay:
return makeSyncWalletRedirect(
"pay.html",
details.tabId,
details.url,
{
talerPayUri: talerUri,
},
);
case TalerUriType.TalerTip:
return makeSyncWalletRedirect(
"tip.html",
details.tabId,
details.url,
{
talerTipUri: talerUri,
},
);
case TalerUriType.TalerRefund:
return makeSyncWalletRedirect(
"refund.html",
details.tabId,
details.url,
{
talerRefundUri: talerUri,
},
);
case TalerUriType.TalerNotifyReserve:
Promise.resolve().then(() => {
const w = currentWallet;
if (!w) {
return;
}
w.handleNotifyReserve();
});
break;
default:
console.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
);
break;
}
}
}
}
return;
}
function setupHeaderListener(): void {
// Handlers for catching HTTP requests
chrome.webRequest.onHeadersReceived.addListener(
headerListener,
{ urls: ["https://*/*", "http://*/*"] },
["responseHeaders", "blocking"],
);
}
/** /**
* Main function to run for the WebExtension backend. * Main function to run for the WebExtension backend.
* *
@ -474,79 +601,5 @@ export async function wxMain(): Promise<void> {
return true; return true;
}); });
// Handlers for catching HTTP requests setupHeaderListener();
chrome.webRequest.onHeadersReceived.addListener(
(details) => {
const wallet = currentWallet;
if (!wallet) {
console.warn("wallet not available while handling header");
return;
}
if (details.statusCode === 402 || details.statusCode === 202) {
console.log(`got 402/202 from ${details.url}`);
for (const header of details.responseHeaders || []) {
if (header.name.toLowerCase() === "taler") {
const talerUri = header.value || "";
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeSyncWalletRedirect(
"withdraw.html",
details.tabId,
details.url,
{
talerWithdrawUri: talerUri,
},
);
case TalerUriType.TalerPay:
return makeSyncWalletRedirect(
"pay.html",
details.tabId,
details.url,
{
talerPayUri: talerUri,
},
);
case TalerUriType.TalerTip:
return makeSyncWalletRedirect(
"tip.html",
details.tabId,
details.url,
{
talerTipUri: talerUri,
},
);
case TalerUriType.TalerRefund:
return makeSyncWalletRedirect(
"refund.html",
details.tabId,
details.url,
{
talerRefundUri: talerUri,
},
);
case TalerUriType.TalerNotifyReserve:
Promise.resolve().then(() => {
const w = currentWallet;
if (!w) {
return;
}
w.handleNotifyReserve();
});
break;
default:
console.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
);
break;
}
}
}
}
return;
},
{ urls: ["https://*/*", "http://*/*"] },
["responseHeaders", "blocking"],
);
} }

View File

@ -24,6 +24,10 @@
"permissions": [ "permissions": [
"storage", "storage",
"activeTab"
],
"optional_permissions": [
"tabs", "tabs",
"webRequest", "webRequest",
"webRequestBlocking", "webRequestBlocking",
@ -39,16 +43,6 @@
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"content_scripts": [
{
"matches": ["*://*/*"],
"js": [
"contentScript.js"
],
"run_at": "document_start"
}
],
"background": { "background": {
"page": "background.html", "page": "background.html",
"persistent": true "persistent": true

View File

@ -1,14 +1,15 @@
body { body {
font-size: 100%; font-size: 100%;
overflow-y: scroll; overflow-y: scroll;
margin-top: 2em;
} }
#main { #main {
border: solid 1px black; border: solid 5px black;
border-radius: 10px; border-radius: 10px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 2em; padding-top: 2em;
max-width: 50%; max-width: 50%;
padding: 2em; padding: 2em;
} }
@ -18,16 +19,6 @@ header {
height: 100px; height: 100px;
margin: 0; margin: 0;
padding: 0; padding: 0;
border-bottom: 1px solid black;
}
header h1 {
font-size: 200%;
margin: 0;
padding: 0 0 0 120px;
position: relative;
top: 50%;
transform: translateY(-50%);
} }
header #logo { header #logo {
@ -37,7 +28,6 @@ header #logo {
padding: 0; padding: 0;
margin: 0; margin: 0;
text-align: center; text-align: center;
border-right: 1px solid black;
background-image: url(/img/logo.png); background-image: url(/img/logo.png);
background-size: 100px; background-size: 100px;
} }
@ -50,7 +40,6 @@ aside {
section#main { section#main {
margin: auto; margin: auto;
padding: 20px; padding: 20px;
border-left: 1px solid black;
height: 100%; height: 100%;
max-width: 50%; max-width: 50%;
} }
@ -61,19 +50,23 @@ section#main h1:first-child {
h1 { h1 {
font-size: 160%; font-size: 160%;
font-family: "monospace";
} }
h2 { h2 {
font-size: 140%; font-size: 140%;
font-family: "monospace";
} }
h3 { h3 {
font-size: 120%; font-size: 120%;
font-family: "monospace";
} }
h4, h4,
h5, h5,
h6 { h6 {
font-family: "monospace";
font-size: 100%; font-size: 100%;
} }
@ -281,3 +274,17 @@ a.opener {
object.svg-icon.svg-baseline { object.svg-icon.svg-baseline {
transform: translate(0, 0.125em); transform: translate(0, 0.125em);
} }
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Taler Wallet: Withdraw</title> <title>Taler Wallet Installed</title>
<link rel="icon" href="/img/icon.png" /> <link rel="icon" href="/img/icon.png" />
<link rel="stylesheet" type="text/css" href="/style/pure.css" /> <link rel="stylesheet" type="text/css" href="/style/pure.css" />
@ -12,7 +12,12 @@
<body> <body>
<section id="main"> <section id="main">
<h1>GNU Taler Wallet Installed!</h1> <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
<h1 style="font-family: monospace; font-size: 250%;">
<span style="color: #aa3939;"></span>Taler Wallet<span style="color: #aa3939;"></span>
</h1>
</div>
<h1>Browser Extension Installed!</h1>
<div id="container">Loading...</div> <div id="container">Loading...</div>
</section> </section>
</body> </body>

View File

@ -11,7 +11,11 @@
<body> <body>
<section id="main"> <section id="main">
<h1>GNU Taler Wallet</h1> <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
<h1 style="font-family: monospace; font-size: 250%;">
<span style="color: #aa3939;"></span>Taler Wallet<span style="color: #aa3939;"></span>
</h1>
</div>
<div class="fade" id="container"></div> <div class="fade" id="container"></div>
</section> </section>
</body> </body>