Lesson 06-Electron Networking and Security

Electron Networking and Security Basics

Overview of Networking Features in Electron

Electron, built on Chromium and Node.js, provides robust networking capabilities:

  • HTTP/HTTPS Requests: Via Node.js’s http and https modules.
  • Custom Protocols: Using the protocol module to intercept and handle requests.
  • Web Content Integration: Supports webview and iframe for loading external pages.

Importance of Security in Electron

  • Cross-Origin Security: Prevents unauthorized resource access.
  • File System Protection: Restricts access to local files.
  • Content Isolation: Ensures external content does not compromise application security.

Development Environment Setup

Initialize Project

mkdir ElectronNetworkDemo
cd ElectronNetworkDemo
npm init -y
npm install electron --save-dev

Configure package.json

{
  "name": "electron-network-demo",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^28.2.2"
  }
}

Networking and Security

CORS and Electron

CORS (Cross-Origin Resource Sharing) is typically a web development concern, but in Electron, it requires special handling since the renderer process operates in a Chromium environment.

Example Code (Renderer Process Making Requests)

index.html

<!DOCTYPE html>
<html>
<head>
  <title>CORS Test</title>
</head>
<body>
  <h1>CORS Test</h1>
  <button onclick="fetchData()">Fetch Data</button>
  <script>
    function fetchData() {
      fetch('https://api.github.com')
        .then(response => response.json())
        .then(data => alert(JSON.stringify(data, null, 2)))
        .catch(err => alert('Error: ' + err.message));
    }
  </script>
</body>
</html>

main.js

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  win.loadFile('index.html');

  // Bypass CORS (optional)
  win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
    details.requestHeaders['Origin'] = 'http://localhost';
    callback({ requestHeaders: details.requestHeaders });
  });
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

Step-by-Step Analysis

  • CORS in Electron:
    • By default, the renderer process is subject to CORS restrictions.
    • The main process, using Node.js’s http module, is unaffected.
  • onBeforeSendHeaders:
    • Modifies request headers to bypass CORS (for testing only).
  • Security Considerations:
    • Avoid directly modifying headers; prefer handling network requests in the main process.

Main Process Request Handling

const { ipcMain } = require('electron');
const https = require('https');

