Compare commits

...

31 Commits

Author SHA1 Message Date
刘嘉伟
2238afa420 🏗️ refactor ServerService and index.vue to enhance HTTPS to HTTP handling and improve local file selection process 2025-02-26 17:12:43 +08:00
刘嘉伟
50adc33aef 🏗️ refactor ServerService to enhance HTTP proxy configuration handling and include basic authentication support 2025-02-26 16:45:18 +08:00
刘嘉伟
e6992f4e4c 🏗️ refactor config view to improve state management and enhance visibility handling in index.vue 2025-02-26 16:19:24 +08:00
刘嘉伟
dfbd9c93bd 🏗️ refactor ProxyService to integrate FrpcProcessService and reload process on proxy modifications in electron service 2025-02-26 16:12:11 +08:00
刘嘉伟
62151d613e 🏗️ refactor proxy handling to improve configuration generation and enhance visitors model management in ServerService 2025-02-26 15:58:48 +08:00
刘嘉伟
20f1fe8318 🏗️ refactor proxy handling to replace stcpModel with visitorsModel and enhance configuration structure in ServerService 2025-02-26 14:56:51 +08:00
刘嘉伟
f0d8c20f4d 🏗️ refactor proxy handling to replace stcpModel with visitorsModel and enhance configuration structure in ServerService 2025-02-26 14:56:10 +08:00
刘嘉伟
f77605a73d 🏗️ refactor file selection handling to improve response structure and utilize path data in SystemController 2025-02-26 12:31:57 +08:00
刘嘉伟
ce709fe5e7 🏗️ refactor file selection handling to improve response structure and utilize path data in SystemController 2025-02-26 12:21:40 +08:00
刘嘉伟
7515726a71 🏗️ refactor file selection handling to improve response structure and utilize path data in SystemController 2025-02-26 11:49:55 +08:00
刘嘉伟
1882260096 🏗️ refactor file selection handling to improve response structure and utilize path data in SystemController 2025-02-25 23:19:02 +08:00
刘嘉伟
91afd0f457 🏗️ refactor logger component to handle empty log data and improve error resolution in SystemService 2025-02-25 23:09:53 +08:00
刘嘉伟
efd68fb453 🏗️ refactor error handling in controllers to utilize BusinessError and enhance response structure 2025-02-25 23:01:57 +08:00
刘嘉伟
fb2846c0b5 🏗️ refactor error handling in controllers and integrate logging functionality 2025-02-25 17:55:13 +08:00
刘嘉伟
cd5122c6cf 🏗️ refactor config handling and implement TOML import functionality 2025-02-25 17:37:52 +08:00
刘嘉伟
7da8bf025b 🏗️ refactor file selection handling and enhance export configuration process 2025-02-25 17:09:14 +08:00
刘嘉伟
7a58500a92 🏗️ refactor frpc process management and integrate Pinia for state handling 2025-02-25 15:57:11 +08:00
刘嘉伟
711af4a31a 🏗️ refactor UI components and enhance form handling with lodash integration 2025-02-25 15:14:54 +08:00
刘嘉伟
64c9623019 🏗️ refactor version import handling and improve IPC communication 2025-02-25 14:37:31 +08:00
刘嘉伟
3373e30331 🏗️ refactor IPC listener management and enhance logging response handling 2025-02-25 14:15:18 +08:00
刘嘉伟
20a8208240 🏗️ refactor response handling to use ResponseUtils and improve code consistency 2025-02-25 12:37:16 +08:00
刘嘉伟
b5fd0c6747 🏗️ enhance logging functionality and add SCSS support in configuration 2025-02-25 12:32:48 +08:00
刘嘉伟
9946b50d5d 🏗️ refactor data access layer and enhance service architecture 2025-02-25 11:57:54 +08:00
刘嘉伟
ff8b01c360 🏗️ refactor configuration management and implement system service features 2025-02-24 16:38:39 +08:00
刘嘉伟
6d9f9269b7 🏗️ refactor FrpcProcessService and enhance process management 2025-02-24 14:35:11 +08:00
刘嘉伟
9f46ea781d 🏗️ implement proxy management features and refactor related services 2025-02-23 21:07:44 +08:00
刘嘉伟
2edbbcb871 🐛 fix version handling and improve IPC communication for downloads 2025-02-23 02:11:17 +08:00
刘嘉伟
8d56faeb80 🚧 v2 2025-02-21 18:30:13 +08:00
刘嘉伟
3ef92a8af9 🏗️ lower electron version 2025-02-18 17:52:09 +08:00
刘嘉伟
183a86d10b 📌 lower electron version 2025-02-13 14:50:12 +08:00
刘嘉伟
5451f5bc70 🐛 修复window下路径的问题 2025-02-13 14:49:44 +08:00
70 changed files with 4052 additions and 3560 deletions

View File

@ -24,5 +24,6 @@ jobs:
- name: Build
run: |
npm install
npx run release
npm i -g pnpm
pnpm install
pnpm build:electron

View File

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

View File

@ -1,21 +0,0 @@
import {dialog, ipcMain} from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
export const initFileApi = () => {
ipcMain.handle("file.selectFile", async (event, args) => {
logInfo(LogModule.APP, `Attempting to open file dialog with filters: ${JSON.stringify(args)}`);
try {
const result = dialog.showOpenDialogSync({
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

@ -1,722 +0,0 @@
import { app, ipcMain, Notification } from "electron";
import { getConfig } from "../storage/config";
import { listProxy } from "../storage/proxy";
import { getVersionById } from "../storage/version";
import treeKill from "tree-kill";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
const fs = require("fs");
const path = require("path");
const { exec, spawn } = require("child_process");
export let frpcProcess = null;
const runningCmd = {
commandPath: null,
configPath: null
};
let frpcStatusListener = null;
const getFrpcVersionWorkerPath = (
versionId: number,
callback: (workerPath: string) => void
) => {
getVersionById(versionId, (err2, version) => {
if (!err2) {
if (version) {
callback(version["frpcVersionPath"]);
}
}
});
};
const isRangePort = (m: Proxy) => {
return (
(m.type === "tcp" || m.type === "udp") &&
(String(m.localPort).indexOf("-") !== -1 ||
String(m.localPort).indexOf(",") !== -1)
);
};
/**
* toml配置文件
* @param config
* @param proxys
*/
export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => {
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":
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}\n`;
}
break;
case "http":
case "https":
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}"
bindAddr = "${m.bindAddr}"
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}\n`;
}
toml += `secretKey="${m.secretKey}"\n`;
break;
default:
break;
}
if (rangePort) {
toml += `{{- end }}\n`;
}
return toml;
});
const toml = `serverAddr = "${config.serverAddr}"
serverPort = ${config.serverPort}
${
config.authMethod === "token"
? `auth.method = "token"
auth.token = "${config.authToken}"`
: ""
}
${
config.authMethod === "multiuser"
? `user = "${config.user}"
metadatas.token = "${config.metaToken}"`
: ""
}
log.to = "frpc.log"
log.level = "${config.logLevel}"
log.maxDays = ${config.logMaxDays}
webServer.addr = "127.0.0.1"
webServer.port = ${config.webPort}
${
config.transportProtocol
? `transport.protocol = "${config.transportProtocol}"`
: ""
}
${
config.transportPoolCount
? `transport.poolCount = ${config.transportPoolCount}`
: ""
}
${
config.transportDialServerTimeout
? `transport.dialServerTimeout = ${config.transportDialServerTimeout}`
: ""
}
${
config.transportDialServerKeepalive
? `transport.dialServerKeepalive = ${config.transportDialServerKeepalive}`
: ""
}
${
config.transportHeartbeatInterval
? `transport.heartbeatInterval = ${config.transportHeartbeatInterval}`
: ""
}
${
config.transportHeartbeatTimeout
? `transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}`
: ""
}
${config.transportTcpMux ? `transport.tcpMux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `transport.tcpMuxKeepaliveInterval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
transport.tls.enable = ${config.tlsConfigEnable}
${
config.tlsConfigEnable && config.tlsConfigCertFile
? `transport.tls.certFile = "${config.tlsConfigCertFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `transport.tls.keyFile = "${config.tlsConfigKeyFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `transport.tls.serverName = "${config.tlsConfigServerName}"`
: ""
}
${
config.proxyConfigEnable
? `transport.proxyURL = "${config.proxyConfigProxyUrl}"`
: ""
}
${proxyToml.join("")}`;
return toml;
};
/**
* ini配置
* @param config
* @param proxys
*/
export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => {
const rangePort = isRangePort(m);
let ini = `[${rangePort ? "range:" : ""}${m.name}]
type = "${m.type}"
`;
switch (m.type) {
case "tcp":
case "udp":
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
remote_port = ${m.remotePort}\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}\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}\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}\n`;
}
ini += `
sk="${m.secretKey}"\n`;
break;
default:
break;
}
return ini;
});
const ini = `
[common]
server_addr = ${config.serverAddr}
server_port = ${config.serverPort}
${
config.authMethod === "token"
? `
authentication_method = ${config.authMethod}
token = ${config.authToken}\n`
: ""
}
${
config.authMethod === "multiuser"
? `
user = ${config.user}
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
? `
heartbeat_interval = ${config.transportHeartbeatInterval}
`
: ""
}
${
config.transportHeartbeatTimeout
? `
heartbeat_timeout = ${config.transportHeartbeatTimeout}
`
: ""
}
${config.transportTcpMux ? `transport.tcp_mux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `tcp_mux_keepalive_interval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigCertFile
? `
tls_cert_file = ${config.tlsConfigCertFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `
tls_key_file = ${config.tlsConfigKeyFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `
tls_server_name = ${config.tlsConfigServerName}\n`
: ""
}
${
config.proxyConfigEnable
? `
http_proxy = "${config.proxyConfigProxyUrl}"\n`
: ""
}
log_file = "frpc.log"
log_level = ${config.logLevel}
log_max_days = ${config.logMaxDays}
admin_addr = 127.0.0.1
admin_port = ${config.webPort}
tls_enable = ${config.tlsConfigEnable}
${proxyIni.join("")}
`;
return ini;
};
/**
*
*/
export const generateConfig = (
config: FrpConfig,
callback: (configPath: string) => void
) => {
listProxy((err3, proxys) => {
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);
}
}
);
});
};
/**
* frpc子进程
* @param cwd
* @param commandPath
* @param configPath
*/
const startFrpcProcess = (commandPath: string, configPath: string) => {
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"),
shell: true
});
runningCmd.commandPath = commandPath;
runningCmd.configPath = configPath;
frpcProcess.stdout.on("data", data => {
logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
});
frpcProcess.stdout.on("error", data => {
logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`);
stopFrpcProcess(() => {});
});
frpcStatusListener = setInterval(() => {
const status = frpcProcessStatus();
logDebug(
LogModule.FRP_CLIENT,
`Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
);
if (!status) {
new Notification({
title: "Frpc Desktop",
body: "Connection lost, please check the logs for details."
}).show();
logError(
LogModule.FRP_CLIENT,
"Frpc process status check failed. Connection lost."
);
clearInterval(frpcStatusListener);
}
}, 3000);
};
/**
* frpc配置
*/
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}`;
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."
);
}
};
/**
* frpc子进程
*/
export const stopFrpcProcess = (callback?: () => void) => {
if (frpcProcess) {
treeKill(frpcProcess.pid, (error: Error) => {
if (error) {
logError(
LogModule.FRP_CLIENT,
`Failed to stop frpc process with pid: ${frpcProcess.pid}. Error: ${error.message}`
);
callback();
} else {
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();
}
};
/**
* frpc子进程状态
*/
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;
}
};
/**
* frpc流程
* @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",
"请先前往设置页面,修改配置后再启动"
);
return;
}
if (!config.currentVersion) {
logWarn(
LogModule.FRP_CLIENT,
"Current version not set in configuration. Prompting user."
);
event.reply(
"Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动"
);
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,598 +0,0 @@
import electron, {
app,
dialog,
BrowserWindow,
ipcMain,
net,
shell
} from "electron";
import {
deleteVersionById,
getVersionById,
insertVersion,
listVersion
} from "../storage/version";
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const { download } = require("electron-dl");
const AdmZip = require("adm-zip");
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"],
win32_arm64: ["window", "arm64"],
win32_ia32: ["window", "386"],
darwin_arm64: ["darwin", "arm64"],
darwin_x64: ["darwin", "amd64"],
darwin_amd64: ["darwin", "amd64"],
linux_x64: ["linux", "amd64"],
linux_arm64: ["linux", "arm64"]
};
const platform = process.platform;
const arch = process.arch;
let currArch = `${platform}_${arch}`;
const frpArch = versionRelation[currArch];
const unTarGZ = (tarGzPath: string, targetPath: string) => {
const tar = require("tar");
const unzip = zlib.createGunzip();
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, mode: 0o777 });
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
}
readStream.on("error", err => {
logError(LogModule.APP, `Error reading tar.gz file: ${err.message}`);
});
readStream
.pipe(unzip)
.on("error", err => {
logError(LogModule.APP, `Error during gunzip: ${err.message}`);
})
.pipe(
tar
.extract({
cwd: targetPath,
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) => {
if (!fs.existsSync(path.join(targetPath, path.basename(zipPath, ".zip")))) {
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")}`
);
}
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);
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 = win => {
// 版本
let versions: FrpVersion[] = [];
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 } = 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 = 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) {
logError(
LogModule.GITHUB,
`No adaptive asset found for version ID: ${versionId}`
);
}
return asset;
};
/**
* 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.");
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, mirror: string) => {
const { api } = conventMirrorUrl(mirror);
const mirrorUrl = api;
logInfo(LogModule.GITHUB, `Requesting mirror URL: ${mirrorUrl}`);
const request = net.request({
method: "get",
url: `${mirrorUrl}/repos/fatedier/frp/releases?page=1&per_page=1000`
});
let githubReleaseJsonStr = null;
request.on("response", response => {
logInfo(
LogModule.GITHUB,
`Received response with status code: ${response.statusCode}`
);
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
if (response.statusCode === 200) {
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
logWarn(
LogModule.GITHUB,
"Failed to retrieve data, using local JSON instead. Status code: " +
response.statusCode
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
}
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
});
request.on("error", error => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
request.end();
});
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 = getVersionByGithubVersionId(versionId);
const asset = getAdaptiveAsset(versionId);
const { browser_download_url } = asset;
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}`
);
await download(BrowserWindow.getFocusedWindow(), url, {
filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"),
onProgress: progress => {
event.reply("Download.frpVersionDownloadOnProgress", {
id: versionId,
progress: progress
});
logDebug(
LogModule.GITHUB,
`Download progress for versionId: ${versionId} is ${
progress.percent * 100
}%`
);
},
onCompleted: () => {
logInfo(
LogModule.GITHUB,
`Download completed for versionId: ${versionId}, asset: ${asset.name}`
);
const frpcVersionPath = decompressFrp(
asset.name,
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
)
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
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}`);
}
});
}
});
});
/**
*
*/
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, () => {
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) => {
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: "删除成功"
});
}
});
});
/**
*
*/
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"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
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,108 +0,0 @@
import {ipcMain} from "electron";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const {exec, spawn} = require("child_process");
type LocalPort = {
protocol: string;
ip: string;
port: number;
}
export const initLocalApi = () => {
const command = process.platform === 'win32'
? 'netstat -a -n'
: 'netstat -an | grep LISTEN';
ipcMain.on("local.getLocalPorts", async (event, args) => {
logInfo(LogModule.APP, "Starting to retrieve local ports");
// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
logError(LogModule.APP, `getLocalPorts - error: ${error.message}`);
return;
}
if (stderr) {
logWarn(LogModule.APP, `getLocalPorts - stderr: ${stderr}`);
return;
}
logDebug(LogModule.APP, `Command output: ${stdout}`);
let ports = [];
if (stdout) {
if (process.platform === 'win32') {
// window
ports = stdout.split('\r\n')
.filter(f => f.indexOf('TCP') !== -1 || f.indexOf('UDP') !== -1)
.map(m => {
const cols = m.split(' ')
.filter(f => f != '')
const local = cols[1]
const s = local.lastIndexOf(":")
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
}
return singe;
})
} else if (process.platform === 'darwin') {
// mac
ports = stdout.split('\n')
.filter(m => {
const cols = m.split(' ')
.filter(f => f != '')
const local = cols[3]
return local
})
.map(m => {
const cols = m.split(' ')
.filter(f => f != '')
const local = cols[3]
const s = local.lastIndexOf(".")
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
}
return singe;
})
} else if (process.platform === 'linux') {
ports = stdout.split('\n')
.filter(f =>
f.indexOf('tcp') !== -1||
f.indexOf('tcp6') !== -1||
f.indexOf('udp') !== -1 ||
f.indexOf('udp6') !== -1
).map(m => {
const cols = m.split(' ')
.filter(f => f != '')
const local = cols[3]
const s = local.lastIndexOf(":")
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
}
return singe;
})
}
}
ports.sort((a, b) => a.port - b.port);
event.reply("local.getLocalPorts.hook", {
data: ports
});
});
});
}

View File

