enable context isolation

This commit is contained in:
刘浩远 2020-06-30 19:15:37 +08:00
parent 394643b95e
commit 59c5d663f1
15 changed files with 111 additions and 58 deletions

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"program": "${workspaceRoot}/dist/electron.js",
"args" : ["."],
"outputCapture": "std",
"sourceMaps": true
}
]
}

View File

@ -3,14 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src-elem 'sha256-XUiRvcvPhKzBU50B7nIFBLySVn2CJ3cNFgqtfeiRcTA='; img-src http://* https://*; style-src 'self' 'unsafe-inline'; frame-src http://* https://*; media-src http://* https://*"> content="default-src 'none'; script-src-elem 'sha256-prYLVBOTCtLoXJ5JJGBEADdvxnqlbKVTWQs/C8BrYsQ='; img-src http://* https://*; style-src 'self' 'unsafe-inline'; frame-src http://* https://*; media-src http://* https://*">
<title>Article</title> <title>Article</title>
<link rel="stylesheet" href="scroll.css" /> <link rel="stylesheet" href="scroll.css" />
<link rel="stylesheet" href="article.css" /> <link rel="stylesheet" href="article.css" />
</head> </head>
<body> <body>
<div id="main"></div> <div id="main"></div>
<script integrity="sha256-XUiRvcvPhKzBU50B7nIFBLySVn2CJ3cNFgqtfeiRcTA=" src="article.js"></script> <script integrity="sha256-prYLVBOTCtLoXJ5JJGBEADdvxnqlbKVTWQs/C8BrYsQ=" src="article.js"></script>
<!-- Run "cat article.js | openssl dgst -sha256 -binary | openssl enc -base64 -A" for hash --> <!-- Run "cat article.js | openssl dgst -sha256 -binary | openssl enc -base64 -A" for hash -->
</body> </body>
</html> </html>

View File

@ -17,18 +17,20 @@ for (let s of dom.querySelectorAll("script")) {
} }
let main = document.getElementById("main") let main = document.getElementById("main")
main.innerHTML = dom.body.innerHTML main.innerHTML = dom.body.innerHTML
document.addEventListener("click", event => {
event.preventDefault() let contextOn = false
let target = event.target const dismissListener = () => {
while (target.nodeName !== "#document") { if (contextOn) {
if (target.href) { contextOn = false
window.renderer.requestNavigation(target.href) window.renderer.dismissContextMenu()
break
} }
target = target.parentNode }
} document.addEventListener("mousedown", dismissListener)
}) document.addEventListener("scroll", dismissListener)
document.addEventListener("contextmenu", event => { document.addEventListener("contextmenu", event => {
let text = document.getSelection().toString() let text = document.getSelection().toString()
if (text) window.renderer.contextMenu([event.clientX, event.clientY], text) if (text) {
contextOn = true
window.renderer.contextMenu([event.clientX, event.clientY], text)
}
}) })

View File

@ -1,10 +1,10 @@
const { contextBridge, ipcRenderer } = require("electron") const { contextBridge, ipcRenderer } = require("electron")
contextBridge.exposeInMainWorld("renderer",{ contextBridge.exposeInMainWorld("renderer",{
requestNavigation: (href) => { dismissContextMenu: () => {
ipcRenderer.sendToHost("request-navigation", href) ipcRenderer.invoke("webview-context-menu", null, null)
}, },
contextMenu: (pos, text) => { contextMenu: (pos, text) => {
ipcRenderer.sendToHost("context-menu", pos, text) ipcRenderer.invoke("webview-context-menu", pos, text)
} }
}) })

View File

@ -61,6 +61,13 @@ const settingsBridge = {
return ipcRenderer.sendSync("get-locale") return ipcRenderer.sendSync("get-locale")
}, },
getFontSize: (): number => {
return ipcRenderer.sendSync("get-font-size")
},
setFontSize: (size: number) => {
ipcRenderer.invoke("set-font-size", size)
},
getAll: () => { getAll: () => {
return ipcRenderer.sendSync("get-all-settings") as Object return ipcRenderer.sendSync("get-all-settings") as Object
}, },

View File