ipcMain.handle('fetch-data', async () => {
  return new Promise((resolve, reject) => {
    https.get('https://api.github.com', {
      headers: { 'User-Agent': 'ElectronApp' },
    }, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    }).on('error', (err) => reject(err.message));
  });
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  fetchData: () => ipcRenderer.invoke('fetch-data'),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>CORS Test</title>
</head>
<body>
  <h1>CORS Test</h1>
  <button onclick="fetchData()">Fetch Data</button>
  <script>
    function fetchData() {
      window.electronAPI.fetchData()
        .then(data => alert(JSON.stringify(data, null, 2)))
        .catch(err => alert('Error: ' + err.message));
    }
  </script>
</body>
</html>

Analysis

  • Main Process Requests: Avoids CORS issues.
  • IPC: Safely transfers data to the renderer process.

Using the protocol Module for Custom Protocols

The protocol module enables custom URL protocols.

Example Code

main.js

const { app, BrowserWindow, protocol } = require('electron');
const path = require('path');
const fs = require('fs').promises;

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(() => {
  protocol.registerFileProtocol('custom', (request, callback) => {
    const url = request.url.replace('custom://', '');
    const filePath = path.join(__dirname, url);
    callback({ path: filePath });
  });

  protocol.registerBufferProtocol('secure', async (request, callback) => {
    const url = request.url.replace('secure://', '');
    try {
      const content = await fs.readFile(path.join(__dirname, url));
      callback({ data: content, mimeType: 'text/plain' });
    } catch (error) {
      callback({ statusCode: 404 });
    }
  });

  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Custom Protocol Test</title>
</head>
<body>
  <h1>Custom Protocol Test</h1>
  <img src="custom://assets/image.png" alt="Local Image">
  <div id="content"></div>
  <script>
    fetch('secure://test.txt')
      .then(response => response.text())
      .then(text => document.getElementById('content').textContent = text)
      .catch(err => console.error('Error:', err));
  </script>
</body>
</html>

Project Structure

ElectronNetworkDemo/
├── main.js
├── index.html
├── assets/
   └── image.png
├── test.txt

Analysis

  • registerFileProtocol: Maps custom protocol to local files.
  • registerBufferProtocol: Returns file content as a buffer.
  • Use Cases: Load local resources or intercept requests.

Secure Access to Local File System

Electron allows renderer processes to access the file system by default, but security must be enhanced.

Example Code

main.js

const { ipcMain } = require('electron');
const fs = require('fs').promises;
const path = require('path');

ipcMain.handle('read-file', async (event, filePath) => {
  const safePath = path.join(__dirname, filePath); // Restrict path
  if (!safePath.startsWith(__dirname)) {
    throw new Error('Access denied: Path outside application directory');
  }
  return await fs.readFile(safePath, 'utf8');
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('fsAPI', {
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>File System Demo</title>
</head>
<body>
  <h1>File System Demo</h1>
  <button onclick="readFile()">Read File</button>
  <div id="content"></div>
  <script>
    function readFile() {
      window.fsAPI.readFile('test.txt')
        .then(content => document.getElementById('content').textContent = content)
        .catch(err => alert('Error: ' + err.message));
    }
  </script>
</body>
</html>

Analysis

  • Path Restriction: Ensures file access is within the application directory.
  • IPC Isolation: Prevents direct file system access from the renderer process.

Integrating Web Content

Using webview Tag to Load External Web Pages

The webview tag embeds external web pages.

Example Code

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Webview Test</title>
</head>
<body>
  <h1>Webview Test</h1>
  <webview id="webview" src="https://example.com" style="width: 100%; height: 500px;" preload="./webview-preload.js"></webview>
</body>
</html>

webview-preload.js

window.addEventListener('DOMContentLoaded', () => {
  console.log('Webview loaded:', document.title);
});

main.js

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
      webviewTag: true, // Enable webview
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

Analysis

  • webviewTag: Enables webview support.
  • Preload: webview-preload.js executes in the webview context.
  • Isolation: Webview content is separated from the renderer process.

iframe Integration with Electron

iframe embeds external content.

Example Code

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Iframe Test</title>
</head>
<body>
  <h1>Iframe Test</h1>
  <iframe id="iframe" src="https://example.com" style="width: 100%; height: 500px;"></iframe>
  <button onclick="sendMessage()">Send Message</button>
  <script>
    function sendMessage() {
      const iframe = document.getElementById('iframe');
      iframe.contentWindow.postMessage('Hello from Electron', '*');
    }
  </script>
</body>
</html>

Analysis

  • postMessage: Communicates with the iframe.
  • Limitations: iframes are subject to same-origin policy.

Comprehensive Case Study: Secure Web Browser

Requirements Analysis

Implement a simple web browser:

  • Load local pages via custom protocol.
  • Display external web pages using webview.
  • Securely access local files.

Implementation Code Breakdown

main.js

const { app, BrowserWindow, ipcMain, protocol } = require('electron');
const path = require('path');
const fs = require('fs').promises;

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
      webviewTag: true,
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(() => {
  protocol.registerFileProtocol('local', (request, callback) => {
    const url = request.url.replace('local://', '');
    const filePath = path.join(__dirname, url);
    callback({ path: filePath });
  });

  createWindow();
});

ipcMain.handle('read-file', async (event, filePath) => {
  const safePath = path.join(__dirname, filePath);
  if (!safePath.startsWith(__dirname)) throw new Error('Access denied');
  return await fs.readFile(safePath, 'utf8');
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Safe Web Browser</title>
</head>
<body>
  <h1>Safe Web Browser</h1>
  <input id="url" placeholder="Enter URL or local:// path" style="width: 300px;">
  <button onclick="loadWebview()">Load</button>
  <button onclick="readFile()">Read File</button>
  <div id="content"></div>
  <webview id="webview" style="width: 100%; height: 500px;" preload="./webview-preload.js"></webview>
  <script>
    function loadWebview() {
      const url = document.getElementById('url').value;
      document.getElementById('webview').src = url;
    }
    function readFile() {
      window.electronAPI.readFile('test.txt')
        .then(content => document.getElementById('content').textContent = content)
        .catch(err => alert('Error: ' + err.message));
    }
  </script>
</body>
</html>

webview-preload.js

window.addEventListener('DOMContentLoaded', () => {
  console.log('Webview content loaded');
});

Project Structure

ElectronNetworkDemo/
├── main.js
├── preload.js
├── webview-preload.js
├── index.html
├── test.txt

Run

npm start

Analysis

  • Custom Protocol: local:// loads local files.
  • webview: Displays external web pages.
  • Secure File Access: Restricts paths to the application directory.

Advanced Techniques and Considerations

Network Performance Optimization

  • Cache Requests: Use session to cache network responses.
win.webContents.session.setCacheSize(100 * 1024 * 1024); // 100MB
  • Asynchronous Loading: Avoid blocking the renderer process.

Security Best Practices

  • Disable Node Integration: nodeIntegration: false.
  • Enable Sandbox: sandbox: true.
  • Content Security Policy (CSP):

html <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';">

Share your love