@ -1,53 +0,0 @@
import { app, ipcMain, shell } from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
const fs = require("fs");
const path = require("path");
export const initLoggerApi = () => {
const logPath = path.join(app.getPath("userData"), "frpc.log");
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

@ -1,115 +0,0 @@
import { ipcMain } from "electron";
import {
deleteProxyById,
getProxyById,
insertProxy,
listProxy,
updateProxyById,
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
});
});
});
ipcMain.on("proxy.insertProxy", async (event, args) => {
delete args["_id"];
logInfo(LogModule.APP, "Inserting a new proxy.");
insertProxy(args, (err, documents) => {
if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy inserted successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.insertProxy.hook", {
err: err,
data: documents
});
});
});
ipcMain.on("proxy.deleteProxyById", async (event, args) => {
logInfo(LogModule.APP, `Deleting proxy with ID: ${args._id}`);
deleteProxyById(args, (err, documents) => {
if (err) {
logError(LogModule.APP, `Error deleting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy deleted successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.deleteProxyById.hook", {
err: err,
data: documents
});
});
});
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
});
});
});
ipcMain.on("proxy.updateProxy", async (event, args) => {
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) {
logError(LogModule.APP, `Error updating proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy updated successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.updateProxy.hook", {
err: err,
data: documents
});
});
});
ipcMain.on("proxy.updateProxyStatus", async (event, args) => {
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) {
logError(LogModule.APP, `Error updating proxy status: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy status updated successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.updateProxyStatus.hook", {
err: err,
data: documents
});
});
});
};

View File

@ -1,81 +0,0 @@
import {app, dialog, autoUpdater, BrowserWindow} from "electron";
const log = require('electron-log');
export const initUpdaterApi = (win: BrowserWindow) => {
//更新测试打开
Object.defineProperty(app, 'isPackaged', {
get() {
return true;
}
});
const server = 'https://hazel-git-master-uiluck.vercel.app'
let packageName = null
const platform = process.platform;
const arch = process.arch;
switch (platform) {
case "darwin":
if (arch == "arm64") {
packageName = "darwin_arm64";
} else {
packageName = "darwin";
}
break;
case "win32":
packageName = "exe";
break;
case "linux":
packageName = "AppImage";
if (arch == "arm64") {
packageName = "AppImage_arm64";
} else {
packageName = "AppImage";
}
break;
}
const url = `${server}/update/${packageName}/${app.getVersion()}`
log.info(`开启自动更新 ${url}`);
autoUpdater.setFeedURL({url: url})
autoUpdater.on('checking-for-update', () => {
log.info("正在检查更新")
})
autoUpdater.on('update-available', (event, info) => {
log.info(`发现新版本`)
})
autoUpdater.on('update-not-available', () => {
log.info('没有可用的更新')
})
autoUpdater.on('error', (err) => {
log.error(`更新错误:${err.message}`)
})
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
type: 'info',
title: '应用更新',
message: '发现新版本,是否更新?',
buttons: ['是', '否']
}).then((buttonIndex) => {
if (buttonIndex.response == 0) { //选择是,则退出程序,安装新版本
autoUpdater.quitAndInstall()
app.quit()
}
})
})
// setInterval(() => {
// log.initialize("定时检查更新")
// // autoUpdater.checkForUpdates();
// }, 60000)
autoUpdater.checkForUpdates();
log.info("手动检查更新一次")
}

View File

@ -0,0 +1,21 @@
class BaseController {
// success<T>(data: any, message?: string) {
// const resp: ApiResponse<T> = {
// success: true,
// data: data,
// message: message || "successful."
// };
// return resp;
// }
//
// fail(message?: string) {
// const resp: ApiResponse<any> = {
// success: false,
// data: null,
// message: message || "internal error."
// };
// return resp;
// }
}
export default BaseController;

View File

@ -0,0 +1,175 @@
import BaseController from "./BaseController";
import ServerService from "../service/ServerService";
import PathUtils from "../utils/PathUtils";
import fs from "fs";
import FrpcProcessService from "../service/FrpcProcessService";
import SystemService from "../service/SystemService";
import moment from "moment";
import ResponseUtils from "../utils/ResponseUtils";
import { BrowserWindow, dialog } from "electron";
import Logger from "../core/Logger";
import BeanFactory from "../core/BeanFactory";
class ConfigController extends BaseController {
private readonly _serverService: ServerService;
private readonly _systemService: SystemService;
private readonly _frpcProcessService: FrpcProcessService;
constructor(
serverService: ServerService,
systemService: SystemService,
frpcProcessService: FrpcProcessService
) {
super();
this._serverService = serverService;
this._systemService = systemService;
this._frpcProcessService = frpcProcessService;
}
saveConfig(req: ControllerParam) {
this._serverService
.saveServerConfig(req.args)
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("ConfigController.saveConfig", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
getServerConfig(req: ControllerParam) {
this._serverService
.getServerConfig()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ConfigController.getServerConfig", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
openAppData(req: ControllerParam) {
this._systemService
.openLocalPath(PathUtils.getAppData())
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ConfigController.openAppData", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
resetAllConfig(req: ControllerParam) {
// await this._serverDao.truncate();
// await this._proxyDao.truncate();
// await this._versionDao.truncate();
this._frpcProcessService
.stopFrpcProcess()
.then(() => {
fs.rmSync(PathUtils.getDataBaseStoragePath(), {
recursive: true,
force: true
});
fs.rmSync(PathUtils.getDownloadStoragePath(), {
recursive: true,
force: true
});
fs.rmSync(PathUtils.getVersionStoragePath(), {
recursive: true,
force: true
});
fs.rmSync(PathUtils.getFrpcLogStoragePath(), {
recursive: true,
force: true
});
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("ConfigController.resetAllConfig", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
exportConfig(req: ControllerParam) {
dialog
.showOpenDialog({
properties: ["openDirectory"]
})
.then(result => {
if (result.canceled) {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: true,
path: ""
})
);
} else {
const path = `${result.filePaths[0]}/frpc-${moment(new Date()).format(
"YYYYMMDDhhmmss"
)}.toml`;
this._serverService.genTomlConfig(path).then(() => {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: false,
path: path
})
);
});
}
})
.catch((err: Error) => {
Logger.error("ConfigController.exportConfig", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
importTomlConfig(req: ControllerParam) {
const win: BrowserWindow = BeanFactory.getBean("win");
dialog
.showOpenDialog(win, {
properties: ["openFile"],
filters: [{ name: "Frpc Toml ConfigFile", extensions: ["toml"] }]
})
.then(result => {
if (result.canceled) {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: true,
path: ""
})
);
} else {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: false,
path: ""
})
);
}
});
// if (result.canceled) {
// } else {
// }
// this._serverService
// .importTomlConfig()
// .then(() => {
// req.event.reply(req.channel, ResponseUtils.success());
// })
// .catch((err: Error) => {
// Logger.error("ConfigController.importTomlConfig", err);
// req.event.reply(req.channel, ResponseUtils.fail(err));
// });
}
}
export default ConfigController;

View File

@ -0,0 +1,44 @@
import BaseController from "./BaseController";
import FrpcProcessService from "../service/FrpcProcessService";
import ResponseUtils from "../utils/ResponseUtils";
import Logger from "../core/Logger";
class LaunchController extends BaseController {
private readonly _frpcProcessService: FrpcProcessService;
constructor(frpcProcessService: FrpcProcessService) {
super();
this._frpcProcessService = frpcProcessService;
}
launch(req: ControllerParam) {
this._frpcProcessService
.startFrpcProcess()
.then(r => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("LaunchController.launch", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
terminate(req: ControllerParam) {
this._frpcProcessService
.stopFrpcProcess()
.then(r => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch(err => {
Logger.error("LaunchController.terminate", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
getStatus(req: ControllerParam) {
const running = this._frpcProcessService.isRunning();
req.event.reply(req.channel, ResponseUtils.success(running));
}
}
export default LaunchController;

View File

@ -0,0 +1,50 @@
import BaseController from "./BaseController";
import LogService from "../service/LogService";
import ResponseUtils from "../utils/ResponseUtils";
import Logger from "../core/Logger";
class LogController extends BaseController {
private readonly _logService: LogService;
constructor(logService: LogService) {
super();
this._logService = logService;
}
getFrpLogContent(req: ControllerParam) {
this._logService
.getFrpLogContent()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("LogController.getFrpLogContent", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
// watchFrpcLogContent(req: ControllerRequest) {
// this._logService.watchFrpcLog().then(data => {
// console.log('reply watch', data);
// req.event.reply(req.reply, this.ResponseUtils.success(data));
// });
// }
openFrpcLogFile(req: ControllerParam) {
this._logService
.openFrpcLogFile()
.then(data => {
if (data) {
ResponseUtils.success();
} else {
// ResponseUtils.fail();
}
})
.catch((err: Error) => {
Logger.error("LogController.openFrpcLogFile", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
}
export default LogController;

View File

@ -0,0 +1,90 @@
import BaseController from "./BaseController";
import ProxyService from "../service/ProxyService";
import ResponseUtils from "../utils/ResponseUtils";
import ProxyRepository from "../repository/ProxyRepository";
import Logger from "../core/Logger";
class ProxyController extends BaseController {
private readonly _proxyService: ProxyService;
private readonly _proxyDao: ProxyRepository;
constructor(proxyService: ProxyService, proxyDao: ProxyRepository) {
super();
this._proxyService = proxyService;
this._proxyDao = proxyDao;
}
createProxy(req: ControllerParam) {
this._proxyService
.insertProxy(req.args)
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ProxyController.createProxy", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
modifyProxy(req: ControllerParam) {
this._proxyService
.updateProxy(req.args)
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ProxyController.modifyProxy", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
getAllProxies(req: ControllerParam) {
this._proxyDao
.findAll()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ProxyController.getAllProxies", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
deleteProxy(req: ControllerParam) {
this._proxyService
.deleteProxy(req.args)
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ProxyController.deleteProxy", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
modifyProxyStatus(req: ControllerParam) {
this._proxyDao
.updateProxyStatus(req.args.id, req.args.status)
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("ProxyController.modifyProxyStatus", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
getLocalPorts(req: ControllerParam) {
this._proxyService
.getLocalPorts()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("ProxyController.getLocalPorts", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
}
export default ProxyController;

View File

@ -0,0 +1,90 @@
import SystemService from "../service/SystemService";
import ResponseUtils from "../utils/ResponseUtils";
import PathUtils from "../utils/PathUtils";
import { BrowserWindow, dialog } from "electron";
import BeanFactory from "../core/BeanFactory";
import Logger from "../core/Logger";
class SystemController {
private readonly _systemService: SystemService;
constructor(systemService: SystemService) {
this._systemService = systemService;
}
openUrl(req: ControllerParam) {
this._systemService
.openUrl(req.args.url)
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("SystemController.openUrl", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
relaunchApp(req: ControllerParam) {
this._systemService
.relaunch()
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("SystemController.relaunchApp", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
openAppData(req: ControllerParam) {
this._systemService
.openLocalPath(PathUtils.getAppData())
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("SystemController.openAppData", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
selectLocalFile(req: ControllerParam) {
const { name, extensions } = req.args;
if (!extensions || extensions.length === 0) {
return;
// req.event.reply(req.channel, ResponseUtils.fail("可选择扩展名不能为空"));
}
const win: BrowserWindow = BeanFactory.getBean("win");
dialog
.showOpenDialog(win, {
properties: ["openFile"],
filters: [{ name: name, extensions: extensions }]
})
.then(result => {
if (result.canceled) {
// todo canceled
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: true,
path: ""
})
);
} else {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: false,
path: result.filePaths[0]
})
);
}
})
.catch((err: Error) => {
Logger.error("SystemController.selectLocalFile", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
}
export default SystemController;

View File

@ -0,0 +1,141 @@
import BaseController from "./BaseController";
import VersionService from "../service/VersionService";
import ResponseUtils from "../utils/ResponseUtils";
import VersionRepository from "../repository/VersionRepository";
import Logger from "../core/Logger";
import { BrowserWindow, dialog } from "electron";
import BeanFactory from "../core/BeanFactory";
class VersionController extends BaseController {
private readonly _versionService: VersionService;
private readonly _versionDao: VersionRepository;
constructor(versionService: VersionService, versionDao: VersionRepository) {
super();
this._versionService = versionService;
this._versionDao = versionDao;
}
getVersions(req: ControllerParam) {
this._versionService
.getFrpVersionsByGitHub()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch(err => {
Logger.error("VersionController.getVersions", err);
this._versionService.getFrpVersionByLocalJson().then(localData => {
req.event.reply(req.channel, ResponseUtils.success(localData));
});
});
}
getDownloadedVersions(req: ControllerParam) {
this._versionDao
.findAll()
.then(data => {
req.event.reply(req.channel, ResponseUtils.success(data));
})
.catch((err: Error) => {
Logger.error("VersionController.getDownloadedVersions", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
downloadFrpVersion(req: ControllerParam) {
this._versionService
.downloadFrpVersion(req.args.githubReleaseId, progress => {
req.event.reply(
req.channel,
ResponseUtils.success({
percent: progress.percent,
githubReleaseId: req.args.githubReleaseId,
completed: progress.percent >= 1
})
);
})
.then(r => {
req.event.reply(
req.channel,
ResponseUtils.success({
percent: 1,
githubReleaseId: req.args.githubReleaseId,
completed: true
})
);
})
.catch((err: Error) => {
Logger.error("VersionController.downloadFrpVersion", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
deleteDownloadedVersion(req: ControllerParam) {
this._versionService
.deleteFrpVersion(req.args.githubReleaseId)
.then(() => {
req.event.reply(req.channel, ResponseUtils.success());
})
.catch((err: Error) => {
Logger.error("VersionController.deleteDownloadedVersion", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
importLocalFrpcVersion(req: ControllerParam) {
const win: BrowserWindow = BeanFactory.getBean("win");
dialog
.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "Frpc", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
]
})
.then(result => {
if (result.canceled) {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: true
})
);
return;
} else {
const filePath = result.filePaths[0];
this._versionService
.importLocalFrpcVersion(filePath)
.then(data => {
req.event.reply(
req.channel,
ResponseUtils.success({
canceled: false
})
);
})
.catch((err: Error) => {
Logger.error("VersionController.importLocalFrpcVersion", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
}
})
.catch(err => {
Logger.error("VersionController.importLocalFrpcVersion", err);
req.event.reply(req.channel, ResponseUtils.fail(err));
});
// const win: BrowserWindow = BeanFactory.getBean("win");
// const result = await dialog.showOpenDialog(win, {
// properties: ["openFile"],
// filters: [
// { name: "Frpc", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
// ]
// });
// if (result.canceled) {
//
// }else {
//
// }
}
}
export default VersionController;

View File

@ -0,0 +1,47 @@
import Logger from "./Logger";
class BeanFactory {
private static _beans: Map<string, any> = new Map<string, any>();
static registerBean(clazz: Function, beanName?: string): void {
if (!beanName) {
beanName = this.getBeanName(clazz.name);
}
if (this.hasBean(beanName)) {
return;
}
const instance = new (clazz as any)();
this._beans.set(beanName, instance);
}
public static setBean<T>(name: string, bean: T): void {
this._beans.set(name, bean);
Logger.info(
`${this.name}.${arguments[0]}`,
`register bean ${name} ${bean}`
);
// Logger.info(`register bean ${name} ${bean}`);
}
public static getBean<T>(name: string): T {
return this._beans.get(name);
}
public static hasBean(name: string): boolean {
return this._beans.has(name);
}
public static clear(): void {
this._beans.clear();
}
public static getBeanName(className: string) {
return className.charAt(0).toLowerCase() + className.slice(1);
}
}
export default BeanFactory;

View File

@ -0,0 +1,31 @@
enum ResponseCode {
SUCCESS = "A1000;successful.",
INTERNAL_ERROR = "B1000;internal error.",
NOT_CONFIG = "B1001;未配置",
VERSION_EXISTS = "B1002;导入失败,版本已存在",
VERSION_ARGS_ERROR = "B1003;所选 frp 架构与操作系统不符",
UNKNOWN_VERSION = "B1004;无法识别文件"
}
class BusinessError extends Error {
private readonly _bizCode: string;
// constructor(bizCode: string, message: string) {
// super(message);
// this.bizCode = bizCode;
// this.name = "BusinessError";
// }
constructor(bizErrorEnum: ResponseCode) {
const [bizCode, message] = bizErrorEnum.split(";");
super(message);
this._bizCode = bizCode;
this.name = "BusinessError";
}
get bizCode(): string {
return this._bizCode;
}
}
export { BusinessError, ResponseCode };

View File

@ -0,0 +1,24 @@
class GlobalConstant {
public static ZIP_EXT = ".zip";
public static TOML_EXT = ".toml";
public static GZ_EXT = ".gz";
public static TAR_GZ_EXT = ".tar.gz";
public static LOCAL_IP = "127.0.0.1";
public static FRPC_LOGIN_FAIL_EXIT = false;
public static FRP_ARCH_VERSION_MAPPING = {
win32_x64: ["window", "amd64"],
win32_arm64: ["window", "arm64"],
win32_ia32: ["window", "386"],
darwin_arm64: ["darwin", "arm64"],
darwin_x64: ["darwin", "amd64"],
darwin_amd64: ["darwin", "amd64"],
linux_x64: ["linux", "amd64"],
linux_arm64: ["linux", "arm64"]
};
public static FRPC_PROCESS_STATUS_CHECK_INTERVAL = 3000;
}
export default GlobalConstant;

126
electron/core/IpcRouter.ts Normal file
View File

@ -0,0 +1,126 @@
export const ipcRouters: IpcRouters = {
SERVER: {
saveConfig: {
path: "server/saveConfig",
controller: "configController.saveConfig"
},
getServerConfig: {
path: "server/getServerConfig",
controller: "configController.getServerConfig"
},
resetAllConfig: {
path: "server/resetAllConfig",
controller: "configController.resetAllConfig"
},
exportConfig: {
path: "server/exportConfig",
controller: "configController.exportConfig"
},
importTomlConfig: {
path: "server/importTomlConfig",
controller: "configController.importTomlConfig"
}
},
LOG: {
getFrpLogContent: {
path: "log/getFrpLogContent",
controller: "logController.getFrpLogContent"
},
openFrpcLogFile: {
path: "log/openFrpcLogFile",
controller: "logController.openFrpcLogFile"
}
},
VERSION: {
getVersions: {
path: "version/getVersions",
controller: "versionController.getVersions"
},
downloadVersion: {
path: "version/downloadVersion",
controller: "versionController.downloadFrpVersion"
},
getDownloadedVersions: {
path: "version/getDownloadedVersions",
controller: "versionController.getDownloadedVersions"
},
deleteDownloadedVersion: {
path: "version/deleteDownloadedVersion",
controller: "versionController.deleteDownloadedVersion"
},
importLocalFrpcVersion: {
path: "version/importLocalFrpcVersion",
controller: "versionController.importLocalFrpcVersion"
}
},
LAUNCH: {
launch: {
path: "launch/launch",
controller: "launchController.launch"
},
terminate: {
path: "launch/terminate",
controller: "launchController.terminate"
},
getStatus: {
path: "launch/getStatus",
controller: "launchController.getStatus"
}
},
PROXY: {
createProxy: {
path: "proxy/createProxy",
controller: "proxyController.createProxy"
},
modifyProxy: {
path: "proxy/modifyProxy",
controller: "proxyController.modifyProxy"
},
deleteProxy: {
path: "proxy/deleteProxy",
controller: "proxyController.deleteProxy"
},
getAllProxies: {
path: "proxy/getAllProxies",
controller: "proxyController.getAllProxies"
},
modifyProxyStatus: {
path: "proxy/modifyProxyStatus",
controller: "proxyController.modifyProxyStatus"
},
getLocalPorts: {
path: "proxy/getLocalPorts",
controller: "proxyController.getLocalPorts"
}
},
SYSTEM: {
openUrl: {
path: "system/openUrl",
controller: "systemController.openUrl"
},
relaunchApp: {
path: "system/relaunchApp",
controller: "systemController.relaunchApp"
},
openAppData: {
path: "system/openAppData",
controller: "systemController.openAppData"
},
selectLocalFile: {
path: "system/selectLocalFile",
controller: "systemController.selectLocalFile"
}
}
};
export const listeners: Listeners = {
watchFrpcLog: {
listenerMethod: "logService.watchFrpcLog",
channel: "log:watchFrpcLog"
},
watchFrpcProcess: {
listenerMethod: "frpcProcessService.watchFrpcProcess",
channel: "frpcProcess:watchFrpcLog"
}
};

26
electron/core/Logger.ts Normal file
View File

@ -0,0 +1,26 @@
import log from "electron-log";
class Logger {
static {
log.transports.file.level = "debug";
log.transports.console.level = "debug";
}
public static info(module: string, msg: string) {
log.info(`[${module}] ${msg}`);
}
public static debug(module: string, msg: string) {
log.debug(`[${module}] ${msg}`);
}
public static warn(module: string, msg: string) {
log.warn(`[${module}] ${msg}`);
}
public static error(module: string, error: Error) {
log.error(`[${module}] ${error}`);
}
}
export default Logger;

View File

@ -0,0 +1,25 @@
// // Class decorator
// import "reflect-metadata";
// import BeanFactory from "../BeanFactory";
//
// const Component =
// (): ClassDecorator =>
// target => {
// // BeanFactory.registerBean(
// // beanName || BeanFactory.getBeanName(target.name),
// // target
// // );
// };
//
//
//
//
// export default Component;
//
//
// // export default function Component(beanName?: string): ClassDecorator {
// // return function (target) {
// // const paramtypes = Reflect.getMetadata('design:paramtypes', target);
// // console.log(paramtypes);
// // };
// // }

View File

@ -0,0 +1,8 @@
// export default function Logger(module?: string): ClassDecorator {
// return function (target: Object) {
// Object.defineProperty(target, "logger", {
// value: new (require("../LoggerFactory").default)(module),
// writable: true
// });
// };
// }

View File

@ -0,0 +1,5 @@
// export default function Resource(beanName?: string): PropertyDecorator {
// return function (target: Object, propertyKey: string | symbol) {
// console.log(target, propertyKey);
// };
// }

View File

@ -1,11 +0,0 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
VSCODE_DEBUG?: 'true'
DIST_ELECTRON: string
DIST: string
/** /dist/ or /public/ */
VITE_PUBLIC: string
}
}

View File

@ -10,21 +10,25 @@ import {
} 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";
import BeanFactory from "../core/BeanFactory";
import ServerRepository from "../repository/ServerRepository";
import VersionRepository from "../repository/VersionRepository";
import ProxyRepository from "../repository/ProxyRepository";
import SystemService from "../service/SystemService";
import ServerService from "../service/ServerService";
import GitHubService from "../service/GitHubService";
import VersionService from "../service/VersionService";
import LogService from "../service/LogService";
import FrpcProcessService from "../service/FrpcProcessService";
import ProxyService from "../service/ProxyService";
import ConfigController from "../controller/ConfigController";
import VersionController from "../controller/VersionController";
import LogController from "../controller/LogController";
import LaunchController from "../controller/LaunchController";
import ProxyController from "../controller/ProxyController";
import SystemController from "../controller/SystemController";
import { ipcRouters, listeners } from "../core/IpcRouter";
import Logger from "../core/Logger";
process.env.DIST_ELECTRON = join(__dirname, "..");
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
@ -32,273 +36,386 @@ process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST;
let win: BrowserWindow | null = null;
let tray = null;
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
let isQuiting;
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());
class FrpcDesktopApp {
private _win: BrowserWindow | null = null;
private readonly _silentStart = false;
private _quitting = false;
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
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);
constructor() {
this.initializeBeans();
this.initializeListeners();
this.initializeRouters();
this.initializeElectronApp();
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
// 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();
}
}
},
{
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}`);
initializeWindow() {
if (this._win) {
return;
}
createWindow(config)
.then(r => {
logInfo(LogModule.APP, `Window created successfully.`);
createTray(config);
if (config) {
logInfo(
LogModule.APP,
`Config retrieved: ${JSON.stringify(
maskSensitiveData(config, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
if (config.systemStartupConnect) {
startFrpWorkerProcess(config);
}
}
// Initialize APIs
try {
initGitHubApi(win);
logInfo(LogModule.APP, `GitHub API initialized.`);
initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
initProxyApi();
logInfo(LogModule.APP, `Proxy API initialized.`);
initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
initLoggerApi();
logInfo(LogModule.APP, `Logger API initialized.`);
initFileApi();
logInfo(LogModule.APP, `File API initialized.`);
initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`);
initLocalApi();
logInfo(LogModule.APP, `Local API initialized.`);
// initUpdaterApi(win);
logInfo(LogModule.APP, `Updater API initialization skipped.`);
} catch (error) {
logError(
LogModule.APP,
`Error during API initialization: ${error.message}`
);
}
})
.catch(error => {
logError(LogModule.APP, `Error creating window: ${error.message}`);
});
});
});
app.on("window-all-closed", () => {
logInfo(LogModule.APP, `All windows closed.`);
win = null;
if (process.platform !== "darwin") {
stopFrpcProcess(() => {
logInfo(LogModule.APP, `FRPC process stopped. Quitting application.`);
app.quit();
this._win = new BrowserWindow({
title: app.getName(),
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: true
});
}
});
app.on("second-instance", () => {
logInfo(LogModule.APP, `Second instance detected.`);
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on("activate", () => {
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.`);
});
});
}
});
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
BeanFactory.setBean("win", this._win);
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
this._win.loadURL(url).then(() => {});
// Open devTool if the app is not packaged
this._win.webContents.openDevTools();
} else {
this._win.loadFile(indexHtml).then(() => {});
}
});
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}`
this._win.webContents.on("did-finish-load", () => {
this._win?.webContents.send(
"main-process-message",
new Date().toLocaleString()
);
});
this._win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
const { Menu } = require("electron");
Menu.setApplicationMenu(null);
const that = this;
this._win.on("minimize", function (event) {
event.preventDefault();
that._win.hide();
});
this._win.on("close", function (event) {
if (!that._quitting) {
event.preventDefault();
that._win.hide();
if (process.platform === "darwin") {
app.dock.hide();
}
}
return false;
});
Logger.info(
`FrpcDesktopApp.initializeWindow`,
`Window initialized.`
);
}
});
initializeTray() {
const that = this;
let menu: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "显示主窗口",
click: function () {
that._win.show();
if (process.platform === "darwin") {
app.dock.show().then(() => {});
}
}
},
{
label: "退出",
click: () => {
that._quitting = true;
// todo stop frpc process
app.quit();
}
}
];
const tray = new Tray(
node_path.join(process.env.VITE_PUBLIC, "logo/only/16x16.png")
);
tray.setToolTip(app.getName());
const contextMenu = Menu.buildFromTemplate(menu);
tray.setContextMenu(contextMenu);
// 托盘双击打开
tray.on("double-click", () => {
this._win.show();
});
Logger.info(
`FrpcDesktopApp.initializeTray`,
`Tray initialized.`
);
}
initializeElectronApp() {
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
app.whenReady().then(() => {
this.initializeWindow();
this.initializeTray();
// initLog();
// logInfo(
// LogModule.APP,
// `Application started. Current system architecture: ${
// process.arch
// }, platform: ${process.platform}, version: ${app.getVersion()}.`
// );
// getConfig((err, config) => {
// if (err) {
// logError(LogModule.APP, `Failed to get config: ${err.message}`);
// return;
// }
// createWindow(config)
// .then(r => {
// logInfo(LogModule.APP, `Window created successfully.`);
// createTray(config);
//
// // if (config) {
// // logInfo(
// // LogModule.APP,
// // `Config retrieved: ${JSON.stringify(
// // maskSensitiveData(config, [
// // "serverAddr",
// // "serverPort",
// // "authToken",
// // "user",
// // "metaToken"
// // ])
// // )}`
// // );
// //
// // if (config.systemStartupConnect) {
// // startFrpWorkerProcess(config);
// // }
// // }
// // const ipcRouterConfig = new IpcRouterConfigurate(win);
// // Initialize APIs
// // try {
// // initGitHubApi(win);
// // logInfo(LogModule.APP, `GitHub API initialized.`);
// //
// // initConfigApi(win);
// // logInfo(LogModule.APP, `Config API initialized.`);
// //
// // initFileApi();
// // logInfo(LogModule.APP, `File API initialized.`);
// //
// // // initUpdaterApi(win);
// // logInfo(LogModule.APP, `Updater API initialization skipped.`);
// // } catch (error) {
// // logError(
// // LogModule.APP,
// // `Error during API initialization: ${error.message}`
// // );
// // }
// })
// .catch(error => {
// logError(LogModule.APP, `Error creating window: ${error.message}`);
// });
// });
});
app.on("window-all-closed", () => {
// logInfo(LogModule.APP, `All windows closed.`);
this._win = null;
if (process.platform !== "darwin") {
// todo stop frpc process
// stopFrpcProcess(() => {
// logInfo(LogModule.APP, `FRPC process stopped. Quitting application.`);
app.quit();
// });
}
});
app.on("second-instance", () => {
if (this._win) {
if (this._win.isMinimized()) this._win.restore();
this._win.focus();
}
});
app.on("activate", () => {
// logInfo(LogModule.APP, `Application activated.`);
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
this.initializeWindow();
}
});
app.on("before-quit", () => {
// todo stop frpc process
this._quitting = true;
});
Logger.info(
`FrpcDesktopApp.initializeElectronApp`,
`ElectronApp initialized.`
);
}
initializeBeans() {
BeanFactory.setBean("serverRepository", new ServerRepository());
BeanFactory.setBean("versionRepository", new VersionRepository());
BeanFactory.setBean("proxyRepository", new ProxyRepository());
BeanFactory.setBean("systemService", new SystemService());
BeanFactory.setBean(
"serverService",
new ServerService(
BeanFactory.getBean("serverRepository"),
BeanFactory.getBean("proxyRepository")
)
);
BeanFactory.setBean("gitHubService", new GitHubService());
BeanFactory.setBean(
"versionService",
new VersionService(
BeanFactory.getBean("versionRepository"),
BeanFactory.getBean("systemService"),
BeanFactory.getBean("gitHubService")
)
);
BeanFactory.setBean(
"logService",
new LogService(BeanFactory.getBean("systemService"))
);
BeanFactory.setBean(
"frpcProcessService",
new FrpcProcessService(
BeanFactory.getBean("serverService"),
BeanFactory.getBean("versionRepository")
)
);
BeanFactory.setBean(
"proxyService",
new ProxyService(
BeanFactory.getBean("proxyRepository"),
BeanFactory.getBean("frpcProcessService"),
)
);
BeanFactory.setBean(
"configController",
new ConfigController(
BeanFactory.getBean("serverService"),
BeanFactory.getBean("systemService"),
BeanFactory.getBean("frpcProcessService")
)
);
BeanFactory.setBean(
"versionController",
new VersionController(
BeanFactory.getBean("versionService"),
BeanFactory.getBean("versionRepository")
)
);
BeanFactory.setBean(
"logController",
new LogController(BeanFactory.getBean("logService"))
);
BeanFactory.setBean(
"launchController",
new LaunchController(BeanFactory.getBean("frpcProcessService"))
);
BeanFactory.setBean(
"proxyController",
new ProxyController(
BeanFactory.getBean("proxyService"),
BeanFactory.getBean("proxyRepository")
)
);
BeanFactory.setBean(
"systemController",
new SystemController(BeanFactory.getBean("systemService"))
);
Logger.info(
`FrpcDesktopApp.initializeBeans`,
`Beans initialized.`
);
}
/**
* initJob
* @private
*/
private initializeListeners() {
Object.keys(listeners).forEach(listenerKey => {
console.log(listenerKey, "listenerKey", listeners[listenerKey]);
const { listenerMethod, channel } = listeners[listenerKey];
const [beanName, method] = listenerMethod.split(".");
const bean = BeanFactory.getBean(beanName);
const listenerParam: ListenerParam = {
// win: BeanFactory.getBean("win"),
channel: channel,
args: []
};
bean[method].call(bean, listenerParam);
});
Logger.info(
`FrpcDesktopApp.initializeListeners`,
`Listeners initialized.`
);
// this._beans.get("logService").watchFrpcLog(this._win);
}
/**
* initRouters
* @private
*/
private initializeRouters() {
Object.keys(ipcRouters).forEach(routerKey => {
const routerGroup = ipcRouters[routerKey];
Object.keys(routerGroup).forEach(method => {
const router = routerGroup[method];
ipcMain.on(router.path, (event, args) => {
const req: ControllerParam = {
// win: BeanFactory.getBean("win"),
channel: `${router.path}:hook`,
event: event,
args: args
};
const [beanName, method] = router.controller.split(".");
const bean = BeanFactory.getBean(beanName);
bean[method].call(bean, req);
Logger.debug(
`ipcRouter`,
`path: ${router.path} + req: (channel: ${
req.channel
}, args: ${JSON.stringify(
req.args
)}) => bean: ${beanName}.${method}`
);
});
});
});
Logger.info(
`FrpcDesktopApp.initializeRouters`,
`Routers initialized.`
);
}
}
new FrpcDesktopApp();

