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><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>
## TODO
@ -38,6 +41,7 @@
- [x] 一键清空所有配置
- [x] 支持导入识别frpc.toml
- [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-10-14: 发布v1.1.3版本 支持xtcp协议、优化体验
- 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,33 +100,42 @@
## 捐赠
**捐赠方式**
一块两块不嫌少,十块二十那更好。
👉👉👉[点击去捐赠](https://jwinks.com/donate/)👈👈👈
**捐赠名单**
| 🕰 时间 | 📡 平台 | 🤲 捐赠者 | 💰 金额 | ✉️ 捐赠留言 |
|------------|-------|-----------|-------|--------------------------|
| 2024-08-06 | 微信 | 三木 | 1 元 | 无 |
| 2024-08-25 | 微信 | 晚风 | 1 元 | 无 |
| 2024-08-27 | 微信 | x | 1 元 | 无 |
| 2024-10-09 | 微信 | 解脱 | 20 元 | 无 |
| 2024-10-09 | 微信 | KMDN | 20 元 | 无 |
| 2024-10-14 | 微信 | 121 | 5 元 | 无 |
| 2024-10-14 | 微信 | Different | 10 元 | 感谢您的开源 |
| 2024-10-16 | 微信 | 。 。 。 | 50 元 | 感谢开源的frp软件 |
| 2024-11-2 | 微信 | gesoft | 10 元 | 加油 |
| 2024-11-7 | 微信 | *进 | 10 元 | 谢谢,可见可得,省心省力 |
| 2024-11-8 | 微信 | **创 | 10 元 | 无 |
| 2024-11-20 | 微信 | 一東 | 20 元 | 请你喝咖啡 |
| 2024-11-20 | 微信 | KEVINSKH | 10 元 | 感谢开发方便快捷的图形化操作界面👍 |
| 2024-11-26 | 微信 | | 3 元 | 无 |
| 2024-11-26 | 微信 | Kaori | 1 元 | 谢谢大佬的项目要是能添加web控制页面就更好了 |
| 2024-12-03 | 微信 | 17¥ | 20 元 | 谢谢,很方便的软件 |
| 2024-12-03 | 微信 | Cr@k3r | 5 元 | 感谢你的工作 |
| 🕰 时间 | 📡 平台 | 🤲 捐赠者 | 💰 金额 | ✉️ 捐赠留言 |
|------------|-------|---------------|---------|--------------------------|
| 2024-08-06 | 微信 | 三木 | 1 元 | 无 |
| 2024-08-25 | 微信 | 晚风 | 1 元 | 无 |
| 2024-08-27 | 微信 | x | 1 元 | 无 |
| 2024-10-09 | 微信 | 解脱 | 20 元 | 无 |
| 2024-10-09 | 微信 | KMDN | 20 元 | 无 |
| 2024-10-14 | 微信 | 121 | 5 元 | 无 |
| 2024-10-14 | 微信 | Different | 10 元 | 感谢您的开源 |
| 2024-10-16 | 微信 | 。 。 。 | 50 元 | 感谢开源的frp软件 |
| 2024-11-2 | 微信 | gesoft | 10 元 | 加油 |
| 2024-11-7 | 微信 | *进 | 10 元 | 谢谢,可见可得,省心省力 |
| 2024-11-8 | 微信 | **创 | 10 元 | 无 |
| 2024-11-20 | 微信 | 一東 | 20 元 | 请你喝咖啡 |
| 2024-11-20 | 微信 | KEVINSKH | 10 元 | 感谢开发方便快捷的图形化操作界面👍 |
| 2024-11-26 | 微信 | | 3 元 | 无 |
| 2024-11-26 | 微信 | Kaori | 1 元 | 谢谢大佬的项目要是能添加web控制页面就更好了 |
| 2024-12-03 | 微信 | 17¥ | 20 元 | 谢谢,很方便的软件 |
| 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 log from "electron-log";
import { logError, logInfo, LogModule, logWarn } from "../utils/log";
export const initCommonApi = () => {
// 打开链接
ipcMain.on("common.openUrl", async (event, args) => {
if (args) {
log.info(`打开链接:${args}`);
shell.openExternal(args).then(() => {});
logInfo(LogModule.APP, `Attempting to open URL: ${args}`);
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", () => {
logInfo(LogModule.APP, "Application is relaunching.");
app.relaunch();
app.quit();
});

View File

@ -1,268 +1,337 @@
import {app, dialog, ipcMain, shell} from "electron";
import {clearConfig, getConfig, saveConfig} from "../storage/config";
import {clearVersion, listVersion} from "../storage/version";
import {genIniConfig, genTomlConfig, stopFrpcProcess} from "./frpc";
import {clearProxy, insertProxy, listProxy} from "../storage/proxy";
import { app, dialog, ipcMain, shell } from "electron";
import { clearConfig, getConfig, saveConfig } from "../storage/config";
import { clearVersion, listVersion } from "../storage/version";
import { genIniConfig, genTomlConfig, stopFrpcProcess } from "./frpc";
import { clearProxy, insertProxy, listProxy } from "../storage/proxy";
import path from "path";
import fs from "fs";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const log = require("electron-log");
const toml = require("@iarna/toml");
const {v4: uuidv4} = require("uuid");
const { v4: uuidv4 } = require("uuid");
export const initConfigApi = win => {
ipcMain.on("config.saveConfig", async (event, args) => {
saveConfig(args, (err, numberOfUpdated, upsert) => {
if (!err) {
const start = args.systemSelfStart || false;
log.info("开启自启状态", start);
app.setLoginItemSettings({
openAtLogin: start, //win
openAsHidden: start //macOs
});
ipcMain.on("config.saveConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to save configuration.");
saveConfig(args, (err, numberOfUpdated, upsert) => {
if (!err) {
const start = args.systemSelfStart || false;
logDebug(LogModule.APP, "Startup status set to: " + start);
app.setLoginItemSettings({
openAtLogin: start, //win
openAsHidden: start //macOs
});
logInfo(LogModule.APP, "Configuration saved successfully.");
} else {
logError(LogModule.APP, `Error saving configuration: ${err}`);
}
event.reply("Config.saveConfig.hook", {
err: err,
numberOfUpdated: numberOfUpdated,
upsert: upsert
});
});
});
ipcMain.on("config.getConfig", async (event, args) => {
logInfo(LogModule.APP, "Requesting configuration.");
getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving configuration: ${err}`);
}
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.versions", event => {
logInfo(LogModule.APP, "Requesting version information.");
listVersion((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving version information: ${err}`);
}
event.reply("Config.versions.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.hasConfig", event => {
logInfo(LogModule.APP, "Checking if configuration exists.");
getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error checking configuration: ${err}`);
}
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.exportConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to export configuration.");
const result = await dialog.showOpenDialog({
properties: ["openDirectory"]
});
const outputDirectory = result.filePaths[0];
if (!outputDirectory) {
logWarn(LogModule.APP, "Export canceled by user.");
return;
}
logInfo(
LogModule.APP,
`Exporting configuration to directory ${outputDirectory} with type: ${args}`
);
getConfig((err1, config) => {
if (!err1 && config) {
listProxy((err2, proxys) => {
if (!err2) {
let configContent = "";
if (args === "ini") {
configContent = genIniConfig(config, proxys);
} else if (args === "toml") {
configContent = genTomlConfig(config, proxys);
}
event.reply("Config.saveConfig.hook", {
err: err,
numberOfUpdated: numberOfUpdated,
upsert: upsert
});
});
});
ipcMain.on("config.getConfig", async (event, args) => {
getConfig((err, doc) => {
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.versions", event => {
listVersion((err, doc) => {
event.reply("Config.versions.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.hasConfig", event => {
getConfig((err, doc) => {
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.exportConfig", async (event, args) => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory"]
});
const outputDirectory = result.filePaths[0];
if (!outputDirectory) {
// 取消了
return;
}
log.info(`导出目录 ${outputDirectory} 类型:${args}`);
getConfig((err1, config) => {
if (!err1 && config) {
listProxy((err2, proxys) => {
if (!err2) {
let configContent = "";
if (args === "ini") {
configContent = genIniConfig(config, proxys);
} else if (args === "toml") {
configContent = genTomlConfig(config, proxys);
}
const configPath = path.join(
outputDirectory,
`frpc-desktop.${args}`
);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{flag: "w"},
err => {
if (!err) {
// callback(filename);
event.reply("config.exportConfig.hook", {
data: "导出错误",
err: err
});
}
}
);
event.reply("Config.exportConfig.hook", {
data: {
configPath: configPath
}
});
const configPath = path.join(
outputDirectory,
`frpc-desktop.${args}`
);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{ flag: "w" },
err => {
if (err) {
logError(
LogModule.APP,
`Error writing configuration file: ${err}`
);
event.reply("config.exportConfig.hook", {
data: "导出错误",
err: err
});
} else {
logInfo(
LogModule.APP,
"Configuration exported successfully."
);
event.reply("Config.exportConfig.hook", {
data: {
configPath: configPath
}
});
}
});
}
}
);
} else {
logError(LogModule.APP, `Error listing proxies: ${err2}`);
}
});
} else {
logError(LogModule.APP, `Error retrieving configuration: ${err1}`);
}
});
});
const parseTomlConfig = (tomlPath: string) => {
const importConfigPath = tomlPath;
const tomlData = fs.readFileSync(importConfigPath, "utf-8");
log.info(`读取到配置内容 ${tomlData}`);
const sourceConfig = toml.parse(tomlData);
// log.info(`解析结果 ${sourceConfig}`);
// console.log(sourceConfig, "frpcConfig");
// 解析config
const targetConfig: FrpConfig = {
currentVersion: null,
serverAddr: sourceConfig.serverAddr || "",
serverPort: sourceConfig.serverPort || "",
authMethod: sourceConfig?.user
? "multiuser"
: sourceConfig?.auth?.method || "",
authToken: sourceConfig?.auth?.token || "",
transportHeartbeatInterval:
sourceConfig?.transport?.heartbeatInterval || 30,
transportHeartbeatTimeout:
sourceConfig?.transport?.heartbeatTimeout || 90,
tlsConfigEnable: sourceConfig?.transport?.tls?.enable || false,
tlsConfigCertFile: sourceConfig?.transport?.tls?.certFile || "",
tlsConfigKeyFile: sourceConfig?.transport?.tls?.keyFile || "",
tlsConfigServerName: sourceConfig?.transport?.tls?.serverName || "",
tlsConfigTrustedCaFile: sourceConfig?.transport?.tls?.trustedCaFile || "",
logLevel: sourceConfig?.log?.level || "info",
logMaxDays: sourceConfig?.log?.maxDays || 3,
proxyConfigProxyUrl: sourceConfig?.transport?.proxyURL || "",
proxyConfigEnable: Boolean(sourceConfig?.transport?.proxyURL) || false,
user: sourceConfig?.user || "",
metaToken: sourceConfig?.metadatas?.token || "",
systemSelfStart: false,
systemStartupConnect: false,
systemSilentStartup: false,
webEnable: true,
webPort: sourceConfig?.webServer?.port || 57400
};
let frpcProxys = [];
// 解析proxy
if (sourceConfig?.proxies && sourceConfig.proxies.length > 0) {
const frpcProxys1 = sourceConfig.proxies.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: m?.localIP || "",
localPort: m?.localPort || null,
remotePort: m?.remotePort || null,
customDomains: m?.customDomains || [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visited",
serverName: "",
secretKey: m?.secretKey || "",
bindAddr: "",
bindPort: null,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys1];
}
// 解析stcp的访问者
if (sourceConfig?.visitors && sourceConfig.visitors.length > 0) {
const frpcProxys2 = sourceConfig.visitors.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: "",
localPort: null,
remotePort: null,
customDomains: [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visitors",
serverName: m?.serverName,
secretKey: m?.secretKey || "",
bindAddr: m?.bindAddr,
bindPort: m?.bindPort,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys2];
}
if (targetConfig) {
clearConfig(() => {
saveConfig(targetConfig);
});
}
if (frpcProxys && frpcProxys.length > 0) {
clearProxy(() => {
frpcProxys.forEach(f => {
insertProxy(f, err => {
console.log("插入", f, err);
});
});
});
}
const parseTomlConfig = (tomlPath: string) => {
logInfo(LogModule.APP, `Parsing TOML configuration from ${tomlPath}`);
const importConfigPath = tomlPath;
const tomlData = fs.readFileSync(importConfigPath, "utf-8");
logInfo(LogModule.APP, "Configuration content read successfully.");
const sourceConfig = toml.parse(tomlData);
// 解析config
const targetConfig: FrpConfig = {
currentVersion: null,
serverAddr: sourceConfig.serverAddr || "",
serverPort: sourceConfig.serverPort || "",
authMethod: sourceConfig?.user
? "multiuser"
: sourceConfig?.auth?.method || "",
authToken: sourceConfig?.auth?.token || "",
transportHeartbeatInterval:
sourceConfig?.transport?.heartbeatInterval || 30,
transportHeartbeatTimeout:
sourceConfig?.transport?.heartbeatTimeout || 90,
tlsConfigEnable: sourceConfig?.transport?.tls?.enable || false,
tlsConfigCertFile: sourceConfig?.transport?.tls?.certFile || "",
tlsConfigKeyFile: sourceConfig?.transport?.tls?.keyFile || "",
tlsConfigServerName: sourceConfig?.transport?.tls?.serverName || "",
tlsConfigTrustedCaFile: sourceConfig?.transport?.tls?.trustedCaFile || "",
logLevel: sourceConfig?.log?.level || "info",
logMaxDays: sourceConfig?.log?.maxDays || 3,
proxyConfigProxyUrl: sourceConfig?.transport?.proxyURL || "",
proxyConfigEnable: Boolean(sourceConfig?.transport?.proxyURL) || false,
user: sourceConfig?.user || "",
metaToken: sourceConfig?.metadatas?.token || "",
systemSelfStart: false,
systemStartupConnect: false,
systemSilentStartup: false,
webEnable: true,
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
};
ipcMain.on("config.importConfig", async (event, args) => {
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{name: "FrpcConfig Files", extensions: ["toml", "ini"]} // 允许选择的文件类型
]
});
if (result.canceled) {
return;
} else {
const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath); // 获取文件后缀名
log.info(`导入文件 ${filePath} ${fileExtension}`);
if (fileExtension === ".toml") {
parseTomlConfig(filePath);
event.reply("Config.importConfig.hook", {
success: true
});
let frpcProxys = [];
// 解析proxy
if (sourceConfig?.proxies && sourceConfig.proxies.length > 0) {
const frpcProxys1 = sourceConfig.proxies.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: m?.localIP || "",
localPort: m?.localPort || null,
remotePort: m?.remotePort || null,
customDomains: m?.customDomains || [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visited",
serverName: "",
secretKey: m?.secretKey || "",
bindAddr: "",
bindPort: null,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys1];
logInfo(LogModule.APP, "Parsed proxies from configuration.");
}
// 解析stcp的访问者
if (sourceConfig?.visitors && sourceConfig.visitors.length > 0) {
const frpcProxys2 = sourceConfig.visitors.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: "",
localPort: null,
remotePort: null,
customDomains: [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visitors",
serverName: m?.serverName,
secretKey: m?.secretKey || "",
bindAddr: m?.bindAddr,
bindPort: m?.bindPort,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys2];
logInfo(LogModule.APP, "Parsed visitors from configuration.");
}
if (targetConfig) {
clearConfig(() => {
logInfo(LogModule.APP, "Clearing existing configuration.");
saveConfig(targetConfig);
logInfo(LogModule.APP, "New configuration saved.");
});
}
if (frpcProxys && frpcProxys.length > 0) {
clearProxy(() => {
frpcProxys.forEach(f => {
insertProxy(f, err => {
if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err}`);
} else {
event.reply("Config.importConfig.hook", {
success: false,
data: `导入失败,暂不支持 ${fileExtension} 格式文件`
});
logInfo(LogModule.APP, `Inserted proxy: ${JSON.stringify(f)}`);
}
}
});
ipcMain.on("config.clearAll", async (event, args) => {
stopFrpcProcess(() => {
clearConfig();
clearProxy();
clearVersion();
event.reply("Config.clearAll.hook", {});
});
});
});
});
}
};
ipcMain.on("config.openDataFolder", async (event, args) => {
const userDataPath = app.getPath("userData");
shell.openPath(userDataPath).then((errorMessage) => {
if (errorMessage) {
console.error('Failed to open Logger:', errorMessage);
event.reply("Config.openDataFolder.hook", false);
} else {
console.log('Logger opened successfully');
event.reply("Config.openDataFolder.hook", true);
}
});
ipcMain.on("config.importConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to import configuration.");
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "FrpcConfig Files", extensions: ["toml", "ini"] } // 允许选择的文件类型
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
return;
} else {
const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath); // 获取文件后缀名
logWarn(
LogModule.APP,
`Importing file ${filePath} with extension ${fileExtension}`
);
if (fileExtension === ".toml") {
parseTomlConfig(filePath);
event.reply("Config.importConfig.hook", {
success: true
});
} else {
logError(
LogModule.APP,
`Import failed, unsupported file format: ${fileExtension}`
);
event.reply("Config.importConfig.hook", {
success: false,
data: `导入失败,暂不支持 ${fileExtension} 格式文件`
});
}
}
});
ipcMain.on("config.clearAll", async (event, args) => {
logInfo(LogModule.APP, "Clearing all configurations.");
stopFrpcProcess(() => {
clearConfig();
clearProxy();
clearVersion();
event.reply("Config.clearAll.hook", {});
logInfo(LogModule.APP, "All configurations cleared.");
});
});
ipcMain.on("config.openDataFolder", async (event, args) => {
const userDataPath = app.getPath("userData");
logInfo(LogModule.APP, "Attempting to open data folder.");
shell.openPath(userDataPath).then(errorMessage => {
if (errorMessage) {
logError(LogModule.APP, `Failed to open data folder: ${errorMessage}`);
event.reply("Config.openDataFolder.hook", false);
} else {
logInfo(LogModule.APP, "Data folder opened successfully.");
event.reply("Config.openDataFolder.hook", true);
}
});
});
};

