diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-05-01 14:16:56 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-05-01 14:16:56 +0530 |
commit | 609397d95a73bdae55de41c47b19932e810d0320 (patch) | |
tree | 34fb9168eb25567c2d14daa5f69301d6932d58c1 | |
parent | 3f52d293be88f19e8e68aaa8ee6a80cd6c7cc47a (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.ts | 5 | ||||
-rw-r--r-- | src/webex/messages.ts | 8 | ||||
-rw-r--r-- | src/webex/pageEntryPoint.ts | 4 | ||||
-rw-r--r-- | src/webex/pages/pay.tsx | 2 | ||||
-rw-r--r-- | src/webex/pages/popup.tsx | 111 | ||||
-rw-r--r-- | src/webex/pages/welcome.tsx | 57 | ||||
-rw-r--r-- | src/webex/pages/withdraw.tsx | 7 | ||||
-rw-r--r-- | src/webex/renderHtml.tsx | 4 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 15 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 203 | ||||
-rw-r--r-- | webextension/manifest.json | 14 | ||||
-rw-r--r-- | webextension/static/style/wallet.css | 35 | ||||
-rw-r--r-- | webextension/static/welcome.html | 9 | ||||
-rw-r--r-- | webextension/static/withdraw.html | 6 |
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> |