🏗️ implement proxy management features and refactor related services

This commit is contained in:
刘嘉伟 2025-02-23 21:07:44 +08:00
parent 2edbbcb871
commit 9f46ea781d
23 changed files with 588 additions and 387 deletions

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,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,5 +1,17 @@
import BaseController from "./BaseController";
import FrpcProcessService from "../service/FrpcProcessService";
class LaunchController extends BaseController {
private readonly _frpcProcessService: FrpcProcessService;
constructor(frpcProcessService: FrpcProcessService) {
super();
this._frpcProcessService = frpcProcessService;
}
launch(req: ControllerParam) {
this._frpcProcessService.startFrpcProcess().then(r => {});
}
}
export default LaunchController;

View File

@ -1,7 +1,53 @@
import BaseController from "./BaseController";
import ProxyService from "../service/ProxyService";
import { success } from "../utils/response";
import ProxyDao from "../dao/ProxyDao";
class ProxyController extends BaseController {
constructor() {
private readonly _proxyService: ProxyService;
private readonly _proxyDao: ProxyDao;
constructor(proxyService: ProxyService, proxyDao: ProxyDao) {
super();
this._proxyService = proxyService;
this._proxyDao = proxyDao;
}
createProxy(req: ControllerParam) {
this._proxyService.insertProxy(req.args).then(data => {
req.event.reply(req.channel, success(data));
});
}
modifyProxy(req: ControllerParam) {
this._proxyService.updateProxy(req.args).then(data => {
req.event.reply(req.channel, success(data));
});
}
getAllProxies(req: ControllerParam) {
this._proxyDao.findAll().then(data => {
req.event.reply(req.channel, success(data));
});
}
deleteProxy(req: ControllerParam) {
this._proxyService.deleteProxy(req.args).then(data => {
req.event.reply(req.channel, success(data));
});
}
modifyProxyStatus(req: ControllerParam) {
this._proxyDao.updateProxyStatus(req.args.id, req.args.status).then(() => {
req.event.reply(req.channel, success());
});
}
getLocalPorts(req: ControllerParam) {
this._proxyService.getLocalPorts().then(data => {
req.event.reply(req.channel, success(data));
});
}
}
export default ProxyController;

View File

@ -1,19 +1,23 @@
import BaseController from "./BaseController";
import ServerService from "../service/ServerService";
import { success } from "../utils/response";
import FileService from "../service/FileService";
import PathUtils from "../utils/PathUtils";
class ServerController extends BaseController {
private readonly _serverService: ServerService;
private readonly _fileService: FileService;
constructor(serverService: ServerService) {
constructor(serverService: ServerService, fileService: FileService) {
super();
this._serverService = serverService;
this._fileService = fileService;
}
saveConfig(req: ControllerParam) {
this._serverService.saveServerConfig(req.args).then(() => {
req.event.reply(req.channel, success());
})
});
}
getServerConfig(req: ControllerParam) {
@ -21,7 +25,12 @@ class ServerController extends BaseController {
this._serverService.getServerConfig().then(data => {
req.event.reply(req.channel, success(data));
});
}
openAppData(req: ControllerParam) {
this._fileService.openLocalPath(PathUtils.getAppData()).then(data => {
req.event.reply(req.channel, success(data));
});
}
}

View File

@ -62,6 +62,10 @@ class VersionController extends BaseController {
req.event.reply(req.channel, fail());
});
}
importLocalFrpcVersion (){
}
}
export default VersionController;

View File

