feat: 代理/证书/拦截

This commit is contained in:
lecepin 2022-05-20 18:49:04 +08:00
parent 7a4e26c8ce
commit 08253c9e49
12 changed files with 286 additions and 53 deletions

42
electron/cert.js Normal file
View File

@ -0,0 +1,42 @@
import CONFIG from './const';
import mkdirp from 'mkdirp';
import fs from 'fs';
import path from 'path';
import sudo from 'sudo-prompt';
import { clipboard, dialog } from 'electron';
function checkCertInstalled() {
return fs.existsSync(CONFIG.INSTALL_CERT_FLAG);
}
export async function installCert(checkInstalled = true) {
if (checkInstalled && checkCertInstalled()) {
return;
}
if (process.platform === 'darwin') {
return new Promise((resolve, reject) => {
mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG));
clipboard.writeText(
`echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CONFIG.CERT_PUBLIC_PATH}" && touch ${CONFIG.INSTALL_CERT_FLAG} && echo "安装完成"`,
);
dialog.showMessageBoxSync({
type: 'info',
message: `命令已复制到剪贴板,粘贴命令到终端并运行以安装并信任证书`,
});
reject();
});
} else {
return sudo.exec(
`${CONFIG.WIN_CERT_INSTALL_HELPER} -c -add ${CONFIG.CERT_PUBLIC_PATH} -s root`,
{ name: CONFIG.APP_EN_NAME },
(error, stdout) => {
if (error) {
reject(error);
}
resolve(stdout);
},
);
}
}

View File

@ -1,24 +1,34 @@
import path from "path"; import path from 'path';
import isDev from "electron-is-dev"; import isDev from 'electron-is-dev';
import url from "url"; import url from 'url';
import { app } from "electron"; import os from 'os';
import { app } from 'electron';
const APP_PATH = app.getAppPath(); const APP_PATH = app.getAppPath();
// 对于一些 shell 去执行的文件asar 目录下无法使用。配合 extraResources // 对于一些 shell 去执行的文件asar 目录下无法使用。配合 extraResources
const EXECUTABLE_PATH = path.join( const EXECUTABLE_PATH = path.join(
APP_PATH.indexOf("app.asar") > -1 APP_PATH.indexOf('app.asar') > -1
? APP_PATH.substring(0, APP_PATH.indexOf("app.asar")) ? APP_PATH.substring(0, APP_PATH.indexOf('app.asar'))
: APP_PATH, : APP_PATH,
"public" 'public',
); );
const HOME_PATH = path.join(os.homedir(), '.wechat-video-downloader');
export default { export default {
APP_START_URL: isDev APP_START_URL: isDev
? "http://localhost:3000" ? 'http://localhost:3000'
: url.format({ : url.format({
pathname: path.join(APP_PATH, "./build/index.html"), pathname: path.join(APP_PATH, './build/index.html'),
protocol: "file:", protocol: 'file:',
slashes: true, slashes: true,
}), }),
IS_DEV: isDev, IS_DEV: isDev,
EXECUTABLE_PATH,
HOME_PATH,
CERT_PRIVATE_PATH: path.join(EXECUTABLE_PATH, './keys/private.pem'),
CERT_PUBLIC_PATH: path.join(EXECUTABLE_PATH, './keys/public.pem'),
INSTALL_CERT_FLAG: path.join(HOME_PATH, './installed.lock'),
WIN_CERT_INSTALL_HELPER: path.join(EXECUTABLE_PATH, './w_c.exe'),
APP_CN_NAME: '微信视频号下载器',
APP_EN_NAME: 'WeChat Video Downloader',
}; };

View File