View File

@ -0,0 +1,138 @@
import Datastore from "nedb";
import path from "path";
import PathUtils from "../utils/PathUtils";
import IdUtils from "../utils/IdUtils";
// interface BaseDaoInterface<T> {
// db: Datastore;
//
// insert(t: T): Promise<T>;
//
// //
// updateById(id: string, t: T): Promise<T>;
//
// //
// // deleteById(id: string): void;
// //
// // findAll(): T[];
//
// findById(id: string): Promise<T>;
// }
class BaseRepository<T> {
protected readonly db: Datastore;
constructor(dbName: string) {
const dbFilename = path.join(
PathUtils.getDataBaseStoragePath(),
`${dbName}-v2.db`
);
this.db = new Datastore({
autoload: true,
filename: dbFilename
});
// todo log
}
protected genId(): string {
return IdUtils.genUUID();
}
// async insert(t: T): Promise<T> {
// return new Promise<T>((resolve, reject) => {
// resolve(t);
// });
// }
//
insert(t: T): Promise<T> {
return new Promise<T>((resolve, reject) => {
t["_id"] = this.genId();
this.db.insert(t, (err, document) => {
if (err) {
reject(err);
}
resolve(document);
});
});
}
updateById(id: string, t: T): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.db.update(
{ _id: id },
t,
{ upsert: true },
(err, numberOfUpdated, upsert) => {
if (err) {
reject(err);
} else {
t["_id"] = id;
resolve(t);
// this.findById(id)
// .then(data => {
// resolve(t);
// })
// .catch(err2 => {
// reject(err2);
// });
}
}
);
});
}
deleteById(id: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.db.remove({ _id: id }, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
//
// findAll(): T[] {
// return null;
// }
findById(id: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.db.findOne({ _id: id }, (err, document) => {
if (err) {
reject(err);
} else {
resolve(document);
}
});
});
}
findAll(): Promise<Array<T>> {
return new Promise<Array<T>>((resolve, reject) => {
this.db.find({}, (err, document) => {
if (err) {
reject(err);
} else {
resolve(document);
}
});
});
}
truncate() {
return new Promise<void>((resolve, reject) => {
this.db.remove({}, { multi: true }, (err, n) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
export default BaseRepository;

View File

@ -0,0 +1,28 @@
import BaseRepository from "./BaseRepository";
import Component from "../core/annotation/Component";
// @Component()
class ProxyRepository extends BaseRepository<FrpcProxy> {
constructor() {
super("proxy");
}
updateProxyStatus(id: string, status: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.db.update(
{ _id: id },
{ $set: { status: status } },
{},
(err, numberOfUpdated, upsert) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
}
export default ProxyRepository;

View File

@ -0,0 +1,23 @@
import BaseRepository from "./BaseRepository";
import Component from "../core/annotation/Component";
// @Component()
class ServerRepository extends BaseRepository<OpenSourceFrpcDesktopServer> {
constructor() {
super("server");
}
exists(id: string): Promise<boolean> {
return new Promise((resolve, reject) => {
this.db.count({ _id: id }, (err, count) => {
if (err) {
reject(err);
} else {
resolve(count > 0);
}
});
});
}
}
export default ServerRepository

View File

@ -0,0 +1,35 @@
import BaseRepository from "./BaseRepository";
import Component from "../core/annotation/Component";
// @Component()
class VersionRepository extends BaseRepository<FrpcVersion> {
constructor() {
super("version");
}
findByGithubReleaseId(githubReleaseId: number): Promise<FrpcVersion> {
return new Promise<FrpcVersion>((resolve, reject) => {
this.db.findOne({ githubReleaseId: githubReleaseId }, (err, document) => {
if (err) {
reject(err);
} else {
resolve(document);
}
});
});
}
exists(githubReleaseId: number): Promise<boolean> {
return new Promise((resolve, reject) => {
this.db.count({ githubReleaseId: githubReleaseId }, (err, count) => {
if (err) {
reject(err);
} else {
resolve(count > 0);
}
});
});
}
}
export default VersionRepository;

View File

@ -0,0 +1,16 @@
import BaseRepository from "../repository/BaseRepository";
interface BaseServiceInterface<T> {
// dao: BaseRepository<T>;
}
class BaseService<T> implements BaseServiceInterface<T> {
// dao: BaseRepository<T>;
//
// constructor(dao: BaseRepository<T>) {
// this.dao = dao;
// }
}
export default BaseService;

View File

@ -0,0 +1,164 @@
import ServerService from "./ServerService";
import VersionRepository from "../repository/VersionRepository";
import PathUtils from "../utils/PathUtils";
import GlobalConstant from "../core/GlobalConstant";
import { app, BrowserWindow, Notification } from "electron";
import treeKill from "tree-kill";
import BeanFactory from "../core/BeanFactory";
import ResponseUtils from "../utils/ResponseUtils";
import { BusinessError, ResponseCode } from "../core/BusinessError";
import Logger from "../core/Logger";
class FrpcProcessService {
private readonly _serverService: ServerService;
private readonly _versionDao: VersionRepository;
private _frpcProcess: any;
private _frpcProcessListener: any;
private _frpcLastStartTime: number = -1;
constructor(serverService: ServerService, versionDao: VersionRepository) {
this._serverService = serverService;
this._versionDao = versionDao;
}
isRunning(): boolean {
if (!this._frpcProcess) {
return false;
}
try {
Logger.debug(
`FrpcProcessService.isRunning`,
`pid: ${this._frpcProcess.pid}`
);
process.kill(this._frpcProcess.pid, 0);
return true;
} catch (err) {
return false;
}
}
async startFrpcProcess() {
if (this.isRunning()) {
return;
}
const config = await this._serverService.getServerConfig();
if (!config) {
throw new BusinessError(ResponseCode.NOT_CONFIG);
}
const version = await this._versionDao.findByGithubReleaseId(
config.frpcVersion
);
const configPath = PathUtils.getTomlConfigFilePath();
await this._serverService.genTomlConfig(configPath);
const command = `./${PathUtils.getFrpcFilename()} -c "${configPath}"`;
this._frpcProcess = require("child_process").spawn(command, {
cwd: version.localPath,
shell: true
});
this._frpcLastStartTime = Date.now();
Logger.debug(
`FrpcProcessService.startFrpcProcess`,
`start command: ${command}`
);
this._frpcProcess.stdout.on("data", data => {
Logger.debug(`FrpcProcessService.startFrpcProcess`, `stdout: ${data}`);
});
this._frpcProcess.stderr.on("data", data => {
Logger.debug(`FrpcProcessService.startFrpcProcess`, `stderr: ${data}`);
});
}
async stopFrpcProcess() {
if (this._frpcProcess && this.isRunning()) {
Logger.debug(
`FrpcProcessService.stopFrpcProcess`,
`pid: ${this._frpcProcess.pid}`
);
treeKill(this._frpcProcess.pid, (error: Error) => {
if (error) {
throw error;
} else {
this._frpcProcess = null;
this._frpcLastStartTime = -1;
// clearInterval(this._frpcProcessListener);
}
});
}
}
async reloadFrpcProcess() {
if (!this.isRunning()) {
return;
}
const config = await this._serverService.getServerConfig();
if (!config) {
throw new BusinessError(ResponseCode.NOT_CONFIG);
}
const version = await this._versionDao.findByGithubReleaseId(
config.frpcVersion
);
const configPath = PathUtils.getTomlConfigFilePath();
await this._serverService.genTomlConfig(configPath);
const command = `./${PathUtils.getFrpcFilename()} reload -c "${configPath}"`;
require("child_process").exec(
command,
{
cwd: version.localPath,
shell: true
},
(error, stdout, stderr) => {
if (error) {
Logger.error(`FrpcProcessService.reloadFrpcProcess`, error);
return;
}
if (stderr) {
Logger.debug(
`FrpcProcessService.reloadFrpcProcess`,
`stderr: ${stderr}`
);
}
Logger.debug(
`FrpcProcessService.reloadFrpcProcess`,
`stderr: ${stdout}`
);
}
);
}
watchFrpcProcess(listenerParam: ListenerParam) {
this._frpcProcessListener = setInterval(() => {
const running = this.isRunning();
// todo return status to view.
// logDebug(
// LogModule.FRP_CLIENT,
// `Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
// );
Logger.debug(
`FrpcProcessService.watchFrpcProcess`,
`running: ${running}`
);
if (!running) {
if (this._frpcLastStartTime !== -1) {
new Notification({
title: app.getName(),
body: "Connection lost, please check the logs for details."
}).show();
}
// logError(
// LogModule.FRP_CLIENT,
// "Frpc process status check failed. Connection lost."
// );
// clearInterval(this._frpcProcessListener);
}
const win: BrowserWindow = BeanFactory.getBean("win");
win.webContents.send(
listenerParam.channel,
ResponseUtils.success(running)
);
}, GlobalConstant.FRPC_PROCESS_STATUS_CHECK_INTERVAL);
}
}
export default FrpcProcessService;

View File

@ -0,0 +1,64 @@
class GitHubService {
constructor() {}
getGithubRepoAllReleases(githubRepo: string): Promise<Array<GithubRelease>> {
return new Promise((resolve, reject) => {
const { net } = require("electron");
const request = net.request({
method: "get",
url: `https://api.github.com/repos/${githubRepo}/releases?page=1&per_page=1000`
});
request.on("response", response => {
// logInfo(
// LogModule.GITHUB,
// `Received response with status code: ${response.statusCode}`
// );
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
if (response.statusCode === 200) {
this.parseGitHubVersion(responseData.toString())
.then(data => {
resolve(data);
})
.catch(err => reject(err));
// logInfo(
// LogModule.GITHUB,
// "Successfully retrieved GitHub release data."
// );
} else {
// logWarn(
// LogModule.GITHUB,
// "Failed to retrieve data, using local JSON instead. Status code: " +
// response.statusCode
// );
}
});
});
request.on("error", error => {
reject(error);
});
request.end();
});
}
parseGitHubVersion(
githubReleaseJsonStr: string
): Promise<Array<GithubRelease>> {
return new Promise<Array<GithubRelease>>(resolve => {
const githubReleases: Array<GithubRelease> =
JSON.parse(githubReleaseJsonStr);
resolve(githubReleases);
});
}
}
export default GitHubService;

View File

@ -0,0 +1,67 @@
import fs from "fs";
import PathUtils from "../utils/PathUtils";
import SystemService from "./SystemService";
import BeanFactory from "../core/BeanFactory";
import { BrowserWindow } from "electron";
import ResponseUtils from "../utils/ResponseUtils";
class LogService {
private readonly _systemService: SystemService;
private readonly _logPath: string = PathUtils.getFrpcLogFilePath();
constructor(systemService: SystemService) {
this._systemService = systemService;
}
async getFrpLogContent() {
return new Promise((resolve, reject) => {
if (!fs.existsSync(this._logPath)) {
resolve("");
}
fs.readFile(this._logPath, "utf-8", (error, data) => {
if (!error) {
resolve(data);
} else {
reject(error);
}
});
});
}
watchFrpcLog(listenerParam: ListenerParam) {
if (!fs.existsSync(this._logPath)) {
setTimeout(() => this.watchFrpcLog(listenerParam), 1000);
return;
}
console.log("watchFrpcLog succcess");
fs.watch(this._logPath, (eventType, filename) => {
if (eventType === "change") {
console.log("change", eventType, listenerParam.channel);
const win: BrowserWindow = BeanFactory.getBean("win");
win.webContents.send(
listenerParam.channel,
ResponseUtils.success(true)
);
} else {
}
});
// return new Promise<boolean>((resolve, reject) => {
//
// });
}
openFrpcLogFile(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
this._systemService
.openLocalFile(this._logPath)
.then(result => {
resolve(result);
})
.catch(err => {
reject(err);
});
});
}
}
export default LogService;

View File

@ -0,0 +1,141 @@
import ProxyRepository from "../repository/ProxyRepository";
import FrpcProcessService from "./FrpcProcessService";
class ProxyService {
private readonly _proxyDao: ProxyRepository;
private readonly _frpcProcessService: FrpcProcessService;
constructor(
public proxyDao: ProxyRepository,
frpcProcessService: FrpcProcessService
) {
this._proxyDao = proxyDao;
this._frpcProcessService = frpcProcessService;
}
async insertProxy(proxy: FrpcProxy) {
const proxy2 = await this._proxyDao.insert(proxy);
await this._frpcProcessService.reloadFrpcProcess();
return proxy2;
}
async updateProxy(proxy: FrpcProxy) {
const proxy2 = await this._proxyDao.updateById(proxy._id, proxy);
await this._frpcProcessService.reloadFrpcProcess();
return proxy2;
}
async deleteProxy(proxyId: string) {
await this._proxyDao.deleteById(proxyId);
await this._frpcProcessService.reloadFrpcProcess();
}
async getLocalPorts(): Promise<Array<LocalPort>> {
const command =
process.platform === "win32"
? "netstat -a -n"
: "netstat -an | grep LISTEN";
return new Promise((resolve, reject) => {
require("child_process").exec(command, (error, stdout, stderr) => {
if (error) {
reject(error);
}
if (stderr) {
reject(stderr);
}
let ports: Array<LocalPort> = [];
if (stdout) {
if (process.platform === "win32") {
// window
ports = stdout
.split("\r\n")
.filter(f => f.indexOf("TCP") !== -1 || f.indexOf("UDP") !== -1)
.map(m => {
const cols = m.split(" ").filter(f => f != "");
const local = cols[1];
const s = local.lastIndexOf(":");
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
};
return singe;
});
} else if (process.platform === "darwin") {
// mac
ports = stdout
.split("\n")
.filter(m => {
const cols = m.split(" ").filter(f => f != "");
const local = cols[3];
return local;
})
.map(m => {
const cols = m.split(" ").filter(f => f != "");
const local = cols[3];
const s = local.lastIndexOf(".");
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
};
return singe;
});
} else if (process.platform === "linux") {
ports = stdout
.split("\n")
.filter(
f =>
f.indexOf("tcp") !== -1 ||
f.indexOf("tcp6") !== -1 ||
f.indexOf("udp") !== -1 ||
f.indexOf("udp6") !== -1
)
.map(m => {
const cols = m.split(" ").filter(f => f != "");
const local = cols[3];
const s = local.lastIndexOf(":");
let localIP = local.slice(0, s);
let localPort = local.slice(s - local.length + 1);
const singe: LocalPort = {
protocol: cols[0],
ip: localIP,
port: localPort
};
return singe;
});
}
}
ports.sort((a, b) => a.port - b.port);
resolve(ports);
});
// exec(command, (error, stdout, stderr) => {
// if (error) {
// logError(LogModule.APP, `getLocalPorts - error: ${error.message}`);
// return;
// }
// if (stderr) {
// logWarn(LogModule.APP, `getLocalPorts - stderr: ${stderr}`);
// return;
// }
//
// logDebug(LogModule.APP, `Command output: ${stdout}`);
// let ports = [];
//
// event.reply("local.getLocalPorts.hook", {
// data: ports
// });
// });
});
}
}
export default ProxyService;