@ -40,6 +40,13 @@ const utilsBridge = {
await ipcRenderer.invoke("clear-cache") await ipcRenderer.invoke("clear-cache")
}, },
addWebviewContextListener: (callback: (pos: [number, number], text: string) => any) => {
ipcRenderer.removeAllListeners("webview-context-menu")
ipcRenderer.on("webview-context-menu", (_, pos, text) => {
callback(pos, text)
})
},
addWebviewKeydownListener: (id: number, callback: (event: Electron.Input) => any) => { addWebviewKeydownListener: (id: number, callback: (event: Electron.Input) => any) => {
ipcRenderer.invoke("add-webview-keydown-listener", id) ipcRenderer.invoke("add-webview-keydown-listener", id)
ipcRenderer.removeAllListeners("webview-keydown") ipcRenderer.removeAllListeners("webview-keydown")

View File

@ -4,9 +4,7 @@ import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react" import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source" import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
import { store } from "../scripts/settings"
const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20] const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
type ArticleProps = { type ArticleProps = {
@ -20,6 +18,7 @@ type ArticleProps = {
toggleStarred: (item: RSSItem) => void toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void toggleHidden: (item: RSSItem) => void
textMenu: (text: string, position: [number, number]) => void textMenu: (text: string, position: [number, number]) => void
dismissContextMenu: () => void
} }
type ArticleState = { type ArticleState = {
@ -36,13 +35,14 @@ class Article extends React.Component<ArticleProps, ArticleState> {
fontSize: this.getFontSize(), fontSize: this.getFontSize(),
loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage
} }
window.utils.addWebviewContextListener(this.contextMenuHandler)
} }
getFontSize = () => { getFontSize = () => {
return store.get(FONT_SIZE_STORE_KEY, 16) return window.settings.getFontSize()
} }
setFontSize = (size: number) => { setFontSize = (size: number) => {
store.set(FONT_SIZE_STORE_KEY, size) window.settings.setFontSize(size)
this.setState({fontSize: size}) this.setState({fontSize: size})
} }
@ -79,27 +79,16 @@ class Article extends React.Component<ArticleProps, ArticleState> {
] ]
}) })
ipcHandler = event => { contextMenuHandler = (pos: [number, number], text: string) => {
switch (event.channel) { if (pos) {
case "request-navigation": {
window.utils.openExternal(event.args[0])
break
}
case "context-menu": {
let articlePos = document.getElementById("article").getBoundingClientRect() let articlePos = document.getElementById("article").getBoundingClientRect()
let [x, y] = event.args[0] let [x, y] = pos
this.props.textMenu(event.args[1], [x + articlePos.x, y + articlePos.y]) this.props.textMenu(text, [x + articlePos.x, y + articlePos.y])
break } else {
this.props.dismissContextMenu()
} }
} }
}
popUpHandler = event => {
window.utils.openExternal(event.url)
}
navigationHandler = event => {
window.utils.openExternal(event.url)
this.props.dismiss()
}
keyDownHandler = (input: Electron.Input) => { keyDownHandler = (input: Electron.Input) => {
if (input.type === "keyDown") { if (input.type === "keyDown") {
switch (input.key) { switch (input.key) {
@ -134,11 +123,9 @@ class Article extends React.Component<ArticleProps, ArticleState> {
componentDidMount = () => { componentDidMount = () => {
let webview = document.getElementById("article") as Electron.WebviewTag let webview = document.getElementById("article") as Electron.WebviewTag
if (webview != this.webview) { if (webview != this.webview) {
webview.addEventListener("ipc-message", this.ipcHandler)
webview.addEventListener("new-window", this.popUpHandler)
webview.addEventListener("will-navigate", this.navigationHandler)
webview.addEventListener("dom-ready", () => { webview.addEventListener("dom-ready", () => {
window.utils.addWebviewKeydownListener(webview.getWebContentsId(), this.keyDownHandler) let id = webview.getWebContentsId()
window.utils.addWebviewKeydownListener(id, this.keyDownHandler)
}) })
this.webview = webview this.webview = webview
webview.focus() webview.focus()
@ -227,7 +214,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
key={this.props.item._id + (this.state.loadWebpage ? "_" : "")} key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
src={this.state.loadWebpage ? this.props.item.link : this.articleView()} src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"} preload={this.state.loadWebpage ? null : "article/preload.js"}
webpreferences="contextIsolation,sandbox,disableDialogs,autoplayPolicy=document-user-activation-required" webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
partition="sandbox" /> partition="sandbox" />
</FocusZone> </FocusZone>
) )

View File

@ -5,7 +5,7 @@ import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcu
import { AppDispatch } from "../scripts/utils" import { AppDispatch } from "../scripts/utils"
import { dismissItem, showOffsetItem } from "../scripts/models/page" import { dismissItem, showOffsetItem } from "../scripts/models/page"
import Article from "../components/article" import Article from "../components/article"
import { openTextMenu } from "../scripts/models/app" import { openTextMenu, closeContextMenu } from "../scripts/models/app"
type ArticleContainerProps = { type ArticleContainerProps = {
itemId: string itemId: string
@ -34,7 +34,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)), toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)), toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)), toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)) textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)),
dismissContextMenu: () => dispatch(closeContextMenu())
} }
} }

View File

@ -3,6 +3,7 @@ import { ThemeSettings, SchemaTypes } from "./schema-types"
import { store } from "./main/settings" import { store } from "./main/settings"
import performUpdate from "./main/update-scripts" import performUpdate from "./main/update-scripts"
import { WindowManager } from "./main/window" import { WindowManager } from "./main/window"
import { openExternal } from "./main/utils"
if (!process.mas) { if (!process.mas) {
const locked = app.requestSingleInstanceLock() const locked = app.requestSingleInstanceLock()
@ -71,3 +72,14 @@ ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default) nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
winManager.mainWindow.close() winManager.mainWindow.close()
}) })
app.on("web-contents-created", (_, contents) => {
contents.on("new-window", (event, url) => {
if (winManager.hasWindow()) event.preventDefault()
if (contents.getType() === "webview") openExternal(url)
})
contents.on("will-navigate", (event, url) => {
event.preventDefault()
if (contents.getType() === "webview") openExternal(url)
})
})