@ -1,18 +1,20 @@
import { app, BrowserWindow } from "electron"; import { app, BrowserWindow } from 'electron';
import log from "electron-log"; import log from 'electron-log';
import CONFIG from "./const"; import CONFIG from './const';
import { checkUpdate } from "./utils"; import { checkUpdate } from './utils';
import { startServer } from './proxyServer';
import { installCert } from './cert';
app.commandLine.appendSwitch("--no-proxy-server"); app.commandLine.appendSwitch('--no-proxy-server');
function createWindow() { function createWindow() {
// electron.Menu.setApplicationMenu(null); // electron.Menu.setApplicationMenu(null);
checkUpdate( checkUpdate(
"https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json", 'https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json',
"https://github.com/lecepin/electron-react-tpl/releases" 'https://github.com/lecepin/electron-react-tpl/releases',
); );
mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
// resizable: false, // resizable: false,
@ -24,26 +26,29 @@ function createWindow() {
}, },
}); });
mainWindow.loadURL(CONFIG.APP_START_URL); mainWindow.loadURL('https://baidu.com' ?? CONFIG.APP_START_URL);
CONFIG.IS_DEV && mainWindow.webContents.openDevTools(); CONFIG.IS_DEV && mainWindow.webContents.openDevTools();
installCert()
mainWindow.on("closed", function () { .then(data => {
mainWindow = null; console.log('install cert success', data);
})
.catch(err => {
log.error('err', err);
}); });
} }
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
app.on("activate", () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();
} }
}); });
}); });
app.on("window-all-closed", () => { app.on('window-all-closed', () => {
if (process.platform !== "darwin") { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
}); });

37
electron/proxyServer.js Normal file
View File

@ -0,0 +1,37 @@
import path from 'path';
import fs from 'fs';
import hoxy from 'hoxy';
import getPort from 'get-port';
import { app } from 'electron';
import CONFIG from './const';
import { setProxy, closeProxy } from './setProxy';
export async function startServer({ interceptCallback = f => f, errorCallback = f => f }) {
const port = await getPort();
const proxy = hoxy
.createServer({
certAuthority: {
key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH),
cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH),
},
})
.listen(port, () => {
setProxy('127.0.0.1', port).catch(errorCallback);
});
proxy.intercept(
{
phase: 'request',
},
interceptCallback,
);
}
app.on('before-quit', async e => {
e.preventDefault();
try {
await closeProxy();
} catch (error) {}
app.exit();
});

118
electron/setProxy.js Normal file
View File

