scripts/private-network.mjs
Rama main · réplica de lectura del repositorio original.
import { createHash, X509Certificate } from "node:crypto";
import { readFile, writeFile, mkdir, mkdtemp, rename, rm, copyFile, stat } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { integrationAccessUrl } from "../packages/config/dist/index.js";
import { PRIVATE_NETWORK_TOOLS, privateNetworkRoot, privateGatewayPorts } from "../apps/agent/dist/private-gateway.js";
export async function provisionPrivateNetwork(config) {
if (!config.server.privateGateway) return;
if (process.platform !== "win32" || process.arch !== "x64") throw new Error("Private gateway provisioning currently supports Windows x64.");
const root = privateNetworkRoot();
await mkdir(root, { recursive: true });
for (const tool of PRIVATE_NETWORK_TOOLS) {
const target = path.join(root, `${tool.name}-${tool.version}.exe`);
try { if (!(await stat(target)).isFile()) throw new Error("Tool target must be a regular file"); continue; } catch (error) { if (error.code !== "ENOENT") throw error; }
const staging = await mkdtemp(path.join(root, "download-"));
try {
const response = await fetch(`https://api.github.com/repos/${tool.repository}/releases/assets/${tool.assetId}`, { headers: { Accept: "application/octet-stream", "User-Agent": "PalmTTY-setup" }, signal: AbortSignal.timeout(600_000) });
if (!response.ok) throw new Error(`Could not download official ${tool.name} archive: ${response.status}`);
const chunks = []; let size = 0;
for await (const chunk of response.body) {
size += chunk.length;
if (size > 64 * 1024 * 1024) throw new Error("Official tool archive exceeds 64 MiB limit.");
chunks.push(chunk);
}
const archive = Buffer.concat(chunks);
if (createHash("sha256").update(archive).digest("hex") !== tool.sha256) throw new Error(`${tool.name} archive integrity check failed.`);
const archivePath = path.join(staging, "tool.zip");
await writeFile(archivePath, archive);
const unpack = spawnSync("tar.exe", ["-xf", archivePath, "-C", staging, `${tool.name}.exe`], { windowsHide: true, stdio: "ignore", timeout: 30_000 });
if (unpack.status !== 0) throw new Error(`Could not unpack ${tool.name}.`);
await rename(path.join(staging, `${tool.name}.exe`), target);
console.log(`[PalmTTY] Verified ${tool.name} ${tool.version}.`);
} finally { await rm(staging, { recursive: true, force: true }); }
}
}
export async function exportPrivateNetworkSetup(config) {
const gateway = config.server.privateGateway;
if (!gateway) return;
const root = privateNetworkRoot();
const publicRoot = path.join(root, "public");
await mkdir(publicRoot, { recursive: true });
const codeEnabled = config.integrations.vscodeWeb?.mode === "managed";
let certificate;
if (codeEnabled) {
const caPath = path.join(root, "pki", "pki", "authorities", "local", "root.crt");
certificate = new X509Certificate(await readFile(caPath));
for (const destination of [root, publicRoot]) await copyFile(caPath, path.join(destination, "PalmTTY-root.crt"));
// Preserve the existing profile identity and CA; only the new IP leaf changes.
const profile = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>PayloadType</key><string>Configuration</string><key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>home.arpa.palmtty.root</string>
<key>PayloadUUID</key><string>1E60905C-46F7-4B16-A8B5-1542B1448A76</string>
<key>PayloadDisplayName</key><string>PalmTTY Private HTTPS</string>
<key>PayloadContent</key><array><dict>
<key>PayloadType</key><string>com.apple.security.root</string><key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>home.arpa.palmtty.root.certificate</string>
<key>PayloadUUID</key><string>B35712DC-FDB0-4A66-A281-D18C6F4DC526</string>
<key>PayloadDisplayName</key><string>PalmTTY Private Root</string>
<key>PayloadContent</key><data>${certificate.raw.toString("base64")}</data>
</dict></array></dict></plist>`;
for (const destination of [root, publicRoot]) await writeFile(path.join(destination,"PalmTTY-HTTPS.mobileconfig"),profile);
} else {
for (const name of ["PalmTTY-root.crt", "PalmTTY-HTTPS.mobileconfig"]) await rm(path.join(publicRoot,name),{force:true});
}
const palmRoot = `http://${gateway.listenHost}:${config.server.port}/`;
const setupRoot = palmRoot + "network-setup/";
const applications = Object.keys(config.integrations).filter(id=>config.integrations[id]?.mode==="managed")
.map(id=>({id,url:integrationAccessUrl(config,id)}));
const trustInstructions = certificate ? `Code HTTPS 首次使用需信任根证书;已有根信任无需重装。
iPhone:安装 PalmTTY-HTTPS.mobileconfig,在 设置 > 通用 > 关于本机 > 证书信任设置 启用完全信任。
Windows:certutil -user -addstore Root "${path.join(root,"PalmTTY-root.crt")}"
根证书 SHA-256:${certificate.fingerprint256}` : "当前只有 HTTP 应用,不需要证书。";
await writeFile(path.join(root,"连接说明.txt"),`PalmTTY 内网 IP 访问
入口:${palmRoot}
${applications.map(app=>app.id+":"+app.url).join("\n")}
家中 Wi-Fi 与 5G + WireGuard 使用相同地址,无需设置私有 DNS 或 hosts。
主机 IP 应固定;WireGuard AllowedIPs 需包含 ${gateway.listenHost}。
若旧 WireGuard DNS 仍为此主机,请先恢复为隧道可达的正常 DNS;本软件不再提供 DNS。
${trustInstructions}
应用仍有各自认证。同 IP 不同端口不隔离 Cookie,仅适用于同一可信主机上的自用应用。
如需更新防火墙,请在管理员 PowerShell 明确运行:
& "${path.join(root,"configure-firewall.ps1")}"
脚本只更新具名、程序/私有 IP/私有来源受限的 HTTP/HTTPS 规则,并移除旧 DNS 规则;不自动提权。
公开证书配置页:${setupRoot}
不需要公网端口转发。`,"utf8");
const certificateLinks = certificate ? `<li>Code HTTPS:<a href="${setupRoot}PalmTTY-HTTPS.mobileconfig">iPhone 根证书描述文件</a>;<a href="${setupRoot}PalmTTY-root.crt">公开根证书</a>。已信任无需重装。</li><li>根证书 SHA-256:${certificate.fingerprint256}</li>` : "";
await writeFile(path.join(publicRoot,"index.html"),`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PalmTTY 内网 IP 访问</title><body><h1>PalmTTY 内网 IP 访问</h1><p>Wi-Fi 和 WireGuard 使用相同 IP 地址,无需私有 DNS。不要在此输入访问令牌。</p><ul><li><a href="${palmRoot}">打开 PalmTTY</a></li>${applications.map(app=>`<li>${app.id}:${app.url}</li>`).join("")}${certificateLinks}</ul><p>WireGuard 路由需包含主机 IP。首次根信任需明确操作,iPhone 安装后需启用完全信任。</p></body></html>`,"utf8");
const caddy = path.join(root,`caddy-${PRIVATE_NETWORK_TOOLS[0].version}.exe`);
const literal = value=>"'"+String(value).replaceAll("'","''")+"'";
const firewall = `# Requires an explicit administrator PowerShell. No automatic elevation.
$ErrorActionPreference = 'Stop'
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this reviewed script in an administrator PowerShell.' }
$privateSources = @('LocalSubnet','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','100.64.0.0/10')
foreach ($retired in @('PalmTTY-PrivateDNS-UDP','PalmTTY-PrivateDNS-TCP')) {
if (Get-NetFirewallRule -Name $retired -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $retired }
}
$rules = @(
@{ Name='PalmTTY-PrivateHTTPS'; Program=${literal(caddy)}; Ports=@(${privateGatewayPorts(config).join(",")}) },
@{ Name='PalmTTY-PrivateHTTP'; Program=${literal(process.execPath)}; Ports=@(${config.server.port}) }
)
foreach ($rule in $rules) {
if (Get-NetFirewallRule -Name $rule.Name -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $rule.Name }
New-NetFirewallRule -Name $rule.Name -DisplayName $rule.Name -Direction Inbound -Action Allow -Profile Any -Program $rule.Program -Protocol TCP -LocalAddress ${gateway.listenHost} -LocalPort $rule.Ports -RemoteAddress $privateSources | Out-Null
}
Write-Output 'PalmTTY private-source HTTP/HTTPS firewall rules configured; retired DNS rules removed.'
`;
await writeFile(path.join(root,"configure-firewall.ps1"),firewall,"utf8");
console.log(`[PalmTTY] Private IP client setup: ${path.join(root,"连接说明.txt")}`);
}