diff options
| -rw-r--r-- | gui/src/config.json | 4 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 112 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 12 | ||||
| -rw-r--r-- | gui/src/renderer/components/ErrorBoundary.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/Support.tsx | 18 | ||||
| -rw-r--r-- | gui/src/renderer/containers/SupportPage.tsx | 4 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 4 |
7 files changed, 107 insertions, 51 deletions
diff --git a/gui/src/config.json b/gui/src/config.json index 456da1ad02..59a214b604 100644 --- a/gui/src/config.json +++ b/gui/src/config.json @@ -1,11 +1,11 @@ { + "supportEmail": "support@mullvad.net", "links": { "purchase": "https://mullvad.net/account/", "manageKeys": "https://mullvad.net/account/ports/", "faq": "https://mullvad.net/help/tag/mullvad-app/", "download": "https://mullvad.net/download/", - "betaDownload": "https://mullvad.net/download/beta", - "supportEmail": "support@mullvad.net" + "betaDownload": "https://mullvad.net/download/beta" }, "colors": { "darkBlue": "rgb(25, 46, 69)", diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 5d08689036..efbf9a4aa3 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -14,6 +14,7 @@ import moment from 'moment'; import * as path from 'path'; import { sprintf } from 'sprintf-js'; import * as uuid from 'uuid'; +import config from '../config.json'; import { hasExpired } from '../shared/account-expiry'; import BridgeSettingsBuilder from '../shared/bridge-settings-builder'; import { @@ -385,7 +386,12 @@ class ApplicationMain { // fetching. https://github.com/electron/electron/issues/22995 session.defaultSession.setSpellCheckerDictionaryDownloadURL('https://00.00/'); + // Blocks scripts in the renderer process from asking for any permission. + this.blockPermissionRequests(); + // Blocks any http(s) and file requests that aren't supposed to happen. this.blockRequests(); + // Blocks navigation since it's not needed. + this.blockNavigation(); this.translations = this.updateCurrentLocale(); @@ -1138,7 +1144,8 @@ class ApplicationMain { }); IpcMainEventChannel.problemReport.handleCollectLogs((toRedact) => { - const reportPath = path.join(app.getPath('temp'), uuid.v4() + '.log'); + const id = uuid.v4(); + const reportPath = this.getProblemReportPath(id); const executable = resolveBin('mullvad-problem-report'); const args = ['collect', '--output', reportPath]; if (toRedact.length > 0) { @@ -1156,15 +1163,16 @@ class ApplicationMain { reject(error.message); } else { log.debug(`Problem report was written to ${reportPath}`); - resolve(reportPath); + resolve(id); } }); }); }); - IpcMainEventChannel.problemReport.handleSendReport(({ email, message, savedReport }) => { + IpcMainEventChannel.problemReport.handleSendReport(({ email, message, savedReportId }) => { const executable = resolveBin('mullvad-problem-report'); - const args = ['send', '--email', email, '--message', message, '--report', savedReport]; + const reportPath = this.getProblemReportPath(savedReportId); + const args = ['send', '--email', email, '--message', message, '--report', reportPath]; return new Promise((resolve, reject) => { execFile(executable, args, { windowsHide: true }, (error, stdout, stderr) => { @@ -1183,9 +1191,16 @@ class ApplicationMain { }); }); + IpcMainEventChannel.problemReport.handleViewLog((savedReportId) => + shell.openPath(this.getProblemReportPath(savedReportId)), + ); + IpcMainEventChannel.app.handleQuit(() => app.quit()); - IpcMainEventChannel.app.handleOpenUrl((url) => shell.openExternal(url)); - IpcMainEventChannel.app.handleOpenPath((path) => shell.openPath(path)); + IpcMainEventChannel.app.handleOpenUrl(async (url) => { + if (Object.values(config.links).find((link) => url.startsWith(link))) { + await shell.openExternal(url); + } + }); IpcMainEventChannel.app.handleShowOpenDialog((options) => dialog.showOpenDialog(options)); } @@ -1399,37 +1414,74 @@ class ApplicationMain { }; } + private blockPermissionRequests() { + session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => { + callback(false); + }); + session.defaultSession.setPermissionCheckHandler(() => false); + } + // Since the app frontend never performs any network requests, all requests originating from the // renderer process are blocked to protect against the potential threat of malicious third party // dependencies. There are a few exceptions which are described further down. private blockRequests() { - session.defaultSession.webRequest.onBeforeRequest( - { urls: ['*://*/*'] }, - (details, callback) => { - if ( - process.env.NODE_ENV === 'development' && - // Local web server providing assests (index.html, index.js and css files) - (details.url.startsWith('http://localhost:8080/') || - // Automatic reloading performed by the browser-sync module - details.url.startsWith('http://localhost:35829/browser-sync/') || - // Downloading of React and Redux developer tools. - details.url.startsWith('https://clients2.google.com') || - details.url.startsWith('https://clients2.googleusercontent.com')) - ) { - callback({}); - } else { - log.error(`${details.method} request blocked: ${details.url}`); - callback({ cancel: true }); + session.defaultSession.webRequest.onBeforeRequest((details, callback) => { + if (this.allowFileAccess(details.url) || this.allowDevelopmentRequest(details.url)) { + callback({}); + } else { + log.error(`${details.method} request blocked: ${details.url}`); + callback({ cancel: true }); - // Throw error in development to notify since this should never happen. - if (process.env.NODE_ENV === 'development') { - throw new Error('Web request blocked'); - } + // Throw error in development to notify since this should never happen. + if (process.env.NODE_ENV === 'development') { + throw new Error('Web request blocked'); } - }, + } + }); + } + + private allowFileAccess(url: string): boolean { + const buildDir = path.normalize(path.join(path.resolve(__dirname), '..', '..')); + + if (url.startsWith('file:')) { + // Extract the path from the URL + let filePath = decodeURI(new URL(url).pathname); + if (process.platform === 'win32') { + // Windows paths shouldn't start with a '/' + filePath = filePath.replace(/^\//, ''); + } + filePath = path.resolve(filePath); + + return !path.relative(buildDir, filePath).includes('..'); + } else { + return false; + } + } + + private allowDevelopmentRequest(url: string): boolean { + return ( + process.env.NODE_ENV === 'development' && + // Local web server providing assests (index.html, index.js and css files) + (url.startsWith('http://localhost:8080/') || + // Automatic reloading performed by the browser-sync module + url.startsWith('ws://localhost:35829/browser-sync') || + url.startsWith('http://localhost:35829/browser-sync/') || + // Downloading of React and Redux developer tools. + url.startsWith('devtools://devtools/') || + url.startsWith('chrome-extension://') || + url.startsWith('https://clients2.google.com') || + url.startsWith('https://clients2.googleusercontent.com')) ); } + private blockNavigation() { + app.on('web-contents-created', (_event, contents) => { + contents.on('will-navigate', (event) => { + event.preventDefault(); + }); + }); + } + private async installDevTools() { // eslint-disable-next-line @typescript-eslint/no-var-requires const installer = require('electron-devtools-installer'); @@ -1773,6 +1825,10 @@ class ApplicationMain { return shell.openExternal(url); } } + + private getProblemReportPath(id: string): string { + return path.join(app.getPath('temp'), `${id}.log`); + } } const applicationMain = new ApplicationMain(); diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 1277db2e63..6f656c5dfe 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -460,9 +460,13 @@ export default class AppRenderer { public async sendProblemReport( email: string, message: string, - savedReport: string, + savedReportId: string, ): Promise<void> { - await IpcRendererEventChannel.problemReport.sendReport({ email, message, savedReport }); + await IpcRendererEventChannel.problemReport.sendReport({ email, message, savedReportId }); + } + + public viewLog(id: string): Promise<string> { + return IpcRendererEventChannel.problemReport.viewLog(id); } public quit(): void { @@ -473,10 +477,6 @@ export default class AppRenderer { return IpcRendererEventChannel.app.openUrl(url); } - public openPath(path: string): Promise<string> { - return IpcRendererEventChannel.app.openPath(path); - } - public showOpenDialog( options: Electron.OpenDialogOptions, ): Promise<Electron.OpenDialogReturnValue> { diff --git a/gui/src/renderer/components/ErrorBoundary.tsx b/gui/src/renderer/components/ErrorBoundary.tsx index 040ff71826..82e2910cc0 100644 --- a/gui/src/renderer/components/ErrorBoundary.tsx +++ b/gui/src/renderer/components/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { colors, links } from '../../config.json'; +import { colors, supportEmail } from '../../config.json'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import PlatformWindowContainer from '../containers/PlatformWindowContainer'; @@ -71,7 +71,7 @@ export default class ErrorBoundary extends React.Component<IProps, IState> { messages .pgettext('error-boundary-view', 'Something went wrong. Please contact us at %(email)s') .split('%(email)s', 2); - reachBackMessage.splice(1, 0, <Email>{links.supportEmail}</Email>); + reachBackMessage.splice(1, 0, <Email>{supportEmail}</Email>); return ( <PlatformWindowContainer> diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx index d6657d4c44..e7960a30c7 100644 --- a/gui/src/renderer/components/Support.tsx +++ b/gui/src/renderer/components/Support.tsx @@ -40,7 +40,7 @@ enum SendState { interface ISupportState { email: string; message: string; - savedReport?: string; + savedReportId?: string; sendState: SendState; disableActions: boolean; showOutdatedVersionWarning: boolean; @@ -56,7 +56,7 @@ interface ISupportProps { saveReportForm: (form: ISupportReportForm) => void; clearReportForm: () => void; collectProblemReport: (accountsToRedact: string[]) => Promise<string>; - sendProblemReport: (email: string, message: string, savedReport: string) => Promise<void>; + sendProblemReport: (email: string, message: string, savedReportId: string) => Promise<void>; outdatedVersion: boolean; suggestedIsBeta: boolean; onExternalLink: (url: string) => void; @@ -66,7 +66,7 @@ export default class Support extends React.Component<ISupportProps, ISupportStat public state = { email: '', message: '', - savedReport: undefined, + savedReportId: undefined, sendState: SendState.initial, disableActions: false, showOutdatedVersionWarning: false, @@ -102,8 +102,8 @@ export default class Support extends React.Component<ISupportProps, ISupportStat public onViewLog = () => { this.performWithActionsDisabled(async () => { try { - const reportPath = await this.collectLog(); - this.props.viewLog(reportPath); + const reportId = await this.collectLog(); + this.props.viewLog(reportId); } catch (error) { // TODO: handle error } @@ -199,9 +199,9 @@ export default class Support extends React.Component<ISupportProps, ISupportStat this.collectLogPromise = collectPromise; try { - const reportPath = await collectPromise; + const reportId = await collectPromise; return new Promise((resolve) => { - this.setState({ savedReport: reportPath }, () => resolve(reportPath)); + this.setState({ savedReportId: reportId }, () => resolve(reportId)); }); } catch (error) { this.collectLogPromise = undefined; @@ -216,8 +216,8 @@ export default class Support extends React.Component<ISupportProps, ISupportStat this.setState({ sendState: SendState.sending }, async () => { try { const { email, message } = this.state; - const reportPath = await this.collectLog(); - await this.props.sendProblemReport(email, message, reportPath); + const reportId = await this.collectLog(); + await this.props.sendProblemReport(email, message, reportId); this.props.clearReportForm(); this.setState({ sendState: SendState.success }, () => { resolve(); diff --git a/gui/src/renderer/containers/SupportPage.tsx b/gui/src/renderer/containers/SupportPage.tsx index 94053f1ccd..1df4827223 100644 --- a/gui/src/renderer/containers/SupportPage.tsx +++ b/gui/src/renderer/containers/SupportPage.tsx @@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext & RouteC onClose() { props.history.goBack(); }, - viewLog(path: string) { - consumePromise(props.app.openPath(path)); + viewLog(id: string) { + consumePromise(props.app.viewLog(id)); }, saveReportForm, clearReportForm, diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 3f0d86d995..5cc347e51e 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -126,7 +126,6 @@ export const ipcSchema = { app: { quit: send<void>(), openUrl: invoke<string, void>(), - openPath: invoke<string, string>(), showOpenDialog: invoke<Electron.OpenDialogOptions, Electron.OpenDialogReturnValue>(), }, tunnel: { @@ -185,7 +184,8 @@ export const ipcSchema = { }, problemReport: { collectLogs: invoke<string[], string>(), - sendReport: invoke<{ email: string; message: string; savedReport: string }, void>(), + sendReport: invoke<{ email: string; message: string; savedReportId: string }, void>(), + viewLog: invoke<string, string>(), }, logging: { log: send<ILogEntry>(), |
