openship 0.6.0 → 0.6.5
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-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
- package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
- package/dist/chunk-6HOA5JT7.js +124 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
- package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
- package/dist/chunk-7S4USQYF.js +1753 -0
- package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/chunk-EZPPTWJQ.js +292 -0
- package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
- package/dist/chunk-HPXP6LXZ.js +428 -0
- package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
- package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
- package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
- package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
- package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
- package/dist/chunk-RFWJE7ZN.js +68 -0
- package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
- package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
- package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
- package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
- package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
- package/dist/chunk-WFRXYWK2.js +444 -0
- package/dist/chunk-XRNRT37W.js +316 -0
- package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
- package/dist/cloud-3DODOWTR.js +36 -0
- package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
- package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
- package/dist/edge-preflight-ES6FH4JK.js +42 -0
- package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
- package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
- package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
- package/dist/index.js +3668 -4063
- package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
- package/dist/privilege-YTVI4SKF.js +19 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/index.js +21501 -13137
- package/dist/server/lua/geo_country.lua +4 -3
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +22 -24
- 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/meta/_journal.json +49 -0
- package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
- package/dist/up-XEO3SR6Z.js +58 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-E5M6VMCG.js +0 -31
- package/dist/server/lua/pipe_log.lua +0 -77
- /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
|
@@ -9,20 +9,27 @@ import {
|
|
|
9
9
|
} from "./chunk-I3DPMTSF.js";
|
|
10
10
|
import {
|
|
11
11
|
edgeFailureReason
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-WE7BXW6L.js";
|
|
13
13
|
import {
|
|
14
14
|
ACME_CHALLENGE_LOCATION,
|
|
15
15
|
ACME_HTTP01_PORT,
|
|
16
16
|
EDGE_CHALLENGE_DIR,
|
|
17
17
|
EDGE_CHALLENGE_LOCATION,
|
|
18
18
|
EDGE_CHALLENGE_URL_PREFIX,
|
|
19
|
+
EDGE_SAME_PATH_MOUNTS,
|
|
19
20
|
LUA_LOGGER_PATH,
|
|
21
|
+
OPENRESTY_DEFAULT_PATHS,
|
|
20
22
|
RULES_GUARD_PATH,
|
|
21
23
|
buildReloadCommand,
|
|
24
|
+
cloudEdgeRealIpConf,
|
|
22
25
|
detectOpenRestyPaths,
|
|
23
26
|
edgeChallengeVhostConf,
|
|
27
|
+
isCloudFrontedHost,
|
|
24
28
|
luaSourceAvailable
|
|
25
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-PZRRL5PC.js";
|
|
30
|
+
import {
|
|
31
|
+
EXECUTOR_DELEGATE
|
|
32
|
+
} from "./chunk-WFRXYWK2.js";
|
|
26
33
|
import {
|
|
27
34
|
logEntry,
|
|
28
35
|
sq
|
|
@@ -34,7 +41,7 @@ import {
|
|
|
34
41
|
resolveRedirectStatus,
|
|
35
42
|
safeErrorMessage,
|
|
36
43
|
sanitizeProxySettings
|
|
37
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-LEYSL46B.js";
|
|
38
45
|
|
|
39
46
|
// ../../packages/adapters/src/infra/nginx.ts
|
|
40
47
|
import {
|
|
@@ -43,6 +50,7 @@ import {
|
|
|
43
50
|
rm as fsRm,
|
|
44
51
|
mkdir as fsMkdir,
|
|
45
52
|
readFile as fsReadFile,
|
|
53
|
+
readdir as fsReaddir,
|
|
46
54
|
rename as fsRename,
|
|
47
55
|
stat as fsStat
|
|
48
56
|
} from "fs/promises";
|
|
@@ -168,6 +176,7 @@ ${result.output}` };
|
|
|
168
176
|
}
|
|
169
177
|
return new Proxy(inner, {
|
|
170
178
|
get(target, prop) {
|
|
179
|
+
if (prop === EXECUTOR_DELEGATE) return void 0;
|
|
171
180
|
if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
|
|
172
181
|
return overrides[prop];
|
|
173
182
|
}
|
|
@@ -305,6 +314,7 @@ async function probeStaticOutput(executor, servedPath) {
|
|
|
305
314
|
|
|
306
315
|
// ../../packages/adapters/src/infra/nginx.ts
|
|
307
316
|
var { dirname, join } = edgePath;
|
|
317
|
+
var CTL = "\\x00-\\x1f\\x7f";
|
|
308
318
|
var PROXY_HEADERS = `proxy_set_header Host $host;
|
|
309
319
|
proxy_set_header X-Real-IP $remote_addr;
|
|
310
320
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
@@ -318,16 +328,69 @@ var FORWARD_VARS = ` set $openship_fwd_proto $scheme;
|
|
|
318
328
|
set $openship_fwd_proto https;
|
|
319
329
|
set $openship_redirect_https 0;
|
|
320
330
|
}`;
|
|
321
|
-
|
|
331
|
+
var VHOST_GENERATION = 1;
|
|
332
|
+
var GENERATION_MARKER = `# openship-vhost-gen: ${VHOST_GENERATION}`;
|
|
333
|
+
var GENERATED_HEADER = `# Auto-generated by Openship - do not edit manually
|
|
334
|
+
${GENERATION_MARKER}`;
|
|
335
|
+
function readVhostGeneration(conf) {
|
|
336
|
+
const m = conf.match(/^# openship-vhost-gen: (\d+)$/m);
|
|
337
|
+
return m ? Number(m[1]) : null;
|
|
338
|
+
}
|
|
339
|
+
var HTTPS_UPGRADE = `if ($openship_redirect_https) {
|
|
340
|
+
return 301 https://$server_name$request_uri;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
`;
|
|
344
|
+
var WEBHOOK_LOCATION_PREFIX = "/_openship/hooks/";
|
|
345
|
+
function renderProxyLocations(route, opts) {
|
|
322
346
|
if (!route.proxyLocations || route.proxyLocations.length === 0) return "";
|
|
323
|
-
|
|
347
|
+
const head = `${opts.httpsUpgrade ? HTTPS_UPGRADE : ""}${renderRedirectRules(route, " ")}`;
|
|
348
|
+
const claimed = /* @__PURE__ */ new Set(["/", WEBHOOK_LOCATION_PREFIX]);
|
|
349
|
+
const blocks = [];
|
|
350
|
+
for (const loc of route.proxyLocations) {
|
|
324
351
|
assertValidUpstream(loc.targetUrl);
|
|
325
|
-
|
|
326
|
-
|
|
352
|
+
assertNotReservedLocation(loc.pathPrefix);
|
|
353
|
+
const headers = loc.external ? externalProxyHeaders(loc.targetUrl) : PROXY_HEADERS;
|
|
354
|
+
if (!loc.pattern || !loc.upstreamPath) {
|
|
355
|
+
assertValidLiteralPath(loc.pathPrefix, "proxy location prefix");
|
|
356
|
+
if (claimed.has(loc.pathPrefix)) continue;
|
|
357
|
+
claimed.add(loc.pathPrefix);
|
|
358
|
+
blocks.push(`
|
|
359
|
+
location ^~ ${loc.pathPrefix} {
|
|
360
|
+
${head}proxy_pass ${loc.targetUrl};
|
|
361
|
+
${headers}
|
|
362
|
+
}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
assertValidRedirectPattern(loc.pattern);
|
|
366
|
+
assertNotReservedLocation(loc.pattern);
|
|
367
|
+
assertNoNginxInjection(loc.upstreamPath, "rewrite upstream path");
|
|
368
|
+
assertNoRuntimeVariable(loc.upstreamPath, "rewrite upstream path", true);
|
|
369
|
+
const match = `^${toNginxPattern(loc.pattern)}$`;
|
|
370
|
+
if (claimed.has(`~ ${match}`)) continue;
|
|
371
|
+
claimed.add(`~ ${match}`);
|
|
372
|
+
blocks.push(`
|
|
373
|
+
location ~ ${match} {
|
|
374
|
+
${head}rewrite ${match} ${loc.upstreamPath} break;
|
|
327
375
|
proxy_pass ${loc.targetUrl};
|
|
328
|
-
${
|
|
329
|
-
}
|
|
330
|
-
}
|
|
376
|
+
${headers}
|
|
377
|
+
}`);
|
|
378
|
+
}
|
|
379
|
+
return blocks.join("");
|
|
380
|
+
}
|
|
381
|
+
function assertNotReservedLocation(pathOrPattern) {
|
|
382
|
+
if (pathOrPattern.startsWith(WELL_KNOWN_PREFIX)) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Refusing to generate a location under ${WELL_KNOWN_PREFIX}: ${pathOrPattern}. That namespace belongs to the edge \u2014 a location there can hijack certificate issuance.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
var WELL_KNOWN_PREFIX = "/.well-known/";
|
|
389
|
+
function externalProxyHeaders(targetUrl) {
|
|
390
|
+
const host = new URL(targetUrl).host;
|
|
391
|
+
assertNoNginxInjection(host, "external proxy host");
|
|
392
|
+
return `${PROXY_HEADERS.replace("Host $host;", `Host ${host};`)}
|
|
393
|
+
proxy_ssl_server_name on;`;
|
|
331
394
|
}
|
|
332
395
|
function renderHostRedirect(route) {
|
|
333
396
|
const redirect = route.redirectHost;
|
|
@@ -335,23 +398,98 @@ function renderHostRedirect(route) {
|
|
|
335
398
|
assertValidDomain(redirect.target);
|
|
336
399
|
return `return ${resolveRedirectStatus(redirect.statusCode)} https://${redirect.target}$request_uri;`;
|
|
337
400
|
}
|
|
338
|
-
function
|
|
401
|
+
function renderRedirectRules(route, indent) {
|
|
339
402
|
if (!route.redirects || route.redirects.length === 0) return "";
|
|
340
|
-
|
|
341
|
-
(r)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
403
|
+
const rules = route.redirects.map((r) => {
|
|
404
|
+
assertValidRedirect(r);
|
|
405
|
+
const match = r.pattern ? `^${toNginxPattern(r.pattern, "?")}(?:[?].*)?$` : r.exact ? `^${escapeLiteralPath(r.path)}(?:[?].*)?$` : `^${escapeLiteralPath(r.path)}`;
|
|
406
|
+
const query = r.destination.includes("?") ? "" : "$is_args$args";
|
|
407
|
+
return `${indent}if ($request_uri ~ "${match}") {
|
|
408
|
+
${indent} return ${r.statusCode} ${r.destination}${query};
|
|
409
|
+
${indent}}`;
|
|
410
|
+
});
|
|
411
|
+
return `${rules.join("\n")}
|
|
412
|
+
|
|
413
|
+
${indent}`;
|
|
414
|
+
}
|
|
415
|
+
function assertValidHeader(key, value) {
|
|
416
|
+
if (!/^[A-Za-z0-9-]+$/.test(key)) throw new Error(`Invalid response header name: ${key}`);
|
|
417
|
+
if (/[\r\n]/.test(value)) throw new Error(`Invalid response header value for ${key}`);
|
|
418
|
+
assertNoRuntimeVariable(value, `response header value for ${key}`);
|
|
346
419
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
420
|
+
function addHeaderLine(key, value) {
|
|
421
|
+
assertValidHeader(key, value);
|
|
422
|
+
return ` add_header ${key} "${value.replace(/"/g, '\\"')}" always;`;
|
|
423
|
+
}
|
|
424
|
+
function renderServerHeaders(route, slug) {
|
|
425
|
+
const lines = headerEntries(route, slug).map(
|
|
426
|
+
(e) => e.variable === null ? addHeaderLine(e.header.key, e.header.value) : ` add_header ${e.header.key} $${e.variable} always;`
|
|
351
427
|
);
|
|
352
428
|
return lines.length > 0 ? `
|
|
353
429
|
${lines.join("\n")}` : "";
|
|
354
430
|
}
|
|
431
|
+
function headerEntries(route, slug) {
|
|
432
|
+
const prefix = `osh_hdr_${slug.replace(/[^A-Za-z0-9]+/g, "_")}`;
|
|
433
|
+
let n = 0;
|
|
434
|
+
return (route.headerRules ?? []).flatMap(
|
|
435
|
+
(rule) => rule.headers.map((header) => ({
|
|
436
|
+
rule,
|
|
437
|
+
header,
|
|
438
|
+
variable: rule.path === "/" ? null : `${prefix}_${n++}`
|
|
439
|
+
}))
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
function renderHeaderMaps(route, slug) {
|
|
443
|
+
const scoped = headerEntries(route, slug).filter((e) => e.variable !== null);
|
|
444
|
+
if (scoped.length === 0) return "";
|
|
445
|
+
const blocks = scoped.map((e) => {
|
|
446
|
+
assertValidLiteralPath(e.rule.path, "header rule path");
|
|
447
|
+
assertValidHeader(e.header.key, e.header.value);
|
|
448
|
+
return `map $request_uri $${e.variable} {
|
|
449
|
+
default "";
|
|
450
|
+
~^${escapeLiteralPath(e.rule.path)} "${e.header.value.replace(/"/g, '\\"')}";
|
|
451
|
+
}`;
|
|
452
|
+
});
|
|
453
|
+
return `${blocks.join("\n")}
|
|
454
|
+
`;
|
|
455
|
+
}
|
|
456
|
+
function renderUrlShape(route) {
|
|
457
|
+
if (!("staticRoot" in route) || !route.staticRoot) return "";
|
|
458
|
+
const rules = [];
|
|
459
|
+
if (route.cleanUrls) {
|
|
460
|
+
rules.push(` if ($request_uri ~ "^([^?]*)[.]html([?].*)?$") {
|
|
461
|
+
return 308 $1$2;
|
|
462
|
+
}`);
|
|
463
|
+
}
|
|
464
|
+
if (route.trailingSlash === false) {
|
|
465
|
+
rules.push(` if ($request_uri ~ "^([^?]+)/([?].*)?$") {
|
|
466
|
+
return 308 $1$2;
|
|
467
|
+
}`);
|
|
468
|
+
}
|
|
469
|
+
return rules.length > 0 ? `${rules.join("\n").trimStart()}
|
|
470
|
+
|
|
471
|
+
` : "";
|
|
472
|
+
}
|
|
473
|
+
var SLASH_FALLBACK_NAME = "@osh_add_slash";
|
|
474
|
+
function renderSlashFallback(staticRoot) {
|
|
475
|
+
return ` location ${SLASH_FALLBACK_NAME} {
|
|
476
|
+
root ${staticRoot};
|
|
477
|
+
|
|
478
|
+
if ($request_uri ~ "^([^?]*[^/?])([?].*)?$") {
|
|
479
|
+
return 308 $1/$2;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try_files /index.html =404;
|
|
483
|
+
}`;
|
|
484
|
+
}
|
|
485
|
+
function renderStaticTryFiles(route) {
|
|
486
|
+
const parts = ["$uri"];
|
|
487
|
+
if (route.cleanUrls) parts.push("$uri.html");
|
|
488
|
+
if (route.trailingSlash === false) parts.push("$uri/index.html");
|
|
489
|
+
else parts.push("$uri/");
|
|
490
|
+
parts.push(route.trailingSlash === true ? SLASH_FALLBACK_NAME : "/index.html");
|
|
491
|
+
return `try_files ${parts.join(" ")};`;
|
|
492
|
+
}
|
|
355
493
|
function renderProxyDirectives(proxy, opts) {
|
|
356
494
|
const lines = resolveProxyDirectives(proxy, {
|
|
357
495
|
scope: opts.tlsOnly ? "tls" : "shared",
|
|
@@ -427,16 +565,58 @@ function assertValidDomain(domain) {
|
|
|
427
565
|
}
|
|
428
566
|
}
|
|
429
567
|
function assertNoNginxInjection(value, what) {
|
|
430
|
-
if (/[\s;{}#\\]/.test(value)) {
|
|
568
|
+
if (/[\s;{}#\\'"]/.test(value)) {
|
|
431
569
|
throw new Error(`Invalid ${what} (contains characters that could inject nginx config): ${value}`);
|
|
432
570
|
}
|
|
433
571
|
}
|
|
572
|
+
function assertNoRuntimeVariable(value, what, allowCaptures = false) {
|
|
573
|
+
const residue = allowCaptures ? value.replace(/\$[1-9]/g, "") : value;
|
|
574
|
+
if (residue.includes("$")) {
|
|
575
|
+
throw new Error(`Invalid ${what} (contains an nginx variable reference): ${value}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
434
578
|
function assertValidUpstream(targetUrl) {
|
|
435
579
|
assertNoNginxInjection(targetUrl, "proxy target");
|
|
580
|
+
assertNoRuntimeVariable(targetUrl, "proxy target");
|
|
436
581
|
if (!/^https?:\/\/.+/.test(targetUrl)) {
|
|
437
582
|
throw new Error(`Invalid proxy target (must be http/https URL): ${targetUrl}`);
|
|
438
583
|
}
|
|
439
584
|
}
|
|
585
|
+
var REDIRECT_PATTERN_RE = /^\/(?:[A-Za-z0-9_~\-/]|\\\.|\(\.\*\)|\(\.\+\)|\(\[\^\/\][*+]\))*$/;
|
|
586
|
+
function assertValidRedirectPattern(pattern) {
|
|
587
|
+
if (!REDIRECT_PATTERN_RE.test(pattern)) {
|
|
588
|
+
throw new Error(`Invalid capture pattern: ${pattern}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
var REGEX_METACHARS = /[*+?^$()[\]|]/;
|
|
592
|
+
function assertValidLiteralPath(value, what) {
|
|
593
|
+
assertNoNginxInjection(value, what);
|
|
594
|
+
if (!value.startsWith("/") || REGEX_METACHARS.test(value)) {
|
|
595
|
+
throw new Error(`Invalid ${what} (not a plain path): ${value}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function escapeLiteralPath(value) {
|
|
599
|
+
return value.replace(/\./g, "[.]");
|
|
600
|
+
}
|
|
601
|
+
function assertValidRedirect(redirect) {
|
|
602
|
+
if (redirect.pattern !== void 0) assertValidRedirectPattern(redirect.pattern);
|
|
603
|
+
assertValidLiteralPath(redirect.path, "redirect path");
|
|
604
|
+
assertNoNginxInjection(redirect.destination, "redirect destination");
|
|
605
|
+
assertNoRuntimeVariable(redirect.destination, "redirect destination", true);
|
|
606
|
+
}
|
|
607
|
+
var NGINX_CAPTURE = {
|
|
608
|
+
"(.*)": `([^${CTL}]*)`,
|
|
609
|
+
"(.+)": `([^${CTL}]+)`,
|
|
610
|
+
"([^/]+)": `([^/${CTL}]+)`,
|
|
611
|
+
"([^/]*)": `([^/${CTL}]*)`
|
|
612
|
+
};
|
|
613
|
+
function toNginxPattern(pattern, alsoExclude = "") {
|
|
614
|
+
return pattern.replace(/\(\.[*+]\)|\(\[\^\/\][*+]\)/g, (group) => {
|
|
615
|
+
const hardened = NGINX_CAPTURE[group];
|
|
616
|
+
if (!hardened) return group;
|
|
617
|
+
return alsoExclude ? hardened.replace(`${CTL}]`, `${CTL}${alsoExclude}]`) : hardened;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
440
620
|
var CHALLENGE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._~-]{7,127}$/;
|
|
441
621
|
function assertValidChallengeToken(token) {
|
|
442
622
|
if (!CHALLENGE_TOKEN_RE.test(token) || token.includes("..")) {
|
|
@@ -531,10 +711,11 @@ var NginxProvider = class {
|
|
|
531
711
|
async _writeFile(path, content, mode) {
|
|
532
712
|
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
533
713
|
if (this.executor) {
|
|
714
|
+
if (mode !== void 0) this._assertChmodReaches(path, mode);
|
|
534
715
|
try {
|
|
535
716
|
await this.executor.writeFile(tmpPath, content);
|
|
536
717
|
if (mode !== void 0) {
|
|
537
|
-
await this.
|
|
718
|
+
await this._chmod(tmpPath, mode);
|
|
538
719
|
}
|
|
539
720
|
if (this.executor.rename) {
|
|
540
721
|
await this.executor.rename(tmpPath, path);
|
|
@@ -590,6 +771,46 @@ var NginxProvider = class {
|
|
|
590
771
|
await fsMkdir(path, { recursive: true });
|
|
591
772
|
}
|
|
592
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Do a FILE op and a COMMAND at `path` reach the same bytes?
|
|
776
|
+
*
|
|
777
|
+
* On a container edge they travel different namespaces: `writeFile`/`mkdir` land on
|
|
778
|
+
* the HOST (bind mount) while `exec` runs INSIDE the container. They name the same
|
|
779
|
+
* file only where a mount carries the same string on both sides
|
|
780
|
+
* ({@link EDGE_SAME_PATH_MOUNTS}). A bare or remote edge has one namespace, so
|
|
781
|
+
* everything qualifies.
|
|
782
|
+
*/
|
|
783
|
+
_sharesHostNamespace(path) {
|
|
784
|
+
if (!this.containerEdge) return true;
|
|
785
|
+
return EDGE_SAME_PATH_MOUNTS.some((m) => path === m || path.startsWith(`${m}/`));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Refuse an operation that mixes the two channels at a path where they are not the
|
|
789
|
+
* same file. `action` completes "Refusing to …" and must name the path.
|
|
790
|
+
*
|
|
791
|
+
* Never "skip it and continue", because both failure modes are silent. A `chmod`
|
|
792
|
+
* either misses or tightens a DIFFERENT file that does exist in the container,
|
|
793
|
+
* leaving a TLS private key or the ACME EAB HMAC key at the default mode and
|
|
794
|
+
* reporting success. An `openssl -keyout` writes the key into the container's own
|
|
795
|
+
* filesystem, where the host path OpenResty reads never sees it — and a vhost
|
|
796
|
+
* pointing at material the host lacks fails `openresty -t` for the WHOLE edge.
|
|
797
|
+
*/
|
|
798
|
+
_assertOneNamespace(path, action) {
|
|
799
|
+
if (this._sharesHostNamespace(path)) return;
|
|
800
|
+
throw new Error(
|
|
801
|
+
`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.`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
/** The chmod half of {@link _assertOneNamespace}: the write travels the file channel, the mode the command channel. */
|
|
805
|
+
_assertChmodReaches(path, mode) {
|
|
806
|
+
this._assertOneNamespace(path, `write ${path} with mode 0${mode.toString(8)}`);
|
|
807
|
+
}
|
|
808
|
+
/** No-op without an executor: the local path sets the mode at creation instead. */
|
|
809
|
+
async _chmod(path, mode) {
|
|
810
|
+
if (!this.executor) return;
|
|
811
|
+
this._assertChmodReaches(path, mode);
|
|
812
|
+
await this.executor.exec(`chmod ${sq(mode.toString(8))} ${sq(path)}`);
|
|
813
|
+
}
|
|
593
814
|
/**
|
|
594
815
|
* Put `fullchain.pem` + `privkey.pem` in `dir` as ONE atomic step.
|
|
595
816
|
*
|
|
@@ -606,7 +827,7 @@ var NginxProvider = class {
|
|
|
606
827
|
try {
|
|
607
828
|
await this._mkdir(staging);
|
|
608
829
|
await this._writeFile(join(staging, "fullchain.pem"), cert.certPem);
|
|
609
|
-
await this._writeFile(join(staging, "privkey.pem"), cert.keyPem);
|
|
830
|
+
await this._writeFile(join(staging, "privkey.pem"), cert.keyPem, 384);
|
|
610
831
|
if (this.executor) {
|
|
611
832
|
await this._mkdir(dir);
|
|
612
833
|
await this.executor.exec(
|
|
@@ -666,6 +887,12 @@ var NginxProvider = class {
|
|
|
666
887
|
const dir = this.bootstrapCertDir(domain);
|
|
667
888
|
const certPath = join(dir, "fullchain.pem");
|
|
668
889
|
const keyPath = join(dir, "privkey.pem");
|
|
890
|
+
if (!this._sharesHostNamespace(dir)) {
|
|
891
|
+
console.warn(
|
|
892
|
+
`[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.`
|
|
893
|
+
);
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
669
896
|
try {
|
|
670
897
|
if (await this._exists(certPath) && await this._exists(keyPath)) {
|
|
671
898
|
return { certPath, keyPath };
|
|
@@ -693,6 +920,7 @@ var NginxProvider = class {
|
|
|
693
920
|
"-addext",
|
|
694
921
|
`subjectAltName=DNS:${domain}`
|
|
695
922
|
]);
|
|
923
|
+
await this._chmod(join(staging, "privkey.pem"), 384);
|
|
696
924
|
await this._mkdir(dir);
|
|
697
925
|
if (this.executor) {
|
|
698
926
|
await this.executor.exec(
|
|
@@ -796,13 +1024,10 @@ var NginxProvider = class {
|
|
|
796
1024
|
if (!this.acmeEabKid || !this.acmeEabHmacKey) return null;
|
|
797
1025
|
const dir = join(dirname(this.certDir), `.openship-eab-${randomBytes(12).toString("hex")}`);
|
|
798
1026
|
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
|
-
}
|
|
1027
|
+
if (this.executor) await this.executor.mkdir(dir);
|
|
1028
|
+
else await fsMkdir(dir, { recursive: true, mode: 448 });
|
|
805
1029
|
try {
|
|
1030
|
+
await this._chmod(dir, 448);
|
|
806
1031
|
await this._writeFile(
|
|
807
1032
|
path,
|
|
808
1033
|
`eab-kid = ${this.acmeEabKid}
|
|
@@ -854,14 +1079,20 @@ eab-hmac-key = ${this.acmeEabHmacKey}
|
|
|
854
1079
|
const hostRedirect = renderHostRedirect(route);
|
|
855
1080
|
const locationBody = hostRedirect ? hostRedirect : "staticRoot" in route && route.staticRoot ? `root ${route.staticRoot};
|
|
856
1081
|
index index.html;
|
|
857
|
-
|
|
1082
|
+
${renderStaticTryFiles(route)}` : `proxy_pass ${route.targetUrl};
|
|
858
1083
|
${PROXY_HEADERS}`;
|
|
859
|
-
const
|
|
860
|
-
const
|
|
1084
|
+
const urlShape = hostRedirect ? "" : renderUrlShape(route);
|
|
1085
|
+
const redirectRules = hostRedirect ? "" : renderRedirectRules(route, " ");
|
|
1086
|
+
const extraLocations = hostRedirect ? "" : renderProxyLocations(route, { httpsUpgrade: false });
|
|
1087
|
+
const upgradingExtraLocations = hostRedirect ? "" : renderProxyLocations(route, { httpsUpgrade: true });
|
|
1088
|
+
const serverHeaders = hostRedirect ? "" : renderServerHeaders(route, slug);
|
|
1089
|
+
const prelude = hostRedirect ? "" : renderHeaderMaps(route, slug);
|
|
861
1090
|
const proxyOpts = renderProxyOptions(route.proxy, this.nginxVersion);
|
|
862
1091
|
const proxyTlsOpts = renderProxyTlsOptions(route.proxy, this.nginxVersion);
|
|
1092
|
+
const preamble = isCloudFrontedHost(route.domain) ? `${cloudEdgeRealIpConf()}
|
|
1093
|
+
${FORWARD_VARS}` : FORWARD_VARS;
|
|
863
1094
|
const webhookLocation = route.webhookProxy && !hostRedirect ? `
|
|
864
|
-
location
|
|
1095
|
+
location ^~ ${WEBHOOK_LOCATION_PREFIX} {
|
|
865
1096
|
proxy_pass ${route.webhookProxy};
|
|
866
1097
|
proxy_set_header Host $host;
|
|
867
1098
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -872,24 +1103,25 @@ eab-hmac-key = ${this.acmeEabHmacKey}
|
|
|
872
1103
|
const edgeLua = luaSourceAvailable();
|
|
873
1104
|
const luaProxy = edgeLua ? ` log_by_lua_file ${LUA_LOGGER_PATH};
|
|
874
1105
|
access_by_lua_file ${RULES_GUARD_PATH};` : "";
|
|
1106
|
+
const slashFallback = !hostRedirect && "staticRoot" in route && route.staticRoot && route.trailingSlash === true ? `
|
|
1107
|
+
|
|
1108
|
+
${renderSlashFallback(route.staticRoot)}` : "";
|
|
875
1109
|
const serveLocation = ` location / {
|
|
876
|
-
${locationBody}
|
|
877
|
-
}`;
|
|
1110
|
+
${redirectRules}${urlShape}${locationBody}
|
|
1111
|
+
}${slashFallback}`;
|
|
878
1112
|
const redirectOrServeLocation = hostRedirect ? serveLocation : ` location / {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
${locationBody}
|
|
884
|
-
}`;
|
|
1113
|
+
${HTTPS_UPGRADE}${redirectRules}${urlShape}${locationBody}
|
|
1114
|
+
}${slashFallback}`;
|
|
885
1115
|
const sharedBody = `${serverHeaders}${proxyOpts}
|
|
886
1116
|
${webhookLocation}${extraLocations}`;
|
|
887
|
-
const
|
|
1117
|
+
const httpsUpgradeBody = `${serverHeaders}${proxyOpts}
|
|
1118
|
+
${webhookLocation}${upgradingExtraLocations}`;
|
|
1119
|
+
const httpOnlyBlock = `${GENERATED_HEADER}
|
|
888
1120
|
server {
|
|
889
1121
|
listen 80;
|
|
890
1122
|
server_name ${route.domain};
|
|
891
1123
|
|
|
892
|
-
${
|
|
1124
|
+
${preamble}
|
|
893
1125
|
|
|
894
1126
|
${luaProxy}
|
|
895
1127
|
|
|
@@ -905,19 +1137,19 @@ ${serveLocation}
|
|
|
905
1137
|
if (route.tls && await this.certsExist(route.domain)) {
|
|
906
1138
|
const certPath = join(this.certDir, route.domain, "fullchain.pem");
|
|
907
1139
|
const keyPath = join(this.certDir, route.domain, "privkey.pem");
|
|
908
|
-
serverBlock =
|
|
1140
|
+
serverBlock = `${GENERATED_HEADER}
|
|
909
1141
|
server {
|
|
910
1142
|
listen 80;
|
|
911
1143
|
server_name ${route.domain};
|
|
912
1144
|
|
|
913
|
-
${
|
|
1145
|
+
${preamble}
|
|
914
1146
|
|
|
915
1147
|
${luaProxy}
|
|
916
1148
|
|
|
917
1149
|
${ACME_CHALLENGE_LOCATION}
|
|
918
1150
|
|
|
919
1151
|
${EDGE_CHALLENGE_LOCATION}
|
|
920
|
-
${
|
|
1152
|
+
${httpsUpgradeBody}
|
|
921
1153
|
${redirectOrServeLocation}
|
|
922
1154
|
}
|
|
923
1155
|
|
|
@@ -925,7 +1157,7 @@ server {
|
|
|
925
1157
|
listen 443 ssl;
|
|
926
1158
|
server_name ${route.domain};
|
|
927
1159
|
|
|
928
|
-
${
|
|
1160
|
+
${preamble}
|
|
929
1161
|
|
|
930
1162
|
${luaProxy}
|
|
931
1163
|
|
|
@@ -938,7 +1170,7 @@ ${serveLocation}
|
|
|
938
1170
|
} else if (route.tls && route.terminatesTlsLocally) {
|
|
939
1171
|
const bootstrap = await this.ensureBootstrapCert(route.domain);
|
|
940
1172
|
bootstrapServed = !!bootstrap;
|
|
941
|
-
serverBlock = bootstrap ?
|
|
1173
|
+
serverBlock = bootstrap ? `${GENERATED_HEADER}
|
|
942
1174
|
# openship-bootstrap-tls: ${route.domain} is serving a TEMPORARY self-signed
|
|
943
1175
|
# certificate because its real certificate has not been issued yet. Openship
|
|
944
1176
|
# replaces this file automatically once it lands. Do not adopt this material.
|
|
@@ -946,7 +1178,7 @@ server {
|
|
|
946
1178
|
listen 80;
|
|
947
1179
|
server_name ${route.domain};
|
|
948
1180
|
|
|
949
|
-
${
|
|
1181
|
+
${preamble}
|
|
950
1182
|
|
|
951
1183
|
${luaProxy}
|
|
952
1184
|
|
|
@@ -961,7 +1193,7 @@ server {
|
|
|
961
1193
|
listen 443 ssl;
|
|
962
1194
|
server_name ${route.domain};
|
|
963
1195
|
|
|
964
|
-
${
|
|
1196
|
+
${preamble}
|
|
965
1197
|
|
|
966
1198
|
${luaProxy}
|
|
967
1199
|
|
|
@@ -975,7 +1207,7 @@ ${serveLocation}
|
|
|
975
1207
|
serverBlock = httpOnlyBlock;
|
|
976
1208
|
}
|
|
977
1209
|
const snapshot = await this._captureFile(configPath);
|
|
978
|
-
await this._writeFile(configPath, serverBlock);
|
|
1210
|
+
await this._writeFile(configPath, `${prelude}${serverBlock}`);
|
|
979
1211
|
try {
|
|
980
1212
|
await this.reload();
|
|
981
1213
|
} catch (err) {
|
|
@@ -991,6 +1223,112 @@ ${serveLocation}
|
|
|
991
1223
|
routeStatePath(slug) {
|
|
992
1224
|
return join(this.sitesDir, `${slug}.route.json`);
|
|
993
1225
|
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Re-emit every vhost an older generator wrote, from its own persisted RouteConfig.
|
|
1228
|
+
*
|
|
1229
|
+
* Why this exists: a fix to the EMITTED shape otherwise never reaches a running box.
|
|
1230
|
+
* `registerRoute` is the only writer, and the only things that call it are a deploy, a
|
|
1231
|
+
* domain save, a Retry, and cert issuance — so a project nobody redeploys keeps the vhost
|
|
1232
|
+
* it was first given for as long as it lives. That is how the Cloud-fronted `X-Real-IP`
|
|
1233
|
+
* block shipped and then applied to nothing: correct code, upgraded box, unchanged file.
|
|
1234
|
+
*
|
|
1235
|
+
* Cheap and idempotent by construction: a vhost already stamped at
|
|
1236
|
+
* {@link VHOST_GENERATION} is read and skipped, so the pass writes nothing and reloads
|
|
1237
|
+
* nothing once converged. Only the FIRST run after a bump does work.
|
|
1238
|
+
*
|
|
1239
|
+
* That first run is not free, and the release that introduced the marker is its worst
|
|
1240
|
+
* case: every conf written before it is unstamped, so a box with N routes reapplies all
|
|
1241
|
+
* N. `registerRoute` writes and reloads per route by design — a conf that fails
|
|
1242
|
+
* `openresty -t` is rolled back and reloaded on its own, which is why one bad route
|
|
1243
|
+
* cannot take the other N-1 down — so that run costs N whole-config tests and N reloads,
|
|
1244
|
+
* serially, on a deploy. Batching them behind a single test would trade that isolation
|
|
1245
|
+
* for the seconds it saves once per box, which is the wrong trade. What it does mean is
|
|
1246
|
+
* that a VHOST_GENERATION bump is a fleet-wide cost, not a free redeploy: bump it for a
|
|
1247
|
+
* fix that has to reach existing boxes, not to tidy the emitted text.
|
|
1248
|
+
*
|
|
1249
|
+
* Never throws and never aborts the sweep. One route whose sidecar is unreadable, or
|
|
1250
|
+
* whose re-render fails `openresty -t` (registerRoute rolls that file back on its own),
|
|
1251
|
+
* must not stop the rest — a half-converged edge with a reported failure is strictly
|
|
1252
|
+
* better than a wedged one. A route with no sidecar is left ALONE rather than
|
|
1253
|
+
* reconstructed from its conf: the scrape recovers only the primary target, so replaying
|
|
1254
|
+
* it would drop composite locations and the webhook proxy.
|
|
1255
|
+
*/
|
|
1256
|
+
async reapplyStoredRoutes() {
|
|
1257
|
+
const repaired = [];
|
|
1258
|
+
const failed = [];
|
|
1259
|
+
let entries;
|
|
1260
|
+
try {
|
|
1261
|
+
entries = await this._lsSites();
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
return { scanned: 0, repaired, failed: [{ slug: "*", reason: safeErrorMessage(err) }] };
|
|
1264
|
+
}
|
|
1265
|
+
const slugs = entries.filter((name) => name.endsWith(".route.json")).map((name) => name.slice(0, -".route.json".length));
|
|
1266
|
+
const stale = await this._staleVhostSlugs().catch(() => null);
|
|
1267
|
+
for (const slug of slugs) {
|
|
1268
|
+
if (stale && !stale.has(slug)) continue;
|
|
1269
|
+
try {
|
|
1270
|
+
const conf = await this._readFile(join(this.sitesDir, `${slug}.conf`)).catch(() => null);
|
|
1271
|
+
if (conf === null) continue;
|
|
1272
|
+
const gen = readVhostGeneration(conf);
|
|
1273
|
+
if (gen !== null && gen >= VHOST_GENERATION) continue;
|
|
1274
|
+
const saved = JSON.parse(await this._readFile(this.routeStatePath(slug)));
|
|
1275
|
+
if (!saved?.domain) {
|
|
1276
|
+
failed.push({ slug, reason: "sidecar has no domain" });
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
await this.registerRoute(saved);
|
|
1280
|
+
repaired.push(saved.domain);
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
failed.push({ slug, reason: safeErrorMessage(err) });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return { scanned: slugs.length, repaired, failed };
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Names in the sites directory.
|
|
1289
|
+
*
|
|
1290
|
+
* Listing goes through the COMMAND channel, so on a container edge it must name the
|
|
1291
|
+
* path commands see: `sitesDir` is the HOST side of the bind mount and does not exist
|
|
1292
|
+
* inside the container. Same directory either way, and only basenames are used — the
|
|
1293
|
+
* reads that follow go back through the file channel at the host path.
|
|
1294
|
+
*/
|
|
1295
|
+
async _lsSites() {
|
|
1296
|
+
if (!this.executor) return fsReaddir(this.sitesDir);
|
|
1297
|
+
const out = await this.executor.exec(`ls -1 ${sq(this._commandSitesDir())} 2>/dev/null || true`);
|
|
1298
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Slugs whose `.conf` does not carry the CURRENT generation marker — `null` when the
|
|
1302
|
+
* question can't be answered in one command and the caller should fall back to reading.
|
|
1303
|
+
*
|
|
1304
|
+
* `grep -L` on the whole directory, so the steady state (everything current) costs one
|
|
1305
|
+
* round trip instead of one per route. The pattern is anchored on both ends and built from
|
|
1306
|
+
* the same constant the writer emits, so `gen: 1` cannot pass for `gen: 10` and the two
|
|
1307
|
+
* halves cannot drift apart.
|
|
1308
|
+
*/
|
|
1309
|
+
async _staleVhostSlugs() {
|
|
1310
|
+
if (!this.executor) return null;
|
|
1311
|
+
const dir = this._commandSitesDir();
|
|
1312
|
+
const out = await this.executor.exec(
|
|
1313
|
+
`grep -L ${sq(`^${GENERATION_MARKER}$`)} ${sq(dir)}/*.conf 2>/dev/null || true`
|
|
1314
|
+
);
|
|
1315
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const line of out.split("\n")) {
|
|
1317
|
+
const name = line.trim().split("/").pop();
|
|
1318
|
+
if (name?.endsWith(".conf")) slugs.add(name.slice(0, -".conf".length));
|
|
1319
|
+
}
|
|
1320
|
+
return slugs;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* The sites directory as COMMANDS see it.
|
|
1324
|
+
*
|
|
1325
|
+
* On a container edge `sitesDir` is the HOST side of the bind mount and does not exist
|
|
1326
|
+
* inside the container, where `exec` runs. Same directory, two names; only basenames are
|
|
1327
|
+
* used, and the reads that follow go back through the file channel at the host path.
|
|
1328
|
+
*/
|
|
1329
|
+
_commandSitesDir() {
|
|
1330
|
+
return this.containerEdge ? OPENRESTY_DEFAULT_PATHS.sitesDir : this.sitesDir;
|
|
1331
|
+
}
|
|
994
1332
|
/**
|
|
995
1333
|
* Recover the proxy tunables from a vhost we already wrote, for the legacy
|
|
996
1334
|
* no-sidecar re-register below. Rewriting the conf without them would silently
|
|
@@ -1083,7 +1421,7 @@ ${serveLocation}
|
|
|
1083
1421
|
const path2 = join(this.sitesDir, `${stem}.conf`);
|
|
1084
1422
|
const conf = await this._readFile(path2).catch(() => "");
|
|
1085
1423
|
if (!conf || !this.serverNamesIn(conf).includes(host)) continue;
|
|
1086
|
-
if (conf.includes(
|
|
1424
|
+
if (conf.includes(`${EDGE_CHALLENGE_URL_PREFIX} {`)) {
|
|
1087
1425
|
return { served: true, via: "existing-vhost" };
|
|
1088
1426
|
}
|
|
1089
1427
|
return {
|
|
@@ -1619,6 +1957,8 @@ export {
|
|
|
1619
1957
|
validateCertFor,
|
|
1620
1958
|
isSafeCertPath,
|
|
1621
1959
|
readDeclaredPair,
|
|
1960
|
+
VHOST_GENERATION,
|
|
1961
|
+
readVhostGeneration,
|
|
1622
1962
|
renderProxyOptions,
|
|
1623
1963
|
renderProxyTlsOptions,
|
|
1624
1964
|
renderGeoEntries,
|