Compare commits

...

50 Commits

Author SHA1 Message Date
刘嘉伟
64828404fb
Merge pull request #76 from luckjiawei/luckjiawei-patch-1
Update README.md
2025-02-21 10:36:48 +08:00
刘嘉伟
7681dc6350
Update README.md 2025-02-21 10:36:05 +08:00
刘嘉伟
08c2a2cb5a
Merge pull request #68 from locolocoer/main
bugfix: httpstohttp path error in windows
2025-02-13 14:33:43 +08:00
Daxian_feng
f0b420fe2e
Update webpack.yml 2025-01-24 21:58:18 +08:00
locolocoer
3bfb96b091 bugfix:path error in windows 2025-01-24 15:29:15 +08:00
Daxian_feng
29ddfe41f5
Create webpack.yml 2025-01-24 14:28:31 +08:00
刘嘉伟
5d124c7efb 🍻 add trending badge 2025-01-15 23:32:04 +08:00
刘嘉伟
40de47640c 💸 increase donation records 2025-01-15 23:27:42 +08:00
刘嘉伟
ea02338102 🔖 v1.1.6 2025-01-09 14:38:17 +08:00
刘嘉伟
816dcd4481 💸 update donation list 2025-01-09 14:38:06 +08:00
刘嘉伟
ced3bda8bb 🎉 vscode config 2025-01-09 14:31:34 +08:00
刘嘉伟
59be2d2173 🐛 Add the keepTunnelOpen parameter 2025-01-09 12:25:21 +08:00
刘嘉伟
b1dbd46db4 🐛 ini improvement 2025-01-09 12:23:05 +08:00
刘嘉伟
f25dec4c87 xtcp keepTunnelOpen 2025-01-09 12:10:55 +08:00
刘嘉伟
27c1d42596 🔊 Improve the log 2025-01-09 11:40:12 +08:00
刘嘉伟
58f625baf4 Supports importing frp 2025-01-09 11:34:34 +08:00
刘嘉伟
4bad940aff 💄 Icon optimization 2025-01-09 11:34:15 +08:00
刘嘉伟
d4f743663b Supports importing frp 2025-01-09 11:33:48 +08:00
刘嘉伟
4c0a6de1e9 Add JSON extraction script and generate SHA256 checksums: Introduced a new script to extract release information from a JSON file and fetch corresponding SHA256 checksums from remote assets. The checksums are saved in a new JSON file for easy access and verification. Enhanced logging for better traceability during the extraction process. 2025-01-08 14:51:14 +08:00
刘嘉伟
c359714bc5 Add new 'link' icon to IconifyIcon and update proxy view: Introduced the 'link' icon in the IconifyIcon component and replaced the 'content-copy' icon with 'link' in the proxy view for improved user interface consistency. 2025-01-08 14:51:03 +08:00
刘嘉伟
ffb42bd131 🔊 Refactor logging across multiple modules: Updated logging statements to utilize a new structured logging utility for improved traceability and consistency. Enhanced error handling and added informative logs for database operations, configuration management, and proxy handling. Masked sensitive data in logs to improve security during configuration operations. 2025-01-08 14:17:05 +08:00
刘嘉伟
9ab29259b1 🔊 Refactor logging in GitHub API: Replaced deprecated logging with a new logging utility for improved traceability. Updated log statements to use structured logging for mirror URL requests and response status codes, enhancing clarity and consistency in logs. 2025-01-08 13:53:42 +08:00
刘嘉伟
9ad8773b4d 🐛 Mirror source improvement 2025-01-08 13:50:51 +08:00
刘嘉伟
a34a1886de 🐛 Fixing the issue where frpc still runs after exiting with command + q. 2025-01-08 13:39:43 +08:00
刘嘉伟
f4f18afb7a 🔊 Improve the log 2025-01-08 12:07:42 +08:00
刘嘉伟
fcb01e83b2 🔊 Improve the log 2025-01-08 12:05:15 +08:00
刘嘉伟
cfba180603 Improve the log 2025-01-08 12:01:05 +08:00
刘嘉伟
aad6fd78d5 🔊 Improve the log 2025-01-08 12:01:00 +08:00
刘嘉伟
19fde43f8b Improve logging and error handling in API modules: Refactored common, file, and logger APIs to utilize a new logging utility for better traceability. Enhanced error handling for URL opening and file dialog interactions, ensuring informative logs for success and failure cases. Introduced a utility to mask sensitive data in configuration logs, improving security during application initialization. 2025-01-08 11:22:09 +08:00
刘嘉伟
9510a4cb67 Refactor logging and application initialization: Introduced a new logging utility for structured logging across the application. Enhanced the main process to log application events and errors, improving traceability and debugging. Cleaned up the code by removing deprecated comments and unused imports, and ensured proper error handling during configuration retrieval and API initialization. 2025-01-08 10:45:54 +08:00
刘嘉伟
337a3b6831 Enhance GitHub API integration: Added support for dynamic mirror selection and improved version handling. Introduced local JSON fallback for release data and refactored version retrieval logic. Updated UI to reflect mirror changes. 2025-01-07 14:43:58 +08:00
刘嘉伟
91b97df99a 🚧 传输配置完善 2025-01-06 16:14:52 +08:00
刘嘉伟
a1910be29c 增加 https2http 插件 2025-01-05 21:17:56 +08:00
刘嘉伟
3157b935e0 Merge remote-tracking branch 'origin/develop' into develop1226 2024-12-26 18:01:19 +08:00
刘嘉伟
eaa588698a 🚧 htts2http插件应用 2024-12-26 18:00:59 +08:00
刘嘉伟
5e436ccaf1 🚧 htts2http插件应用 2024-12-26 17:46:15 +08:00
刘嘉伟
ae4b346084
Merge pull request #52 from webb-z/main
🐛 修复生成ini配置文件自定义域名错误的问题
2024-12-21 10:32:29 +08:00
webb
d95f7034f8 customDomain map时 不添加" 2024-12-20 19:03:11 +08:00
刘嘉伟
3ed09818ef 💸 更新捐赠列表 2024-12-20 15:02:32 +08:00
刘嘉伟
5f34a64fde 💸 增加捐赠 2024-12-18 17:14:40 +08:00
刘嘉伟
96b161f431 💸 增加捐赠 2024-12-12 17:34:40 +08:00
刘嘉伟
01299b2fa2 💸 增加捐赠 2024-12-11 17:50:53 +08:00
刘嘉伟
827b94f75f 🌱 增加捐赠 2024-12-09 22:08:26 +08:00
刘嘉伟
acfcdfbb16 🚑 修复window下toml目录错误 2024-12-06 17:12:25 +08:00
刘嘉伟
453f41ff19 优化 2024-12-05 12:07:01 +08:00
刘嘉伟
e2a01325f4 🐛 修复显示 2024-12-05 10:56:19 +08:00
刘嘉伟
2581caa9e3 🔖 发布新版本 2024-12-04 17:08:20 +08:00
刘嘉伟
8e0c152fd8 🔖 发布新版本 2024-12-04 17:06:54 +08:00
刘嘉伟
3751a52cd6 Merge branch 'develop' 2024-12-04 17:06:30 +08:00
刘嘉伟
40e4623413 认领HelloGithub 2024-12-02 09:19:36 +08:00
29 changed files with 49742 additions and 843 deletions

28
.github/workflows/webpack.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: NodeJS with Webpack
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.8.0]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
npm install
npx run release