View File

@ -0,0 +1,229 @@
import BaseService from "./BaseService";
import ServerRepository from "../repository/ServerRepository";
import fs from "fs";
import PathUtils from "../utils/PathUtils";
import ProxyRepository from "../repository/ProxyRepository";
import { BrowserWindow, dialog } from "electron";
import BeanFactory from "../core/BeanFactory";
import path from "path";
import GlobalConstant from "../core/GlobalConstant";
import TOML from "smol-toml";
class ServerService extends BaseService<OpenSourceFrpcDesktopServer> {
private readonly _serverDao: ServerRepository;
private readonly _proxyDao: ProxyRepository;
// private readonly _systemService: SystemService;
private readonly _serverId: string = "1";
constructor(
serverDao: ServerRepository,
proxyDao: ProxyRepository
// systemService: SystemService
) {
super();
this._serverDao = serverDao;
this._proxyDao = proxyDao;
// this._systemService = systemService;
}
async saveServerConfig(
frpcServer: OpenSourceFrpcDesktopServer
): Promise<OpenSourceFrpcDesktopServer> {
frpcServer._id = this._serverId;
return await this._serverDao.updateById(this._serverId, frpcServer);
}
async getServerConfig(): Promise<OpenSourceFrpcDesktopServer> {
return await this._serverDao.findById(this._serverId);
}
hasServerConfig(): Promise<boolean> {
return new Promise((resolve, reject) => {
this._serverDao
.exists(this._serverId)
.then(r => {
resolve(r);
})
.catch(err => reject(err));
});
}
private isRagePort(proxy: FrpcProxy) {
return (
["tcp", "udp"].indexOf(proxy.type) >= 0 &&
(String(proxy.localPort).indexOf("-") !== -1 ||
String(proxy.localPort).indexOf(",") !== -1)
);
}
private isVisitors(proxy: FrpcProxy) {
return (
["stcp", "sudp", "xtcp"].indexOf(proxy.type) >= 0 &&
proxy.visitorsModel === "visitors"
);
}
private isEnableProxy(proxy: FrpcProxy) {
return proxy.status === 1;
}
private isHttps2http(proxy: FrpcProxy) {
return proxy.https2http;
}
async genTomlConfig(outputPath: string) {
if (!outputPath) {
return;
}
const server = await this.getServerConfig();
const proxies = await this._proxyDao.findAll();
const enabledRangePortProxies = proxies
.filter(f => this.isEnableProxy(f))
.filter(f => !this.isVisitors(f))
.filter(f => this.isRagePort(f))
.map(proxy => {
return `
{{- range $_, $v := parseNumberRangePair "${proxy.localPort}" "${proxy.remotePort}" }}
[[proxies]]
type = "${proxy.type}"
name = "${proxy.name}-{{ $v.First }}"
localPort = {{ $v.First }}
remotePort = {{ $v.Second }}
{{- end }}
`;
});
const enabledProxies = proxies
.filter(f => this.isEnableProxy(f))
.filter(f => !this.isVisitors(f))
.filter(f => !this.isRagePort(f))
.map(proxy => {
if (proxy.type === "tcp" || proxy.type === "udp") {
const localPort = parseInt(proxy.localPort);
const remotePort = parseInt(proxy.remotePort);
return {
name: proxy.name,
type: proxy.type,
localIP: proxy.localIP,
localPort: localPort,
remotePort: remotePort
};
} else if (proxy.type === "http" || proxy.type === "https") {
if (this.isHttps2http(proxy) && proxy.type === "https") {
return {
name: proxy.name,
type: proxy.type,
customDomains: proxy.customDomains,
subdomain: proxy.subdomain,
...(proxy.https2http
? {
plugin: {
type: "https2http",
localAddr: `${proxy.localIP}:${proxy.localPort}`,
crtPath: proxy.https2httpCaFile,
keyPath: proxy.https2httpKeyFile
}
}
: {})
};
} else {
return {
name: proxy.name,
type: proxy.type,
localIP: proxy.localIP,
localPort: parseInt(proxy.localPort),
customDomains: proxy.customDomains,
subdomain: proxy.subdomain,
...(proxy.basicAuth
? { httpUser: proxy.httpUser, httpPassword: proxy.httpPassword }
: {})
};
}
} else if (
proxy.type === "stcp" ||
proxy.type === "xtcp" ||
proxy.type === "sudp"
) {
return {
name: proxy.name,
type: proxy.type,
localIP: proxy.localIP,
localPort: parseInt(proxy.localPort),
secretKey: proxy.secretKey
};
}
});
const enableVisitors = proxies
.filter(f => this.isEnableProxy(f))
.filter(f => this.isVisitors(f))
.map(proxy => {
if (proxy.type === "xtcp") {
return {
name: proxy.name,
type: proxy.type,
// serverUser: proxy.serverUser,
serverName: proxy.serverName,
secretKey: proxy.secretKey,
bindAddr: proxy.bindAddr,
bindPort: proxy.bindPort,
keepTunnelOpen: proxy.keepTunnelOpen,
fallbackTo: proxy.fallbackTo,
fallbackTimeoutMs: proxy.fallbackTimeoutMs
};
} else {
return {
name: proxy.name,
type: proxy.type,
serverName: proxy.serverName,
secretKey: proxy.secretKey,
bindAddr: proxy.bindAddr,
bindPort: proxy.bindPort
};
}
});
const { frpcVersion, _id, system, ...commonConfig } = server;
const frpcConfig = { ...commonConfig };
frpcConfig.log.to = PathUtils.getFrpcLogFilePath();
frpcConfig.loginFailExit = GlobalConstant.FRPC_LOGIN_FAIL_EXIT;
frpcConfig.webServer.addr = GlobalConstant.LOCAL_IP;
let toml = TOML.stringify({
...frpcConfig,
...(enabledProxies.length > 0 ? { proxies: enabledProxies } : {}),
...(enableVisitors.length > 0 ? { visitors: enableVisitors } : {})
});
enabledRangePortProxies.forEach(f => {
toml += `
${f}`;
});
fs.writeFileSync(outputPath, toml, { flag: "w" });
}
async importTomlConfig() {
const win: BrowserWindow = BeanFactory.getBean("win");
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [{ name: "Frpc Toml ConfigFile", extensions: ["toml"] }]
});
if (result.canceled) {
} else {
const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath);
if (fileExtension === GlobalConstant.TOML_EXT) {
const tomlData = fs.readFileSync(filePath, "utf-8");
const sourceConfig = TOML.parse(tomlData);
} else {
throw new Error(`导入失败,暂不支持 ${fileExtension} 格式文件`);
}
return;
}
}
}
export default ServerService;

