Compare commits

...

99 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
刘嘉伟
0b4824975a 增加打开数据目录按钮 2024-12-22 22:04:52 +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
刘嘉伟
32555d45b1 📝 更新捐赠 2024-12-04 17:06:05 +08:00
刘嘉伟
ab2323f421 增加捐赠等 2024-12-04 17:01:43 +08:00
刘嘉伟
139191065b 手动刷新日志 2024-12-04 16:35:00 +08:00
刘嘉伟
d70a7488fc ⬆️ 升级依赖 2024-12-04 16:19:27 +08:00
刘嘉伟
6c4a052bf6 打开外部日志的功能 2024-12-04 16:19:17 +08:00
刘嘉伟
5ac69de59a 增加webPort修改功能 2024-12-02 17:49:04 +08:00
刘嘉伟
90233f006a 增加适配sudp 2024-12-02 14:53:21 +08:00
刘嘉伟
40e4623413 认领HelloGithub 2024-12-02 09:19:36 +08:00
刘嘉伟
dc511fabc4 修复github api 限流问题 2024-12-01 10:10:59 +08:00
刘嘉伟
30b799f710 🌱 增加捐赠 2024-11-28 16:53:59 +08:00
刘嘉伟
b82215ca16 增加捐赠 2024-11-12 14:53:14 +08:00
刘嘉伟
654f6c7fd6 🔖 发布v1.1.4 2024-11-08 16:45:42 +08:00
刘嘉伟
919370c56c xtcp模式下打洞失败回退 stcp 2024-11-08 16:17:21 +08:00
刘嘉伟
e7f4572768 xtcp模式下打洞失败回退 stcp 2024-11-08 16:10:54 +08:00
刘嘉伟
dba788a1dd 🐛 warning 模式下报错 2024-11-08 16:10:15 +08:00
刘嘉伟
6c7c568a48 增加捐赠 2024-11-08 09:17:20 +08:00
刘嘉伟
88c687b66d
baidu
1
2024-10-29 10:09:35 +08:00
刘嘉伟
4a58fbd955 增加捐赠 2024-10-16 14:07:54 +08:00
刘嘉伟
d694d9537d 增加捐赠 2024-10-16 14:07:06 +08:00
刘嘉伟
c5883bb25b 增加捐赠 2024-10-14 15:15:21 +08:00
刘嘉伟
f04e08ee3e 新版本 2024-10-14 12:10:04 +08:00
刘嘉伟
6d2f1c7b64 生成代理名称 2024-10-14 12:09:36 +08:00
刘嘉伟
fe697059b4 生成代理名称 2024-10-14 12:06:08 +08:00
刘嘉伟
2d894844b0 xtcp 支持 2024-10-14 11:53:19 +08:00
刘嘉伟
ca885af394 静默启动 2024-10-14 11:27:50 +08:00
刘嘉伟
4d9c5bf003 静默启动 2024-10-14 11:24:54 +08:00
刘嘉伟
2ccddbe4b6 复制访问地址 2024-10-14 11:08:19 +08:00
刘嘉伟
843ab85215 子域名和自定义域名可以二选一 2024-09-25 11:43:42 +08:00
刘嘉伟
ed1f3dc378 子域名和自定义域名可以二选一 2024-09-25 10:48:43 +08:00
刘嘉伟
acbf075280 🐛 启动报错 2024-09-25 10:48:09 +08:00
刘嘉伟
59ef02033a Merge branch 'refs/heads/develop'
# Conflicts:
#	src/views/proxy/index.vue
2024-09-25 10:26:18 +08:00
刘嘉伟
d57e6f41ed 💄 修复窗口大小 2024-09-25 10:25:28 +08:00
刘嘉伟
0c1d6919ec 💫 修改 logo动画 2024-09-25 10:24:51 +08:00
刘嘉伟
f788990705 🐛 启动报错和支持 http Basic、子域名 2024-09-25 10:21:29 +08:00
刘嘉伟
139fed40d8 🐛 修复下载报错 2024-09-25 10:20:37 +08:00
刘嘉伟
6d5f985dfe 优化 2024-09-15 10:47:48 +08:00
刘嘉伟
686acdd7c5 🐛 修复下载报错 2024-09-15 10:31:21 +08:00
刘嘉伟
0fe365e3d9
Update issue templates 2024-09-10 16:52:04 +08:00
刘嘉伟
e0dcd66453
Update issue templates 2024-09-10 16:50:54 +08:00
刘嘉伟
c3f914e7e5 🚑 发布 1.1.1 2024-09-09 20:13:49 +08:00
刘嘉伟
d45a9b84bb 🐛 修复启动报错 2024-09-08 21:24:55 +08:00
刘嘉伟
efb5e94d4e 📝 批量端口 2024-09-08 00:21:52 +08:00
刘嘉伟
e378421c72 📝 批量端口 2024-09-07 18:08:37 +08:00
刘嘉伟
9d57006d16 📝 批量端口 2024-09-07 17:27:03 +08:00
刘嘉伟
e4cd09d1dc ini批量端口 2024-09-07 17:24:51 +08:00
刘嘉伟
60fbc354d0 批量端口前端校验 2024-09-07 17:18:59 +08:00
刘嘉伟
dbfceb9550 toml 批量端口 2024-09-07 17:01:49 +08:00
刘嘉伟
e0b3f6e7a7 范围端口 2024-09-07 16:49:38 +08:00
刘嘉伟
96247a29d4 ip 提示 2024-09-07 16:18:14 +08:00
37 changed files with 50639 additions and 785 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**🖥️ 操作系统**
**🔖 Frpc-Desktop版本号**
**🤔 问题描述**
**🖼️ 错误截图**
**🔊 日志**
> 日志目录:
* Window: `%APPDATA%\Frpc-Desktop\logs\`
* MacOS: `~/Library/Logs/Frpc-Desktop/logs/`
* Linux: `~/Library/Logs/Frpc-Desktop/`

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

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,9 +22,13 @@
</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
- [x] 开机自启动
- [x] 适配多用户 user & meta_token
- [x] 便携版
@ -36,14 +40,23 @@
- [x] 支持所有配置的导入导出
- [x] 一键清空所有配置
- [x] 支持导入识别frpc.toml
- [ ] tcp、udp协议支持批量端口
- [x] tcp、udp协议支持批量端口
- [ ] support multiple languages
## 常见问题
### Mac提示已损坏
执行命令:`sudo xattr -cr Frpc-Desktop.app`
## 里程碑
- 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、子域名
- 2024-09-07: 发布v1.1.0版本 支持批量端口、支持单条代理开关控制
- 2024-08-24: 发布v1.0.9版本 支持镜像下载、导出导入配置
- 2024-08-17: 发布v1.0.8版本 支持stcp代理
- 2024-08-11: 发布v1.0.7版本
@ -56,14 +69,20 @@
- 2023-11-28: 发布v1.0版本
## 社区
~~微信扫描加入开源项目交流群 广告勿进!!!~~
<img src="screenshots/wechat-qr.png" alt="二维码" width="200">
广告勿进!!!
**微信群超过200人无法扫码进群 关注公众号进群 **
### TG
<img src="screenshots/mp_qr.jpg" alt="公众号二维码" width="200">
[https://t.me/+4kziSBL3LxVmYzVl](https://t.me/+4kziSBL3LxVmYzVl)
### 微信群
**~~微信扫描加入开源项目交流群~~ 微信群超过200人无法扫码进群 关注公众号进群**
<img src="screenshots/wechat-qr.png" alt="二维码" width="200"><img src="screenshots/mp_qr.jpg" alt="公众号二维码" width="200">
## 演示
@ -79,11 +98,47 @@
![about](https://github.com/luckjiawei/frpc-desktop/blob/main/screenshots/about.png?raw=true)
## 赞助
## 捐赠
<img src="screenshots/gratuity.jpg" alt="赞助二维码" width="200">
👉👉👉[点击去捐赠](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-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 客户端前来支援 |
## 贡献者
<a href="https://github.com/luckjiawei/frpc-desktop/graphs/contributors">
<img src="https://contrib.rocks/image?repo=luckjiawei/frpc-desktop" />
</a>
@ -96,11 +151,19 @@
[![Star History Chart](https://api.star-history.com/svg?repos=luckjiawei/frpc-desktop&type=Date)](https://star-history.com/#luckjiawei/frpc-desktop&Date)
<!-- MARKDOWN LINKS & IMAGES -->
[forks-shield]: https://img.shields.io/github/forks/luckjiawei/frpc-desktop.svg?style=for-the-badge
[forks-url]: https://github.com/luckjiawei/frpc-desktop/network/members
[stars-shield]: https://img.shields.io/github/stars/luckjiawei/frpc-desktop.svg?style=for-the-badge
[stars-url]: https://github.com/luckjiawei/frpc-desktop/stargazers
[issues-shield]: https://img.shields.io/github/issues/luckjiawei/frpc-desktop.svg?style=for-the-badge
[issues-url]: https://github.com/luckjiawei/frpc-desktop/issues
[license-shield]: https://img.shields.io/github/license/luckjiawei/frpc-desktop.svg?style=for-the-badge
[license-url]: https://github.com/luckjiawei/frpc-desktop/blob/master/LICENSE

11
baidu_urls.txt Normal file
View File

@ -0,0 +1,11 @@
https://jwinks.com/p/frpc-desktop113
https://jwinks.com/p/free-jsaqy
https://jwinks.com/p/frpc-desktop110
https://jwinks.com/p/frpc-desktop109
https://jwinks.com/p/nginx-proxy-manager
https://jwinks.com/p/frp
https://jwinks.com/p/acme
https://jwinks.com/archives
https://jwinks.com/search
https://jwinks.com/%E9%93%BE%E6%8E%A5
https://jwinks.com/donate

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,31 +1,29 @@
import { app, dialog, ipcMain } from "electron";
import { app, dialog, ipcMain, shell } from "electron";
import { clearConfig, getConfig, saveConfig } from "../storage/config";
import { clearVersion, listVersion } from "../storage/version";
import {
generateConfig,
genIniConfig,
genTomlConfig,
stopFrpcProcess
} from "./frpc";
import { exec } from "child_process";
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");
export const initConfigApi = win => {
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;
log.info("开启自启状态", start);
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,
@ -36,7 +34,11 @@ export const initConfigApi = win => {
});
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
@ -45,7 +47,11 @@ export const initConfigApi = win => {
});
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
@ -54,7 +60,11 @@ export const initConfigApi = win => {
});
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
@ -63,15 +73,20 @@ export const initConfigApi = win => {
});
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;
}
log.info(`导出目录 ${outputDirectory} 类型:${args}`);
logInfo(
LogModule.APP,
`Exporting configuration to directory ${outputDirectory} with type: ${args}`
);
getConfig((err1, config) => {
if (!err1 && config) {
listProxy((err2, proxys) => {
@ -91,33 +106,44 @@ export const initConfigApi = win => {
configContent, // 配置文件内容
{ flag: "w" },
err => {
if (!err) {
// callback(filename);
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
}
});
}
}
);
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) => {
logInfo(LogModule.APP, `Parsing TOML configuration from ${tomlPath}`);
const importConfigPath = tomlPath;
const tomlData = fs.readFileSync(importConfigPath, "utf-8");
log.info(`读取到配置内容 ${tomlData}`);
logInfo(LogModule.APP, "Configuration content read successfully.");
const sourceConfig = toml.parse(tomlData);
// log.info(`解析结果 ${sourceConfig}`);
// console.log(sourceConfig, "frpcConfig");
// 解析config
const targetConfig: FrpConfig = {
currentVersion: null,
@ -143,7 +169,19 @@ export const initConfigApi = win => {
user: sourceConfig?.user || "",
metaToken: sourceConfig?.metadatas?.token || "",
systemSelfStart: false,
systemStartupConnect: 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
};
let frpcProxys = [];
// 解析proxy
@ -157,17 +195,28 @@ export const initConfigApi = win => {
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
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) {
@ -180,28 +229,45 @@ export const initConfigApi = win => {
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
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 => {
console.log("插入", f, err);
if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err}`);
} else {
logInfo(LogModule.APP, `Inserted proxy: ${JSON.stringify(f)}`);
}
});
});
});
@ -209,6 +275,7 @@ export const initConfigApi = win => {
};
ipcMain.on("config.importConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to import configuration.");
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
@ -216,17 +283,25 @@ export const initConfigApi = win => {
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
return;
} else {
const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath); // 获取文件后缀名
log.info(`导入文件 ${filePath} ${fileExtension}`);
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} 格式文件`
@ -236,11 +311,27 @@ export const initConfigApi = win => {
});
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
@ -33,6 +29,14 @@ const getFrpcVersionWorkerPath = (
});
};
const isRangePort = (m: Proxy) => {
return (
(m.type === "tcp" || m.type === "udp") &&
(String(m.localPort).indexOf("-") !== -1 ||
String(m.localPort).indexOf(",") !== -1)
);
};
/**
* toml配置文件
* @param config
@ -40,133 +44,180 @@ const getFrpcVersionWorkerPath = (
*/
export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => {
let toml = `
[[${m.type === "stcp" && m.stcpModel === "visitors" ? "visitors" : "proxies"}]]
name = "${m.name}"
type = "${m.type}"
`;
const rangePort = isRangePort(m);
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}"`}
type = "${m.type}"\n`;
switch (m.type) {
case "tcp":
case "udp":
toml += `
localIP = "${m.localIp}"
if (rangePort) {
toml += `name = "${m.name}-{{ $v.First }}"
localPort = {{ $v.First }}
remotePort = {{ $v.Second }}\n`;
} else {
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}"`)}]
`;
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 "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}\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 }}\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 = 57400
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;
};
@ -177,8 +228,8 @@ ${proxyToml.join("")}
*/
export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => {
let ini = `
[${m.name}]
const rangePort = isRangePort(m);
let ini = `[${rangePort ? "range:" : ""}${m.name}]
type = "${m.type}"
`;
switch (m.type) {
@ -187,35 +238,67 @@ 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 += `
custom_domains=[${m.customDomains.map(m => `${m}`)}]
subdomain="${m.subdomain}"\n`;
if (m.basicAuth) {
ini += `
httpUser = "${m.httpUser}"
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}
custom_domains=[${m.customDomains.map(m => `"${m}"`)}]
`;
local_port = ${m.localPort}\n`;
}
break;
case "xtcp":
if (m.stcpModel === "visitors") {
ini += `keep_tunnel_open = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp":
if (m.stcpModel === "visitors") {
// 访问者
ini += `
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}\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;
@ -231,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
? `
@ -258,50 +351,51 @@ heartbeat_timeout = ${config.transportHeartbeatTimeout}
`
: ""
}
log_file = "frpc.log"
log_level = ${config.logLevel}
log_max_days = ${config.logMaxDays}
admin_addr = 127.0.0.1
admin_port = 57400
tls_enable = ${config.tlsConfigEnable}
${config.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}
`
tls_cert_file = ${config.tlsConfigCertFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `
tls_key_file = ${config.tlsConfigKeyFile}
`
tls_key_file = ${config.tlsConfigKeyFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}
`
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `
tls_server_name = ${config.tlsConfigServerName}
`
tls_server_name = ${config.tlsConfigServerName}\n`
: ""
}
${
config.proxyConfigEnable
? `
http_proxy = "${config.proxyConfigProxyUrl}"
`
: ""
}
${
config.proxyConfigEnable
? `
http_proxy = "${config.proxyConfigProxyUrl}"\n`
: ""
}
log_file = "frpc.log"
log_level = ${config.logLevel}
log_max_days = ${config.logMaxDays}
admin_addr = 127.0.0.1
admin_port = ${config.webPort}
tls_enable = ${config.tlsConfigEnable}
${proxyIni.join("")}
`;
@ -316,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);
}
}
);
});
};
@ -359,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"),
@ -367,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);
@ -392,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."
);
}
};
@ -416,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();
}
};
@ -435,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;
}
};
@ -452,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",
"请先前往设置页面,修改配置后再启动"
@ -482,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",
"请先前往设置页面,修改配置后再启动"
@ -489,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,126 +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: `${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", () => {
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);
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);
if (response.statusCode === 200) {
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
logWarn(
LogModule.GITHUB,
"Failed to retrieve data, using local JSON instead. Status code: " +
response.statusCode
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
}
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
});
request.on("error", error => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
request.end();
});
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"),
@ -182,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) {
@ -215,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}`);
}
});
}
@ -227,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: "删除成功"
});
}
});
});
@ -245,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"
@ -255,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 } from "electron";
import { app, ipcMain, shell } from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
const fs = require("fs");
const path = require("path");
@ -8,24 +9,45 @@ 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) => {
logInfo(LogModule.APP, "Attempting to open log file.");
shell.openPath(logPath).then((errorMessage) => {
if (errorMessage) {
logError(LogModule.APP, `Failed to open Logger: ${errorMessage}`);
event.reply("Logger.openLog.hook", false);
} else {
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

@ -1,31 +1,43 @@
import {app, BrowserWindow, ipcMain, Menu, MenuItem, MenuItemConstructorOptions, shell, Tray} from "electron";
import {release} from "node:os";
import node_path, {join} from "node:path";
import {initGitHubApi} from "../api/github";
import {initConfigApi} from "../api/config";
import {initProxyApi} from "../api/proxy";
import {initFrpcApi, startFrpWorkerProcess, stopFrpcProcess} from "../api/frpc";
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 {
app,
BrowserWindow,
ipcMain,
Menu,
MenuItem,
MenuItemConstructorOptions,
shell,
Tray
} from "electron";
import { release } from "node:os";
import node_path, { join } from "node:path";
import { initGitHubApi } from "../api/github";
import { initConfigApi } from "../api/config";
import { initProxyApi } from "../api/proxy";
import {
initFrpcApi,
startFrpWorkerProcess,
stopFrpcProcess
} from "../api/frpc";
import { initLoggerApi } from "../api/logger";
import { initFileApi } from "../api/file";
import { getConfig } from "../storage/config";
import { initCommonApi } from "../api/common";
import { initLocalApi } from "../api/local";
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;
? 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();
@ -34,193 +46,259 @@ if (release().startsWith("6.1")) app.disableHardwareAcceleration();
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
app.quit();
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'
async function createWindow(config: FrpConfig) {
let show = true;
if (config) {
show = !config.systemSilentStartup;
}
win = new BrowserWindow({
title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
width: 800,
height: 600,
minWidth: 800,
minHeight: 600,
maxWidth: 1280,
maxHeight: 960,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false
},
show: show
});
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
win.loadURL(url);
// Open devTool if the app is not packaged
win.webContents.openDevTools();
} else {
win.loadFile(indexHtml);
}
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";
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
async function createWindow() {
win = new BrowserWindow({
title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
width: 800,
height: 600,
minWidth: 640,
minHeight: 480,
maxWidth: 1280,
maxHeight: 960,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
// 隐藏菜单栏
const { Menu } = require("electron");
Menu.setApplicationMenu(null);
// hide menu for Mac
// if (process.platform !== "darwin") {
// app.dock.hide();
// }
win.on("minimize", function (event) {
event.preventDefault();
win.hide();
});
win.on("close", function (event) {
if (!isQuiting) {
event.preventDefault();
win.hide();
if (process.platform === "darwin") {
app.dock.hide();
}
}
return false;
});
}
export const createTray = (config: FrpConfig) => {
let menu: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "显示主窗口",
click: function () {
win.show();
if (process.platform === "darwin") {
app.dock.show();
}
});
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
win.loadURL(url);
// Open devTool if the app is not packaged
win.webContents.openDevTools();
} else {
win.loadFile(indexHtml);
}
},
{
label: "退出",
click: () => {
isQuiting = true;
stopFrpcProcess(() => {
app.quit();
});
}
}
];
tray = new Tray(
node_path.join(process.env.VITE_PUBLIC, "logo/only/16x16.png")
);
tray.setToolTip("Frpc Desktop");
const contextMenu = Menu.buildFromTemplate(menu);
tray.setContextMenu(contextMenu);
// 托盘双击打开
tray.on("double-click", () => {
win.show();
});
logInfo(LogModule.APP, `Tray created successfully.`);
};
app.whenReady().then(() => {
initLog();
logInfo(
LogModule.APP,
`Application started. Current system architecture: ${
process.arch
}, platform: ${process.platform}, version: ${app.getVersion()}.`
);
getConfig((err, config) => {
if (err) {
logError(LogModule.APP, `Failed to get config: ${err.message}`);
return;
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
createWindow(config)
.then(r => {
logInfo(LogModule.APP, `Window created successfully.`);
createTray(config);
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith("https:")) shell.openExternal(url);
return {action: "deny"};
});
if (config) {
logInfo(
LogModule.APP,
`Config retrieved: ${JSON.stringify(
maskSensitiveData(config, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
// 隐藏菜单栏
const {Menu} = require("electron");
Menu.setApplicationMenu(null);
// hide menu for Mac
// if (process.platform !== "darwin") {
// app.dock.hide();
// }
win.on('minimize', function (event) {
event.preventDefault();
win.hide();
});
win.on('close', function (event) {
if (!isQuiting) {
event.preventDefault();
win.hide();
if (process.platform === "darwin") {
app.dock.hide();
}
if (config.systemStartupConnect) {
startFrpWorkerProcess(config);
}
}
return false;
});
// Initialize APIs
try {
initGitHubApi(win);
logInfo(LogModule.APP, `GitHub API initialized.`);
}
initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
export const createTray = () => {
log.info(`当前环境 platform${process.platform} arch${process.arch} appData${app.getPath("userData")} version:${app.getVersion()}`)
let menu: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
{
label: '显示主窗口', click: function () {
win.show();
if (process.platform === "darwin") {
app.dock.show();
}
}
},
{
label: '退出',
click: () => {
isQuiting = true;
stopFrpcProcess(() => {
app.quit();
})
}
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}`
);
}
];
tray = new Tray(node_path.join(process.env.VITE_PUBLIC, "logo/only/16x16.png"))
tray.setToolTip('Frpc Desktop')
const contextMenu = Menu.buildFromTemplate(menu)
tray.setContextMenu(contextMenu)
// 托盘双击打开
tray.on('double-click', () => {
win.show();
})
getConfig((err, config) => {
if (!err) {
if (config) {
if (config.systemStartupConnect) {
log.info(`已开启自动连接 正在自动连接服务器`)
startFrpWorkerProcess(config)
}
}
}
})
}
app.whenReady().then(() => {
createWindow().then(r => {
createTray()
// 初始化各个API
initGitHubApi();
initConfigApi(win);
initProxyApi();
initFrpcApi();
initLoggerApi();
initFileApi();
initCommonApi();
initLocalApi();
// initUpdaterApi(win);
})
})
.catch(error => {
logError(LogModule.APP, `Error creating window: ${error.message}`);
});
});
});
app.on("window-all-closed", () => {
win = null;
if (process.platform !== "darwin") {
stopFrpcProcess(() => {
app.quit();
})
}
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", () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
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();
win.focus();
}
});
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
createWindow();
}
});
app.on('before-quit', () => {
log.info("before-quit")
isQuiting = true;
})
// New window example arg: new windows url
ipcMain.handle("open-win", (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false
}
logInfo(LogModule.APP, `Application activated.`);
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
getConfig((err, config) => {
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.`);
});
});
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`);
} else {
childWindow.loadFile(indexHtml, {hash: arg});
}
}
});
app.on("before-quit", () => {
logInfo(LogModule.APP, `Application is about to quit.`);
stopFrpcProcess(() => {
isQuiting = true;
});
});
ipcMain.handle("open-win", (_, arg) => {
logInfo(LogModule.APP, `Opening new window with argument: ${arg}`);
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false
}
});
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,17 +1,21 @@
{
"name": "Frpc-Desktop",
"version": "1.1.0",
"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",
"proxy",
"electron-app"
"electron-app",
"vue",
"vue3",
"vite"
],
"engines": {
"node": ">= 18"
@ -26,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",
@ -65,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

@ -1,10 +1,5 @@
import {addIcon} from "@iconify/vue/dist/offline";
/**
* src/layout/index.vue
*/
// 本地菜单图标后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标
import Cloud from "@iconify-icons/material-symbols/cloud";
import RocketLaunchRounded from "@iconify-icons/material-symbols/rocket-launch-rounded";
import Download from "@iconify-icons/material-symbols/download-2";
@ -17,7 +12,6 @@ import MoreVert from "@iconify-icons/material-symbols/more-vert";
import Add from "@iconify-icons/material-symbols/add";
import BringYourOwnIpRounded from "@iconify-icons/material-symbols/bring-your-own-ip-rounded";
import DeleteRounded from "@iconify-icons/material-symbols/delete-rounded";
import RefreshRounded from "@iconify-icons/material-symbols/refresh-rounded";
import CancelPresentation from "@iconify-icons/material-symbols/cancel-presentation";
import GestureSelect from "@iconify-icons/material-symbols/gesture-select";
import SaveRounded from "@iconify-icons/material-symbols/save-rounded";
@ -35,6 +29,15 @@ import downloadRounded from "@iconify-icons/material-symbols/download-rounded";
import deviceReset from "@iconify-icons/material-symbols/device-reset";
import switchAccessOutlineRounded from "@iconify-icons/material-symbols/switch-access-outline-rounded";
import switchAccessRounded from "@iconify-icons/material-symbols/switch-access-rounded";
import chargerRounded from "@iconify-icons/material-symbols/charger-rounded";
import fileOpenRounded from "@iconify-icons/material-symbols/file-open-rounded";
import attachMoneyRounded from "@iconify-icons/material-symbols/attach-money-rounded";
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);
@ -46,11 +49,11 @@ addIcon("refresh-rounded", refreshRounded);
addIcon("more-vert", MoreVert);
addIcon("add", Add);
addIcon("bring-your-own-ip-rounded", BringYourOwnIpRounded);
addIcon("charger-rounded", chargerRounded);
addIcon("delete-rounded", DeleteRounded);
addIcon("cancel-presentation", CancelPresentation);
addIcon("gesture-select", GestureSelect);
addIcon("save-rounded", SaveRounded);
addIcon("refresh-rounded", RefreshRounded);
addIcon("info", Info);
addIcon("question-mark", QuestionMark);
addIcon("check-circle-rounded", CheckCircleRounded);
@ -65,5 +68,12 @@ addIcon("downloadRounded", downloadRounded);
addIcon("deviceReset", deviceReset);
addIcon("switchAccessOutlineRounded", switchAccessOutlineRounded);
addIcon("switchAccessRounded", switchAccessRounded);
addIcon("file-open-rounded", fileOpenRounded);
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

@ -95,7 +95,7 @@ onMounted(() => {
<div class="logo-container">
<img
src="/logo/only/128x128.png"
class="logo animate__animated animate__flip"
class="logo animate__animated animate__bounceInLeft"
alt="Logo"
/>
</div>
@ -118,14 +118,28 @@ onMounted(() => {
></IconifyIconOffline>
</li>
</ul>
<div
class="version mb-2 animate__animated"
@click="handleOpenGitHubReleases"
:data-step="guideSteps.Version?.step"
:data-intro="guideSteps.Version?.intro"
data-position="top"
>
{{ pkg.version }}
<div class="menu-footer mb-2">
<!-- <div-->
<!-- class="menu animate__animated"-->
<!-- @click="handleOpenGitHubReleases"-->
<!-- :data-step="guideSteps.Version?.step"-->
<!-- :data-intro="guideSteps.Version?.intro"-->
<!-- data-position="top"-->
<!-- >-->
<!-- <IconifyIconOffline-->
<!-- class="animate__animated"-->
<!-- icon="attach-money-rounded"-->
<!-- ></IconifyIconOffline>-->
<!-- </div>-->
<div
class="version animate__animated"
@click="handleOpenGitHubReleases"
:data-step="guideSteps.Version?.step"
:data-intro="guideSteps.Version?.intro"
data-position="top"
>
{{ pkg.version }}
</div>
</div>
</div>
</template>

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

@ -54,11 +54,18 @@ $danger-color: #F56C6C;
display: flex; /* 设置为 flexbox */
flex-direction: column; /* 纵向排列子元素 */
.menu-footer {
margin-top: auto;
}
.version {
height: 40px;
width: 60px;
display: flex;
justify-content: center;
align-items: center;
color: $primary-color;
text-align: center;
margin-top: auto;
font-weight: bold;
font-size: 14px;
cursor: pointer;
@ -68,7 +75,7 @@ $danger-color: #F56C6C;
animation: heartBeat 1s;
}
.menu-container {
.menu-container, .menu-footer {
.menu {
display: flex;

View File

@ -44,6 +44,22 @@ const handleOpenGitHub = () => {
ipcRenderer.send("common.openUrl", "https://github.com/luckjiawei/frpc-desktop")
}
/**
* 打开捐赠界面
*/
const handleOpenDonate = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/donate")
}
/**
* 打开文档
*/
const handleOpenDoc = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/p/frp")
}
/**
* 获取最后一个版本
*/
@ -124,6 +140,14 @@ defineComponent({
</p>
</div>
<div class="mt-[12px]">
<el-button plain type="success" @click="handleOpenDoc">
<IconifyIconOffline class="cursor-pointer mr-2" icon="description"/>
使用教程
</el-button>
<el-button plain type="success" @click="handleOpenDonate">
<IconifyIconOffline class="cursor-pointer mr-2" icon="volunteer-activism-sharp"/>
捐赠名单
</el-button>
<el-button plain type="primary" @click="handleOpenGitHub">
<Icon class="cursor-pointer mr-2" icon="logos:github-icon"/>
仓库地址

View File

@ -41,10 +41,19 @@ const defaultFormData = ref<FrpConfig>({
proxyConfigProxyUrl: "",
systemSelfStart: false,
systemStartupConnect: false,
systemSilentStartup: false,
user: "",
metaToken: "",
transportHeartbeatInterval: 30,
transportHeartbeatTimeout: 90
transportHeartbeatTimeout: 90,
webEnable: true,
webPort: 57400,
transportProtocol: "tcp",
transportDialServerTimeout: 10,
transportDialServerKeepalive: 7200,
transportPoolCount: 0,
transportTcpMux: true,
transportTcpMuxKeepaliveInterval: 30
});
const formData = ref<FrpConfig>(defaultFormData.value);
@ -101,11 +110,41 @@ const rules = reactive<FormRules>({
systemSelfStart: [
{ required: true, message: "请选择是否开机自启", trigger: "change" }
],
systemSilentStartup: [
{ required: true, message: "请选择是否开启静默启动", trigger: "change" }
],
systemStartupConnect: [
{ required: true, message: "请选择是否开启自动连接", trigger: "change" }
],
transportHeartbeatInterval: [
{ required: true, message: "心跳间隔时间不能为空", trigger: "change" }
],
transportHeartbeatTimeout: [
{ required: true, message: "心跳超时时间不能为空", trigger: "change" }
],
webEnable: [
{ required: true, message: "web界面开关不能为空", trigger: "change" }
],
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" }
]
});
@ -180,6 +219,50 @@ onMounted(() => {
data.transportHeartbeatTimeout =
defaultFormData.value.transportHeartbeatTimeout;
}
if (data.webEnable == null || data.webEnable == undefined) {
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;
}
}
@ -238,6 +321,15 @@ onMounted(() => {
ElMessageBox.alert(data, `提示`);
}
});
ipcRenderer.on("Config.openDataFolder.hook", (event, args) => {
if (args) {
ElMessage({
type: "success",
message: "打开数据目录成功"
});
}
});
});
const handleSelectFile = (type: number, ext: string[]) => {
@ -352,25 +444,36 @@ const handleResetConfig = () => {
});
};
/**
* 打开数据目录
*/
const handleOpenDataFolder = useDebounceFn(() => {
ipcRenderer.send("config.openDataFolder");
}, 1000);
onUnmounted(() => {
ipcRenderer.removeAllListeners("Config.getConfig.hook");
ipcRenderer.removeAllListeners("Config.saveConfig.hook");
ipcRenderer.removeAllListeners("Config.versions.hook");
ipcRenderer.removeAllListeners("Config.exportConfig.hook");
ipcRenderer.removeAllListeners("Config.clearAll.hook");
ipcRenderer.removeAllListeners("Config.openDataFolder.hook");
});
</script>
<template>
<div class="main">
<breadcrumb>
<el-button plain type="primary" @click="handleOpenDataFolder">
<IconifyIconOffline icon="folder-rounded" />
</el-button>
<el-button plain type="primary" @click="handleResetConfig">
<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" />
@ -383,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">
@ -537,6 +640,7 @@ onUnmounted(() => {
placeholder="token"
type="password"
v-model="formData.authToken"
:show-password="true"
/>
</el-form-item>
</el-col>
@ -592,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="心跳间隔:"
@ -675,11 +845,178 @@ 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="是否启用TLS" prop="tlsConfigEnable">
<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
active-text="开"
inline-prompt
@ -864,29 +1201,52 @@ onUnmounted(() => {
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<div class="h2">代理</div>
<div class="h2">Web 界面</div>
</el-col>
<el-col :span="24">
<el-form-item label="是否启动代理:" prop="proxyConfigEnable">
<el-col :span="12">
<el-form-item label="启用Web界面" prop="webEnable">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover width="300" placement="top" trigger="hover">
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
热更新等功能依赖于web界面<span
class="font-black text-[#5A3DAA]"
>不可停用Web</span
>
</el-popover>
</div>
启用Web
</template>
<el-switch
active-text="开"
inline-prompt
disabled
inactive-text="关"
v-model="formData.proxyConfigEnable"
v-model="formData.webEnable"
/>
</el-form-item>
</el-col>
<template v-if="formData.proxyConfigEnable">
<el-col :span="24">
<el-form-item label="代理地址:" prop="proxyConfigProxyUrl">
<template v-if="formData.webEnable">
<el-col :span="12">
<el-form-item label="Web 端口:" prop="webPort">
<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
>
>webServer.port</span
><br />
自行保证端口没有被占用否则会导致启动失败
</template>
<template #reference>
<IconifyIconOffline
@ -897,12 +1257,16 @@ onUnmounted(() => {
</template>
</el-popover>
</div>
代理地址
Web 端口
</template>
<el-input
v-model="formData.proxyConfigProxyUrl"
placeholder="http://user:pwd@192.168.1.128:8080"
/>
<el-input-number
placeholder="57400"
v-model="formData.webPort"
:min="0"
:max="65535"
controls-position="right"
class="w-full"
></el-input-number>
</el-form-item>
</el-col>
</template>
@ -915,7 +1279,7 @@ onUnmounted(() => {
<el-select v-model="formData.logLevel">
<el-option label="info" value="info" />
<el-option label="debug" value="debug" />
<el-option label="waring" value="waring" />
<el-option label="warn" value="warn" />
<el-option label="error" value="error" />
</el-select>
</el-form-item>
@ -932,7 +1296,7 @@ onUnmounted(() => {
<el-col :span="24">
<div class="h2">系统配置</div>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="开机自启:" prop="systemSelfStart">
<template #label>
<div class="h-full flex items-center mr-1">
@ -962,7 +1326,36 @@ onUnmounted(() => {
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="静默启动:" prop="systemSilentStartup">
<template #label>
<div class="h-full flex items-center mr-1">
<el-popover placement="top" trigger="hover">
<template #default>
开启后启动时<span class="font-black text-[#5A3DAA]"
>不打开界面</span
>
</template>
<template #reference>
<IconifyIconOffline
class="text-base"
color="#5A3DAA"
icon="info"
/>
</template>
</el-popover>
</div>
静默启动
</template>
<el-switch
active-text="开"
inline-prompt
inactive-text="关"
v-model="formData.systemSilentStartup"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="自动连接:" prop="systemStartupConnect">
<template #label>
<div class="h-full flex items-center mr-1">

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

@ -1,7 +1,10 @@
<script lang="ts" setup>
import {defineComponent, onMounted, onUnmounted, ref} from "vue";
import { createVNode, defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import {ipcRenderer} from "electron";
import { ipcRenderer } from "electron";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { useDebounce, useDebounceFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
defineComponent({
name: "Logger"
@ -11,25 +14,30 @@ const loggerContent = ref('<div class="text-white">暂无日志</div>');
const handleLog2Html = (logContent: string) => {
const logs = logContent
.split("\n")
.filter(f => f)
.map(m => {
if (m.indexOf("[E]") !== -1) {
return `<div class="text-[#FF0006]">${m}</div> `;
} else if (m.indexOf("[I]") !== -1) {
return `<div class="text-[#48BB31]">${m}</div> `;
} else if (m.indexOf("[D]") !== -1) {
return `<div class="text-[#0070BB]">${m}</div> `;
} else if (m.indexOf("[W]") !== -1) {
return `<div class="text-[#BBBB23]">${m}</div> `;
} else {
return `<div class="text-[#BBBBBB]">${m}</div> `;
}
});
.split("\n")
.filter(f => f)
.map(m => {
if (m.indexOf("[E]") !== -1) {
return `<div class="text-[#FF0006]">${m}</div> `;
} else if (m.indexOf("[I]") !== -1) {
return `<div class="text-[#48BB31]">${m}</div> `;
} else if (m.indexOf("[D]") !== -1) {
return `<div class="text-[#0070BB]">${m}</div> `;
} else if (m.indexOf("[W]") !== -1) {
return `<div class="text-[#BBBB23]">${m}</div> `;
} else {
return `<div class="text-[#BBBBBB]">${m}</div> `;
}
});
return logs.reverse().join("");
};
const refreshStatus = ref(false);
const logLoading = ref(true);
onMounted(() => {
console.log('logger mounted')
ipcRenderer.send("logger.getLog");
ipcRenderer.on("Logger.getLog.hook", (event, args) => {
// console.log("", args, args.indexOf("\n"));
@ -38,26 +46,69 @@ onMounted(() => {
if (args) {
loggerContent.value = handleLog2Html(args);
}
ipcRenderer.send("logger.update");
logLoading.value = false;
if (refreshStatus.value) {
//
ElMessage({
type: "success",
message: "刷新成功"
});
} else {
ipcRenderer.send("logger.update");
}
});
ipcRenderer.on("Logger.update.hook", (event, args) => {
console.log("logger update hook", 1);
if (args) {
loggerContent.value = handleLog2Html(args);
}
});
ipcRenderer.on("Logger.openLog.hook", (event, args) => {
if (args) {
ElMessage({
type: "success",
message: "打开日志成功"
});
}
});
});
const openLocalLog = useDebounceFn(() => {
ipcRenderer.send("logger.openLog");
}, 1000);
const refreshLog = useDebounceFn(() => {
// ElMessage({
// type: "warning",
// icon: "<IconifyIconOffline icon=\"file-open-rounded\" />",
// message: "..."
// });
refreshStatus.value = true;
logLoading.value = true;
ipcRenderer.send("logger.getLog");
}, 300);
onUnmounted(() => {
console.log('logger unmounted')
ipcRenderer.removeAllListeners("Logger.getLog.hook");
ipcRenderer.removeAllListeners("Logger.openLog.hook");
});
</script>
<template>
<div class="main">
<breadcrumb/>
<div class="app-container-breadcrumb">
<breadcrumb>
<el-button plain type="primary" @click="refreshLog">
<IconifyIconOffline icon="refresh-rounded" />
</el-button>
<el-button plain type="primary" @click="openLocalLog">
<IconifyIconOffline icon="file-open-rounded" />
</el-button>
</breadcrumb>
<div class="app-container-breadcrumb" v-loading="logLoading">
<div
class="w-full h-full p-2 bg-[#2B2B2B] rounded drop-shadow-lg overflow-y-auto"
v-html="loggerContent"
class="w-full h-full p-2 bg-[#2B2B2B] rounded drop-shadow-lg overflow-y-auto"
v-html="loggerContent"
></div>
</div>
</div>

View File

@ -1,5 +1,14 @@
[
{
"value": "127.0.0.11"
"value": "127.0.0.1"
},
{
"value": "192.168.1.1"
},
{
"value": "192.168.0.1"
},
{
"value": "192.168.5.1"
}
]

File diff suppressed because it is too large Load Diff

25
types/global.d.ts vendored
View File

@ -18,15 +18,25 @@ declare global {
name: string;
type: string;
localIp: string;
localPort: number;
remotePort: number;
localPort: any;
remotePort: string;
customDomains: string[];
stcpModel: string;
serverName: string;
secretKey: string;
bindAddr: string;
bindPort: number;
status: boolean
status: boolean;
subdomain: string;
basicAuth: boolean;
httpUser: string;
httpPassword: string;
fallbackTo: string;
fallbackTimeoutMs: number;
https2http: boolean;
https2httpCaFile: string;
https2httpKeyFile: string;
keepTunnelOpen: boolean;
};
/**
@ -72,10 +82,19 @@ declare global {
proxyConfigProxyUrl: string;
systemSelfStart: boolean;
systemStartupConnect: boolean;
systemSilentStartup: boolean;
user: string;
metaToken: string;
transportHeartbeatInterval: number;
transportHeartbeatTimeout: number;
webEnable: boolean;
webPort: number;
transportProtocol: string;
transportDialServerTimeout: number;
transportDialServerKeepalive: number;
transportPoolCount: number;
transportTcpMux: boolean;
transportTcpMuxKeepaliveInterval: number;
};
type GitHubMirror = {