openship 0.6.1 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{bare-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
- package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
- package/dist/chunk-4I2BM2DT.js +68 -0
- package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
- package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
- package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
- package/dist/chunk-CBLD2HS5.js +428 -0
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
- package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
- package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
- package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
- package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
- package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
- package/dist/chunk-NK5N5LXN.js +1681 -0
- package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
- package/dist/chunk-Q7WGTQQ5.js +88 -0
- package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
- package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
- package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
- package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
- package/dist/chunk-UVNMQM7W.js +444 -0
- package/dist/chunk-VTGPEC6B.js +308 -0
- package/dist/chunk-WVAML4H6.js +2145 -0
- package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
- package/dist/chunk-YMMQRE5J.js +292 -0
- package/dist/cloud-75VIT4KV.js +37 -0
- package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
- package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
- package/dist/edge-preflight-GBNCPNVU.js +42 -0
- package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
- package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
- package/dist/host-channel-preflight-KE6OONYK.js +50 -0
- package/dist/index.js +3783 -4221
- package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
- package/dist/privilege-I4W3ZNZK.js +19 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +77659 -61909
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +77 -0
- package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
- package/dist/ssh-client-JBMA7SN4.js +28 -0
- package/dist/up-K4BV7466.js +57 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-M2YLWEXO.js +0 -31
- /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -2,31 +2,41 @@
|
|
|
2
2
|
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
edgeDownExplanation,
|
|
7
|
-
explainEdgeDown,
|
|
8
|
-
isEdgeDownFailure
|
|
9
|
-
} from "./chunk-I3DPMTSF.js";
|
|
10
|
-
import {
|
|
11
|
-
edgeFailureReason
|
|
12
|
-
} from "./chunk-VZ2ZHPS2.js";
|
|
13
5
|
import {
|
|
14
6
|
ACME_CHALLENGE_LOCATION,
|
|
15
7
|
ACME_HTTP01_PORT,
|
|
16
8
|
EDGE_CHALLENGE_DIR,
|
|
17
9
|
EDGE_CHALLENGE_LOCATION,
|
|
18
10
|
EDGE_CHALLENGE_URL_PREFIX,
|
|
11
|
+
EDGE_SAME_PATH_MOUNTS,
|
|
19
12
|
LUA_LOGGER_PATH,
|
|
13
|
+
OPENRESTY_DEFAULT_PATHS,
|
|
20
14
|
RULES_GUARD_PATH,
|
|
21
15
|
buildReloadCommand,
|
|
16
|
+
cloudEdgeRealIpConf,
|
|
22
17
|
detectOpenRestyPaths,
|
|
23
18
|
edgeChallengeVhostConf,
|
|
19
|
+
isCloudFrontedHost,
|
|
24
20
|
luaSourceAvailable
|
|
25
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-RSR7PFIQ.js";
|
|
22
|
+
import {
|
|
23
|
+
edgeDownExplanation,
|
|
24
|
+
explainEdgeDown,
|
|
25
|
+
isEdgeDownFailure
|
|
26
|
+
} from "./chunk-I3DPMTSF.js";
|
|
27
|
+
import {
|
|
28
|
+
EXECUTOR_DELEGATE
|
|
29
|
+
} from "./chunk-UVNMQM7W.js";
|
|
26
30
|
import {
|
|
27
31
|
logEntry,
|
|
28
32
|
sq
|
|
29
33
|
} from "./chunk-ZFJXPCQP.js";
|
|
34
|
+
import {
|
|
35
|
+
edgeFailureReason
|
|
36
|
+
} from "./chunk-2YM3NI2E.js";
|
|
37
|
+
import {
|
|
38
|
+
probeListeningPort
|
|
39
|
+
} from "./chunk-SM26HDS2.js";
|
|
30
40
|
import {
|
|
31
41
|
PROXY_DIRECTIVES,
|
|
32
42
|
parseProxyValue,
|
|
@@ -34,7 +44,7 @@ import {
|
|
|
34
44
|
resolveRedirectStatus,
|
|
35
45
|
safeErrorMessage,
|
|
36
46
|
sanitizeProxySettings
|
|
37
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-UJ6JZM65.js";
|
|
38
48
|
|
|
39
49
|
// ../../packages/adapters/src/infra/nginx.ts
|
|
40
50
|
import {
|
|
@@ -43,6 +53,7 @@ import {
|
|
|
43
53
|
rm as fsRm,
|
|
44
54
|
mkdir as fsMkdir,
|
|
45
55
|
readFile as fsReadFile,
|
|
56
|
+
readdir as fsReaddir,
|
|
46
57
|
rename as fsRename,
|
|
47
58
|
stat as fsStat
|
|
48
59
|
} from "fs/promises";
|
|
@@ -168,6 +179,7 @@ ${result.output}` };
|
|
|
168
179
|
}
|
|
169
180
|
return new Proxy(inner, {
|
|
170
181
|
get(target, prop) {
|
|
182
|
+
if (prop === EXECUTOR_DELEGATE) return void 0;
|
|
171
183
|
if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
|
|
172
184
|
return overrides[prop];
|
|
173
185
|
}
|
|
@@ -305,29 +317,84 @@ async function probeStaticOutput(executor, servedPath) {
|
|
|
305
317
|
|
|
306
318
|
// ../../packages/adapters/src/infra/nginx.ts
|
|
307
319
|
var { dirname, join } = edgePath;
|
|
320
|
+
var CTL = "\\x00-\\x1f\\x7f";
|
|
308
321
|
var PROXY_HEADERS = `proxy_set_header Host $host;
|
|
309
322
|
proxy_set_header X-Real-IP $remote_addr;
|
|
310
323
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
311
324
|
proxy_set_header X-Forwarded-Proto $openship_fwd_proto;
|
|
312
325
|
proxy_http_version 1.1;
|
|
313
326
|
proxy_set_header Upgrade $http_upgrade;
|
|
314
|
-
proxy_set_header Connection "upgrade"
|
|
327
|
+
proxy_set_header Connection "upgrade";
|
|
328
|
+
proxy_pass_header X-Accel-Buffering;`;
|
|
315
329
|
var FORWARD_VARS = ` set $openship_fwd_proto $scheme;
|
|
316
330
|
set $openship_redirect_https 1;
|
|
317
331
|
if ($http_x_forwarded_proto = "https") {
|
|
318
332
|
set $openship_fwd_proto https;
|
|
319
333
|
set $openship_redirect_https 0;
|
|
320
334
|
}`;
|
|
321
|
-
|
|
335
|
+
var VHOST_GENERATION = 2;
|
|
336
|
+
var GENERATION_MARKER = `# openship-vhost-gen: ${VHOST_GENERATION}`;
|
|
337
|
+
var GENERATED_HEADER = `# Auto-generated by Openship - do not edit manually
|
|
338
|
+
${GENERATION_MARKER}`;
|
|
339
|
+
function readVhostGeneration(conf) {
|
|
340
|
+
const m = conf.match(/^# openship-vhost-gen: (\d+)$/m);
|
|
341
|
+
return m ? Number(m[1]) : null;
|
|
342
|
+
}
|
|
343
|
+
var HTTPS_UPGRADE = `if ($openship_redirect_https) {
|
|
344
|
+
return 301 https://$server_name$request_uri;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
`;
|
|
348
|
+
var WEBHOOK_LOCATION_PREFIX = "/_openship/hooks/";
|
|
349
|
+
function renderProxyLocations(route, opts) {
|
|
322
350
|
if (!route.proxyLocations || route.proxyLocations.length === 0) return "";
|
|
323
|
-
|
|
351
|
+
const head = `${opts.httpsUpgrade ? HTTPS_UPGRADE : ""}${renderRedirectRules(route, " ")}`;
|
|
352
|
+
const claimed = /* @__PURE__ */ new Set(["/", WEBHOOK_LOCATION_PREFIX]);
|
|
353
|
+
const blocks = [];
|
|
354
|
+
for (const loc of route.proxyLocations) {
|
|
324
355
|
assertValidUpstream(loc.targetUrl);
|
|
325
|
-
|
|
326
|
-
|
|
356
|
+
assertNotReservedLocation(loc.pathPrefix);
|
|
357
|
+
const headers = loc.external ? externalProxyHeaders(loc.targetUrl) : PROXY_HEADERS;
|
|
358
|
+
if (!loc.pattern || !loc.upstreamPath) {
|
|
359
|
+
assertValidLiteralPath(loc.pathPrefix, "proxy location prefix");
|
|
360
|
+
if (claimed.has(loc.pathPrefix)) continue;
|
|
361
|
+
claimed.add(loc.pathPrefix);
|
|
362
|
+
blocks.push(`
|
|
363
|
+
location ^~ ${loc.pathPrefix} {
|
|
364
|
+
${head}proxy_pass ${loc.targetUrl};
|
|
365
|
+
${headers}
|
|
366
|
+
}`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
assertValidRedirectPattern(loc.pattern);
|
|
370
|
+
assertNotReservedLocation(loc.pattern);
|
|
371
|
+
assertNoNginxInjection(loc.upstreamPath, "rewrite upstream path");
|
|
372
|
+
assertNoRuntimeVariable(loc.upstreamPath, "rewrite upstream path", true);
|
|
373
|
+
const match = `^${toNginxPattern(loc.pattern)}$`;
|
|
374
|
+
if (claimed.has(`~ ${match}`)) continue;
|
|
375
|
+
claimed.add(`~ ${match}`);
|
|
376
|
+
blocks.push(`
|
|
377
|
+
location ~ ${match} {
|
|
378
|
+
${head}rewrite ${match} ${loc.upstreamPath} break;
|
|
327
379
|
proxy_pass ${loc.targetUrl};
|
|
328
|
-
${
|
|
329
|
-
}
|
|
330
|
-
}
|
|
380
|
+
${headers}
|
|
381
|
+
}`);
|
|
382
|
+
}
|
|
383
|
+
return blocks.join("");
|
|
384
|
+
}
|
|
385
|
+
function assertNotReservedLocation(pathOrPattern) {
|
|
386
|
+
if (pathOrPattern.startsWith(WELL_KNOWN_PREFIX)) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Refusing to generate a location under ${WELL_KNOWN_PREFIX}: ${pathOrPattern}. That namespace belongs to the edge \u2014 a location there can hijack certificate issuance.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
var WELL_KNOWN_PREFIX = "/.well-known/";
|
|
393
|
+
function externalProxyHeaders(targetUrl) {
|
|
394
|
+
const host = new URL(targetUrl).host;
|
|
395
|
+
assertNoNginxInjection(host, "external proxy host");
|
|
396
|
+
return `${PROXY_HEADERS.replace("Host $host;", `Host ${host};`)}
|
|
397
|
+
proxy_ssl_server_name on;`;
|
|
331
398
|
}
|
|
332
399
|
function renderHostRedirect(route) {
|
|
333
400
|
const redirect = route.redirectHost;
|
|
@@ -335,23 +402,98 @@ function renderHostRedirect(route) {
|
|
|
335
402
|
assertValidDomain(redirect.target);
|
|
336
403
|
return `return ${resolveRedirectStatus(redirect.statusCode)} https://${redirect.target}$request_uri;`;
|
|
337
404
|
}
|
|
338
|
-
function
|
|
405
|
+
function renderRedirectRules(route, indent) {
|
|
339
406
|
if (!route.redirects || route.redirects.length === 0) return "";
|
|
340
|
-
|
|
341
|
-
(r)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
407
|
+
const rules = route.redirects.map((r) => {
|
|
408
|
+
assertValidRedirect(r);
|
|
409
|
+
const match = r.pattern ? `^${toNginxPattern(r.pattern, "?")}(?:[?].*)?$` : r.exact ? `^${escapeLiteralPath(r.path)}(?:[?].*)?$` : `^${escapeLiteralPath(r.path)}`;
|
|
410
|
+
const query = r.destination.includes("?") ? "" : "$is_args$args";
|
|
411
|
+
return `${indent}if ($request_uri ~ "${match}") {
|
|
412
|
+
${indent} return ${r.statusCode} ${r.destination}${query};
|
|
413
|
+
${indent}}`;
|
|
414
|
+
});
|
|
415
|
+
return `${rules.join("\n")}
|
|
416
|
+
|
|
417
|
+
${indent}`;
|
|
346
418
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
419
|
+
function assertValidHeader(key, value) {
|
|
420
|
+
if (!/^[A-Za-z0-9-]+$/.test(key)) throw new Error(`Invalid response header name: ${key}`);
|
|
421
|
+
if (/[\r\n]/.test(value)) throw new Error(`Invalid response header value for ${key}`);
|
|
422
|
+
assertNoRuntimeVariable(value, `response header value for ${key}`);
|
|
423
|
+
}
|
|
424
|
+
function addHeaderLine(key, value) {
|
|
425
|
+
assertValidHeader(key, value);
|
|
426
|
+
return ` add_header ${key} "${value.replace(/"/g, '\\"')}" always;`;
|
|
427
|
+
}
|
|
428
|
+
function renderServerHeaders(route, slug) {
|
|
429
|
+
const lines = headerEntries(route, slug).map(
|
|
430
|
+
(e) => e.variable === null ? addHeaderLine(e.header.key, e.header.value) : ` add_header ${e.header.key} $${e.variable} always;`
|
|
351
431
|
);
|
|
352
432
|
return lines.length > 0 ? `
|
|
353
433
|
${lines.join("\n")}` : "";
|
|
354
434
|
}
|
|
435
|
+
function headerEntries(route, slug) {
|
|
436
|
+
const prefix = `osh_hdr_${slug.replace(/[^A-Za-z0-9]+/g, "_")}`;
|
|
437
|
+
let n = 0;
|
|
438
|
+
return (route.headerRules ?? []).flatMap(
|
|
439
|
+
(rule) => rule.headers.map((header) => ({
|
|
440
|
+
rule,
|
|
441
|
+
header,
|
|
442
|
+
variable: rule.path === "/" ? null : `${prefix}_${n++}`
|
|
443
|
+
}))
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function renderHeaderMaps(route, slug) {
|
|
447
|
+
const scoped = headerEntries(route, slug).filter((e) => e.variable !== null);
|
|
448
|
+
if (scoped.length === 0) return "";
|
|
449
|
+
const blocks = scoped.map((e) => {
|
|
450
|
+
assertValidLiteralPath(e.rule.path, "header rule path");
|
|
451
|
+
assertValidHeader(e.header.key, e.header.value);
|
|
452
|
+
return `map $request_uri $${e.variable} {
|
|
453
|
+
default "";
|
|
454
|
+
~^${escapeLiteralPath(e.rule.path)} "${e.header.value.replace(/"/g, '\\"')}";
|
|
455
|
+
}`;
|
|
456
|
+
});
|
|
457
|
+
return `${blocks.join("\n")}
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
function renderUrlShape(route) {
|
|
461
|
+
if (!("staticRoot" in route) || !route.staticRoot) return "";
|
|
462
|
+
const rules = [];
|
|
463
|
+
if (route.cleanUrls) {
|
|
464
|
+
rules.push(` if ($request_uri ~ "^([^?]*)[.]html([?].*)?$") {
|
|
465
|
+
return 308 $1$2;
|
|
466
|
+
}`);
|
|
467
|
+
}
|
|
468
|
+
if (route.trailingSlash === false) {
|
|
469
|
+
rules.push(` if ($request_uri ~ "^([^?]+)/([?].*)?$") {
|
|
470
|
+
return 308 $1$2;
|
|
471
|
+
}`);
|
|
472
|
+
}
|
|
473
|
+
return rules.length > 0 ? `${rules.join("\n").trimStart()}
|
|
474
|
+
|
|
475
|
+
` : "";
|
|
476
|
+
}
|
|
477
|
+
var SLASH_FALLBACK_NAME = "@osh_add_slash";
|
|
478
|
+
function renderSlashFallback(staticRoot) {
|
|
479
|
+
return ` location ${SLASH_FALLBACK_NAME} {
|
|
480
|
+
root ${staticRoot};
|
|
481
|
+
|
|
482
|
+
if ($request_uri ~ "^([^?]*[^/?])([?].*)?$") {
|
|
483
|
+
return 308 $1/$2;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try_files /index.html =404;
|
|
487
|
+
}`;
|
|
488
|
+
}
|
|
489
|
+
function renderStaticTryFiles(route) {
|
|
490
|
+
const parts = ["$uri"];
|
|
491
|
+
if (route.cleanUrls) parts.push("$uri.html");
|
|
492
|
+
if (route.trailingSlash === false) parts.push("$uri/index.html");
|
|
493
|
+
else parts.push("$uri/");
|
|
494
|
+
parts.push(route.trailingSlash === true ? SLASH_FALLBACK_NAME : "/index.html");
|
|
495
|
+
return `try_files ${parts.join(" ")};`;
|
|
496
|
+
}
|
|
355
497
|
function renderProxyDirectives(proxy, opts) {
|
|
356
498
|
const lines = resolveProxyDirectives(proxy, {
|
|
357
499
|
scope: opts.tlsOnly ? "tls" : "shared",
|
|
@@ -427,16 +569,58 @@ function assertValidDomain(domain) {
|
|
|
427
569
|
}
|
|
428
570
|
}
|
|
429
571
|
function assertNoNginxInjection(value, what) {
|
|
430
|
-
if (/[\s;{}#\\]/.test(value)) {
|
|
572
|
+
if (/[\s;{}#\\'"]/.test(value)) {
|
|
431
573
|
throw new Error(`Invalid ${what} (contains characters that could inject nginx config): ${value}`);
|
|
432
574
|
}
|
|
433
575
|
}
|
|
576
|
+
function assertNoRuntimeVariable(value, what, allowCaptures = false) {
|
|
577
|
+
const residue = allowCaptures ? value.replace(/\$[1-9]/g, "") : value;
|
|
578
|
+
if (residue.includes("$")) {
|
|
579
|
+
throw new Error(`Invalid ${what} (contains an nginx variable reference): ${value}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
434
582
|
function assertValidUpstream(targetUrl) {
|
|
435
583
|
assertNoNginxInjection(targetUrl, "proxy target");
|
|
584
|
+
assertNoRuntimeVariable(targetUrl, "proxy target");
|
|
436
585
|
if (!/^https?:\/\/.+/.test(targetUrl)) {
|
|
437
586
|
throw new Error(`Invalid proxy target (must be http/https URL): ${targetUrl}`);
|
|
438
587
|
}
|
|
439
588
|
}
|
|
589
|
+
var REDIRECT_PATTERN_RE = /^\/(?:[A-Za-z0-9_~\-/]|\\\.|\(\.\*\)|\(\.\+\)|\(\[\^\/\][*+]\))*$/;
|
|
590
|
+
function assertValidRedirectPattern(pattern) {
|
|
591
|
+
if (!REDIRECT_PATTERN_RE.test(pattern)) {
|
|
592
|
+
throw new Error(`Invalid capture pattern: ${pattern}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
var REGEX_METACHARS = /[*+?^$()[\]|]/;
|
|
596
|
+
function assertValidLiteralPath(value, what) {
|
|
597
|
+
assertNoNginxInjection(value, what);
|
|
598
|
+
if (!value.startsWith("/") || REGEX_METACHARS.test(value)) {
|
|
599
|
+
throw new Error(`Invalid ${what} (not a plain path): ${value}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function escapeLiteralPath(value) {
|
|
603
|
+
return value.replace(/\./g, "[.]");
|
|
604
|
+
}
|
|
605
|
+
function assertValidRedirect(redirect) {
|
|
606
|
+
if (redirect.pattern !== void 0) assertValidRedirectPattern(redirect.pattern);
|
|
607
|
+
assertValidLiteralPath(redirect.path, "redirect path");
|
|
608
|
+
assertNoNginxInjection(redirect.destination, "redirect destination");
|
|
609
|
+
assertNoRuntimeVariable(redirect.destination, "redirect destination", true);
|
|
610
|
+
}
|
|
611
|
+
var NGINX_CAPTURE = {
|
|
612
|
+
"(.*)": `([^${CTL}]*)`,
|
|
613
|
+
"(.+)": `([^${CTL}]+)`,
|
|
614
|
+
"([^/]+)": `([^/${CTL}]+)`,
|
|
615
|
+
"([^/]*)": `([^/${CTL}]*)`
|
|
616
|
+
};
|
|
617
|
+
function toNginxPattern(pattern, alsoExclude = "") {
|
|
618
|
+
return pattern.replace(/\(\.[*+]\)|\(\[\^\/\][*+]\)/g, (group) => {
|
|
619
|
+
const hardened = NGINX_CAPTURE[group];
|
|
620
|
+
if (!hardened) return group;
|
|
621
|
+
return alsoExclude ? hardened.replace(`${CTL}]`, `${CTL}${alsoExclude}]`) : hardened;
|
|
622
|
+
});
|
|
623
|
+
}
|
|
440
624
|
var CHALLENGE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._~-]{7,127}$/;
|
|
441
625
|
function assertValidChallengeToken(token) {
|
|
442
626
|
if (!CHALLENGE_TOKEN_RE.test(token) || token.includes("..")) {
|
|
@@ -458,6 +642,11 @@ function assertValidStaticRoot(root, opts) {
|
|
|
458
642
|
}
|
|
459
643
|
}
|
|
460
644
|
var execFileAsync = promisify(cpExecFile);
|
|
645
|
+
function isAcmePortBindFailure(output) {
|
|
646
|
+
return /could not bind (?:tcp )?port|address already in use|problem binding to port/i.test(
|
|
647
|
+
output || ""
|
|
648
|
+
);
|
|
649
|
+
}
|
|
461
650
|
function summarizeCertbotFailure(output, domain) {
|
|
462
651
|
const edgeDown = edgeDownExplanation(output);
|
|
463
652
|
if (edgeDown) return edgeDown;
|
|
@@ -474,7 +663,9 @@ function certbotDiagnosis(output, domain) {
|
|
|
474
663
|
const challengeUrl = pick(/https?:\/\/\S*\.well-known\/acme-challenge\/\S*/i);
|
|
475
664
|
const redirectedToHttps = /^https:/i.test(challengeUrl ?? "");
|
|
476
665
|
let diagnosis;
|
|
477
|
-
if (
|
|
666
|
+
if (isAcmePortBindFailure(text)) {
|
|
667
|
+
diagnosis = `Couldn't start the certificate challenge listener for ${domain} on port ${ACME_HTTP01_PORT}: something on this server is already holding it. That port is used only while a certificate is being issued, so this is almost always a certificate run from an earlier attempt that hasn't exited \u2014 retry, and if it persists find the holder with \`ss -ltnp | grep ${ACME_HTTP01_PORT}\` and stop it. Nothing was requested from Let's Encrypt, so this is not a DNS or routing problem.`;
|
|
668
|
+
} else if (/timeout during connect|connection refused|failed to connect|Timeout/i.test(text)) {
|
|
478
669
|
diagnosis = `Port 80 for ${domain} isn't reachable from the internet \u2014 a firewall / cloud security group is blocking it, the domain doesn't point at this server, or another proxy is still bound to :80.`;
|
|
479
670
|
} else if (/NXDOMAIN|no\s+(A|AAAA)\s+record|DNS problem|could not be resolved|no records? found/i.test(text)) {
|
|
480
671
|
diagnosis = `${domain} doesn't resolve to this server yet \u2014 the DNS A record is missing or hasn't propagated. Wait for propagation, then retry from the Domains tab.`;
|
|
@@ -531,10 +722,11 @@ var NginxProvider = class {
|
|
|
531
722
|
async _writeFile(path, content, mode) {
|
|
532
723
|
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
533
724
|
if (this.executor) {
|
|
725
|
+
if (mode !== void 0) this._assertChmodReaches(path, mode);
|
|
534
726
|
try {
|
|
535
727
|
await this.executor.writeFile(tmpPath, content);
|
|
536
728
|
if (mode !== void 0) {
|
|
537
|
-
await this.
|
|
729
|
+
await this._chmod(tmpPath, mode);
|
|
538
730
|
}
|
|
539
731
|
if (this.executor.rename) {
|
|
540
732
|
await this.executor.rename(tmpPath, path);
|
|
@@ -590,6 +782,46 @@ var NginxProvider = class {
|
|
|
590
782
|
await fsMkdir(path, { recursive: true });
|
|
591
783
|
}
|
|
592
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Do a FILE op and a COMMAND at `path` reach the same bytes?
|
|
787
|
+
*
|
|
788
|
+
* On a container edge they travel different namespaces: `writeFile`/`mkdir` land on
|
|
789
|
+
* the HOST (bind mount) while `exec` runs INSIDE the container. They name the same
|
|
790
|
+
* file only where a mount carries the same string on both sides
|
|
791
|
+
* ({@link EDGE_SAME_PATH_MOUNTS}). A bare or remote edge has one namespace, so
|
|
792
|
+
* everything qualifies.
|
|
793
|
+
*/
|
|
794
|
+
_sharesHostNamespace(path) {
|
|
795
|
+
if (!this.containerEdge) return true;
|
|
796
|
+
return EDGE_SAME_PATH_MOUNTS.some((m) => path === m || path.startsWith(`${m}/`));
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Refuse an operation that mixes the two channels at a path where they are not the
|
|
800
|
+
* same file. `action` completes "Refusing to …" and must name the path.
|
|
801
|
+
*
|
|
802
|
+
* Never "skip it and continue", because both failure modes are silent. A `chmod`
|
|
803
|
+
* either misses or tightens a DIFFERENT file that does exist in the container,
|
|
804
|
+
* leaving a TLS private key or the ACME EAB HMAC key at the default mode and
|
|
805
|
+
* reporting success. An `openssl -keyout` writes the key into the container's own
|
|
806
|
+
* filesystem, where the host path OpenResty reads never sees it — and a vhost
|
|
807
|
+
* pointing at material the host lacks fails `openresty -t` for the WHOLE edge.
|
|
808
|
+
*/
|
|
809
|
+
_assertOneNamespace(path, action) {
|
|
810
|
+
if (this._sharesHostNamespace(path)) return;
|
|
811
|
+
throw new Error(
|
|
812
|
+
`Refusing to ${action}: this edge runs in a container, so file operations land on the host while commands run inside it. Only paths under ${EDGE_SAME_PATH_MOUNTS.join(", ")} are the same path on both sides.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
/** The chmod half of {@link _assertOneNamespace}: the write travels the file channel, the mode the command channel. */
|
|
816
|
+
_assertChmodReaches(path, mode) {
|
|
817
|
+
this._assertOneNamespace(path, `write ${path} with mode 0${mode.toString(8)}`);
|
|
818
|
+
}
|
|
819
|
+
/** No-op without an executor: the local path sets the mode at creation instead. */
|
|
820
|
+
async _chmod(path, mode) {
|
|
821
|
+
if (!this.executor) return;
|
|
822
|
+
this._assertChmodReaches(path, mode);
|
|
823
|
+
await this.executor.exec(`chmod ${sq(mode.toString(8))} ${sq(path)}`);
|
|
824
|
+
}
|
|
593
825
|
/**
|
|
594
826
|
* Put `fullchain.pem` + `privkey.pem` in `dir` as ONE atomic step.
|
|
595
827
|
*
|
|
@@ -606,7 +838,7 @@ var NginxProvider = class {
|
|
|
606
838
|
try {
|
|
607
839
|
await this._mkdir(staging);
|
|
608
840
|
await this._writeFile(join(staging, "fullchain.pem"), cert.certPem);
|
|
609
|
-
await this._writeFile(join(staging, "privkey.pem"), cert.keyPem);
|
|
841
|
+
await this._writeFile(join(staging, "privkey.pem"), cert.keyPem, 384);
|
|
610
842
|
if (this.executor) {
|
|
611
843
|
await this._mkdir(dir);
|
|
612
844
|
await this.executor.exec(
|
|
@@ -666,6 +898,12 @@ var NginxProvider = class {
|
|
|
666
898
|
const dir = this.bootstrapCertDir(domain);
|
|
667
899
|
const certPath = join(dir, "fullchain.pem");
|
|
668
900
|
const keyPath = join(dir, "privkey.pem");
|
|
901
|
+
if (!this._sharesHostNamespace(dir)) {
|
|
902
|
+
console.warn(
|
|
903
|
+
`[SSL] not creating a temporary certificate for ${domain}: ${dir} is not the same path inside the edge container as on the host, so OpenResty could not read the key \u2014 serving HTTP only until a real certificate is issued.`
|
|
904
|
+
);
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
669
907
|
try {
|
|
670
908
|
if (await this._exists(certPath) && await this._exists(keyPath)) {
|
|
671
909
|
return { certPath, keyPath };
|
|
@@ -693,6 +931,7 @@ var NginxProvider = class {
|
|
|
693
931
|
"-addext",
|
|
694
932
|
`subjectAltName=DNS:${domain}`
|
|
695
933
|
]);
|
|
934
|
+
await this._chmod(join(staging, "privkey.pem"), 384);
|
|
696
935
|
await this._mkdir(dir);
|
|
697
936
|
if (this.executor) {
|
|
698
937
|
await this.executor.exec(
|
|
@@ -796,13 +1035,10 @@ var NginxProvider = class {
|
|
|
796
1035
|
if (!this.acmeEabKid || !this.acmeEabHmacKey) return null;
|
|
797
1036
|
const dir = join(dirname(this.certDir), `.openship-eab-${randomBytes(12).toString("hex")}`);
|
|
798
1037
|
const path = join(dir, "eab.ini");
|
|
799
|
-
if (this.executor)
|
|
800
|
-
|
|
801
|
-
await this._exec("chmod", ["700", dir]);
|
|
802
|
-
} else {
|
|
803
|
-
await fsMkdir(dir, { recursive: true, mode: 448 });
|
|
804
|
-
}
|
|
1038
|
+
if (this.executor) await this.executor.mkdir(dir);
|
|
1039
|
+
else await fsMkdir(dir, { recursive: true, mode: 448 });
|
|
805
1040
|
try {
|
|
1041
|
+
await this._chmod(dir, 448);
|
|
806
1042
|
await this._writeFile(
|
|
807
1043
|
path,
|
|
808
1044
|
`eab-kid = ${this.acmeEabKid}
|
|
@@ -854,14 +1090,20 @@ eab-hmac-key = ${this.acmeEabHmacKey}
|
|
|
854
1090
|
const hostRedirect = renderHostRedirect(route);
|
|
855
1091
|
const locationBody = hostRedirect ? hostRedirect : "staticRoot" in route && route.staticRoot ? `root ${route.staticRoot};
|
|
856
1092
|
index index.html;
|
|
857
|
-
|
|
1093
|
+
${renderStaticTryFiles(route)}` : `proxy_pass ${route.targetUrl};
|
|
858
1094
|
${PROXY_HEADERS}`;
|
|
859
|
-
const
|
|
860
|
-
const
|
|
1095
|
+
const urlShape = hostRedirect ? "" : renderUrlShape(route);
|
|
1096
|
+
const redirectRules = hostRedirect ? "" : renderRedirectRules(route, " ");
|
|
1097
|
+
const extraLocations = hostRedirect ? "" : renderProxyLocations(route, { httpsUpgrade: false });
|
|
1098
|
+
const upgradingExtraLocations = hostRedirect ? "" : renderProxyLocations(route, { httpsUpgrade: true });
|
|
1099
|
+
const serverHeaders = hostRedirect ? "" : renderServerHeaders(route, slug);
|
|
1100
|
+
const prelude = hostRedirect ? "" : renderHeaderMaps(route, slug);
|
|
861
1101
|
const proxyOpts = renderProxyOptions(route.proxy, this.nginxVersion);
|
|
862
1102
|
const proxyTlsOpts = renderProxyTlsOptions(route.proxy, this.nginxVersion);
|
|
1103
|
+
const preamble = isCloudFrontedHost(route.domain) ? `${cloudEdgeRealIpConf()}
|
|
1104
|
+
${FORWARD_VARS}` : FORWARD_VARS;
|
|
863
1105
|
const webhookLocation = route.webhookProxy && !hostRedirect ? `
|
|
864
|
-
location
|
|
1106
|
+
location ^~ ${WEBHOOK_LOCATION_PREFIX} {
|
|
865
1107
|
proxy_pass ${route.webhookProxy};
|
|
866
1108
|
proxy_set_header Host $host;
|
|
867
1109
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -872,24 +1114,25 @@ eab-hmac-key = ${this.acmeEabHmacKey}
|
|
|
872
1114
|
const edgeLua = luaSourceAvailable();
|
|
873
1115
|
const luaProxy = edgeLua ? ` log_by_lua_file ${LUA_LOGGER_PATH};
|
|
874
1116
|
access_by_lua_file ${RULES_GUARD_PATH};` : "";
|
|
1117
|
+
const slashFallback = !hostRedirect && "staticRoot" in route && route.staticRoot && route.trailingSlash === true ? `
|
|
1118
|
+
|
|
1119
|
+
${renderSlashFallback(route.staticRoot)}` : "";
|
|
875
1120
|
const serveLocation = ` location / {
|
|
876
|
-
${locationBody}
|
|
877
|
-
}`;
|
|
1121
|
+
${redirectRules}${urlShape}${locationBody}
|
|
1122
|
+
}${slashFallback}`;
|
|
878
1123
|
const redirectOrServeLocation = hostRedirect ? serveLocation : ` location / {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
${locationBody}
|
|
884
|
-
}`;
|
|
1124
|
+
${HTTPS_UPGRADE}${redirectRules}${urlShape}${locationBody}
|
|
1125
|
+
}${slashFallback}`;
|
|
885
1126
|
const sharedBody = `${serverHeaders}${proxyOpts}
|
|
886
1127
|
${webhookLocation}${extraLocations}`;
|
|
887
|
-
const
|
|
1128
|
+
const httpsUpgradeBody = `${serverHeaders}${proxyOpts}
|
|
1129
|
+
${webhookLocation}${upgradingExtraLocations}`;
|
|
1130
|
+
const httpOnlyBlock = `${GENERATED_HEADER}
|
|
888
1131
|
server {
|
|
889
1132
|
listen 80;
|
|
890
1133
|
server_name ${route.domain};
|
|
891
1134
|
|
|
892
|
-
${
|
|
1135
|
+
${preamble}
|
|
893
1136
|
|
|
894
1137
|
${luaProxy}
|
|
895
1138
|
|
|
@@ -905,19 +1148,19 @@ ${serveLocation}
|
|
|
905
1148
|
if (route.tls && await this.certsExist(route.domain)) {
|
|
906
1149
|
const certPath = join(this.certDir, route.domain, "fullchain.pem");
|
|
907
1150
|
const keyPath = join(this.certDir, route.domain, "privkey.pem");
|
|
908
|
-
serverBlock =
|
|
1151
|
+
serverBlock = `${GENERATED_HEADER}
|
|
909
1152
|
server {
|
|
910
1153
|
listen 80;
|
|
911
1154
|
server_name ${route.domain};
|
|
912
1155
|
|
|
913
|
-
${
|
|
1156
|
+
${preamble}
|
|
914
1157
|
|
|
915
1158
|
${luaProxy}
|
|
916
1159
|
|
|
917
1160
|
${ACME_CHALLENGE_LOCATION}
|
|
918
1161
|
|
|
919
1162
|
${EDGE_CHALLENGE_LOCATION}
|
|
920
|
-
${
|
|
1163
|
+
${httpsUpgradeBody}
|
|
921
1164
|
${redirectOrServeLocation}
|
|
922
1165
|
}
|
|
923
1166
|
|
|
@@ -925,7 +1168,7 @@ server {
|
|
|
925
1168
|
listen 443 ssl;
|
|
926
1169
|
server_name ${route.domain};
|
|
927
1170
|
|
|
928
|
-
${
|
|
1171
|
+
${preamble}
|
|
929
1172
|
|
|
930
1173
|
${luaProxy}
|
|
931
1174
|
|
|
@@ -938,7 +1181,7 @@ ${serveLocation}
|
|
|
938
1181
|
} else if (route.tls && route.terminatesTlsLocally) {
|
|
939
1182
|
const bootstrap = await this.ensureBootstrapCert(route.domain);
|
|
940
1183
|
bootstrapServed = !!bootstrap;
|
|
941
|
-
serverBlock = bootstrap ?
|
|
1184
|
+
serverBlock = bootstrap ? `${GENERATED_HEADER}
|
|
942
1185
|
# openship-bootstrap-tls: ${route.domain} is serving a TEMPORARY self-signed
|
|
943
1186
|
# certificate because its real certificate has not been issued yet. Openship
|
|
944
1187
|
# replaces this file automatically once it lands. Do not adopt this material.
|
|
@@ -946,7 +1189,7 @@ server {
|
|
|
946
1189
|
listen 80;
|
|
947
1190
|
server_name ${route.domain};
|
|
948
1191
|
|
|
949
|
-
${
|
|
1192
|
+
${preamble}
|
|
950
1193
|
|
|
951
1194
|
${luaProxy}
|
|
952
1195
|
|
|
@@ -961,7 +1204,7 @@ server {
|
|
|
961
1204
|
listen 443 ssl;
|
|
962
1205
|
server_name ${route.domain};
|
|
963
1206
|
|
|
964
|
-
${
|
|
1207
|
+
${preamble}
|
|
965
1208
|
|
|
966
1209
|
${luaProxy}
|
|
967
1210
|
|
|
@@ -975,7 +1218,7 @@ ${serveLocation}
|
|
|
975
1218
|
serverBlock = httpOnlyBlock;
|
|
976
1219
|
}
|
|
977
1220
|
const snapshot = await this._captureFile(configPath);
|
|
978
|
-
await this._writeFile(configPath, serverBlock);
|
|
1221
|
+
await this._writeFile(configPath, `${prelude}${serverBlock}`);
|
|
979
1222
|
try {
|
|
980
1223
|
await this.reload();
|
|
981
1224
|
} catch (err) {
|
|
@@ -991,6 +1234,112 @@ ${serveLocation}
|
|
|
991
1234
|
routeStatePath(slug) {
|
|
992
1235
|
return join(this.sitesDir, `${slug}.route.json`);
|
|
993
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Re-emit every vhost an older generator wrote, from its own persisted RouteConfig.
|
|
1239
|
+
*
|
|
1240
|
+
* Why this exists: a fix to the EMITTED shape otherwise never reaches a running box.
|
|
1241
|
+
* `registerRoute` is the only writer, and the only things that call it are a deploy, a
|
|
1242
|
+
* domain save, a Retry, and cert issuance — so a project nobody redeploys keeps the vhost
|
|
1243
|
+
* it was first given for as long as it lives. That is how the Cloud-fronted `X-Real-IP`
|
|
1244
|
+
* block shipped and then applied to nothing: correct code, upgraded box, unchanged file.
|
|
1245
|
+
*
|
|
1246
|
+
* Cheap and idempotent by construction: a vhost already stamped at
|
|
1247
|
+
* {@link VHOST_GENERATION} is read and skipped, so the pass writes nothing and reloads
|
|
1248
|
+
* nothing once converged. Only the FIRST run after a bump does work.
|
|
1249
|
+
*
|
|
1250
|
+
* That first run is not free, and the release that introduced the marker is its worst
|
|
1251
|
+
* case: every conf written before it is unstamped, so a box with N routes reapplies all
|
|
1252
|
+
* N. `registerRoute` writes and reloads per route by design — a conf that fails
|
|
1253
|
+
* `openresty -t` is rolled back and reloaded on its own, which is why one bad route
|
|
1254
|
+
* cannot take the other N-1 down — so that run costs N whole-config tests and N reloads,
|
|
1255
|
+
* serially, on a deploy. Batching them behind a single test would trade that isolation
|
|
1256
|
+
* for the seconds it saves once per box, which is the wrong trade. What it does mean is
|
|
1257
|
+
* that a VHOST_GENERATION bump is a fleet-wide cost, not a free redeploy: bump it for a
|
|
1258
|
+
* fix that has to reach existing boxes, not to tidy the emitted text.
|
|
1259
|
+
*
|
|
1260
|
+
* Never throws and never aborts the sweep. One route whose sidecar is unreadable, or
|
|
1261
|
+
* whose re-render fails `openresty -t` (registerRoute rolls that file back on its own),
|
|
1262
|
+
* must not stop the rest — a half-converged edge with a reported failure is strictly
|
|
1263
|
+
* better than a wedged one. A route with no sidecar is left ALONE rather than
|
|
1264
|
+
* reconstructed from its conf: the scrape recovers only the primary target, so replaying
|
|
1265
|
+
* it would drop composite locations and the webhook proxy.
|
|
1266
|
+
*/
|
|
1267
|
+
async reapplyStoredRoutes() {
|
|
1268
|
+
const repaired = [];
|
|
1269
|
+
const failed = [];
|
|
1270
|
+
let entries;
|
|
1271
|
+
try {
|
|
1272
|
+
entries = await this._lsSites();
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
return { scanned: 0, repaired, failed: [{ slug: "*", reason: safeErrorMessage(err) }] };
|
|
1275
|
+
}
|
|
1276
|
+
const slugs = entries.filter((name) => name.endsWith(".route.json")).map((name) => name.slice(0, -".route.json".length));
|
|
1277
|
+
const stale = await this._staleVhostSlugs().catch(() => null);
|
|
1278
|
+
for (const slug of slugs) {
|
|
1279
|
+
if (stale && !stale.has(slug)) continue;
|
|
1280
|
+
try {
|
|
1281
|
+
const conf = await this._readFile(join(this.sitesDir, `${slug}.conf`)).catch(() => null);
|
|
1282
|
+
if (conf === null) continue;
|
|
1283
|
+
const gen = readVhostGeneration(conf);
|
|
1284
|
+
if (gen !== null && gen >= VHOST_GENERATION) continue;
|
|
1285
|
+
const saved = JSON.parse(await this._readFile(this.routeStatePath(slug)));
|
|
1286
|
+
if (!saved?.domain) {
|
|
1287
|
+
failed.push({ slug, reason: "sidecar has no domain" });
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
await this.registerRoute(saved);
|
|
1291
|
+
repaired.push(saved.domain);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
failed.push({ slug, reason: safeErrorMessage(err) });
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return { scanned: slugs.length, repaired, failed };
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Names in the sites directory.
|
|
1300
|
+
*
|
|
1301
|
+
* Listing goes through the COMMAND channel, so on a container edge it must name the
|
|
1302
|
+
* path commands see: `sitesDir` is the HOST side of the bind mount and does not exist
|
|
1303
|
+
* inside the container. Same directory either way, and only basenames are used — the
|
|
1304
|
+
* reads that follow go back through the file channel at the host path.
|
|
1305
|
+
*/
|
|
1306
|
+
async _lsSites() {
|
|
1307
|
+
if (!this.executor) return fsReaddir(this.sitesDir);
|
|
1308
|
+
const out = await this.executor.exec(`ls -1 ${sq(this._commandSitesDir())} 2>/dev/null || true`);
|
|
1309
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Slugs whose `.conf` does not carry the CURRENT generation marker — `null` when the
|
|
1313
|
+
* question can't be answered in one command and the caller should fall back to reading.
|
|
1314
|
+
*
|
|
1315
|
+
* `grep -L` on the whole directory, so the steady state (everything current) costs one
|
|
1316
|
+
* round trip instead of one per route. The pattern is anchored on both ends and built from
|
|
1317
|
+
* the same constant the writer emits, so `gen: 1` cannot pass for `gen: 10` and the two
|
|
1318
|
+
* halves cannot drift apart.
|
|
1319
|
+
*/
|
|
1320
|
+
async _staleVhostSlugs() {
|
|
1321
|
+
if (!this.executor) return null;
|
|
1322
|
+
const dir = this._commandSitesDir();
|
|
1323
|
+
const out = await this.executor.exec(
|
|
1324
|
+
`grep -L ${sq(`^${GENERATION_MARKER}$`)} ${sq(dir)}/*.conf 2>/dev/null || true`
|
|
1325
|
+
);
|
|
1326
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1327
|
+
for (const line of out.split("\n")) {
|
|
1328
|
+
const name = line.trim().split("/").pop();
|
|
1329
|
+
if (name?.endsWith(".conf")) slugs.add(name.slice(0, -".conf".length));
|
|
1330
|
+
}
|
|
1331
|
+
return slugs;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* The sites directory as COMMANDS see it.
|
|
1335
|
+
*
|
|
1336
|
+
* On a container edge `sitesDir` is the HOST side of the bind mount and does not exist
|
|
1337
|
+
* inside the container, where `exec` runs. Same directory, two names; only basenames are
|
|
1338
|
+
* used, and the reads that follow go back through the file channel at the host path.
|
|
1339
|
+
*/
|
|
1340
|
+
_commandSitesDir() {
|
|
1341
|
+
return this.containerEdge ? OPENRESTY_DEFAULT_PATHS.sitesDir : this.sitesDir;
|
|
1342
|
+
}
|
|
994
1343
|
/**
|
|
995
1344
|
* Recover the proxy tunables from a vhost we already wrote, for the legacy
|
|
996
1345
|
* no-sidecar re-register below. Rewriting the conf without them would silently
|
|
@@ -1083,7 +1432,7 @@ ${serveLocation}
|
|
|
1083
1432
|
const path2 = join(this.sitesDir, `${stem}.conf`);
|
|
1084
1433
|
const conf = await this._readFile(path2).catch(() => "");
|
|
1085
1434
|
if (!conf || !this.serverNamesIn(conf).includes(host)) continue;
|
|
1086
|
-
if (conf.includes(
|
|
1435
|
+
if (conf.includes(`${EDGE_CHALLENGE_URL_PREFIX} {`)) {
|
|
1087
1436
|
return { served: true, via: "existing-vhost" };
|
|
1088
1437
|
}
|
|
1089
1438
|
return {
|
|
@@ -1112,18 +1461,24 @@ ${serveLocation}
|
|
|
1112
1461
|
/**
|
|
1113
1462
|
* Provision a TLS certificate using certbot.
|
|
1114
1463
|
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1464
|
+
* HTTP-01 ONLY — there is no DNS-01 path anywhere in this file, and the DNS credentials the
|
|
1465
|
+
* product stores are for record MANAGEMENT, not for challenges.
|
|
1466
|
+
*
|
|
1467
|
+
* The authenticator is `--standalone` on {@link ACME_HTTP01_PORT} (a loopback alt-port), with
|
|
1468
|
+
* the edge proxying `/.well-known/acme-challenge/` to it — see `ACME_CHALLENGE_LOCATION`. So
|
|
1469
|
+
* Let's Encrypt fetches the challenge over :80 from OpenResty, which forwards it to certbot's
|
|
1470
|
+
* transient listener. No port-80 fight, no webroot directory to keep in sync, and zero
|
|
1471
|
+
* downtime: certbot holds that port only while issuing.
|
|
1118
1472
|
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1473
|
+
* THIS COMMENT USED TO DESCRIBE `--webroot`, and argued that `--standalone` was dead code
|
|
1474
|
+
* because "certbot --standalone binds to port 80 itself, but OpenResty already owns 80". That
|
|
1475
|
+
* was true of plain `--standalone`, and it is exactly the problem `--http-01-port` solves —
|
|
1476
|
+
* the two sat contradicting each other in one file, with the doc describing a mechanism the
|
|
1477
|
+
* code below had stopped using. Left recorded because the stale version's reasoning is
|
|
1478
|
+
* persuasive enough to "fix" the code back.
|
|
1479
|
+
*
|
|
1480
|
+
* The caller (route-registration.ts) wraps this in try/catch, so a failure becomes a "deploy
|
|
1481
|
+
* continues on HTTP, retry from the Domains tab" warning rather than a deploy abort.
|
|
1127
1482
|
*/
|
|
1128
1483
|
async provisionCert(domain, opts) {
|
|
1129
1484
|
assertValidDomain(domain);
|
|
@@ -1155,7 +1510,15 @@ ${serveLocation}
|
|
|
1155
1510
|
...opts?.force ? ["--force-renewal"] : []
|
|
1156
1511
|
], opts?.onLog);
|
|
1157
1512
|
} catch (err) {
|
|
1158
|
-
|
|
1513
|
+
const raw = this.redactAcmeSecrets(safeErrorMessage(err));
|
|
1514
|
+
let summary = summarizeCertbotFailure(raw, domain);
|
|
1515
|
+
if (isAcmePortBindFailure(raw) && this.executor) {
|
|
1516
|
+
const occupant = await probeListeningPort(this.executor, ACME_HTTP01_PORT).catch(() => null);
|
|
1517
|
+
if (occupant) {
|
|
1518
|
+
summary += ` Currently held by ${occupant.command}` + (occupant.pid !== null ? ` (pid ${occupant.pid})` : "") + (occupant.systemdUnit ? ` via ${occupant.systemdUnit}` : "") + `.`;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
throw new Error(summary);
|
|
1159
1522
|
} finally {
|
|
1160
1523
|
if (eabConfig) await this._rm(dirname(eabConfig)).catch(() => void 0);
|
|
1161
1524
|
}
|
|
@@ -1619,12 +1982,15 @@ export {
|
|
|
1619
1982
|
validateCertFor,
|
|
1620
1983
|
isSafeCertPath,
|
|
1621
1984
|
readDeclaredPair,
|
|
1985
|
+
VHOST_GENERATION,
|
|
1986
|
+
readVhostGeneration,
|
|
1622
1987
|
renderProxyOptions,
|
|
1623
1988
|
renderProxyTlsOptions,
|
|
1624
1989
|
renderGeoEntries,
|
|
1625
1990
|
acmeKeyArgs,
|
|
1626
1991
|
assertValidChallengeToken,
|
|
1627
1992
|
MANAGED_STATIC_BASE,
|
|
1993
|
+
isAcmePortBindFailure,
|
|
1628
1994
|
summarizeCertbotFailure,
|
|
1629
1995
|
NginxProvider
|
|
1630
1996
|
};
|