Files
server-config-cli/src/lib/tasks.js
2026-05-22 14:11:15 +08:00

336 lines
11 KiB
JavaScript

"use strict";
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
DEFAULT_FRP_VERSION,
DEFAULT_FRP_ARCH,
DEFAULT_FRP_SERVER_ADDR,
DEFAULT_FRP_SERVER_PORT,
DEFAULT_SERVICE_NAME,
defaultInstallDir,
defaultConfigPath,
parseFrpConfig,
renderParsedFrpConfig,
renderFrpConfig,
buildGlobals,
createUniqueProxyName,
assertProxyName,
} = require("./frp-config");
const NVM_INSTALL_VERSION = "v0.40.4";
const NVM_INSTALL_COMMAND = `set -eo pipefail
export NVM_DIR="$HOME/.nvm"
if [ ! -s "$NVM_DIR/nvm.sh" ]; then
if command -v curl >/dev/null 2>&1; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_INSTALL_VERSION}/install.sh | bash
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_INSTALL_VERSION}/install.sh | bash
else
echo "curl or wget is required to install nvm" >&2
exit 1
fi
fi
. "$NVM_DIR/nvm.sh"
nvm install --lts
nvm alias default 'lts/*'
nvm use --lts`;
const OH_MY_ZSH_COMMAND =
'RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://gitee.com/pocmon/ohmyzsh/raw/master/tools/install.sh)"';
function zshInstallPlan() {
const customDir = process.env.ZSH_CUSTOM || path.join(os.homedir(), ".oh-my-zsh", "custom");
const autosuggestionsDir = path.join(customDir, "plugins", "zsh-autosuggestions");
const syntaxHighlightingDir = path.join(customDir, "plugins", "zsh-syntax-highlighting");
const steps = [
run("apt update", "sudo", ["apt", "update"]),
run("apt upgrade", "sudo", ["apt", "upgrade", "-y"]),
run("install zsh & friends", "sudo", ["apt", "install", "zsh", "git", "curl", "wget", "-y"]),
run("change shell to zsh", "sudo", ["chsh", "-s", "/bin/zsh", os.userInfo().username]),
run("install oh-my-zsh", "sh", ["-c", OH_MY_ZSH_COMMAND]),
run("install nvm + node LTS", "bash", ["-c", NVM_INSTALL_COMMAND]),
gitPluginStep("zsh-autosuggestions", "https://github.com/zsh-users/zsh-autosuggestions", autosuggestionsDir),
gitPluginStep(
"zsh-syntax-highlighting",
"https://github.com/zsh-users/zsh-syntax-highlighting.git",
syntaxHighlightingDir,
),
{
kind: "zshrc-plugins",
label: "update ~/.zshrc plugin list",
path: path.join(os.homedir(), ".zshrc"),
plugins: ["git", "zsh-autosuggestions", "zsh-syntax-highlighting"],
},
];
return { title: "Install zsh + oh-my-zsh + nvm", steps };
}
function gitPluginStep(name, repo, destination) {
if (fs.existsSync(destination)) {
return run(`update ${name}`, "git", ["-C", destination, "pull", "--ff-only"]);
}
return run(`clone ${name}`, "git", ["clone", repo, destination]);
}
function sshInstallPlan() {
return {
title: "Install OpenSSH server",
steps: [
run("apt update", "sudo", ["apt", "update"]),
run("install openssh-server", "sudo", ["apt", "install", "openssh-server", "-y"]),
run("enable + start ssh", "sudo", ["systemctl", "enable", "--now", "ssh"]),
],
};
}
function frpInstallPlan(options = {}) {
const version = options.version || DEFAULT_FRP_VERSION;
const arch = options.arch || DEFAULT_FRP_ARCH;
const installDir = options.installDir || defaultInstallDir(version, arch);
const configPath = options.configPath || defaultConfigPath(installDir);
const serviceName = options.serviceName || DEFAULT_SERVICE_NAME;
const archiveName = `frp_${version}_linux_${arch}.tar.gz`;
const archivePath = `/tmp/${archiveName}`;
const extractedDir = `/tmp/frp_${version}_linux_${arch}`;
const url = `https://github.com/fatedier/frp/releases/download/v${version}/${archiveName}`;
const token = requireToken(options.token);
const force = Boolean(options.force);
const proxies = (fs.existsSync(configPath) && force)
? [...parseFrpConfig(fs.readFileSync(configPath, "utf8")).sections.values()].map((section) => ({
name: section.name,
...section.values,
}))
: [];
const configContent = renderFrpConfig(buildGlobalsFromOptions(token, options), proxies);
const serviceContent = renderSystemdService(installDir, configPath);
const steps = [
run("download frp archive", "wget", ["-O", archivePath, url]),
run("extract archive", "tar", ["-zxf", archivePath, "-C", "/tmp"]),
run("create install dir", "sudo", ["mkdir", "-p", installDir]),
run("copy frpc binary", "sudo", [
"cp",
"-f",
path.join(extractedDir, "frpc"),
path.join(installDir, "frpc"),
]),
run("chmod +x frpc", "sudo", ["chmod", "+x", path.join(installDir, "frpc")]),
];
if (!fs.existsSync(configPath) || force) {
steps.push(writeFile(`write ${path.basename(configPath)}`, configPath, configContent));
}
steps.push(writeFile(`write ${serviceName}.service`, `/etc/systemd/system/${serviceName}.service`, serviceContent));
steps.push(run("systemctl daemon-reload", "sudo", ["systemctl", "daemon-reload"]));
steps.push(run(`enable + start ${serviceName}`, "sudo", ["systemctl", "enable", "--now", serviceName]));
return { title: `Install frp ${version} (${serviceName})`, steps };
}
function frpInitConfigPlan(options = {}) {
const installDir = options.installDir || defaultInstallDir();
const configPath = options.configPath || defaultConfigPath(installDir);
const exists = fs.existsSync(configPath);
if (exists && !options.force) {
throw new Error(`${configPath} already exists. Re-run with --force to rewrite.`);
}
const token = requireToken(options.token);
const proxies = (exists && options.force)
? [...parseFrpConfig(fs.readFileSync(configPath, "utf8")).sections.values()].map((section) => ({
name: section.name,
...section.values,
}))
: [];
const content = renderFrpConfig(buildGlobalsFromOptions(token, options), proxies);
return {
title: `Rewrite ${configPath}`,
steps: [writeFile(`write ${path.basename(configPath)}`, configPath, content)],
};
}
function frpAddProxyPlan(options) {
const baseName = options.name;
if (!baseName) {
throw new Error("Proxy name is required.");
}
assertProxyName(baseName);
if (!options.localPort || !options.remotePort) {
throw new Error("local port and remote port are required.");
}
const installDir = options.installDir || defaultInstallDir();
const configPath = options.configPath || defaultConfigPath(installDir);
if (!fs.existsSync(configPath)) {
throw new Error(`${configPath} does not exist. Install frp or run init first.`);
}
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
const proxyName = createUniqueProxyName(baseName, config.sections);
config.sections.set(proxyName, {
name: proxyName,
values: {
type: options.type || "tcp",
local_ip: options.localIp || "127.0.0.1",
local_port: options.localPort,
remote_port: options.remotePort,
},
});
const content = renderParsedFrpConfig(config);
const steps = [writeFile(`write proxy ${proxyName}`, configPath, content)];
if (options.restart) {
steps.push(run(`restart ${options.serviceName || DEFAULT_SERVICE_NAME}`, "sudo", [
"systemctl",
"restart",
options.serviceName || DEFAULT_SERVICE_NAME,
]));
}
return {
title: `Add proxy ${proxyName}`,
steps,
meta: { proxyName },
};
}
function frpRemoveProxyPlan(options) {
if (!options.name) {
throw new Error("Proxy name is required.");
}
const installDir = options.installDir || defaultInstallDir();
const configPath = options.configPath || defaultConfigPath(installDir);
if (!fs.existsSync(configPath)) {
throw new Error(`${configPath} does not exist.`);
}
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
if (!config.sections.delete(options.name)) {
throw new Error(`No proxy named ${options.name} in ${configPath}.`);
}
const content = renderParsedFrpConfig(config);
const steps = [writeFile(`remove proxy ${options.name}`, configPath, content)];
if (options.restart) {
steps.push(run(`restart ${options.serviceName || DEFAULT_SERVICE_NAME}`, "sudo", [
"systemctl",
"restart",
options.serviceName || DEFAULT_SERVICE_NAME,
]));
}
return { title: `Remove proxy ${options.name}`, steps };
}
function frpRestartPlan(options = {}) {
const serviceName = options.serviceName || DEFAULT_SERVICE_NAME;
return {
title: `Restart ${serviceName}`,
steps: [run(`restart ${serviceName}`, "sudo", ["systemctl", "restart", serviceName])],
};
}
function bootstrapPlan(options) {
const zsh = zshInstallPlan();
const ssh = sshInstallPlan();
const frp = frpInstallPlan(options);
return {
title: "Bootstrap: zsh + ssh + frp",
steps: [...zsh.steps, ...ssh.steps, ...frp.steps],
};
}
function listProxies(options = {}) {
const installDir = options.installDir || defaultInstallDir();
const configPath = options.configPath || defaultConfigPath(installDir);
if (!fs.existsSync(configPath)) {
throw new Error(`${configPath} does not exist.`);
}
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
return [...config.sections.values()].map((section) => ({
name: section.name,
type: section.values.type || "tcp",
localIp: section.values.local_ip || "127.0.0.1",
localPort: section.values.local_port,
remotePort: section.values.remote_port,
}));
}
function buildGlobalsFromOptions(token, options) {
return buildGlobals({
serverAddr: options.serverAddr || DEFAULT_FRP_SERVER_ADDR,
serverPort: options.serverPort || DEFAULT_FRP_SERVER_PORT,
token,
tlsEnable: options.tlsEnable ?? false,
tcpMux: options.tcpMux ?? true,
logFile: options.logFile || "/var/log/frpc.log",
logLevel: options.logLevel || "info",
logMaxDays: options.logMaxDays ?? 7,
});
}
function renderSystemdService(installDir, configPath) {
return `[Unit]
Description=frp client
After=network.target
[Service]
Type=simple
ExecStart=${path.join(installDir, "frpc")} -c ${configPath}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
`;
}
function requireToken(value) {
const token = value || process.env.FRP_TOKEN || "";
if (!token) {
throw new Error("frp token is required.");
}
return token;
}
function run(label, command, args) {
return { kind: "run", label, command, args };
}
function writeFile(label, filePath, content) {
return { kind: "write", label, path: filePath, content };
}
module.exports = {
zshInstallPlan,
sshInstallPlan,
frpInstallPlan,
frpInitConfigPlan,
frpAddProxyPlan,
frpRemoveProxyPlan,
frpRestartPlan,
bootstrapPlan,
listProxies,
DEFAULTS: {
version: DEFAULT_FRP_VERSION,
arch: DEFAULT_FRP_ARCH,
serverAddr: DEFAULT_FRP_SERVER_ADDR,
serverPort: DEFAULT_FRP_SERVER_PORT,
serviceName: DEFAULT_SERVICE_NAME,
},
};