336 lines
11 KiB
JavaScript
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,
|
|
},
|
|
};
|