@ -9,6 +9,11 @@ import VersionController from "../controller/VersionController";
import FileService from "../service/FileService";
import VersionDao from "../dao/VersionDao";
import GitHubService from "../service/GitHubService";
import FrpcProcessService from "../service/FrpcProcessService";
import LaunchController from "../controller/LaunchController";
import ProxyDao from "../dao/ProxyDao";
import ProxyService from "../service/ProxyService";
import ProxyController from "../controller/ProxyController";
export const ipcRouters: IpcRouters = {
SERVER: {
@ -48,6 +53,38 @@ export const ipcRouters: IpcRouters = {
path: "version/deleteDownloadedVersion",
controller: "versionController.deleteDownloadedVersion"
}
},
LAUNCH: {
launch: {
path: "launch/launch",
controller: "launchController.launch"
}
},
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"
}
}
};
@ -70,8 +107,9 @@ class IpcRouterConfigurate {
private initializeBeans() {
const serverDao = new ServerDao();
const versionDao = new VersionDao();
const proxyDao = new ProxyDao();
const fileService = new FileService();
const serverService = new ServerService(serverDao);
const serverService = new ServerService(serverDao, proxyDao);
const gitHubService = new GitHubService();
const versionService = new VersionService(
versionDao,
@ -79,19 +117,31 @@ class IpcRouterConfigurate {
gitHubService
);
const logService = new LogService(fileService);
const serverController = new ServerController(serverService);
const frpcProcessService = new FrpcProcessService(
serverService,
versionDao
);
const proxyService = new ProxyService(proxyDao);
const serverController = new ServerController(serverService, fileService);
const versionController = new VersionController(versionService, versionDao);
const logController = new LogController(logService);
const launchController = new LaunchController(frpcProcessService);
const proxyController = new ProxyController(proxyService, proxyDao);
this._beans.set("serverDao", serverDao);
this._beans.set("versionDao", versionDao);
this._beans.set("proxyDao", proxyDao);
this._beans.set("fileService", fileService);
this._beans.set("serverService", serverService);
this._beans.set("versionService", versionService);
this._beans.set("logService", logService);
this._beans.set("proxyService", proxyService);
this._beans.set("frpcProcessService", frpcProcessService);
this._beans.set("serverController", serverController);
this._beans.set("versionController", versionController);
this._beans.set("logController", logController);
this._beans.set("launchController", launchController);
this._beans.set("proxyController", proxyController);
}
/**

26
electron/dao/ProxyDao.ts Normal file
View File

@ -0,0 +1,26 @@
import BaseDao from "./BaseDao";
class ProxyDao extends BaseDao<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 ProxyDao;

View File

@ -12,7 +12,6 @@ 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,
@ -21,7 +20,6 @@ import {
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 IpcRouterConfigurate from "../core/IpcRouter";
@ -201,8 +199,6 @@ app.whenReady().then(() => {
initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
initProxyApi();
logInfo(LogModule.APP, `Proxy API initialized.`);
initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
@ -215,8 +211,6 @@ app.whenReady().then(() => {
initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`);
initLocalApi();
logInfo(LogModule.APP, `Local API initialized.`);
// initUpdaterApi(win);
logInfo(LogModule.APP, `Updater API initialization skipped.`);

View File

@ -1,10 +1,11 @@
import { shell } from "electron";
import { dialog, shell } from "electron";
import path from "path";
import fs from "fs";
import zlib from "zlib";
import admZip from "adm-zip";
import GlobalConstant from "../core/GlobalConstant";
// import tar from "tar";
const tar = require("tar");
@ -28,6 +29,18 @@ class FileService {
});
}
openLocalPath(path: string) {
return new Promise<boolean>((resolve, reject) => {
shell.openPath(path).then(errorMessage => {
if (errorMessage) {
resolve(false);
} else {
reject(true);
}
});
});
}
decompressZipFile(zipFilePath: string, targetPath: string) {
if (!zipFilePath.endsWith(GlobalConstant.ZIP_EXT)) {
throw new Error("The file is not a .zip file");
@ -86,6 +99,18 @@ class FileService {
// );
});
}
selectLocalFile(name: string, path: any) {
dialog.showOpenDialogSync({
properties: ["openFile"],
filters: [
{
name: name,
extensions: path
}
]
});
}
}
export default FileService;

View File

@ -1,13 +1,13 @@
import ServerService from "./ServerService";
import VersionDao from "../dao/VersionDao";
import PathUtils from "../utils/PathUtils";
import { frpcProcess } from "../api/frpc";
import GlobalConstant from "../core/GlobalConstant";
import { Notification } from "electron";
import { logDebug, LogModule } from "../utils/log";
const { exec, spawn } = require("child_process");
class FrpProcessService {
class FrpcProcessService {
private readonly _serverService: ServerService;
private readonly _versionDao: VersionDao;
private _frpcProcess: any;
@ -28,6 +28,7 @@ class FrpProcessService {
config.frpcVersion
);
// todo genConfigfile.
await this._serverService.genTomlConfig();
const configPath = "";
const command = `${PathUtils.getFrpcFilename()} -c ${configPath}`;
this._frpcProcess = spawn(command, {
@ -35,11 +36,11 @@ class FrpProcessService {
shell: true
});
frpcProcess.stdout.on("data", data => {
// logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
this._frpcProcess.stdout.on("data", data => {
logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
});
frpcProcess.stdout.on("error", data => {
this._frpcProcess.stdout.on("error", data => {
// logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`);
// stopFrpcProcess(() => {});
this.stopFrpcProcess();
@ -71,4 +72,4 @@ class FrpProcessService {
}
}
export default FrpProcessService;
export default FrpcProcessService;

View File

@ -0,0 +1,132 @@
import ProxyDao from "../dao/ProxyDao";
const { exec, spawn } = require("child_process");
class ProxyService {
private readonly _proxyDao: ProxyDao;
constructor(proxyDao: ProxyDao) {
this._proxyDao = proxyDao;
}
async insertProxy(proxy: FrpcProxy) {
return await this._proxyDao.insert(proxy);
}
async updateProxy(proxy: FrpcProxy) {
return await this._proxyDao.updateById(proxy._id, proxy);
}
async deleteProxy(proxyId: string) {
return await this._proxyDao.deleteById(proxyId);
}
async getLocalPorts(): Promise<Array<LocalPort>> {
const command =
process.platform === "win32"
? "netstat -a -n"
: "netstat -an | grep LISTEN";
return new Promise((resolve, reject) => {
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

@ -1,33 +1,68 @@
import BaseService from "./BaseService";
import ServerDao from "../dao/ServerDao";
import TOML from "smol-toml";
import fs from "fs";
import PathUtils from "../utils/PathUtils";
import ProxyDao from "../dao/ProxyDao";
class ServerService extends BaseService<FrpcDesktopServer> {
private readonly _serverDao: ServerDao;
constructor(serverDao: ServerDao) {
private readonly _proxyDao: ProxyDao;
private readonly _serverId: string = "1";
constructor(serverDao: ServerDao, proxyDao: ProxyDao) {
super();
this._serverDao = serverDao;
this._proxyDao = proxyDao;
}
async saveServerConfig(
frpcServer: FrpcDesktopServer
): Promise<FrpcDesktopServer> {
return await this._serverDao.updateById("1", frpcServer);
frpcServer._id = this._serverId;
return await this._serverDao.updateById(this._serverId, frpcServer);
}
async getServerConfig(): Promise<FrpcDesktopServer> {
return await this._serverDao.findById("1");
return await this._serverDao.findById(this._serverId);
}
hasServerConfig(): Promise<boolean> {
return new Promise((resolve, reject) => {
this._serverDao
.exists("1")
.exists(this._serverId)
.then(r => {
resolve(r);
})
.catch(err => reject(err));
});
}
async genTomlConfig() {
const server = await this.getServerConfig();
const proxies = await this._proxyDao.findAll();
const enabledProxies = proxies
.filter(f => f.status === 1)
.map(proxy => {
const { _id, status, ...frpProxyConfig } = proxy;
return frpProxyConfig
});
const { frpcVersion, _id, system, ...commonConfig } = server;
const frpcConfig = { ...commonConfig };
frpcConfig.log.to = PathUtils.getFrpcLogFilePath();
const toml = TOML.stringify({ ...frpcConfig, enabledProxies });
fs.writeFile(
PathUtils.getTomlConfigFilePath(), // 配置文件目录
toml, // 配置文件内容
{ flag: "w" },
err => {
if (err) {
} else {
// callback(filename);
}
}
);
}
}
export default ServerService;

View File

@ -4,20 +4,21 @@ import GitHubService from "./GitHubService";
import FileService from "./FileService";
import frpReleasesJson from "../json/frp-releases.json";
import { download } from "electron-dl";
import { BrowserWindow } from "electron";
import { BrowserWindow, dialog } 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";
class VersionService extends BaseService<FrpcVersion> {
private readonly _versionDao: VersionDao;
private readonly _fileService: FileService;
private readonly _gitHubService: GitHubService;
private readonly _currFrpArch: Array<string>;
private versions: Array<FrpcVersion> = [];
private _versions: Array<FrpcVersion> = [];
constructor(
versionDao: VersionDao,
@ -34,7 +35,7 @@ class VersionService extends BaseService<FrpcVersion> {
downloadFrpVersion(githubReleaseId: number, onProgress: Function) {
return new Promise((resolve, reject) => {
const version = this.versions.find(
const version = this._versions.find(
f => f.githubReleaseId === githubReleaseId
);
if (!version) {
@ -133,8 +134,8 @@ class VersionService extends BaseService<FrpcVersion> {
.then(async (releases: Array<GithubRelease>) => {
const versions: Array<FrpcVersion> =
await this.githubRelease2FrpcVersion(releases);
// const versions: Array<FrpcVersion> = (this.versions = versions);
this.versions = versions;
// const _versions: Array<FrpcVersion> = (this._versions = _versions);
this._versions = versions;
resolve(versions);
})
.catch(err => reject(err));
@ -158,6 +159,10 @@ class VersionService extends BaseService<FrpcVersion> {
): 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);
})
@ -197,6 +202,31 @@ class VersionService extends BaseService<FrpcVersion> {
}
return false;
}
async importLocalFrpcVersion(win: BrowserWindow) {
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "Frpc", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
]
});
if (result.canceled) {
return;
} else {
const filePath = result.filePaths[0];
const checksum = FileUtils.calculateFileChecksum(filePath);
const frpName = frpChecksums[checksum];
if (frpName) {
if (this._currFrpArch.every(item => frpName.includes(item))) {
const version = this.getFrpVersionByAssetName(frpName);
}
}
}
}
getFrpVersionByAssetName(assetName: string) {
return this._versions.find(f => f.assetName === assetName);
}
}
export default VersionService;

View File

@ -18,6 +18,12 @@ class FileUtils {
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;

View File

@ -2,29 +2,61 @@ import SecureUtils from "./SecureUtils";
import { app } from "electron";
import path from "path";
import FileUtils from "./FileUtils";
class PathUtils {
public static getDownloadStoragePath() {
return path.join(
PathUtils.getAppData(),
SecureUtils.calculateMD5("download")
);
const result = path.join(PathUtils.getAppData(), "download");
FileUtils.mkdir(result);
return result;
}
public static getVersionStoragePath() {
return path.join(
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")
return SecureUtils.calculateMD5("frpc");
}
public static getAppData() {
return app.getPath("userData");
}
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

@ -85,6 +85,7 @@
"snowflakify": "^1.0.5",
"tar": "^6.2.0",
"unused-filename": "^4.0.1",
"uuid": "^10.0.0"
"uuid": "^10.0.0",
"smol-toml": "^1.3.1"
}
}

View File

@ -200,15 +200,12 @@ const handleSubmit = useDebounceFn(() => {
if (valid) {
loading.value = 1;
const data = clone(formData.value);
ipcRenderer.send("server/saveConfig", data);
send(ipcRouters.SERVER.saveConfig, data);
// ipcRenderer.send("server/saveConfig", data);
}
});
}, 300);
const handleLoadVersions = () => {
ipcRenderer.send("config.versions");
};
const handleAuthMethodChange = e => {
if (e === "multiuser") {
ElMessageBox.alert(
@ -242,12 +239,14 @@ const handleLoadSavedConfig = () => {
send(ipcRouters.SERVER.getServerConfig);
};
onMounted(() => {
handleLoadDownloadedVersion();
handleLoadSavedConfig();
on(ipcRouters.SERVER.getServerConfig, data => {
console.log("data", data);
formData.value = data;
loading.value--;
});
@ -256,6 +255,14 @@ onMounted(() => {
versions.value = data;
// checkAndResetVersion();
});
on(ipcRouters.SERVER.saveConfig, data => {
ElMessage({
type: "success",
message: "保存成功"
});
loading.value--;
});
// ipcRenderer.send("config.getConfig");
// handleLoadVersions();
// ipcRenderer.on("Config.getConfig.hook", (event, args) => {

View File

@ -5,6 +5,8 @@ import { ipcRenderer } from "electron";
import { ElMessageBox } from "element-plus";
import router from "@/router";
import { useDebounceFn, useIntervalFn } from "@vueuse/core";
import { send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
defineComponent({
name: "Home"
@ -13,7 +15,8 @@ defineComponent({
const running = ref(false);
const handleStartFrpc = () => {
ipcRenderer.send("frpc.start");
// ipcRenderer.send("frpc.start");
send(ipcRouters.LAUNCH.launch);
};
const handleStopFrpc = () => {

View File

@ -15,6 +15,8 @@ import { useClipboard, useDebounceFn } from "@vueuse/core";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import commonIps from "./commonIp.json";
import path from "path";
import { on, removeRouterListeners, send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
defineComponent({
name: "Proxy"
@ -23,7 +25,7 @@ defineComponent({
/**
* 代理列表
*/
const proxys = ref<Array<Proxy>>([]);
const proxys = ref<Array<FrpcProxy>>([]);
/**
* loading
*/
@ -44,11 +46,13 @@ 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: [""],
@ -57,7 +61,6 @@ const defaultForm = ref<Proxy>({
secretKey: "",
bindAddr: "",
bindPort: null,
status: true,
subdomain: "",
basicAuth: false,
httpUser: "",
@ -67,13 +70,14 @@ const defaultForm = ref<Proxy>({
https2http: false,
https2httpCaFile: "",
https2httpKeyFile: "",
keepTunnelOpen: false
});
keepTunnelOpen: false,
status: 1
};
/**
* 表单内容
*/
const editForm = ref<Proxy>(defaultForm.value);
const editForm = ref<FrpcProxy>(defaultForm);
/**
* 代理类型
@ -104,7 +108,7 @@ const editFormRules = reactive<FormRules>({
// }
],
type: [{ required: true, message: "请选择类型", trigger: "blur" }],
localIp: [
localIP: [
{ required: true, message: "请输入内网地址", trigger: "blur" },
{
pattern: /^[\w-]+(\.[\w-]+)+$/,
@ -247,7 +251,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);
@ -278,9 +282,9 @@ const handleSubmit = async () => {
loading.value.form = 1;
const data = clone(editForm.value);
if (data._id) {
ipcRenderer.send("proxy.updateProxy", data);
send(ipcRouters.PROXY.createProxy, data);
} else {
ipcRenderer.send("proxy.insertProxy", data);
send(ipcRouters.PROXY.modifyProxy, data);
}
}
});
@ -305,97 +309,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 = 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,10 +335,10 @@ const handleOpenInsert = () => {
};
};
const handleOpenUpdate = (proxy: Proxy) => {
const handleOpenUpdate = (proxy: FrpcProxy) => {
editForm.value = clone(proxy);
if (!editForm.value.fallbackTimeoutMs) {
editForm.value.fallbackTimeoutMs = defaultForm.value.fallbackTimeoutMs;
editForm.value.fallbackTimeoutMs = defaultForm.fallbackTimeoutMs;
}
edit.value = {
title: "修改代理",
@ -414,17 +346,17 @@ const handleOpenUpdate = (proxy: Proxy) => {
};
};
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) => {
@ -441,7 +373,7 @@ 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])
@ -460,7 +392,7 @@ const allowCopyAccessAddress = (proxy: Proxy) => {
return true;
};
const handleCopyAccessAddress = (proxy: Proxy) => {
const handleCopyAccessAddress = (proxy: FrpcProxy) => {
if (
(proxy.type === "http" || proxy.type === "https") &&
(proxy.customDomains.length < 1 || !proxy.customDomains[0])
@ -584,26 +516,79 @@ const handleSelectFile = (type: number, ext: string[]) => {
};
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;
});
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 => {
console.log("data", data);
insertOrUpdateHook("新增成功");
});
on(ipcRouters.PROXY.modifyProxy, data => {
console.log("data", 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;
// }
// }
// });
});
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);
});
</script>
<template>
@ -641,7 +626,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"
@ -741,7 +726,7 @@ onUnmounted(() => {
"
>
<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'">
@ -919,15 +904,15 @@ onUnmounted(() => {
</el-col>
<template v-if="!(isStcp || isXtcp || isSudp) || isStcpVisited">
<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-->
<!-- />-->

2
types/core.d.ts vendored
View File

@ -31,6 +31,8 @@ enum IpcRouterKeys {
SERVER = "SERVER",
LOG = "LOG",
VERSION = "VERSION",
LAUNCH = "LAUNCH",
PROXY = "PROXY",
}
type IpcRouters = Record<

22
types/frp.d.ts vendored
View File

@ -58,6 +58,24 @@ interface FrpcProxyConfig {
name: string;
type: string;
localIP: string;
localPort: number;
remotePort: number;
localPort: any;
remotePort: any;
customDomains: string[];
locations: string[];
hostHeaderRewrite: string;
stcpModel: 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;
}

View File

@ -2,7 +2,7 @@ type FrpcDesktopProxy = FrpcProxyConfig & {};
interface BaseEntity {
_id: string;
};
}
interface FrpcSystemConfiguration {
launchAtStartup: boolean;
@ -10,9 +10,11 @@ interface FrpcSystemConfiguration {
autoConnectOnStartup: boolean;
}
type FrpcDesktopServer = FrpcCommonConfig & {
frpcVersion: number;
};
type FrpcDesktopServer = BaseEntity &
FrpcCommonConfig & {
frpcVersion: number;
system: any;
};
type FrpcVersion = BaseEntity & {
githubReleaseId: number;
@ -30,4 +32,8 @@ type FrpcVersion = BaseEntity & {
type OpenSourceFrpcDesktopServer = FrpcDesktopServer & {
system: FrpcSystemConfiguration;
};
type FrpcProxy = BaseEntity & FrpcProxyConfig & {
status: number; // 0: disable 1: enable
};