712 lines
19 KiB
JavaScript
712 lines
19 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { spawnSync } = require("node:child_process");
|
|
|
|
const DEFAULT_FRP_VERSION = "0.58.1";
|
|
const DEFAULT_FRP_ARCH = "amd64";
|
|
const DEFAULT_FRP_SERVER_ADDR = "81.70.134.9";
|
|
const DEFAULT_FRP_SERVER_PORT = 15443;
|
|
const DEFAULT_SERVICE_NAME = "frpc";
|
|
const NVM_INSTALL_VERSION = "v0.40.4";
|
|
const NVM_INSTALL_COMMAND = `set -euo 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`;
|
|
|
|
function main(argv) {
|
|
const parsed = parseArgs(argv);
|
|
|
|
if (parsed.flags.help || parsed.positionals.length === 0) {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
const [group, action, ...rest] = parsed.positionals;
|
|
const commandFlags = parseArgs(rest).flags;
|
|
const flags = { ...parsed.flags, ...commandFlags };
|
|
const runner = createRunner(flags);
|
|
|
|
try {
|
|
if (group === "zsh" && action === "install") {
|
|
installZsh(runner, flags);
|
|
return;
|
|
}
|
|
|
|
if (group === "ssh" && action === "install") {
|
|
installSsh(runner);
|
|
return;
|
|
}
|
|
|
|
if (group === "frp") {
|
|
handleFrp(action, rest, flags, runner);
|
|
return;
|
|
}
|
|
|
|
if (group === "bootstrap") {
|
|
installZsh(runner, flags);
|
|
installSsh(runner);
|
|
installFrp(runner, flags);
|
|
return;
|
|
}
|
|
|
|
fail(`Unknown command: ${[group, action].filter(Boolean).join(" ")}`);
|
|
} catch (error) {
|
|
fail(error.message);
|
|
}
|
|
}
|
|
|
|
function handleFrp(action, argv, rootFlags, runner) {
|
|
const parsed = parseArgs(argv);
|
|
const flags = { ...rootFlags, ...parsed.flags };
|
|
|
|
if (action === "install") {
|
|
installFrp(runner, flags);
|
|
return;
|
|
}
|
|
|
|
if (action === "init") {
|
|
initFrpConfig(flags, runner);
|
|
return;
|
|
}
|
|
|
|
if (action === "add") {
|
|
const name = parsed.positionals[0];
|
|
addFrpProxy(name, flags, runner);
|
|
return;
|
|
}
|
|
|
|
if (action === "remove") {
|
|
const name = parsed.positionals[0];
|
|
removeFrpProxy(name, flags, runner);
|
|
return;
|
|
}
|
|
|
|
if (action === "list") {
|
|
listFrpProxies(flags);
|
|
return;
|
|
}
|
|
|
|
if (action === "restart") {
|
|
runner.run("sudo", ["systemctl", "restart", stringFlag(flags, "service-name", DEFAULT_SERVICE_NAME)]);
|
|
return;
|
|
}
|
|
|
|
fail(`Unknown frp command: ${action || ""}`.trim());
|
|
}
|
|
|
|
function installZsh(runner, flags) {
|
|
runner.run("sudo", ["apt", "update"]);
|
|
runner.run("sudo", ["apt", "upgrade", "-y"]);
|
|
runner.run("sudo", ["apt", "install", "zsh", "git", "curl", "wget", "-y"]);
|
|
runner.run("chsh", ["-s", "/bin/zsh"]);
|
|
|
|
runner.run("sh", [
|
|
"-c",
|
|
'RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://gitee.com/pocmon/ohmyzsh/raw/master/tools/install.sh)"',
|
|
]);
|
|
runner.run("bash", ["-c", NVM_INSTALL_COMMAND]);
|
|
|
|
const customDir = process.env.ZSH_CUSTOM || path.join(os.homedir(), ".oh-my-zsh", "custom");
|
|
installGitPlugin(
|
|
runner,
|
|
"https://github.com/zsh-users/zsh-autosuggestions",
|
|
path.join(customDir, "plugins", "zsh-autosuggestions"),
|
|
);
|
|
installGitPlugin(
|
|
runner,
|
|
"https://github.com/zsh-users/zsh-syntax-highlighting.git",
|
|
path.join(customDir, "plugins", "zsh-syntax-highlighting"),
|
|
);
|
|
|
|
updateZshrcPlugins(["git", "zsh-autosuggestions", "zsh-syntax-highlighting"], flags.dryRun);
|
|
}
|
|
|
|
function installGitPlugin(runner, repo, destination) {
|
|
if (fs.existsSync(destination)) {
|
|
runner.run("git", ["-C", destination, "pull", "--ff-only"]);
|
|
return;
|
|
}
|
|
|
|
runner.run("git", ["clone", repo, destination]);
|
|
}
|
|
|
|
function updateZshrcPlugins(plugins, dryRun) {
|
|
const zshrc = path.join(os.homedir(), ".zshrc");
|
|
const pluginLine = `plugins=(${plugins.join(" ")})`;
|
|
|
|
if (dryRun) {
|
|
console.log(`[dry-run] update ${zshrc}: ${pluginLine}`);
|
|
return;
|
|
}
|
|
|
|
const original = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
|
|
const next = /^plugins=\([^)]*\)$/m.test(original)
|
|
? original.replace(/^plugins=\([^)]*\)$/m, pluginLine)
|
|
: `${original.trimEnd()}\n${pluginLine}\n`;
|
|
|
|
fs.writeFileSync(zshrc, next);
|
|
console.log(`Updated ${zshrc}`);
|
|
}
|
|
|
|
function installSsh(runner) {
|
|
runner.run("sudo", ["apt", "update"]);
|
|
runner.run("sudo", ["apt", "install", "openssh-server", "-y"]);
|
|
runner.run("sudo", ["systemctl", "enable", "--now", "ssh"]);
|
|
}
|
|
|
|
function installFrp(runner, flags) {
|
|
const version = stringFlag(flags, "version", DEFAULT_FRP_VERSION);
|
|
const arch = stringFlag(flags, "arch", DEFAULT_FRP_ARCH);
|
|
const installDir = getInstallDir(flags, version, arch);
|
|
const archiveName = `frp_${version}_linux_${arch}.tar.gz`;
|
|
const extractedDir = `/tmp/frp_${version}_linux_${arch}`;
|
|
const archivePath = `/tmp/${archiveName}`;
|
|
const url = `https://github.com/fatedier/frp/releases/download/v${version}/${archiveName}`;
|
|
|
|
runner.run("wget", ["-O", archivePath, url]);
|
|
runner.run("tar", ["-zxf", archivePath, "-C", "/tmp"]);
|
|
runner.run("sudo", ["mkdir", "-p", installDir]);
|
|
runner.run("sudo", ["cp", "-f", path.join(extractedDir, "frpc"), path.join(installDir, "frpc")]);
|
|
runner.run("sudo", ["chmod", "+x", path.join(installDir, "frpc")]);
|
|
|
|
initFrpConfig({ ...flags, "install-dir": installDir }, runner);
|
|
writeFrpService(flags, runner, installDir);
|
|
runner.run("sudo", ["systemctl", "daemon-reload"]);
|
|
runner.run("sudo", ["systemctl", "enable", "--now", stringFlag(flags, "service-name", DEFAULT_SERVICE_NAME)]);
|
|
}
|
|
|
|
function initFrpConfig(flags, runner) {
|
|
const configPath = getConfigPath(flags);
|
|
const force = Boolean(flags.force);
|
|
const configExists = fs.existsSync(configPath);
|
|
|
|
if (configExists && !force) {
|
|
console.log(`${configPath} already exists. Use --force to rewrite it.`);
|
|
return;
|
|
}
|
|
|
|
const token = stringFlag(flags, "token", process.env.FRP_TOKEN || "");
|
|
if (!token) {
|
|
throw new Error("frp token is required. Pass --token <value> or set FRP_TOKEN.");
|
|
}
|
|
|
|
const proxies = configExists && force
|
|
? [...parseFrpConfig(fs.readFileSync(configPath, "utf8")).sections.values()].map((section) => ({
|
|
name: section.name,
|
|
...section.values,
|
|
}))
|
|
: [];
|
|
|
|
const config = renderFrpConfig({
|
|
serverAddr: stringFlag(flags, "server-addr", DEFAULT_FRP_SERVER_ADDR),
|
|
serverPort: numberFlag(flags, "server-port", DEFAULT_FRP_SERVER_PORT),
|
|
"auth.method": "token",
|
|
"auth.token": token,
|
|
"transport.tls.enable": booleanFlag(flags, "tls-enable", false),
|
|
"transport.tcpMux": booleanFlag(flags, "tcp-mux", true),
|
|
"log.to": stringFlag(flags, "log-file", "/var/log/frpc.log"),
|
|
"log.level": stringFlag(flags, "log-level", "info"),
|
|
"log.maxDays": numberFlag(flags, "log-max-days", 7),
|
|
}, proxies);
|
|
|
|
writeFile(configPath, config, runner);
|
|
}
|
|
|
|
function writeFrpService(flags, runner, installDir) {
|
|
const serviceName = stringFlag(flags, "service-name", DEFAULT_SERVICE_NAME);
|
|
const configPath = getConfigPath({ ...flags, "install-dir": installDir });
|
|
const servicePath = `/etc/systemd/system/${serviceName}.service`;
|
|
const service = `[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
|
|
`;
|
|
|
|
writeFile(servicePath, service, runner);
|
|
}
|
|
|
|
function addFrpProxy(name, flags, runner) {
|
|
if (!name) {
|
|
throw new Error("frp add requires a proxy name, for example: frp add ssh --local-port 22 --remote-port 17227");
|
|
}
|
|
|
|
assertProxyName(name);
|
|
|
|
const configPath = getConfigPath(flags);
|
|
if (!fs.existsSync(configPath)) {
|
|
throw new Error(`${configPath} does not exist. Run frp init or frp install first.`);
|
|
}
|
|
|
|
const localPort = numberFlag(flags, "local-port");
|
|
const remotePort = numberFlag(flags, "remote-port");
|
|
if (!localPort || !remotePort) {
|
|
throw new Error("frp add requires --local-port and --remote-port.");
|
|
}
|
|
|
|
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
|
|
config.sections.set(name, {
|
|
name,
|
|
values: {
|
|
type: stringFlag(flags, "type", "tcp"),
|
|
local_ip: stringFlag(flags, "local-ip", "127.0.0.1"),
|
|
local_port: localPort,
|
|
remote_port: remotePort,
|
|
},
|
|
});
|
|
|
|
writeFile(configPath, renderParsedFrpConfig(config), runner);
|
|
restartServiceIfRequested(flags, runner);
|
|
}
|
|
|
|
function removeFrpProxy(name, flags, runner) {
|
|
if (!name) {
|
|
throw new Error("frp remove requires a proxy name.");
|
|
}
|
|
|
|
const configPath = getConfigPath(flags);
|
|
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
|
|
|
|
if (!config.sections.delete(name)) {
|
|
console.log(`No proxy named ${name} in ${configPath}`);
|
|
return;
|
|
}
|
|
|
|
writeFile(configPath, renderParsedFrpConfig(config), runner);
|
|
restartServiceIfRequested(flags, runner);
|
|
}
|
|
|
|
function listFrpProxies(flags) {
|
|
const configPath = getConfigPath(flags);
|
|
const config = parseFrpConfig(fs.readFileSync(configPath, "utf8"));
|
|
|
|
if (config.sections.size === 0) {
|
|
console.log("No frp proxies configured.");
|
|
return;
|
|
}
|
|
|
|
for (const section of config.sections.values()) {
|
|
const values = section.values;
|
|
console.log(`${section.name}: ${values.type || "tcp"} ${values.local_ip || "127.0.0.1"}:${values.local_port || "?"} -> remote:${values.remote_port || "?"}`);
|
|
}
|
|
}
|
|
|
|
function restartServiceIfRequested(flags, runner) {
|
|
if (!flags.restart) {
|
|
return;
|
|
}
|
|
|
|
runner.run("sudo", ["systemctl", "restart", stringFlag(flags, "service-name", DEFAULT_SERVICE_NAME)]);
|
|
}
|
|
|
|
function parseFrpConfig(text) {
|
|
const globals = [];
|
|
const sections = new Map();
|
|
let current = null;
|
|
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const proxyMatch = line.match(/^\s*\[\[proxies\]\]\s*$/);
|
|
if (proxyMatch) {
|
|
current = { name: "", values: {} };
|
|
continue;
|
|
}
|
|
|
|
const sectionMatch = line.match(/^\s*\[([A-Za-z0-9_.-]+)]\s*$/);
|
|
if (sectionMatch) {
|
|
current = { name: sectionMatch[1], values: {} };
|
|
sections.set(current.name, current);
|
|
continue;
|
|
}
|
|
|
|
if (!current) {
|
|
globals.push(migrateFrpGlobalLine(line));
|
|
continue;
|
|
}
|
|
|
|
const keyValue = parseTomlKeyValue(line);
|
|
if (keyValue) {
|
|
applyProxyConfigValue(current, keyValue, sections);
|
|
}
|
|
}
|
|
|
|
return { globals: trimTrailingBlankLines(globals), sections };
|
|
}
|
|
|
|
function migrateFrpGlobalLine(line) {
|
|
const keyMap = {
|
|
server_addr: "serverAddr",
|
|
server_port: "serverPort",
|
|
token: "auth.token",
|
|
tls_enable: "transport.tls.enable",
|
|
tcp_mux: "transport.tcpMux",
|
|
log_file: "log.to",
|
|
log_level: "log.level",
|
|
log_max_days: "log.maxDays",
|
|
};
|
|
const match = line.match(/^(\s*)([A-Za-z0-9_.-]+)(\s*=.*)$/);
|
|
if (!match) {
|
|
return line;
|
|
}
|
|
|
|
const [, indent, key, rest] = match;
|
|
return `${indent}${keyMap[key] || key}${rest}`;
|
|
}
|
|
|
|
function applyProxyConfigValue(current, keyValue, sections) {
|
|
const key = normalizeProxyKey(keyValue.key);
|
|
|
|
if (key === "name") {
|
|
if (current.name) {
|
|
sections.delete(current.name);
|
|
}
|
|
|
|
current.name = String(keyValue.value);
|
|
sections.set(current.name, current);
|
|
return;
|
|
}
|
|
|
|
current.values[key] = keyValue.value;
|
|
}
|
|
|
|
function normalizeProxyKey(key) {
|
|
const keyMap = {
|
|
localIP: "local_ip",
|
|
localPort: "local_port",
|
|
remotePort: "remote_port",
|
|
};
|
|
|
|
return keyMap[key] || key;
|
|
}
|
|
|
|
function parseTomlKeyValue(line) {
|
|
const withoutComment = line.replace(/\s+#.*$/, "").trim();
|
|
const match = withoutComment.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const [, key, rawValue] = match;
|
|
if (/^".*"$/.test(rawValue)) {
|
|
return { key, value: rawValue.slice(1, -1) };
|
|
}
|
|
|
|
if (rawValue === "true") {
|
|
return { key, value: true };
|
|
}
|
|
|
|
if (rawValue === "false") {
|
|
return { key, value: false };
|
|
}
|
|
|
|
if (/^\d+$/.test(rawValue)) {
|
|
return { key, value: Number(rawValue) };
|
|
}
|
|
|
|
return { key, value: rawValue };
|
|
}
|
|
|
|
function renderParsedFrpConfig(config) {
|
|
const lines = [...config.globals];
|
|
|
|
for (const section of config.sections.values()) {
|
|
if (lines.length && lines[lines.length - 1] !== "") {
|
|
lines.push("");
|
|
}
|
|
|
|
lines.push(renderProxy(section.name, section.values).trimEnd());
|
|
}
|
|
|
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
}
|
|
|
|
function renderFrpConfig(globals, proxies) {
|
|
const lines = Object.entries(globals).map(([key, value]) => `${key} = ${formatTomlValue(value)}`);
|
|
|
|
for (const proxy of proxies) {
|
|
lines.push("");
|
|
lines.push(renderProxy(proxy.name, proxy));
|
|
}
|
|
|
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
}
|
|
|
|
function renderProxy(name, values) {
|
|
return `[[proxies]]
|
|
name = ${formatTomlValue(name)}
|
|
type = ${formatTomlValue(values.type || "tcp")}
|
|
localIP = ${formatTomlValue(values.local_ip || "127.0.0.1")}
|
|
localPort = ${formatTomlValue(values.local_port)}
|
|
remotePort = ${formatTomlValue(values.remote_port)}
|
|
`;
|
|
}
|
|
|
|
function formatTomlValue(value) {
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
|
|
return JSON.stringify(String(value));
|
|
}
|
|
|
|
function writeFile(filePath, content, runner) {
|
|
const dir = path.dirname(filePath);
|
|
|
|
if (runner.dryRun) {
|
|
console.log(`[dry-run] mkdir -p ${dir}`);
|
|
console.log(`[dry-run] write ${filePath}`);
|
|
console.log(content.trimEnd());
|
|
return;
|
|
}
|
|
|
|
if (canWriteDirectly(filePath)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(filePath, content);
|
|
console.log(`Wrote ${filePath}`);
|
|
return;
|
|
}
|
|
|
|
runner.run("sudo", ["mkdir", "-p", dir]);
|
|
const result = spawnSync("sudo", ["tee", filePath], {
|
|
input: content,
|
|
stdio: ["pipe", "ignore", "inherit"],
|
|
encoding: "utf8",
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`Failed to write ${filePath}`);
|
|
}
|
|
|
|
console.log(`Wrote ${filePath}`);
|
|
}
|
|
|
|
function canWriteDirectly(filePath) {
|
|
if (process.getuid && process.getuid() === 0) {
|
|
return true;
|
|
}
|
|
|
|
const dir = path.dirname(filePath);
|
|
if (fs.existsSync(filePath)) {
|
|
return isWritable(filePath);
|
|
}
|
|
|
|
return fs.existsSync(dir) && isWritable(dir);
|
|
}
|
|
|
|
function isWritable(target) {
|
|
try {
|
|
fs.accessSync(target, fs.constants.W_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createRunner(flags) {
|
|
const dryRun = Boolean(flags.dryRun || flags["dry-run"]);
|
|
|
|
return {
|
|
dryRun,
|
|
run(command, args) {
|
|
if (dryRun) {
|
|
console.log(`[dry-run] ${formatCommand(command, args)}`);
|
|
return;
|
|
}
|
|
|
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`Command failed: ${formatCommand(command, args)}`);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function formatCommand(command, args) {
|
|
return [command, ...args].map(shellQuote).join(" ");
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const flags = {};
|
|
const positionals = [];
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
|
|
if (arg === "--") {
|
|
positionals.push(...argv.slice(index + 1));
|
|
break;
|
|
}
|
|
|
|
if (arg.startsWith("--")) {
|
|
const eqIndex = arg.indexOf("=");
|
|
const rawKey = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
|
|
const key = rawKey.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
const value = eqIndex === -1 ? argv[index + 1] : arg.slice(eqIndex + 1);
|
|
|
|
if (eqIndex === -1 && value && !value.startsWith("-")) {
|
|
flags[key] = value;
|
|
flags[toCamel(key)] = value;
|
|
index += 1;
|
|
} else {
|
|
flags[key] = true;
|
|
flags[toCamel(key)] = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (arg === "-h") {
|
|
flags.help = true;
|
|
continue;
|
|
}
|
|
|
|
positionals.push(arg);
|
|
}
|
|
|
|
return { flags, positionals };
|
|
}
|
|
|
|
function toCamel(key) {
|
|
return key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
}
|
|
|
|
function getInstallDir(flags, version = DEFAULT_FRP_VERSION, arch = DEFAULT_FRP_ARCH) {
|
|
return stringFlag(flags, "install-dir", `/opt/frp/frp_${version}_linux_${arch}`);
|
|
}
|
|
|
|
function getConfigPath(flags) {
|
|
return stringFlag(flags, "config", path.join(getInstallDir(flags), "frpc.toml"));
|
|
}
|
|
|
|
function stringFlag(flags, key, fallback = undefined) {
|
|
const value = flags[key] ?? flags[toCamel(key)] ?? fallback;
|
|
return value === undefined ? undefined : String(value);
|
|
}
|
|
|
|
function numberFlag(flags, key, fallback = undefined) {
|
|
const value = flags[key] ?? flags[toCamel(key)] ?? fallback;
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
throw new Error(`--${key} must be an integer between 1 and 65535.`);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function booleanFlag(flags, key, fallback) {
|
|
const value = flags[key] ?? flags[toCamel(key)] ?? fallback;
|
|
if (typeof value === "boolean") {
|
|
return value;
|
|
}
|
|
|
|
if (String(value).toLowerCase() === "true") {
|
|
return true;
|
|
}
|
|
|
|
if (String(value).toLowerCase() === "false") {
|
|
return false;
|
|
}
|
|
|
|
throw new Error(`--${key} must be true or false.`);
|
|
}
|
|
|
|
function assertProxyName(name) {
|
|
if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
|
|
throw new Error("Proxy name may only contain letters, numbers, underscore, dash and dot.");
|
|
}
|
|
}
|
|
|
|
function trimTrailingBlankLines(lines) {
|
|
const next = [...lines];
|
|
while (next.length && next[next.length - 1].trim() === "") {
|
|
next.pop();
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`Usage:
|
|
server-config zsh install [--dry-run]
|
|
server-config ssh install [--dry-run]
|
|
server-config bootstrap --token <frp-token> [--dry-run]
|
|
|
|
server-config frp install --token <frp-token> [options]
|
|
server-config frp init --token <frp-token> [options]
|
|
server-config frp add <name> --local-port <port> --remote-port <port> [options]
|
|
server-config frp remove <name> [--restart]
|
|
server-config frp list [--config <path>]
|
|
server-config frp restart [--service-name frpc]
|
|
|
|
Common options:
|
|
--dry-run Print commands and file changes without executing them.
|
|
|
|
frp options:
|
|
--version <version> Default: ${DEFAULT_FRP_VERSION}
|
|
--arch <arch> Default: ${DEFAULT_FRP_ARCH}
|
|
--install-dir <path> Default: /opt/frp/frp_<version>_linux_<arch>
|
|
--config <path> Default: <install-dir>/frpc.toml
|
|
--service-name <name> Default: ${DEFAULT_SERVICE_NAME}
|
|
--server-addr <ip> Default: ${DEFAULT_FRP_SERVER_ADDR}
|
|
--server-port <port> Default: ${DEFAULT_FRP_SERVER_PORT}
|
|
--token <token> Or set FRP_TOKEN.
|
|
--tls-enable <true|false> Default: false
|
|
--tcp-mux <true|false> Default: true
|
|
--restart Restart frpc after add/remove.
|
|
|
|
Examples:
|
|
server-config frp install --token "$FRP_TOKEN"
|
|
server-config frp add ssh --local-port 22 --remote-port 17227 --restart
|
|
server-config frp add mysql --local-port 3306 --remote-port 33061 --restart
|
|
`);
|
|
}
|
|
|
|
function fail(message) {
|
|
console.error(message);
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
module.exports = {
|
|
main,
|
|
parseArgs,
|
|
parseFrpConfig,
|
|
renderParsedFrpConfig,
|
|
};
|