禁用 Electron 的同源策略
const win = new BrowserWindow({width: 800,height: 600,webPreferences: {nodeIntegration: true,// 此参数禁用当前窗口的同源策略webSecurity: false,},});
相似地,webPreferences
配置项下还有一个 allowRunningInsecureContent
参数,如果把它设置为 true
,那么你就可以在一个 HTTPS 页面内访问 HTTP 协议提供的服务了,这在默认情况下也是不被允许的。当开发者把 webSecurity
设置为 false
时,allowRunningInsecureContent
也会被自动设置为 true
。
Electron 为我们提供了一个 net 模块,允许我们使用 Chromium 的原生网络库发出 HTTP 或 HTTPS 请求,它内部会自动判断请求地址是基于什么协议的。
const { app } = require('electron');app.whenReady().then(() => {const { net } = require('electron');const request = net.request('https://github.com');request.on('response', (response) => {console.log(`STATUS: ${response.statusCode}`);console.log(`HEADERS: ${JSON.stringify(response.headers)}`);response.on('data', (chunk) => {console.log(`BODY: ${chunk}`);});response.on('end', () => {console.log('No more data in response.');});});request.end();});
如果只是要拦截 AJAX 请求,那么为第三方网页注入一个脚本,让这个脚本修改 XMLHttpRequest 对象以获取第三方网页 AJAX 请求后的数据即可。
const open = window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {this.addEventListener('readystatechange',function () {if (this.readyState === 4 && this.status === 200) {// 服务端响应的数据console.log(this.responseText);}},false);open.apply(this, arguments);};
在网页加载之初,注入并执行上面的代码,就为接下来的每个 AJAX 请求都注册了一个监听器,开发者可以通过 open
方法的 method
、url
等参数过滤具体的请求,以实现在不影响第三方网页原有逻辑的前提下,精准截获 AJAX 响应数据的目的。
这种方法虽然可以正确截获 AJAX 请求的响应数据,但对结果静态文件请求及修改响应数据无能为力。如果开发者想获得这方面的能力,可以使用 Electron 提供的 WebRequest 对象的方法:
const { BrowserWindow } = require('electron');const win = new BrowserWindow({ width: 800, height: 600 });win.webContents.session.webRequest.onBeforeRequest({urls: ['https://*/*'],},(details, cb) => {if (details.url === 'https://targetDomain.com/vendors.js') {cb({redirectURL: 'http://yourDomain.com/vendors.js',});} else {cb({});}});win.loadURL('http://github.com');
在上面的代码中,我们使用了 WebRequest 的 onBeforeRequest
方法监听第三方网页的请求,当请求发生时,判断请求的路径是否为我们关注的路径,如果是,则把请求转发到另一个地址,新的地址往往是我们服务器的一个地址。
在加载 URL 的时候更改 User-Agent:
win.webContents.loadURL('https://www.baidu.com', {userAgent: 'Mozilla/5.0(Windows NT 10.0; Win64; x64; rv:69.8) Gecko/20200112 Firefox/69.8',});
除了修改请求头,你的脚本源码最好也加密,免得留下蛛丝马迹。
可以修改 app.userAgentFallback
属性的值来全局设置 User-Agent,修改后你的 Electron 程序内的所有请求的会使用此 UA。
系统内进程间通信一般会使用 IPC 命名管道技术实现(此类技术在 UNIX 系统下被称为域套接字),Electron 并没有提供相应的 API。
ICP 命名管道区分客户端和服务端,其中服务端用于监听和接收数据,客户端主要用于连接和发送数据。服务端和客户端时可以做到持久双向通信的。
假设有一个第三方的程序,需要发送数据给我们的程序,我们可以在 Electron 应用中创建一个命名管道服务以接收数据,代码如下:
const net = require('net');const PIPE_PATH = '\\\\.\\ pipe\\ mypipe';const server = net.createServer(function (conn) {conn.on('data', (data) => console.log(`接收到数据:${data.toString()}`));conn.on('end', () => console.log('客户端已关闭连接'));conn.write('当客户端建立连接后,发送此字符串数据给客户端');});server.on('close', () => console.log('服务关闭'));server.listen(PIPE_PATH, () => console.log('服务启动,正在监听'));
假设第三方程序开启了命名管道服务,需要我们的程序给它发送数据,那么可以在我们的应用中创建一个命名管道客户端:
const net = require('net');const PIPE_PATH = '\\\\.\\ pipe\\ mypipe';const client = net.connect(PIPE_PATH, () => {console.log('连接建立成功');client.write('这是我发送的第一个数据包');});client.on('data', (data) => {console.log(`接收到的数据:${data.toString()}`);client.end('这是我发送的第二个数据包,发送完之后我将关闭');});client.on('end', () => console.log('服务端关闭了连接'));
除了命名管道外,你还可以通过 socket、剪切板、共享文件(通过监控文件的变化来实现应用间通信)等方法来与其他进程通信,但最常见的还是命名管道。
在 Electron 应用程序内启动一个 HTTP 服务,然后再在网页内跨域访问这个 HTTP 服务,即可完成网页与 Electron 应用的通信。
const http = require('http');const server = http.createServer((request, response) => {if (request.url === '/helloElectron') {let jsonString = '';request.on('data', (data) => jsonString);request.on('end', () => {let post = JSON.parse(jsonString);// 处理业务逻辑response.writeHead(200, {'Content-Type': 'text/html','Access-Control-Allow-Origin': '*',});const result = JSON.stringify({ok: true,msg: '请求成功',});response.end(result);});return;}});server.on('error', (err) => {// 此处可把错误信息发送给渲染进程,由渲染进程显示错误信息给用户});server.listen(9416);
程序通过 http.createServer
创建了一个 Web 服务,该服务监听本机的 9416 端口。注意,createServer
方法返回的 server
实例应该是全局对象或者挂在在全局对象,目的是避免被垃圾回收器回收。
当网页请求 http://localhost:9416/helloElectron
时,Electron 应用会接收到该请求,并获取到网页发送的数据,同时通知 response
的 writeHead
和 end
方法给网页响应数据。
在实际应用中,端口很可能被系统其他程序占用,所以如果商用产品一定要先确认端口可用,再进行监听。
你可以给 server.listen
方法传入 0 或什么都不传,让 Node.js 自动给你选一个可用的端口进行监听。一旦 Node.js 找到可用的端口,开始监听后,会触发 listening
事件。在此事件中你可以获取 Node.js 分配的端口号。
server.on('listening', () => {console.log(server.address().port);});server.listen(0);
你可以把端口上报网页服务端,然后再由服务端下发给网页前端应用,此时网页与 Electron 应用通信,就知道要请求什么地址了。
Electron 允许开发者自定义类似 HTTP 或 File 的协议,当应用内出现此类协议的请求时,开发者可以定义拦截函数,处理并响应请求。
在自定义协议前,我们需要告知 Electron 我们打算声明一个怎样的协议。这个协议具备一定的特权,这里的特权时指该协议下的内容不受 CSP 限制,可以使用内置的 Fetch API 等。
const { protocol } = require('electron');const options = [{ shceme: 'app', privileges: { secure: true, standard: true } }];
此代码务必在程序启动之初,app
的 ready
事件触发之前执行,且只能执行一次。代码中,通过 shceme
属性指定了协议名称为 app
,与 http://
类似。
注册这个自定义协议:
const { protocol } = require('electron');const path = require('path');const { readFile } = require('fs');const { URL } = require('url');protocol.registerBufferProtocol('app',(request, respond) => {let pathName = new URL(request.url).pathname;pathName = decodeURI(pathName);const fullName = path.join(__dirname, pathName);readFile(fullName, (err, data) => {if (error) console.log(error);const extension = path.extname(pathName).toLowerCase();let mimeType = '';switch (extension) {case '.js':mimeType = 'text/javascript';break;case '.html':mimeType = 'text/html';break;case '.css':mimeType = 'text/css';break;case '.json':mimeType = 'application/json';break;}respond({ mimeType, data });});},(err) => {if (err) {console.log(err);}});
通过 protocol 的 registerBufferProtocol 方法,注册了一个基于缓冲区的协议,reigsterBufferProtocol
方法接收一个回调函数,当用户发起基于 app://
协议的请求时,此回调函数会截获用户的请求。
在回调函数内,先获取到用户请求的文件的绝对路径,然后读取文件内容,接着按指定的 mimeType
响应请求。这其实就是一个简单的静态文件服务。
自定义协议注册完成之后,啾可以通过如下方式使用此自定义协议了:
win.loadURL('app://./index.html');
使用 Electron 内置的 socks 代理访问网络服务。
const result = await win.webContents.session.setProxy({proxyRules: 'socks5://58.218.200.249:2071',});win.loadURL('http://www.ipip.net');
以上仅为单个渲染进程设置代理,如果你需要给整个应用程序设置代理,可以使用如下代码完成:
app.commandLine.appendSwitch('proxy-server', 'socks5://58.218.200.249:2071');