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;
denomSig: string;
}
export interface ExtendedPermissionsResponse {
newValue: boolean;
}

View File

@ -164,6 +164,14 @@ export interface MessageMap {
request: {};
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 { createWithdrawPage } from "./pages/withdraw";
import { createWelcomePage } from "./pages/welcome";
import { createPayPage } from "./pages/pay";
function main(): void {
try {
@ -43,6 +44,9 @@ function main(): void {
case "welcome.html":
mainElement = createWelcomePage();
break;
case "pay.html":
mainElement = createPayPage();
break;
default:
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 talerPayUri = url.searchParams.get("talerPayUri");
if (!talerPayUri) {

View File

@ -34,11 +34,12 @@ import { WalletBalance, WalletBalanceEntry } from "../../types/walletTypes";
import { abbrev, renderAmount, PageLink } from "../renderHtml";
import * as wxApi from "../wxApi";
import React, { Fragment } from "react";
import React, { Fragment, useState, useEffect } from "react";
import { HistoryEvent } from "../../types/history";
import moment from "moment";
import { Timestamp } from "../../util/time";
import { classifyTalerUri, TalerUriType } from "../../util/taleruri";
// FIXME: move to newer react functions
/* 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 {
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 (
<div>
<WalletNavBar />
@ -777,6 +884,6 @@ function WalletPopup(): JSX.Element {
}
export function createPopup(): JSX.Element {
chrome.runtime.connect({ name: "popup" });
//chrome.runtime.connect({ name: "popup" });
return <WalletPopup />;
}

View File

@ -24,8 +24,9 @@ import React, { useState, useEffect } from "react";
import { getDiagnostics } from "../wxApi";
import { PageLink } from "../renderHtml";
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 [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
undefined,
@ -55,7 +56,7 @@ function Diagnostics(): JSX.Element {
if (diagnostics) {
if (diagnostics.errors.length === 0) {
return <p>Running diagnostics ... everything looks fine.</p>;
return null;
} else {
return (
<div
@ -96,16 +97,56 @@ function Diagnostics(): 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 (
<>
<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 />
<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 (
<div>
<h1>Digital Cash Withdrawal</h1>
<i18n.Translate wrap="p">
You are about to withdraw{" "}
<strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from
your bank account into your wallet.
</i18n.Translate>
{ selectedExchange ?
<p>
The exchange <strong>{selectedExchange}</strong> will be used as the Taler payment service provider.
</p> : null
}
<div>
<button
className="pure-button button-success"

View File

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

View File

@ -41,6 +41,7 @@ import {
WithdrawDetails,
PreparePayResult,
AcceptWithdrawalResponse,
ExtendedPermissionsResponse,
} from "../types/walletTypes";
import { MessageMap, MessageType } from "./messages";
@ -324,3 +325,17 @@ export function acceptWithdrawal(
export function getDiagnostics(): Promise<WalletDiagnostics> {
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 extendedPermissions = {
permissions: ["webRequest", "webRequestBlocking", "tabs"],
origins: ["http://*/*", "https://*/*"],
};
async function handleMessage(
sender: MessageSender,
type: MessageType,
@ -282,6 +287,43 @@ async function handleMessage(
}
case "prepare-pay":
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:
// Exhaustiveness check.
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@ -453,6 +495,91 @@ try {
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.
*
@ -474,79 +601,5 @@ export async function wxMain(): Promise<void> {
return true;
});
// Handlers for catching HTTP requests
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"],
);
setupHeaderListener();
}

View File

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

View File

@ -1,14 +1,15 @@
body {
font-size: 100%;
overflow-y: scroll;
margin-top: 2em;
}
#main {
border: solid 1px black;
border: solid 5px black;
border-radius: 10px;
margin-left: auto;
margin-right: auto;
margin-top: 2em;
padding-top: 2em;
max-width: 50%;
padding: 2em;
}
@ -18,16 +19,6 @@ header {
height: 100px;
margin: 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 {
@ -37,7 +28,6 @@ header #logo {
padding: 0;
margin: 0;
text-align: center;
border-right: 1px solid black;
background-image: url(/img/logo.png);
background-size: 100px;
}
@ -50,7 +40,6 @@ aside {
section#main {
margin: auto;
padding: 20px;
border-left: 1px solid black;
height: 100%;
max-width: 50%;
}
@ -61,19 +50,23 @@ section#main h1:first-child {
h1 {
font-size: 160%;
font-family: "monospace";
}
h2 {
font-size: 140%;
font-family: "monospace";
}
h3 {
font-size: 120%;
font-family: "monospace";
}
h4,
h5,
h6 {
font-family: "monospace";
font-size: 100%;
}
@ -281,3 +274,17 @@ a.opener {
object.svg-icon.svg-baseline {
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>
<head>
<meta charset="UTF-8" />
<title>Taler Wallet: Withdraw</title>
<title>Taler Wallet Installed</title>
<link rel="icon" href="/img/icon.png" />
<link rel="stylesheet" type="text/css" href="/style/pure.css" />
@ -12,7 +12,12 @@
<body>
<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>
</section>
</body>

View File

@ -11,7 +11,11 @@
<body>
<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>
</section>
</body>