Files
5chan/electron/main.js
T

316 lines
10 KiB
JavaScript
Raw Normal View History

2023-08-25 20:43:40 +00:00
require('./log')
2023-04-24 19:42:49 +00:00
const {
app,
BrowserWindow,
Menu,
MenuItem,
Tray,
screen: electronScreen,
shell,
dialog,
2023-08-25 20:43:40 +00:00
} = require('electron')
const isDev = require('electron-is-dev')
const path = require('path')
const startIpfs = require('./startIpfs')
const { URL } = require('node:url')
2023-04-24 19:42:49 +00:00
2023-08-25 20:43:40 +00:00
let startIpfsError
2023-04-24 19:42:49 +00:00
startIpfs().catch((e) => {
2023-08-25 20:43:40 +00:00
startIpfsError = e
console.error(e)
})
// use common user agent instead of electron so img, video, audio, iframe elements don't get blocked
// https://www.whatismybrowser.com/guides/the-latest-version/chrome
// https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
// NOTE: eventually should probably fake sec-ch-ua header as well
let fakeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'darwin')
fakeUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'linux')
fakeUserAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
const realUserAgent = `plebchan/${require('../package.json').version}`
2023-04-24 19:42:49 +00:00
// add right click menu
2023-08-25 20:43:40 +00:00
const contextMenu = require('electron-context-menu')
2023-04-24 19:42:49 +00:00
contextMenu({
// prepend custom buttons to top
prepend: (defaultActions, parameters, browserWindow) => [
{
label: 'Back',
visible: parameters.mediaType === 'none',
enabled: browserWindow?.webContents?.canGoBack(),
click: () => browserWindow?.webContents?.goBack(),
},
{
label: 'Forward',
visible: parameters.mediaType === 'none',
enabled: browserWindow?.webContents?.canGoForward(),
click: () => browserWindow?.webContents?.goForward(),
},
{
label: 'Reload',
visible: parameters.mediaType === 'none',
click: () => browserWindow?.webContents?.reload(),
},
],
showLookUpSelection: false,
showCopyImage: true,
showCopyImageAddress: true,
showSaveImageAs: true,
showSaveLinkAs: true,
showInspectElement: true,
showServices: false,
showSearchWithGoogle: false,
2023-08-25 20:43:40 +00:00
})
2023-04-24 19:42:49 +00:00
const createMainWindow = () => {
let mainWindow = new BrowserWindow({
width: 1000,
height: 600,
show: false,
backgroundColor: 'white',
webPreferences: {
2023-08-25 20:43:40 +00:00
webSecurity: true, // must be true or iframe embeds like youtube can do remote code execution
2023-04-24 19:42:49 +00:00
nodeIntegration: false,
contextIsolation: true,
devTools: true, // TODO: change to isDev when no bugs left
preload: path.join(__dirname, 'preload.js'),
},
2023-08-25 20:43:40 +00:00
})
// set fake user agent
mainWindow.webContents.userAgent = fakeUserAgent
// set custom user agent and other headers for window.fetch requests to prevent origin errors
mainWindow.webContents.session.webRequest.onBeforeSendHeaders({urls: ['*://*/*']}, (details, callback) => {
// if not a fetch request, do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr') {
return callback({requestHeaders: details.requestHeaders})
}
// console.log(details.method, details.url, details.resourceType, 'webContents:', details.referrer, 'referrer:', details.webContents.getURL(), details.requestHeaders)
details.requestHeaders['User-Agent'] = realUserAgent
details.requestHeaders['sec-ch-ua'] = undefined
details.requestHeaders['sec-ch-ua-platform'] = undefined
details.requestHeaders['sec-ch-ua-mobile'] = undefined
details.requestHeaders['Sec-Fetch-Dest'] = undefined
details.requestHeaders['Sec-Fetch-Mode'] = undefined
details.requestHeaders['Sec-Fetch-Site'] = undefined
details.requestHeaders['Origin'] = undefined
callback({requestHeaders: details.requestHeaders})
})
// fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution
mainWindow.webContents.session.webRequest.onHeadersReceived({urls: ['*://*/*']}, (details, callback) => {
// if not a fetch request, do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr') {
return callback({responseHeaders: details.responseHeaders})
}
// console.log(details.method, details.url, details.resourceType, 'webContents:', details.referrer, 'referrer:', details.webContents.getURL(), details.responseHeaders)
2023-08-25 21:00:01 +00:00
delete details.responseHeaders['access-control-allow-origin'] // must delete or both '*, *' get added
2023-08-25 20:43:40 +00:00
details.responseHeaders['Access-Control-Allow-Origin'] = '*'
callback({responseHeaders: details.responseHeaders})
})
2023-04-24 19:42:49 +00:00
const startURL = isDev
? 'http://localhost:3000'
2023-08-25 20:43:40 +00:00
: `file://${path.join(__dirname, '../build/index.html')}`
2023-04-24 19:42:49 +00:00
2023-08-25 20:43:40 +00:00
mainWindow.loadURL(startURL)
2023-04-24 19:42:49 +00:00
mainWindow.once('ready-to-show', async () => {
// make sure back button is disabled on launch
2023-08-25 20:43:40 +00:00
mainWindow.webContents.clearHistory()
2023-04-24 19:42:49 +00:00
2023-08-25 20:43:40 +00:00
mainWindow.show()
2023-04-24 19:42:49 +00:00
if (isDev) {
2023-08-25 20:43:40 +00:00
mainWindow.openDevTools()
2023-04-24 19:42:49 +00:00
}
if (startIpfsError) {
2023-08-25 20:43:40 +00:00
dialog.showErrorBox('IPFS error', startIpfsError.message)
2023-04-24 19:42:49 +00:00
}
2023-08-25 20:43:40 +00:00
})
2023-04-24 19:42:49 +00:00
mainWindow.on('closed', () => {
2023-08-25 20:43:40 +00:00
mainWindow = null
})
2023-04-24 19:42:49 +00:00
// don't open new windows
mainWindow.webContents.on('new-window', (event, url) => {
2023-08-25 20:43:40 +00:00
event.preventDefault()
mainWindow.loadURL(url)
})
2023-04-24 19:42:49 +00:00
// open links in external browser
// do not open links in plebchan or will lead to remote execution
mainWindow.webContents.on('will-navigate', (e, originalUrl) => {
if (originalUrl != mainWindow.webContents.getURL()) {
2023-08-25 20:43:40 +00:00
e.preventDefault()
2023-04-24 19:42:49 +00:00
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
2023-08-25 20:43:40 +00:00
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
2023-07-04 13:42:19 +02:00
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
2023-08-25 20:43:40 +00:00
serializedUrl = validatedUrl.toString()
2023-07-04 13:42:19 +02:00
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
2023-08-25 20:43:40 +00:00
serializedUrl = validatedUrl.toString()
2023-07-04 13:42:19 +02:00
} else {
2023-08-25 20:43:40 +00:00
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
2023-04-24 19:42:49 +00:00
}
2023-08-25 20:43:40 +00:00
shell.openExternal(serializedUrl)
2023-04-24 19:42:49 +00:00
} catch (e) {
2023-08-25 20:43:40 +00:00
console.warn(e)
2023-04-24 19:42:49 +00:00
}
}
2023-08-25 20:43:40 +00:00
})
2023-04-24 19:42:49 +00:00
// open links (with target="_blank") in external browser
// do not open links in plebchan or will lead to remote execution
mainWindow.webContents.setWindowOpenHandler(({url}) => {
2023-08-25 20:43:40 +00:00
const originalUrl = url
2023-04-24 19:42:49 +00:00
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
2023-08-25 20:43:40 +00:00
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
2023-07-04 13:42:19 +02:00
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
2023-08-25 20:43:40 +00:00
serializedUrl = validatedUrl.toString()
2023-07-04 13:42:19 +02:00
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
2023-08-25 20:43:40 +00:00
serializedUrl = validatedUrl.toString()
2023-07-04 13:42:19 +02:00
} else {
2023-08-25 20:43:40 +00:00
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
2023-04-24 19:42:49 +00:00
}
2023-08-25 20:43:40 +00:00
shell.openExternal(serializedUrl)
2023-04-24 19:42:49 +00:00
} catch (e) {
2023-08-25 20:43:40 +00:00
console.warn(e)
2023-04-24 19:42:49 +00:00
}
2023-08-25 20:43:40 +00:00
return {action: 'deny'}
2023-04-24 19:42:49 +00:00
})
// deny permissions like location, notifications, etc https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// deny all permissions
return callback(false)
})
// deny attaching webview https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
mainWindow.webContents.on('will-attach-webview', (e, webPreferences, params) => {
// deny all
e.preventDefault()
})
2023-06-26 09:38:17 +02:00
if (process.platform !== 'darwin') {
// tray
const trayIconPath = path.join(
__dirname,
'..',
isDev ? 'public' : 'build',
'electron-tray-icon.png'
2023-08-25 20:43:40 +00:00
)
const tray = new Tray(trayIconPath)
tray.setToolTip('plebchan')
2023-06-26 09:38:17 +02:00
const trayMenu = Menu.buildFromTemplate([
{
label: 'Open plebchan',
click: () => {
2023-08-25 20:43:40 +00:00
mainWindow.show()
2023-06-26 09:38:17 +02:00
},
2023-04-24 19:42:49 +00:00
},
2023-06-26 09:38:17 +02:00
{
label: 'Quit plebchan',
click: () => {
2023-08-25 20:43:40 +00:00
mainWindow.destroy()
app.quit()
2023-06-26 09:38:17 +02:00
},
2023-04-24 19:42:49 +00:00
},
2023-08-25 20:43:40 +00:00
])
tray.setContextMenu(trayMenu)
2023-06-29 21:15:56 +02:00
2023-06-26 09:38:17 +02:00
// show/hide on tray right click
tray.on('right-click', () => {
2023-08-25 20:43:40 +00:00
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
2023-04-24 19:42:49 +00:00
2023-06-29 21:15:56 +02:00
// close to tray
if (!isDev) {
2023-08-25 20:43:40 +00:00
let isQuiting = false
2023-06-29 21:15:56 +02:00
app.on('before-quit', () => {
2023-08-25 20:43:40 +00:00
isQuiting = true
})
2023-06-29 21:15:56 +02:00
mainWindow.on('close', (event) => {
if (!isQuiting) {
2023-08-25 20:43:40 +00:00
event.preventDefault()
mainWindow.hide()
event.returnValue = false
2023-06-29 21:15:56 +02:00
}
2023-08-25 20:43:40 +00:00
})
2023-06-29 21:15:56 +02:00
}
2023-04-24 19:42:49 +00:00
}
2023-08-10 21:43:32 +02:00
const appMenuBack = new MenuItem({
label: '←',
enabled: mainWindow?.webContents?.canGoBack(),
click: () => mainWindow?.webContents?.goBack(),
2023-08-25 20:43:40 +00:00
})
2023-08-10 21:43:32 +02:00
const appMenuForward = new MenuItem({
label: '→',
enabled: mainWindow?.webContents?.canGoForward(),
click: () => mainWindow?.webContents?.goForward(),
2023-08-25 20:43:40 +00:00
})
2023-08-10 21:43:32 +02:00
const appMenuReload = new MenuItem({
label: '⟳',
role: 'reload',
click: () => mainWindow?.webContents?.reload(),
2023-08-25 20:43:40 +00:00
})
2023-08-10 21:43:32 +02:00
2023-04-24 19:42:49 +00:00
// application menu
// hide useless electron help menu
2023-06-29 21:15:56 +02:00
if (process.platform === 'darwin') {
2023-08-25 20:43:40 +00:00
const appMenu = Menu.getApplicationMenu()
appMenu.insert(1, appMenuBack)
appMenu.insert(2, appMenuForward)
appMenu.insert(3, appMenuReload)
Menu.setApplicationMenu(appMenu)
2023-06-29 21:15:56 +02:00
} else {
// Other platforms
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter(
(item) => item.role !== 'help'
2023-08-25 20:43:40 +00:00
)
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp]
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu))
2023-06-29 21:15:56 +02:00
}
2023-08-25 20:43:40 +00:00
}
2023-04-24 19:42:49 +00:00
2023-06-29 21:15:56 +02:00
app.whenReady().then(() => {
2023-08-25 20:43:40 +00:00
createMainWindow()
2023-06-28 09:53:42 +02:00
2023-06-29 21:15:56 +02:00
app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) {
2023-08-25 20:43:40 +00:00
createMainWindow()
2023-04-24 19:42:49 +00:00
}
2023-08-25 20:43:40 +00:00
})
})
2023-06-29 21:15:56 +02:00
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
2023-08-25 20:43:40 +00:00
app.quit()
2023-06-29 21:15:56 +02:00
}
2023-08-25 20:43:40 +00:00
})