View File

@ -1,13 +1,21 @@
import {dialog, ipcMain} from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
export const initFileApi = () => {
ipcMain.handle("file.selectFile", async (event, args) => {
const result = dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{name: 'Text Files', extensions: args},
]
})
return result;
logInfo(LogModule.APP, `Attempting to open file dialog with filters: ${JSON.stringify(args)}`);
try {
const result = dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: args },
]
});
logInfo(LogModule.APP, `File dialog result: ${JSON.stringify(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 { getVersionById } from "../storage/version";
import treeKill from "tree-kill";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
const fs = require("fs");
const path = require("path");
const { exec, spawn } = require("child_process");
const log = require("electron-log");
export let frpcProcess = null;
const runningCmd = {
commandPath: null,
@ -15,11 +16,6 @@ const runningCmd = {
};
let frpcStatusListener = null;
/**
*
* @param versionId ID
* @param callback
*/
const getFrpcVersionWorkerPath = (
versionId: number,
callback: (workerPath: string) => void
@ -49,170 +45,179 @@ const isRangePort = (m: Proxy) => {
export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => {
const rangePort = isRangePort(m);
let toml = `
${
rangePort
? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}`
: ""
}
config.tlsConfigKeyFile = config.tlsConfigKeyFile.replace(/\\/g, "\\\\");
config.tlsConfigCertFile = config.tlsConfigCertFile.replace(/\\/g, "\\\\");
config.tlsConfigTrustedCaFile = config.tlsConfigTrustedCaFile.replace(
/\\/g,
"\\\\"
);
let toml = `${
rangePort
? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}`
: ""
}
[[${
(m.type === "stcp" || m.type === "xtcp" || m.type === "sudp") &&
m.stcpModel === "visitors"
? "visitors"
: "proxies"
}]]
${rangePort ? "" : `name = "${m.name}"\n`}
type = "${m.type}"
`;
${rangePort ? "" : `name = "${m.name}"`}
type = "${m.type}"\n`;
switch (m.type) {
case "tcp":
case "udp":
if (rangePort) {
toml += `
name = "${m.name}-{{ $v.First }}"
toml += `name = "${m.name}-{{ $v.First }}"
localPort = {{ $v.First }}
remotePort = {{ $v.Second }}
`;
remotePort = {{ $v.Second }}\n`;
} else {
toml += `
localIP = "${m.localIp}"
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}
remotePort = ${m.remotePort}
`;
remotePort = ${m.remotePort}\n`;
}
break;
case "http":
case "https":
toml += `
localIP = "${m.localIp}"
localPort = ${m.localPort}
customDomains=[${m.customDomains.map(m => `"${m}"`)}]
subdomain="${m.subdomain}"
`;
if (m.basicAuth) {
toml += `
httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"
`;
const customDomains = m.customDomains.filter(f1 => f1 !== "");
if (customDomains && customDomains.length > 0) {
toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]\n`;
}
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;
case "stcp":
case "xtcp":
if (m.stcpModel === "visitors") {
toml += `keepTunnelOpen = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp":
if (m.stcpModel === "visitors") {
// 访问者
toml += `
serverName = "${m.serverName}"
toml += `serverName = "${m.serverName}"
bindAddr = "${m.bindAddr}"
bindPort = ${m.bindPort}
`;
bindPort = ${m.bindPort}\n`;
if (m.fallbackTo) {
toml += `
fallbackTo = "${m.fallbackTo}"
fallbackTimeoutMs = ${m.fallbackTimeoutMs || 500}
`;
toml += `fallbackTo = "${m.fallbackTo}"
fallbackTimeoutMs = ${m.fallbackTimeoutMs || 500}\n`;
}
} else if (m.stcpModel === "visited") {
// 被访问者
toml += `
localIP = "${m.localIp}"
localPort = ${m.localPort}`;
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}\n`;
}
toml += `
secretKey="${m.secretKey}"
`;
toml += `secretKey="${m.secretKey}"\n`;
break;
default:
break;
}
if (rangePort) {
toml += `{{- end }}`;
toml += `{{- end }}\n`;
}
return toml;
});
const toml = `
serverAddr = "${config.serverAddr}"
const toml = `serverAddr = "${config.serverAddr}"
serverPort = ${config.serverPort}
${
config.authMethod === "token"
? `
auth.method = "token"
auth.token = "${config.authToken}"
`
? `auth.method = "token"
auth.token = "${config.authToken}"`
: ""
}
${
config.authMethod === "multiuser"
? `
user = "${config.user}"
metadatas.token = "${config.metaToken}"
`
? `user = "${config.user}"
metadatas.token = "${config.metaToken}"`
: ""
}
${
config.transportHeartbeatInterval
? `
transport.heartbeatInterval = ${config.transportHeartbeatInterval}
`
: ""
}
${
config.transportHeartbeatTimeout
? `
transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}
`
: ""
}
log.to = "frpc.log"
log.level = "${config.logLevel}"
log.maxDays = ${config.logMaxDays}
webServer.addr = "127.0.0.1"
webServer.port = ${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}
${
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
? `
transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"
`
? `transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `
transport.tls.serverName = "${config.tlsConfigServerName}"
`
? `transport.tls.serverName = "${config.tlsConfigServerName}"`
: ""
}
${
config.proxyConfigEnable
? `
transport.proxyURL = "${config.proxyConfigProxyUrl}"
`
? `transport.proxyURL = "${config.proxyConfigProxyUrl}"`
: ""
}
${proxyToml.join("")}
`;
${proxyToml.join("")}`;
return toml;
};
@ -224,8 +229,7 @@ ${proxyToml.join("")}
export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => {
const rangePort = isRangePort(m);
let ini = `
[${rangePort ? "range:" : ""}${m.name}]
let ini = `[${rangePort ? "range:" : ""}${m.name}]
type = "${m.type}"
`;
switch (m.type) {
@ -234,26 +238,46 @@ type = "${m.type}"
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
remote_port = ${m.remotePort}
`;
remote_port = ${m.remotePort}\n`;
break;
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":
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
custom_domains=[${m.customDomains.map(m => `"${m}"`)}]
subdomain="${m.subdomain}"
`;
custom_domains=[${m.customDomains.map(m => `${m}`)}]
subdomain="${m.subdomain}"\n`;
if (m.basicAuth) {
ini += `
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;
case "stcp":
case "xtcp":
if (m.stcpModel === "visitors") {
ini += `keep_tunnel_open = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp":
if (m.stcpModel === "visitors") {
// 访问者
@ -261,23 +285,20 @@ httpPassword = "${m.httpPassword}"
role = visitor
server_name = "${m.serverName}"
bind_addr = "${m.bindAddr}"
bind_port = ${m.bindPort}
`;
bind_port = ${m.bindPort}\n`;
if (m.fallbackTo) {
ini += `
fallback_to = ${m.fallbackTo}
fallback_timeout_ms = ${m.fallbackTimeoutMs || 500}
`;
fallback_timeout_ms = ${m.fallbackTimeoutMs || 500}\n`;
}
} else if (m.stcpModel === "visited") {
// 被访问者
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}`;
local_port = ${m.localPort}\n`;
}
ini += `
sk="${m.secretKey}"
`;
sk="${m.secretKey}"\n`;
break;
default:
break;
@ -293,19 +314,29 @@ ${
config.authMethod === "token"
? `
authentication_method = ${config.authMethod}
token = ${config.authToken}
`
token = ${config.authToken}\n`
: ""
}
${
config.authMethod === "multiuser"
? `
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
? `
@ -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_level = ${config.logLevel}
@ -328,42 +396,6 @@ admin_addr = 127.0.0.1
admin_port = ${config.webPort}
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("")}
`;
@ -378,39 +410,65 @@ export const generateConfig = (
callback: (configPath: string) => void
) => {
listProxy((err3, proxys) => {
if (!err3) {
const { currentVersion } = config;
let filename = null;
let configContent = "";
const filtered = proxys
.map(m => {
if (m.status == null || m.status == undefined) {
m.status = true;
}
return m;
})
.filter(f => f.status);
if (currentVersion < 124395282) {
// 版本小于v0.52.0
filename = "frp.ini";
configContent = genIniConfig(config, filtered);
} else {
filename = "frp.toml";
configContent = genTomlConfig(config, filtered);
}
const configPath = path.join(app.getPath("userData"), filename);
log.info(`生成配置成功 配置路径:${configPath}`);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{ flag: "w" },
err => {
if (!err) {
callback(filename);
}
if (err3) {
logError(LogModule.FRP_CLIENT, `Failed to list proxies: ${err3.message}`);
return;
}
const { currentVersion } = config;
let filename = null;
let configContent = "";
const filtered = proxys
.map(m => {
if (m.status == null || m.status == undefined) {
m.status = true;
}
return m;
})
.filter(f => f.status);
if (currentVersion < 124395282) {
// 版本小于v0.52.0
filename = "frp.ini";
configContent = genIniConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using INI format for configuration: ${filename}`
);
} else {
filename = "frp.toml";
configContent = genTomlConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using TOML format for configuration: ${filename}`
);
}
const configPath = path.join(app.getPath("userData"), filename);
logInfo(
LogModule.FRP_CLIENT,
`Writing configuration to file: ${configPath}`
);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{ flag: "w" },
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);
}
}
);
});
};
@ -421,7 +479,13 @@ export const generateConfig = (
* @param configPath
*/
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}`;
frpcProcess = spawn(command, {
cwd: app.getPath("userData"),
@ -429,21 +493,31 @@ const startFrpcProcess = (commandPath: string, configPath: string) => {
});
runningCmd.commandPath = commandPath;
runningCmd.configPath = configPath;
frpcProcess.stdout.on("data", data => {
log.debug(`启动输出:${data}`);
logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
});
frpcProcess.stdout.on("error", data => {
log.error(`启动错误:${data}`);
logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`);
stopFrpcProcess(() => {});
});
frpcStatusListener = setInterval(() => {
const status = frpcProcessStatus();
log.debug(`监听frpc子进程状态${status} ${frpcStatusListener}`);
logDebug(
LogModule.FRP_CLIENT,
`Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
);
if (!status) {
new Notification({
title: "Frpc Desktop",
body: "连接已断开,请前往日志查看原因"
body: "Connection lost, please check the logs for details."
}).show();
logError(
LogModule.FRP_CLIENT,
"Frpc process status check failed. Connection lost."
);
clearInterval(frpcStatusListener);
}
}, 3000);
@ -454,20 +528,58 @@ const startFrpcProcess = (commandPath: string, configPath: string) => {
*/
export const reloadFrpcProcess = () => {
if (frpcProcess && !frpcProcess.killed) {
logDebug(
LogModule.FRP_CLIENT,
"Attempting to reload frpc process configuration."
);
getConfig((err1, config) => {
if (!err1) {
if (config) {
generateConfig(config, configPath => {
const command = `${runningCmd.commandPath} reload -c ${configPath}`;
log.info(`重载配置:${command}`);
exec(command, {
cwd: app.getPath("userData"),
shell: true
});
logInfo(
LogModule.FRP_CLIENT,
`Reloading configuration: ${command}`
);
exec(
command,
{
cwd: app.getPath("userData"),
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) {
treeKill(frpcProcess.pid, (error: 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();
} else {
log.info(`关闭frpc子进程成功`);
logInfo(
LogModule.FRP_CLIENT,
`Successfully stopped frpc process with pid: ${frpcProcess.pid}.`
);
frpcProcess = null;
clearInterval(frpcStatusListener);
callback();
}
});
} 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();
}
};
@ -497,14 +620,23 @@ export const stopFrpcProcess = (callback?: () => void) => {
*/
export const frpcProcessStatus = () => {
if (!frpcProcess) {
logDebug(LogModule.FRP_CLIENT, "frpc process is not running.");
return false;
}
try {
// 发送信号给进程,如果进程存在,会正常返回
process.kill(frpcProcess.pid, 0);
logDebug(
LogModule.FRP_CLIENT,
`frpc process is running with pid: ${frpcProcess.pid}`
);
return true;
} catch (error) {
// 进程不存在,抛出异常
logError(
LogModule.FRP_CLIENT,
`frpc process not found. Error: ${error.message}`
);
return false;
}
};
@ -514,29 +646,47 @@ export const frpcProcessStatus = () => {
* @param config
*/
export const startFrpWorkerProcess = async (config: FrpConfig) => {
logInfo(LogModule.FRP_CLIENT, "Starting frpc worker process...");
getFrpcVersionWorkerPath(config.currentVersion, (frpcVersionPath: string) => {
if (frpcVersionPath) {
logInfo(
LogModule.FRP_CLIENT,
`Found frpc version path: ${frpcVersionPath}`
);
generateConfig(config, configPath => {
const platform = process.platform;
if (platform === "win32") {
logInfo(LogModule.FRP_CLIENT, "Starting frpc on Windows.");
startFrpcProcess(path.join(frpcVersionPath, "frpc.exe"), configPath);
} else {
logInfo(
LogModule.FRP_CLIENT,
"Starting frpc on non-Windows platform."
);
startFrpcProcess(path.join(frpcVersionPath, "frpc"), configPath);
}
});
} else {
logError(LogModule.FRP_CLIENT, "frpc version path not found.");
}
});
};
export const initFrpcApi = () => {
ipcMain.handle("frpc.running", async (event, args) => {
logDebug(LogModule.FRP_CLIENT, "Checking if frpc process is running...");
return frpcProcessStatus();
});
ipcMain.on("frpc.start", async (event, args) => {
logInfo(LogModule.FRP_CLIENT, "Received request to start frpc process.");
getConfig((err1, config) => {
if (!err1) {
if (!config) {
logWarn(
LogModule.FRP_CLIENT,
"Configuration not found. Prompting user to set configuration."
);
event.reply(
"Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动"
@ -544,6 +694,10 @@ export const initFrpcApi = () => {
return;
}
if (!config.currentVersion) {
logWarn(
LogModule.FRP_CLIENT,
"Current version not set in configuration. Prompting user."
);
event.reply(
"Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动"
@ -551,13 +705,18 @@ export const initFrpcApi = () => {
return;
}
startFrpWorkerProcess(config);
} else {
logError(LogModule.FRP_CLIENT, `Error getting configuration: ${err1}`);
}
});
});
ipcMain.on("frpc.stop", () => {
logInfo(LogModule.FRP_CLIENT, "Received request to stop frpc process.");
if (frpcProcess && !frpcProcess.killed) {
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 {
deleteVersionById,
getVersionById,
insertVersion,
listVersion
} from "../storage/version";
@ -10,7 +18,11 @@ const path = require("path");
const zlib = require("zlib");
const { download } = require("electron-dl");
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 = {
win32_x64: ["window", "amd64"],
@ -30,23 +42,44 @@ const frpArch = versionRelation[currArch];
const unTarGZ = (tarGzPath: string, targetPath: string) => {
const tar = require("tar");
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);
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({
cwd: targetPath,
filter: filePath => path.basename(filePath) === "frpc"
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}`);
})
);
const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
log.debug(`解压完成 解压后目录:${frpcPath}`);
return frpcPath;
// .on("finish", () => {
// console.log("解压完成!");
// });
.pipe(
tar
.extract({
cwd: targetPath,
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"));
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
});
return path.join("frp", path.basename(tarGzPath, ".tar.gz"));
};
const unZip = (zipPath: string, targetPath: string) => {
@ -54,133 +87,300 @@ const unZip = (zipPath: string, targetPath: string) => {
fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), {
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}`);
/**
* unzipper解压
*/
// fs.createReadStream(zipPath)
// .pipe(unzipper.Extract({ path: targetPath }))
// // 只解压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);
// });
logDebug(
LogModule.APP,
`Starting to unzip: ${zipPath} to target directory: ${targetPath}`
);
logInfo(LogModule.APP, `Starting to extract zip file: ${zipPath}`);
const zip = new AdmZip(zipPath);
zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件
const frpcPath = path.join("frp", path.basename(zipPath, ".zip"));
log.debug(`解压完成 解压后目录:${frpcPath}`);
return frpcPath;
try {
zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件
const frpcPath = path.join("frp", path.basename(zipPath, ".zip"));
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
logDebug(
LogModule.APP,
`Unzip completed. Extracted directory: ${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[] = [];
const getVersion = versionId => {
return versions.find(f => f.id === versionId);
const getVersionByGithubVersionId = 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 { 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 a = versionRelation[currArch]
const a = frpArch;
if (a) {
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;
}
logWarn(
LogModule.GITHUB,
`No architecture match found for asset: ${f.name}`
);
return false;
});
if (asset) {
log.info(`找到对应版本 ${asset.name}`);
if (!asset) {
logError(
LogModule.GITHUB,
`No adaptive asset found for version ID: ${versionId}`
);
}
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"];
/**
* handle github api release json
* @param githubReleaseJsonStr jsonStr
* @returns versions
*/
const handleApiResponse = (githubReleaseJsonStr: string) => {
const downloadPath = path.join(app.getPath("userData"), "download");
const frpPath = path.join(app.getPath("userData"), "frp");
logInfo(LogModule.GITHUB, "Parsing GitHub release JSON response.");
const i = Math.floor(Math.log(bytes) / Math.log(k)); // 计算单位索引
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // 返回格式化的字符串
versions = JSON.parse(githubReleaseJsonStr);
if (versions) {
logInfo(
LogModule.GITHUB,
"Successfully parsed versions from GitHub response."
);
const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id))
.map(m => {
const asset = getAdaptiveAsset(m.id);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
if (asset) {
const absPath = path.join(
frpPath,
asset.name.replace(/(\.tar\.gz|\.zip)$/, "")
);
m.absPath = absPath;
m.download_completed = fs.existsSync(absPath);
m.download_count = download_count;
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;
});
logDebug(
LogModule.GITHUB,
`Retrieved FRP versions: ${JSON.stringify(returnVersionsData)}`
);
return returnVersionsData;
} else {
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 => {
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: "https://api.github.com/repos/fatedier/frp/releases?page=1&per_page=1000"
url: "https://api.jwinks.com/github/releases"
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", () => {
log.info(
`开始获取frp版本 当前架构:${currArch} 对应frp架构${frpArch} 状态码:${response.statusCode}`
);
const downloadPath = path.join(app.getPath("userData"), "download");
if (response.statusCode === 200) {
versions = JSON.parse(responseData.toString());
}
// const borderContent: Electron.WebContents =
// BrowserWindow.getFocusedWindow().webContents;
if (versions) {
const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id))
.map(m => {
const asset = getAdaptiveAsset(m.id);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
if (asset) {
const absPath = `${downloadPath}/${asset.name}`;
m.absPath = absPath;
m.download_completed = fs.existsSync(absPath);
m.download_count = download_count;
m.size = formatBytes(asset.size);
}
return m;
});
// log.debug(`获取到frp版本${JSON.stringify(returnVersionsData)}`)
event.reply("Download.frpVersionHook", returnVersionsData);
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
event.reply("Download.frpVersionHook", []);
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();
});
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) => {
const { versionId, mirror } = args;
const version = getVersion(versionId);
const version = getVersionByGithubVersionId(versionId);
const asset = getAdaptiveAsset(versionId);
const { browser_download_url } = asset;
let url = browser_download_url;
if (mirror === "ghproxy") {
url = "https://mirror.ghproxy.com/" + url;
}
let url = browser_download_url.replace(
"https://github.com",
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, {
filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"),
@ -189,32 +389,26 @@ export const initGitHubApi = () => {
id: versionId,
progress: progress
});
logDebug(
LogModule.GITHUB,
`Download progress for versionId: ${versionId} is ${
progress.percent * 100
}%`
);
},
onCompleted: () => {
log.info(`frp下载完成 url${url} asset${asset.name}`);
const targetPath = path.resolve(
path.join(app.getPath("userData"), "frp")
logInfo(
LogModule.GITHUB,
`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;
insertVersion(version, (err, document) => {
if (!err) {
@ -222,7 +416,13 @@ export const initGitHubApi = () => {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.frpVersionDownloadOnCompleted", versionId);
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) => {
const { absPath, id } = args;
logDebug(
LogModule.GITHUB,
`Attempting to delete version with ID: ${id} and path: ${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, () => {
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) => {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.deleteVersion.hook", {
err: null,
data: "删除成功"
});
if (err) {
logError(LogModule.GITHUB, `Error listing versions: ${err}`);
} else {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.deleteVersion.hook", {
err: null,
data: "删除成功"
});
}
});
});
@ -252,6 +474,7 @@ export const initGitHubApi = () => {
*
*/
ipcMain.on("github.getFrpcDesktopLastVersions", async event => {
logInfo(LogModule.GITHUB, "Requesting the latest version from GitHub.");
const request = net.request({
method: "get",
url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest"
@ -262,26 +485,114 @@ export const initGitHubApi = () => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
versions = JSON.parse(responseData.toString());
// const borderContent: Electron.WebContents =
// BrowserWindow.getFocusedWindow().webContents;
// const downloadPath = path.join(app.getPath("userData"), "download");
// 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);
try {
versions = JSON.parse(responseData.toString());
logInfo(
LogModule.GITHUB,
"Successfully retrieved the latest version."
);
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();
});
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 log from "electron-log";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const {exec, spawn} = require("child_process");
@ -15,19 +15,19 @@ export const initLocalApi = () => {
: 'netstat -an | grep LISTEN';
ipcMain.on("local.getLocalPorts", async (event, args) => {
log.info("开始获取本地端口")
logInfo(LogModule.APP, "Starting to retrieve local ports");
// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
log.error(`getLocalPorts - error ${error.message}`)
logError(LogModule.APP, `getLocalPorts - error: ${error.message}`);
return;
}
if (stderr) {
log.error(`getLocalPorts - stderr ${stderr}`)
logWarn(LogModule.APP, `getLocalPorts - stderr: ${stderr}`);
return;
}
log.debug(`sc ${stdout}`)
logDebug(LogModule.APP, `Command output: ${stdout}`);
let ports = [];
if (stdout) {
if (process.platform === 'win32') {
@ -72,7 +72,6 @@ export const initLocalApi = () => {
}
return singe;
})
// .filter(f => f.indexOf('TCP') > 0 || f.indexOf('UDP') > 0)
} else if (process.platform === 'linux') {
ports = stdout.split('\n')

View File

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

View File

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

View File

@ -56,10 +56,7 @@ export const initUpdaterApi = (win: BrowserWindow) => {
})
autoUpdater.on('update-downloaded', () => {
console.log('update-downloaded')
dialog.showMessageBox({
type: 'info',
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 { initFileApi } from "../api/file";
import { getConfig } from "../storage/config";
import log from "electron-log";
import { initCommonApi } from "../api/common";
import { initLocalApi } from "../api/local";
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
import { initLog, logError, logInfo, LogModule } from "../utils/log";
import { maskSensitiveData } from "../utils/desensitize";
process.env.DIST_ELECTRON = join(__dirname, "..");
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public")
: 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
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -51,28 +50,11 @@ if (!app.requestSingleInstanceLock()) {
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) {
console.log("config", config);
let show = true;
if (config) {
show = !config.systemSilentStartup;
}
console.log("界面", show);
win = new BrowserWindow({
title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
@ -138,11 +120,6 @@ async function createWindow(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> = [
{
label: "显示主窗口",
@ -175,42 +152,100 @@ export const createTray = (config: FrpConfig) => {
win.show();
});
if (config) {
if (config.systemStartupConnect) {
log.info(`已开启自动连接 正在自动连接服务器`);
startFrpWorkerProcess(config);
}
}
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) => {
createWindow(config).then(r => {
createTray(config);
// 初始化各个API
initGitHubApi();
initConfigApi(win);
initProxyApi();
initFrpcApi();
initLoggerApi();
initFileApi();
initCommonApi();
initLocalApi();
// initUpdaterApi(win);
});
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) {
logInfo(
LogModule.APP,
`Config retrieved: ${JSON.stringify(
maskSensitiveData(config, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
if (config.systemStartupConnect) {
startFrpWorkerProcess(config);
}
}
// Initialize APIs
try {
initGitHubApi(win);
logInfo(LogModule.APP, `GitHub API initialized.`);
initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
initProxyApi();
logInfo(LogModule.APP, `Proxy API initialized.`);
initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
initLoggerApi();
logInfo(LogModule.APP, `Logger API initialized.`);
initFileApi();
logInfo(LogModule.APP, `File API initialized.`);
initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`);
initLocalApi();
logInfo(LogModule.APP, `Local API initialized.`);
// 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", () => {
logInfo(LogModule.APP, `All windows closed.`);
win = null;
if (process.platform !== "darwin") {
stopFrpcProcess(() => {
logInfo(LogModule.APP, `FRPC process stopped. Quitting application.`);
app.quit();
});
}
});
app.on("second-instance", () => {
logInfo(LogModule.APP, `Second instance detected.`);
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
@ -219,23 +254,35 @@ app.on("second-instance", () => {
});
app.on("activate", () => {
logInfo(LogModule.APP, `Application activated.`);
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
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", () => {
log.info("before-quit");
isQuiting = true;
logInfo(LogModule.APP, `Application is about to quit.`);
stopFrpcProcess(() => {
isQuiting = true;
});
});
// New window example arg: new windows url
ipcMain.handle("open-win", (_, arg) => {
logInfo(LogModule.APP, `Opening new window with argument: ${arg}`);
const childWindow = new BrowserWindow({
webPreferences: {
preload,
@ -246,7 +293,12 @@ ipcMain.handle("open-win", (_, arg) => {
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`);
logInfo(LogModule.APP, `Child window loaded URL: ${url}#${arg}`);
} else {
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 { 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({
autoload: true,
filename: path.join(app.getPath("userData"), "config.db")
});
/**
*
*/
@ -17,8 +17,37 @@ export const saveConfig = (
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
document["_id"] = "1";
log.debug(`保存日志 ${JSON.stringify(document)}`);
configDB.update({ _id: "1" }, document, { upsert: true }, cb);
logDebug(
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 = (
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) => {
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 { app } from "electron";
const log = require("electron-log");
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const proxyDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "proxy.db")
});
/**
*
* @param proxy
* @param cb
*/
export const insertProxy = (
proxy: Proxy,
cb?: (err: Error | null, document: Proxy) => void
) => {
log.debug(`新增代理:${JSON.stringify(proxy)}`);
proxyDB.insert(proxy, cb);
logInfo(LogModule.DB, `Inserting proxy: ${JSON.stringify(proxy)}`);
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 = (
_id: string,
cb?: (err: Error | null, n: number) => void
) => {
log.debug(`删除代理:${_id}`);
proxyDB.remove({ _id: _id }, cb);
logInfo(LogModule.DB, `Deleting proxy with ID: ${_id}`);
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 = (
proxy: Proxy,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
log.debug(`修改代理:${proxy}`);
proxyDB.update({ _id: proxy._id }, proxy, {}, cb);
logInfo(LogModule.DB, `Updating proxy: ${JSON.stringify(proxy)}`);
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 = (
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 = (
id: string,
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) => {
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 = (
id: string,
st: boolean,
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 { app } from "electron";
const log = require("electron-log");
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const versionDB = new Datastore({
autoload: true,
@ -10,7 +10,7 @@ const versionDB = new Datastore({
});
/**
*
* Insert version
* @param version
* @param cb
*/
@ -18,35 +18,73 @@ export const insertVersion = (
version: FrpVersion,
cb?: (err: Error | null, document: any) => void
) => {
log.debug(`新增版本:${JSON.stringify(version)}`);
versionDB.insert(version, cb);
logInfo(LogModule.DB, `Inserting version: ${JSON.stringify(version)}`);
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
*/
export const listVersion = (
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 = (
id: number,
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 = (
id: string,
callback: (err: Error | null, document: any) => void
) => {
log.debug(`删除版本:${id}`);
versionDB.remove({ id: id }, callback);
logInfo(LogModule.DB, `Deleting version: ${id}`);
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) => {
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",
"version": "1.1.4",
"version": "1.1.6",
"main": "dist-electron/main/index.js",
"description": "FRP跨平台桌面客户端可视化配置轻松实现内网穿透",
"repository": "github:luckjiawei/frpc-desktop",
"author": "刘嘉伟 <8473136@qq.com>",
"license": "MIT",
"private": true,
"type": "commonjs",
"keywords": [
"frp",
"frpc",
@ -29,6 +30,7 @@
"build": "vue-tsc --noEmit && vite build",
"build:electron": "npm run build && electron-builder --mac --win --linux",
"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:linux": "npm run build && electron-builder --linux",
"release": "npm run build && electron-builder --mac --win --linux -p always",
@ -68,14 +70,14 @@
"vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-tsc": "^2.0.22",
"vue-tsc": "2.0.22",
"vue-types": "^5.1.1"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"adm-zip": "^0.5.14",
"animate.css": "^4.1.1",
"electron-dl": "^3.5.1",
"electron-dl": "3.5.1",
"electron-log": "^5.1.7",
"intro.js": "^8.0.0-beta.1",
"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 description from "@iconify-icons/material-symbols/description";
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("rocket-launch-rounded", RocketLaunchRounded);
@ -70,4 +73,7 @@ addIcon("attach-money-rounded", attachMoneyRounded);
addIcon("volunteer-activism-sharp", volunteerActivismSharp);
addIcon("description", description);
addIcon("folder-rounded", folderRounded);
addIcon("link", link);
addIcon("unarchive", unarchive);
addIcon("file-save-rounded", fileSaveRounded);

View File

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

View File

@ -47,7 +47,13 @@ const defaultFormData = ref<FrpConfig>({
transportHeartbeatInterval: 30,
transportHeartbeatTimeout: 90,
webEnable: true,
webPort: 57400
webPort: 57400,
transportProtocol: "tcp",
transportDialServerTimeout: 10,
transportDialServerKeepalive: 7200,
transportPoolCount: 0,
transportTcpMux: true,
transportTcpMuxKeepaliveInterval: 30
});
const formData = ref<FrpConfig>(defaultFormData.value);
@ -121,6 +127,24 @@ const rules = reactive<FormRules>({
],
webPort: [
{ 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;
}
if (data.webEnable == null || data.webEnable == undefined) {
data.webEnable = true;
data.webPort = 57400;
data.webEnable = defaultFormData.value.webEnable;
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;
}
}
@ -406,10 +470,10 @@ onUnmounted(() => {
<IconifyIconOffline icon="deviceReset" />
</el-button>
<el-button plain type="primary" @click="handleImportConfig">
<IconifyIconOffline icon="uploadRounded" />
<IconifyIconOffline icon="file-open-rounded" />
</el-button>
<el-button plain type="primary" @click="handleShowExportDialog">
<IconifyIconOffline icon="downloadRounded" />
<IconifyIconOffline icon="file-save-rounded" />
</el-button>
<el-button type="primary" @click="handleSubmit">
<IconifyIconOffline icon="save-rounded" />
@ -422,7 +486,7 @@ onUnmounted(() => {
:rules="rules"
label-position="right"
ref="formRef"
label-width="130"
label-width="150"
>
<el-row :gutter="10">
<el-col :span="24">
@ -632,6 +696,72 @@ onUnmounted(() => {
/>
</el-form-item>
</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-form-item
label="心跳间隔:"
@ -715,9 +845,176 @@ onUnmounted(() => {
<!-- </el-input>-->
</el-form-item>
</el-col>
<el-col :span="24">
<div class="h2">TLS Config</div>
<el-col :span="12">
<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 :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-form-item label="启用TLS" prop="tlsConfigEnable">
<el-switch
@ -904,48 +1201,6 @@ onUnmounted(() => {
</el-form-item>
</el-col>
</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">
<div class="h2">Web 界面</div>
@ -963,9 +1218,10 @@ onUnmounted(() => {
icon="info"
/>
</template>
热更新等功能依赖于web界面<span class="font-black text-[#5A3DAA]"
>不可停用Web</span
>
热更新等功能依赖于web界面<span
class="font-black text-[#5A3DAA]"
>不可停用Web</span
>
</el-popover>
</div>
启用Web
@ -989,7 +1245,7 @@ onUnmounted(() => {
<template #default>
对应参数<span class="font-black text-[#5A3DAA]"
>webServer.port</span
><br/>
><br />
自行保证端口没有被占用否则会导致启动失败
</template>
<template #reference>
@ -1009,6 +1265,7 @@ onUnmounted(() => {
:min="0"
:max="65535"
controls-position="right"
class="w-full"
></el-input-number>
</el-form-item>
</el-col>

View File

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

View File

@ -63,7 +63,11 @@ const defaultForm = ref<Proxy>({
httpUser: "",
httpPassword: "",
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") {
return false;
}
if (proxy.type === "sudp" && proxy.stcpModel === "visited") {
return false;
}
return true;
};
@ -558,6 +565,25 @@ const handleRandomProxyName = () => {
`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(() => {
handleInitHook();
handleLoadProxys();
@ -659,7 +685,7 @@ onUnmounted(() => {
class="text-xl text-[#ADADAD] hover:text-[#5A3DAA]"
@click="handleCopyAccessAddress(proxy)"
>
<IconifyIconOffline icon="content-copy" />
<IconifyIconOffline icon="link" />
</a>
<el-dropdown size="small">
<a
@ -709,7 +735,9 @@ onUnmounted(() => {
<div
class="text-sm text-left"
v-if="
(proxy.type !== 'stcp' && proxy.type !== 'xtcp') ||
(proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors'
"
>
@ -724,7 +752,9 @@ onUnmounted(() => {
<div
class="text-sm text-center"
v-if="
(proxy.type !== 'stcp' && proxy.type !== 'xtcp') ||
(proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors'
"
>
@ -735,7 +765,9 @@ onUnmounted(() => {
<div
class="text-sm text-center"
v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') &&
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
"
>
@ -746,7 +778,9 @@ onUnmounted(() => {
<div
class="text-sm text-center"
v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') &&
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
"
>
@ -757,7 +791,9 @@ onUnmounted(() => {
<div
class="text-sm text-center"
v-if="
(proxy.type === 'stcp' || proxy.type === 'xtcp') &&
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
"
>
@ -1086,6 +1122,136 @@ onUnmounted(() => {
</el-form-item>
</el-col>
</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">
<el-col :span="24">
<el-form-item label="被访问者代理名称:" prop="serverName">
@ -1260,6 +1426,50 @@ onUnmounted(() => {
/>
</el-form-item>
</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>
<el-col :span="24">
<el-form-item>
@ -1377,4 +1587,8 @@ onUnmounted(() => {
:deep(.el-drawer__body) {
//padding-top: 0;
}
.button-input {
width: calc(100% - 68px);
}
</style>

10
types/global.d.ts vendored
View File

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