aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-05-01 14:16:56 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-05-01 14:16:56 +0530
commit609397d95a73bdae55de41c47b19932e810d0320 (patch)
tree34fb9168eb25567c2d14daa5f69301d6932d58c1
parent3f52d293be88f19e8e68aaa8ee6a80cd6c7cc47a (diff)
drastically reduce permissions for Web integration
The old web integration with more permissions is still available on an opt-in basis.
-rw-r--r--src/types/walletTypes.ts5
-rw-r--r--src/webex/messages.ts8
-rw-r--r--src/webex/pageEntryPoint.ts4
-rw-r--r--src/webex/pages/pay.tsx2
-rw-r--r--src/webex/pages/popup.tsx111
-rw-r--r--src/webex/pages/welcome.tsx57
-rw-r--r--src/webex/pages/withdraw.tsx7
-rw-r--r--src/webex/renderHtml.tsx4
-rw-r--r--src/webex/wxApi.ts15
-rw-r--r--src/webex/wxBackend.ts203
-rw-r--r--webextension/manifest.json14
-rw-r--r--webextension/static/style/wallet.css35
-rw-r--r--webextension/static/welcome.html9
-rw-r--r--webextension/static/withdraw.html6
14 files changed, 365 insertions, 115 deletions
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index 113a137ca..ed334bc47 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -475,3 +475,8 @@ export interface DepositInfo {
denomPub: string;
denomSig: string;
}
+
+
+export interface ExtendedPermissionsResponse {
+ newValue: boolean;
+} \ No newline at end of file
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 745e309c7..179eec88a 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -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;
+ };
}
/**
diff --git a/src/webex/pageEntryPoint.ts b/src/webex/pageEntryPoint.ts
index dd9c13031..b9bdba06e 100644
--- a/src/webex/pageEntryPoint.ts
+++ b/src/webex/pageEntryPoint.ts
@@ -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`);
}
diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx
index 61f287708..a69b6b766 100644
--- a/src/webex/pages/pay.tsx
+++ b/src/webex/pages/pay.tsx
@@ -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) {
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 0fd2477f6..f6d95e2f9 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -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 />;
}
diff --git a/src/webex/pages/welcome.tsx b/src/webex/pages/welcome.tsx
index eecbe2be5..5092d2dd8 100644
--- a/src/webex/pages/welcome.tsx
+++ b/src/webex/pages/welcome.tsx
@@ -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>
</>
);
}
diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx
index efd0adc86..1647a7065 100644
--- a/src/webex/pages/withdraw.tsx
+++ b/src/webex/pages/withdraw.tsx
@@ -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"
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index b1363abfb..a56af37fc 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -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>
- ,
</>
);
}
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 07b223c87..128041e57 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -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", {});
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 6bd87b456..17e5215f4 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -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();
}
diff --git a/webextension/manifest.json b/webextension/manifest.json
index 5bcb8c060..7592b3509 100644
--- a/webextension/manifest.json
+++ b/webextension/manifest.json
@@ -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
diff --git a/webextension/static/style/wallet.css b/webextension/static/style/wallet.css
index 16a414b3d..7c06f2386 100644
--- a/webextension/static/style/wallet.css
+++ b/webextension/static/style/wallet.css
@@ -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;
+} \ No newline at end of file
diff --git a/webextension/static/welcome.html b/webextension/static/welcome.html
index dc893211c..07ecac707 100644
--- a/webextension/static/welcome.html
+++ b/webextension/static/welcome.html
@@ -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>
diff --git a/webextension/static/withdraw.html b/webextension/static/withdraw.html
index d2aab1b64..5137204bd 100644
--- a/webextension/static/withdraw.html
+++ b/webextension/static/withdraw.html
@@ -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>