View File

@ -105,6 +105,14 @@ ipcMain.on("get-locale", (event) => {
event.returnValue = locale event.returnValue = locale
}) })
const FONT_SIZE_STORE_KEY = "fontSize"
ipcMain.on("get-font-size", (event) => {
event.returnValue = store.get(FONT_SIZE_STORE_KEY, 16)
})
ipcMain.handle("set-font-size", (_, size: number) => {
store.set(FONT_SIZE_STORE_KEY, size)
})
ipcMain.on("get-all-settings", (event) => { ipcMain.on("get-all-settings", (event) => {
let output = {} let output = {}
for (let [key, value] of store) { for (let [key, value] of store) {

View File

@ -2,14 +2,18 @@ import { ipcMain, shell, dialog, app, session, webContents, clipboard } from "el
import { WindowManager } from "./window" import { WindowManager } from "./window"
import fs = require("fs") import fs = require("fs")
export function openExternal(url: string) {
if (url.startsWith("https://") || url.startsWith("http://"))
shell.openExternal(url)
}
export function setUtilsListeners(manager: WindowManager) { export function setUtilsListeners(manager: WindowManager) {
ipcMain.on("get-version", (event) => { ipcMain.on("get-version", (event) => {
event.returnValue = app.getVersion() event.returnValue = app.getVersion()
}) })
ipcMain.handle("open-external", (_, url: string) => { ipcMain.handle("open-external", (_, url: string) => {
if (url.startsWith("https://") || url.startsWith("http://")) openExternal(url)
shell.openExternal(url)
}) })
ipcMain.handle("show-error-box", (_, title, content) => { ipcMain.handle("show-error-box", (_, title, content) => {
@ -76,6 +80,12 @@ export function setUtilsListeners(manager: WindowManager) {
await session.defaultSession.clearCache() await session.defaultSession.clearCache()
}) })
ipcMain.handle("webview-context-menu", (_, pos, text) => {
if (manager.hasWindow()) {
manager.mainWindow.webContents.send("webview-context-menu", pos, text)
}
})
ipcMain.handle("add-webview-keydown-listener", (_, id) => { ipcMain.handle("add-webview-keydown-listener", (_, id) => {
let contents = webContents.fromId(id) let contents = webContents.fromId(id)
contents.on("before-input-event", (_, input) => { contents.on("before-input-event", (_, input) => {

View File

@ -57,9 +57,9 @@ export class WindowManager {
fullscreenable: false, fullscreenable: false,
show: false, show: false,
webPreferences: { webPreferences: {
nodeIntegration: true,
webviewTag: true, webviewTag: true,
enableRemoteModule: true, enableRemoteModule: false,
contextIsolation: true,
preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js") preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js")
} }
}) })

View File

@ -2,5 +2,5 @@ import { contextBridge } from "electron"
import settingsBridge from "./bridges/settings" import settingsBridge from "./bridges/settings"
import utilsBridge from "./bridges/utils" import utilsBridge from "./bridges/utils"
window.settings = settingsBridge contextBridge.exposeInMainWorld("settings", settingsBridge)
window.utils = utilsBridge contextBridge.exposeInMainWorld("utils", utilsBridge)

View File

@ -1,11 +1,8 @@
import { IPartialTheme, loadTheme } from "@fluentui/react" import { IPartialTheme, loadTheme } from "@fluentui/react"
import locales from "./i18n/_locales" import locales from "./i18n/_locales"
import Store = require("electron-store") import { ThemeSettings } from "../schema-types"
import { ThemeSettings, SchemaTypes } from "../schema-types"
import intl from "react-intl-universal" import intl from "react-intl-universal"
export const store = new Store<SchemaTypes>()
const lightTheme: IPartialTheme = { const lightTheme: IPartialTheme = {
defaultFontStyle: { fontFamily: '"Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif' } defaultFontStyle: { fontFamily: '"Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif' }
} }
@ -39,7 +36,6 @@ const darkTheme: IPartialTheme = {
} }
} }
const THEME_STORE_KEY = "theme"
export function setThemeSettings(theme: ThemeSettings) { export function setThemeSettings(theme: ThemeSettings) {
window.settings.setThemeSettings(theme) window.settings.setThemeSettings(theme)
applyThemeSettings() applyThemeSettings()

View File

@ -16,6 +16,7 @@ module.exports = [
}] }]
}, },
output: { output: {
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
path: __dirname + '/dist', path: __dirname + '/dist',
filename: 'electron.js' filename: 'electron.js'
} }
@ -42,8 +43,11 @@ module.exports = [
{ {
mode: 'production', mode: 'production',
entry: './src/index.tsx', entry: './src/index.tsx',
target: 'electron-renderer', target: 'web',
devtool: 'source-map', devtool: 'source-map',
performance: {
hints: false
},
module: { module: {
rules: [{ rules: [{
test: /\.ts(x?)$/, test: /\.ts(x?)$/,