import { app, ipcMain, Notification } from "electron"; import { getConfig } from "../storage/config"; import { listProxy } from "../storage/proxy"; import { getVersionById } from "../storage/version"; import treeKill from "tree-kill"; const fs = require("fs"); const path = require("path"); const { exec, spawn } = require("child_process"); const log = require("electron-log"); export let frpcProcess = null; const runningCmd = { commandPath: null, configPath: null }; let frpcStatusListener = null; /** * 获取选择版本的工作目录 * @param versionId 版本ID * @param callback */ const getFrpcVersionWorkerPath = ( versionId: number, callback: (workerPath: string) => void ) => { getVersionById(versionId, (err2, version) => { if (!err2) { if (version) { callback(version["frpcVersionPath"]); } } }); }; const isRangePort = (m: Proxy) => { return ( (m.localPort.indexOf("-") !== -1 || m.localPort.indexOf(",") !== -1) && (m.type === "tcp" || m.type === "udp") ); }; /** * 生成toml配置文件 * @param config * @param proxys */ export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => { const proxyToml = proxys.map(m => { const rangePort = isRangePort(m); let toml = ` ${ rangePort ? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}` : "" } [[${m.type === "stcp" && m.stcpModel === "visitors" ? "visitors" : "proxies"}]] ${rangePort ? "" : `name = "${m.name}"\n`} type = "${m.type}" `; switch (m.type) { case "tcp": case "udp": if (rangePort) { toml += ` name = "${m.name}-{{ $v.First }}" localPort = {{ $v.First }} remotePort = {{ $v.Second }} `; } else { toml += ` localIP = "${m.localIp}" localPort = ${m.localPort} remotePort = ${m.remotePort} `; } break; case "http": case "https": toml += ` localIP = "${m.localIp}" localPort = ${m.localPort} customDomains=[${m.customDomains.map(m => `"${m}"`)}] `; break; case "stcp": if (m.stcpModel === "visitors") { // 访问者 toml += ` serverName = "${m.serverName}" bindAddr = "${m.bindAddr}" bindPort = ${m.bindPort} `; } else if (m.stcpModel === "visited") { // 被访问者 toml += ` localIP = "${m.localIp}" localPort = ${m.localPort}`; } toml += ` secretKey="${m.secretKey}" `; break; default: break; } if (rangePort) { toml += `{{- end }}`; } return toml; }); const toml = ` serverAddr = "${config.serverAddr}" serverPort = ${config.serverPort} ${ config.authMethod === "token" ? ` auth.method = "token" auth.token = "${config.authToken}" ` : "" } ${ config.authMethod === "multiuser" ? ` user = "${config.user}" metadatas.token = "${config.metaToken}" ` : "" } ${ config.transportHeartbeatInterval ? ` transport.heartbeatInterval = ${config.transportHeartbeatInterval} ` : "" } ${ config.transportHeartbeatTimeout ? ` transport.heartbeatTimeout = ${config.transportHeartbeatTimeout} ` : "" } log.to = "frpc.log" log.level = "${config.logLevel}" log.maxDays = ${config.logMaxDays} webServer.addr = "127.0.0.1" webServer.port = 57400 transport.tls.enable = ${config.tlsConfigEnable} ${ config.tlsConfigEnable && config.tlsConfigCertFile ? ` transport.tls.certFile = "${config.tlsConfigCertFile}" ` : "" } ${ config.tlsConfigEnable && config.tlsConfigKeyFile ? ` transport.tls.keyFile = "${config.tlsConfigKeyFile}" ` : "" } ${ config.tlsConfigEnable && config.tlsConfigTrustedCaFile ? ` transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}" ` : "" } ${ config.tlsConfigEnable && config.tlsConfigServerName ? ` transport.tls.serverName = "${config.tlsConfigServerName}" ` : "" } ${ config.proxyConfigEnable ? ` transport.proxyURL = "${config.proxyConfigProxyUrl}" ` : "" } ${proxyToml.join("")} `; return toml; }; /** * 生成ini配置 * @param config * @param proxys */ export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => { const proxyIni = proxys.map(m => { const rangePort = isRangePort(m); let ini = ` [${rangePort ? 'range:' : ''}${m.name}] type = "${m.type}" `; switch (m.type) { case "tcp": case "udp": ini += ` local_ip = "${m.localIp}" local_port = ${m.localPort} remote_port = ${m.remotePort} `; break; case "http": case "https": ini += ` local_ip = "${m.localIp}" local_port = ${m.localPort} custom_domains=[${m.customDomains.map(m => `"${m}"`)}] `; break; case "stcp": if (m.stcpModel === "visitors") { // 访问者 ini += ` role = visitor server_name = "${m.serverName}" bind_addr = "${m.bindAddr}" bind_port = ${m.bindPort} `; } else if (m.stcpModel === "visited") { // 被访问者 ini += ` local_ip = "${m.localIp}" local_port = ${m.localPort}`; } ini += ` sk="${m.secretKey}" `; break; default: break; } return ini; }); const ini = ` [common] server_addr = ${config.serverAddr} server_port = ${config.serverPort} ${ config.authMethod === "token" ? ` authentication_method = ${config.authMethod} token = ${config.authToken} ` : "" } ${ config.authMethod === "multiuser" ? ` user = ${config.user} meta_token = ${config.metaToken} ` : "" } ${ config.transportHeartbeatInterval ? ` heartbeat_interval = ${config.transportHeartbeatInterval} ` : "" } ${ config.transportHeartbeatTimeout ? ` heartbeat_timeout = ${config.transportHeartbeatTimeout} ` : "" } log_file = "frpc.log" log_level = ${config.logLevel} log_max_days = ${config.logMaxDays} admin_addr = 127.0.0.1 admin_port = 57400 tls_enable = ${config.tlsConfigEnable} ${ config.tlsConfigEnable && config.tlsConfigCertFile ? ` tls_cert_file = ${config.tlsConfigCertFile} ` : "" } ${ config.tlsConfigEnable && config.tlsConfigKeyFile ? ` tls_key_file = ${config.tlsConfigKeyFile} ` : "" } ${ config.tlsConfigEnable && config.tlsConfigTrustedCaFile ? ` tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile} ` : "" } ${ config.tlsConfigEnable && config.tlsConfigServerName ? ` tls_server_name = ${config.tlsConfigServerName} ` : "" } ${ config.proxyConfigEnable ? ` http_proxy = "${config.proxyConfigProxyUrl}" ` : "" } ${proxyIni.join("")} `; return ini; }; /** * 生成配置文件 */ export const generateConfig = ( config: FrpConfig, callback: (configPath: string) => void ) => { listProxy((err3, proxys) => { if (!err3) { const { currentVersion } = config; let filename = null; let configContent = ""; const filtered = proxys .map(m => { if (m.status == null || m.status == undefined) { m.status = true; } return m; }) .filter(f => f.status); if (currentVersion < 124395282) { // 版本小于v0.52.0 filename = "frp.ini"; configContent = genIniConfig(config, filtered); } else { filename = "frp.toml"; configContent = genTomlConfig(config, filtered); } const configPath = path.join(app.getPath("userData"), filename); log.info(`生成配置成功 配置路径:${configPath}`); fs.writeFile( configPath, // 配置文件目录 configContent, // 配置文件内容 { flag: "w" }, err => { if (!err) { callback(filename); } } ); } }); }; /** * 启动frpc子进程 * @param cwd * @param commandPath * @param configPath */ const startFrpcProcess = (commandPath: string, configPath: string) => { log.info(`启动frpc 目录:${app.getPath("userData")} 命令:${commandPath}`); const command = `${commandPath} -c ${configPath}`; frpcProcess = spawn(command, { cwd: app.getPath("userData"), shell: true }); runningCmd.commandPath = commandPath; runningCmd.configPath = configPath; frpcProcess.stdout.on("data", data => { log.debug(`启动输出:${data}`); }); frpcProcess.stdout.on("error", data => { log.error(`启动错误:${data}`); stopFrpcProcess(() => {}); }); frpcStatusListener = setInterval(() => { const status = frpcProcessStatus(); log.debug(`监听frpc子进程状态:${status} ${frpcStatusListener}`); if (!status) { new Notification({ title: "Frpc Desktop", body: "连接已断开,请前往日志查看原因" }).show(); clearInterval(frpcStatusListener); } }, 3000); }; /** * 重载frpc配置 */ export const reloadFrpcProcess = () => { if (frpcProcess && !frpcProcess.killed) { getConfig((err1, config) => { if (!err1) { if (config) { generateConfig(config, configPath => { const command = `${runningCmd.commandPath} reload -c ${configPath}`; log.info(`重载配置:${command}`); exec(command, { cwd: app.getPath("userData"), shell: true }); }); } } }); } }; /** * 停止frpc子进程 */ export const stopFrpcProcess = (callback?: () => void) => { if (frpcProcess) { treeKill(frpcProcess.pid, (error: Error) => { if (error) { log.error(`关闭frpc子进程失败 pid:${frpcProcess.pid} error:${error}`); callback(); } else { log.info(`关闭frpc子进程成功`); frpcProcess = null; clearInterval(frpcStatusListener); callback(); } }); } else { callback(); } }; /** * 获取frpc子进程状态 */ export const frpcProcessStatus = () => { if (!frpcProcess) { return false; } try { // 发送信号给进程,如果进程存在,会正常返回 process.kill(frpcProcess.pid, 0); return true; } catch (error) { // 进程不存在,抛出异常 return false; } }; /** * 启动frpc流程 * @param config */ export const startFrpWorkerProcess = async (config: FrpConfig) => { getFrpcVersionWorkerPath(config.currentVersion, (frpcVersionPath: string) => { if (frpcVersionPath) { generateConfig(config, configPath => { const platform = process.platform; if (platform === "win32") { startFrpcProcess(path.join(frpcVersionPath, "frpc.exe"), configPath); } else { startFrpcProcess(path.join(frpcVersionPath, "frpc"), configPath); } }); } }); }; export const initFrpcApi = () => { ipcMain.handle("frpc.running", async (event, args) => { return frpcProcessStatus(); }); ipcMain.on("frpc.start", async (event, args) => { getConfig((err1, config) => { if (!err1) { if (!config) { event.reply( "Home.frpc.start.error.hook", "请先前往设置页面,修改配置后再启动" ); return; } if (!config.currentVersion) { event.reply( "Home.frpc.start.error.hook", "请先前往设置页面,修改配置后再启动" ); return; } startFrpWorkerProcess(config); } }); }); ipcMain.on("frpc.stop", () => { if (frpcProcess && !frpcProcess.killed) { stopFrpcProcess(() => {}); } }); };