View File

@ -0,0 +1,112 @@
import { app, shell } from "electron";
import GlobalConstant from "../core/GlobalConstant";
import path from "path";
import fs from "fs";
import zlib from "zlib";
import admZip from "adm-zip";
const tar = require("tar");
class SystemService {
async openUrl(url: string) {
if (url) {
await shell.openExternal(url);
}
}
async relaunch() {
await app.relaunch();
app.quit();
}
openLocalFile(filePath: string) {
return new Promise<boolean>((resolve, reject) => {
shell
.openPath(filePath)
.then(errorMessage => {
if (errorMessage) {
resolve(false);
} else {
resolve(true);
}
})
.catch(err => {
reject(err);
});
});
}
openLocalPath(path: string) {
return new Promise<boolean>((resolve, reject) => {
shell.openPath(path).then(errorMessage => {
if (errorMessage) {
resolve(false);
} else {
resolve(true);
}
});
});
}
decompressZipFile(zipFilePath: string, targetPath: string) {
if (!zipFilePath.endsWith(GlobalConstant.ZIP_EXT)) {
throw new Error("The file is not a .zip file");
}
if (!fs.existsSync(zipFilePath)) {
throw new Error("The file does not exist");
}
// const zipBasename = path.basename(zipFilePath, GlobalConstant.ZIP_EXT);
const targetFolder = path.join(targetPath, targetPath);
if (!fs.existsSync) {
// not exists. do mkdir
fs.mkdirSync(targetFolder, {
recursive: true
});
}
// starting unzip.
const zip = new admZip(zipFilePath);
zip.extractAllTo(targetPath, true); // true: cover exists file.
// todo 2025-02-21 return targetPath.
// const frpcPath = path.join("frp", path.basename(zipFilePath, zipExt));
}
decompressTarGzFile(tarGzPath: string, targetPath: string, finish: Function) {
// const targetFolder = path.join(targetPath, targetPath);
const unzip = zlib.createGunzip();
const readStream = fs.createReadStream(tarGzPath);
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath, { recursive: true, mode: 0o777 });
}
readStream
.pipe(unzip)
.on("error", err => {
// logError(LogModule.APP, `Error during gunzip: ${err.message}`);
})
.pipe(
tar
.extract({
cwd: targetPath,
strip: 1,
filter: filePath => path.basename(filePath) === "frpc"
})
.on("error", err => {
// logError(
// LogModule.APP,
// `Error extracting tar file: ${err.message}`
// );
})
)
.on("finish", () => {
finish();
// const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
// logInfo(
// LogModule.APP,
// `Extraction completed. Extracted directory: ${frpcPath}`
// );
});
}
}
export default SystemService;

View File

@ -0,0 +1,239 @@
import VersionRepository from "../repository/VersionRepository";
import BaseService from "./BaseService";
import GitHubService from "./GitHubService";
import frpReleasesJson from "../json/frp-releases.json";
import { download } from "electron-dl";
import { BrowserWindow } from "electron";
import GlobalConstant from "../core/GlobalConstant";
import path from "path";
import fs from "fs";
import SecureUtils from "../utils/SecureUtils";
import PathUtils from "../utils/PathUtils";
import FileUtils from "../utils/FileUtils";
import frpChecksums from "../json/frp_all_sha256_checksums.json";
import SystemService from "./SystemService";
import { BusinessError, ResponseCode } from "../core/BusinessError";
class VersionService extends BaseService<FrpcVersion> {
private readonly _versionDao: VersionRepository;
private readonly _systemService: SystemService;
private readonly _gitHubService: GitHubService;
private readonly _currFrpArch: Array<string>;
private _versions: Array<FrpcVersion> = [];
constructor(
versionDao: VersionRepository,
systemService: SystemService,
gitHubService: GitHubService
) {
super();
this._versionDao = versionDao;
this._gitHubService = gitHubService;
this._systemService = systemService;
const nodeVersion = `${process.platform}_${process.arch}`;
this._currFrpArch = GlobalConstant.FRP_ARCH_VERSION_MAPPING[nodeVersion];
}
downloadFrpVersion(githubReleaseId: number, onProgress: Function) {
return new Promise((resolve, reject) => {
const version = this._versions.find(
f => f.githubReleaseId === githubReleaseId
);
if (!version) {
reject(new Error("version not found"));
}
const url = version.browserDownloadUrl;
const downloadedFilePath = path.join(
PathUtils.getDownloadStoragePath(),
`${version.assetName}`
);
const versionFilePath = path.join(
PathUtils.getVersionStoragePath(),
SecureUtils.calculateMD5(version.name)
);
if (fs.existsSync(versionFilePath)) {
fs.rmSync(versionFilePath, { recursive: true, force: true });
}
// const targetPath = path.resolve();
download(BrowserWindow.getFocusedWindow(), url, {
filename: `${version.assetName}`,
directory: PathUtils.getDownloadStoragePath(),
onProgress: progress => {
onProgress(progress);
},
onCompleted: () => {
this.decompressFrp(version, downloadedFilePath)
.then(data => {
resolve(data);
})
.catch(err => {
reject(err);
});
}
});
});
}
async deleteFrpVersion(githubReleaseId: number) {
if (!githubReleaseId) {
return;
}
const version = await this._versionDao.findByGithubReleaseId(
githubReleaseId
);
if (this.frpcVersionExists(version)) {
fs.rmSync(version.localPath, { recursive: true, force: true });
await this._versionDao.deleteById(version._id);
}
}
async getFrpVersionsByGitHub(): Promise<Array<FrpcVersion>> {
return new Promise<Array<FrpcVersion>>((resolve, reject) => {
this._gitHubService
.getGithubRepoAllReleases("fatedier/frp")
.then(async (releases: Array<GithubRelease>) => {
const versions: Array<FrpcVersion> =
await this.githubRelease2FrpcVersion(releases);
// const _versions: Array<FrpcVersion> = (this._versions = _versions);
this._versions = versions;
resolve(versions);
})
.catch(err => reject(err));
});
}
async getFrpVersionByLocalJson(): Promise<Array<FrpcVersion>> {
return this.githubRelease2FrpcVersion(frpReleasesJson);
}
getFrpVersion() {}
private findCurrentArchitectureAsset(assets: Array<GithubAsset>) {
return assets.find((af: GithubAsset) => {
return this._currFrpArch.every(item => af.name.includes(item));
});
}
private async githubRelease2FrpcVersion(
releases: Array<GithubRelease>
): Promise<Array<FrpcVersion>> {
const allVersions = await this._versionDao.findAll();
return releases
.filter(release => {
// only support toml version.
return release.id > 124395282;
})
.filter(release => {
return this.findCurrentArchitectureAsset(release.assets);
})
.map(m => {
const asset = this.findCurrentArchitectureAsset(m.assets);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
const currVersion = allVersions.find(ff => ff.githubReleaseId === m.id);
const v: FrpcVersion = {
_id: "",
githubReleaseId: m.id,
githubAssetId: asset.id,
githubCreatedAt: asset.created_at,
name: m.name,
assetName: asset.name,
versionDownloadCount: download_count,
assetDownloadCount: asset.download_count,
browserDownloadUrl: asset.browser_download_url,
downloaded: this.frpcVersionExists(currVersion),
localPath: currVersion && currVersion.localPath,
size: FileUtils.formatBytes(asset.size)
};
return v;
});
}
private frpcVersionExists(version: FrpcVersion): boolean {
// const version = await this._versionDao.findByGithubReleaseId(
// githubReleaseId
// );
if (version) {
return fs.existsSync(version.localPath);
}
return false;
}
async importLocalFrpcVersion(filePath: string) {
const checksum = FileUtils.calculateFileChecksum(filePath);
const frpName = frpChecksums[checksum];
if (frpName) {
if (this._currFrpArch.every(item => frpName.includes(item))) {
const version = this.getFrpVersionByAssetName(frpName);
const existsVersion = await this._versionDao.findByGithubReleaseId(
version.githubReleaseId
);
if (existsVersion) {
throw new BusinessError(ResponseCode.VERSION_EXISTS);
}
return this.decompressFrp(version, filePath);
} else {
throw new BusinessError(ResponseCode.VERSION_ARGS_ERROR);
}
} else {
throw new BusinessError(ResponseCode.UNKNOWN_VERSION);
}
}
getFrpVersionByAssetName(assetName: string) {
return this._versions.find(f => f.assetName === assetName);
}
async decompressFrp(version: FrpcVersion, compressedPath: string) {
const versionFilePath = path.join(
PathUtils.getVersionStoragePath(),
SecureUtils.calculateMD5(version.name)
);
const ext = path.extname(version.assetName);
if (ext === GlobalConstant.ZIP_EXT) {
this._systemService.decompressZipFile(compressedPath, versionFilePath);
// todo delete frps and other file.
} else if (
ext === GlobalConstant.GZ_EXT &&
version.assetName.includes(GlobalConstant.TAR_GZ_EXT)
) {
this._systemService.decompressTarGzFile(
compressedPath,
versionFilePath,
() => {
// rename frpc.
const frpcFilePath = path.join(versionFilePath, "frpc");
if (fs.existsSync(frpcFilePath)) {
const newFrpcFilePath = path.join(
versionFilePath,
PathUtils.getFrpcFilename()
);
fs.renameSync(frpcFilePath, newFrpcFilePath);
}
// delete downloaded file.
// todo has bug.
const downloadedFile = path.join(
PathUtils.getDownloadStoragePath(),
version.assetName
);
if (fs.existsSync(downloadedFile)) {
fs.rmSync(downloadedFile, { recursive: true, force: true });
}
}
);
}
// todo 2025-02-23 delete downloaded file.
version.localPath = versionFilePath;
version.downloaded = true;
return await this._versionDao.insert(version);
}
}
export default VersionService;

View File

@ -1,90 +0,0 @@
import Datastore from "nedb";
import path from "path";
import { app } from "electron";
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")
});
/**
*
*/
export const saveConfig = (
document: FrpConfig,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
document["_id"] = "1";
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);
}
);
};
/**
*
* @param cb
*/
export const getConfig = (
cb: (err: Error | null, document: FrpConfig) => void
) => {
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) => {
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

@ -1,143 +0,0 @@
import Datastore from "nedb";
import path from "path";
import { app } from "electron";
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const proxyDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "proxy.db")
});
export const insertProxy = (
proxy: Proxy,
cb?: (err: Error | null, document: Proxy) => void
) => {
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);
});
};
export const deleteProxyById = (
_id: string,
cb?: (err: Error | null, n: number) => void
) => {
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
) => {
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);
}
);
};
export const listProxy = (
callback: (err: Error | null, documents: Proxy[]) => void
) => {
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);
});
};
export const getProxyById = (
id: string,
callback: (err: Error | null, document: Proxy) => void
) => {
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);
});
};
export const clearProxy = (cb?: (err: Error | null, n: number) => void) => {
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);
});
};
export const updateProxyStatus = (
id: string,
st: boolean,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
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

@ -1,90 +0,0 @@
import Datastore from "nedb";
import path from "path";
import { app } from "electron";
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const versionDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "version.db")
});
/**
* Insert version
* @param version
* @param cb
*/
export const insertVersion = (
version: FrpVersion,
cb?: (err: Error | null, document: any) => void
) => {
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
) => {
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
) => {
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
) => {
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) => {
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,29 @@
import { createHash } from "crypto";
import fs from "fs";
class FileUtils {
public static 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
}
public static calculateFileChecksum(filePath: string) {
const fileBuffer = fs.readFileSync(filePath);
const hash = createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
}
public static mkdir(path: string) {
if (!fs.existsSync(path)) {
fs.mkdirSync(path, { recursive: true, mode: 0o777 });
}
}
}
export default FileUtils;

21
electron/utils/IdUtils.ts Normal file
View File

@ -0,0 +1,21 @@
class IdUtils {
public static genUUID() {
let uuidValue = "",
k,
randomValue;
for (k = 0; k < 32; k++) {
randomValue = (Math.random() * 16) | 0;
if (k == 8 || k == 12 || k == 16 || k == 20) {
uuidValue += "-";
}
uuidValue += (
k == 12 ? 4 : k == 16 ? (randomValue & 3) | 8 : randomValue
).toString(16);
}
return uuidValue;
}
}
export default IdUtils;

View File

@ -0,0 +1,68 @@
import SecureUtils from "./SecureUtils";
import { app } from "electron";
import path from "path";
import FileUtils from "./FileUtils";
class PathUtils {
public static getDownloadStoragePath() {
const result = path.join(PathUtils.getAppData(), "download");
FileUtils.mkdir(result);
return result;
}
public static getVersionStoragePath() {
const result = path.join(
PathUtils.getAppData(),
SecureUtils.calculateMD5("frpc")
);
FileUtils.mkdir(result);
return result;
}
public static getConfigStoragePath() {
const result = path.join(
PathUtils.getAppData(),
// SecureUtils.calculateMD5("config")
"config"
);
FileUtils.mkdir(result);
return result;
}
public static getFrpcFilename() {
return SecureUtils.calculateMD5("frpc");
}
public static getAppData() {
return app.getPath("userData");
}
public static getDataBaseStoragePath() {
const result = path.join(PathUtils.getAppData(), "db");
FileUtils.mkdir(result);
return result;
}
public static getTomlConfigFilePath() {
return path.join(
PathUtils.getConfigStoragePath(),
SecureUtils.calculateMD5("frpc") + ".toml"
);
}
public static getFrpcLogStoragePath() {
const result = path.join(PathUtils.getAppData(), "log");
FileUtils.mkdir(result);
return result;
}
public static getFrpcLogFilePath() {
return path.join(
PathUtils.getFrpcLogStoragePath(),
SecureUtils.calculateMD5("frpc-log") + ".log"
);
}
}
export default PathUtils;

View File

@ -0,0 +1,40 @@
import { BusinessError, ResponseCode } from "../core/BusinessError";
class ResponseUtils {
public static success<T>(data?: any, message?: string) {
const [bizCode, message2] = ResponseCode.SUCCESS.split(";");
const resp: ApiResponse<T> = {
bizCode: bizCode,
data: data,
message: message || message2
};
return resp;
}
// public static fail(bizCode?: string, message?: string) {
// const resp: ApiResponse<any> = {
// success: false,
// bizCode: bizCode,
// data: null,
// message: message || "internal error."
// };
// return resp;
// }
public static fail(err: Error) {
if (!(err instanceof BusinessError)) {
err = new BusinessError(ResponseCode.INTERNAL_ERROR);
}
const bizCode = (err as BusinessError).bizCode;
const message = (err as BusinessError).message;
const resp: ApiResponse<any> = {
bizCode: bizCode,
data: null,
message: message
};
return resp;
}
}
export default ResponseUtils;

View File

@ -0,0 +1,11 @@
import { createHash } from "crypto";
class SecureUtils {
public static calculateMD5(str: string): string {
const hash = createHash("md5");
hash.update(str);
return hash.digest("hex");
}
}
export default SecureUtils;

View File

@ -1,12 +0,0 @@
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;
};

View File

@ -1,19 +0,0 @@
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");
};

View File

@ -1,30 +0,0 @@
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

@ -50,7 +50,7 @@
"autoprefixer": "^10.4.15",
"canvas-confetti": "^1.9.0",
"cssnano": "^6.0.1",
"electron": "^26.6.10",
"electron": "22.3.27",
"electron-builder": "^24.13.3",
"electron-builder-squirrel-windows": "^24.13.3",
"element-plus": "^2.4.2",
@ -64,9 +64,9 @@
"sass": "^1.66.1",
"tailwindcss": "^3.3.3",
"tree-kill": "^1.2.2",
"typescript": "^5.1.6",
"vite": "^4.4.9",
"vite-plugin-electron": "^0.15.3",
"typescript": "5.7.3",
"vite": "^5.4.11",
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
@ -82,6 +82,10 @@
"intro.js": "^8.0.0-beta.1",
"isbinaryfile": "4.0.10",
"js-base64": "^3.7.7",
"lodash": "^4.17.21",
"pinia": "^3.0.1",
"smol-toml": "^1.3.1",
"snowflakify": "^1.0.5",
"tar": "^6.2.0",
"unused-filename": "^4.0.1",
"uuid": "^10.0.0"

View File

