diff --git a/README.md b/README.md index e1ad2a0..61c57a0 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,7 @@ UHRP_CADDY_PATH=~/tmp/.uhrp ``` ```bash -# not auto install caddy when postinstall npm i unplugin-https-reverse-proxy --save-dev - -# auto install -UHRP_AUTO_INSTALL_CADDY=true npm i unplugin-https-reverse-proxy --save-dev ``` ```ts diff --git a/eslint.config.js b/eslint.config.js index 452d056..e9e8e85 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,6 @@ export default antfu({ ignores: [ 'docs/**/*', 'examples/**/*', - 'scripts/postinstall.cjs', ], }, ]) diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts index 9173100..ca3cc3d 100644 --- a/examples/nuxt/nuxt.config.ts +++ b/examples/nuxt/nuxt.config.ts @@ -1,10 +1,11 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ devtools: { enabled: true }, - modules: [ - ['~/../../../src/nuxt', { - enabled: true, - target: 'test-nuxt.abc.com', - }], - ], + // uncomment to enable the plugin + // modules: [ + // ['~/../../../src/nuxt', { + // enabled: true, + // target: 'test-nuxt.abc.com', + // }], + // ], }) diff --git a/examples/vite/package.json b/examples/vite/package.json index 159fb57..52beb0e 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,5 +1,6 @@ { "private": true, + "type": "module", "scripts": { "dev": "nodemon -w '../src/**/*.ts' -e .ts -x vite" }, diff --git a/package.json b/package.json index 9fbc57f..df17017 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unplugin-https-reverse-proxy", "type": "module", - "version": "2.0.2", + "version": "2.0.3-beta.1", "description": "", "license": "MIT", "homepage": "https://github.com/zcf0508/unplugin-https-reverse-proxy#readme", @@ -75,11 +75,9 @@ }, "files": [ "dist", - "scripts/postinstall.cjs", "src" ], "scripts": { - "postinstall": "node scripts/postinstall.cjs", "build": "tsup", "dev": "tsup --watch src", "build:fix": "tsx scripts/postbuild.ts", @@ -156,11 +154,14 @@ "@types/proper-lockfile": "^4.1.4", "@types/qrcode-terminal": "^0.12.2", "@vue/cli": "^5.0.8", + "alien-signals": "^2.0.7", + "arktype": "^2.1.20", "bumpp": "^9.11.1", "chalk": "^5.4.1", "cross-env": "^7.0.3", "eslint": "^9.20.0", "fast-glob": "^3.3.3", + "liquidjs": "^10.21.1", "nodemon": "^3.1.9", "proper-lockfile": "^4.1.2", "rimraf": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8fdf5d..ff4d42e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,12 @@ importers: '@vue/cli': specifier: ^5.0.8 version: 5.0.8(encoding@0.1.13) + alien-signals: + specifier: ^2.0.7 + version: 2.0.7 + arktype: + specifier: ^2.1.20 + version: 2.1.20 bumpp: specifier: ^9.11.1 version: 9.11.1 @@ -93,6 +99,9 @@ importers: fast-glob: specifier: ^3.3.3 version: 3.3.3 + liquidjs: + specifier: ^10.21.1 + version: 10.21.1 nodemon: specifier: ^3.1.9 version: 3.1.9 @@ -329,6 +338,12 @@ packages: '@apollographql/graphql-playground-html@1.6.29': resolution: {integrity: sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==} + '@ark/schema@0.46.0': + resolution: {integrity: sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==} + + '@ark/util@0.46.0': + resolution: {integrity: sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg==} + '@babel/code-frame@7.22.13': resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -3325,6 +3340,9 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + alien-signals@2.0.7: + resolution: {integrity: sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==} + alphanum-sort@1.0.2: resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==} @@ -3481,6 +3499,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arktype@2.1.20: + resolution: {integrity: sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==} + arr-diff@4.0.0: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} @@ -4204,6 +4225,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@2.17.1: resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} @@ -7082,6 +7107,11 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + liquidjs@10.21.1: + resolution: {integrity: sha512-NZXmCwv3RG5nire3fmIn9HsOyJX3vo+ptp0yaXUHAMzSNBhx74Hm+dAGJvscUA6lNqbLuYfXgNavRQ9UbUJhQQ==} + engines: {node: '>=14'} + hasBin: true + listhen@1.5.6: resolution: {integrity: sha512-gTpEJhT5L85L0bFgmu+Boqu5rP4DwDtEb4Exq5gdQUxWRwx4jbzdInZkmyLONo5EwIcQB0k7ZpWlpCDPdL77EQ==} hasBin: true @@ -9337,6 +9367,7 @@ packages: rspack-resolver@1.2.2: resolution: {integrity: sha512-Fwc19jMBA3g+fxDJH2B4WxwZjE0VaaOL7OX/A4Wn5Zv7bOD/vyPZhzXfaO73Xc2GAlfi96g5fGUa378WbIGfFw==} + deprecated: Please migrate to the brand new `@rspack/resolver` or `unrs-resolver` instead rss-parser@3.13.0: resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} @@ -9685,6 +9716,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -11450,6 +11482,12 @@ snapshots: dependencies: xss: 1.0.14 + '@ark/schema@0.46.0': + dependencies: + '@ark/util': 0.46.0 + + '@ark/util@0.46.0': {} + '@babel/code-frame@7.22.13': dependencies: '@babel/highlight': 7.22.20 @@ -15108,6 +15146,8 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + alien-signals@2.0.7: {} + alphanum-sort@1.0.2: {} ansi-align@3.0.1: @@ -15288,6 +15328,11 @@ snapshots: argparse@2.0.1: {} + arktype@2.1.20: + dependencies: + '@ark/schema': 0.46.0 + '@ark/util': 0.46.0 + arr-diff@4.0.0: {} arr-flatten@1.1.0: {} @@ -16237,6 +16282,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@10.0.1: {} + commander@2.17.1: {} commander@2.19.0: {} @@ -19505,6 +19552,10 @@ snapshots: lines-and-columns@1.2.4: {} + liquidjs@10.21.1: + dependencies: + commander: 10.0.1 + listhen@1.5.6: dependencies: '@parcel/watcher': 2.3.0 diff --git a/scripts/postinstall.cjs b/scripts/postinstall.cjs deleted file mode 100644 index 5820aa1..0000000 --- a/scripts/postinstall.cjs +++ /dev/null @@ -1,314 +0,0 @@ -"use strict"; -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// scripts/postinstall.ts -var import_node_fs3 = require("fs"); -var import_node_process5 = __toESM(require("process"), 1); - -// src/caddy/index.ts -var import_node_child_process4 = require("child_process"); -var import_node_fs2 = require("fs"); -var import_promises2 = require("fs/promises"); -var import_node_os3 = require("os"); -var import_node_path3 = require("path"); -var import_node_process4 = __toESM(require("process"), 1); -var import_got_cjs = require("got-cjs"); -var import_http_proxy_agent = require("http-proxy-agent"); -var import_https_proxy_agent = require("https-proxy-agent"); -var lockfile = __toESM(require("proper-lockfile"), 1); - -// src/host/index.ts -var import_node_child_process2 = require("child_process"); -var import_node_fs = require("fs"); -var import_node_process2 = __toESM(require("process"), 1); -var import_node_util = require("util"); -var import_hostile = __toESM(require("hostile"), 1); - -// src/utils.ts -var import_node_child_process = require("child_process"); -var import_promises = require("fs/promises"); -var import_node_os = require("os"); -var import_node_path = __toESM(require("path"), 1); -var import_node_process = __toESM(require("process"), 1); -var import_consola = require("consola"); -var import_picocolors = __toESM(require("picocolors"), 1); -var import_qrcode_terminal = __toESM(require("qrcode-terminal"), 1); -var consola = (0, import_consola.createConsola)({ - level: 3 -}).withTag("reverse-proxy"); -function once(fn) { - let called = false; - return (...args) => { - if (called) - return; - called = true; - return fn(...args); - }; -} - -// src/host/index.ts -var _setHost = (0, import_node_util.promisify)(import_hostile.default.set); -var _removeHost = (0, import_node_util.promisify)(import_hostile.default.remove); - -// src/caddy/constants.ts -var import_node_os2 = __toESM(require("os"), 1); -var import_node_path2 = require("path"); -var import_node_process3 = __toESM(require("process"), 1); -var import_dotenv_flow = __toESM(require("dotenv-flow"), 1); -import_dotenv_flow.default.config({ - silent: true -}); -function resolvePath(path2) { - if (path2.startsWith("~")) - return (0, import_node_path2.join)(import_node_os2.default.homedir() || "", path2.slice(1)); - return path2; -} -var TEMP_DIR = resolvePath(import_node_process3.default.env.UHRP_CADDY_PATH || import_node_process3.default.env.TEMP || import_node_process3.default.env.TMPDIR || "/tmp"); -var caddyPath = (0, import_node_path2.join)(TEMP_DIR, `caddy${import_node_process3.default.platform === "win32" ? ".exe" : ""}`); -var caddyFilePath = (0, import_node_path2.join)(TEMP_DIR, "CADDYFILE"); -var caddyLockFilePath = (0, import_node_path2.join)(TEMP_DIR, "caddylock"); -var supportList = [ - { - platform: "dragonfly", - arch: "amd64" - }, - { - platform: "freebsd", - arch: "amd64" - }, - { - platform: "freebsd", - arch: "arm-6" - }, - { - platform: "freebsd", - arch: "arm-7" - }, - { - platform: "freebsd", - arch: "arm64" - }, - { - platform: "linux", - arch: "amd64" - }, - { - platform: "linux", - arch: "arm-5" - }, - { - platform: "linux", - arch: "arm-6" - }, - { - platform: "linux", - arch: "arm-7" - }, - { - platform: "linux", - arch: "arm64" - }, - { - platform: "linux", - arch: "mips" - }, - { - platform: "linux", - arch: "mips64" - }, - { - platform: "linux", - arch: "mips64le" - }, - { - platform: "linux", - arch: "mipsle" - }, - { - platform: "linux", - arch: "ppc64" - }, - { - platform: "linux", - arch: "ppc64le" - }, - { - platform: "linux", - arch: "s390x" - }, - { - platform: "darwin", - arch: "amd64" - }, - { - platform: "darwin", - arch: "arm64" - }, - { - platform: "openbsd", - arch: "amd64" - }, - { - platform: "openbsd", - arch: "arm-6" - }, - { - platform: "openbsd", - arch: "arm-7" - }, - { - platform: "openbsd", - arch: "arm64" - }, - { - platform: "windows", - arch: "amd64" - }, - { - platform: "windows", - arch: "arm-6" - }, - { - platform: "windows", - arch: "arm-7" - }, - { - platform: "windows", - arch: "arm64" - } -]; - -// src/caddy/utils.ts -var import_node_child_process3 = require("child_process"); -var import_node_net = require("net"); -var import_ora = __toESM(require("ora"), 1); -var spinner; -function getNumber(number) { - if (number <= 0) - return "0%"; - else if (number < 100) - return `${number.toFixed(2)}%`; - else - return "100%"; -} -function logProgress(percent) { - if (!spinner) { - consola.log(""); - spinner = (0, import_ora.default)({ - text: "Start Download Caddy: 0%", - indent: 2 - }); - spinner.start(); - } else { - spinner.text = `Download Caddy: ${getNumber(Number(percent) * 100)}`; - } -} -function logProgressOver() { - if (!spinner) { - spinner = (0, import_ora.default)({ - text: "Start Download Caddy: 100%", - indent: 2 - }); - spinner.start(); - } else { - spinner.succeed(); - consola.log(""); - } -} - -// src/caddy/index.ts -async function download() { - if (await testCaddy()) - return caddyPath; - return new Promise((resolve, reject) => { - if (!(0, import_node_fs2.existsSync)(TEMP_DIR)) { - (0, import_node_fs2.mkdirSync)(TEMP_DIR, { recursive: true }); - (0, import_node_fs2.chmodSync)(TEMP_DIR, 511); - } - const file = (0, import_node_fs2.createWriteStream)(caddyPath); - let _platform = (0, import_node_os3.platform)().toString(); - _platform === "win32" && (_platform = "windows"); - let _arch = import_node_process4.default.arch.toString(); - _arch === "x64" && (_arch = "amd64"); - const support = supportList.find((item) => item.platform === _platform && item.arch === _arch); - if (!support) - return reject(new Error("not support")); - const dowmloadLink = `https://caddyserver.com/api/download?os=${support.platform}&arch=${support.arch}&p=github.com%2Fcaddyserver%2Fforwardproxy`; - (0, import_node_fs2.existsSync)(caddyPath) && (0, import_node_fs2.unlinkSync)(caddyPath); - const httpProxy = import_node_process4.default.env.HTTP_PROXY || import_node_process4.default.env.http_proxy; - const httpsProxy = import_node_process4.default.env.HTTPS_PROXY || import_node_process4.default.env.https_proxy; - const httpAgent = httpProxy ? new import_http_proxy_agent.HttpProxyAgent(httpProxy) : void 0; - const httpsAgent = httpsProxy ? new import_https_proxy_agent.HttpsProxyAgent(httpsProxy) : void 0; - const chmodCaddyOnce = once(() => { - (0, import_node_fs2.chmodSync)(caddyPath, 511); - }); - import_got_cjs.got.stream(dowmloadLink, { - agent: { - http: httpAgent, - https: httpsAgent - } - }).on("downloadProgress", (progress) => { - logProgress(progress.percent); - chmodCaddyOnce(); - }).pipe(file).on("finish", () => { - logProgressOver(); - if (import_node_process4.default.platform === "win32") - return resolve(caddyPath); - (0, import_node_fs2.chmodSync)(caddyPath, 511); - resolve(caddyPath); - }).on("error", (err) => { - reject(err); - }); - }); -} -function testCaddy() { - return new Promise((resolve, reject) => { - if (!(0, import_node_fs2.existsSync)(caddyPath)) - return resolve(false); - (0, import_node_fs2.chmodSync)(caddyPath, 511); - const child = import_node_process4.default.platform === "win32" ? (0, import_node_child_process4.spawn)(caddyPath, []) : (0, import_node_child_process4.spawn)("sudo", ["-E", caddyPath]); - child.on("close", () => { - return resolve(false); - }); - child.on("error", (err) => { - return reject(err); - }); - child.stdout.on("data", (_data) => { - return resolve(true); - }); - }); -} - -// scripts/postinstall.ts -async function run() { - try { - if (import_node_process5.default.env.UHRP_AUTO_INSTALL_CADDY) { - (0, import_node_fs3.existsSync)(caddyPath) && (0, import_node_fs3.unlinkSync)(caddyPath); - await download(); - } - } catch (e) { - consola.error(e.code); - } -} -run(); diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts deleted file mode 100644 index 5f1a9df..0000000 --- a/scripts/postinstall.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { existsSync, unlinkSync } from 'node:fs' -import process from 'node:process' -import { download } from '../src/caddy' -import { caddyPath } from '../src/caddy/constants' -import { consola } from '../src/utils' - -async function run() { - try { - if (process.env.UHRP_AUTO_INSTALL_CADDY) { - existsSync(caddyPath) && unlinkSync(caddyPath) - await download() - } - } - catch (e: any) { - consola.error(e.code) - } -} - -run() diff --git a/src/caddy/caddyfile.ts b/src/caddy/caddyfile.ts new file mode 100644 index 0000000..8415455 --- /dev/null +++ b/src/caddy/caddyfile.ts @@ -0,0 +1,81 @@ +import { type } from 'arktype' +import { Liquid } from 'liquidjs' + +export const ProxyType = type({ + target: 'string', + port_suffix: 'string', + tls: 'string', + source: 'string', + base: 'string', +}) + +const CaddyContext = type({ + caddy_root: 'string', + proxies: ProxyType.array(), +}) + +export function validateTemplateContext(ctx: unknown): typeof CaddyContext.infer { + const out = CaddyContext(ctx as any) + if (out instanceof type.errors) + throw new Error(out.summary) + + return ctx as typeof CaddyContext.infer +} + +export const caddyTemplate = `{ + debug + auto_https disable_redirects +} + +:7601 { + root * "{{ caddy_root }}" + file_server browse +} + +# 正向代理配置 +:7600 { + bind 0.0.0.0 + + forward_proxy { + hide_ip + hide_via + acl { + allow all + } + } +} + +{% for p in proxies %} +{{ p.target }}{{ p.port_suffix }} { + {{ p.tls }} + reverse_proxy http://{{ p.source }} { + health_uri {{ p.base }} + health_interval 2s + health_timeout 5s + fail_duration 2s + unhealthy_status 502 503 504 404 + lb_try_duration 3s + lb_try_interval 300ms + health_headers { + Accept text/html + Accept-Encoding gzip, deflate, br + User-Agent Caddy-Health-Check + } + + @error status 502 503 504 404 + handle_response @error { + respond * 503 { + body "Service Unavailable" + close + } + } + } +} +{% endfor %} +` + +const liquid = new Liquid() + +export async function genCaddyfile(ctx: typeof CaddyContext.infer): Promise { + return await liquid.parseAndRender(caddyTemplate, ctx) +} diff --git a/src/caddy/config.ts b/src/caddy/config.ts new file mode 100644 index 0000000..fea35d6 --- /dev/null +++ b/src/caddy/config.ts @@ -0,0 +1,67 @@ +import type { ProxyType } from './caddyfile' +import { existsSync, readFileSync, watch, writeFileSync } from 'node:fs' +import { unlink } from 'node:fs/promises' +import { computed, signal } from 'alien-signals' +import { caddyConfigPath } from './constants' + +export function useCaddyConfig(): { + configJsonRef: () => (Record | undefined) + writeCaddyConfig: (proxies: Array) => void + cleanUp: () => Promise +} { + const configContentRef = signal('{}') + + const configJsonRef = computed(() => { + try { + return JSON.parse(configContentRef()) as Record + } + catch { + return undefined + } + }) + + function setConfigContent(): void { + if (!existsSync(caddyConfigPath)) { + writeFileSync(caddyConfigPath, '{}') + } + try { + configContentRef(readFileSync(caddyConfigPath, 'utf-8')) + } + catch { + configContentRef('') + } + } + + if (!existsSync(caddyConfigPath)) { + writeFileSync(caddyConfigPath, '{}') + } + + const watchCaddyConfig = watch(caddyConfigPath, () => { + setConfigContent() + }) + + function writeCaddyConfig(_proxies: Array): void { + const proxies = configJsonRef() || {} + + _proxies.forEach((p) => { + const index = `${p.target}${p.port_suffix}` + proxies[index] = p + }) + + const content = JSON.stringify(proxies, null, 2) + configContentRef(content) + return writeFileSync(caddyConfigPath, content) + } + + async function cleanUp(): Promise { + await unlink(caddyConfigPath).catch(() => { }) + configContentRef('{}') + watchCaddyConfig.close() + } + + return { + configJsonRef, + writeCaddyConfig, + cleanUp, + } +} diff --git a/src/caddy/constants.ts b/src/caddy/constants.ts index a7b40fa..f62cf4c 100644 --- a/src/caddy/constants.ts +++ b/src/caddy/constants.ts @@ -16,6 +16,8 @@ function resolvePath(path: string): string { export const TEMP_DIR = resolvePath(process.env.UHRP_CADDY_PATH || process.env.TEMP || process.env.TMPDIR || '/tmp') export const caddyPath = join(TEMP_DIR, `caddy${process.platform === 'win32' ? '.exe' : ''}`) export const caddyFilePath = join(TEMP_DIR, 'CADDYFILE') + +export const caddyConfigPath = join(TEMP_DIR, '.CADDYFILE.config.json') export const caddyLockFilePath = join(TEMP_DIR, 'caddylock') export const supportList = [ diff --git a/src/caddy/index.ts b/src/caddy/index.ts index 47b0eed..6f74086 100644 --- a/src/caddy/index.ts +++ b/src/caddy/index.ts @@ -4,12 +4,15 @@ import { readFile, unlink, writeFile } from 'node:fs/promises' import { platform } from 'node:os' import { join } from 'node:path' import process from 'node:process' +import { effect } from 'alien-signals' import { got } from 'got-cjs' import { HttpProxyAgent } from 'http-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent' import * as lockfile from 'proper-lockfile' import { addHost, removeHost } from '../host' import { chmodRecursive, consola, once } from '../utils' +import { genCaddyfile, validateTemplateContext } from './caddyfile' +import { useCaddyConfig } from './config' import { caddyFilePath, caddyLockFilePath, caddyPath, supportList, TEMP_DIR } from './constants' import { kill, logProgress, logProgressOver, tryPort } from './utils' @@ -101,11 +104,13 @@ interface RunOptions { https: boolean } +const { cleanUp, configJsonRef, writeCaddyConfig } = useCaddyConfig() + export class CaddyInstant { private inited = false private stoped = false - /** has locked */ + /** has locked when start */ private locked = false private caddyfile: string | undefined @@ -113,11 +118,10 @@ export class CaddyInstant { private _source: string | undefined private _target: string | undefined + private _stopEffects: Array< (() => void)> = [] constructor() { testCaddy().then(async () => { - await this.getCaddyfile() - this.inited = true }) @@ -131,45 +135,22 @@ export class CaddyInstant { } async getCaddyfile(): Promise { - if (!existsSync(caddyFilePath)) - await this.initCaddyfile() + if (!existsSync(caddyFilePath)) { + this.caddyfile = '' + return '' + } this.caddyfile = await readFile(caddyFilePath, 'utf-8') return this.caddyfile } - async initCaddyfile(): Promise { - const contet = ` -{ - debug - auto_https disable_redirects -} - -:7601 { - root * "${join( - process.platform === 'win32' - ? `${process.env.AppData!}/Caddy` - : `${process.env.HOME!}/Library/Application Support/Caddy`, - './pki/authorities/local', - )}" - file_server browse -} - -# 正向代理配置 -:7600 { - bind 0.0.0.0 - - forward_proxy { - hide_ip - hide_via - acl { - allow all - } - } -} -` - await writeFile(caddyFilePath, contet) - this.caddyfile = contet + get caddyRoot(): string { + return join( + process.platform === 'win32' + ? `${process.env.AppData!}/Caddy` + : `${process.env.HOME!}/Library/Application Support/Caddy`, + './pki/authorities/local', + ) } async writeCaddyfile(): Promise { @@ -208,38 +189,35 @@ export class CaddyInstant { throw new Error('update host failed') consola.success('update host success\n') - await this.getCaddyfile() - if (!(this.caddyfile || '').includes(`${target.split(':')[0]}${https ? '' : ':80'}`)) { - this.caddyfile = `${this.caddyfile} -${target.split(':')[0]}${https ? '' : ':80'} { - ${https ? 'tls internal' : ''} - reverse_proxy http://${source} { - health_uri ${base} - health_interval 2s - health_timeout 5s - fail_duration 2s - unhealthy_status 502 503 504 404 - lb_try_duration 3s - lb_try_interval 300ms - health_headers { - Accept text/html - Accept-Encoding gzip, deflate, br - User-Agent Caddy-Health-Check - } + const targetHost = target.split(':')[0] - @error status 502 503 504 404 - handle_response @error { - respond * 503 { - body "Service Unavailable" - close - } - } - } -} -` + const proxies = [ + { + target: targetHost, + port_suffix: https ? '' : ':80', + tls: https ? 'tls internal' : '', + source, + base, + }, + ] + + writeCaddyConfig(proxies) + + this._stopEffects.push( + effect(async () => { + const proxies = Object.values(configJsonRef() || {}) + if (!(this.caddyfile || '').includes(`${target.split(':')[0]}${https ? '' : ':80'}`)) { + const ctx = validateTemplateContext({ + caddy_root: this.caddyRoot, + proxies, + }) + + this.caddyfile = await genCaddyfile(ctx) + await this.writeCaddyfile() + } + }), + ) - await this.writeCaddyfile() - } if (!existsSync(caddyLockFilePath)) writeFileSync(caddyLockFilePath, '') @@ -366,6 +344,8 @@ ${target.split(':')[0]}${https ? '' : ':80'} { } async baseCleanup(restore: boolean = true): Promise { + this._stopEffects.forEach(fn => fn()) + try { if (restore && this._source && this._target) { if (await removeHost(this._source.split(':')[0], this._target.split(':')[0])) @@ -385,6 +365,7 @@ ${target.split(':')[0]}${https ? '' : ':80'} { await lockfile.unlock(caddyLockFilePath).catch(() => { }) await unlink(caddyFilePath).catch(() => { }) await unlink(caddyLockFilePath).catch(() => { }) + await cleanUp() } catch (e) { consola.error(e) diff --git a/test/caddy/index.test.ts b/test/caddy/index.test.ts index 796b6d4..3b6a6a0 100644 --- a/test/caddy/index.test.ts +++ b/test/caddy/index.test.ts @@ -14,6 +14,10 @@ beforeAll(async () => { await unlink(resolve(baseTestUrl, TEMP_DIR)) }) +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + describe('caddy', () => { it('download', async () => { expect(await download()).toBe(caddyPath) @@ -39,8 +43,7 @@ describe('caddy', () => { const caddyfile = await caddy.getCaddyfile() expect(caddyfile).toMatchInlineSnapshot(` - " - { + "{ debug auto_https disable_redirects } @@ -63,6 +66,7 @@ describe('caddy', () => { } } + test-1.abc.com:80 { reverse_proxy http://127.0.0.1:8080 { @@ -88,6 +92,7 @@ describe('caddy', () => { } } } + " `) @@ -105,8 +110,7 @@ describe('caddy', () => { const caddyfile = await caddy.getCaddyfile() expect(caddyfile).toMatchInlineSnapshot(` - " - { + "{ debug auto_https disable_redirects } @@ -129,6 +133,7 @@ describe('caddy', () => { } } + test-2.abc.com { tls internal reverse_proxy http://127.0.0.1:8080 { @@ -154,6 +159,7 @@ describe('caddy', () => { } } } + " `) await caddy.baseCleanup() @@ -168,8 +174,7 @@ describe('caddy', () => { let caddyfile = await caddy_80.getCaddyfile() expect(caddyfile).toMatchInlineSnapshot(` - " - { + "{ debug auto_https disable_redirects } @@ -192,6 +197,7 @@ describe('caddy', () => { } } + test-3.abc.com:80 { reverse_proxy http://127.0.0.1:8080 { @@ -217,6 +223,7 @@ describe('caddy', () => { } } } + " `) @@ -228,10 +235,10 @@ describe('caddy', () => { expect(caddy_443.checkLock()).toBe(true) + await sleep(50) caddyfile = await caddy_443.getCaddyfile() expect(caddyfile).toMatchInlineSnapshot(` - " - { + "{ debug auto_https disable_redirects } @@ -254,6 +261,7 @@ describe('caddy', () => { } } + test-3.abc.com:80 { reverse_proxy http://127.0.0.1:8080 { @@ -305,6 +313,7 @@ describe('caddy', () => { } } } + " `) @@ -324,8 +333,7 @@ describe('caddy', () => { const caddyfile = await caddy.getCaddyfile() expect(caddyfile).toMatchInlineSnapshot(` - " - { + "{ debug auto_https disable_redirects } @@ -348,6 +356,7 @@ describe('caddy', () => { } } + test-2.abc.com { tls internal reverse_proxy http://127.0.0.1:8080 { @@ -373,6 +382,7 @@ describe('caddy', () => { } } } + " `) diff --git a/tsup.config.ts b/tsup.config.ts index 7a828ee..4024e4f 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -13,15 +13,4 @@ export default [ onSuccess: 'npm run build:fix', external: ['proper-lockfile'], }, - { - entryPoints: [ - 'scripts/postinstall.ts', - ], - target: 'node16', - platform: 'node', - outDir: 'scripts', - format: ['cjs'], - onSuccess: 'npm run build:fix', - external: ['proper-lockfile'], - }, ] satisfies Options[]