2023-12-16 16:49:26 +00:00
require ( './log' );
const { app , BrowserWindow , Menu , MenuItem , Tray , screen : electronScreen , shell , dialog } = require ( 'electron' );
const isDev = require ( 'electron-is-dev' );
const path = require ( 'path' );
const startIpfs = require ( './start-ipfs' );
const startPlebbitRpcServer = require ( './start-plebbit-rpc' );
const { URL } = require ( 'node:url' );
2023-04-24 19:42:49 +00:00
2023-12-16 16:49:26 +00:00
let startIpfsError ;
2023-12-20 16:05:50 +00:00
startIpfs . onError = ( error ) => {
// only show error once or it spams the user
const alreadyShownIpfsError = !! startIpfsError ;
startIpfsError = error ;
if ( ! alreadyShownIpfsError ) {
dialog . showErrorBox ( 'IPFS warning' , error . message );
2023-12-11 00:49:36 +00:00
}
2023-12-20 16:05:50 +00:00
};
2023-08-25 20:43:40 +00:00
// 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
2023-12-16 16:49:26 +00:00
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-12-16 16:49:26 +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-12-16 16:49:26 +00:00
});
2023-04-24 19:42:49 +00:00
const createMainWindow = () => {
let mainWindow = new BrowserWindow ({
width : 1000 ,
height : 600 ,
show : false ,
2023-12-11 01:07:15 +00:00
backgroundColor : '#fffee' ,
2023-04-24 19:42:49 +00:00
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-12-16 16:49:26 +00:00
});
2023-08-25 20:43:40 +00:00
// set fake user agent
2023-12-16 16:49:26 +00:00
mainWindow . webContents . userAgent = fakeUserAgent ;
2023-08-25 20:43:40 +00:00
// set custom user agent and other headers for window.fetch requests to prevent origin errors
2023-12-16 16:49:26 +00:00
mainWindow . webContents . session . webRequest . onBeforeSendHeaders ({ urls : [ '*://*/*' ] }, ( details , callback ) => {
const isIframe = !! details . frame ? . parent ;
2023-08-26 15:13:24 +00:00
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
2023-08-25 23:37:03 +00:00
if ( details . resourceType !== 'xhr' || isIframe ) {
2023-12-16 16:49:26 +00:00
return callback ({ requestHeaders : details . requestHeaders });
2023-08-25 20:43:40 +00:00
}
2023-08-26 15:13:24 +00:00
// add privacy
2023-12-16 16:49:26 +00:00
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 ;
2023-08-26 15:13:24 +00:00
// prevent origin errors
2023-12-16 16:49:26 +00:00
details . requestHeaders [ 'Origin' ] = undefined ;
callback ({ requestHeaders : details . requestHeaders });
});
2023-08-25 20:43:40 +00:00
// fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution
2023-12-16 16:49:26 +00:00
mainWindow . webContents . session . webRequest . onHeadersReceived ({ urls : [ '*://*/*' ] }, ( details , callback ) => {
const isIframe = !! details . frame ? . parent ;
2023-08-26 15:13:24 +00:00
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
2023-08-25 23:37:03 +00:00
if ( details . resourceType !== 'xhr' || isIframe ) {
2023-12-16 16:49:26 +00:00
return callback ({ responseHeaders : details . responseHeaders });
2023-08-25 20:43:40 +00:00
}
2023-08-26 15:13:24 +00:00
// must delete lower case headers or both '*, *' could get added
2023-12-16 16:49:26 +00:00
delete details . responseHeaders [ 'access-control-allow-origin' ];
delete details . responseHeaders [ 'access-control-allow-headers' ];
delete details . responseHeaders [ 'access-control-allow-methods' ];
delete details . responseHeaders [ 'access-control-expose-headers' ];
details . responseHeaders [ 'Access-Control-Allow-Origin' ] = '*' ;
details . responseHeaders [ 'Access-Control-Allow-Headers' ] = '*' ;
details . responseHeaders [ 'Access-Control-Allow-Methods' ] = '*' ;
details . responseHeaders [ 'Access-Control-Expose-Headers' ] = '*' ;
callback ({ responseHeaders : details . responseHeaders });
});
2023-08-25 20:43:40 +00:00
2023-12-16 16:49:26 +00:00
const startURL = isDev ? 'http://localhost:3000' : `file:// ${ path . join ( __dirname , '../build/index.html' ) } ` ;
2023-04-24 19:42:49 +00:00
2023-12-16 16:49:26 +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-12-16 16:49:26 +00:00
mainWindow . webContents . clearHistory ();
2023-04-24 19:42:49 +00:00
2023-12-16 16:49:26 +00:00
mainWindow . show ();
2023-04-24 19:42:49 +00:00
if ( isDev ) {
2023-12-16 16:49:26 +00:00
mainWindow . openDevTools ();
2023-04-24 19:42:49 +00:00
}
if ( startIpfsError ) {
2023-12-20 16:05:50 +00:00
dialog . showErrorBox ( 'IPFS warning' , startIpfsError . message );
2023-04-24 19:42:49 +00:00
}
2023-12-16 16:49:26 +00:00
});
2023-04-24 19:42:49 +00:00
mainWindow . on ( 'closed' , () => {
2023-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +00:00
serializedUrl = validatedUrl . toString ();
2023-07-04 13:42:19 +02:00
} else {
2023-12-16 16:49:26 +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-12-16 16:49:26 +00:00
shell . openExternal ( serializedUrl );
2023-04-24 19:42:49 +00:00
} catch ( e ) {
2023-12-16 16:49:26 +00:00
console . warn ( e );
2023-04-24 19:42:49 +00:00
}
}
2023-12-16 16:49:26 +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
2023-12-16 16:49:26 +00:00
mainWindow . webContents . setWindowOpenHandler (({ url }) => {
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-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +00:00
serializedUrl = validatedUrl . toString ();
2023-07-04 13:42:19 +02:00
} else {
2023-12-16 16:49:26 +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-12-16 16:49:26 +00:00
shell . openExternal ( serializedUrl );
2023-04-24 19:42:49 +00:00
} catch ( e ) {
2023-12-16 16:49:26 +00:00
console . warn ( e );
2023-04-24 19:42:49 +00:00
}
2023-12-16 16:49:26 +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
2023-12-16 16:49:26 +00:00
return callback ( false );
});
2023-04-24 19:42:49 +00:00
// 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
2023-12-16 16:49:26 +00:00
e . preventDefault ();
});
2023-04-24 19:42:49 +00:00
2023-06-26 09:38:17 +02:00
if ( process . platform !== 'darwin' ) {
// tray
2023-12-16 16:49:26 +00:00
const trayIconPath = path . join ( __dirname , '..' , isDev ? 'public' : 'build' , 'electron-tray-icon.png' );
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-12-16 16:49:26 +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-12-16 16:49:26 +00:00
mainWindow . destroy ();
app . quit ();
2023-06-26 09:38:17 +02:00
},
2023-04-24 19:42:49 +00:00
},
2023-12-16 16:49:26 +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-12-16 16:49:26 +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-12-16 16:49:26 +00:00
let isQuiting = false ;
2023-06-29 21:15:56 +02:00
app . on ( 'before-quit' , () => {
2023-12-16 16:49:26 +00:00
isQuiting = true ;
});
2023-06-29 21:15:56 +02:00
mainWindow . on ( 'close' , ( event ) => {
if ( ! isQuiting ) {
2023-12-16 16:49:26 +00:00
event . preventDefault ();
mainWindow . hide ();
event . returnValue = false ;
2023-06-29 21:15:56 +02:00
}
2023-12-16 16:49:26 +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-12-16 16:49:26 +00:00
});
2023-08-10 21:43:32 +02:00
const appMenuForward = new MenuItem ({
label : '→' ,
enabled : mainWindow ? . webContents ? . canGoForward (),
click : () => mainWindow ? . webContents ? . goForward (),
2023-12-16 16:49:26 +00:00
});
2023-08-10 21:43:32 +02:00
const appMenuReload = new MenuItem ({
label : '⟳' ,
role : 'reload' ,
click : () => mainWindow ? . webContents ? . reload (),
2023-12-16 16:49:26 +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-12-16 16:49:26 +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
2023-12-16 16:49:26 +00:00
const originalAppMenuWithoutHelp = Menu . getApplicationMenu () ? . items . filter (( item ) => item . role !== 'help' );
const appMenu = [ appMenuBack , appMenuForward , appMenuReload , ... originalAppMenuWithoutHelp ];
Menu . setApplicationMenu ( Menu . buildFromTemplate ( appMenu ));
2023-06-29 21:15:56 +02:00
}
2023-12-16 16:49:26 +00:00
};
2023-04-24 19:42:49 +00:00
2023-06-29 21:15:56 +02:00
app . whenReady (). then (() => {
2023-12-16 16:49:26 +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-12-16 16:49:26 +00:00
createMainWindow ();
2023-04-24 19:42:49 +00:00
}
2023-12-16 16:49:26 +00:00
});
});
2023-06-29 21:15:56 +02:00
app . on ( 'window-all-closed' , () => {
if ( process . platform !== 'darwin' ) {
2023-12-16 16:49:26 +00:00
app . quit ();
2023-06-29 21:15:56 +02:00
}
2023-12-16 16:49:26 +00:00
});