@ -2,15 +2,17 @@
import { computed, defineComponent, onMounted, ref } from "vue";
import router from "@/router";
import { RouteRecordRaw } from "vue-router";
import pkg from "../../../package.json";
import Intro from "@/intro";
import "intro.js/introjs.css";
import confetti from "canvas-confetti/src/confetti.js";
import { useFrpcDesktopStore } from "@/store/frpcDesktop";
import pkg from "../../../package.json";
defineComponent({
name: "AppMain"
});
const frpcDesktopStore = useFrpcDesktopStore();
const routes = ref<Array<RouteRecordRaw>>([]);
const guideSteps = ref({
Home: {
@ -93,11 +95,14 @@ onMounted(() => {
<template>
<div class="left-menu-container drop-shadow-xl">
<div class="logo-container">
<img
src="/logo/only/128x128.png"
class="logo animate__animated animate__bounceInLeft"
alt="Logo"
/>
<!-- <img-->
<!-- src="/logo/only/128x128.png"-->
<!-- class="logo animate__animated animate__bounceInLeft"-->
<!-- alt="Logo"-->
<!-- />-->
<!-- <el-badge :value="'v1.1.2'" class="logo" type="primary" :offset="[-10, 42]">-->
<img src="/logo/only/128x128.png" class="logo" alt="Logo" />
<!-- </el-badge>-->
</div>
<ul class="menu-container">
<!-- enter-active-class="animate__animated animate__bounceIn"-->
@ -119,18 +124,24 @@ onMounted(() => {
</li>
</ul>
<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>-->
<!-- <el-tag-->
<!-- :type="frpcProcessStore.running ? 'primary' : 'warning'"-->
<!-- effect="light"-->
<!-- size="small"-->
<!-- >{{ frpcProcessStore.running ? "运行中" : "已断开" }}-->
<!-- </el-tag>-->
<!-- <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"

View File

@ -1,22 +1,33 @@
import {createApp} from "vue";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "./styles/index.scss";
import 'animate.css';
import "animate.css";
import ElementPlus from "element-plus";
import {
IconifyIconOffline,
IconifyIconOnline,
IconifyIconOffline,
IconifyIconOnline
} from "./components/IconifyIcon";
import { createPinia } from "pinia";
import { useFrpcDesktopStore } from "@/store/frpcDesktop";
const pinia = createPinia();
const app = createApp(App);
app.component("IconifyIconOffline", IconifyIconOffline);
app.component("IconifyIconOnline", IconifyIconOnline);
app
.use(router)
.use(ElementPlus)
.use(pinia)
.mount("#app")
.$nextTick(() => {
postMessage({ payload: "removeLoading" }, "*");
const frpcDesktopStore = useFrpcDesktopStore();
frpcDesktopStore.onListenerFrpcProcessRunning();
frpcDesktopStore.onListenerDownloadedVersion();
frpcDesktopStore.refreshDownloadedVersion();
})
.then(r => {});
app.use(router)
.use(ElementPlus)
.mount("#app")
.$nextTick(() => {
postMessage({payload: "removeLoading"}, "*");
});

36
src/store/frpcDesktop.ts Normal file
View File

@ -0,0 +1,36 @@
import { defineStore } from "pinia";
import { on, onListener, send } from "@/utils/ipcUtils";
import { ipcRouters, listeners } from "../../electron/core/IpcRouter";
export const useFrpcDesktopStore = defineStore("frpcDesktop", {
state: () => ({ isRunning: false, versions: [] }),
getters: {
running: state => state.isRunning,
downloadedVersions: state => state.versions
},
actions: {
onListenerFrpcProcessRunning() {
onListener(listeners.watchFrpcProcess, data => {
console.log("watchFrpcProcess", data);
this.isRunning = data;
});
on(ipcRouters.LAUNCH.getStatus, data => {
console.log("getStatus", data);
this.isRunning = data;
});
},
onListenerDownloadedVersion() {
on(ipcRouters.VERSION.getDownloadedVersions, data => {
this.versions = data;
});
},
refreshRunning() {
send(ipcRouters.LAUNCH.getStatus);
},
refreshDownloadedVersion() {
send(ipcRouters.VERSION.getDownloadedVersions);
}
}
});

View File

@ -6,12 +6,13 @@ $danger-color: #F56C6C;
background: $main-bg;
width: calc(100% - 60px);
height: 100vh;
padding: 20px;
padding: 15px;
.main {
height: 100%;
width: 100%;
overflow: hidden;
//overflow: hidden;
}
.app-container-breadcrumb {
@ -26,7 +27,7 @@ $danger-color: #F56C6C;
.breadcrumb-left {
color: $primary-color;
font-size: 36px;
height: 30px;
height: 32px;
svg {
vertical-align: top;

View File

@ -1,19 +0,0 @@
export function clone<T>(value: T): T {
/** 空 */
if (!value) return value;
/** 数组 */
if (Array.isArray(value))
return value.map(item => clone(item)) as unknown as T;
/** 日期 */
if (value instanceof Date) return new Date(value) as unknown as T;
/** 普通对象 */
if (typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([k, v]: [string, any]) => {
return [k, clone(v)];
})
) as unknown as T;
}
/** 基本类型 */
return value;
}

72
src/utils/ipcUtils.ts Normal file
View File

@ -0,0 +1,72 @@
import { ipcRenderer } from "electron";
import { ElMessage } from "element-plus";
export const send = (router: IpcRouter, params?: any) => {
ipcRenderer.send(router.path, params);
};
// export const invoke = (router: IpcRouter, params?: any) => {
// return new Promise((resolve, reject) => {
// ipcRenderer
// .invoke(router.path, params)
// .then((args: ApiResponse<any>) => {
// const { success, data, message } = args;
// if (success) {
// resolve(data);
// } else {
// // reject(new Error(message));
// }
// })
// .catch(err => reject(err));
// });
// };
export const on = (
router: IpcRouter,
listerHandler: (data: any) => void,
errHandler?: (bizCode: string, message: string) => void
) => {
ipcRenderer.on(`${router.path}:hook`, (event, args: ApiResponse<any>) => {
const { bizCode, data, message } = args;
if (bizCode === "A1000") {
listerHandler(data);
} else {
if (errHandler) {
errHandler(bizCode, message);
} else {
// ElMessageBox.alert(message,"出错了");
ElMessage({
message: message,
type: "error"
});
}
// reject(new Error(message));
}
});
};
export const onListener = (
listener: Listener,
listerHandler: (data: any) => void
) => {
// return new Promise((resolve, reject) => {
ipcRenderer.on(`${listener.channel}`, (event, args: ApiResponse<any>) => {
const { bizCode, data, message } = args;
if (bizCode === "A1000") {
listerHandler(data);
}
});
// });
};
export const removeRouterListeners = (router: IpcRouter) => {
ipcRenderer.removeAllListeners(`${router.path}:hook`);
};
export const removeRouterListeners2 = (listen: Listener) => {
ipcRenderer.removeAllListeners(`${listen.channel}`);
}
// export const removeAllListeners = (listen: Listener) => {
// ipcRenderer.removeAllListeners(`${listen.channel}:hook`);
// };

View File

@ -1,11 +1,12 @@
<script lang="ts" setup>
import {computed, defineComponent, onMounted, onUnmounted, ref} from "vue";
import {ipcRenderer} from "electron";
import {Icon} from "@iconify/vue";
import { computed, defineComponent, onMounted, onUnmounted, ref } from "vue";
import { ipcRenderer } from "electron";
import { Icon } from "@iconify/vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import pkg from '../../../package.json';
import {ElMessage, ElMessageBox} from "element-plus";
import pkg from "../../../package.json";
import { ElMessage, ElMessageBox } from "element-plus";
import { send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
/**
* 最后一个版本号
@ -17,143 +18,161 @@ const isLastVersion = computed(() => {
return true;
}
// tagName
const tagName = latestVersionInfo.value['tag_name']
console.log(tagName, latestVersionInfo.value, 'tagName')
const tagName = latestVersionInfo.value["tag_name"];
console.log(tagName, latestVersionInfo.value, "tagName");
if (!tagName) {
return true;
}
//
const lastVersion = tagName.replace('v', '').toString();
const lastVersion = tagName.replace("v", "").toString();
const currVersion = pkg.version;
console.log(lastVersion, currVersion, currVersion >= lastVersion, "isLast")
console.log(lastVersion, currVersion, currVersion >= lastVersion, "isLast");
// console.log()
return currVersion >= lastVersion;
// return false;
})
});
/**
* 打开github issues
*/
const handleOpenGitHubIssues = () => {
ipcRenderer.send("common.openUrl", "https://github.com/luckjiawei/frpc-desktop/issues")
}
send(ipcRouters.SYSTEM.openUrl, {
url: "https://github.com/luckjiawei/frpc-desktop/issues"
});
};
/**
* 打开github主页
*/
const handleOpenGitHub = () => {
ipcRenderer.send("common.openUrl", "https://github.com/luckjiawei/frpc-desktop")
}
send(ipcRouters.SYSTEM.openUrl, {
url: "https://github.com/luckjiawei/frpc-desktop"
});
};
/**
* 打开捐赠界面
*/
const handleOpenDonate = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/donate")
}
send(ipcRouters.SYSTEM.openUrl, {
url: "https://jwinks.com/donate"
});
};
/**
* 打开文档
*/
const handleOpenDoc = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/p/frp")
}
send(ipcRouters.SYSTEM.openUrl, {
url: "https://jwinks.com/p/frp"
});
};
/**
* 获取最后一个版本
*/
const handleGetLastVersion = () => {
ipcRenderer.send("github.getFrpcDesktopLastVersions")
}
ipcRenderer.send("github.getFrpcDesktopLastVersions");
};
const handleOpenNewVersion = () => {
ipcRenderer.send("common.openUrl", latestVersionInfo.value['html_url'])
}
ipcRenderer.send("common.openUrl", latestVersionInfo.value["html_url"]);
};
onMounted(() => {
ipcRenderer.on("github.getFrpcDesktopLastVersionsHook", (event, args) => {
latestVersionInfo.value = args;
console.log(latestVersionInfo.value, '1')
console.log(latestVersionInfo.value, "1");
if (!isLastVersion.value) {
let content = latestVersionInfo.value.body
content = content.replaceAll('\n', '<br/>')
let content = latestVersionInfo.value.body;
content = content.replaceAll("\n", "<br/>");
ElMessageBox.alert(content, `🎉 发现新版本 ${args.name}`, {
showCancelButton: true,
cancelButtonText: "关闭",
dangerouslyUseHTMLString: true,
confirmButtonText: "去下载"
}).then(() => {
handleOpenNewVersion()
})
handleOpenNewVersion();
});
} else {
ElMessage({
message: "当前已是最新版本",
type: "success"
})
});
}
});
handleGetLastVersion();
})
});
onUnmounted(() => {
ipcRenderer.removeAllListeners("github.getFrpcDesktopLastVersionsHook");
})
});
defineComponent({
name: "About"
});
</script>
<template>
<div class="main">
<breadcrumb/>
<breadcrumb />
<div class="app-container-breadcrumb">
<div
class="w-full h-full bg-white p-4 rounded drop-shadow-lg overflow-y-auto flex justify-center items-center flex-col"
class="w-full h-full bg-white p-4 rounded drop-shadow-lg overflow-y-auto flex justify-center items-center flex-col"
>
<img src="/logo/pack/1024x1024.png"
class="w-[95px] h-[95px] mt-[-50px] animate__animated animate__flip" alt="Logo"/>
<img
src="/logo/pack/1024x1024.png"
class="w-[95px] h-[95px] mt-[-50px] animate__animated animate__flip"
alt="Logo"
/>
<div class="mt-[8px] text-2xl">Frpc Desktop</div>
<div class="mt-[8px] text-neutral-400 flex items-center">
<el-link
:class="!isLastVersion? 'line-through': ''"
class="ml-2 font-bold">v{{ pkg.version }}
:class="!isLastVersion ? 'line-through' : ''"
class="ml-2 font-bold"
>v{{ pkg.version }}
</el-link>
<el-link v-if="!isLastVersion && latestVersionInfo"
@click="handleOpenNewVersion"
class="ml-2 text-[#67C23A] font-bold"
type="success">v{{ latestVersionInfo.name }}
<el-link
v-if="!isLastVersion && latestVersionInfo"
@click="handleOpenNewVersion"
class="ml-2 text-[#67C23A] font-bold"
type="success"
>v{{ latestVersionInfo.name }}
</el-link>
<IconifyIconOffline class="ml-1.5 cursor-pointer check-update" icon="refresh-rounded"
@click="handleGetLastVersion"/>
<IconifyIconOffline
class="ml-1.5 cursor-pointer check-update"
icon="refresh-rounded"
@click="handleGetLastVersion"
/>
</div>
<div class="mt-[8px] text-sm text-center">
<p>
🎉 {{ pkg.description }}
</p>
<p>
开机自启 / 可视化配置 / 免费开源
</p>
<p>🎉 {{ pkg.description }}</p>
<p>开机自启 / 可视化配置 / 免费开源</p>
</div>
<div class="mt-[12px]">
<el-button plain type="success" @click="handleOpenDoc">
<IconifyIconOffline class="cursor-pointer mr-2" icon="description"/>
<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"/>
<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"/>
<Icon class="cursor-pointer mr-2" icon="logos:github-icon" />
仓库地址
</el-button>
<el-button type="danger" plain @click="handleOpenGitHubIssues">
<IconifyIconOffline class="cursor-pointer mr-2" icon="question-mark"/>
<IconifyIconOffline
class="cursor-pointer mr-2"
icon="question-mark"
/>
反馈问题
</el-button>
</div>
@ -164,6 +183,6 @@ defineComponent({
<style lang="scss" scoped>
.check-update:hover {
color: #5F3BB0;
color: #5f3bb0;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,19 @@
<script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { ipcRenderer } from "electron";
import moment from "moment";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useDebounceFn } from "@vueuse/core";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { on, removeRouterListeners, send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
import { useFrpcDesktopStore } from "@/store/frpcDesktop";
defineComponent({
name: "Download"
});
const versions = ref<Array<FrpVersion>>([]);
const versions = ref<Array<FrpcVersion>>([]);
const loading = ref(1);
const downloadPercentage = ref(0);
const downloading = ref<Map<number, number>>(new Map<number, number>());
@ -22,32 +24,31 @@ const mirrors = ref<Array<GitHubMirror>>([
name: "github"
}
]);
const frpcDesktopStore = useFrpcDesktopStore();
/**
* 获取版本
*/
const handleLoadVersions = () => {
ipcRenderer.send("github.getFrpVersions", currMirror.value);
const handleLoadAllVersions = () => {
send(ipcRouters.VERSION.getVersions);
};
/**
* 下载
* @param version
*/
const handleDownload = useDebounceFn((version: FrpVersion) => {
// console.log(version, currMirror.value);
ipcRenderer.send("github.download", {
versionId: version.id,
mirror: currMirror.value
const handleDownload = useDebounceFn((version: FrpcVersion) => {
send(ipcRouters.VERSION.downloadVersion, {
githubReleaseId: version.githubReleaseId
});
downloading.value.set(version.id, 0);
downloading.value.set(version.githubReleaseId, 0);
}, 300);
/**
* 删除下载
* @param version
*/
const handleDeleteVersion = useDebounceFn((version: FrpVersion) => {
const handleDeleteVersion = useDebounceFn((version: FrpcVersion) => {
ElMessageBox.alert(
`确认要删除 <span class="text-primary font-bold">${version.name} </span> 吗?`,
"提示",
@ -58,95 +59,103 @@ const handleDeleteVersion = useDebounceFn((version: FrpVersion) => {
confirmButtonText: "删除"
}
).then(() => {
ipcRenderer.send("github.deleteVersion", {
id: version.id,
absPath: version.absPath
send(ipcRouters.VERSION.deleteDownloadedVersion, {
githubReleaseId: version.githubReleaseId
});
});
}, 300);
const handleInitDownloadHook = () => {
ipcRenderer.on("Download.frpVersionHook", (event, args) => {
loading.value--;
versions.value = args.map(m => {
m.published_at = moment(m.published_at).format("YYYY-MM-DD");
return m as FrpVersion;
}) as Array<FrpVersion>;
console.log(versions, "versions");
});
//
ipcRenderer.on("Download.frpVersionDownloadOnProgress", (event, args) => {
const { id, progress } = args;
downloading.value.set(
id,
Number(Number(progress.percent * 100).toFixed(2))
);
});
ipcRenderer.on("Download.frpVersionDownloadOnCompleted", (event, args) => {
downloading.value.delete(args);
const version: FrpVersion | undefined = versions.value.find(
f => f.id === args
);
if (version) {
version.download_completed = true;
}
});
ipcRenderer.on("Download.deleteVersion.hook", (event, args) => {
const { err, data } = args;
if (!err) {
loading.value++;
ElMessage({
type: "success",
message: "删除成功"
});
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();
handleLoadAllVersions();
};
onMounted(() => {
handleLoadVersions();
handleInitDownloadHook();
// ipcRenderer.invoke("process").then((r: any) => {
// console.log(r, "rrr");
// });
handleLoadAllVersions();
on(ipcRouters.VERSION.getVersions, data => {
versions.value = data.map(m => {
m.githubCreatedAt = moment(m.githubCreatedAt).format("YYYY-MM-DD");
return m as FrpcVersion;
}) as Array<FrpcVersion>;
loading.value--;
});
on(ipcRouters.VERSION.downloadVersion, data => {
const { githubReleaseId, completed, percent } = data;
if (completed) {
downloading.value.delete(githubReleaseId);
const version: FrpcVersion | undefined = versions.value.find(
f => f.githubReleaseId === githubReleaseId
);
if (version) {
version.downloaded = true;
}
} else {
downloading.value.set(
githubReleaseId,
Number(Number(percent * 100).toFixed(2))
);
}
frpcDesktopStore.refreshDownloadedVersion();
});
on(ipcRouters.VERSION.deleteDownloadedVersion, () => {
loading.value++;
ElMessage({
type: "success",
message: "删除成功"
});
handleLoadAllVersions();
frpcDesktopStore.refreshDownloadedVersion();
});
on(
ipcRouters.VERSION.importLocalFrpcVersion,
data => {
const { canceled } = data;
if (!canceled) {
loading.value++;
ElMessage({
type: "success",
message: "导入成功"
});
handleLoadAllVersions();
frpcDesktopStore.refreshDownloadedVersion();
}
},
(bizCode: string, message: string) => {
if (bizCode === "B1002") {
//
ElMessageBox.alert(`${message}`, `导入失败`);
}
if (bizCode === "B1003") {
// frp
ElMessageBox.alert(`${message}`, `导入失败`);
}
if (bizCode === "B1004") {
//
ElMessageBox.alert(`${message}`, `导入失败`);
}
}
);
});
const handleImportFrp = () => {
ipcRenderer.send("download.importFrpFile");
send(ipcRouters.VERSION.importLocalFrpcVersion);
};
onUnmounted(() => {
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnProgress");
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnCompleted");
ipcRenderer.removeAllListeners("Download.frpVersionHook");
ipcRenderer.removeAllListeners("Download.deleteVersion.hook");
ipcRenderer.removeAllListeners("Download.importFrpFile.hook");
removeRouterListeners(ipcRouters.VERSION.deleteDownloadedVersion);
removeRouterListeners(ipcRouters.VERSION.downloadVersion);
removeRouterListeners(ipcRouters.VERSION.getVersions);
removeRouterListeners(ipcRouters.VERSION.importLocalFrpcVersion);
});
</script>
<template>
<div class="main">
<!-- <breadcrumb> -->
<breadcrumb>
<div class="flex">
<div class="h-full flex items-center justify-center mr-4">
<div class="h-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">下载源 </span>
<el-select
class="w-40"
@ -161,11 +170,10 @@ onUnmounted(() => {
/>
</el-select>
</div>
<el-button class="mr-2" type="primary" @click="handleImportFrp">
<el-button 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"-->
@ -181,7 +189,7 @@ onUnmounted(() => {
<div class="app-container-breadcrumb pr-2" v-loading="loading > 0">
<div class="w-full">
<template v-if="versions && versions.length > 0">
<el-row :gutter="20">
<el-row :gutter="15">
<!-- <el-col :span="24">-->
<!-- <div class="h2 flex justify-between !mb-[10px]">-->
<!-- <div>镜像源</div>-->
@ -199,16 +207,16 @@ onUnmounted(() => {
<!-- </el-col>-->
<el-col
v-for="version in versions"
:key="version.id"
:key="version.githubAssetId"
:lg="6"
:md="8"
:sm="12"
:xl="6"
:xs="12"
class="mb-[20px]"
class="mb-[15px]"
>
<div
class="w-full download-card bg-white rounded p-4 drop-shadow flex justify-between items-center"
class="w-full download-card bg-white rounded p-4 drop-shadow flex justify-between items-center animate__animated"
>
<div class="left">
<div class="mb-2 flex items-center justify-center">
@ -223,19 +231,19 @@ onUnmounted(() => {
<span class="text-primary font-bold"
>{{
// moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
version.download_count
version.versionDownloadCount
}}
</span>
</div>
<div class="text-[12px]">
发布时间<span class="text-primary font-bold">{{
// moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
version.published_at
version.githubCreatedAt
}}</span>
</div>
</div>
<div class="right">
<div v-if="version.download_completed">
<div v-if="version.downloaded">
<!-- <span class="text-[12px] text-primary font-bold mr-2"-->
<!-- >已下载</span-->
<!-- >-->
@ -259,14 +267,15 @@ onUnmounted(() => {
</el-button>
</div>
<!-- <el-button type="text"></el-button>-->
</div>
<template v-else>
<div class="w-32" v-if="downloading.has(version.id)">
<div
class="w-32"
v-if="downloading.has(version.githubReleaseId)"
>
<el-progress
:percentage="downloading.get(version.id)"
:percentage="downloading.get(version.githubReleaseId)"
:text-inside="false"
/>
</div>
@ -300,4 +309,18 @@ onUnmounted(() => {
.download-card {
border-left: 5px solid #5a3daa;
}
//@keyframes download-card-animation {
// //0% {
// // border-left-width: 5px;
// //}
// 100% {
// border-left-width: 10px;
// }
//}
//
//
//.download-card:hover {
// animation: download-card-animation 0.5s;
//}
</style>

View File

@ -1,27 +1,33 @@
<script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ipcRenderer } from "electron";
import { useDebounceFn } from "@vueuse/core";
import { on, removeRouterListeners, send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
import { useFrpcDesktopStore } from "@/store/frpcDesktop";
import { ElMessageBox } from "element-plus";
import router from "@/router";
import { useDebounceFn, useIntervalFn } from "@vueuse/core";
defineComponent({
name: "Home"
});
const running = ref(false);
const frpcDesktopStore = useFrpcDesktopStore();
// const running = ref(false);
const loading = ref(false);
const handleStartFrpc = () => {
ipcRenderer.send("frpc.start");
send(ipcRouters.LAUNCH.launch);
};
const handleStopFrpc = () => {
ipcRenderer.send("frpc.stop");
send(ipcRouters.LAUNCH.terminate);
};
const handleButtonClick = useDebounceFn(() => {
if (running.value) {
loading.value = true;
if (frpcDesktopStore.running) {
handleStopFrpc();
} else {
handleStartFrpc();
@ -29,30 +35,54 @@ const handleButtonClick = useDebounceFn(() => {
}, 300);
onMounted(() => {
useIntervalFn(() => {
ipcRenderer.invoke("frpc.running").then(data => {
running.value = data;
console.log("进程状态", data);
});
}, 500);
ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => {
if (args) {
ElMessageBox.alert(args, "提示", {
showCancelButton: true,
cancelButtonText: "取消",
confirmButtonText: "去设置"
}).then(() => {
router.replace({
name: "Config"
on(
ipcRouters.LAUNCH.launch,
() => {
// send(ipcRouters.LAUNCH.getStatus);
frpcDesktopStore.refreshRunning();
loading.value = false;
},
(bizCode: string, message: string) => {
if (bizCode === "B1001") {
ElMessageBox.alert("请先前往设置页面,修改配置后再启动", "提示", {
// showCancelButton: true,
// cancelButtonText: "",
confirmButtonText: "去设置"
}).then(() => {
router.replace({
name: "Config"
});
});
});
}
loading.value = false;
}
);
on(ipcRouters.LAUNCH.terminate, () => {
// send(ipcRouters.LAUNCH.getStatus);
frpcDesktopStore.refreshRunning();
loading.value = false;
});
// ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => {
// if (args) {
// ElMessageBox.alert(args, "", {
// showCancelButton: true,
// cancelButtonText: "",
// confirmButtonText: ""
// }).then(() => {
// router.replace({
// name: "Config"
// });
// });
// }
// });
});
onUnmounted(() => {
ipcRenderer.removeAllListeners("Home.frpc.start.error.hook");
// ipcRenderer.removeAllListeners("Home.frpc.start.error.hook");
// removeRouterListeners2(listeners.watchFrpcProcess);
removeRouterListeners(ipcRouters.LAUNCH.launch);
removeRouterListeners(ipcRouters.LAUNCH.terminate);
});
</script>
@ -69,19 +99,19 @@ onUnmounted(() => {
>
<transition name="fade">
<div
v-show="running"
v-show="frpcDesktopStore.running"
class="z-0 rounded-full opacity-20 left-circle bg-[#5A3DAA] w-full h-full animation-rotate-1"
/>
</transition>
<transition name="fade">
<div
v-show="running"
v-show="frpcDesktopStore.running"
class="z-0 rounded-full opacity-20 right-circle top-[10px] bg-[#5A3DAA] w-full h-full animation-rotate-2"
/>
</transition>
<transition name="fade">
<div
v-show="running"
v-show="frpcDesktopStore.running"
class="z-0 rounded-full opacity-20 top-circle bg-[#5A3DAA] w-full h-full animation-rotate-3"
/>
</transition>
@ -96,7 +126,7 @@ onUnmounted(() => {
<transition name="fade">
<div class="font-bold text-2xl text-center">
<IconifyIconOffline
v-if="running"
v-if="frpcDesktopStore.running"
class="text-[#7EC050] inline-block relative top-1"
icon="check-circle-rounded"
/>
@ -105,7 +135,7 @@ onUnmounted(() => {
class="text-[#E47470] inline-block relative top-1"
icon="error"
/>
Frpc {{ running ? "已启动" : "已断开" }}
Frpc {{ frpcDesktopStore.running ? "已启动" : "已断开" }}
</div>
</transition>
<!-- <el-button-->
@ -117,18 +147,25 @@ onUnmounted(() => {
<!-- </el-button>-->
<div class="w-full justify-center text-center">
<el-link
v-if="running"
v-if="frpcDesktopStore.running"
type="primary"
@click="$router.replace({ name: 'Logger' })"
>查看日志</el-link
>
</div>
<div
class="w-full h-8 bg-[#563EA4] rounded flex justify-center items-center text-white font-bold cursor-pointer"
<el-button
type="primary"
@click="handleButtonClick"
>
{{ running ? "断 开" : "启 动" }}
</div>
size="large"
:disabled="loading"
>{{ frpcDesktopStore.running ? "断 开" : "启 动" }}
</el-button>
<!-- <div-->
<!-- class="w-full h-8 bg-[#563EA4] rounded flex justify-center items-center text-white font-bold cursor-pointer"-->
<!-- >-->
<!-- </div>-->
</div>
</div>
</div>

View File

@ -1,10 +1,17 @@
<script lang="ts" setup>
import { createVNode, defineComponent, onMounted, onUnmounted, ref } from "vue";
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ipcRenderer } from "electron";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { useDebounce, useDebounceFn } from "@vueuse/core";
import { useDebounceFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
import { ipcRouters, listeners } from "../../../electron/core/IpcRouter";
import {
on,
onListener,
removeRouterListeners,
removeRouterListeners2,
send
} from "@/utils/ipcUtils";
defineComponent({
name: "Logger"
@ -12,7 +19,7 @@ defineComponent({
const loggerContent = ref('<div class="text-white">暂无日志</div>');
const handleLog2Html = (logContent: string) => {
const formatLogContent = (logContent: string) => {
const logs = logContent
.split("\n")
.filter(f => f)
@ -31,21 +38,17 @@ const handleLog2Html = (logContent: string) => {
});
return logs.reverse().join("");
};
const refreshStatus = ref(false);
const logLoading = ref(true);
// const isWatch = ref(false);
onMounted(() => {
console.log('logger mounted')
ipcRenderer.send("logger.getLog");
ipcRenderer.on("Logger.getLog.hook", (event, args) => {
// console.log("", args, args.indexOf("\n"));
// const logs = args.split("\n");
// console.log(logs, "2");
if (args) {
loggerContent.value = handleLog2Html(args);
send(ipcRouters.LOG.getFrpLogContent);
on(ipcRouters.LOG.getFrpLogContent, data => {
if (data) {
loggerContent.value = formatLogContent(data as string);
}
logLoading.value = false;
if (refreshStatus.value) {
//
@ -53,29 +56,23 @@ onMounted(() => {
type: "success",
message: "刷新成功"
});
} else {
ipcRenderer.send("logger.update");
refreshStatus.value = false;
}
});
ipcRenderer.on("Logger.update.hook", (event, args) => {
console.log("logger update hook", 1);
if (args) {
loggerContent.value = handleLog2Html(args);
}
on(ipcRouters.LOG.openFrpcLogFile, () => {
ElMessage({
type: "success",
message: "打开日志成功"
});
});
ipcRenderer.on("Logger.openLog.hook", (event, args) => {
if (args) {
ElMessage({
type: "success",
message: "打开日志成功"
});
}
onListener(listeners.watchFrpcLog, data => {
console.log("onListener Data", data);
send(ipcRouters.LOG.getFrpLogContent);
});
});
const openLocalLog = useDebounceFn(() => {
ipcRenderer.send("logger.openLog");
send(ipcRouters.LOG.openFrpcLogFile);
}, 1000);
const refreshLog = useDebounceFn(() => {
@ -86,13 +83,12 @@ const refreshLog = useDebounceFn(() => {
// });
refreshStatus.value = true;
logLoading.value = true;
ipcRenderer.send("logger.getLog");
send(ipcRouters.LOG.getFrpLogContent);
}, 300);
onUnmounted(() => {
console.log('logger unmounted')
ipcRenderer.removeAllListeners("Logger.getLog.hook");
ipcRenderer.removeAllListeners("Logger.openLog.hook");
removeRouterListeners(ipcRouters.LOG.getFrpLogContent);
removeRouterListeners2(listeners.watchFrpcLog);
});
</script>
<template>

View File

@ -9,12 +9,13 @@ import {
} from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import { ipcRenderer } from "electron";
import { clone } from "@/utils/clone";
import { formatDate, useClipboard, useDebounceFn } from "@vueuse/core";
import { useClipboard, useDebounceFn } from "@vueuse/core";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import commonIps from "./commonIp.json";
import router from "@/router";
import path from "path";
import { on, removeRouterListeners, send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
import _ from "lodash";
defineComponent({
name: "Proxy"
@ -23,7 +24,7 @@ defineComponent({
/**
* 代理列表
*/
const proxys = ref<Array<Proxy>>([]);
const proxys = ref<Array<FrpcProxy>>([]);
/**
* loading
*/
@ -44,20 +45,21 @@ const edit = ref({
visible: false
});
const defaultForm = ref<Proxy>({
const defaultForm: FrpcProxy = {
_id: "",
hostHeaderRewrite: "",
locations: [],
name: "",
type: "http",
localIp: "",
localIP: "",
localPort: "8080",
remotePort: "8080",
customDomains: [""],
stcpModel: "visitors",
visitorsModel: "visitors",
serverName: "",
secretKey: "",
bindAddr: "",
bindPort: null,
status: true,
subdomain: "",
basicAuth: false,
httpUser: "",
@ -67,27 +69,29 @@ const defaultForm = ref<Proxy>({
https2http: false,
https2httpCaFile: "",
https2httpKeyFile: "",
keepTunnelOpen: false
});
keepTunnelOpen: false,
status: 1
};
/**
* 表单内容
*/
const editForm = ref<Proxy>(defaultForm.value);
const editForm = ref<FrpcProxy>(_.cloneDeep(defaultForm));
/**
* 代理类型
*/
const proxyTypes = ref(["http", "https", "tcp", "udp", "stcp", "xtcp", "sudp"]);
const currSelectLocalFileType = ref();
const stcpModels = ref([
const visitorsModels = ref([
{
label: "访问者",
value: "visitors"
},
{
label: "被访问者",
value: "visited"
value: "visitorsProvider"
}
]);
@ -104,7 +108,7 @@ const editFormRules = reactive<FormRules>({
// }
],
type: [{ required: true, message: "请选择类型", trigger: "blur" }],
localIp: [
localIP: [
{ required: true, message: "请输入内网地址", trigger: "blur" },
{
pattern: /^[\w-]+(\.[\w-]+)+$/,
@ -128,7 +132,9 @@ const editFormRules = reactive<FormRules>({
trigger: "blur"
}
],
stcpModel: [{ required: true, message: "请选择stcp模式", trigger: "blur" }],
visitorsModel: [
{ required: true, message: "请选择stcp模式", trigger: "blur" }
],
secretKey: [
{ required: true, message: "请输入stcp共享密钥", trigger: "blur" }
],
@ -151,6 +157,7 @@ const editFormRules = reactive<FormRules>({
httpPassword: [{ required: true, message: "请输入认证密码", trigger: "blur" }]
});
/**
* 表单dom
*/
@ -184,12 +191,12 @@ const isXtcp = computed(() => {
return editForm.value.type === "xtcp";
});
const isStcpVisited = computed(() => {
const isStcpvisitorsProvider = computed(() => {
return (
(editForm.value.type === "stcp" ||
editForm.value.type === "sudp" ||
editForm.value.type === "xtcp") &&
editForm.value.stcpModel === "visited"
editForm.value.visitorsModel === "visitorsProvider"
);
});
@ -198,7 +205,7 @@ const isStcpVisitors = computed(() => {
(editForm.value.type === "stcp" ||
editForm.value.type === "sudp" ||
editForm.value.type === "xtcp") &&
editForm.value.stcpModel === "visitors"
editForm.value.visitorsModel === "visitors"
);
});
@ -247,7 +254,7 @@ const handleRangePort = () => {
*/
const handleSubmit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(valid => {
editFormRef.value.validate(valid => {
if (valid) {
if (handleRangePort()) {
const lc = handleGetPortCount(editForm.value.localPort);
@ -276,11 +283,12 @@ const handleSubmit = async () => {
return;
}
loading.value.form = 1;
const data = clone(editForm.value);
const data = _.cloneDeep(editForm.value);
console.log("submit", data);
if (data._id) {
ipcRenderer.send("proxy.updateProxy", data);
send(ipcRouters.PROXY.modifyProxy, data);
} else {
ipcRenderer.send("proxy.insertProxy", data);
send(ipcRouters.PROXY.createProxy, data);
}
}
});
@ -305,97 +313,25 @@ const handleDeleteDomain = (index: number) => {
* 加载代理
*/
const handleLoadProxys = () => {
ipcRenderer.send("proxy.getProxys");
send(ipcRouters.PROXY.getAllProxies);
};
/**
* 删除代理
* @param proxy
*/
const handleDeleteProxy = (proxy: Proxy) => {
ipcRenderer.send("proxy.deleteProxyById", proxy._id);
const handleDeleteProxy = (proxy: FrpcProxy) => {
send(ipcRouters.PROXY.deleteProxy, proxy._id);
// ipcRenderer.send("proxy.deleteProxyById", proxy._id);
};
/**
* 重置表单
*/
const handleResetForm = () => {
editForm.value = defaultForm.value;
editForm.value = _.cloneDeep(defaultForm);
};
/**
* 初始化回调
*/
const handleInitHook = () => {
const InsertOrUpdateHook = (message: string, args: any) => {
loading.value.form--;
const { err } = args;
if (!err) {
ElMessage({
type: "success",
message: message
});
handleResetForm();
handleLoadProxys();
edit.value.visible = false;
}
};
ipcRenderer.on("Proxy.insertProxy.hook", (event, args) => {
InsertOrUpdateHook("新增成功", args);
});
ipcRenderer.on("Proxy.updateProxy.hook", (event, args) => {
InsertOrUpdateHook("修改成功", args);
});
ipcRenderer.on("Proxy.updateProxyStatus.hook", (event, args) => {
if (args.data > 0) {
handleLoadProxys();
}
console.log("更新结果", args);
});
ipcRenderer.on("local.getLocalPorts.hook", (event, args) => {
loading.value.localPorts--;
localPorts.value = args.data;
console.log("内网端口", localPorts.value);
});
// ipcRenderer.on("Proxy.updateProxy.hook", (event, args) => {
// loading.value.form--;
// const { err } = args;
// if (!err) {
// ElMessage({
// type: "success",
// message: ""
// });
// handleResetForm();
// handleLoadProxys();
// edit.value.visible = false;
// }
// });
ipcRenderer.on("Proxy.getProxys.hook", (event, args) => {
loading.value.list--;
const { err, data } = args;
if (!err) {
data.forEach(f => {
if (f.status === null || f.status === undefined) {
f.status = true;
}
});
proxys.value = data;
}
});
ipcRenderer.on("Proxy.deleteProxyById.hook", (event, args) => {
const { err, data } = args;
if (!err) {
handleLoadProxys();
ElMessage({
type: "success",
message: "删除成功"
});
}
});
};
const handleOpenInsert = () => {
edit.value = {
title: "新增代理",
@ -403,32 +339,33 @@ const handleOpenInsert = () => {
};
};
const handleOpenUpdate = (proxy: Proxy) => {
editForm.value = clone(proxy);
if (!editForm.value.fallbackTimeoutMs) {
editForm.value.fallbackTimeoutMs = defaultForm.value.fallbackTimeoutMs;
}
const handleOpenUpdate = (proxy: FrpcProxy) => {
editForm.value = _.cloneDeep(proxy);
// if (!editForm.value.fallbackTimeoutMs) {
// editForm.value.fallbackTimeoutMs = defaultForm.fallbackTimeoutMs;
// }
edit.value = {
title: "修改代理",
visible: true
};
};
const handleReversalUpdate = (proxy: Proxy) => {
console.log("更新", proxy);
ipcRenderer.send("proxy.updateProxyStatus", {
_id: proxy._id,
status: !proxy.status
const handleReversalUpdate = (proxy: FrpcProxy) => {
send(ipcRouters.PROXY.modifyProxyStatus, {
id: proxy._id,
status: proxy.status === 1 ? 0 : 1
});
};
const handleLoadLocalPorts = () => {
loading.value.localPorts = 1;
ipcRenderer.send("local.getLocalPorts");
// ipcRenderer.send("local.getLocalPorts");
send(ipcRouters.PROXY.getLocalPorts);
};
const handleSelectLocalPort = useDebounceFn((port: number) => {
editForm.value.localPort = port?.toString();
editForm.value.localIP = "127.0.0.1";
handleCloseLocalPortDialog();
});
@ -441,36 +378,36 @@ const handleOpenLocalPortDialog = () => {
handleLoadLocalPorts();
};
const allowCopyAccessAddress = (proxy: Proxy) => {
const allowCopyAccessAddress = (proxy: FrpcProxy) => {
if (
(proxy.type === "http" || proxy.type === "https") &&
(proxy.customDomains.length < 1 || !proxy.customDomains[0])
) {
return false;
}
if (proxy.type === "stcp" && proxy.stcpModel === "visited") {
if (proxy.type === "stcp" && proxy.visitorsModel === "visitorsProvider") {
return false;
}
if (proxy.type === "xtcp" && proxy.stcpModel === "visited") {
if (proxy.type === "xtcp" && proxy.visitorsModel === "visitorsProvider") {
return false;
}
if (proxy.type === "sudp" && proxy.stcpModel === "visited") {
if (proxy.type === "sudp" && proxy.visitorsModel === "visitorsProvider") {
return false;
}
return true;
};
const handleCopyAccessAddress = (proxy: Proxy) => {
const handleCopyAccessAddress = (proxy: FrpcProxy) => {
if (
(proxy.type === "http" || proxy.type === "https") &&
(proxy.customDomains.length < 1 || !proxy.customDomains[0])
) {
return;
}
if (proxy.type === "stcp" && proxy.stcpModel === "visited") {
if (proxy.type === "stcp" && proxy.visitorsModel === "visitorsProvider") {
return;
}
if (proxy.type === "xtcp" && proxy.stcpModel === "visited") {
if (proxy.type === "xtcp" && proxy.visitorsModel === "visitorsProvider") {
return;
}
let accessAddressStr = "";
@ -565,59 +502,139 @@ const handleRandomProxyName = () => {
`df_${editForm.value.type}_${result}`.toLocaleLowerCase();
};
import path from "path";
function normalizePath(filePath: string) {
const normalizePath = (filePath: string) => {
return path.normalize(filePath).replace(/\\/g, "/");
}
};
const handleSelectFile = (type: number, ext: string[]) => {
ipcRenderer.invoke("file.selectFile", ext).then(r => {
switch (type) {
case 1:
editForm.value.https2httpCaFile = normalizePath(r[0]);
break;
case 2:
editForm.value.https2httpKeyFile = normalizePath(r[0]);
break;
}
console.log(r);
currSelectLocalFileType.value = type;
send(ipcRouters.SYSTEM.selectLocalFile, {
name: "",
extensions: ext
});
// ipcRenderer.invoke("file.selectFile", ext).then(r => {
// switch (type) {
// case 1:
// editForm.value.https2httpCaFile = normalizePath(r[0]);
// break;
// case 2:
// editForm.value.https2httpKeyFile = normalizePath(r[0]);
// break;
// }
// console.log(r);
// });
};
onMounted(() => {
handleInitHook();
handleLoadProxys();
ipcRenderer.send("config.getConfig");
ipcRenderer.on("Config.getConfig.hook", (event, args) => {
const { err, data } = args;
if (!err) {
if (data) {
frpcConfig.value = data;
on(ipcRouters.PROXY.getAllProxies, data => {
console.log("allProxies", data);
loading.value.list--;
proxys.value = data;
});
on(ipcRouters.SYSTEM.selectLocalFile, data => {
console.log("data", data);
if (!data.canceled) {
switch (currSelectLocalFileType.value) {
case 1:
editForm.value.https2httpCaFile = data.path as string;
break;
case 2:
editForm.value.https2httpKeyFile = data.path as string;
break;
}
}
});
const insertOrUpdateHook = (message: string) => {
loading.value.form--;
// const { err } = args;
// if (!err) {
ElMessage({
type: "success",
message: message
});
handleResetForm();
handleLoadProxys();
edit.value.visible = false;
// }
};
on(ipcRouters.PROXY.createProxy, data => {
insertOrUpdateHook("新增成功");
});
on(ipcRouters.PROXY.modifyProxy, data => {
insertOrUpdateHook("修改成功");
});
on(ipcRouters.PROXY.deleteProxy, () => {
handleLoadProxys();
ElMessage({
type: "success",
message: "删除成功"
});
});
on(ipcRouters.PROXY.modifyProxyStatus, () => {
ElMessage({
type: "success",
message: "修改成功"
});
// handleResetForm();
handleLoadProxys();
// edit.value.visible = false;
});
on(ipcRouters.PROXY.getLocalPorts, data => {
loading.value.localPorts--;
localPorts.value = data;
});
// ipcRenderer.send("config.getConfig");
// ipcRenderer.on("Config.getConfig.hook", (event, args) => {
// const { err, data } = args;
// if (!err) {
// if (data) {
// frpcConfig.value = data;
// }
// }
// });
});
const handleProxyTypeChange = e => {
if (e === "http" || e === "https" || e === "tcp" || e === "udp") {
editForm.value.visitorsModel = "";
} else {
if (editForm.value.visitorsModel === "") {
editForm.value.visitorsModel = "visitorsProvider";
}
}
};
onUnmounted(() => {
ipcRenderer.removeAllListeners("Proxy.insertProxy.hook");
ipcRenderer.removeAllListeners("Proxy.updateProxy.hook");
ipcRenderer.removeAllListeners("Proxy.updateProxyStatus.hook");
ipcRenderer.removeAllListeners("Proxy.deleteProxyById.hook");
ipcRenderer.removeAllListeners("Proxy.getProxys.hook");
ipcRenderer.removeAllListeners("local.getLocalPorts.hook");
removeRouterListeners(ipcRouters.PROXY.createProxy);
removeRouterListeners(ipcRouters.PROXY.modifyProxy);
removeRouterListeners(ipcRouters.PROXY.deleteProxy);
removeRouterListeners(ipcRouters.PROXY.getAllProxies);
removeRouterListeners(ipcRouters.PROXY.modifyProxyStatus);
removeRouterListeners(ipcRouters.PROXY.getLocalPorts);
removeRouterListeners(ipcRouters.SYSTEM.selectLocalFile);
});
</script>
<template>
<!-- <coming-soon />-->
<div class="main">
<breadcrumb>
<el-button class="mr-2" type="primary" @click="handleOpenInsert">
<el-button type="primary" @click="handleOpenInsert">
<IconifyIconOffline icon="add" />
</el-button>
</breadcrumb>
<div class="app-container-breadcrumb pr-2" v-loading="loading.list > 0">
<div class="app-container-breadcrumb" v-loading="loading.list > 0">
<template v-if="proxys && proxys.length > 0">
<el-row :gutter="20">
<el-row :gutter="15">
<el-col
v-for="proxy in proxys"
:key="proxy._id"
@ -626,13 +643,13 @@ onUnmounted(() => {
:sm="12"
:xl="6"
:xs="12"
class="mb-[20px]"
class="mb-[15px]"
>
<div class="bg-white w-full rounded drop-shadow-xl p-4">
<div class="w-full flex justify-between">
<div class="flex">
<div
class="w-12 h-12 rounded mr-4 flex justify-center items-center font-bold"
class="w-12 h-12 rounded mr-3 flex justify-center items-center font-bold"
:class="proxy.type"
>
<span class="text-white text-sm">{{ proxy.type }}</span>
@ -642,7 +659,7 @@ onUnmounted(() => {
<span>{{ proxy.name }}</span>
</div>
<el-tag
v-if="!proxy.status"
v-if="proxy.status === 0"
class="mr-2"
type="danger"
size="small"
@ -653,7 +670,7 @@ onUnmounted(() => {
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
proxy.visitorsModel === 'visitors'
"
size="small"
>
@ -665,7 +682,7 @@ onUnmounted(() => {
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visited'
proxy.visitorsModel === 'visitorsProvider'
"
>被访问者
</el-tag>
@ -738,11 +755,11 @@ onUnmounted(() => {
(proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors'
proxy.visitorsModel !== 'visitors'
"
>
<p class="text-[#ADADAD] font-bold">内网地址</p>
<p>{{ proxy.localIp }}</p>
<p>{{ proxy.localIP }}</p>
</div>
<div class="text-sm text-center" v-if="proxy.type === 'tcp'">
@ -755,7 +772,7 @@ onUnmounted(() => {
(proxy.type !== 'stcp' &&
proxy.type !== 'xtcp' &&
proxy.type !== 'sudp') ||
proxy.stcpModel !== 'visitors'
proxy.visitorsModel !== 'visitors'
"
>
<p class="text-[#ADADAD] font-bold">内网端口</p>
@ -768,7 +785,7 @@ onUnmounted(() => {
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
proxy.visitorsModel === 'visitors'
"
>
<p class="text-[#ADADAD] font-bold">访问者名称</p>
@ -781,7 +798,7 @@ onUnmounted(() => {
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
proxy.visitorsModel === 'visitors'
"
>
<p class="text-[#ADADAD] font-bold">绑定地址</p>
@ -794,7 +811,7 @@ onUnmounted(() => {
(proxy.type === 'stcp' ||
proxy.type === 'xtcp' ||
proxy.type === 'sudp') &&
proxy.stcpModel === 'visitors'
proxy.visitorsModel === 'visitors'
"
>
<p class="text-[#ADADAD] font-bold">绑定端口</p>
@ -819,7 +836,7 @@ onUnmounted(() => {
v-model="edit.visible"
direction="rtl"
size="60%"
@close="editForm = defaultForm"
@close="editForm = _.cloneDeep(defaultForm)"
>
<!-- <el-dialog-->
<!-- v-model="edit.visible"-->
@ -838,7 +855,10 @@ onUnmounted(() => {
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="代理类型:" prop="type">
<el-radio-group v-model="editForm.type">
<el-radio-group
v-model="editForm.type"
@change="handleProxyTypeChange"
>
<el-radio-button
v-for="p in proxyTypes"
:key="p"
@ -850,10 +870,13 @@ onUnmounted(() => {
</el-col>
<template v-if="isStcp || isSudp || isXtcp">
<el-col :span="12">
<el-form-item :label="`${editForm.type}模式:`" prop="stcpModel">
<el-radio-group v-model="editForm.stcpModel">
<el-form-item
:label="`${editForm.type}模式:`"
prop="visitorsModel"
>
<el-radio-group v-model="editForm.visitorsModel">
<el-radio
v-for="p in stcpModels"
v-for="p in visitorsModels"
:key="p.value"
:label="p.label"
:value="p.value"
@ -918,17 +941,19 @@ onUnmounted(() => {
</el-button>
</el-form-item>
</el-col>
<template v-if="!(isStcp || isXtcp || isSudp) || isStcpVisited">
<template
v-if="!(isStcp || isXtcp || isSudp) || isStcpvisitorsProvider"
>
<el-col :span="12">
<el-form-item label="内网地址:" prop="localIp">
<el-form-item label="内网地址:" prop="localIP">
<el-autocomplete
v-model="editForm.localIp"
v-model="editForm.localIP"
:fetch-suggestions="handleIpFetchSuggestions"
clearable
placeholder="127.0.0.1"
/>
<!-- <el-input-->
<!-- v-model="editForm.localIp"-->
<!-- v-model="editForm.localIP"-->
<!-- placeholder="127.0.0.1"-->
<!-- clearable-->
<!-- />-->

View File

@ -22,7 +22,7 @@
]
},
"types": [
// "node",
// "node",
"vite/client",
"element-plus/global"
],

46
types/core.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
interface ApiResponse<T> {
bizCode: string;
data: T;
message: string;
}
interface ControllerParam {
// win: BrowserWindow;
channel: string;
event: Electron.IpcMainEvent;
args: any;
}
interface ListenerParam {
// win: BrowserWindow;
channel: string;
args: any[];
}
type IpcRouter = {
path: string;
controller: string;
}
type Listener = {
channel: string;
listenerMethod: any;
};
enum IpcRouterKeys {
SERVER = "SERVER",
LOG = "LOG",
VERSION = "VERSION",
LAUNCH = "LAUNCH",
PROXY = "PROXY",
SYSTEM = "SYSTEM",
}
type IpcRouters = Record<
IpcRouterKeys,
{
[method: string]: IpcRouter;
}
>;
type Listeners = Record<string, Listener>;

81
types/frp.d.ts vendored Normal file
View File

@ -0,0 +1,81 @@
type LogConfig = {
to: string;
level: string;
maxDays: number;
disablePrintColor: boolean;
};
type AuthConfig = {
method: string;
token: string;
};
type WebServerConfig = {
addr: string;
port: number;
user: string;
password: string;
pprofEnable: boolean;
};
type TransportTlsConfig = {
enable: boolean;
certFile: string;
keyFile: string;
trustedCaFile: string;
serverName: string;
disableCustomTLSFirstByte: boolean;
};
type TransportConfig = {
dialServerTimeout: number;
dialServerKeepalive: number;
poolCount: number;
tcpMux: boolean;
tcpMuxKeepaliveInterval: number;
protocol: string;
connectServerLocalIP: string;
proxyURL: string;
tls: TransportTlsConfig;
heartbeatInterval: number;
heartbeatTimeout: number;
};
interface FrpcCommonConfig {
user: string;
serverAddr: string;
serverPort: number;
loginFailExit: boolean;
log: LogConfig;
auth: AuthConfig;
webServer: WebServerConfig;
transport: TransportConfig;
udpPacketSize: number;
metadatas: Record<string, any>;
}
interface FrpcProxyConfig {
name: string;
type: string;
localIP: string;
localPort: any;
remotePort: any;
customDomains: string[];
locations: string[];
hostHeaderRewrite: string;
visitorsModel: string;
serverName: string;
secretKey: string;
bindAddr: string;
bindPort: number;
subdomain: string;
basicAuth: boolean;
httpUser: string;
httpPassword: string;
fallbackTo: string;
fallbackTimeoutMs: number;
https2http: boolean;
https2httpCaFile: string;
https2httpKeyFile: string;
keepTunnelOpen: boolean;
}

39
types/frpc-desktop.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
type FrpcDesktopProxy = FrpcProxyConfig & {};
interface BaseEntity {
_id: string;
}
interface FrpcSystemConfiguration {
launchAtStartup: boolean;
silentStartup: boolean;
autoConnectOnStartup: boolean;
}
type FrpcDesktopServer = BaseEntity &
FrpcCommonConfig & {
frpcVersion: number;
// system: any;
};
type FrpcVersion = BaseEntity & {
githubReleaseId: number;
githubAssetId: number;
githubCreatedAt: string;
name: string;
assetName: string;
versionDownloadCount: number;
assetDownloadCount: number;
browserDownloadUrl: string;
downloaded: boolean;
localPath: string;
size: string;
};
type OpenSourceFrpcDesktopServer = FrpcDesktopServer & {
system: FrpcSystemConfiguration;
};
type FrpcProxy = BaseEntity & FrpcProxyConfig & {
status: number; // 0: disable 1: enable
};

18
types/github.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
interface GithubAsset {
id: number;
name: string;
content_type: string;
download_count: number;
size: number;
browser_download_url: string;
created_at: string;
updated_at: string;
}
interface GithubRelease {
id: number;
name: string;
created_at: string;
published_at: string;
assets: GithubAsset[]
}

2
types/global.d.ts vendored
View File

@ -21,7 +21,7 @@ declare global {
localPort: any;
remotePort: string;
customDomains: string[];
stcpModel: string;
visitorsModel: string;
serverName: string;
secretKey: string;
bindAddr: string;

View File

@ -23,6 +23,13 @@ export default defineConfig(({ command }) => {
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return {
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler"
}
}
},
plugins: [
vue(),
electron([