Продакшн рефактор без легаси
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { InstallContext, InstallOptions } from "../types/context";
|
||||
import { exists, readText, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
@@ -16,10 +15,6 @@ import { applyFirewall } from "../steps/firewall";
|
||||
import { writePostInstallEnv } from "../steps/env";
|
||||
import { smoke } from "../steps/smoke";
|
||||
|
||||
function secret(): string {
|
||||
return randomBytes(24).toString("base64url");
|
||||
}
|
||||
|
||||
export async function install(options: InstallOptions): Promise<void> {
|
||||
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
|
||||
if (options.sourceConfigPath && !hasSourceConfig) {
|
||||
@@ -30,15 +25,22 @@ export async function install(options: InstallOptions): Promise<void> {
|
||||
const config = parseRuntimeEnv(sourceConfigRaw);
|
||||
|
||||
const context: InstallContext = {
|
||||
mode: "install",
|
||||
options,
|
||||
config,
|
||||
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
|
||||
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
|
||||
installDate: new Date().toISOString(),
|
||||
hysteriaAuthPassword: secret(),
|
||||
hysteriaVersion: "unknown"
|
||||
hysteriaVersion: "unknown",
|
||||
hysteriaTargetVersion: await readPackageValue(options.packageDir, "hysteria.version", ""),
|
||||
hysteriaArtifactUrl: await readPackageValue(options.packageDir, "hysteria.url", ""),
|
||||
hysteriaArtifactSha256: await readPackageValue(options.packageDir, "hysteria.sha256", "")
|
||||
};
|
||||
|
||||
if (!context.hysteriaTargetVersion || !context.hysteriaArtifactUrl || !context.hysteriaArtifactSha256) {
|
||||
throw new Error("missing Hysteria lock metadata in package: hysteria.version/hysteria.url/hysteria.sha256");
|
||||
}
|
||||
|
||||
step("preflight");
|
||||
await preflight(context);
|
||||
step("system dependencies");
|
||||
@@ -48,6 +50,8 @@ export async function install(options: InstallOptions): Promise<void> {
|
||||
step("write runtime env");
|
||||
await runVisible`mkdir -p /etc/hy2xs`;
|
||||
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
||||
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
||||
step("bundled UI");
|
||||
await deployUi(context);
|
||||
step("Hysteria2 upstream install");
|
||||
|
||||
@@ -46,13 +46,13 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
||||
const configRaw = await readText(options.sourceConfigPath);
|
||||
const config = parseRuntimeEnv(configRaw);
|
||||
|
||||
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
||||
const context: ReconfigureContext = {
|
||||
mode: "reconfigure",
|
||||
options,
|
||||
config,
|
||||
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
|
||||
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
|
||||
installDate: new Date().toISOString(),
|
||||
hysteriaAuthPassword: "managed-by-ui-auth",
|
||||
hysteriaVersion: await readInstalledHysteriaVersion()
|
||||
};
|
||||
|
||||
@@ -81,6 +81,8 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
||||
await applyFirewall(context);
|
||||
step("write env artifacts");
|
||||
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
||||
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
||||
await writePostInstallEnv(context);
|
||||
step("smoke checks");
|
||||
await smoke(context);
|
||||
|
||||
@@ -148,8 +148,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
||||
false
|
||||
),
|
||||
hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml",
|
||||
allowLatestHysteria: parseBool("HY2XS_ALLOW_LATEST_HYSTERIA", env.HY2XS_ALLOW_LATEST_HYSTERIA, false),
|
||||
hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "v2.6.0",
|
||||
installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin",
|
||||
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
|
||||
logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs",
|
||||
@@ -240,8 +238,6 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
|
||||
`HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
|
||||
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
|
||||
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
|
||||
`HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`,
|
||||
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
|
||||
`HY2XS_INSTALL_DIR=${config.installDir}`,
|
||||
`HY2XS_DATA_DIR=${config.dataDir}`,
|
||||
`HY2XS_LOG_DIR=${config.logDir}`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function generateConfig(context: InstallContext): Promise<void> {
|
||||
export async function generateConfig(context: RuntimeContext): Promise<void> {
|
||||
const tlsAcmeBlock = context.config.tlsMode === "acme"
|
||||
? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0\n type: ${context.config.acmeType}`
|
||||
: "";
|
||||
@@ -14,7 +14,6 @@ export async function generateConfig(context: InstallContext): Promise<void> {
|
||||
const rendered = renderTemplate(template, {
|
||||
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||
HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword,
|
||||
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
||||
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
||||
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function writePostInstallEnv(context: InstallContext): Promise<void> {
|
||||
export async function writePostInstallEnv(context: RuntimeContext): Promise<void> {
|
||||
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
|
||||
PACKAGE_VERSION: context.packageVersion,
|
||||
PACKAGE_BUILD_ID: context.packageBuildId,
|
||||
@@ -30,6 +31,8 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
|
||||
});
|
||||
|
||||
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
|
||||
await runVisible`chown root:root /etc/hysteria/post-install.env`;
|
||||
await runVisible`chmod 0600 /etc/hysteria/post-install.env`;
|
||||
await writeText(
|
||||
context.config.bootstrapAdminSecretPath,
|
||||
[
|
||||
@@ -39,4 +42,6 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
|
||||
].join("\n") + "\n",
|
||||
0o600
|
||||
);
|
||||
await runVisible`chown root:root ${context.config.bootstrapAdminSecretPath}`;
|
||||
await runVisible`chmod 0600 ${context.config.bootstrapAdminSecretPath}`;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import { runVisible } from "../lib/process";
|
||||
export async function prepareFilesystem(context: InstallContext): Promise<void> {
|
||||
await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`;
|
||||
await runVisible`id -u hy2xs-admin >/dev/null 2>&1 || useradd --system --home ${context.config.dataDir} --shell /usr/sbin/nologin hy2xs-admin`;
|
||||
await runVisible`mkdir -p /etc/hy2xs /etc/hysteria /var/lib/hysteria ${context.config.installDir} ${context.config.dataDir} ${context.config.logDir} /usr/local/lib/hy2xs /etc/nftables.d`;
|
||||
await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`;
|
||||
await runVisible`chown -R root:root ${context.config.installDir}`;
|
||||
await runVisible`install -d -m 0700 -o root -g root /etc/hy2xs`;
|
||||
await runVisible`install -d -m 0755 -o root -g root /etc/hysteria`;
|
||||
await runVisible`install -d -m 0750 -o hysteria -g hysteria /var/lib/hysteria`;
|
||||
await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.dataDir}`;
|
||||
await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.logDir}`;
|
||||
await runVisible`install -d -m 0755 -o root -g root ${context.config.installDir}`;
|
||||
await runVisible`install -d -m 0755 -o root -g root /usr/local/lib/hy2xs`;
|
||||
await runVisible`install -d -m 0755 -o root -g root /etc/nftables.d`;
|
||||
await runVisible`chmod -R go-w ${context.config.installDir}`;
|
||||
await runVisible`chown -R hy2xs-admin:hy2xs-admin ${context.config.dataDir} ${context.config.logDir}`;
|
||||
await runVisible`chmod 700 /etc/hy2xs`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { exists, readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { fail, info } from "../lib/log";
|
||||
import { runVisible } from "../lib/process";
|
||||
@@ -27,7 +27,7 @@ function isSafeNftablesEntrypoint(content: string): boolean {
|
||||
return effective === "flush ruleset";
|
||||
}
|
||||
|
||||
export async function applyFirewall(context: InstallContext): Promise<void> {
|
||||
export async function applyFirewall(context: RuntimeContext): Promise<void> {
|
||||
if (context.options.skipFirewall || !context.config.firewallEnabled) {
|
||||
info("firewall skipped by flag");
|
||||
return;
|
||||
|
||||
@@ -9,41 +9,32 @@ function normalizeInstalledVersion(raw: string): string {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
function validateVersionPolicy(value: string): void {
|
||||
function validatePinnedVersion(value: string): void {
|
||||
if (/^v\d+\.\d+\.\d+$/.test(value)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`invalid HY2XS_HYSTERIA_VERSION policy: ${value}`);
|
||||
throw new Error(`invalid pinned Hysteria version in package metadata: ${value}`);
|
||||
}
|
||||
|
||||
export async function installHysteria(context: InstallContext): Promise<void> {
|
||||
const policy = context.config.hysteriaVersionPolicy;
|
||||
if (policy === "latest" && !context.config.allowLatestHysteria) {
|
||||
throw new Error("HY2XS_HYSTERIA_VERSION=latest is not allowed in production; pin vX.Y.Z or set HY2XS_ALLOW_LATEST_HYSTERIA=true");
|
||||
}
|
||||
validateVersionPolicy(policy);
|
||||
validatePinnedVersion(context.hysteriaTargetVersion);
|
||||
|
||||
const scriptPath = "/tmp/hy2xs-install-hysteria.sh";
|
||||
await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location https://get.hy2.sh/ -o ${scriptPath}`;
|
||||
await runVisible`test -s ${scriptPath}`;
|
||||
await runVisible`chmod 700 ${scriptPath}`;
|
||||
const tmp = "/tmp/hy2xs-hysteria-linux-amd64";
|
||||
|
||||
if (policy === "latest") {
|
||||
await runVisible`bash ${scriptPath}`;
|
||||
} else {
|
||||
await runVisible`bash ${scriptPath} --version ${policy}`;
|
||||
}
|
||||
await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location ${context.hysteriaArtifactUrl} -o ${tmp}`;
|
||||
await runVisible`test -s ${tmp}`;
|
||||
await runVisible`printf '%s %s\n' ${context.hysteriaArtifactSha256} ${tmp} | sha256sum -c -`;
|
||||
await runVisible`install -m 0755 ${tmp} /usr/local/bin/hysteria`;
|
||||
await runVisible`rm -f ${tmp}`;
|
||||
|
||||
await runVisible`test -x /usr/local/bin/hysteria`;
|
||||
const versionOutput = await run`/usr/local/bin/hysteria version`;
|
||||
const installedVersion = normalizeInstalledVersion(versionOutput);
|
||||
context.hysteriaVersion = installedVersion;
|
||||
|
||||
if (policy !== "latest" && installedVersion !== policy) {
|
||||
if (installedVersion !== context.hysteriaTargetVersion) {
|
||||
throw new Error(
|
||||
`installed Hysteria version mismatch: expected ${policy}, got ${installedVersion}. Review upstream installer env contract.`
|
||||
`installed Hysteria version mismatch: expected ${context.hysteriaTargetVersion}, got ${installedVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
await runVisible`rm -f ${scriptPath}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { exists, readText } from "../lib/fs";
|
||||
import { fail, info } from "../lib/log";
|
||||
import { run } from "../lib/process";
|
||||
@@ -21,8 +21,8 @@ async function isUnitActive(unit: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function preflight(context: InstallContext): Promise<void> {
|
||||
const isReconfigure = context.packageVersion === "reconfigure";
|
||||
export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
const isReconfigure = context.mode === "reconfigure";
|
||||
|
||||
if (process.getuid?.() !== 0) {
|
||||
fail("installer must run as root");
|
||||
@@ -33,7 +33,16 @@ export async function preflight(context: InstallContext): Promise<void> {
|
||||
fail("HY2XS baseline supports only clean Debian 12");
|
||||
}
|
||||
|
||||
if (!(await exists(`${context.options.packageDir}/ui/hy2xs-admin`))) {
|
||||
if (!(await exists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) {
|
||||
fail("missing hy2xs-admin systemd unit in package");
|
||||
}
|
||||
if (!(await exists(`${context.options.packageDir}/systemd/hysteria-server.service`))) {
|
||||
fail("missing hysteria-server systemd unit in package");
|
||||
}
|
||||
if (!(await exists(`${context.options.packageDir}/templates/hysteria/config.yaml.tpl`))) {
|
||||
fail("missing Hysteria config template in package");
|
||||
}
|
||||
if (context.mode === "install" && !(await exists(`${context.options.packageDir}/ui/hy2xs-admin/hy2xs-admin`))) {
|
||||
fail("bundled HY2XS admin is missing from install package");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { info } from "../lib/log";
|
||||
import { runHidden, runSecret, runVisible } from "../lib/process";
|
||||
|
||||
export async function smoke(context: InstallContext): Promise<void> {
|
||||
export async function smoke(context: RuntimeContext): Promise<void> {
|
||||
if (context.options.skipStart) {
|
||||
info("service start and smoke checks skipped by flag");
|
||||
return;
|
||||
@@ -30,6 +30,9 @@ export async function smoke(context: InstallContext): Promise<void> {
|
||||
await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`;
|
||||
await runVisible`sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml`;
|
||||
await runVisible`sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml`;
|
||||
await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env`;
|
||||
await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/bootstrap-admin.secret`;
|
||||
await runVisible`sudo -u hysteria test ! -r /etc/hy2xs/bootstrap-admin.secret`;
|
||||
await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`;
|
||||
if (context.config.uiBindHost === "127.0.0.1") {
|
||||
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`;
|
||||
@@ -42,6 +45,13 @@ export async function smoke(context: InstallContext): Promise<void> {
|
||||
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
const response = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
||||
if (!/"ok"\s*:\s*false/.test(response)) {
|
||||
throw new Error(`unexpected auth response during rate-limit smoke: ${response}`);
|
||||
}
|
||||
}
|
||||
|
||||
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
|
||||
if (!adminConPass) {
|
||||
throw new Error("admin connection password is empty in bootstrap secret file");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import type { RuntimeContext } from "../types/context";
|
||||
import { readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function deploySystemd(context: InstallContext): Promise<void> {
|
||||
export async function deploySystemd(context: RuntimeContext): Promise<void> {
|
||||
const values = {
|
||||
UI_BIND_HOST: context.config.uiBindHost,
|
||||
UI_PORT: context.config.uiPort,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type InstallOptions = {
|
||||
export type CommonOptions = {
|
||||
packageDir: string;
|
||||
sourceConfigPath: string;
|
||||
runtimeConfigPath: string;
|
||||
@@ -7,17 +7,15 @@ export type InstallOptions = {
|
||||
skipStart: boolean;
|
||||
};
|
||||
|
||||
export type ReconfigureOptions = {
|
||||
packageDir: string;
|
||||
sourceConfigPath: string;
|
||||
runtimeConfigPath: string;
|
||||
nonInteractive: boolean;
|
||||
export type InstallOptions = CommonOptions;
|
||||
|
||||
export type ReconfigureOptions = CommonOptions & {
|
||||
dryRun: boolean;
|
||||
apply: boolean;
|
||||
skipFirewall: boolean;
|
||||
skipStart: boolean;
|
||||
};
|
||||
|
||||
export type RunMode = "install" | "reconfigure";
|
||||
|
||||
export type TlsMode = "acme" | "file" | "self_signed_dev";
|
||||
|
||||
export type RuntimeConfig = {
|
||||
@@ -54,25 +52,31 @@ export type RuntimeConfig = {
|
||||
hysteriaBandwidthDown: string;
|
||||
hysteriaIgnoreClientBandwidth: boolean;
|
||||
hysteriaConfigPath: string;
|
||||
allowLatestHysteria: boolean;
|
||||
hysteriaVersionPolicy: string;
|
||||
installDir: string;
|
||||
dataDir: string;
|
||||
logDir: string;
|
||||
bootstrapAdminSecretPath: string;
|
||||
};
|
||||
|
||||
export type InstallContext = {
|
||||
options: InstallOptions;
|
||||
export type RuntimeContext = {
|
||||
mode: RunMode;
|
||||
options: CommonOptions;
|
||||
config: RuntimeConfig;
|
||||
packageVersion: string;
|
||||
packageBuildId: string;
|
||||
installDate: string;
|
||||
hysteriaAuthPassword: string;
|
||||
hysteriaVersion: string;
|
||||
};
|
||||
|
||||
export type ReconfigureContext = {
|
||||
options: ReconfigureOptions;
|
||||
config: RuntimeConfig;
|
||||
export type InstallContext = RuntimeContext & {
|
||||
mode: "install";
|
||||
options: InstallOptions;
|
||||
hysteriaTargetVersion: string;
|
||||
hysteriaArtifactUrl: string;
|
||||
hysteriaArtifactSha256: string;
|
||||
};
|
||||
|
||||
export type ReconfigureContext = RuntimeContext & {
|
||||
mode: "reconfigure";
|
||||
options: ReconfigureOptions;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user