maxstue / vite-reactts-electron-starter

Starter using Vite2 + React + Typescript + Tailwind + Electron Starter.
MIT License
305 stars 91 forks source link

Any reference to ipcRenderer in App.tsx seems to break rendering #15

Closed Seanmclem closed 2 years ago

Seanmclem commented 2 years ago

I'm using a new version at the moment

    "electron": "^11.3.0",
    "electron-builder": "^22.9.1",
    "esbuild": "^0.8.53",
    "typescript": "^4.1.2",
    "vite": "^2.0.4",

etc

my App.tsx looks like this

import type * as React from 'react'

import { ipcRenderer } from 'electron'
import { useEffect } from 'react'

function App() {
  useEffect(() => {
    ipcRenderer.send('test', 'test') // here
  }, [])

  return (
    <div tw="h-screen w-screen flex flex-col pt-12">
      <div>Test</div>
    </div>
  )
}

export default App

If i run yarn dev on this, it doesn't render anything, but starts up. No terminal or dev-tools console errors. If I comment out ipcRenderer.send line, then it renders. I've made sure I have a corresponding event in Main, with no difference. What am I missing? This should work, right?

...Edit1

Actually, there are a couple errors:

client:188 ReferenceError: __dirname is not defined
    at index.js:4
    at chunk.2VCUNPV2.js?v=6b15f8ee:4
    at dep:electron:1

...

client:190 [hmr] Failed to reload /App.tsx. This could be due to syntax errors or importing non-existent modules. (see errors above)

...Edit 2

Seems I can get it to work by changing the import in App.tsx to const { ipcRenderer } = window.require('electron')

and

windowOptions.webPreferences.contextIsolation to false, in index.ts

But, I have to imagine something about both of these changes is maybe not ideal? I would be happy for a better or alternative pattern. ... unless this is fine? I don't know electron extremely well. Please advise

briosheje commented 2 years ago

I had a similar problem, the solution in electron in general is to never use ipc in the client app at all, but you would rather make a bridge.

To make a brief history, the core idea in general is to exclude IPC entirely for safety reasons. In my real-case app scenario I did this:

file main.ts: you need to reference preload.ts in the browserWindow object, like this (in my case I had webpack, I need to check how to do that with vite, but I don't think it's a complex task, you can see an example in this repository):

const mainWindow = new BrowserWindow({
    height: 600,
    width: 800,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY
    },
    autoHideMenuBar: true
  });

preload.ts:

import { IPC_CHANNELS } from "./ipc";

import {
  contextBridge,
  ipcRenderer
} from "electron";

console.log('-loading preload.js-');

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
process.once('loaded', () => {
  contextBridge.exposeInMainWorld(
    "api", {
        send: (channel: IPC_CHANNELS, data: any) => {
            console.log(`[preload.ts] -> sending command ${channel}, data=${data}`)
            // whitelist channels
            let validChannels = Object.values(IPC_CHANNELS);
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel: IPC_CHANNELS, func: any) => {
            let validChannels = Object.values(IPC_CHANNELS);
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => {
                  console.log(`[preload.ts] <- sending result for ${channel}, args=${args}`)
                  func(...args)
                });
            }
        }
    }
  );  
});

file ipc/index.ts

export enum IPC_CHANNELS {
  PING = 'PING'
}

Example in the client environment (renderer with react in this case):

useEffect(() => {
    window.api.receive(channel, (args) => {
      console.log('[useIpcChannel]:: setting message to', args);
      setMessage(args)
    });
  }, [channel]);

Note that window.api is what is exposed in preload.

The same as above can be applied with window.api.send

Example to handle a command from the server:

  ipcMain.on(
    IPC_CHANNELS.PING, (evt, args) => {
      console.log('-> got request:', args);
      win.webContents.send(IPC_CHANNELS.PING, args);
    }
  );

The core idea behind this is to have a shared file that has the IPC CHANNELS defined (through an enum in my case): in this way you can safely normalize the communication protocol between the main and the renderer process.

Hope this helps you to get an idea of how you can structure that.

Seanmclem commented 2 years ago

Thanks @briosheje i might try that. If you know, can you elaborate on the reason why this is insecure? I only ask because the app I'm working on relies on elevated levels of security anyway. So just wondering if I really need to take these steps. What extra security are we getting? Vulnerabilities we are avoiding?

briosheje commented 2 years ago

Thanks @briosheje i might try that. If you know, can you elaborate on the reason why this is insecure? I only ask because the app I'm working on relies on elevated levels of security anyway. So just wondering if I really need to take these steps. What extra security are we getting? Vulnerabilities we are avoiding?

Electron relies on two processes: main and renderer.

The main process is the node (you can imagine it as the server) process, while the renderer process is the chromium process.

The communication between the main and the renderer usually happens through the ipc channel and one of the main concerns was that the renderer process could easily communicate with the main through ipcRenderer without any sort of "control", which is the reason why ipcRenderer is not exposed to the renderer process anymore.

In fact, as you can see, in this repo there is the preload.ts which is already warning you about this: https://github.com/maxstue/vite-reactts-electron-starter/blob/ea9638fb1592685e00cc7bcf58f70b15a60c495f/electron/preload.ts#L42

The "secure" way to implement this is to expose an API to the renderer from the preload where only what is expected to be received from the main process is effectively send. Further validation is of course required from the main process but still, in this way you cannot directly use ipcRenderer (and eventually other main libraries) in the renderer process.

maxstue commented 2 years ago

Thanks for explaning and helping out :)