23
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "dev",
"request": "launch",
"command": "npm run dev",
"cwd": "${workspaceFolder}"
},
{
"type": "node-terminal",
"name": "build electron",
"request": "launch",
"command": "npm run build:electron",
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -22,6 +22,9 @@
</p> </p>
<p><a href="https://jwinks.com/p/frp/#frp%E6%98%AF%E4%BB%80%E4%B9%88">使用教程</a></p> <p><a href="https://jwinks.com/p/frp/#frp%E6%98%AF%E4%BB%80%E4%B9%88">使用教程</a></p>
<a href="https://trendshift.io/repositories/12489" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12489" alt="luckjiawei%2Ffrpc-desktop | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/b0dc116e9f2e4b8188da5a6d3e1bd8a4" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=b0dc116e9f2e4b8188da5a6d3e1bd8a4&claim_uid=8ZMOhz30mGJAHpa" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div> </div>
## TODO ## TODO
@ -38,6 +41,7 @@
- [x] 一键清空所有配置 - [x] 一键清空所有配置
- [x] 支持导入识别frpc.toml - [x] 支持导入识别frpc.toml
- [x] tcp、udp协议支持批量端口 - [x] tcp、udp协议支持批量端口
- [ ] support multiple languages
## 常见问题 ## 常见问题
@ -47,6 +51,8 @@
## 里程碑 ## 里程碑
- 2025-01-09: 发布v1.1.6版本
- 2024-12-04: 发布v1.1.5版本 优化体验、支持修改webport、解决github限流问题、日志优化
- 2024-11-08: 发布v1.1.4版本 修复已知BUG - 2024-11-08: 发布v1.1.4版本 修复已知BUG
- 2024-10-14: 发布v1.1.3版本 支持xtcp协议、优化体验 - 2024-10-14: 发布v1.1.3版本 支持xtcp协议、优化体验
- 2024-09-25: 发布v1.1.2版本 支持 http basic、子域名 - 2024-09-25: 发布v1.1.2版本 支持 http basic、子域名
@ -64,13 +70,19 @@
## 社区 ## 社区
~~微信扫描加入开源项目交流群 广告勿进!!!~~ 广告勿进!!!
<img src="screenshots/wechat-qr.png" alt="二维码" width="200"> ### TG
**微信群超过200人无法扫码进群 关注公众号进群 ** [https://t.me/+4kziSBL3LxVmYzVl](https://t.me/+4kziSBL3LxVmYzVl)
<img src="screenshots/mp_qr.jpg" alt="公众号二维码" width="200"> ### 微信群
**~~微信扫描加入开源项目交流群~~ 微信群超过200人无法扫码进群 关注公众号进群**
<img src="screenshots/wechat-qr.png" alt="二维码" width="200"><img src="screenshots/mp_qr.jpg" alt="公众号二维码" width="200">
## 演示 ## 演示
@ -88,16 +100,12 @@
## 捐赠 ## 捐赠
**捐赠方式**
一块两块不嫌少,十块二十那更好。
👉👉👉[点击去捐赠](https://jwinks.com/donate/)👈👈👈 👉👉👉[点击去捐赠](https://jwinks.com/donate/)👈👈👈
**捐赠名单** **捐赠名单**
| 🕰 时间 | 📡 平台 | 🤲 捐赠者 | 💰 金额 | ✉️ 捐赠留言 | | 🕰 时间 | 📡 平台 | 🤲 捐赠者 | 💰 金额 | ✉️ 捐赠留言 |
|------------|-------|-----------|-------|--------------------------| |------------|-------|---------------|---------|--------------------------|
| 2024-08-06 | 微信 | 三木 | 1 元 | 无 | | 2024-08-06 | 微信 | 三木 | 1 元 | 无 |
| 2024-08-25 | 微信 | 晚风 | 1 元 | 无 | | 2024-08-25 | 微信 | 晚风 | 1 元 | 无 |
| 2024-08-27 | 微信 | x | 1 元 | 无 | | 2024-08-27 | 微信 | x | 1 元 | 无 |
@ -115,6 +123,19 @@
| 2024-11-26 | 微信 | Kaori | 1 元 | 谢谢大佬的项目要是能添加web控制页面就更好了 | | 2024-11-26 | 微信 | Kaori | 1 元 | 谢谢大佬的项目要是能添加web控制页面就更好了 |
| 2024-12-03 | 微信 | 17¥ | 20 元 | 谢谢,很方便的软件 | | 2024-12-03 | 微信 | 17¥ | 20 元 | 谢谢,很方便的软件 |
| 2024-12-03 | 微信 | Cr@k3r | 5 元 | 感谢你的工作 | | 2024-12-03 | 微信 | Cr@k3r | 5 元 | 感谢你的工作 |
| 2024-12-09 | 微信 | Vince | 20 元 | 支持国人开发! |
| 2024-12-11 | 支付宝 | **萌 | 20 元 | 加油加油 |
| 2024-12-11 | 支付宝 | *石 | 20 元 | 无 |
| 2024-12-16 | 微信 | 铁汉柔情 | 1 元 | 加油支持国人 |
| 2024-12-16 | 微信 | 亚索🌪️ | 1 元 | 无 |
| 2024-12-17 | 微信 | ppp789 | 1.6 元 | 无 |
| 2024-12-17 | 支付宝 | *涛 | 10 元 | 无 |
| 2024-12-18 | 微信 | 觉远 | 6.66 元 | 开源不易 |
| 2024-12-19 | 微信 | 官方提醒 | 1 元 | 无 |
| 2024-12-19 | 微信 | 木~易 | 6.66 元 | 加油 |
| 2025-01-06 | 微信 | 如是 | 2 元 | 支持开源 |
| 2025-01-13 | 微信 | David Veith | 18.88 元 | 开源无价,么么哒 |
| 2025-01-14 | 微信 | Xterminal SSH | 199 元 | Xterminal SSH 客户端前来支援 |
## 贡献者 ## 贡献者

View File

@ -1,16 +1,26 @@
import { app, ipcMain, shell } from "electron"; import { app, ipcMain, shell } from "electron";
import log from "electron-log"; import { logError, logInfo, LogModule, logWarn } from "../utils/log";
export const initCommonApi = () => { export const initCommonApi = () => {
// 打开链接
ipcMain.on("common.openUrl", async (event, args) => { ipcMain.on("common.openUrl", async (event, args) => {
if (args) { if (args) {
log.info(`打开链接:${args}`); logInfo(LogModule.APP, `Attempting to open URL: ${args}`);
shell.openExternal(args).then(() => {}); try {
await shell.openExternal(args);
logInfo(LogModule.APP, `Successfully opened URL: ${args}`);
} catch (error) {
logError(
LogModule.APP,
`Failed to open URL: ${args}. Error: ${error.message}`
);
}
} else {
logWarn(LogModule.APP, "No URL provided to open.");
} }
}); });
ipcMain.on("common.relaunch", () => { ipcMain.on("common.relaunch", () => {
logInfo(LogModule.APP, "Application is relaunching.");
app.relaunch(); app.relaunch();
app.quit(); app.quit();
}); });

View File

@ -1,25 +1,29 @@
import {app, dialog, ipcMain, shell} from "electron"; import { app, dialog, ipcMain, shell } from "electron";
import {clearConfig, getConfig, saveConfig} from "../storage/config"; import { clearConfig, getConfig, saveConfig } from "../storage/config";
import {clearVersion, listVersion} from "../storage/version"; import { clearVersion, listVersion } from "../storage/version";
import {genIniConfig, genTomlConfig, stopFrpcProcess} from "./frpc"; import { genIniConfig, genTomlConfig, stopFrpcProcess } from "./frpc";
import {clearProxy, insertProxy, listProxy} from "../storage/proxy"; import { clearProxy, insertProxy, listProxy } from "../storage/proxy";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const log = require("electron-log");
const toml = require("@iarna/toml"); const toml = require("@iarna/toml");
const {v4: uuidv4} = require("uuid"); const { v4: uuidv4 } = require("uuid");
export const initConfigApi = win => { export const initConfigApi = win => {
ipcMain.on("config.saveConfig", async (event, args) => { ipcMain.on("config.saveConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to save configuration.");
saveConfig(args, (err, numberOfUpdated, upsert) => { saveConfig(args, (err, numberOfUpdated, upsert) => {
if (!err) { if (!err) {
const start = args.systemSelfStart || false; const start = args.systemSelfStart || false;
log.info("开启自启状态", start); logDebug(LogModule.APP, "Startup status set to: " + start);
app.setLoginItemSettings({ app.setLoginItemSettings({
openAtLogin: start, //win openAtLogin: start, //win
openAsHidden: start //macOs openAsHidden: start //macOs
}); });
logInfo(LogModule.APP, "Configuration saved successfully.");
} else {
logError(LogModule.APP, `Error saving configuration: ${err}`);
} }
event.reply("Config.saveConfig.hook", { event.reply("Config.saveConfig.hook", {
err: err, err: err,
@ -30,7 +34,11 @@ export const initConfigApi = win => {
}); });
ipcMain.on("config.getConfig", async (event, args) => { ipcMain.on("config.getConfig", async (event, args) => {
logInfo(LogModule.APP, "Requesting configuration.");
getConfig((err, doc) => { getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving configuration: ${err}`);
}
event.reply("Config.getConfig.hook", { event.reply("Config.getConfig.hook", {
err: err, err: err,
data: doc data: doc
@ -39,7 +47,11 @@ export const initConfigApi = win => {
}); });
ipcMain.on("config.versions", event => { ipcMain.on("config.versions", event => {
logInfo(LogModule.APP, "Requesting version information.");
listVersion((err, doc) => { listVersion((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving version information: ${err}`);
}
event.reply("Config.versions.hook", { event.reply("Config.versions.hook", {
err: err, err: err,
data: doc data: doc
@ -48,7 +60,11 @@ export const initConfigApi = win => {
}); });
ipcMain.on("config.hasConfig", event => { ipcMain.on("config.hasConfig", event => {
logInfo(LogModule.APP, "Checking if configuration exists.");
getConfig((err, doc) => { getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error checking configuration: ${err}`);
}
event.reply("Config.getConfig.hook", { event.reply("Config.getConfig.hook", {
err: err, err: err,
data: doc data: doc
@ -57,15 +73,20 @@ export const initConfigApi = win => {
}); });
ipcMain.on("config.exportConfig", async (event, args) => { ipcMain.on("config.exportConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to export configuration.");
const result = await dialog.showOpenDialog({ const result = await dialog.showOpenDialog({
properties: ["openDirectory"] properties: ["openDirectory"]
}); });
const outputDirectory = result.filePaths[0]; const outputDirectory = result.filePaths[0];
if (!outputDirectory) { if (!outputDirectory) {
// 取消了 logWarn(LogModule.APP, "Export canceled by user.");
return; return;
} }
log.info(`导出目录 ${outputDirectory} 类型:${args}`);
logInfo(
LogModule.APP,
`Exporting configuration to directory ${outputDirectory} with type: ${args}`
);
getConfig((err1, config) => { getConfig((err1, config) => {
if (!err1 && config) { if (!err1 && config) {
listProxy((err2, proxys) => { listProxy((err2, proxys) => {
@ -83,16 +104,21 @@ export const initConfigApi = win => {
fs.writeFile( fs.writeFile(
configPath, // 配置文件目录 configPath, // 配置文件目录
configContent, // 配置文件内容 configContent, // 配置文件内容
{flag: "w"}, { flag: "w" },
err => { err => {
if (!err) { if (err) {
// callback(filename); logError(
LogModule.APP,
`Error writing configuration file: ${err}`
);
event.reply("config.exportConfig.hook", { event.reply("config.exportConfig.hook", {
data: "导出错误", data: "导出错误",
err: err err: err
}); });
} } else {
} logInfo(
LogModule.APP,
"Configuration exported successfully."
); );
event.reply("Config.exportConfig.hook", { event.reply("Config.exportConfig.hook", {
data: { data: {
@ -100,18 +126,24 @@ export const initConfigApi = win => {
} }
}); });
} }
}
);
} else {
logError(LogModule.APP, `Error listing proxies: ${err2}`);
}
}); });
} else {
logError(LogModule.APP, `Error retrieving configuration: ${err1}`);
} }
}); });
}); });
const parseTomlConfig = (tomlPath: string) => { const parseTomlConfig = (tomlPath: string) => {
logInfo(LogModule.APP, `Parsing TOML configuration from ${tomlPath}`);
const importConfigPath = tomlPath; const importConfigPath = tomlPath;
const tomlData = fs.readFileSync(importConfigPath, "utf-8"); const tomlData = fs.readFileSync(importConfigPath, "utf-8");
log.info(`读取到配置内容 ${tomlData}`); logInfo(LogModule.APP, "Configuration content read successfully.");
const sourceConfig = toml.parse(tomlData); const sourceConfig = toml.parse(tomlData);
// log.info(`解析结果 ${sourceConfig}`);
// console.log(sourceConfig, "frpcConfig");
// 解析config // 解析config
const targetConfig: FrpConfig = { const targetConfig: FrpConfig = {
currentVersion: null, currentVersion: null,
@ -140,7 +172,16 @@ export const initConfigApi = win => {
systemStartupConnect: false, systemStartupConnect: false,
systemSilentStartup: false, systemSilentStartup: false,
webEnable: true, webEnable: true,
webPort: sourceConfig?.webServer?.port || 57400 webPort: sourceConfig?.webServer?.port || 57400,
transportProtocol: sourceConfig?.transport?.protocol || "tcp",
transportDialServerTimeout:
sourceConfig?.transport?.dialServerTimeout || 10,
transportDialServerKeepalive:
sourceConfig?.transport?.dialServerKeepalive || 70,
transportPoolCount: sourceConfig?.transport?.poolCount || 0,
transportTcpMux: sourceConfig?.transport?.tcpMux || true,
transportTcpMuxKeepaliveInterval:
sourceConfig?.transport?.tcpMuxKeepaliveInterval || 7200
}; };
let frpcProxys = []; let frpcProxys = [];
// 解析proxy // 解析proxy
@ -166,11 +207,16 @@ export const initConfigApi = win => {
bindPort: null, bindPort: null,
status: m?.status || true, status: m?.status || true,
fallbackTo: m?.fallbackTo, fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500 fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
}; };
return rm; return rm;
}); });
frpcProxys = [...frpcProxys, ...frpcProxys1]; frpcProxys = [...frpcProxys, ...frpcProxys1];
logInfo(LogModule.APP, "Parsed proxies from configuration.");
} }
// 解析stcp的访问者 // 解析stcp的访问者
if (sourceConfig?.visitors && sourceConfig.visitors.length > 0) { if (sourceConfig?.visitors && sourceConfig.visitors.length > 0) {
@ -195,22 +241,33 @@ export const initConfigApi = win => {
bindPort: m?.bindPort, bindPort: m?.bindPort,
status: m?.status || true, status: m?.status || true,
fallbackTo: m?.fallbackTo, fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500 fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
}; };
return rm; return rm;
}); });
frpcProxys = [...frpcProxys, ...frpcProxys2]; frpcProxys = [...frpcProxys, ...frpcProxys2];
logInfo(LogModule.APP, "Parsed visitors from configuration.");
} }
if (targetConfig) { if (targetConfig) {
clearConfig(() => { clearConfig(() => {
logInfo(LogModule.APP, "Clearing existing configuration.");
saveConfig(targetConfig); saveConfig(targetConfig);
logInfo(LogModule.APP, "New configuration saved.");
}); });
} }
if (frpcProxys && frpcProxys.length > 0) { if (frpcProxys && frpcProxys.length > 0) {
clearProxy(() => { clearProxy(() => {
frpcProxys.forEach(f => { frpcProxys.forEach(f => {
insertProxy(f, err => { insertProxy(f, err => {
console.log("插入", f, err); if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err}`);
} else {
logInfo(LogModule.APP, `Inserted proxy: ${JSON.stringify(f)}`);
}
}); });
}); });
}); });
@ -218,24 +275,33 @@ export const initConfigApi = win => {
}; };
ipcMain.on("config.importConfig", async (event, args) => { ipcMain.on("config.importConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to import configuration.");
const result = await dialog.showOpenDialog(win, { const result = await dialog.showOpenDialog(win, {
properties: ["openFile"], properties: ["openFile"],
filters: [ filters: [
{name: "FrpcConfig Files", extensions: ["toml", "ini"]} // 允许选择的文件类型 { name: "FrpcConfig Files", extensions: ["toml", "ini"] } // 允许选择的文件类型
] ]
}); });
if (result.canceled) { if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
return; return;
} else { } else {
const filePath = result.filePaths[0]; const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath); // 获取文件后缀名 const fileExtension = path.extname(filePath); // 获取文件后缀名
log.info(`导入文件 ${filePath} ${fileExtension}`); logWarn(
LogModule.APP,
`Importing file ${filePath} with extension ${fileExtension}`
);
if (fileExtension === ".toml") { if (fileExtension === ".toml") {
parseTomlConfig(filePath); parseTomlConfig(filePath);
event.reply("Config.importConfig.hook", { event.reply("Config.importConfig.hook", {
success: true success: true
}); });
} else { } else {
logError(
LogModule.APP,
`Import failed, unsupported file format: ${fileExtension}`
);
event.reply("Config.importConfig.hook", { event.reply("Config.importConfig.hook", {
success: false, success: false,
data: `导入失败,暂不支持 ${fileExtension} 格式文件` data: `导入失败,暂不支持 ${fileExtension} 格式文件`
@ -245,22 +311,25 @@ export const initConfigApi = win => {
}); });
ipcMain.on("config.clearAll", async (event, args) => { ipcMain.on("config.clearAll", async (event, args) => {
logInfo(LogModule.APP, "Clearing all configurations.");
stopFrpcProcess(() => { stopFrpcProcess(() => {
clearConfig(); clearConfig();
clearProxy(); clearProxy();
clearVersion(); clearVersion();
event.reply("Config.clearAll.hook", {}); event.reply("Config.clearAll.hook", {});
logInfo(LogModule.APP, "All configurations cleared.");
}); });
}); });
ipcMain.on("config.openDataFolder", async (event, args) => { ipcMain.on("config.openDataFolder", async (event, args) => {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
shell.openPath(userDataPath).then((errorMessage) => { logInfo(LogModule.APP, "Attempting to open data folder.");
shell.openPath(userDataPath).then(errorMessage => {
if (errorMessage) { if (errorMessage) {
console.error('Failed to open Logger:', errorMessage); logError(LogModule.APP, `Failed to open data folder: ${errorMessage}`);
event.reply("Config.openDataFolder.hook", false); event.reply("Config.openDataFolder.hook", false);
} else { } else {
console.log('Logger opened successfully'); logInfo(LogModule.APP, "Data folder opened successfully.");
event.reply("Config.openDataFolder.hook", true); event.reply("Config.openDataFolder.hook", true);
} }
}); });

View File

@ -1,13 +1,21 @@
import {dialog, ipcMain} from "electron"; import {dialog, ipcMain} from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
export const initFileApi = () => { export const initFileApi = () => {
ipcMain.handle("file.selectFile", async (event, args) => { ipcMain.handle("file.selectFile", async (event, args) => {
logInfo(LogModule.APP, `Attempting to open file dialog with filters: ${JSON.stringify(args)}`);
try {
const result = dialog.showOpenDialogSync({ const result = dialog.showOpenDialogSync({
properties: ['openFile'], properties: ['openFile'],
filters: [ filters: [
{name: 'Text Files', extensions: args}, { name: 'Text Files', extensions: args },
] ]
}) });
logInfo(LogModule.APP, `File dialog result: ${JSON.stringify(result)}`);
return result; return result;
} catch (error) {
logError(LogModule.APP, `Error opening file dialog: ${error.message}`);
return null;
}
}); });
} }

View File

@ -3,11 +3,12 @@ import { getConfig } from "../storage/config";
import { listProxy } from "../storage/proxy"; import { listProxy } from "../storage/proxy";
import { getVersionById } from "../storage/version"; import { getVersionById } from "../storage/version";
import treeKill from "tree-kill"; import treeKill from "tree-kill";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const { exec, spawn } = require("child_process"); const { exec, spawn } = require("child_process");
const log = require("electron-log");
export let frpcProcess = null; export let frpcProcess = null;
const runningCmd = { const runningCmd = {
commandPath: null, commandPath: null,
@ -15,11 +16,6 @@ const runningCmd = {
}; };
let frpcStatusListener = null; let frpcStatusListener = null;
/**
*
* @param versionId ID
* @param callback
*/
const getFrpcVersionWorkerPath = ( const getFrpcVersionWorkerPath = (
versionId: number, versionId: number,
callback: (workerPath: string) => void callback: (workerPath: string) => void
@ -49,170 +45,179 @@ const isRangePort = (m: Proxy) => {
export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => { export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => { const proxyToml = proxys.map(m => {
const rangePort = isRangePort(m); const rangePort = isRangePort(m);
let toml = ` config.tlsConfigKeyFile = config.tlsConfigKeyFile.replace(/\\/g, "\\\\");
${ config.tlsConfigCertFile = config.tlsConfigCertFile.replace(/\\/g, "\\\\");
config.tlsConfigTrustedCaFile = config.tlsConfigTrustedCaFile.replace(
/\\/g,
"\\\\"
);
let toml = `${
rangePort rangePort
? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}` ? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}`
: "" : ""
} }
[[${ [[${
(m.type === "stcp" || m.type === "xtcp" || m.type === "sudp") && (m.type === "stcp" || m.type === "xtcp" || m.type === "sudp") &&
m.stcpModel === "visitors" m.stcpModel === "visitors"
? "visitors" ? "visitors"
: "proxies" : "proxies"
}]] }]]
${rangePort ? "" : `name = "${m.name}"\n`} ${rangePort ? "" : `name = "${m.name}"`}
type = "${m.type}" type = "${m.type}"\n`;
`;
switch (m.type) { switch (m.type) {
case "tcp": case "tcp":
case "udp": case "udp":
if (rangePort) { if (rangePort) {
toml += ` toml += `name = "${m.name}-{{ $v.First }}"
name = "${m.name}-{{ $v.First }}"
localPort = {{ $v.First }} localPort = {{ $v.First }}
remotePort = {{ $v.Second }} remotePort = {{ $v.Second }}\n`;
`;
} else { } else {
toml += ` toml += `localIP = "${m.localIp}"
localIP = "${m.localIp}"
localPort = ${m.localPort} localPort = ${m.localPort}
remotePort = ${m.remotePort} remotePort = ${m.remotePort}\n`;
`;
} }
break; break;
case "http": case "http":
case "https": case "https":
toml += ` const customDomains = m.customDomains.filter(f1 => f1 !== "");
localIP = "${m.localIp}" if (customDomains && customDomains.length > 0) {
localPort = ${m.localPort} toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]\n`;
customDomains=[${m.customDomains.map(m => `"${m}"`)}]
subdomain="${m.subdomain}"
`;
if (m.basicAuth) {
toml += `
httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"
`;
} }
if (m.subdomain) {
toml += `subdomain="${m.subdomain}"\n`;
}
if (m.basicAuth) {
toml += `httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"\n`;
}
if (m.https2http) {
toml += `[proxies.plugin]
type = "https2http"
localAddr = "${m.localIp}:${m.localPort}"
crtPath = "${m.https2httpCaFile}"
keyPath = "${m.https2httpKeyFile}"\n`;
} else {
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}\n`;
}
break; break;
case "stcp":
case "xtcp": case "xtcp":
if (m.stcpModel === "visitors") {
toml += `keepTunnelOpen = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp": case "sudp":
if (m.stcpModel === "visitors") { if (m.stcpModel === "visitors") {
// 访问者 // 访问者
toml += ` toml += `serverName = "${m.serverName}"
serverName = "${m.serverName}"
bindAddr = "${m.bindAddr}" bindAddr = "${m.bindAddr}"
bindPort = ${m.bindPort} bindPort = ${m.bindPort}\n`;
`;
if (m.fallbackTo) { if (m.fallbackTo) {
toml += ` toml += `fallbackTo = "${m.fallbackTo}"
fallbackTo = "${m.fallbackTo}" fallbackTimeoutMs = ${m.fallbackTimeoutMs || 500}\n`;
fallbackTimeoutMs = ${m.fallbackTimeoutMs || 500}
`;
} }
} else if (m.stcpModel === "visited") { } else if (m.stcpModel === "visited") {
// 被访问者 // 被访问者
toml += ` toml += `localIP = "${m.localIp}"
localIP = "${m.localIp}" localPort = ${m.localPort}\n`;
localPort = ${m.localPort}`;
} }
toml += `
secretKey="${m.secretKey}" toml += `secretKey="${m.secretKey}"\n`;
`;
break; break;
default: default:
break; break;
} }
if (rangePort) { if (rangePort) {
toml += `{{- end }}`; toml += `{{- end }}\n`;
} }
return toml; return toml;
}); });
const toml = ` const toml = `serverAddr = "${config.serverAddr}"
serverAddr = "${config.serverAddr}"
serverPort = ${config.serverPort} serverPort = ${config.serverPort}
${ ${
config.authMethod === "token" config.authMethod === "token"
? ` ? `auth.method = "token"
auth.method = "token" auth.token = "${config.authToken}"`
auth.token = "${config.authToken}"
`
: "" : ""
} }
${ ${
config.authMethod === "multiuser" config.authMethod === "multiuser"
? ` ? `user = "${config.user}"
user = "${config.user}" metadatas.token = "${config.metaToken}"`
metadatas.token = "${config.metaToken}"
`
: "" : ""
} }
${
config.transportHeartbeatInterval
? `
transport.heartbeatInterval = ${config.transportHeartbeatInterval}
`
: ""
}
${
config.transportHeartbeatTimeout
? `
transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}
`
: ""
}
log.to = "frpc.log" log.to = "frpc.log"
log.level = "${config.logLevel}" log.level = "${config.logLevel}"
log.maxDays = ${config.logMaxDays} log.maxDays = ${config.logMaxDays}
webServer.addr = "127.0.0.1" webServer.addr = "127.0.0.1"
webServer.port = ${config.webPort} webServer.port = ${config.webPort}
${
config.transportProtocol
? `transport.protocol = "${config.transportProtocol}"`
: ""
}
${
config.transportPoolCount
? `transport.poolCount = ${config.transportPoolCount}`
: ""
}
${
config.transportDialServerTimeout
? `transport.dialServerTimeout = ${config.transportDialServerTimeout}`
: ""
}
${
config.transportDialServerKeepalive
? `transport.dialServerKeepalive = ${config.transportDialServerKeepalive}`
: ""
}
${
config.transportHeartbeatInterval
? `transport.heartbeatInterval = ${config.transportHeartbeatInterval}`
: ""
}
${
config.transportHeartbeatTimeout
? `transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}`
: ""
}
${config.transportTcpMux ? `transport.tcpMux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `transport.tcpMuxKeepaliveInterval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
transport.tls.enable = ${config.tlsConfigEnable} transport.tls.enable = ${config.tlsConfigEnable}
${ ${
config.tlsConfigEnable && config.tlsConfigCertFile config.tlsConfigEnable && config.tlsConfigCertFile
? ` ? `transport.tls.certFile = "${config.tlsConfigCertFile}"`
transport.tls.certFile = "${config.tlsConfigCertFile}" : ""
` }
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `transport.tls.keyFile = "${config.tlsConfigKeyFile}"`
: "" : ""
} }
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `
transport.tls.keyFile = "${config.tlsConfigKeyFile}"
`
: ""
}
${ ${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? ` ? `transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"`
transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"
`
: "" : ""
} }
${ ${
config.tlsConfigEnable && config.tlsConfigServerName config.tlsConfigEnable && config.tlsConfigServerName
? ` ? `transport.tls.serverName = "${config.tlsConfigServerName}"`
transport.tls.serverName = "${config.tlsConfigServerName}"
`
: "" : ""
} }
${ ${
config.proxyConfigEnable config.proxyConfigEnable
? ` ? `transport.proxyURL = "${config.proxyConfigProxyUrl}"`
transport.proxyURL = "${config.proxyConfigProxyUrl}"
`
: "" : ""
} }
${proxyToml.join("")}`;
${proxyToml.join("")}
`;
return toml; return toml;
}; };
@ -224,8 +229,7 @@ ${proxyToml.join("")}
export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => { export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => { const proxyIni = proxys.map(m => {
const rangePort = isRangePort(m); const rangePort = isRangePort(m);
let ini = ` let ini = `[${rangePort ? "range:" : ""}${m.name}]
[${rangePort ? "range:" : ""}${m.name}]
type = "${m.type}" type = "${m.type}"
`; `;
switch (m.type) { switch (m.type) {
@ -234,26 +238,46 @@ type = "${m.type}"
ini += ` ini += `
local_ip = "${m.localIp}" local_ip = "${m.localIp}"
local_port = ${m.localPort} local_port = ${m.localPort}
remote_port = ${m.remotePort} remote_port = ${m.remotePort}\n`;
`;
break; break;
case "http": case "http":
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
custom_domains=[${m.customDomains.map(m => `${m}`)}]
subdomain="${m.subdomain}"\n`;
if (m.basicAuth) {
ini += `
httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"\n`;
}
break;
case "https": case "https":
ini += ` ini += `
local_ip = "${m.localIp}" custom_domains=[${m.customDomains.map(m => `${m}`)}]
local_port = ${m.localPort} subdomain="${m.subdomain}"\n`;
custom_domains=[${m.customDomains.map(m => `"${m}"`)}]
subdomain="${m.subdomain}"
`;
if (m.basicAuth) { if (m.basicAuth) {
ini += ` ini += `
httpUser = "${m.httpUser}" httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}" httpPassword = "${m.httpPassword}"\n`;
`; }
if (m.https2http) {
ini += `
plugin = https2http
plugin_local_addr = ${m.localIp}:${m.localPort}
plugin_crt_path = ${m.https2httpCaFile}
plugin_key_path = ${m.https2httpKeyFile}\n`;
} else {
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}\n`;
} }
break; break;
case "stcp":
case "xtcp": case "xtcp":
if (m.stcpModel === "visitors") {
ini += `keep_tunnel_open = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp": case "sudp":
if (m.stcpModel === "visitors") { if (m.stcpModel === "visitors") {
// 访问者 // 访问者
@ -261,23 +285,20 @@ httpPassword = "${m.httpPassword}"
role = visitor role = visitor
server_name = "${m.serverName}" server_name = "${m.serverName}"
bind_addr = "${m.bindAddr}" bind_addr = "${m.bindAddr}"
bind_port = ${m.bindPort} bind_port = ${m.bindPort}\n`;
`;
if (m.fallbackTo) { if (m.fallbackTo) {
ini += ` ini += `
fallback_to = ${m.fallbackTo} fallback_to = ${m.fallbackTo}
fallback_timeout_ms = ${m.fallbackTimeoutMs || 500} fallback_timeout_ms = ${m.fallbackTimeoutMs || 500}\n`;
`;
} }
} else if (m.stcpModel === "visited") { } else if (m.stcpModel === "visited") {
// 被访问者 // 被访问者
ini += ` ini += `
local_ip = "${m.localIp}" local_ip = "${m.localIp}"
local_port = ${m.localPort}`; local_port = ${m.localPort}\n`;
} }
ini += ` ini += `
sk="${m.secretKey}" sk="${m.secretKey}"\n`;
`;
break; break;
default: default:
break; break;
@ -293,19 +314,29 @@ ${
config.authMethod === "token" config.authMethod === "token"
? ` ? `
authentication_method = ${config.authMethod} authentication_method = ${config.authMethod}
token = ${config.authToken} token = ${config.authToken}\n`
`
: "" : ""
} }
${ ${
config.authMethod === "multiuser" config.authMethod === "multiuser"
? ` ? `
user = ${config.user} user = ${config.user}
meta_token = ${config.metaToken} meta_token = ${config.metaToken}\n`
`
: "" : ""
} }
${config.transportProtocol ? `protocol = ${config.transportProtocol}` : ""}
${config.transportPoolCount ? `pool_count = ${config.transportPoolCount}` : ""}
${
config.transportDialServerTimeout
? `dial_server_timeout = ${config.transportDialServerTimeout}`
: ""
}
${
config.transportDialServerKeepalive
? `dial_server_keepalive = ${config.transportDialServerKeepalive}`
: ""
}
${ ${
config.transportHeartbeatInterval config.transportHeartbeatInterval
? ` ? `
@ -320,6 +351,43 @@ heartbeat_timeout = ${config.transportHeartbeatTimeout}
` `
: "" : ""
} }
${config.transportTcpMux ? `transport.tcp_mux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `tcp_mux_keepalive_interval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigCertFile
? `
tls_cert_file = ${config.tlsConfigCertFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `
tls_key_file = ${config.tlsConfigKeyFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `
tls_server_name = ${config.tlsConfigServerName}\n`
: ""
}
${
config.proxyConfigEnable
? `
http_proxy = "${config.proxyConfigProxyUrl}"\n`
: ""
}
log_file = "frpc.log" log_file = "frpc.log"
log_level = ${config.logLevel} log_level = ${config.logLevel}
@ -328,42 +396,6 @@ admin_addr = 127.0.0.1
admin_port = ${config.webPort} admin_port = ${config.webPort}
tls_enable = ${config.tlsConfigEnable} 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("")} ${proxyIni.join("")}
`; `;
@ -378,7 +410,11 @@ export const generateConfig = (
callback: (configPath: string) => void callback: (configPath: string) => void
) => { ) => {
listProxy((err3, proxys) => { listProxy((err3, proxys) => {
if (!err3) { if (err3) {
logError(LogModule.FRP_CLIENT, `Failed to list proxies: ${err3.message}`);
return;
}
const { currentVersion } = config; const { currentVersion } = config;
let filename = null; let filename = null;
let configContent = ""; let configContent = "";
@ -390,27 +426,49 @@ export const generateConfig = (
return m; return m;
}) })
.filter(f => f.status); .filter(f => f.status);
if (currentVersion < 124395282) { if (currentVersion < 124395282) {
// 版本小于v0.52.0 // 版本小于v0.52.0
filename = "frp.ini"; filename = "frp.ini";
configContent = genIniConfig(config, filtered); configContent = genIniConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using INI format for configuration: ${filename}`
);
} else { } else {
filename = "frp.toml"; filename = "frp.toml";
configContent = genTomlConfig(config, filtered); configContent = genTomlConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using TOML format for configuration: ${filename}`
);
} }
const configPath = path.join(app.getPath("userData"), filename); const configPath = path.join(app.getPath("userData"), filename);
log.info(`生成配置成功 配置路径:${configPath}`); logInfo(
LogModule.FRP_CLIENT,
`Writing configuration to file: ${configPath}`
);
fs.writeFile( fs.writeFile(
configPath, // 配置文件目录 configPath, // 配置文件目录
configContent, // 配置文件内容 configContent, // 配置文件内容
{ flag: "w" }, { flag: "w" },
err => { err => {
if (!err) { if (err) {
logError(
LogModule.FRP_CLIENT,
`Failed to write configuration file: ${err.message}`
);
} else {
logInfo(
LogModule.FRP_CLIENT,
`Configuration file written successfully: ${filename}`
);
callback(filename); callback(filename);
} }
} }
); );
}
}); });
}; };
@ -421,7 +479,13 @@ export const generateConfig = (
* @param configPath * @param configPath
*/ */
const startFrpcProcess = (commandPath: string, configPath: string) => { const startFrpcProcess = (commandPath: string, configPath: string) => {
log.info(`启动frpc 目录:${app.getPath("userData")} 命令:${commandPath}`); logInfo(
LogModule.FRP_CLIENT,
`Starting frpc process. Directory: ${app.getPath(
"userData"
)}, Command: ${commandPath}`
);
const command = `${commandPath} -c ${configPath}`; const command = `${commandPath} -c ${configPath}`;
frpcProcess = spawn(command, { frpcProcess = spawn(command, {
cwd: app.getPath("userData"), cwd: app.getPath("userData"),
@ -429,21 +493,31 @@ const startFrpcProcess = (commandPath: string, configPath: string) => {
}); });
runningCmd.commandPath = commandPath; runningCmd.commandPath = commandPath;
runningCmd.configPath = configPath; runningCmd.configPath = configPath;
frpcProcess.stdout.on("data", data => { frpcProcess.stdout.on("data", data => {
log.debug(`启动输出:${data}`); logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
}); });
frpcProcess.stdout.on("error", data => { frpcProcess.stdout.on("error", data => {
log.error(`启动错误:${data}`); logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`);
stopFrpcProcess(() => {}); stopFrpcProcess(() => {});
}); });
frpcStatusListener = setInterval(() => { frpcStatusListener = setInterval(() => {
const status = frpcProcessStatus(); const status = frpcProcessStatus();
log.debug(`监听frpc子进程状态${status} ${frpcStatusListener}`); logDebug(
LogModule.FRP_CLIENT,
`Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
);
if (!status) { if (!status) {
new Notification({ new Notification({
title: "Frpc Desktop", title: "Frpc Desktop",
body: "连接已断开,请前往日志查看原因" body: "Connection lost, please check the logs for details."
}).show(); }).show();
logError(
LogModule.FRP_CLIENT,
"Frpc process status check failed. Connection lost."
);
clearInterval(frpcStatusListener); clearInterval(frpcStatusListener);
} }
}, 3000); }, 3000);
@ -454,20 +528,58 @@ const startFrpcProcess = (commandPath: string, configPath: string) => {
*/ */
export const reloadFrpcProcess = () => { export const reloadFrpcProcess = () => {
if (frpcProcess && !frpcProcess.killed) { if (frpcProcess && !frpcProcess.killed) {
logDebug(
LogModule.FRP_CLIENT,
"Attempting to reload frpc process configuration."
);
getConfig((err1, config) => { getConfig((err1, config) => {
if (!err1) { if (!err1) {
if (config) { if (config) {
generateConfig(config, configPath => { generateConfig(config, configPath => {
const command = `${runningCmd.commandPath} reload -c ${configPath}`; const command = `${runningCmd.commandPath} reload -c ${configPath}`;
log.info(`重载配置:${command}`); logInfo(
exec(command, { LogModule.FRP_CLIENT,
`Reloading configuration: ${command}`
);
exec(
command,
{
cwd: app.getPath("userData"), cwd: app.getPath("userData"),
shell: true shell: true
}); },
}); (error, stdout, stderr) => {
if (error) {
logError(
LogModule.FRP_CLIENT,
`Error reloading configuration: ${error.message}`
);
return;
}
logDebug(
LogModule.FRP_CLIENT,
`Configuration reload output: ${stdout}`
);
if (stderr) {
logWarn(
LogModule.FRP_CLIENT,
`Configuration reload warnings: ${stderr}`
);
} }
} }
);
}); });
} else {
logWarn(LogModule.FRP_CLIENT, "No configuration found to reload.");
}
} else {
logError(LogModule.FRP_CLIENT, `Error getting configuration: ${err1}`);
}
});
} else {
logDebug(
LogModule.FRP_CLIENT,
"frpc process is not running or has been killed."
);
} }
}; };
@ -478,16 +590,27 @@ export const stopFrpcProcess = (callback?: () => void) => {
if (frpcProcess) { if (frpcProcess) {
treeKill(frpcProcess.pid, (error: Error) => { treeKill(frpcProcess.pid, (error: Error) => {
if (error) { if (error) {
log.error(`关闭frpc子进程失败 pid${frpcProcess.pid} error${error}`); logError(
LogModule.FRP_CLIENT,
`Failed to stop frpc process with pid: ${frpcProcess.pid}. Error: ${error.message}`
);
callback(); callback();
} else { } else {
log.info(`关闭frpc子进程成功`); logInfo(
LogModule.FRP_CLIENT,
`Successfully stopped frpc process with pid: ${frpcProcess.pid}.`
);
frpcProcess = null; frpcProcess = null;
clearInterval(frpcStatusListener); clearInterval(frpcStatusListener);
callback(); callback();
} }
}); });
} else { } else {
logWarn(
LogModule.FRP_CLIENT,
"Attempted to stop frpc process, but no process is running."
);
logWarn(LogModule.FRP_CLIENT, "No frpc process to stop.");
callback(); callback();
} }
}; };
@ -497,14 +620,23 @@ export const stopFrpcProcess = (callback?: () => void) => {
*/ */
export const frpcProcessStatus = () => { export const frpcProcessStatus = () => {
if (!frpcProcess) { if (!frpcProcess) {
logDebug(LogModule.FRP_CLIENT, "frpc process is not running.");
return false; return false;
} }
try { try {
// 发送信号给进程,如果进程存在,会正常返回 // 发送信号给进程,如果进程存在,会正常返回
process.kill(frpcProcess.pid, 0); process.kill(frpcProcess.pid, 0);
logDebug(
LogModule.FRP_CLIENT,
`frpc process is running with pid: ${frpcProcess.pid}`
);
return true; return true;
} catch (error) { } catch (error) {
// 进程不存在,抛出异常 // 进程不存在,抛出异常
logError(
LogModule.FRP_CLIENT,
`frpc process not found. Error: ${error.message}`
);
return false; return false;
} }
}; };
@ -514,29 +646,47 @@ export const frpcProcessStatus = () => {
* @param config * @param config
*/ */
export const startFrpWorkerProcess = async (config: FrpConfig) => { export const startFrpWorkerProcess = async (config: FrpConfig) => {
logInfo(LogModule.FRP_CLIENT, "Starting frpc worker process...");
getFrpcVersionWorkerPath(config.currentVersion, (frpcVersionPath: string) => { getFrpcVersionWorkerPath(config.currentVersion, (frpcVersionPath: string) => {
if (frpcVersionPath) { if (frpcVersionPath) {
logInfo(
LogModule.FRP_CLIENT,
`Found frpc version path: ${frpcVersionPath}`
);
generateConfig(config, configPath => { generateConfig(config, configPath => {
const platform = process.platform; const platform = process.platform;
if (platform === "win32") { if (platform === "win32") {
logInfo(LogModule.FRP_CLIENT, "Starting frpc on Windows.");
startFrpcProcess(path.join(frpcVersionPath, "frpc.exe"), configPath); startFrpcProcess(path.join(frpcVersionPath, "frpc.exe"), configPath);
} else { } else {
logInfo(
LogModule.FRP_CLIENT,
"Starting frpc on non-Windows platform."
);
startFrpcProcess(path.join(frpcVersionPath, "frpc"), configPath); startFrpcProcess(path.join(frpcVersionPath, "frpc"), configPath);
} }
}); });
} else {
logError(LogModule.FRP_CLIENT, "frpc version path not found.");
} }
}); });
}; };
export const initFrpcApi = () => { export const initFrpcApi = () => {
ipcMain.handle("frpc.running", async (event, args) => { ipcMain.handle("frpc.running", async (event, args) => {
logDebug(LogModule.FRP_CLIENT, "Checking if frpc process is running...");
return frpcProcessStatus(); return frpcProcessStatus();
}); });
ipcMain.on("frpc.start", async (event, args) => { ipcMain.on("frpc.start", async (event, args) => {
logInfo(LogModule.FRP_CLIENT, "Received request to start frpc process.");
getConfig((err1, config) => { getConfig((err1, config) => {
if (!err1) { if (!err1) {
if (!config) { if (!config) {
logWarn(
LogModule.FRP_CLIENT,
"Configuration not found. Prompting user to set configuration."
);
event.reply( event.reply(
"Home.frpc.start.error.hook", "Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动" "请先前往设置页面,修改配置后再启动"
@ -544,6 +694,10 @@ export const initFrpcApi = () => {
return; return;
} }
if (!config.currentVersion) { if (!config.currentVersion) {
logWarn(
LogModule.FRP_CLIENT,
"Current version not set in configuration. Prompting user."
);
event.reply( event.reply(
"Home.frpc.start.error.hook", "Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动" "请先前往设置页面,修改配置后再启动"
@ -551,13 +705,18 @@ export const initFrpcApi = () => {
return; return;
} }
startFrpWorkerProcess(config); startFrpWorkerProcess(config);
} else {
logError(LogModule.FRP_CLIENT, `Error getting configuration: ${err1}`);
} }
}); });
}); });
ipcMain.on("frpc.stop", () => { ipcMain.on("frpc.stop", () => {
logInfo(LogModule.FRP_CLIENT, "Received request to stop frpc process.");
if (frpcProcess && !frpcProcess.killed) { if (frpcProcess && !frpcProcess.killed) {
stopFrpcProcess(() => {}); stopFrpcProcess(() => {});
} else {
logWarn(LogModule.FRP_CLIENT, "No frpc process to stop.");
} }
}); });
}; };

View File

@ -1,6 +1,14 @@
import electron, { app, BrowserWindow, ipcMain, net, shell } from "electron"; import electron, {
app,
dialog,
BrowserWindow,
ipcMain,
net,
shell
} from "electron";
import { import {
deleteVersionById, deleteVersionById,
getVersionById,
insertVersion, insertVersion,
listVersion listVersion
} from "../storage/version"; } from "../storage/version";
@ -10,7 +18,11 @@ const path = require("path");
const zlib = require("zlib"); const zlib = require("zlib");
const { download } = require("electron-dl"); const { download } = require("electron-dl");
const AdmZip = require("adm-zip"); const AdmZip = require("adm-zip");
const log = require("electron-log"); import frpReleasesJson from "../json/frp-releases.json";
import frpChecksums from "../json/frp_all_sha256_checksums.json";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
import { calculateFileChecksum, formatBytes } from "../utils/file";
import { el } from "element-plus/es/locale";
const versionRelation = { const versionRelation = {
win32_x64: ["window", "amd64"], win32_x64: ["window", "amd64"],
@ -30,23 +42,44 @@ const frpArch = versionRelation[currArch];
const unTarGZ = (tarGzPath: string, targetPath: string) => { const unTarGZ = (tarGzPath: string, targetPath: string) => {
const tar = require("tar"); const tar = require("tar");
const unzip = zlib.createGunzip(); const unzip = zlib.createGunzip();
log.debug(`开始解压tar.gz${tarGzPath} 目标目录:${targetPath}`); logInfo(
LogModule.APP,
`Starting to extract tar.gz: ${tarGzPath} to ${targetPath}`
);
const readStream = fs.createReadStream(tarGzPath); const readStream = fs.createReadStream(tarGzPath);
if (!fs.existsSync(unzip)) { if (!fs.existsSync(unzip)) {
fs.mkdirSync(targetPath, { recursive: true }); fs.mkdirSync(targetPath, { recursive: true, mode: 0o777 });
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
} }
readStream.pipe(unzip).pipe(
tar.extract({ readStream.on("error", err => {
logError(LogModule.APP, `Error reading tar.gz file: ${err.message}`);
});
readStream
.pipe(unzip)
.on("error", err => {
logError(LogModule.APP, `Error during gunzip: ${err.message}`);
})
.pipe(
tar
.extract({
cwd: targetPath, cwd: targetPath,
filter: filePath => path.basename(filePath) === "frpc" filter: filePath => path.basename(filePath) === "frpc"
}) })
); .on("error", err => {
logError(LogModule.APP, `Error extracting tar file: ${err.message}`);
})
)
.on("finish", () => {
const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz")); const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
log.debug(`解压完成 解压后目录:${frpcPath}`); logInfo(
return frpcPath; LogModule.APP,
// .on("finish", () => { `Extraction completed. Extracted directory: ${frpcPath}`
// console.log("解压完成!"); );
// }); });
return path.join("frp", path.basename(tarGzPath, ".tar.gz"));
}; };
const unZip = (zipPath: string, targetPath: string) => { const unZip = (zipPath: string, targetPath: string) => {
@ -54,90 +87,128 @@ const unZip = (zipPath: string, targetPath: string) => {
fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), { fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), {
recursive: true recursive: true
}); });
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
logInfo(
LogModule.APP,
`Created directory for zip extraction: ${path.basename(zipPath, ".zip")}`
);
} }
log.debug(`开始解压zip${zipPath} 目标目录:${targetPath}`);
/** logDebug(
* unzipper解压 LogModule.APP,
*/ `Starting to unzip: ${zipPath} to target directory: ${targetPath}`
// fs.createReadStream(zipPath) );
// .pipe(unzipper.Extract({ path: targetPath })) logInfo(LogModule.APP, `Starting to extract zip file: ${zipPath}`);
// // 只解压frpc.exe
// // .pipe(unzipper.ParseOne('frpc'))
// // .pipe(fs.createWriteStream(path.join(targetPath, path.basename(zipPath, ".zip"), "frpc.exe")))
// .on('finish', () => {
// console.log('File extracted successfully.');
// })
// .on('error', (err) => {
// console.error('Error extracting file:', err);
// });
const zip = new AdmZip(zipPath); const zip = new AdmZip(zipPath);
try {
zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件 zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件
const frpcPath = path.join("frp", path.basename(zipPath, ".zip")); const frpcPath = path.join("frp", path.basename(zipPath, ".zip"));
log.debug(`解压完成 解压后目录:${frpcPath}`); logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
logDebug(
LogModule.APP,
`Unzip completed. Extracted directory: ${frpcPath}`
);
return frpcPath; return frpcPath;
} catch (error) {
logError(LogModule.APP, `Error extracting zip file: ${error.message}`);
}
return null;
}; };
export const initGitHubApi = () => { export const initGitHubApi = win => {
// 版本 // 版本
let versions: FrpVersion[] = []; let versions: FrpVersion[] = [];
const getVersion = versionId => { const getVersionByGithubVersionId = versionId => {
return versions.find(f => f.id === versionId); logDebug(LogModule.APP, `Attempting to get version with ID: ${versionId}`);
const version = versions.find(f => f.id === versionId);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for ID: ${versionId}`);
}
return version;
};
const getVersionByAssetName = (assetName: string) => {
logDebug(
LogModule.APP,
`Attempting to get version with asset name: ${assetName}`
);
const version = versions.find(f =>
f.assets.some(asset => asset.name === assetName)
);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for asset name: ${assetName}`);
}
return version;
}; };
const getAdaptiveAsset = versionId => { const getAdaptiveAsset = versionId => {
const { assets } = getVersion(versionId); const { assets } = getVersionByGithubVersionId(versionId);
if (!assets || assets.length === 0) {
logWarn(LogModule.GITHUB, `No assets found for version ID: ${versionId}`);
return null;
}
const asset = assets.find(f => { const asset = assets.find(f => {
// const a = versionRelation[currArch]
const a = frpArch; const a = frpArch;
if (a) { if (a) {
const flag = a.every(item => f.name.includes(item)); const flag = a.every(item => f.name.includes(item));
if (flag) {
logInfo(
LogModule.GITHUB,
`Found matching asset: ${f.name} for version ID: ${versionId}`
);
}
return flag; return flag;
} }
logWarn(
LogModule.GITHUB,
`No architecture match found for asset: ${f.name}`
);
return false; return false;
}); });
if (asset) {
log.info(`找到对应版本 ${asset.name}`); if (!asset) {
logError(
LogModule.GITHUB,
`No adaptive asset found for version ID: ${versionId}`
);
} }
return asset; return asset;
}; };
const formatBytes = (bytes: number, decimals: number = 2): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024; // 1 KB = 1024 Bytes
const dm = decimals < 0 ? 0 : decimals; // 确保小数位数不小于 0
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // 计算单位索引
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // 返回格式化的字符串
};
/** /**
* github上的frp所有版本 * handle github api release json
* @param githubReleaseJsonStr jsonStr
* @returns versions
*/ */
ipcMain.on("github.getFrpVersions", async event => { const handleApiResponse = (githubReleaseJsonStr: string) => {
const request = net.request({
method: "get",
// url: "https://api.github.com/repos/fatedier/frp/releases?page=1&per_page=1000"
url: "https://api.jwinks.com/github/releases"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
log.info(
`开始获取frp版本 当前架构:${currArch} 对应frp架构${frpArch} 状态码:${response.statusCode}`
);
const downloadPath = path.join(app.getPath("userData"), "download"); const downloadPath = path.join(app.getPath("userData"), "download");
if (response.statusCode === 200) { const frpPath = path.join(app.getPath("userData"), "frp");
versions = JSON.parse(responseData.toString()); logInfo(LogModule.GITHUB, "Parsing GitHub release JSON response.");
}
// const borderContent: Electron.WebContents = versions = JSON.parse(githubReleaseJsonStr);
// BrowserWindow.getFocusedWindow().webContents;
if (versions) { if (versions) {
logInfo(
LogModule.GITHUB,
"Successfully parsed versions from GitHub response."
);
const returnVersionsData = versions const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id)) .filter(f => getAdaptiveAsset(f.id))
.map(m => { .map(m => {
@ -147,40 +218,169 @@ export const initGitHubApi = () => {
0 0
); );
if (asset) { if (asset) {
const absPath = `${downloadPath}/${asset.name}`; const absPath = path.join(
frpPath,
asset.name.replace(/(\.tar\.gz|\.zip)$/, "")
);
m.absPath = absPath; m.absPath = absPath;
m.download_completed = fs.existsSync(absPath); m.download_completed = fs.existsSync(absPath);
m.download_count = download_count; m.download_count = download_count;
m.size = formatBytes(asset.size); m.size = formatBytes(asset.size);
logInfo(
LogModule.GITHUB,
`Asset found: ${asset.name}, download count: ${download_count}`
);
} else {
logWarn(LogModule.GITHUB, `No asset found for version ID: ${m.id}`);
} }
return m; return m;
}); });
// log.debug(`获取到frp版本${JSON.stringify(returnVersionsData)}`) logDebug(
event.reply("Download.frpVersionHook", returnVersionsData); LogModule.GITHUB,
`Retrieved FRP versions: ${JSON.stringify(returnVersionsData)}`
);
return returnVersionsData;
} else { } else {
event.reply("Download.frpVersionHook", []); logError(
LogModule.GITHUB,
"Failed to parse versions: No versions found in response."
);
return [];
} }
};
/**
* conventMirrorUrl
* @param mirror mirror
* @returns mirrorUrl
*/
const conventMirrorUrl = (mirror: string) => {
switch (mirror) {
case "github":
return {
api: "https://api.github.com",
asset: "https://github.com"
};
default:
return {
api: "https://api.github.com",
asset: "https://github.com"
};
}
};
/**
* github上的frp所有版本
*/
ipcMain.on("github.getFrpVersions", async (event, mirror: string) => {
const { api } = conventMirrorUrl(mirror);
const mirrorUrl = api;
logInfo(LogModule.GITHUB, `Requesting mirror URL: ${mirrorUrl}`);
const request = net.request({
method: "get",
url: `${mirrorUrl}/repos/fatedier/frp/releases?page=1&per_page=1000`
});
let githubReleaseJsonStr = null;
request.on("response", response => {
logInfo(
LogModule.GITHUB,
`Received response with status code: ${response.statusCode}`
);
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
if (response.statusCode === 200) {
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
logWarn(
LogModule.GITHUB,
"Failed to retrieve data, using local JSON instead. Status code: " +
response.statusCode
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
}
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
}); });
}); });
request.on("error", error => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
request.end(); request.end();
}); });
const decompressFrp = (frpFilename: string, compressedFilePath: string) => {
const targetPath = path.resolve(path.join(app.getPath("userData"), "frp"));
const ext = path.extname(frpFilename);
let frpcVersionPath = "";
try {
if (ext === ".zip") {
unZip(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
logInfo(LogModule.APP, `Unzipped file to path: ${frpcVersionPath}`);
frpcVersionPath = path.join("frp", path.basename(frpFilename, ".zip"));
} else if (ext === ".gz" && frpFilename.includes(".tar.gz")) {
unTarGZ(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
frpcVersionPath = path.join(
"frp",
path.basename(frpFilename, ".tar.gz")
);
logInfo(LogModule.APP, `Untarred file to path: ${frpcVersionPath}`);
}
} catch (error) {
logError(LogModule.APP, `Error during extraction: ${error.message}`);
}
return frpcVersionPath;
};
/** /**
* *
*/ */
ipcMain.on("github.download", async (event, args) => { ipcMain.on("github.download", async (event, args) => {
const { versionId, mirror } = args; const { versionId, mirror } = args;
const version = getVersion(versionId); const version = getVersionByGithubVersionId(versionId);
const asset = getAdaptiveAsset(versionId); const asset = getAdaptiveAsset(versionId);
const { browser_download_url } = asset; const { browser_download_url } = asset;
let url = browser_download_url; let url = browser_download_url.replace(
if (mirror === "ghproxy") { "https://github.com",
url = "https://mirror.ghproxy.com/" + url; conventMirrorUrl(mirror).asset
} );
logDebug(
LogModule.GITHUB,
`Starting download for versionId: ${versionId}, mirror: ${mirror}, download URL: ${url}`
);
log.info(`开始下载frp url${url} asset${asset.name}`);
// 数据目录
await download(BrowserWindow.getFocusedWindow(), url, { await download(BrowserWindow.getFocusedWindow(), url, {
filename: `${asset.name}`, filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"), directory: path.join(app.getPath("userData"), "download"),
@ -189,32 +389,26 @@ export const initGitHubApi = () => {
id: versionId, id: versionId,
progress: progress progress: progress
}); });
logDebug(
LogModule.GITHUB,
`Download progress for versionId: ${versionId} is ${
progress.percent * 100
}%`
);
}, },
onCompleted: () => { onCompleted: () => {
log.info(`frp下载完成 url${url} asset${asset.name}`); logInfo(
const targetPath = path.resolve( LogModule.GITHUB,
path.join(app.getPath("userData"), "frp") `Download completed for versionId: ${versionId}, asset: ${asset.name}`
); );
const ext = path.extname(asset.name);
let frpcVersionPath = "";
if (ext === ".zip") {
frpcVersionPath = unZip(
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
),
targetPath
);
} else if (ext === ".gz" && asset.name.includes(".tar.gz")) {
frpcVersionPath = unTarGZ(
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
),
targetPath
);
}
const frpcVersionPath = decompressFrp(
asset.name,
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
)
);
version["frpcVersionPath"] = frpcVersionPath; version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => { insertVersion(version, (err, document) => {
if (!err) { if (!err) {
@ -222,7 +416,13 @@ export const initGitHubApi = () => {
event.reply("Config.versions.hook", { err, data: doc }); event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.frpVersionDownloadOnCompleted", versionId); event.reply("Download.frpVersionDownloadOnCompleted", versionId);
version.download_completed = true; version.download_completed = true;
logInfo(
LogModule.GITHUB,
`Version ${versionId} has been inserted successfully.`
);
}); });
} else {
logError(LogModule.GITHUB, `Error inserting version: ${err}`);
} }
}); });
} }
@ -234,17 +434,39 @@ export const initGitHubApi = () => {
*/ */
ipcMain.on("github.deleteVersion", async (event, args) => { ipcMain.on("github.deleteVersion", async (event, args) => {
const { absPath, id } = args; const { absPath, id } = args;
logDebug(
LogModule.GITHUB,
`Attempting to delete version with ID: ${id} and path: ${absPath}`
);
if (fs.existsSync(absPath)) { if (fs.existsSync(absPath)) {
// if (process.platform === 'darwin') {
// fs.unlinkSync(absPath.replace(/ /g, '\\ '));
// }else{
// fs.unlinkSync(absPath);
// }
fs.rmSync(absPath, { recursive: true, force: true });
deleteVersionById(id, () => { deleteVersionById(id, () => {
fs.unlinkSync(absPath); logInfo(
LogModule.GITHUB,
`Successfully deleted version with ID: ${id}`
);
}); });
} else {
logWarn(
LogModule.GITHUB,
`Version with ID: ${id} not found at path: ${absPath}`
);
} }
listVersion((err, doc) => { listVersion((err, doc) => {
if (err) {
logError(LogModule.GITHUB, `Error listing versions: ${err}`);
} else {
event.reply("Config.versions.hook", { err, data: doc }); event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.deleteVersion.hook", { event.reply("Download.deleteVersion.hook", {
err: null, err: null,
data: "删除成功" data: "删除成功"
}); });
}
}); });
}); });
@ -252,6 +474,7 @@ export const initGitHubApi = () => {
* *
*/ */
ipcMain.on("github.getFrpcDesktopLastVersions", async event => { ipcMain.on("github.getFrpcDesktopLastVersions", async event => {
logInfo(LogModule.GITHUB, "Requesting the latest version from GitHub.");
const request = net.request({ const request = net.request({
method: "get", method: "get",
url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest" url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest"
@ -262,26 +485,114 @@ export const initGitHubApi = () => {
responseData = Buffer.concat([responseData, data]); responseData = Buffer.concat([responseData, data]);
}); });
response.on("end", () => { response.on("end", () => {
try {
versions = JSON.parse(responseData.toString()); versions = JSON.parse(responseData.toString());
// const borderContent: Electron.WebContents = logInfo(
// BrowserWindow.getFocusedWindow().webContents; LogModule.GITHUB,
// const downloadPath = path.join(app.getPath("userData"), "download"); "Successfully retrieved the latest version."
// log.info(`开始获取frp版本 当前架构:${currArch} 对应frp架构${frpArch}`) );
// const returnVersionsData = versions
// .filter(f => getAdaptiveAsset(f.id))
// .map(m => {
// const asset = getAdaptiveAsset(m.id);
// if (asset) {
// const absPath = `${downloadPath}/${asset.name}`;
// m.absPath = absPath;
// m.download_completed = fs.existsSync(absPath);
// }
// return m;
// });
// log.debug(`获取到frp版本${JSON.stringify(returnVersionsData)}`)
event.reply("github.getFrpcDesktopLastVersionsHook", versions); event.reply("github.getFrpcDesktopLastVersionsHook", versions);
} catch (error) {
logError(
LogModule.GITHUB,
`Error parsing response data: ${error.message}`
);
}
}); });
}); });
request.on("error", error => {
logError(LogModule.GITHUB, `Request error: ${error.message}`);
});
request.end(); request.end();
}); });
ipcMain.on(
"download.importFrpFile",
async (event, filePath: string, targetPath: string) => {
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "Frp 文件", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
logWarn(LogModule.GITHUB, "User canceled the file import operation.");
return;
} else {
const filePath = result.filePaths[0];
// const fileExtension = path.extname(filePath);
logInfo(LogModule.APP, `User selected file: ${filePath}`);
const checksum = calculateFileChecksum(filePath);
logInfo(LogModule.APP, `Calculated checksum for the file: ${checksum}`);
const frpName = frpChecksums[checksum];
if (frpName) {
logInfo(LogModule.APP, `FRP file name found: ${frpName}`);
if (frpArch.every(item => frpName.includes(item))) {
logInfo(
LogModule.APP,
`Architecture matches for FRP file: ${frpName}`
);
const version = getVersionByAssetName(frpName);
getVersionById(version.id, (err, existingVersion) => {
if (!err && existingVersion) {
logInfo(
LogModule.APP,
`Version already exists: ${JSON.stringify(existingVersion)}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,版本已存在`
});
return; // 终止后续执行
}
const frpcVersionPath = decompressFrp(frpName, filePath);
logInfo(
LogModule.APP,
`Successfully decompressed FRP file: ${frpName} to path: ${frpcVersionPath}`
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
event.reply("Config.versions.hook", { err, data: doc });
version.download_completed = true;
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入成功`
});
});
} else {
logError(LogModule.APP, `Error inserting version: ${err}`);
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入失败,未知错误`
});
}
});
});
} else {
logWarn(
LogModule.APP,
`Architecture does not match for FRP file: ${frpName}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,所选 frp 架构与操作系统不符`
});
}
} else {
logWarn(
LogModule.APP,
`No matching FRP file name found for checksum: ${checksum}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,无法识别文件`
});
}
}
}
);
}; };

View File

@ -1,5 +1,5 @@
import {ipcMain} from "electron"; import {ipcMain} from "electron";
import log from "electron-log"; import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const {exec, spawn} = require("child_process"); const {exec, spawn} = require("child_process");
@ -15,19 +15,19 @@ export const initLocalApi = () => {
: 'netstat -an | grep LISTEN'; : 'netstat -an | grep LISTEN';
ipcMain.on("local.getLocalPorts", async (event, args) => { ipcMain.on("local.getLocalPorts", async (event, args) => {
log.info("开始获取本地端口") logInfo(LogModule.APP, "Starting to retrieve local ports");
// 执行命令 // 执行命令
exec(command, (error, stdout, stderr) => { exec(command, (error, stdout, stderr) => {
if (error) { if (error) {
log.error(`getLocalPorts - error ${error.message}`) logError(LogModule.APP, `getLocalPorts - error: ${error.message}`);
return; return;
} }
if (stderr) { if (stderr) {
log.error(`getLocalPorts - stderr ${stderr}`) logWarn(LogModule.APP, `getLocalPorts - stderr: ${stderr}`);
return; return;
} }
log.debug(`sc ${stdout}`) logDebug(LogModule.APP, `Command output: ${stdout}`);
let ports = []; let ports = [];
if (stdout) { if (stdout) {
if (process.platform === 'win32') { if (process.platform === 'win32') {
@ -72,7 +72,6 @@ export const initLocalApi = () => {
} }
return singe; return singe;
}) })
// .filter(f => f.indexOf('TCP') > 0 || f.indexOf('UDP') > 0)
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
ports = stdout.split('\n') ports = stdout.split('\n')

View File

@ -1,4 +1,5 @@
import { app, ipcMain, shell } from "electron"; import { app, ipcMain, shell } from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
@ -8,35 +9,43 @@ export const initLoggerApi = () => {
const readLogger = (callback: (content: string) => void) => { const readLogger = (callback: (content: string) => void) => {
fs.readFile(logPath, "utf-8", (error, data) => { fs.readFile(logPath, "utf-8", (error, data) => {
if (!error) { if (!error) {
logInfo(LogModule.APP, "Log file read successfully.");
callback(data); callback(data);
} else {
logError(LogModule.APP, `Error reading log file: ${error.message}`);
} }
}); });
}; };
ipcMain.on("logger.getLog", async (event, args) => { ipcMain.on("logger.getLog", async (event, args) => {
logInfo(LogModule.APP, "Received request to get log.");
readLogger(content => { readLogger(content => {
event.reply("Logger.getLog.hook", content); event.reply("Logger.getLog.hook", content);
logInfo(LogModule.APP, "Log data sent to client.");
}); });
}); });
ipcMain.on("logger.update", (event, args) => { ipcMain.on("logger.update", (event, args) => {
logInfo(LogModule.APP, "Watching log file for changes.");
fs.watch(logPath, (eventType, filename) => { fs.watch(logPath, (eventType, filename) => {
if (eventType === "change") { if (eventType === "change") {
logInfo(LogModule.APP, "Log file changed, reading new content.");
readLogger(content => { readLogger(content => {
event.reply("Logger.update.hook", content); event.reply("Logger.update.hook", content);
logInfo(LogModule.APP, "Updated log data sent to client.");
}); });
} }
}); });
}); });
ipcMain.on("logger.openLog", (event, args) => { ipcMain.on("logger.openLog", (event, args) => {
console.log('正在打开日志'); logInfo(LogModule.APP, "Attempting to open log file.");
shell.openPath(logPath).then((errorMessage) => { shell.openPath(logPath).then((errorMessage) => {
if (errorMessage) { if (errorMessage) {
console.error('Failed to open Logger:', errorMessage); logError(LogModule.APP, `Failed to open Logger: ${errorMessage}`);
event.reply("Logger.openLog.hook", false); event.reply("Logger.openLog.hook", false);
} else { } else {
console.log('Logger opened successfully'); logInfo(LogModule.APP, "Logger opened successfully.");
event.reply("Logger.openLog.hook", true); event.reply("Logger.openLog.hook", true);
} }
}); });

View File

@ -8,10 +8,16 @@ import {
updateProxyStatus updateProxyStatus
} from "../storage/proxy"; } from "../storage/proxy";
import { reloadFrpcProcess } from "./frpc"; import { reloadFrpcProcess } from "./frpc";
import { logError, logInfo, LogModule, logWarn } from "../utils/log";
export const initProxyApi = () => { export const initProxyApi = () => {
ipcMain.on("proxy.getProxys", async (event, args) => { ipcMain.on("proxy.getProxys", async (event, args) => {
logInfo(LogModule.APP, "Requesting to get proxies.");
listProxy((err, documents) => { listProxy((err, documents) => {
if (err) {
logError(LogModule.APP, `Error retrieving proxies: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxies retrieved successfully.");
}
event.reply("Proxy.getProxys.hook", { event.reply("Proxy.getProxys.hook", {
err: err, err: err,
data: documents data: documents
@ -21,8 +27,12 @@ export const initProxyApi = () => {
ipcMain.on("proxy.insertProxy", async (event, args) => { ipcMain.on("proxy.insertProxy", async (event, args) => {
delete args["_id"]; delete args["_id"];
logInfo(LogModule.APP, "Inserting a new proxy.");
insertProxy(args, (err, documents) => { insertProxy(args, (err, documents) => {
if (!err) { if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy inserted successfully.");
reloadFrpcProcess(); reloadFrpcProcess();
} }
event.reply("Proxy.insertProxy.hook", { event.reply("Proxy.insertProxy.hook", {
@ -33,8 +43,12 @@ export const initProxyApi = () => {
}); });
ipcMain.on("proxy.deleteProxyById", async (event, args) => { ipcMain.on("proxy.deleteProxyById", async (event, args) => {
logInfo(LogModule.APP, `Deleting proxy with ID: ${args._id}`);
deleteProxyById(args, (err, documents) => { deleteProxyById(args, (err, documents) => {
if (!err) { if (err) {
logError(LogModule.APP, `Error deleting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy deleted successfully.");
reloadFrpcProcess(); reloadFrpcProcess();
} }
event.reply("Proxy.deleteProxyById.hook", { event.reply("Proxy.deleteProxyById.hook", {
@ -45,7 +59,13 @@ export const initProxyApi = () => {
}); });
ipcMain.on("proxy.getProxyById", async (event, args) => { ipcMain.on("proxy.getProxyById", async (event, args) => {
logInfo(LogModule.APP, `Requesting proxy with ID: ${args._id}`);
getProxyById(args, (err, documents) => { getProxyById(args, (err, documents) => {
if (err) {
logError(LogModule.APP, `Error retrieving proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy retrieved successfully.");
}
event.reply("Proxy.getProxyById.hook", { event.reply("Proxy.getProxyById.hook", {
err: err, err: err,
data: documents data: documents
@ -54,9 +74,16 @@ export const initProxyApi = () => {
}); });
ipcMain.on("proxy.updateProxy", async (event, args) => { ipcMain.on("proxy.updateProxy", async (event, args) => {
if (!args._id) return; if (!args._id) {
logWarn(LogModule.APP, "No proxy ID provided for update.");
return;
}
logInfo(LogModule.APP, `Updating proxy with ID: ${args._id}`);
updateProxyById(args, (err, documents) => { updateProxyById(args, (err, documents) => {
if (!err) { if (err) {
logError(LogModule.APP, `Error updating proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy updated successfully.");
reloadFrpcProcess(); reloadFrpcProcess();
} }
event.reply("Proxy.updateProxy.hook", { event.reply("Proxy.updateProxy.hook", {
@ -67,10 +94,16 @@ export const initProxyApi = () => {
}); });
ipcMain.on("proxy.updateProxyStatus", async (event, args) => { ipcMain.on("proxy.updateProxyStatus", async (event, args) => {
console.log("更新状态", args); logInfo(LogModule.APP, `Updating status for proxy ID: ${args._id}`);
if (!args._id) return; if (!args._id) {
logWarn(LogModule.APP, "No proxy ID provided for status update.");
return;
}
updateProxyStatus(args._id, args.status, (err, documents) => { updateProxyStatus(args._id, args.status, (err, documents) => {
if (!err) { if (err) {
logError(LogModule.APP, `Error updating proxy status: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy status updated successfully.");
reloadFrpcProcess(); reloadFrpcProcess();
} }
event.reply("Proxy.updateProxyStatus.hook", { event.reply("Proxy.updateProxyStatus.hook", {

View File

@ -56,10 +56,7 @@ export const initUpdaterApi = (win: BrowserWindow) => {
}) })
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
console.log('update-downloaded')
dialog.showMessageBox({ dialog.showMessageBox({
type: 'info', type: 'info',
title: '应用更新', title: '应用更新',

49
electron/json/extract.py Normal file
View File

@ -0,0 +1,49 @@
import json
import os
import requests
import logging
# Set up logging configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def extract_frp_releases():
current_dir = os.path.dirname(os.path.abspath(__file__))
json_file_path = os.path.join(current_dir, 'frp-releases.json')
logging.info('Reading JSON file: %s', json_file_path)
with open(json_file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
checksums = {}
for i in data:
for asset in i['assets']:
if asset['name'] == 'frp_sha256_checksums.txt':
logging.info('Found checksum file: %s', asset['browser_download_url'])
content = fetch_txt_content(asset['browser_download_url'])
if content:
lines = content.splitlines()
for line in lines:
if line.strip(): # Ensure the line is not empty
parts = line.split()
if len(parts) == 2:
checksums[parts[0]] = parts[1] # 反转映射关系
logging.debug('Added checksum: %s -> %s', parts[0], parts[1])
output_file_path = os.path.join(current_dir, 'frp_all_sha256_checksums.json')
logging.info('Writing checksums to file: %s', output_file_path)
with open(output_file_path, 'w', encoding='utf-8') as output_file:
json.dump(checksums, output_file, ensure_ascii=False, indent=4)
def fetch_txt_content(url):
logging.info('Fetching content from: %s', url)
response = requests.get(url)
if response.status_code == 200:
logging.info('Successfully fetched content')
return response.text
else:
logging.error('Failed to fetch content, status code: %d', response.status_code)
return None
if __name__ == "__main__":
extract_frp_releases()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,440 @@
{
"013179d5fd3a5c1a48cf8b2da99c8d48f0f95b96907ad77483deb4d8615578f2": "frp_0.61.1_android_arm64.tar.gz",
"403a0ee5e92f083a863d984b7af1e9d70ba2aaa28e87f42f1fe085adf76b8491": "frp_0.61.1_darwin_amd64.tar.gz",
"3e65f13a17a284bd6013e6bb6856bc2720074cea6094cc446c1f4c3932154c2d": "frp_0.61.1_darwin_arm64.tar.gz",
"cb5edf41be38bd47bd7f68210c34500f5dac74423d8084ed8d7e0f3bce050cf5": "frp_0.61.1_freebsd_amd64.tar.gz",
"bff260b68ca7b1461182a46c4f34e9709ba32764eed30a15dd94ac97f50a2c40": "frp_0.61.1_linux_amd64.tar.gz",
"8c7ebb07916e69d959d2a90f80900850c41b6dc1decdb87ccba4bc2f3574f855": "frp_0.61.1_linux_arm.tar.gz",
"af6366f2b43920ebfe6235dba6060770399ed1fb18601e5818552bd46a7621f8": "frp_0.61.1_linux_arm64.tar.gz",
"f956807c4af5cc0fa3b06f2365f342abcb4b8f780b6f948db5a9d8c02bb3e7ba": "frp_0.61.1_linux_arm_hf.tar.gz",
"e4b096fe86b09fa4b2c292b9c833dd9d3f322cbc4b67e8f73d40267f9451edc2": "frp_0.61.1_linux_loong64.tar.gz",
"8c0e0c1384a0e8601153f767392c23c5cd90e8112c522d4d0852c8b477909f8b": "frp_0.61.1_linux_mips.tar.gz",
"96d94c7676aad45b3cb1fb9bb7ee8cfc36ba4db9803fe77636d4bf711e79e0fb": "frp_0.61.1_linux_mips64.tar.gz",
"febb306ffbaa7a74e27fdd37919a342d148f27d33afc13a0ecbec73d68e5e204": "frp_0.61.1_linux_mips64le.tar.gz",
"539d8d91091410abbca80efae36f8a693a9132094f2340ff781b390c7eac5a50": "frp_0.61.1_linux_mipsle.tar.gz",
"7f75d6fc124e94da1703fa94cd9fec87bfe9c2a8c6c6c5bdda53266d5c0b82ca": "frp_0.61.1_linux_riscv64.tar.gz",
"e0094cd0baf03d5ff9ce9739199406871ad8788cf51e766f00ad3a9e7a836f3a": "frp_0.61.1_windows_amd64.zip",
"099914b6f66b1d983a9402e3a9a116bccc683c877f6dbfbbb5c0f171b584675f": "frp_0.61.1_windows_arm64.zip",
"6e4e4c3b73b4637658e8a19bd214efa67d24d18213d0470d8ab564b279935ab4": "frp_0.61.0_android_arm64.tar.gz",
"731bab6f15ac2dcfc63d9b3c2040ee68c465f8520c5369a8a6f37c467568280b": "frp_0.61.0_darwin_amd64.tar.gz",
"dafa23f2424ec1926f689a4d36d37e6e5b0050858755d2977a9e6171e1c8fb25": "frp_0.61.0_darwin_arm64.tar.gz",
"bc257408cc57d5982638f6aecda0ff2d3d46597b5b12d780c5135a8b72ea38c5": "frp_0.61.0_freebsd_amd64.tar.gz",
"720a9fe2a3299346572544909a78c023344c88bde13c55b921e298e8c5ded21f": "frp_0.61.0_linux_amd64.tar.gz",
"38b2d2f9a46b636dcdf4d656373de86f6c869da98a4e323bd9587989c1c06db0": "frp_0.61.0_linux_arm.tar.gz",
"8d54b8faae5df02268bd784f78a155494893c6eb00070a185022198c1997ec7f": "frp_0.61.0_linux_arm64.tar.gz",
"f151b5087870a72faa13c026336f3a6b97f0df2dcae3f3b122c43e604772cd23": "frp_0.61.0_linux_arm_hf.tar.gz",
"19928aeeca3806606958ea4843390cd427c05631e00afbd91b9a48855a18ba79": "frp_0.61.0_linux_loong64.tar.gz",
"601a8b19647db6258381d388861f4d64081fabae419788ae16bb8cd46390f2f7": "frp_0.61.0_linux_mips.tar.gz",
"fbbbab9339c87cfff52b4a7cedaaca0623921599fee10f845139ad7bdd6aa384": "frp_0.61.0_linux_mips64.tar.gz",
"b37c43ec11ae2ab50610eaf0251c9b3f694291d529afc757cd4645f7b8eb111a": "frp_0.61.0_linux_mips64le.tar.gz",
"4defa5cd6372d06188284f61fee90865222d8297a51005253dd6a5161ea538e4": "frp_0.61.0_linux_mipsle.tar.gz",
"0131890f58576631cd3dc8ea937ccd18f70f3162fb39840f56df8339f5799138": "frp_0.61.0_linux_riscv64.tar.gz",
"ab7ae0633df538b35d0490212eabfd6d3a0051e9e446ae0053c9f63921a8c46e": "frp_0.61.0_windows_amd64.zip",
"939f3f3c78be9221a9bc3a2380c1168fb5797ba66504d7188890bf93838ee6d5": "frp_0.61.0_windows_arm64.zip",
"4937324806d0c5aed7720d4e43f2a3fe1fb0e40d9b1b1c18c33417568d168c84": "frp_0.60.0_android_arm64.tar.gz",
"e0109b707168c98b95d2c0382637d0e8e83918d8fe0cdcb1794999f1750d33b9": "frp_0.60.0_darwin_amd64.tar.gz",
"e87da68fb5d3f9af6e7209b0c64cb6f22d3c9c3adfaad509c78c263ecaed7906": "frp_0.60.0_darwin_arm64.tar.gz",
"05222274b4577f71087e57273223a86e9dacfb6c53f9371c0c0e67f371aff146": "frp_0.60.0_freebsd_amd64.tar.gz",
"be5e957d20358d9998407204a03abc27d894f3cd61691f8d8ebe1e5f0eea4ca3": "frp_0.60.0_linux_amd64.tar.gz",
"e9d66ce99b14cadef5e850a6ca63b131486898ecaee580e6b90c5418ac4ccf72": "frp_0.60.0_linux_arm.tar.gz",
"bd52be08eaf872b07a48d9a3f46d7dd8f7e9340d76f12d316f39717f05d644e1": "frp_0.60.0_linux_arm64.tar.gz",
"75627af508bb31c37d13f6559b4acccdd6779304d56324ffc1cc737037055b40": "frp_0.60.0_linux_arm_hf.tar.gz",
"b58952173a2274759059ffa0d7ed3997f953852cddd4144bc3002611e23811a5": "frp_0.60.0_linux_loong64.tar.gz",
"9e3a587a164b24613fc587d3d17270d1b6623729b56135e6d63eebf56c41bb4c": "frp_0.60.0_linux_mips.tar.gz",
"2747e75da664dc9ca54baea63e499a36c0bd3ee34fc7950cec868e7502f7b192": "frp_0.60.0_linux_mips64.tar.gz",
"bfa63bb8f3d5f96033e5c58d41c8fb7bcdbbdbb6f790a77de5b84bf466f24529": "frp_0.60.0_linux_mips64le.tar.gz",
"ad7fc34df2e951a8de187ed7ae667dcb85bae68009f33a2a607da1b8a4caea1e": "frp_0.60.0_linux_mipsle.tar.gz",
"0f29c256bc20942ea5a19d8e6d3caacdd2f4d5134e632c50bfb384046666ae28": "frp_0.60.0_linux_riscv64.tar.gz",
"5243b70972af6416af85779b943945a80050b1733b31ab41fa94f3617f655492": "frp_0.60.0_windows_amd64.zip",
"1a41b2226a7478d3d86e275ca50f43e3587874973141bc43b552d258530f0443": "frp_0.60.0_windows_arm64.zip",
"5d3b92e28e0e293d87d32540bcc724a43ba2f4ebcc0549a1411d733e62f561cf": "frp_0.59.0_android_arm64.tar.gz",
"a784616e949fd9a00dc457e1b2e5fac871a9e420206f50200b5751f967af2d7d": "frp_0.59.0_darwin_amd64.tar.gz",
"54886307b97f60d698ddc0dd940514fc7653764580918f60a470a758eebf2323": "frp_0.59.0_darwin_arm64.tar.gz",
"bac63201e778139da931916328daf53ca575ab88f391c304b12550f04e63eb87": "frp_0.59.0_freebsd_amd64.tar.gz",
"54927eb5c07bd51850771ea55ca23338cdebdbe227d1acf7e2f0d530bd5e09c7": "frp_0.59.0_linux_amd64.tar.gz",
"2c958e363acedd08be939016ec12d17f66470e4da601b21247af1b31ea74e606": "frp_0.59.0_linux_arm.tar.gz",
"5b23e876071521d4f745f60fe09e81753cf84df22a0c7ab47d3692a09d758d01": "frp_0.59.0_linux_arm64.tar.gz",
"ca0a66dc4bd732f7c39116c9bce55a2b416898d3d1584b7eee1795a879779636": "frp_0.59.0_linux_arm_hf.tar.gz",
"a18840130e7056e9c8854ad1835d4b58b8df2c8151459a73dffdabe38b887b2b": "frp_0.59.0_linux_mips.tar.gz",
"e99827b49e17224b07c6c0af3876aa98771813aeead4d98d4e3c71a657c29211": "frp_0.59.0_linux_mips64.tar.gz",
"d32ae7a736082720b1672006783b5c17309287ee13bab552dddc99e17537bed7": "frp_0.59.0_linux_mips64le.tar.gz",
"6dec7136bc1ef26e14e5190d1c4809088f4173ffd140daf84d623bd180949eaa": "frp_0.59.0_linux_mipsle.tar.gz",
"e373ae9a57a286343e9e7fe1c4d1d4ff676680a0aea7041b75dde1ee24494158": "frp_0.59.0_linux_riscv64.tar.gz",
"a53405994ad03b245b36ab98513c8b7370bc0a36be128b057dc687354d6464da": "frp_0.59.0_windows_amd64.zip",
"f9978e5f98b22b1171b03925e73b2601aa485cdd4993dc7ef82557fa0d173ea6": "frp_0.59.0_windows_arm64.zip",
"c31e71a4b845e8ebc34bf5ddcc116a8230322101fff2418de8be9e46f1c6047e": "frp_0.58.1_android_arm64.tar.gz",
"e3a15197675afe2dda9ca7588d76d6597c30c7d55ed8fd2ae8c9d7425c8ce82e": "frp_0.58.1_darwin_amd64.tar.gz",
"be615731a6f34bf92c44765abe4a7df25d82226f255b1d1fda5f36a0ea083ab4": "frp_0.58.1_darwin_arm64.tar.gz",
"73576032ee89de285142e6d74f7bb6fb8cc5c373961e4c8910ded6aa13c3e88c": "frp_0.58.1_freebsd_amd64.tar.gz",
"5bd9f8860b580ed9c42eed1c99dfaa03b196d0f68007dca088f6c098d498430d": "frp_0.58.1_linux_amd64.tar.gz",
"44c59c70aaefe48dd9b0427c7e86a6a24c7e83fc31e38a7a2885dd1d53c05091": "frp_0.58.1_linux_arm.tar.gz",
"25a77f4d7f4c5efeeaa89ed65b951a19014e79baac1efcbd57f0598b3ba95fd7": "frp_0.58.1_linux_arm64.tar.gz",
"d1bbe041d05f48adebdd2a8b6754daf1062f66a8139a32589a10a31890348d49": "frp_0.58.1_linux_arm_hf.tar.gz",
"8a30318eeeb996364eefe40f8f946250fe709a1c487f795643c85a7b53cf86e5": "frp_0.58.1_linux_mips.tar.gz",
"ac445e3f75eca9bd33388ebe7c33d6124f5c3fa0c7a3419b5285dabe2e5415b9": "frp_0.58.1_linux_mips64.tar.gz",
"291d4fb93ccaf8c79087a21744a06716b9899b1bb71e9c2cb48c487adcee3bdb": "frp_0.58.1_linux_mips64le.tar.gz",
"db325305379dd39a82baf0910e5d01a426726231ff8775c2c6295232500d6bbb": "frp_0.58.1_linux_mipsle.tar.gz",
"710638bde211638e7cd096811f77b994cee0081cb0c9c80c7b48d92ae2452f28": "frp_0.58.1_linux_riscv64.tar.gz",
"7aa26c8c082c2fc7960b422811c1bbcbfae219b2a108d6604a645e8dc7476337": "frp_0.58.1_windows_amd64.zip",
"14cb8785617bdac2af87133061ec9a303824044bc8a64850db72e6b150bf0e71": "frp_0.58.1_windows_arm64.zip",
"d3845380ea2f5291f42c382d6d64afae694673d4a41b86be8d4c08e9f03c5b33": "frp_0.58.0_android_arm64.tar.gz",
"7ddf254bbce297e1ffbae0fd696e9a3a3414cdc8a2c270b00b59f45c19b8c5c5": "frp_0.58.0_darwin_amd64.tar.gz",
"0bd38fb57e46d13bd1fe7469a875a3123e298f4d0485feac8dd7a80d885f3676": "frp_0.58.0_darwin_arm64.tar.gz",
"a347a2e294986018591c43b029d85a6911c58a60df060fdb0de11b1d70bb916d": "frp_0.58.0_freebsd_amd64.tar.gz",
"09a6a170070ce02d7de6afa941d341a2caad0aa8e09fb840a64da4acba24d73a": "frp_0.58.0_linux_amd64.tar.gz",
"5e4b8b5ac36fe62f26ef37430aa56b11e562b046849796c51a61fd90251af18a": "frp_0.58.0_linux_arm.tar.gz",
"ceef6df44f3a64b4a2076dfbb0ac02a3750b48fa0ac568478190ef477d4f2ec9": "frp_0.58.0_linux_arm64.tar.gz",
"379ad22bf406186d8665e9be73b8b0600625abf7e8f6403fe3ef9d59e97c011c": "frp_0.58.0_linux_arm_hf.tar.gz",
"3ef58b278d97290540912692f053c1a5d48fc382a85a774f0b8b3ed060af067e": "frp_0.58.0_linux_mips.tar.gz",
"2b90360468e67cebb9a5e656585da0e9de84ee1a811ee9d02bb275b9e736bf7e": "frp_0.58.0_linux_mips64.tar.gz",
"3519353b094158f59f1f10a3bdbac473cf04bd09f572a0225c2ae5f545ee45bd": "frp_0.58.0_linux_mips64le.tar.gz",
"3ccccc5f76b3750137e9227ee904635266a0a434f4d6fae7d431382477372388": "frp_0.58.0_linux_mipsle.tar.gz",
"f024440f7987fc845814ecdf058f4363690a53850c241af5c7c01fba6632c78e": "frp_0.58.0_linux_riscv64.tar.gz",
"f827466f3f0e256350b66881721bd52753ef5243765e6c22347ec83a271d555f": "frp_0.58.0_windows_amd64.zip",
"8bb04979969040c4c495450bc7921b23054f2d7418f2d8378bd65ecbd8b381b2": "frp_0.58.0_windows_arm64.zip",
"e20d90b0670c637a65125f89467170efb3fc227a78f44ee585a6d3fb55b6a881": "frp_0.57.0_android_arm64.tar.gz",
"9a12912bfbf7dad0ebe5fb3b0229b318a8670d078137f2384f81c1aa87bc0fb0": "frp_0.57.0_darwin_amd64.tar.gz",
"b18153bc7a6d6627f402380a6e5ac01b631207df54d7fcc0d89a8f6f81521401": "frp_0.57.0_darwin_arm64.tar.gz",
"22df14e317c351bda4bfaf256c46b6ec281304135ea24c00bb2a71a5e14d4f22": "frp_0.57.0_freebsd_amd64.tar.gz",
"3fd70ccfab20e75b8517627ec58e30b33003a24ca4629ed42650ef1b98f17e7d": "frp_0.57.0_linux_amd64.tar.gz",
"3c9e03e28899ba18e42f51006f7d94192fbae009885fd91cfc75b354cffebf58": "frp_0.57.0_linux_arm.tar.gz",
"e2f75360702bcdc390997de7b2557f21a1f28d7ebd4d1ca74cf2e38849185bcb": "frp_0.57.0_linux_arm64.tar.gz",
"795defca4853f7cded6625d792eae33b45987856b961a82c8b6cc44a8d0b3bc7": "frp_0.57.0_linux_mips.tar.gz",
"a41466714ba9463978139a62d241893a034425235b61ecf2efd868857e1c83b5": "frp_0.57.0_linux_mips64.tar.gz",
"d5d2ee272caa314a731dcc59ed4474c9f34953c617e8c29fdd86ea8c017f2e91": "frp_0.57.0_linux_mips64le.tar.gz",
"2fae90ae2544f8b46582cfb7d46984d837b193601b35aa9d63c2f4f52007e32b": "frp_0.57.0_linux_mipsle.tar.gz",
"4e155fcf4f0c7e186ccd2be94a2e036bb62790c9bc00d9145a2999b5e3f38717": "frp_0.57.0_linux_riscv64.tar.gz",
"18b6a345f7d4fb9250b8d751a99f58a0a2daace02a1f7a4e7bb567237e681335": "frp_0.57.0_windows_amd64.zip",
"6fe6b708ab65d61293fb7f1669a3dceab6d8a7d06f9f9b93db68025873f51c44": "frp_0.57.0_windows_arm64.zip",
"ef44189d246b4a95e0eabbf1d6d86ba94002e6f2bb5eefca8e3e8b8292abc085": "frp_0.56.0_darwin_amd64.tar.gz",
"93afeb34c835796508383b70028216eb3d43b2bf63bb3f7493acd1ec533d588e": "frp_0.56.0_darwin_arm64.tar.gz",
"531be6e910202087c61e10e57e28eee9a079fee380b8a42432de55d570bb25cb": "frp_0.56.0_freebsd_amd64.tar.gz",
"084d3c601a9f5d100ad3be26d94b643f2843fa64dcc5f2f2057c612bf7f9d4f1": "frp_0.56.0_linux_amd64.tar.gz",
"cf5cc61f68d705860538b8d3e865ae026a7b27e4da8c1c1a3f50c5e7827cd097": "frp_0.56.0_linux_arm.tar.gz",
"8805d70a692b0c5e20271214af085ffc3d8ea2176ce5dbe06fd6e4de59d8206f": "frp_0.56.0_linux_arm64.tar.gz",
"03fce0574a2df7993efff8bf3d1e45250b08692081cff53dfd266745db772f27": "frp_0.56.0_linux_mips.tar.gz",
"db2975501126fc0f61097acdff7484655e5d37b01de8c509c2c5e0e88591fb42": "frp_0.56.0_linux_mips64.tar.gz",
"4cdca1cc3d298a5e6628ec40e174882e26039d953492eaef6c0d25cef065ace5": "frp_0.56.0_linux_mips64le.tar.gz",
"0679e059dfca6cd022caf808ffe2709207377463a31ccddee1bcb75c161b341c": "frp_0.56.0_linux_mipsle.tar.gz",
"fdbcc2a7d73552e690bc9ca7fccb69b9efdf10fc4d78f0f7c63b14a9129bb116": "frp_0.56.0_linux_riscv64.tar.gz",
"572872fec378f423b141faa205b44faa07bbf06f7272b0a6a3235c7992a69998": "frp_0.56.0_windows_amd64.zip",
"b1c9ee1dff229639c43c60e39a6023798b5c96ccd38df7e3edd41cfb6990c90a": "frp_0.56.0_windows_arm64.zip",
"efd2156b1477d88b8ce1d9428cdeb1689bd12cefb4b31ca81b70eb7d65e22e59": "frp_0.55.1_darwin_amd64.tar.gz",
"220583e20edd98369dbe929d215a387ceea937b0e0637f62558506b2a6c603a2": "frp_0.55.1_darwin_arm64.tar.gz",
"c20f8abf5e0933bfd88fa974ad3a005c72f494aafc021916927774ab0ce6ca46": "frp_0.55.1_freebsd_amd64.tar.gz",
"4d13675c330ca07d532f7a2ebc72fdc011487fe318f2ee645842a3fa4b23c966": "frp_0.55.1_linux_amd64.tar.gz",
"bc283cb6e280e5fd5089216c8362003235dcf371e9f99bbc14462a0ef05c0b53": "frp_0.55.1_linux_arm.tar.gz",
"f14655042086ef4653c0351a6464fb7d73473baf26e15a5f59c298bd3df23d1c": "frp_0.55.1_linux_arm64.tar.gz",
"550e7d04aa4d00fb81b1cd566c58b056a3da8bcfd05631e5f4edd673232b9062": "frp_0.55.1_linux_mips.tar.gz",
"8a0f1ef0b8723089613e2754d965ac9059eed027064bdd484f417fa6f5756d12": "frp_0.55.1_linux_mips64.tar.gz",
"21b32cdaf6e4c74a88a0b6c3c377a3d40a23f73c0313625fa63ba4a6542616fe": "frp_0.55.1_linux_mips64le.tar.gz",
"b09684adfae58733bc12cd0ee3cf1e20d6b888c3e5280cf9f9e7a6467cf87a71": "frp_0.55.1_linux_mipsle.tar.gz",
"52fabafac257ef8ca28e53cc4f210789cfd882946d0f9d2f9457d63f0344a602": "frp_0.55.1_linux_riscv64.tar.gz",
"eeb4247038f58d6b89bd5608782489eeaa7bcfb83d61b5475284ab612978b328": "frp_0.55.1_windows_amd64.zip",
"b48943e9641fde4b91e0032fa031599fdbe3f9cebdd8612cec9e3477aecf2866": "frp_0.55.1_windows_arm64.zip",
"b509e7d50b164aaa62b30efb189caf965615ce266d51c243e494bca14d2f2864": "frp_0.54.0_darwin_amd64.tar.gz",
"dd8057968d3560e9ecb42b2ed50b796ec09573d5263f689c8e0633a8b8a7127a": "frp_0.54.0_darwin_arm64.tar.gz",
"ed25f0c61c45c7f013f2f5ef9194cb2854805db9c692f656e2b30a6ad1681436": "frp_0.54.0_freebsd_amd64.tar.gz",
"13102618f84a2efa07a90733d9bae72e48b897c29f4df4b38bdacebb99517e52": "frp_0.54.0_linux_amd64.tar.gz",
"0ccc051693da612b7c4eed265598d3c8878019cb21e6ec9e3869f94b93e6ca80": "frp_0.54.0_linux_arm.tar.gz",
"a3f01a59bca7cb330bf680019595bbbf5f8167494fab4c46eaaf836fdc3a1902": "frp_0.54.0_linux_arm64.tar.gz",
"1a1a729fe607c59dae787bc5322efcf8cc5a9e87623c6d10e2a08531829bb9fb": "frp_0.54.0_linux_mips.tar.gz",
"24fccce2e9c6684480bfd8ac0e9ea3e36d4203922fa5a39ae9f63bc0542f68f5": "frp_0.54.0_linux_mips64.tar.gz",
"18ee2a78c352eeceb07d55ba572955af64b14282914fe77edf632baf4ce0f967": "frp_0.54.0_linux_mips64le.tar.gz",
"e73e6a2bc3fc1900fb2810bf53bed0471149fb07c60917027661d9d654c0f6e8": "frp_0.54.0_linux_mipsle.tar.gz",
"3961db6d3c5951da49b40cfdae22c8fd53ea87a2ff97245d8aadd4d4206c6fea": "frp_0.54.0_linux_riscv64.tar.gz",
"95f0d8c8f4781fc8e42b7d644024c647032e3f6cd0ffe425e8f7d5a46d601557": "frp_0.54.0_windows_amd64.zip",
"61b4d21b669ceb671b298a4ed4aa3c70b33d6e3e4281f7417336a76f684424ca": "frp_0.54.0_windows_arm64.zip",
"91b1b306c1a538dd6d60857a1da9019241034bcaf0cc19e0c07abfaa8f6a8f75": "frp_0.53.2_darwin_amd64.tar.gz",
"76d2a7bc7ceb5f542ed5be5208f68253261a36d1f4206fc4689296d9033a59a2": "frp_0.53.2_darwin_arm64.tar.gz",
"3f9462f9c7aad6fec22159529b1db7382acd7254605894fbc44c7a7c464e148b": "frp_0.53.2_freebsd_amd64.tar.gz",
"df7356db409cc406294211063bf387a8b590289370811b1d10d6fdd1023c3250": "frp_0.53.2_linux_amd64.tar.gz",
"0f7acf26d92d39a2e3965ee91bf60e7c331844a1d7e81078ede526cf0459eccd": "frp_0.53.2_linux_arm.tar.gz",
"e67faadd41e6236f2bd67d35c9dfd807ff2941027686632f6f4c339dea8ef263": "frp_0.53.2_linux_arm64.tar.gz",
"5b8d4fddcbe0c9e1e82bf8ca30b97bde3fff668741e49a260d6c13c55584bbc9": "frp_0.53.2_linux_mips.tar.gz",
"ec8938be2d1b535eeaf7ba803dae2b6fa1059c6106791d59d98600928dfcc057": "frp_0.53.2_linux_mips64.tar.gz",
"0950dbcd22a110b50c7636f2ff7ca73ee120568d375d75539546c6590cd75ce9": "frp_0.53.2_linux_mips64le.tar.gz",
"f2f9c488451676a58566f6daf2a8a1c85aea193abdc7d7241ef0e12675238bc9": "frp_0.53.2_linux_mipsle.tar.gz",
"351b90825fb48695f36208f0e6cfbbd53f9539306119b5ca0aeb949bd255066a": "frp_0.53.2_linux_riscv64.tar.gz",
"043cd981e81f756123ea4501569ad8d1fbb8166d1046b349ca423aa6ddc0ce31": "frp_0.53.2_windows_amd64.zip",
"26eb992318437fad2d122ef76cfb3086f1339201486a1cdec910fe1a457ac383": "frp_0.53.2_windows_arm64.zip",
"2c02d8f219e83bea4bb4c9ddf1222bdabc068f656992e967dc702e70a1aafd80": "frp_0.53.0_darwin_amd64.tar.gz",
"a148f12a5261ef3186322b08cf1b1907d987505ec5485adb290a350bb2083f63": "frp_0.53.0_darwin_arm64.tar.gz",
"8b0067e658dcbb21313ae8192aa7e1d364af8e96aeb7893ba7422ea0844e8bd5": "frp_0.53.0_freebsd_amd64.tar.gz",
"662d62af7744b9b639b3473bbdd2c4c70dfa5ac5fe1d058d13ce3cc7ea059500": "frp_0.53.0_linux_amd64.tar.gz",
"e33075389b77f94a816ac45bf1d0ce2b540fd98dafac9828602625088967762f": "frp_0.53.0_linux_arm.tar.gz",
"1d5b17f54911bc22816b0d72b32c258b259eb912d9d0484fdc949a315f5a5d42": "frp_0.53.0_linux_arm64.tar.gz",
"f0439788bbeda72664259defbc0edb12825cbf2928c922e06103b7b715bae88a": "frp_0.53.0_linux_mips.tar.gz",
"32665745aaf03d263a9ce87f0ea7a17eb3476328c25c1a1fcccd0925934f7313": "frp_0.53.0_linux_mips64.tar.gz",
"ad977caa79c00c082206f46f521b8f99a44a051425dbb69ec9da1a152aac6279": "frp_0.53.0_linux_mips64le.tar.gz",
"718c0f0820f65782bc19af479f2406c9654fc564b9999a0936581b4ed1d91bb2": "frp_0.53.0_linux_mipsle.tar.gz",
"0e8f1915a1e2b1b2d37b11e831e49fb5f5fc2a14eea086f7ea5a1e4112095728": "frp_0.53.0_linux_riscv64.tar.gz",
"dfd7bc3410c018dc8bcf897696ddfb10e7aaf5a584b8220ae3949ec87205ea4c": "frp_0.53.0_windows_amd64.zip",
"23bafd6bf4ac0e631b37bcdc68827f4b36f06c3dcf0bd754f5d0f9acb4606a3b": "frp_0.53.0_windows_arm64.zip",
"07a0651b2053508bab9370df884096effa653cb24cfd8c454c438b15971ece63": "frp_0.52.3_linux_riscv64.tar.gz",
"0bf96f473385bbeb64faad3caec3ad721187b328f2228820e49838e187da0e22": "frp_0.52.3_linux_mipsle.tar.gz",
"24395170dfc41544eceeb78529c8de5b57b65250c27a02e058cd013e6f66097f": "frp_0.52.3_windows_arm64.zip",
"25431755a121c12dab3c28fec18eaef027a73aa5e9780b33f6801e152e42ab36": "frp_0.52.3_freebsd_amd64.tar.gz",
"3fcf04657f8efd6c6418047bb8c219878c913c4bdc678a8c4bbc8a49d3a389d1": "frp_0.52.3_windows_amd64.zip",
"46b6b8e83ccbbbc2e639c852dae9a41e79f8523d444fe39f9d8f7cc5e7661081": "frp_0.52.3_linux_arm.tar.gz",
"5e041b19ba9ca6a5255679b353099946065edfdf951d807db2587fa8c95b1447": "frp_0.52.3_linux_amd64.tar.gz",
"8e05baa844d928b6239bd9f43cd3e065fc2af971930bc6344e2c899d7eea14db": "frp_0.52.3_darwin_arm64.tar.gz",
"9aab5a4936295d13f2602c8e087fd789a7910b3b3c9a47b9fb799ec99020192b": "frp_0.52.3_linux_mips64.tar.gz",
"a249c503a622599ba68330f323de22a457e058157cb8e38cd3e59581993c03d2": "frp_0.52.3_linux_arm64.tar.gz",
"b64b34521d1942f05b9224bb21d025af5c0ae99fa2e2dff635f26f91d91a6188": "frp_0.52.3_linux_mips.tar.gz",
"c3b011e15c03348592d4a2adcdb90994e7ed29a43f572945505a429c12645215": "frp_0.52.3_linux_mips64le.tar.gz",
"c992b9a8a53c53465f035d5e254ecc1a9455f260fd110fe1600d5da4a37df413": "frp_0.52.3_darwin_amd64.tar.gz",
"05e2ba6184dcebe6fa334c2a1d4534433e8ff9372636ff98eef96e414212903c": "frp_0.52.2_freebsd_amd64.tar.gz",
"11f2af35bdaa799a38a180a1b73083d68843cf731ecea118a33597a14289589e": "frp_0.52.2_windows_amd64.zip",
"5ad396bc221aefa47d1192d6df11193240891ea3a88d0f0b941e1cb2967e2a01": "frp_0.52.2_linux_arm64.tar.gz",
"6add94e2916fd776bc2fd62a01fa6fd282f040e2f05ba42962e823eac821ae81": "frp_0.52.2_linux_amd64.tar.gz",
"8c47d8f1ad960d0f0459bd0fae7bc33c9266943d04549145b969c9107c59703f": "frp_0.52.2_darwin_amd64.tar.gz",
"94169b8d725d30bb0ddf19db73d18b99544dcc52521507419eb7fb42823ea8ac": "frp_0.52.2_linux_arm.tar.gz",
"b09d38e5eba230a6bb04f144f5d32d26ce69f1424bbbb1058d43c712ff558679": "frp_0.52.2_linux_mips.tar.gz",
"bc886aea03ddb2d4201501904a25816ac962cd3fbe6bc7fab3ca05357069666d": "frp_0.52.2_linux_mips64.tar.gz",
"c32b3159a8aa089b08222987a32b9856c046c276898613c75eec62d370df7e01": "frp_0.52.2_linux_mipsle.tar.gz",
"c7b22ed0a87596cd839b555e4992d80691359e75409063b6dca2dda96e7da480": "frp_0.52.2_darwin_arm64.tar.gz",
"ce70a9a044271be4336d7376aa1d5c5f8de8497b1e284b083f6d2184d6f57042": "frp_0.52.2_windows_arm64.zip",
"dc3220af2b22469da26209d4b376858c11160127e83bce09f85cd0c27a44d5d0": "frp_0.52.2_linux_riscv64.tar.gz",
"f1985ce963979371360df27054ba07df4d4ee35338880bed83ef609a4648c420": "frp_0.52.2_linux_mips64le.tar.gz",
"076d9ce5c8644dbeb313e2d90349ad33d3b718b2701899480573266b3f6f0e6a": "frp_0.52.1_linux_mips64.tar.gz",
"136cc6be28c798b2493875f498b5956a876c24cdbd028773aa9194c8bd846442": "frp_0.52.1_linux_amd64.tar.gz",
"13f227bc915c43961e1f3831f155c6934e7d5a65434af3b29bf494b1d5d276b7": "frp_0.52.1_linux_mipsle.tar.gz",
"1b3c61129cf7b45ad41a6b297f4425b9e700cf6302c8969232c7587ae7e727d9": "frp_0.52.1_darwin_arm64.tar.gz",
"69c08bae93e16aaf57debbe2b10df6824f5dfef32ce21b5d57d750b0698999ee": "frp_0.52.1_freebsd_amd64.tar.gz",
"73f3e7037e5f06e8f6fc30aa47aabbc815b4173decdcab149c647126a4aa6370": "frp_0.52.1_linux_riscv64.tar.gz",
"a7626329b690c269d640555033e156a55cffb967f11556eb782ff130d0ad7982": "frp_0.52.1_linux_arm.tar.gz",
"aff5412e89e7164b5083909f2b5a81d8edaa644a3bb6ef696843a6ee0d129fc3": "frp_0.52.1_linux_arm64.tar.gz",
"b2cb915a6e66c99fcceceae07b08d28002c575a3bc2c6aa8ea88c9ae45294be3": "frp_0.52.1_windows_arm64.zip",
"b993db8bf609419a850d3233f97bf422de7e5e54576120c36de0ad703e541bf2": "frp_0.52.1_darwin_amd64.tar.gz",
"d7c2ffe601af16d168d881b88817df81e9bc8646e56643545bd9a11f01ebac6a": "frp_0.52.1_windows_amd64.zip",
"e61df02bd13c250267ded9f0db8ef0e0f3a3eea63efbb8d041190883b0cee0cb": "frp_0.52.1_linux_mips.tar.gz",
"f64a03af886034ad8380631ef1d65728175f5af79674af39c29978a86c181c7a": "frp_0.52.1_linux_mips64le.tar.gz",
"1411f74ca4f05e63963448b9d0c972e16cbf98ba81864e1c04de0492ebd0c6fa": "frp_0.52.0_linux_mipsle.tar.gz",
"14c37cbee05947b2c67fe8064c132652b363c8b0d72fa401ddaf93efdc9538e3": "frp_0.52.0_linux_mips.tar.gz",
"1a8d2c5bfe3a0367068cdf890b025258e5614c3fef308985c001500902692817": "frp_0.52.0_windows_arm64.zip",
"4669cb8c374ff0ec48c0f6d15a939c59390c2109645914dd52d4deca519c084d": "frp_0.52.0_linux_riscv64.tar.gz",
"4794997fffc632dd8d357e9d00ca616e9efb2741e0f0acd1599f90be6281b9e6": "frp_0.52.0_linux_arm.tar.gz",
"5953e84b6a1590568b6d77a0b75093552577aa61484aff41b3ad0fb35c68719f": "frp_0.52.0_windows_amd64.zip",
"80228ba9bd43db42713f682032c0d4c2faa07ecb01be848bb57f6d51f24fa138": "frp_0.52.0_linux_amd64.tar.gz",
"8a5b86e7ea67bd1355ca5b9ddda60ecfdfb7c0b13cf06af71c1e72e88371016d": "frp_0.52.0_linux_arm64.tar.gz",
"91f46654fd8eae9fcc5a7189c6629a7e4b8f49654d996bbb45432cb4a46ac8f7": "frp_0.52.0_linux_mips64le.tar.gz",
"ad61f4285ae98dd4b8bad622888e97bb290e2ca667cd9ad52ad2877cc2ec6807": "frp_0.52.0_darwin_arm64.tar.gz",
"c17291696d623106324b9bad894599325a90148d7d19970b9142a445b789b571": "frp_0.52.0_freebsd_amd64.tar.gz",
"c68f67a262cf61a81945326e0e0c9e2a3dce209c3125bb0f05a16921141f4231": "frp_0.52.0_darwin_amd64.tar.gz",
"d21b617081093f98de5fc1e57700d4a104df67c4965f3fb99dc2650aefbce86f": "frp_0.52.0_linux_mips64.tar.gz",
"0108697c36c88f6ae776f923064236f4e890f3c887a94e798222e5ba3c08c568": "frp_0.51.3_linux_mips64.tar.gz",
"081e0f8ba995218e30ad3c0fa7a12493f17dcbbbac73fdae4391fddf8af2f918": "frp_0.51.3_freebsd_amd64.tar.gz",
"2e1a85c3cfa7cbbcb8747f53de4d7c913cd8ace7475988d823ca0e30bdcfa44e": "frp_0.51.3_linux_arm64.tar.gz",
"30b14705cdfcc4fbc654b55863d110a99deaa92a1490561e8dfd84326f9a9e9c": "frp_0.51.3_linux_riscv64.tar.gz",
"3fabb19b2157709cb6baea755513f38b2d5674539b54f7853454c48c5a9f22bf": "frp_0.51.3_linux_amd64.tar.gz",
"4cafe6451efd64e50a28f2533055b1f68fc59426838214d20341acba515b0eb5": "frp_0.51.3_windows_arm64.zip",
"5fc4a7caff50594c717e7d8e5929d4cb3e1674d81fd345a29abadce0a86d22f3": "frp_0.51.3_linux_mips.tar.gz",
"62170484c4d450fa47d86ed8b1dd20659b22cd7bc5a36caab330f244d6ea4d97": "frp_0.51.3_linux_mipsle.tar.gz",
"6abdb7353ae5562e16d28e1da142f5f97bd51964359901aafd694b4638f85739": "frp_0.51.3_darwin_arm64.tar.gz",
"bf8ab462d70a288b7ff2e9dda8151d16340ec4758843a619a936b7541f52fe54": "frp_0.51.3_freebsd_386.tar.gz",
"c35dcc7b9549eacce4d5b34a07a3d102b0c631ef4b72682ce0472f65b8777d4a": "frp_0.51.3_darwin_amd64.tar.gz",
"cf873001de9c33445213818c5844992e1a3a02486bd3defce556b95e9b0f4af0": "frp_0.51.3_linux_386.tar.gz",
"d1d9b02741e5d8742853665aad6a36a74a977fb82108b894712008db8d170276": "frp_0.51.3_windows_386.zip",
"d6373caf2bb26e7956c976d7d9142a082a0c259525bac3d5bb2fcfcbbfa63bc6": "frp_0.51.3_windows_amd64.zip",
"f300f69fe05b47e3b3e571a1fd83c7c0f7d69667d50a78ccbaa551bda3078169": "frp_0.51.3_linux_arm.tar.gz",
"ffa8edd59c275f6c592835b11b1f00e7c83c7d1e91aa8d9f6d666d286e902017": "frp_0.51.3_linux_mips64le.tar.gz",
"0b938c1c8389829602f511b4d8ebbe8f6d2ae6fb4e5a88540b1699c922a63610": "frp_0.51.2_linux_arm.tar.gz",
"13ac5e018ec166c098c2d67635068ad1b18247aaf02a8537532f52b4fda2dd29": "frp_0.51.2_windows_arm64.zip",
"3ce4df319c7ea35f8cfa13d1e03a0309fc4f57aeaaa02d05fb9fd560443e67ba": "frp_0.51.2_linux_riscv64.tar.gz",
"81930048c93d8db07af024cd0355809248501dec0ce182a734d16e6bd48055a3": "frp_0.51.2_freebsd_amd64.tar.gz",
"895b5c7ece8b458dff80ed790fc1633675a05fc9c4bd994ac89cf8e9d83bd32b": "frp_0.51.2_freebsd_386.tar.gz",
"9774490a0a4f822960a8da99a214cec6e2320622c2c20cd6b713e0e52806031c": "frp_0.51.2_linux_mipsle.tar.gz",
"99196195845422f6ac5962782fa3676f34fff343e0fed0f354cb6600d894afd8": "frp_0.51.2_linux_amd64.tar.gz",
"a41b7612e1057aff1743cdd0c9cf2dddd07f7e4e0340d419f05c42612b118a02": "frp_0.51.2_darwin_arm64.tar.gz",
"b430c31a107a7c5e48899e3ee800f39aa50300d3d76f87bb7afb7ede58875cfe": "frp_0.51.2_linux_mips64.tar.gz",
"b68640e6866a22639186095138657c53b0bb6626ec0438b488d1a2ffdde23155": "frp_0.51.2_linux_386.tar.gz",
"b83a269ce5fb9ff099695165a5d3565646f6032579c4bc6925c63fe8100aee0f": "frp_0.51.2_linux_arm64.tar.gz",
"c35d5b705e2b321cf612bcdeb44ee27392d6a1202248e8ec30bf178adf00f9da": "frp_0.51.2_windows_amd64.zip",
"cc928db0c984d3a7e9822ebb7ac897ddb90f43848488a5c3261b5704085fa92a": "frp_0.51.2_linux_mips.tar.gz",
"d458887ece9050b08d1d58c2718110643b87f254981cda6c86f25dd5559e3867": "frp_0.51.2_darwin_amd64.tar.gz",
"df37d932eb846e608187b0aca6d182467ff24c548a044b9206a93913ec93c752": "frp_0.51.2_windows_386.zip",
"f56461c7a75839fa5ab3f8be2988f9f5d57c8121c4d7c31e17d2d3a7447d2a7d": "frp_0.51.2_linux_mips64le.tar.gz",
"030544b09aff990592772ae508a62396c5648a267a14e5f2fad08324c3d9eb9a": "frp_0.51.1_linux_mips.tar.gz",
"03dae058d9b192aab4e119e620c40253f7693bfae095820ddd0313403d207d82": "frp_0.51.1_freebsd_amd64.tar.gz",
"1837335417e0bfa4c1caf7ce94047e1ba8020983c246b25679dc5efced9dae75": "frp_0.51.1_darwin_arm64.tar.gz",
"18740144d6c91dea850c695590973733ababc0634ca18073d2faec296f572b07": "frp_0.51.1_linux_mipsle.tar.gz",
"2379c3dc7bf783334051c06aec97ffb50007c9d17572aae45500f07c764ab99a": "frp_0.51.1_windows_arm64.zip",
"291fa7918aa575802ced2fb77e45f33a3cf7fc4b5c27c4ac31a68b2506c50a30": "frp_0.51.1_linux_mips64le.tar.gz",
"2d07711a0e24e3da968ad69aeeb458854572788e7869d276fcfb1189c824f9ff": "frp_0.51.1_linux_arm64.tar.gz",
"429b1032624f2fa211d31521f1d7f3703c022e476f6e225325842500eb3a37c6": "frp_0.51.1_linux_riscv64.tar.gz",
"4ce2100f0e9907d9dc152f94f56bf33bc44d029b2f83efde32b586a57bf55809": "frp_0.51.1_freebsd_386.tar.gz",
"70f57deb3ce57eb890104fe14d6fe442a815e095122a9c2b584e34d3c54f5563": "frp_0.51.1_windows_amd64.zip",
"74df509decd6953a77543ae8febcdc05379bb2bd0614ad2fe53a4a6cfac86caf": "frp_0.51.1_linux_arm.tar.gz",
"9f27cec3b7e600c0223c0de06b65feafa9ed6bf82a8b1dfe338aef6b03bac097": "frp_0.51.1_linux_mips64.tar.gz",
"adbfe65938517a8024565569825526643eac2d3294f4524d12a2846611107e08": "frp_0.51.1_linux_amd64.tar.gz",
"b7a7814aedd230b66e11f3626aa505a2a701d6afc19bc8be2143955bfa3c1d6e": "frp_0.51.1_linux_386.tar.gz",
"e0b8976e986ef0ed0901560810a81cc80cf8c332e087edd35f50e9a5a88c79ae": "frp_0.51.1_darwin_amd64.tar.gz",
"fe1eaa0c7066ad45a8a13838d15a6a6535e69250ecc3ed8c48bfb480c8b87e5a": "frp_0.51.1_windows_386.zip",
"025bf967e37ce095f31bc45d886156d365a0e9dc7aa0e7f3bbc91bd1c9717145": "frp_0.51.0_windows_amd64.zip",
"26acab3487be8980460ef86f0fdc7a446cfdadab02a5a0b27dc760ecce15ffc2": "frp_0.51.0_linux_riscv64.tar.gz",
"26c48aa4fa4458ad29d0de364904e24be40424d4f6c37005c2c2d9c6e41e2b06": "frp_0.51.0_windows_386.zip",
"3f75d981d58670ce7e0e3f5ead2bd3359cdd1f33b96da726c62013567a884639": "frp_0.51.0_linux_mipsle.tar.gz",
"5feca5a4d601ed393a3cc04d8bf3c41194ef56af155c326cf1e7fdfd130ef17a": "frp_0.51.0_freebsd_386.tar.gz",
"6c9628cb8382894dc0a928df8fcea9dad9cb763ff161e31f94f816443c7419e0": "frp_0.51.0_linux_arm.tar.gz",
"7174a1328325da89ed6aabcf522131db9928222154e9607b0d5a2f7b2977ae93": "frp_0.51.0_linux_amd64.tar.gz",
"7402fc76816fd653bbe050a3f8a2dfd7c1363c980e2cc3dc369c60c3f0d502a7": "frp_0.51.0_windows_arm64.zip",
"7ebff99259931e26c3baf8dd78c1af671d73a6c91a1d6ec9107c0c225df76bf0": "frp_0.51.0_freebsd_amd64.tar.gz",
"886ac7c8c0e01bddcb808947f76a5f904572e337fa4023cce4bad71a7ae9ca1c": "frp_0.51.0_linux_mips64le.tar.gz",
"9b3e4c64089c3b78ea1f666f11551e4ae6a435fc0797e39ab4fb07fd633b400c": "frp_0.51.0_linux_mips.tar.gz",
"b4a40bfaca19d5b8570be95ea2839fa82c7814c561510c3e3807ce273ee7c7cf": "frp_0.51.0_linux_mips64.tar.gz",
"b7f2414b1d8be99157e5b25ea578938520c45d094534fffb2e515796559b9b29": "frp_0.51.0_darwin_amd64.tar.gz",
"b8a22f70d3451a7f4b8e1718da28ef02dfb38d37193bcbdc1df39eb52d0da40b": "frp_0.51.0_darwin_arm64.tar.gz",
"ca7baeb243b5c264847067f6e5619311223f1741f73d5371ff7fa90698ff5a3b": "frp_0.51.0_linux_arm64.tar.gz",
"e377afeb481b30d9979fcbf636df6b5c4f9449b44f6c3d21a768aa5cb8767cb6": "frp_0.51.0_linux_386.tar.gz",
"1084631215170fc83b2de13f156a3b0e2ea02f2a0955fc94d3c6c5015391922c": "frp_0.50.0_linux_riscv64.tar.gz",
"1cda556f00b20f5b575ba40f83d8a007a8fa3308ef502c62fb7510989c3b7b10": "frp_0.50.0_freebsd_amd64.tar.gz",
"33893a93b57e6509132b4d6ae29f3e8a1f4c105c21746f0f0f036df0cf8d1979": "frp_0.50.0_linux_mipsle.tar.gz",
"4e2b06bd978472dd092c166b43ec56ab22c1347710fd77616283d2c27ee9ae56": "frp_0.50.0_freebsd_386.tar.gz",
"4f2088aff3460c9bd278121de7781985734969399d408f0c9e3f794165e0a407": "frp_0.50.0_linux_386.tar.gz",
"7bb651eec86e0126af3bd515235901a64b5490115defa10972e703c05bc65345": "frp_0.50.0_windows_amd64.zip",
"7fac327360b72613dec67583e4b939b65af0b88b676660821647b161ec2173fd": "frp_0.50.0_linux_mips64le.tar.gz",
"94e608af6d6f96619de403bf3aed4db8ab602999e0335380279e0d8aca1c6040": "frp_0.50.0_windows_386.zip",
"a5496a0364e4e071aa6a1cbcfd519e35ac8dcb4eac9a24e6a22340c4d4cf1914": "frp_0.50.0_linux_arm.tar.gz",
"b2768608b33e964fc7067657f385ba15a69762b0a875db47981953d70dd36af7": "frp_0.50.0_linux_mips.tar.gz",
"c57526a8a0010b811b9bd367704125033fc71774f6a66dcfd4224ec5478e0490": "frp_0.50.0_darwin_arm64.tar.gz",
"d33d83e8b98ce5413603f71b1c0b38c1b5bbe1d1c826b7ada84a7543a6cc6ea6": "frp_0.50.0_linux_arm64.tar.gz",
"db80349f17c39f502a631afda7cf5b95b2a85cdcafa92359b9f4d0375772c440": "frp_0.50.0_linux_mips64.tar.gz",
"e2047b43e87456568a505b84c45f52e0d2ed146896ec1e3fceb72e818200f11f": "frp_0.50.0_linux_amd64.tar.gz",
"ee8cdc63c2993ce8ab2bf918a56169a815254cd5f5a9a57567a904ec5dbf0145": "frp_0.50.0_windows_arm64.zip",
"f4cb27fb222cdd87a30674270614adfd0aa8350034a8bdbc50fc1967c0f0cb66": "frp_0.50.0_darwin_amd64.tar.gz",
"09329200234dd56722e095ee5b0b3d31bf8d39f3bdacb4a473b9144a7e8e8b7d": "frp_0.49.0_windows_arm64.zip",
"183ee0c672409cdd8b421f31e2b81753a4713bee962e1edf97f1455cda97173d": "frp_0.49.0_linux_amd64.tar.gz",
"1ca8187c73c3c75ace29675193659f9d6ddff3e5ddf2131f49f156844ca7d778": "frp_0.49.0_darwin_amd64.tar.gz",
"429aab2804d7431f684c6d409342af57381dbcafc4b37c49606063be2f92d4a3": "frp_0.49.0_linux_arm64.tar.gz",
"5b4204056ae94aa8281218656a1b3566eaaea2ddf4874eccb4a9c23cf9bc0fd0": "frp_0.49.0_linux_arm.tar.gz",
"76c7a4f5e35f32b726c48fdd32e292f63c7b374ba019a28dc44b04140f03e6de": "frp_0.49.0_darwin_arm64.tar.gz",
"7b6c9cf91ad9d00385d47139ffc69c0c9d72270886dbdb4f71f599efaec2cb64": "frp_0.49.0_freebsd_386.tar.gz",
"9033c6def481bde4bf7f2361966ae0ea92dfda5763a167460dcf0e231a2d02b8": "frp_0.49.0_linux_386.tar.gz",
"94ac6a42a165d913b79a0dcfb2d55a686e81b776697580e113aecd8815607076": "frp_0.49.0_freebsd_amd64.tar.gz",
"a343c8f23ba35c943e1c9311df17eb12f84c682d2ba0e965e244a49759b65f28": "frp_0.49.0_linux_mips64le.tar.gz",
"b117ea60954ad0c8d4e92eb60ca8e748806978506c377d59b4f5bc5295c4e3d1": "frp_0.49.0_linux_mips64.tar.gz",
"c44853992b0d6d3f9f5c777038590ee6a5869dbeb6362dfa5537e9d730aa26f6": "frp_0.49.0_windows_386.zip",
"d3a481b40889bf4c6fd35b18941de04ddaa2316ad51977a5af7bdddf3650f808": "frp_0.49.0_linux_mipsle.tar.gz",
"daf162e5cc90599aab036b7bb4ed6d4c521b2f5732a6cb40b08a00e6714deaa3": "frp_0.49.0_linux_riscv64.tar.gz",
"f39f10c0867a52eb9e4d2adf0bfa821993c950feca35437e84d274fba00bc595": "frp_0.49.0_linux_mips.tar.gz",
"fc5c5c5ff93300cea3141ff55fbccccb07cd0017d4e9cd4bcd324563f88f53fd": "frp_0.49.0_windows_amd64.zip",
"042fa197c0f91b27404c086eabfb62dad3ffaaad7101046f518abf58ae42ee1b": "frp_0.48.0_linux_mips64.tar.gz",
"0cd33dcfe9a38441eda2c60675f05ab3c3875b1e54608583d50d0835c567a30e": "frp_0.48.0_linux_arm.tar.gz",
"1e5b997597bacce1d971b83416c2f8c9cde0cbd294e6b11d91a3939f9c6356a9": "frp_0.48.0_darwin_arm64.tar.gz",
"2ab7b66c09391d9d76bd7a4818e85fb3818a10a46c91a804b982d7d4c9fddce3": "frp_0.48.0_linux_arm64.tar.gz",
"41c75d72848375144e46b9b9fe56168f365ce4bee56280757dada6c92bb8abc0": "frp_0.48.0_linux_riscv64.tar.gz",
"5de51fda0577a049945e42f386df70a8e9eb2769af96bb6b7471cb5072605be0": "frp_0.48.0_windows_386.zip",
"7a9fd341e0deb467ba0ab4913852adc965a0df2ba38e18ec80ab7ef61a9e99e8": "frp_0.48.0_linux_mips64le.tar.gz",
"7dba4f6e942502f0eca2ec37206671734eeb87c40a29f16b96ce14045da9e833": "frp_0.48.0_windows_arm64.zip",
"8ad8905b9296f3c26632f3bfc66302bc082b62295f6bbbb5b78e31d1e6649f26": "frp_0.48.0_windows_amd64.zip",
"a792cd515589050d475a28b714276a2960ed7ef8e0e5baeea3d38301a775fbb4": "frp_0.48.0_linux_mips.tar.gz",
"acd9f040fc6fb2a595f20bfb4faa66d9244615a0feaf9d2e4b03a994ca126a32": "frp_0.48.0_linux_386.tar.gz",
"d48623a74a00577be0409d912f8197a110f13192eab99d3959ceb11496ed0903": "frp_0.48.0_linux_mipsle.tar.gz",
"db53bdef3b270e45fb9efc489af2948be7c7fa1e3a5cae9698f2832e628bcd3b": "frp_0.48.0_linux_amd64.tar.gz",
"dd781cfd710345cca2df4d306245298efb61dc447d8004dd5542c1b2083e39a7": "frp_0.48.0_freebsd_amd64.tar.gz",
"e2dd4933cc48caba288be96ba5b226c7edb5be940c0452d9bc7faa28ab66847f": "frp_0.48.0_darwin_amd64.tar.gz",
"fdc0bca8460360346991a0f13e25233c87805bdc0f055f221f9c57c33b3b60fa": "frp_0.48.0_freebsd_386.tar.gz",
"00ffd863c32645660a29db758db4ea89f7c3eb616b3488cceca55345d8a5d11d": "frp_0.47.0_linux_arm.tar.gz",
"04d9eaf4997d1407feca0324beedaca577c63fa900ef04e6a97de9e8e2391e34": "frp_0.47.0_freebsd_386.tar.gz",
"125f87d334addd8ec7dacaf2a321a9f1c9a8b31c8a673d2d02808162cd67f997": "frp_0.47.0_linux_riscv64.tar.gz",
"3b9f8b80f13f20194490851b076186124b67b9a7845b32e5e035ae4aed2e45dc": "frp_0.47.0_linux_mips64.tar.gz",
"41a3a760ab0e04271f8bee1fd80011ce8e93a8455f78919864bcb13200f758f5": "frp_0.47.0_windows_amd64.zip",
"5b7c15f9e14042a99c38515ddfa694f188f59d72bde10ce341d86cbf7f801b19": "frp_0.47.0_windows_386.zip",
"71a0f3137f02da4116ea2b7d134c38be86a1229cffb0b1dac4469b561ea35985": "frp_0.47.0_linux_386.tar.gz",
"9299c297f6c75c6aa2bbbb5de27172e367328b6f5bbb6f8d1c4ca73c4c4af415": "frp_0.47.0_darwin_amd64.tar.gz",
"95583f7a979910ff4e65a5d9802df699063472a67a1f9e6d6fd6c2fcff448a14": "frp_0.47.0_linux_mips64le.tar.gz",
"95c0695cdf0cd8d399cabdccdff93b25aa7deb97e950bd3702bbbaf9a2baf87a": "frp_0.47.0_linux_mips.tar.gz",
"c53b188ec3eb09f34484d2576f957e61522875c0e7a99e67722d41b2b57cdb4d": "frp_0.47.0_linux_mipsle.tar.gz",
"cfc766cc82568e40d7198493340283cc0f4f42de97463aef863170f7e773ff9c": "frp_0.47.0_darwin_arm64.tar.gz",
"d7a7a6085fa6a9f8de0ae2c221c1ef110b9afc2a0122a058482ef3974d031ac0": "frp_0.47.0_linux_amd64.tar.gz",
"ee2d0d800b14ac26b8aeae4365df031e0186d23be150308735a0be753ec2d3f9": "frp_0.47.0_freebsd_amd64.tar.gz",
"f1dc0436b7f9f3f5c5d404cf5fb4a7319ff1cc22a06a687672020af620693f70": "frp_0.47.0_linux_arm64.tar.gz",
"0476f68f4552ae460d72f0b6c2c9fd4b6fb8dfdbafdec62695f02996d7221f81": "frp_0.46.1_darwin_arm64.tar.gz",
"200244a2c1bc9e186f875c23d0b78c9ab59a88052f4f4132e5c28a70fdc356b6": "frp_0.46.1_windows_amd64.zip",
"4af6b42eb79a5290d1e24e534a0ec34521dc2d30ef60898abd092ddb2e1cd55c": "frp_0.46.1_linux_riscv64.tar.gz",
"54e364bf382cc987a962fa5db328ce8bc375bff74ff7b8afcaeb1905a295e027": "frp_0.46.1_windows_386.zip",
"5f1660b704a8b580082b81e14a41d2da9ff1edeebc59b885acb92f1ab1f46838": "frp_0.46.1_linux_mips64le.tar.gz",
"76e5d42d4d2971de51de652417cfe38461ef9e18672e1070a1138910c8448a2f": "frp_0.46.1_linux_arm64.tar.gz",
"7c6208a3f7131802f24ad7bf7f02c760bba5c17443bdf328598d0758865f80df": "frp_0.46.1_linux_amd64.tar.gz",
"7d299b5695b0076b24e93928bad255f76c8352b5002fd459ef63c0199251abe9": "frp_0.46.1_linux_mips64.tar.gz",
"9704b24b5a58144293f7c7715b095b1ebf43b90e501050dfb9477094e6dca41b": "frp_0.46.1_linux_386.tar.gz",
"a47d75d634790109eaa5768d4e5cb504988e3754dcfe458072ef0b46d9aea419": "frp_0.46.1_freebsd_amd64.tar.gz",
"b330c29f6ef91302c6a2b9a0f6e86c77b498d0babb60fe182440f1b97e0554cb": "frp_0.46.1_linux_mipsle.tar.gz",
"bbb1ab095f30e9ecf1b745579f6ecff80eff11fb712f2bc364a656fbec89f73b": "frp_0.46.1_linux_mips.tar.gz",
"fc465df713f8c9d63c9380aa9da72b6ef639fb44917aed390d9c4d08c475a20d": "frp_0.46.1_linux_arm.tar.gz",
"fdde1a3e82d043cdca44b13c45e7593b61707385b30e919c38615d02d53e4b36": "frp_0.46.1_freebsd_386.tar.gz",
"ff71979ea17d481194beba325a55f5d2a319175ebc6a80df535a202a43614f24": "frp_0.46.1_darwin_amd64.tar.gz",
"0ac137ea9061aea6b6e8e5fc228b1082e14d3e29cafe6103f542ac4ffd728843": "frp_0.46.0_linux_mips64.tar.gz",
"1f1eefdf6a9ade3923edcd716c56941f2755848a4bd97167aaa1ceebfed95194": "frp_0.46.0_freebsd_386.tar.gz",
"275b254a20dfda754d6aba28d335a392df74150d6945d2da20a7c5718dc2c001": "frp_0.46.0_darwin_arm64.tar.gz",
"30199cd67bbed08c65f86c2420f0967491cad2ec791c97936666bc930d65e73e": "frp_0.46.0_linux_mipsle.tar.gz",
"3cd7ec9209b973520d47d784a09a368bfb9e2bb195f3c543ae5311720249e315": "frp_0.46.0_linux_arm.tar.gz",
"41f1014ee2ee7ed0a6e989deb937af9a8c01f4974fc1ef541583065475511d65": "frp_0.46.0_linux_riscv64.tar.gz",
"53242fd2bad1e6b3039fdef38df6219710864d1c9e639208a2106326921d15fd": "frp_0.46.0_darwin_amd64.tar.gz",
"62044b03a7bccb7e8f8f4f691f34838cd1160a643c0bb06ca8489e78d2d65897": "frp_0.46.0_linux_arm64.tar.gz",
"6681551b9bb7311625be8f3a269c183b600e13966787a8b11a8f9e8595a3d66b": "frp_0.46.0_windows_386.zip",
"754d66a918d3550c83e670a458f66954eec0521d6e76a20dd0a865992ad1b55e": "frp_0.46.0_linux_amd64.tar.gz",
"a77d3fa9419c5dc12ebd94eb5b97be3cff2c12b00dbe3884adc9ffcedf73909e": "frp_0.46.0_freebsd_amd64.tar.gz",
"b9c79acc881c58b0185465a5ded032d6210637f860712f04ecb800b66453d125": "frp_0.46.0_windows_amd64.zip",
"c14d5be9b9d80a48354c04dd1c3f80167abae94a1854d2f5116e4e5a0da89b91": "frp_0.46.0_linux_mips.tar.gz",
"c87ffc18bfa386cf946156f91fb8649a0cdbcd762550a0b8ab1f4774cb608455": "frp_0.46.0_linux_386.tar.gz",
"cac2bc6fccb071789d7acc95f02470cfb935cfc9c7c6a1e6d91457e4ff11e8e1": "frp_0.46.0_linux_mips64le.tar.gz",
"1a527c78ae25fa3e393d70fbfcea5b928ca96a689d8e82477f1b0db0cfc51e76": "frp_0.45.0_windows_386.zip",
"40d5025cb0b0a6f26cc79fd23fc78ccdfa050bd7e80d694f2039ab98093f831d": "frp_0.45.0_windows_amd64.zip",
"4c90633d523f467384a424bbfce211f737becbc7c4ac637e10e6c91fda8a6a26": "frp_0.45.0_linux_mips64.tar.gz",
"4faed559dc80bc2bf43b6c3da60e19f86c42ab8ed2b19e3ff0d3f4e4cca6c50c": "frp_0.45.0_linux_386.tar.gz",
"5eb942ba9ed0d45d2ac1ea6ed02fbff802a69c408c8eb68155dd2fb7c6fabb0e": "frp_0.45.0_linux_mips.tar.gz",
"63035108f37cc80d6043c1fcac50f8e856791a4fb8bcef0e792d97c88d8e35c5": "frp_0.45.0_darwin_arm64.tar.gz",
"8b2aee9d9eabc6078ae8a4c718030be85a13464becdb99f97f635e75425eb63e": "frp_0.45.0_freebsd_386.tar.gz",
"8ecf30ac7c14f85da20c1761c6418979282bff12db4d82ade2f4a1a8037bdf6e": "frp_0.45.0_linux_riscv64.tar.gz",
"97b4d3555734cba2af59b72b960ce10891b584dcf8d9e3db9f4f099c0a64131d": "frp_0.45.0_linux_mips64le.tar.gz",
"987f353f6ea282e259738eeb90c20b70fe20e1a49aca498b02acc47200c082bd": "frp_0.45.0_linux_arm.tar.gz",
"a660a94c158cb280974447efd174d3525d806ac7235f6546abeb1a57660a1125": "frp_0.45.0_darwin_amd64.tar.gz",
"b9a1a2387b9b07ec6be9d28e5ed9639c1ea29d41a84bc3a62b39ab476459b1ff": "frp_0.45.0_linux_amd64.tar.gz",
"e2a6179880b852366edc395685fa0e82eec542e9c8a2c3483d30d5740941a0e0": "frp_0.45.0_linux_mipsle.tar.gz",
"e57919a0e3a63705ef452bb2a6bc440f7a6273a8205ed9ce2ccfd063ea9b2215": "frp_0.45.0_linux_arm64.tar.gz",
"f9c6ad68a9e3903d1689cd85e84f00aa892a9e98b368a9f062599da9d2cb4967": "frp_0.45.0_freebsd_amd64.tar.gz",
"0fd011fb817fa36fe8735e3d97df523970d9be4f56f0848840f737b63ba37fbf": "frp_0.44.0_freebsd_amd64.tar.gz",
"23705712274935b9b223412bf731ecd672dcc8b5d0c11a39372aacedaa6a66a4": "frp_0.44.0_windows_amd64.zip",
"3262dee2fa68eb8d9428d209b2e87c2293d007529898850874b19707088c416e": "frp_0.44.0_darwin_arm64.tar.gz",
"3c4e769a29f03bcc9e998adcd1281142abfb5ff1dd66da5a435830a1cff34217": "frp_0.44.0_darwin_amd64.tar.gz",
"3cc79f9fc44300aed80988b31845328b428c0999572eb7f1df949eccee0f518e": "frp_0.44.0_linux_mipsle.tar.gz",
"45f65dafd172f3a5e05eabf3d4efbb954c92a88851a027f79c19f61a10b78287": "frp_0.44.0_linux_amd64.tar.gz",
"4aed98c21ef4534951b6faeab4982376695ae1e10ca90aedd27a9bfcf6caea2e": "frp_0.44.0_windows_386.zip",
"5f3f60a71fa040a36be5de818e6f95c48e8a2ba368b700a079b593f0e281dbd8": "frp_0.44.0_linux_mips.tar.gz",
"5f7c9ad77e37a5921450c013b9792dac4ea5ef5d3114ea9276585f62e2318a79": "frp_0.44.0_freebsd_386.tar.gz",
"60ee29ebb3683135c815b4e9b6681c92a445ac3f40e9302a70b65fca68ff5116": "frp_0.44.0_linux_mips64le.tar.gz",
"7c55322bb55e4085ab950711f0c3406a25f95573f618ed347e8f542ecf93cb78": "frp_0.44.0_linux_mips64.tar.gz",
"ad151125bd46fb8abf11f2a4347c7c85e102bb0e6128c69962c8d6bf9a71fca6": "frp_0.44.0_linux_arm.tar.gz",
"ce18273ca20bd38c567b0355ca2c85575651b39249294969daa51e568077a872": "frp_0.44.0_linux_386.tar.gz",
"f5acd6dd3812f30ed6a2a2a864231563a962d4ff09c64d21be106db6f8806af8": "frp_0.44.0_linux_arm64.tar.gz",
"00c526bdfae8fe448b1810c1c06b2827efa1158b7e324aa69c23a57a8b29f603": "frp_0.43.0_linux_arm64.tar.gz",
"0d05e3ebd2490c026e1b8f6780d901eedde65562af02acf3bf80d729a2aae52b": "frp_0.43.0_windows_amd64.zip",
"1fe64b366408022e4d61c1e37f64e268f7e72f4d351425df36c35fb1cfc534fd": "frp_0.43.0_linux_mips64.tar.gz",
"3c582f611716c77db5e4f69823fc72572006608f63d9859dea598f0dfc74ed0b": "frp_0.43.0_darwin_amd64.tar.gz",
"4eecced7aa167279bda23afe2be0f3dd9b61080531fdbae5137bd257c334992a": "frp_0.43.0_linux_386.tar.gz",
"618b1a0d2bfebc9bc3e59b4c39e67082a445e5aeaaaa0fec9eded436dd64a2d4": "frp_0.43.0_darwin_arm64.tar.gz",
"6a3e20b001ab57b066a52394ba2d992ae6d93b22260b0969307966fad6214692": "frp_0.43.0_linux_mips.tar.gz",
"6bef9db4560b6c7da2def271f7bc5bf6988fafa3e654f8a2bfb589fd7d79b2db": "frp_0.43.0_linux_mips64le.tar.gz",
"7c1416256f7f3637e0dfed99988d08282ae0866784f1eecd53a3639e1a942867": "frp_0.43.0_freebsd_amd64.tar.gz",
"801a1ea2bf02b9ff657c34708918397bec61408bed216f6ed45889973ee09a01": "frp_0.43.0_linux_arm.tar.gz",
"98ab35f179091726b739c9fbb6643cc7328076bfbddd09732bb68b1cdf1b7435": "frp_0.43.0_freebsd_386.tar.gz",
"bb8734f2be2907a2923aedf43757d6ff85a7c66af789b8dbef34ddaf2194f05f": "frp_0.43.0_windows_386.zip",
"c14ccd69607c34707120e7c2d2df9b6c0a11c7f40e22f116d75838e2038edba3": "frp_0.43.0_linux_mipsle.tar.gz",
"d458d70dd88048d1fc898d5422ed570e912d3f3ef3ee5928871438a08514f725": "frp_0.43.0_linux_amd64.tar.gz",
"19ca9f2b318ea2efbe9f2b213c2edd68de54c7ed35dc3f291146c67374d8c57d": "frp_0.42.0_windows_amd64.zip",
"35386af9e43ed1948faa7037050573eda3299d4a11061734fce5f4be51c56dd3": "frp_0.42.0_windows_386.zip",
"4ef082c1788e972f016f00286a2054c82189cec3a1a3e2af8123240c2888b6ff": "frp_0.42.0_linux_386.tar.gz",
"5c4828f6e89b6f2479b671d3e7644b34b6968a6017cac402144c844b48dcc621": "frp_0.42.0_linux_amd64.tar.gz",
"7946d13b2498410bf9fb0cc32fee7ea44bde8be438eb1b1bc67c440a3671589d": "frp_0.42.0_freebsd_amd64.tar.gz",
"7ff954d3f9f0d655be5f250ca50e8b065ddb8b4d3a1da0a55f740cc03301c6f5": "frp_0.42.0_darwin_amd64.tar.gz",
"b53e3cba1a8a3ebaa1e7d04f647eee3aed3417740692e346dc460c813403475c": "frp_0.42.0_linux_mipsle.tar.gz",
"bf980fa58499e947581c6b89b100d55c1d417fdda6f7544422a4a6400248e20d": "frp_0.42.0_freebsd_386.tar.gz",
"c6f00c7458e7546b9339ce65805b2969abf55f95698f0b2f0904ed85f187b3fa": "frp_0.42.0_linux_mips64le.tar.gz",
"c842849be22802e6500167fc34fac869c584ad1f70b6c56dcc66d7391171d567": "frp_0.42.0_linux_arm.tar.gz",
"de3397d1084686a5ab9f82fae2aa65f417cef7d7c2cc12f7eb9da51c0a404de6": "frp_0.42.0_linux_mips.tar.gz",
"de6262f886175411573c98fe2d5838449b4fc2472a07748964159a468ed0ccdf": "frp_0.42.0_darwin_arm64.tar.gz",
"eb8ea449f14a20480c77d6501f8b682516fa4a9394dd15d2a49b6a957aa862a9": "frp_0.42.0_linux_mips64.tar.gz",
"f8b9c30d3cef82aebdf5dfce8ba7d6a4943a4b51ef64223b59c5241e3023d8e5": "frp_0.42.0_linux_arm64.tar.gz"
}

View File

@ -21,25 +21,24 @@ import {
import { initLoggerApi } from "../api/logger"; import { initLoggerApi } from "../api/logger";
import { initFileApi } from "../api/file"; import { initFileApi } from "../api/file";
import { getConfig } from "../storage/config"; import { getConfig } from "../storage/config";
import log from "electron-log";
import { initCommonApi } from "../api/common"; import { initCommonApi } from "../api/common";
import { initLocalApi } from "../api/local"; import { initLocalApi } from "../api/local";
// The built directory structure import { initLog, logError, logInfo, LogModule } from "../utils/log";
// import { maskSensitiveData } from "../utils/desensitize";
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.DIST_ELECTRON = join(__dirname, ".."); process.env.DIST_ELECTRON = join(__dirname, "..");
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public") ? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST; : process.env.DIST;
let win: BrowserWindow | null = null;
let tray = null;
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
let isQuiting;
// Disable GPU Acceleration for Windows 7 // Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration(); if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -51,28 +50,11 @@ if (!app.requestSingleInstanceLock()) {
process.exit(0); process.exit(0);
} }
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
let win: BrowserWindow | null = null;
let tray = null;
// Here, you can also use other preload
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
let isQuiting;
log.transports.file.level = "debug";
log.transports.console.level = "debug";
async function createWindow(config: FrpConfig) { async function createWindow(config: FrpConfig) {
console.log("config", config);
let show = true; let show = true;
if (config) { if (config) {
show = !config.systemSilentStartup; show = !config.systemSilentStartup;
} }
console.log("界面", show);
win = new BrowserWindow({ win = new BrowserWindow({
title: "Frpc Desktop", title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"), icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
@ -138,11 +120,6 @@ async function createWindow(config: FrpConfig) {
} }
export const createTray = (config: FrpConfig) => { export const createTray = (config: FrpConfig) => {
log.info(
`当前环境 platform${process.platform} arch${
process.arch
} appData${app.getPath("userData")} version:${app.getVersion()}`
);
let menu: Array<MenuItemConstructorOptions | MenuItem> = [ let menu: Array<MenuItemConstructorOptions | MenuItem> = [
{ {
label: "显示主窗口", label: "显示主窗口",
@ -175,42 +152,100 @@ export const createTray = (config: FrpConfig) => {
win.show(); win.show();
}); });
logInfo(LogModule.APP, `Tray created successfully.`);
};
app.whenReady().then(() => {
initLog();
logInfo(
LogModule.APP,
`Application started. Current system architecture: ${
process.arch
}, platform: ${process.platform}, version: ${app.getVersion()}.`
);
getConfig((err, config) => {
if (err) {
logError(LogModule.APP, `Failed to get config: ${err.message}`);
return;
}
createWindow(config)
.then(r => {
logInfo(LogModule.APP, `Window created successfully.`);
createTray(config);
if (config) { if (config) {
logInfo(
LogModule.APP,
`Config retrieved: ${JSON.stringify(
maskSensitiveData(config, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
if (config.systemStartupConnect) { if (config.systemStartupConnect) {
log.info(`已开启自动连接 正在自动连接服务器`);
startFrpWorkerProcess(config); startFrpWorkerProcess(config);
} }
} }
}; // Initialize APIs
try {
initGitHubApi(win);
logInfo(LogModule.APP, `GitHub API initialized.`);
app.whenReady().then(() => {
getConfig((err, config) => {
createWindow(config).then(r => {
createTray(config);
// 初始化各个API
initGitHubApi();
initConfigApi(win); initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
initProxyApi(); initProxyApi();
logInfo(LogModule.APP, `Proxy API initialized.`);
initFrpcApi(); initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
initLoggerApi(); initLoggerApi();
logInfo(LogModule.APP, `Logger API initialized.`);
initFileApi(); initFileApi();
logInfo(LogModule.APP, `File API initialized.`);
initCommonApi(); initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`);
initLocalApi(); initLocalApi();
logInfo(LogModule.APP, `Local API initialized.`);
// initUpdaterApi(win); // initUpdaterApi(win);
logInfo(LogModule.APP, `Updater API initialization skipped.`);
} catch (error) {
logError(
LogModule.APP,
`Error during API initialization: ${error.message}`
);
}
})
.catch(error => {
logError(LogModule.APP, `Error creating window: ${error.message}`);
}); });
}); });
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
logInfo(LogModule.APP, `All windows closed.`);
win = null; win = null;
if (process.platform !== "darwin") { if (process.platform !== "darwin") {
stopFrpcProcess(() => { stopFrpcProcess(() => {
logInfo(LogModule.APP, `FRPC process stopped. Quitting application.`);
app.quit(); app.quit();
}); });
} }
}); });
app.on("second-instance", () => { app.on("second-instance", () => {
logInfo(LogModule.APP, `Second instance detected.`);
if (win) { if (win) {
// Focus on the main window if the user tried to open another // Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore(); if (win.isMinimized()) win.restore();
@ -219,23 +254,35 @@ app.on("second-instance", () => {
}); });
app.on("activate", () => { app.on("activate", () => {
logInfo(LogModule.APP, `Application activated.`);
const allWindows = BrowserWindow.getAllWindows(); const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) { if (allWindows.length) {
allWindows[0].focus(); allWindows[0].focus();
} else { } else {
getConfig((err, config) => { getConfig((err, config) => {
createWindow(config).then(r => {}); if (err) {
logError(
LogModule.APP,
`Failed to get config on activate: ${err.message}`
);
return;
}
createWindow(config).then(r => {
logInfo(LogModule.APP, `Window created on activate.`);
});
}); });
} }
}); });
app.on("before-quit", () => { app.on("before-quit", () => {
log.info("before-quit"); logInfo(LogModule.APP, `Application is about to quit.`);
stopFrpcProcess(() => {
isQuiting = true; isQuiting = true;
});
}); });
// New window example arg: new windows url
ipcMain.handle("open-win", (_, arg) => { ipcMain.handle("open-win", (_, arg) => {
logInfo(LogModule.APP, `Opening new window with argument: ${arg}`);
const childWindow = new BrowserWindow({ const childWindow = new BrowserWindow({
webPreferences: { webPreferences: {
preload, preload,
@ -246,7 +293,12 @@ ipcMain.handle("open-win", (_, arg) => {
if (process.env.VITE_DEV_SERVER_URL) { if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`); childWindow.loadURL(`${url}#${arg}`);
logInfo(LogModule.APP, `Child window loaded URL: ${url}#${arg}`);
} else { } else {
childWindow.loadFile(indexHtml, { hash: arg }); childWindow.loadFile(indexHtml, { hash: arg });
logInfo(
LogModule.APP,
`Child window loaded file: ${indexHtml} with hash: ${arg}`
);
} }
}); });

View File

@ -2,13 +2,13 @@ import Datastore from "nedb";
import path from "path"; import path from "path";
import { app } from "electron"; import { app } from "electron";
const log = require("electron-log"); import { logInfo, logError, LogModule, logDebug } from "../utils/log";
import { maskSensitiveData } from "../utils/desensitize";
const configDB = new Datastore({ const configDB = new Datastore({
autoload: true, autoload: true,
filename: path.join(app.getPath("userData"), "config.db") filename: path.join(app.getPath("userData"), "config.db")
}); });
/** /**
* *
*/ */
@ -17,8 +17,37 @@ export const saveConfig = (
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => { ) => {
document["_id"] = "1"; document["_id"] = "1";
log.debug(`保存日志 ${JSON.stringify(document)}`); logDebug(
configDB.update({ _id: "1" }, document, { upsert: true }, cb); LogModule.DB,
`Saving configuration to the database. ${JSON.stringify(
maskSensitiveData(document, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
configDB.update(
{ _id: "1" },
document,
{ upsert: true },
(err, numberOfUpdated, upsert) => {
if (err) {
logError(
LogModule.DB,
`Error saving configuration: ${err.message}`
);
} else {
logInfo(
LogModule.DB,
`Configuration saved successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
); // 添加成功日志
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
}; };
/** /**
@ -28,9 +57,34 @@ export const saveConfig = (
export const getConfig = ( export const getConfig = (
cb: (err: Error | null, document: FrpConfig) => void cb: (err: Error | null, document: FrpConfig) => void
) => { ) => {
configDB.findOne({ _id: "1" }, cb); logInfo(LogModule.DB, "Retrieving configuration from the database."); // 添加信息日志
configDB.findOne({ _id: "1" }, (err, document) => {
if (err) {
logError(
LogModule.DB,
`Error retrieving configuration: ${err.message}`
); // 添加错误日志
} else {
logInfo(LogModule.DB, "Configuration retrieved successfully."); // 添加成功日志
}
cb(err, document);
});
}; };
export const clearConfig = (cb?: (err: Error | null, n: number) => void) => { export const clearConfig = (cb?: (err: Error | null, n: number) => void) => {
configDB.remove({}, { multi: true }, cb); logInfo(LogModule.DB, "Clearing all configurations from the database."); // 添加信息日志
configDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(
LogModule.DB,
`Error clearing configurations: ${err.message}`
); // 添加错误日志
} else {
logInfo(
LogModule.DB,
`Successfully cleared configurations. Number of documents removed: ${n}`
); // 添加成功日志
}
if (cb) cb(err, n);
});
}; };

View File

@ -2,90 +2,142 @@ import Datastore from "nedb";
import path from "path"; import path from "path";
import { app } from "electron"; import { app } from "electron";
const log = require("electron-log"); import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const proxyDB = new Datastore({ const proxyDB = new Datastore({
autoload: true, autoload: true,
filename: path.join(app.getPath("userData"), "proxy.db") filename: path.join(app.getPath("userData"), "proxy.db")
}); });
/**
*
* @param proxy
* @param cb
*/
export const insertProxy = ( export const insertProxy = (
proxy: Proxy, proxy: Proxy,
cb?: (err: Error | null, document: Proxy) => void cb?: (err: Error | null, document: Proxy) => void
) => { ) => {
log.debug(`新增代理:${JSON.stringify(proxy)}`); logInfo(LogModule.DB, `Inserting proxy: ${JSON.stringify(proxy)}`);
proxyDB.insert(proxy, cb); proxyDB.insert(proxy, (err, document) => {
if (err) {
logError(LogModule.DB, `Error inserting proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy inserted successfully: ${JSON.stringify(document)}`
);
}
if (cb) cb(err, document);
});
}; };
/**
*
* @param _id
* @param cb
*/
export const deleteProxyById = ( export const deleteProxyById = (
_id: string, _id: string,
cb?: (err: Error | null, n: number) => void cb?: (err: Error | null, n: number) => void
) => { ) => {
log.debug(`删除代理:${_id}`); logInfo(LogModule.DB, `Deleting proxy with ID: ${_id}`);
proxyDB.remove({ _id: _id }, cb); proxyDB.remove({ _id: _id }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error deleting proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy deleted successfully. Number of documents removed: ${n}`
);
}
if (cb) cb(err, n);
});
}; };
/**
*
*/
export const updateProxyById = ( export const updateProxyById = (
proxy: Proxy, proxy: Proxy,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => { ) => {
log.debug(`修改代理:${proxy}`); logInfo(LogModule.DB, `Updating proxy: ${JSON.stringify(proxy)}`);
proxyDB.update({ _id: proxy._id }, proxy, {}, cb); proxyDB.update(
{ _id: proxy._id },
proxy,
{},
(err, numberOfUpdated, upsert) => {
if (err) {
logError(LogModule.DB, `Error updating proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy updated successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
);
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
}; };
/**
*
* @param cb
*/
export const listProxy = ( export const listProxy = (
callback: (err: Error | null, documents: Proxy[]) => void callback: (err: Error | null, documents: Proxy[]) => void
) => { ) => {
proxyDB.find({}, callback); logInfo(LogModule.DB, `Listing all proxies`);
proxyDB.find({}, (err, documents) => {
if (err) {
logError(LogModule.DB, `Error listing proxies: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxies listed successfully. Count: ${documents.length}`
);
}
callback(err, documents);
});
}; };
/**
* id查询
* @param id
* @param callback
*/
export const getProxyById = ( export const getProxyById = (
id: string, id: string,
callback: (err: Error | null, document: Proxy) => void callback: (err: Error | null, document: Proxy) => void
) => { ) => {
proxyDB.findOne({ _id: id }, callback); logInfo(LogModule.DB, `Getting proxy by ID: ${id}`);
proxyDB.findOne({ _id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error getting proxy by ID: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy retrieved successfully: ${JSON.stringify(document)}`
);
}
callback(err, document);
});
}; };
/**
*
* @param cb
*/
export const clearProxy = (cb?: (err: Error | null, n: number) => void) => { export const clearProxy = (cb?: (err: Error | null, n: number) => void) => {
proxyDB.remove({}, { multi: true }, cb); logInfo(LogModule.DB, `Clearing all proxies`);
proxyDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error clearing proxies: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxies cleared successfully. Number of documents removed: ${n}`
);
}
if (cb) cb(err, n);
});
}; };
/**
*
* @param id id
* @param st
* @param cb
*/
export const updateProxyStatus = ( export const updateProxyStatus = (
id: string, id: string,
st: boolean, st: boolean,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => { ) => {
proxyDB.update({ _id: id }, { $set: { status: st } }, {}, cb); logInfo(LogModule.DB, `Updating proxy status for ID: ${id} to ${st}`);
proxyDB.update(
{ _id: id },
{ $set: { status: st } },
{},
(err, numberOfUpdated, upsert) => {
if (err) {
logError(LogModule.DB, `Error updating proxy status: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy status updated successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
);
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
}; };

View File

@ -2,7 +2,7 @@ import Datastore from "nedb";
import path from "path"; import path from "path";
import { app } from "electron"; import { app } from "electron";
const log = require("electron-log"); import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const versionDB = new Datastore({ const versionDB = new Datastore({
autoload: true, autoload: true,
@ -10,7 +10,7 @@ const versionDB = new Datastore({
}); });
/** /**
* * Insert version
* @param version * @param version
* @param cb * @param cb
*/ */
@ -18,35 +18,73 @@ export const insertVersion = (
version: FrpVersion, version: FrpVersion,
cb?: (err: Error | null, document: any) => void cb?: (err: Error | null, document: any) => void
) => { ) => {
log.debug(`新增版本:${JSON.stringify(version)}`); logInfo(LogModule.DB, `Inserting version: ${JSON.stringify(version)}`);
versionDB.insert(version, cb); versionDB.insert(version, (err, document) => {
if (err) {
logError(LogModule.DB, `Error inserting version: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version inserted successfully: ${JSON.stringify(document)}`);
}
if (cb) cb(err, document);
});
}; };
/** /**
* * List versions
* @param cb * @param cb
*/ */
export const listVersion = ( export const listVersion = (
callback: (err: Error | null, documents: FrpVersion[]) => void callback: (err: Error | null, documents: FrpVersion[]) => void
) => { ) => {
versionDB.find({}, callback); logInfo(LogModule.DB, "Listing all versions.");
versionDB.find({}, (err, documents) => {
if (err) {
logError(LogModule.DB, `Error listing versions: ${err.message}`);
} else {
logInfo(LogModule.DB, `Successfully listed versions: ${documents.length} found.`);
}
callback(err, documents);
});
}; };
export const getVersionById = ( export const getVersionById = (
id: number, id: number,
callback: (err: Error | null, document: FrpVersion) => void callback: (err: Error | null, document: FrpVersion) => void
) => { ) => {
versionDB.findOne({ id: id }, callback); logInfo(LogModule.DB, `Retrieving version by ID: ${id}`);
versionDB.findOne({ id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error retrieving version by ID: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version retrieved successfully: ${JSON.stringify(document)}`);
}
callback(err, document);
});
}; };
export const deleteVersionById = ( export const deleteVersionById = (
id: string, id: string,
callback: (err: Error | null, document: any) => void callback: (err: Error | null, document: any) => void
) => { ) => {
log.debug(`删除版本:${id}`); logInfo(LogModule.DB, `Deleting version: ${id}`);
versionDB.remove({ id: id }, callback); versionDB.remove({ id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error deleting version: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version deleted successfully: ${id}`);
}
callback(err, document);
});
}; };
export const clearVersion = (cb?: (err: Error | null, n: number) => void) => { export const clearVersion = (cb?: (err: Error | null, n: number) => void) => {
versionDB.remove({}, { multi: true }, cb); logInfo(LogModule.DB, "Clearing all versions from the database.");
versionDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error clearing versions: ${err.message}`);
} else {
logInfo(LogModule.DB, `Successfully cleared versions. Number of documents removed: ${n}`);
}
if (cb) cb(err, n);
});
}; };

View File

@ -0,0 +1,12 @@
export const maskSensitiveData = (
obj: Record<string, any>,
keysToMask: string[]
) => {
const maskedObj = JSON.parse(JSON.stringify(obj));
keysToMask.forEach(key => {
if (maskedObj.hasOwnProperty(key)) {
maskedObj[key] = "***";
}
});
return maskedObj;
};

19
electron/utils/file.ts Normal file
View File

@ -0,0 +1,19 @@
import { createHash } from "crypto";
export const formatBytes = (bytes: number, decimals: number = 2): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024; // 1 KB = 1024 Bytes
const dm = decimals < 0 ? 0 : decimals; // Ensure decimal places are not less than 0
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // Calculate unit index
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // Return formatted string
};
export const calculateFileChecksum = (filePath: string): string => {
const fs = require("fs");
const fileBuffer = fs.readFileSync(filePath);
const hash = createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
};

30
electron/utils/log.ts Normal file
View File

@ -0,0 +1,30 @@
import log from "electron-log";
// 定义模块枚举
export enum LogModule {
APP = "app",
FRP_CLIENT = "frpc client",
GITHUB = "github",
DB = "db"
}
export const initLog = () => {
log.transports.file.level = "debug";
log.transports.console.level = "debug";
};
// 自定义日志输出函数,记录到指定业务模块
export const logInfo = (module: LogModule, message: string) => {
log.info(`[${module}] ${message}`);
};
export const logError = (module: LogModule, message: string) => {
log.error(`[${module}] ${message}`);
};
export const logDebug = (module: LogModule, message: string) => {
log.debug(`[${module}] ${message}`);
};
export const logWarn = (module: LogModule, message: string) => {
log.warn(`[${module}] ${message}`);
};

View File

@ -1,12 +1,13 @@
{ {
"name": "Frpc-Desktop", "name": "Frpc-Desktop",
"version": "1.1.4", "version": "1.1.6",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "FRP跨平台桌面客户端可视化配置轻松实现内网穿透", "description": "FRP跨平台桌面客户端可视化配置轻松实现内网穿透",
"repository": "github:luckjiawei/frpc-desktop", "repository": "github:luckjiawei/frpc-desktop",
"author": "刘嘉伟 <8473136@qq.com>", "author": "刘嘉伟 <8473136@qq.com>",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"type": "commonjs",
"keywords": [ "keywords": [
"frp", "frp",
"frpc", "frpc",
@ -29,6 +30,7 @@
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"build:electron": "npm run build && electron-builder --mac --win --linux", "build:electron": "npm run build && electron-builder --mac --win --linux",
"build:electron:mac": "npm run build && electron-builder --mac", "build:electron:mac": "npm run build && electron-builder --mac",
"build:electron:mac:arm": "npm run build && electron-builder --mac --arm64",
"build:electron:win": "npm run build && electron-builder --win", "build:electron:win": "npm run build && electron-builder --win",
"build:electron:linux": "npm run build && electron-builder --linux", "build:electron:linux": "npm run build && electron-builder --linux",
"release": "npm run build && electron-builder --mac --win --linux -p always", "release": "npm run build && electron-builder --mac --win --linux -p always",
@ -68,14 +70,14 @@
"vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4", "vue-router": "^4.2.4",
"vue-tsc": "^2.0.22", "vue-tsc": "2.0.22",
"vue-types": "^5.1.1" "vue-types": "^5.1.1"
}, },
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"electron-dl": "^3.5.1", "electron-dl": "3.5.1",
"electron-log": "^5.1.7", "electron-log": "^5.1.7",
"intro.js": "^8.0.0-beta.1", "intro.js": "^8.0.0-beta.1",
"isbinaryfile": "4.0.10", "isbinaryfile": "4.0.10",

View File

@ -35,6 +35,9 @@ import attachMoneyRounded from "@iconify-icons/material-symbols/attach-money-rou
import volunteerActivismSharp from "@iconify-icons/material-symbols/volunteer-activism-sharp"; import volunteerActivismSharp from "@iconify-icons/material-symbols/volunteer-activism-sharp";
import description from "@iconify-icons/material-symbols/description"; import description from "@iconify-icons/material-symbols/description";
import folderRounded from "@iconify-icons/material-symbols/folder-rounded"; import folderRounded from "@iconify-icons/material-symbols/folder-rounded";
import link from "@iconify-icons/material-symbols/link";
import unarchive from "@iconify-icons/material-symbols/unarchive";
import fileSaveRounded from "@iconify-icons/material-symbols/file-save-rounded";
addIcon("cloud", Cloud); addIcon("cloud", Cloud);
addIcon("rocket-launch-rounded", RocketLaunchRounded); addIcon("rocket-launch-rounded", RocketLaunchRounded);
@ -70,4 +73,7 @@ addIcon("attach-money-rounded", attachMoneyRounded);
addIcon("volunteer-activism-sharp", volunteerActivismSharp); addIcon("volunteer-activism-sharp", volunteerActivismSharp);
addIcon("description", description); addIcon("description", description);
addIcon("folder-rounded", folderRounded); addIcon("folder-rounded", folderRounded);
addIcon("link", link);
addIcon("unarchive", unarchive);
addIcon("file-save-rounded", fileSaveRounded);

View File

@ -1,8 +1,8 @@
@import "reset"; @use "reset";
@import "layout"; @use "layout";
@import "element"; @use "element";
@import "tailwind"; @use "tailwind";
@import "scrollbar"; @use "scrollbar";
/* 自定义全局 CssVar */ /* 自定义全局 CssVar */
:root { :root {
--pure-transition-duration: 0.016s; --pure-transition-duration: 0.016s;

View File

@ -47,7 +47,13 @@ const defaultFormData = ref<FrpConfig>({
transportHeartbeatInterval: 30, transportHeartbeatInterval: 30,
transportHeartbeatTimeout: 90, transportHeartbeatTimeout: 90,
webEnable: true, webEnable: true,
webPort: 57400 webPort: 57400,
transportProtocol: "tcp",
transportDialServerTimeout: 10,
transportDialServerKeepalive: 7200,
transportPoolCount: 0,
transportTcpMux: true,
transportTcpMuxKeepaliveInterval: 30
}); });
const formData = ref<FrpConfig>(defaultFormData.value); const formData = ref<FrpConfig>(defaultFormData.value);
@ -121,6 +127,24 @@ const rules = reactive<FormRules>({
], ],
webPort: [ webPort: [
{ required: true, message: "web界面端口不能为空", trigger: "change" } { required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportProtocol: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportDialServerTimeout: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportDialServerKeepalive: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportPoolCount: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportTcpMux: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
],
transportTcpMuxKeepaliveInterval: [
{ required: true, message: "web界面端口不能为空", trigger: "change" }
] ]
}); });
@ -196,9 +220,49 @@ onMounted(() => {
defaultFormData.value.transportHeartbeatTimeout; defaultFormData.value.transportHeartbeatTimeout;
} }
if (data.webEnable == null || data.webEnable == undefined) { if (data.webEnable == null || data.webEnable == undefined) {
data.webEnable = true; data.webEnable = defaultFormData.value.webEnable;
data.webPort = 57400; data.webPort = defaultFormData.value.webPort;
} }
if (
data.transportProtocol === undefined ||
data.transportProtocol == null
) {
data.transportProtocol = defaultFormData.value.transportProtocol;
}
if (
data.transportDialServerTimeout === undefined ||
data.transportDialServerTimeout == null
) {
data.transportDialServerTimeout =
defaultFormData.value.transportDialServerTimeout;
}
if (
data.transportDialServerKeepalive === undefined ||
data.transportDialServerKeepalive == null
) {
data.transportDialServerKeepalive =
defaultFormData.value.transportDialServerKeepalive;
}
if (
data.transportPoolCount === undefined ||
data.transportPoolCount == null
) {
data.transportPoolCount = defaultFormData.value.transportPoolCount;
}
if (
data.transportTcpMux === undefined ||
data.transportTcpMux == null
) {
data.transportTcpMux = defaultFormData.value.transportTcpMux;
}
if (
data.transportTcpMuxKeepaliveInterval === undefined ||
data.transportTcpMuxKeepaliveInterval == null
) {
data.transportTcpMuxKeepaliveInterval =
defaultFormData.value.transportTcpMuxKeepaliveInterval;
}
formData.value = data; formData.value = data;
} }
} }
@ -406,10 +470,10 @@ onUnmounted(() => {
<IconifyIconOffline icon="deviceReset" /> <IconifyIconOffline icon="deviceReset" />
</el-button> </el-button>
<el-button plain type="primary" @click="handleImportConfig"> <el-button plain type="primary" @click="handleImportConfig">
<IconifyIconOffline icon="uploadRounded" /> <IconifyIconOffline icon="file-open-rounded" />
</el-button> </el-button>
<el-button plain type="primary" @click="handleShowExportDialog"> <el-button plain type="primary" @click="handleShowExportDialog">
<IconifyIconOffline icon="downloadRounded" /> <IconifyIconOffline icon="file-save-rounded" />
</el-button> </el-button>
<el-button type="primary" @click="handleSubmit"> <el-button type="primary" @click="handleSubmit">
<IconifyIconOffline icon="save-rounded" /> <IconifyIconOffline icon="save-rounded" />
@ -422,7 +486,7 @@ onUnmounted(() => {
:rules="rules" :rules="rules"
label-position="right" label-position="right"
ref="formRef" ref="formRef"
label-width="130" label-width="150"
> >
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="24"> <el-col :span="24">
@ -632,6 +696,72 @@ onUnmounted(() => {
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- <el-col :span="24">
<div class="h2">TLS Config</div>
</el-col> -->
<el-col :span="24">
<div class="h2">传输配置</div>
</el-col>
<el-col :span="12">
<el-form-item label="传输协议:" prop="transportProtocol">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
frps 之间的通信协议默认为 tcp<br />
对应参数<span class="font-black text-[#5A3DAA]"
>transport.protocol</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
传输协议
</template>
<el-select v-model="formData.transportProtocol">
<el-option label="tcp" value="tcp" />
<el-option label="kcp" value="kcp" />
<el-option label="quic" value="quic" />
<el-option label="websocket" value="websocket" />
<el-option label="wss" value="wss" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="连接池大小:" prop="transportPoolCount">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>transport.poolCount</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
连接池大小
</template>
<el-input-number
class="w-full"
v-model="formData.transportPoolCount"
controls-position="right"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item <el-form-item
label="心跳间隔:" label="心跳间隔:"
@ -715,9 +845,176 @@ onUnmounted(() => {
<!-- </el-input>--> <!-- </el-input>-->
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="12">
<div class="h2">TLS Config</div> <el-form-item
label="连接超时:"
prop="transportDialServerTimeout"
>
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
与服务器建立连接的最长等待时间默认值为10秒单位
<span class="font-black text-[#5A3DAA]"></span> <br />
对应参数<span class="font-black text-[#5A3DAA]"
>transport.dialServerTimeout</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
连接超时
</template>
<el-input-number
class="w-full"
v-model="formData.transportDialServerTimeout"
controls-position="right"
></el-input-number>
</el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item
label="保活探测间隔:"
prop="transportDialServerKeepalive"
>
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
客户端与服务端之间的连接在一定时间内没有任何数据传输系统会定期发送一些心跳数据包来保持连接的活跃状态如果为负则禁用保活探测
单位
<span class="font-black text-[#5A3DAA]"></span> <br />
对应参数<span class="font-black text-[#5A3DAA]"
>transport.dialServerKeepalive</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
保活探测间隔
</template>
<el-input-number
class="w-full"
v-model="formData.transportDialServerKeepalive"
controls-position="right"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="多路复用:" prop="transportTcpMux">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
TCP 多路复用默认启用<br />
对应参数<span class="font-black text-[#5A3DAA]"
>transport.tcpMux</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
TCP 多路复用
</template>
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="formData.transportTcpMux"
/>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formData.transportTcpMux">
<el-form-item
label="多复心跳间隔:"
prop="transportTcpMuxKeepaliveInterval"
>
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
多路复用的保活间隔默认值为 30 单位
<span class="font-black text-[#5A3DAA]"></span> <br />
对应参数<span class="font-black text-[#5A3DAA]"
>transport.tcpMuxKeepaliveInterval</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
多复心跳间隔
</template>
<el-input-number
class="w-full"
v-model="formData.transportTcpMuxKeepaliveInterval"
controls-position="right"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="启用代理:" prop="proxyConfigEnable">
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="formData.proxyConfigEnable"
/>
</el-form-item>
</el-col>
<template v-if="formData.proxyConfigEnable">
<el-col :span="24">
<el-form-item label="代理地址:" prop="proxyConfigProxyUrl">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>transport.proxyURL</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
代理地址
</template>
<el-input
v-model="formData.proxyConfigProxyUrl"
placeholder="http://user:pwd@192.168.1.128:8080"
/>
</el-form-item>
</el-col>
</template>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="启用TLS" prop="tlsConfigEnable"> <el-form-item label="启用TLS" prop="tlsConfigEnable">
<el-switch <el-switch
@ -904,48 +1201,6 @@ onUnmounted(() => {
</el-form-item> </el-form-item>
</el-col> </el-col>
</template> </template>
<el-col :span="24">
<div class="h2">代理</div>
</el-col>
<el-col :span="24">
<el-form-item label="启用代理:" prop="proxyConfigEnable">
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="formData.proxyConfigEnable"
/>
</el-form-item>
</el-col>
<template v-if="formData.proxyConfigEnable">
<el-col :span="24">
<el-form-item label="代理地址:" prop="proxyConfigProxyUrl">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>transport.proxyURL</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
代理地址
</template>
<el-input
v-model="formData.proxyConfigProxyUrl"
placeholder="http://user:pwd@192.168.1.128:8080"
/>
</el-form-item>
</el-col>
</template>
<el-col :span="24"> <el-col :span="24">
<div class="h2">Web 界面</div> <div class="h2">Web 界面</div>
@ -963,7 +1218,8 @@ onUnmounted(() => {
icon="info" icon="info"
/> />
</template> </template>
热更新等功能依赖于web界面<span class="font-black text-[#5A3DAA]" 热更新等功能依赖于web界面<span
class="font-black text-[#5A3DAA]"
>不可停用Web</span >不可停用Web</span
> >
</el-popover> </el-popover>
@ -989,7 +1245,7 @@ onUnmounted(() => {
<template #default> <template #default>
对应参数<span class="font-black text-[#5A3DAA]" 对应参数<span class="font-black text-[#5A3DAA]"
>webServer.port</span >webServer.port</span
><br/> ><br />
自行保证端口没有被占用否则会导致启动失败 自行保证端口没有被占用否则会导致启动失败
</template> </template>
<template #reference> <template #reference>
@ -1009,6 +1265,7 @@ onUnmounted(() => {
:min="0" :min="0"
:max="65535" :max="65535"
controls-position="right" controls-position="right"
class="w-full"
></el-input-number> ></el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>

View File

@ -20,10 +20,6 @@ const mirrors = ref<Array<GitHubMirror>>([
{ {
id: "github", id: "github",
name: "github" name: "github"
},
{
id: "ghproxy",
name: "ghproxy"
} }
]); ]);
@ -31,7 +27,7 @@ const mirrors = ref<Array<GitHubMirror>>([
* 获取版本 * 获取版本
*/ */
const handleLoadVersions = () => { const handleLoadVersions = () => {
ipcRenderer.send("github.getFrpVersions"); ipcRenderer.send("github.getFrpVersions", currMirror.value);
}; };
/** /**
@ -106,6 +102,23 @@ const handleInitDownloadHook = () => {
handleLoadVersions(); handleLoadVersions();
} }
}); });
ipcRenderer.on("Download.importFrpFile.hook", (event, args) => {
const { success, data } = args;
console.log(args);
// if (err) {
loading.value++;
ElMessage({
type: success ? "success" : "error",
message: data
});
handleLoadVersions();
// }
});
};
const handleMirrorChange = () => {
handleLoadVersions();
}; };
onMounted(() => { onMounted(() => {
@ -116,19 +129,30 @@ onMounted(() => {
// }); // });
}); });
const handleImportFrp = () => {
ipcRenderer.send("download.importFrpFile");
};
onUnmounted(() => { onUnmounted(() => {
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnProgress"); ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnProgress");
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnCompleted"); ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnCompleted");
ipcRenderer.removeAllListeners("Download.frpVersionHook"); ipcRenderer.removeAllListeners("Download.frpVersionHook");
ipcRenderer.removeAllListeners("Download.deleteVersion.hook"); ipcRenderer.removeAllListeners("Download.deleteVersion.hook");
ipcRenderer.removeAllListeners("Download.importFrpFile.hook");
}); });
</script> </script>
<template> <template>
<div class="main"> <div class="main">
<!-- <breadcrumb> -->
<breadcrumb> <breadcrumb>
<div class="h-full flex items-center justify-center"> <div class="flex">
<div class="h-full flex items-center justify-center mr-4">
<span class="text-sm font-bold">下载源 </span> <span class="text-sm font-bold">下载源 </span>
<el-select class="w-40" v-model="currMirror"> <el-select
class="w-40"
v-model="currMirror"
@change="handleMirrorChange"
>
<el-option <el-option
v-for="m in mirrors" v-for="m in mirrors"
:label="m.name" :label="m.name"
@ -137,6 +161,11 @@ onUnmounted(() => {
/> />
</el-select> </el-select>
</div> </div>
<el-button class="mr-2" type="primary" @click="handleImportFrp">
<IconifyIconOffline icon="unarchive" />
</el-button>
</div>
<!-- <div--> <!-- <div-->
<!-- class="cursor-pointer h-[36px] w-[36px] bg-[#5f3bb0] rounded text-white flex justify-center items-center"--> <!-- class="cursor-pointer h-[36px] w-[36px] bg-[#5f3bb0] rounded text-white flex justify-center items-center"-->
<!-- @click="handleOpenInsert"--> <!-- @click="handleOpenInsert"-->

View File

@ -63,7 +63,11 @@ const defaultForm = ref<Proxy>({
httpUser: "", httpUser: "",
httpPassword: "", httpPassword: "",
fallbackTo: "", fallbackTo: "",
fallbackTimeoutMs: 500 fallbackTimeoutMs: 500,
https2http: false,
https2httpCaFile: "",
https2httpKeyFile: "",
keepTunnelOpen: false
}); });
/** /**
@ -450,6 +454,9 @@ const allowCopyAccessAddress = (proxy: Proxy) => {
if (proxy.type === "xtcp" && proxy.stcpModel === "visited") { if (proxy.type === "xtcp" && proxy.stcpModel === "visited") {
return false; return false;
} }
if (proxy.type === "sudp" && proxy.stcpModel === "visited") {
return false;
}
return true; return true;
}; };
@ -558,6 +565,25 @@ const handleRandomProxyName = () => {
`df_${editForm.value.type}_${result}`.toLocaleLowerCase(); `df_${editForm.value.type}_${result}`.toLocaleLowerCase();
}; };
import path from "path";
function normalizePath(filePath: string) {
return path.normalize(filePath).replace(/\\/g, "/");
}
const handleSelectFile = (type: number, ext: string[]) => {
ipcRenderer.invoke("file.selectFile", ext).then(r => {
switch (type) {
case 1:
editForm.value.https2httpCaFile = normalizePath(r[0]);
break;
case 2:
editForm.value.https2httpKeyFile = normalizePath(r[0]);
break;
}
console.log(r);
});
};
onMounted(() => { onMounted(() => {
handleInitHook(); handleInitHook();
handleLoadProxys(); handleLoadProxys();
@ -659,7 +685,7 @@ onUnmounted(() => {
class="text-xl text-[#ADADAD] hover:text-[#5A3DAA]" class="text-xl text-[#ADADAD] hover:text-[#5A3DAA]"
@click="handleCopyAccessAddress(proxy)" @click="handleCopyAccessAddress(proxy)"
> >
<IconifyIconOffline icon="content-copy" /> <IconifyIconOffline icon="link" />
</a> </a>
<el-dropdown size="small"> <el-dropdown size="small">
<a <a
@ -709,7 +735,9 @@ onUnmounted(() => {
<div <div
class="text-sm text-left" class="text-sm text-left"
v-if=" v-if="
(proxy.type !== 'stcp' && proxy.type !== 'xtcp') || (proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors' proxy.stcpModel !== 'visitors'
" "
> >
@ -724,7 +752,9 @@ onUnmounted(() => {
<div <div
class="text-sm text-center" class="text-sm text-center"
v-if=" v-if="
(proxy.type !== 'stcp' && proxy.type !== 'xtcp') || (proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors' proxy.stcpModel !== 'visitors'
" "
> >
@ -735,7 +765,9 @@ onUnmounted(() => {
<div <div
class="text-sm text-center" class="text-sm text-center"
v-if=" v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') && (proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors' proxy.stcpModel === 'visitors'
" "
> >
@ -746,7 +778,9 @@ onUnmounted(() => {
<div <div
class="text-sm text-center" class="text-sm text-center"
v-if=" v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') && (proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors' proxy.stcpModel === 'visitors'
" "
> >
@ -757,7 +791,9 @@ onUnmounted(() => {
<div <div
class="text-sm text-center" class="text-sm text-center"
v-if=" v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') && (proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors' proxy.stcpModel === 'visitors'
" "
> >
@ -1086,6 +1122,136 @@ onUnmounted(() => {
</el-form-item> </el-form-item>
</el-col> </el-col>
</template> </template>
<template v-if="isHttps">
<el-col :span="24">
<el-form-item
label="https2http"
prop="https2http"
:rules="[
{
required: true,
trigger: 'blur'
}
]"
>
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="editForm.https2http"
/>
</el-form-item>
</el-col>
<el-col :span="24" v-if="editForm.https2http">
<el-form-item
label="证书文件:"
prop="https2httpCaFile"
label-width="180"
:rules="[
{
required: true,
message: '证书文件不能为空',
trigger: 'blur'
}
]"
>
<!-- <template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="310" placement="top" trigger="hover">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>transport.tls.trustedCaFile</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
CA 证书文件
</template> -->
<el-input
class="button-input"
v-model="editForm.https2httpCaFile"
placeholder="点击选择证书文件"
readonly
@click="handleSelectFile(1, ['crt', 'pem'])"
/>
<!-- <el-button-->
<!-- class="ml-2"-->
<!-- type="primary"-->
<!-- @click="handleSelectFile(3, ['crt'])"-->
<!-- >选择-->
<!-- </el-button>-->
<el-button
v-if="editForm.https2httpCaFile"
class="ml-2"
type="danger"
@click="editForm.https2httpCaFile = ''"
>清除
</el-button>
</el-form-item>
</el-col>
<el-col :span="24" v-if="editForm.https2http">
<el-form-item
label="密钥文件:"
prop="https2httpKeyFile"
label-width="180"
:rules="[
{
required: true,
message: '密钥文件不能为空',
trigger: 'blur'
}
]"
>
<!-- <template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="310" placement="top" trigger="hover">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>transport.tls.trustedCaFile</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
CA 证书文件
</template> -->
<el-input
class="button-input cursor-pointer"
v-model="editForm.https2httpKeyFile"
placeholder="点击选择密钥文件"
readonly
@click="handleSelectFile(2, ['key'])"
/>
<!-- <el-button-->
<!-- class="ml-2"-->
<!-- type="primary"-->
<!-- @click="handleSelectFile(3, ['crt'])"-->
<!-- >选择-->
<!-- </el-button>-->
<el-button
v-if="editForm.https2httpKeyFile"
class="ml-2"
type="danger"
@click="editForm.https2httpKeyFile = ''"
>清除
</el-button>
</el-form-item>
</el-col>
</template>
<template v-if="isStcpVisitors"> <template v-if="isStcpVisitors">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="被访问者代理名称:" prop="serverName"> <el-form-item label="被访问者代理名称:" prop="serverName">
@ -1260,6 +1426,50 @@ onUnmounted(() => {
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item
label="保持隧道开启:"
prop="keepTunnelOpen"
:rules="[
{
required: true,
message: '保持隧道开启不能为空',
trigger: 'blur'
}
]"
>
<template #label>
<div class="inline-block">
<div class="flex items-center">
<div class="mr-1">
<el-popover placement="top" trigger="hover" width="300">
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>keepTunnelOpen</span
>
开启后即使没有流量通过会保持隧道(即连接)打开
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
保持隧道开启
</div>
</div>
</template>
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="editForm.keepTunnelOpen"
/>
</el-form-item>
</el-col>
</template> </template>
<el-col :span="24"> <el-col :span="24">
<el-form-item> <el-form-item>
@ -1377,4 +1587,8 @@ onUnmounted(() => {
:deep(.el-drawer__body) { :deep(.el-drawer__body) {
//padding-top: 0; //padding-top: 0;
} }
.button-input {
width: calc(100% - 68px);
}
</style> </style>

10
types/global.d.ts vendored
View File

@ -33,6 +33,10 @@ declare global {
httpPassword: string; httpPassword: string;
fallbackTo: string; fallbackTo: string;
fallbackTimeoutMs: number; fallbackTimeoutMs: number;
https2http: boolean;
https2httpCaFile: string;
https2httpKeyFile: string;
keepTunnelOpen: boolean;
}; };
/** /**
@ -85,6 +89,12 @@ declare global {
transportHeartbeatTimeout: number; transportHeartbeatTimeout: number;
webEnable: boolean; webEnable: boolean;
webPort: number; webPort: number;
transportProtocol: string;
transportDialServerTimeout: number;
transportDialServerKeepalive: number;
transportPoolCount: number;
transportTcpMux: boolean;
transportTcpMuxKeepaliveInterval: number;
}; };
type GitHubMirror = { type GitHubMirror = {