10

I want to load an external webpage in Electron using BrowserView. It has pretty much the same API as BrowserWindow.

const currentWindow = remote.getCurrentWindow();
const view = new remote.BrowserView({
  webPreferences: {
    // contextIsolation: true,
    partition: 'my-view-partition',
    enableRemoteModule: false,
    nodeIntegration: false,
    preload: `${__dirname}/preload.js`,
    sandbox: true,
  },
});
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');

In my preload.js file, I simply attach a variable to the global object.

process.once('loaded', () => {
  global.baz = 'qux';
});

The app running on localhost:3000 is a React app which references the value like this:

const sharedString = global.baz || 'Not found';

The problem is I have to comment out the setting contextIsolation: true when creating the BrowserView. This exposes a security vulnerability.

Is it possible to (one way - from Electron to the webpage) inject variables into a BrowserView (or BrowserWindow) while still using contextIsolation to make the Electron environment isolated from any changes made to the global environment by the loaded content?

Update: One possible approach could be intercepting the network protocol, but I'm not sure about this

app.on('ready', () => {
  const { protocol } = session.fromPartition('my-partition')

  protocol.interceptBufferProtocol('https', (req, callback) => {
    if (req.uploadData) {
      // How to handle file uploads?
      callback()
      return
    }

    // This is electron.net, docs: https://electronjs.org/docs/api/net
    net
      .request(req)
      .on('response', (res) => {
        const chunks = []
        res.on('data', (chunk) => {
          chunks.push(Buffer.from(chunk))
        })
        res.on('end', () => {
          const blob = Buffer.concat(chunks)
          const type = res.headers['content-type'] || []
          if (type.includes('text/html') && blob.includes('<head>')) {
            // FIXME?
            const pos = blob.indexOf('<head>')
            // inject contains the Buffer with the injected HTML script
            callback(Buffer.concat([blob.slice(0, pos), inject, blob.slice(pos)]))
          } else {
            callback(blob)
          }
        })
      })
      .on('error', (err) => {
        console.error('error', err)
        callback()
      })
      .end()
  })
})
J. Hesters
  • 13,117
  • 31
  • 133
  • 249
  • If your global variable is a simple string why not pass it as a query param of url, like this: `view.webContents.loadURL('http://localhost:3000?baz=qux');`. And in your react app you can parse the query params. – Vaibhav Vishal Sep 10 '19 at 08:48
  • @VaibhavVishal It is not. I wanted to give a simple example but what I need to inject is a Web3 provider. – J. Hesters Sep 10 '19 at 08:49
  • @J.Hesters does your React app already include `web3.js` library (if I understood your case correctly)? Then you could just pass in a provider URL string. You won't be able to pass in the library as global variable from electron to the page with contextIsolation enabled. Also I am not sure, what your code snippet `protocol.interceptBufferProtocol` has to do with your case (file upload? what https request shall be intercepted?). Looks a bit like an XY problem, which might get done easier. Maybe you could elaborate a bit more on your issue. – ford04 Sep 12 '19 at 08:31
  • If you are able to adjust the web app build: An alternative would be to provide an electron specific version of the React web app which extends your basic web version with added/bundled web3 functionality. This enhanced version could be included in your Electron build, while the web version remained untouched. – ford04 Sep 12 '19 at 08:46

3 Answers3

9

After doing some digging, I found a few pull requests for Electron that detail the issue you are having. The first describes a reproducible example very similar to the problem you are describing.

Expected Behavior

https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content A preload script should be able to attach anything to the window or document with contextIsolation: true.

Actual behavior

Anything attached to the window in the preload.js just disappears in the renderer.

It seems the final comment explains that the expected behavior no longer works

It was actually possible until recently, a PR with Isolated Worlds has changed the behavior.

The second has a user suggest what they have found to be their solution:

After many days of research and fiddling with the IPC, I've concluded that the best way is to go the protocol route.

I looked at the docs for BrowserWindow and BrowserView as well as an example that shows the behavior that you desire, but these PRs suggest this is no longer possible (along this route).

Possible Solution

Looking into the documentation, the webContents object you get from view.webContents has the function executeJavaScript, so you could try the following to set the global variable.

...
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
view.webContents.executeJavaScript("global.baz = 'qux';");
...
dodgez
  • 551
  • 2
  • 5
  • Thank you for your help. I'm going to try `executeJavaScript` and I'm gonna give you the bounty for providing the most helpful answer. Thank you very much. – J. Hesters Sep 17 '19 at 06:24
  • 1
    `executeJavaScript` doesn't work for us because it’s definitely after the page load, which, does not solve the task of preloading a script to define globals. – J. Hesters Sep 18 '19 at 11:35
5

Other answers are outdated, use contextBridge be sure to use sendToHost() instead of send()

    // Preload (Isolated World)
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld(
      'electron',
      {
        doThing: () => ipcRenderer.sendToHost('do-a-thing')
      }
    )
    
    // Renderer (Main World)
    
    window.electron.doThing()
quinton-ashley
  • 133
  • 2
  • 10
1

So, executeJavaScript as suggested by Zapparatus ended up being part of the solution.

This is what's going on in renderer.js.

view.webContents.executeJavaScript(`
  window.communicator = {
    request: function(data) {
      const url = 'prefix://?data=' + encodeURIComponent(JSON.stringify(data))
      const req = new XMLHttpRequest()
      req.open('GET', url)
      req.send();
    },
    receive: function(data) {
      alert('got: ' + JSON.stringify(data))
    }
  };
`)
const setContent = data => view.webContents.executeJavaScript(
  `window.communicator.receive(${JSON.stringify(data)})`
)
ipcRenderer.on('communicator', (event, message) => {
  setContent(`Hello, ${message}!`)
})

We ended up setting up a custom protocol, similar to how its been done here. In your main.js file set up the following:

const { app, session, protocol } = require('electron')
const { appWindows } = require('./main/app-run')
const { URL } = require('url')

protocol.registerSchemesAsPrivileged([
  {
    scheme: 'prefix',
    privileges: {
      bypassCSP: true, // ignore CSP, we won't need to patch CSP
      secure: true // allow requests from https context
    }
  }
])

app.on('ready', () => {
  const sess = session.fromPartition('my-view-partition')

  // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
  sess.setPermissionRequestHandler((webContents, permission, callback) => {
    // Denies the permissions request
    const decision = false
    return callback(decision)
  })

  sess.protocol.registerStringProtocol('prefix', (req, callback) => {
    const url = new URL(req.url)
    try {
      const data = JSON.parse(url.searchParams.get('data'))
      appWindows.main.webContents.send('prefix', data)
    } catch (e) {
      console.error('Could not parse prefix request!')
    }
    const response = {
      mimeType: 'text/plain',
      data: 'ok'
    }
    callback(response)
  })
})

No preload.js or postMessage needed.

J. Hesters
  • 13,117
  • 31
  • 133
  • 249