@ -0,0 +1,118 @@
import { exec } from 'child_process';
import regedit from 'regedit';
export async function setProxy(host, port) {
if (process.platform === 'darwin') {
const networks = await getMacAvailableNetworks();
if (networks.length === 0) {
throw 'no network';
}
return Promise.all(
networks.map(network => {
return new Promise((resolve, reject) => {
exec(`networksetup -setsecurewebproxy "${network}" ${host} ${port}`, error => {
if (error) {
reject(null);
} else {
resolve(network);
}
});
});
}),
);
} else {
const valuesToPut = {
'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings': {
ProxyServer: {
value: `${host}:${port}`,
type: 'REG_SZ',
},
ProxyEnable: {
value: 1,
type: 'REG_DWORD',
},
},
};
return editWinRegPromise(valuesToPut);
}
}
export async function closeProxy() {
if (process.platform === 'darwin') {
const networks = await getMacAvailableNetworks();
if (networks.length === 0) {
throw 'no network';
}
return Promise.all(
networks.map(network => {
return new Promise((resolve, reject) => {
exec(`networksetup -setsecurewebproxystate "${network}" off`, error => {
if (error) {
reject(null);
} else {
resolve(network);
}
});
});
}),
);
} else {
const valuesToPut = {
'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings': {
ProxyEnable: {
value: 0,
type: 'REG_DWORD',
},
},
};
return editWinRegPromise(valuesToPut);
}
}
function getMacAvailableNetworks() {
return new Promise((resolve, reject) => {
exec('networksetup -listallnetworkservices', (error, stdout) => {
if (error) {
reject(error);
} else {
Promise.all(
stdout
.toString()
.split('\n')
.map(network => {
return new Promise(resolve => {
exec(
`networksetup getinfo "${network}" | grep "^IP address:\\s\\d"`,
(error, stdout) => {
if (error) {
resolve(null);
} else {
resolve(stdout ? network : null);
}
},
);
});
}),
).then(networks => {
resolve(networks.filter(Boolean));
});
}
});
});
}
function editWinRegPromise(valuesToPut) {
return new Promise((resolve, reject) => {
regedit.putValue(valuesToPut, function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

View File

@ -1,22 +1,22 @@
import { get } from "axios"; import { get } from 'axios';
const { app, dialog, shell } = require("electron"); const { app, dialog, shell } = require('electron');
import semver from "semver"; import semver from 'semver';
// packageUrl 需要包含 { "version": "1.0.0" } 结构 // packageUrl 需要包含 { "version": "1.0.0" } 结构
function checkUpdate( function checkUpdate(
// 可以使用加速地址 https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json // 可以使用加速地址 https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json
packageUrl = "https://raw.githubusercontent.com/lecepin/electron-react-tpl/master/package.json", packageUrl = 'https://raw.githubusercontent.com/lecepin/electron-react-tpl/master/package.json',
downloadUrl = "https://github.com/lecepin/electron-react-tpl/releases" downloadUrl = 'https://github.com/lecepin/electron-react-tpl/releases',
) { ) {
get(packageUrl) get(packageUrl)
.then(({ data }) => { .then(({ data }) => {
if (semver.gt(data?.version, app.getVersion())) { if (semver.gt(data?.version, app.getVersion())) {
const result = dialog.showMessageBoxSync({ const result = dialog.showMessageBoxSync({
message: "发现新版本,是否更新?", message: '发现新版本,是否更新?',
type: "question", type: 'question',
cancelId: 1, cancelId: 1,
defaultId: 0, defaultId: 0,
buttons: ["进入新版本下载页面", "取消"], buttons: ['进入新版本下载页面', '取消'],
}); });
if (result === 0 && downloadUrl) { if (result === 0 && downloadUrl) {
@ -24,7 +24,7 @@ function checkUpdate(
} }
} }
}) })
.catch((err) => {}); .catch(err => {});
} }
export { checkUpdate }; export { checkUpdate };

View File

@ -8,12 +8,13 @@
"postinstall": "husky install", "postinstall": "husky install",
"start": "concurrently \"cross-env BROWSER=none npm run start-web\" \"wait-on http://localhost:3000 && npm run start-electron\" ", "start": "concurrently \"cross-env BROWSER=none npm run start-web\" \"wait-on http://localhost:3000 && npm run start-electron\" ",
"start-web": "react-app-rewired start", "start-web": "react-app-rewired start",
"start-electron": "parcel build --target electron --no-cache && electron .", "start-electron": "webpack --config webpack.electron.js && electron .",
"build-web": "react-app-rewired build", "build-web": "react-app-rewired build",
"build-electron": "parcel build --target electron --no-cache", "build-electron": "webpack --config webpack.electron.js",
"build-all": "rm -rf ./build && rm -rf ./build-electron && npm run build-electron && npm run build-web", "build-all": "rm -rf ./build && rm -rf ./build-electron && npm run build-electron && npm run build-web",
"pack": "npm run build-all && electron-builder", "pack": "npm run build-all && electron-builder",
"gen-icon": "electron-icon-builder --input=./public/icon/icon.png --output=./public/icon" "gen-icon": "electron-icon-builder --input=./public/icon/icon.png --output=./public/icon",
"pretty": "prettier -c --write \"(src/**/*|electron/**/*)\""
}, },
"targets": { "targets": {
"electron": { "electron": {
@ -41,15 +42,22 @@
"prettier": "^2.6.2", "prettier": "^2.6.2",
"react-app-rewired": "^2.2.1", "react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"wait-on": "^6.0.1" "wait-on": "^6.0.1",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"electron-log": "^4.4.7", "electron-log": "^4.4.7",
"get-port": "^6.1.2",
"hoxy": "^3.3.1",
"mkdirp": "^1.0.4",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"semver": "^7.3.7" "regedit": "^5.1.1",
"semver": "^7.3.7",
"sudo-prompt": "^9.2.1"
}, },
"author": "lecepin", "author": "lecepin",
"license": "ISC", "license": "ISC",

BIN
public/w_c.exe Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
import logo from "./logo.png"; import logo from './logo.png';
import { shell } from "electron"; import { shell } from 'electron';
import "./App.css"; import './App.css';
function App() { function App() {
return ( return (
@ -11,9 +11,9 @@ function App() {
Edit <code>src/App.js</code> and save to reload. Edit <code>src/App.js</code> and save to reload.
</p> </p>
<a <a
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
shell.openExternal("https://github.com/lecepin/electron-react-tpl"); shell.openExternal('https://github.com/lecepin/electron-react-tpl');
}} }}
className="App-link" className="App-link"
href="#" href="#"

View File

@ -1,13 +1,11 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
monospace;
} }

View File

@ -7,5 +7,5 @@ const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>,
); );

15
webpack.electron.js Normal file
View File

@ -0,0 +1,15 @@
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, './electron/index.js'),
output: {
filename: 'index.js',
path: path.resolve(__dirname, './build-electron'),
},
module: {
rules: [],
},
devtool: false && 'source-map',
target: 'electron-main',
node: false,
};