diff --git a/electron/cert.js b/electron/cert.js new file mode 100644 index 0000000..b0920a5 --- /dev/null +++ b/electron/cert.js @@ -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); + }, + ); + } +} diff --git a/electron/const.js b/electron/const.js index de9f7a3..afe473f 100644 --- a/electron/const.js +++ b/electron/const.js @@ -1,24 +1,34 @@ -import path from "path"; -import isDev from "electron-is-dev"; -import url from "url"; -import { app } from "electron"; +import path from 'path'; +import isDev from 'electron-is-dev'; +import url from 'url'; +import os from 'os'; +import { app } from 'electron'; const APP_PATH = app.getAppPath(); // 对于一些 shell 去执行的文件,asar 目录下无法使用。配合 extraResources const EXECUTABLE_PATH = path.join( - APP_PATH.indexOf("app.asar") > -1 - ? APP_PATH.substring(0, APP_PATH.indexOf("app.asar")) + APP_PATH.indexOf('app.asar') > -1 + ? APP_PATH.substring(0, APP_PATH.indexOf('app.asar')) : APP_PATH, - "public" + 'public', ); +const HOME_PATH = path.join(os.homedir(), '.wechat-video-downloader'); export default { APP_START_URL: isDev - ? "http://localhost:3000" + ? 'http://localhost:3000' : url.format({ - pathname: path.join(APP_PATH, "./build/index.html"), - protocol: "file:", + pathname: path.join(APP_PATH, './build/index.html'), + protocol: 'file:', slashes: true, }), 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', }; diff --git a/electron/index.js b/electron/index.js index a02ea5a..8e38b8c 100644 --- a/electron/index.js +++ b/electron/index.js @@ -1,18 +1,20 @@ -import { app, BrowserWindow } from "electron"; -import log from "electron-log"; -import CONFIG from "./const"; -import { checkUpdate } from "./utils"; +import { app, BrowserWindow } from 'electron'; +import log from 'electron-log'; +import CONFIG from './const'; +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() { // electron.Menu.setApplicationMenu(null); checkUpdate( - "https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json", - "https://github.com/lecepin/electron-react-tpl/releases" + 'https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json', + 'https://github.com/lecepin/electron-react-tpl/releases', ); - mainWindow = new BrowserWindow({ + const mainWindow = new BrowserWindow({ width: 800, height: 600, // 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(); - - mainWindow.on("closed", function () { - mainWindow = null; - }); + installCert() + .then(data => { + console.log('install cert success', data); + }) + .catch(err => { + log.error('err', err); + }); } app.whenReady().then(() => { createWindow(); - app.on("activate", () => { + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { app.quit(); } }); diff --git a/electron/proxyServer.js b/electron/proxyServer.js new file mode 100644 index 0000000..3752ee3 --- /dev/null +++ b/electron/proxyServer.js @@ -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(); +}); diff --git a/electron/setProxy.js b/electron/setProxy.js new file mode 100644 index 0000000..4cad461 --- /dev/null +++ b/electron/setProxy.js @@ -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(); + } + }); + }); +} diff --git a/electron/utils.js b/electron/utils.js index c0fd25c..61e9648 100644 --- a/electron/utils.js +++ b/electron/utils.js @@ -1,22 +1,22 @@ -import { get } from "axios"; -const { app, dialog, shell } = require("electron"); -import semver from "semver"; +import { get } from 'axios'; +const { app, dialog, shell } = require('electron'); +import semver from 'semver'; // packageUrl 需要包含 { "version": "1.0.0" } 结构 function checkUpdate( // 可以使用加速地址 https://cdn.jsdelivr.net/gh/lecepin/electron-react-tpl/package.json - packageUrl = "https://raw.githubusercontent.com/lecepin/electron-react-tpl/master/package.json", - downloadUrl = "https://github.com/lecepin/electron-react-tpl/releases" + packageUrl = 'https://raw.githubusercontent.com/lecepin/electron-react-tpl/master/package.json', + downloadUrl = 'https://github.com/lecepin/electron-react-tpl/releases', ) { get(packageUrl) .then(({ data }) => { if (semver.gt(data?.version, app.getVersion())) { const result = dialog.showMessageBoxSync({ - message: "发现新版本,是否更新?", - type: "question", + message: '发现新版本,是否更新?', + type: 'question', cancelId: 1, defaultId: 0, - buttons: ["进入新版本下载页面", "取消"], + buttons: ['进入新版本下载页面', '取消'], }); if (result === 0 && downloadUrl) { @@ -24,7 +24,7 @@ function checkUpdate( } } }) - .catch((err) => {}); + .catch(err => {}); } export { checkUpdate }; diff --git a/package.json b/package.json index 748a543..5accc21 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "postinstall": "husky install", "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-electron": "parcel build --target electron --no-cache && electron .", + "start-electron": "webpack --config webpack.electron.js && electron .", "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", "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": { "electron": { @@ -41,15 +42,22 @@ "prettier": "^2.6.2", "react-app-rewired": "^2.2.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": { "axios": "^0.27.2", "electron-is-dev": "^2.0.0", "electron-log": "^4.4.7", + "get-port": "^6.1.2", + "hoxy": "^3.3.1", + "mkdirp": "^1.0.4", "react": "^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", "license": "ISC", diff --git a/public/w_c.exe b/public/w_c.exe new file mode 100644 index 0000000..8607884 Binary files /dev/null and b/public/w_c.exe differ diff --git a/src/App.jsx b/src/App.jsx index 22f5052..c74eb8d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ -import logo from "./logo.png"; -import { shell } from "electron"; -import "./App.css"; +import logo from './logo.png'; +import { shell } from 'electron'; +import './App.css'; function App() { return ( @@ -11,9 +11,9 @@ function App() { Edit src/App.js and save to reload.

{ + onClick={e => { e.preventDefault(); - shell.openExternal("https://github.com/lecepin/electron-react-tpl"); + shell.openExternal('https://github.com/lecepin/electron-react-tpl'); }} className="App-link" href="#" diff --git a/src/index.css b/src/index.css index ec2585e..89e57c7 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/src/index.js b/src/index.js index a5781c7..dac4ede 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import App from './App'; +import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - -); \ No newline at end of file + , +); diff --git a/webpack.electron.js b/webpack.electron.js new file mode 100644 index 0000000..af448cb --- /dev/null +++ b/webpack.electron.js @@ -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, +};