landfall 0.4.0 → 0.4.2
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/dist/cli.mjs +152 -152
- package/dist/index.html +18 -9
- package/dist/server-BqVugweK.mjs +5 -0
- package/package.json +1 -1
- package/dist/server-BrAinE0R.mjs +0 -5
package/dist/index.html
CHANGED
|
@@ -358,9 +358,8 @@ function btn(label, fn, cls = "", title = "", f = "") {
|
|
|
358
358
|
}
|
|
359
359
|
document.addEventListener("click", (e) => {
|
|
360
360
|
const el = e.target.closest("[data-k]");
|
|
361
|
-
const menu = $("#menu");
|
|
362
361
|
// A click outside an open menu only closes it.
|
|
363
|
-
if (
|
|
362
|
+
if (menuEl && !menuEl.contains(e.target)) {
|
|
364
363
|
closeMenu(false);
|
|
365
364
|
e.preventDefault();
|
|
366
365
|
return void e.stopPropagation();
|
|
@@ -384,6 +383,7 @@ function menuBtn(items, title = "More actions", cls = "sm") {
|
|
|
384
383
|
return btn("⋯", (e, el) => openMenu(el, items), `${cls} more`, title).replace("<button", `<button aria-haspopup="menu" aria-expanded="false" aria-label="${esc(title)}"`);
|
|
385
384
|
}
|
|
386
385
|
let menuAnchor = null;
|
|
386
|
+
let menuEl = null;
|
|
387
387
|
function openMenu(anchor, items) {
|
|
388
388
|
closeMenu(false);
|
|
389
389
|
menuAnchor = anchor;
|
|
@@ -410,10 +410,13 @@ function openMenu(anchor, items) {
|
|
|
410
410
|
all[e.key === "Home" ? 0 : all.length - 1].focus();
|
|
411
411
|
}
|
|
412
412
|
};
|
|
413
|
+
// Keep focus where it is on mousedown: a browser that does not focus clicked buttons would blur the menu and close it before the click.
|
|
414
|
+
m.onmousedown = (e) => e.preventDefault();
|
|
413
415
|
m.addEventListener("focusout", (e) => {
|
|
414
416
|
if (!m.contains(e.relatedTarget)) closeMenu(false);
|
|
415
417
|
});
|
|
416
418
|
document.body.append(m);
|
|
419
|
+
menuEl = m;
|
|
417
420
|
const r = anchor.getBoundingClientRect();
|
|
418
421
|
m.style.left = `${Math.max(8, Math.min(r.right - m.offsetWidth, innerWidth - m.offsetWidth - 8))}px`;
|
|
419
422
|
m.style.top = `${r.bottom + 4 + m.offsetHeight > innerHeight - 8 ? Math.max(8, r.top - m.offsetHeight - 4) : r.bottom + 4}px`;
|
|
@@ -421,8 +424,10 @@ function openMenu(anchor, items) {
|
|
|
421
424
|
}
|
|
422
425
|
/** restore: focus goes back to the "⋯" button (Esc, or an item that opens no dialog). */
|
|
423
426
|
function closeMenu(restore = true) {
|
|
424
|
-
const m =
|
|
427
|
+
const m = menuEl;
|
|
425
428
|
if (!m) return;
|
|
429
|
+
// Removing the focused item fires focusout, which calls closeMenu again: clear menuEl first so that call is a no-op.
|
|
430
|
+
menuEl = null;
|
|
426
431
|
m.remove();
|
|
427
432
|
menuAnchor?.setAttribute("aria-expanded", "false");
|
|
428
433
|
if (restore && menuAnchor?.isConnected) menuAnchor.focus({ preventScroll: true });
|
|
@@ -1358,15 +1363,16 @@ function sitesView(n, o) {
|
|
|
1358
1363
|
const older = [...m.releases].sort((a, b) => (a.at < b.at ? 1 : -1)).filter((r) => r.at < (cur?.at ?? ""));
|
|
1359
1364
|
const proj = (S.state.projects ?? []).find((p) => p.server === n && p.name === name);
|
|
1360
1365
|
const deleteSite = () =>
|
|
1361
|
-
|
|
1366
|
+
dialog({
|
|
1362
1367
|
title: m.adopted ? `Stop and forget ${name}?` : `Delete ${name} from ${n}?`,
|
|
1363
1368
|
text: m.adopted
|
|
1364
1369
|
? `Runs docker compose down in ${m.adopted}; its files and volumes stay.`
|
|
1365
|
-
: "Stops it and removes its files and releases. Its named volumes (databases, uploads) stay.\nThis cannot be undone.",
|
|
1370
|
+
: "Stops it and removes its files and releases. Its named volumes (databases, uploads) stay unless you delete them too.\nThis cannot be undone.",
|
|
1371
|
+
fields: m.adopted ? [] : [{ name: "volumes", type: "check", label: "Also delete its volumes", hint: "Databases, uploads: all its data. lf sites rm --volumes" }],
|
|
1366
1372
|
ok: m.adopted ? `Forget ${name}` : `Delete ${name}`,
|
|
1367
1373
|
danger: true,
|
|
1368
1374
|
typeToConfirm: m.adopted ? "" : name,
|
|
1369
|
-
onOk: () => run(["sites", "rm", name, ...sv(n), "-y"]),
|
|
1375
|
+
onOk: (v) => run(["sites", "rm", name, ...sv(n), "-y", ...(v.volumes ? ["--volumes"] : [])]),
|
|
1370
1376
|
});
|
|
1371
1377
|
const svcMenu = (x) =>
|
|
1372
1378
|
menuBtn([
|
|
@@ -1685,6 +1691,9 @@ function settingsView(n) {
|
|
|
1685
1691
|
if (d) run(["traefik", "setup", ...sv(n), "--dashboard", d], reload);
|
|
1686
1692
|
}, "Apply");
|
|
1687
1693
|
const sites = S.overview[n]?.data?.sites ?? [];
|
|
1694
|
+
// lf traefik setup refuses to touch a Traefik the server already had.
|
|
1695
|
+
const o = S.overview[n]?.data;
|
|
1696
|
+
const foreign = o?.adopted ? o.proxy : "";
|
|
1688
1697
|
const external = (d) => S.overview[n]?.data?.others?.find((e) => e.domains.includes(d))?.name;
|
|
1689
1698
|
const servedBy = (d) => sites.find((s) => s.domains.includes(d))?.meta.name;
|
|
1690
1699
|
const serve = (d) => {
|
|
@@ -1721,8 +1730,8 @@ function settingsView(n) {
|
|
|
1721
1730
|
${x.suggest.length ? `<div class="note">${x.domains.length ? "Other domains" : "Domains"} your sites on this server already use: ${x.suggest.map((g) => `<b>${esc(g.domain)}</b> <span class="faint">(${g.hosts})</span> ${btn("Add", () => run(["server", "domain", n, "--add", g.domain], reload), "sm")}`).join(" ")}</div>` : ""}</div>
|
|
1722
1731
|
</div>
|
|
1723
1732
|
<div class="card">
|
|
1724
|
-
<h2>Traefik <span class="actions">${btn("Dashboard…", dashboard, "sm")}</span></h2>
|
|
1725
|
-
<div class="body">${rows([
|
|
1733
|
+
<h2>Traefik <span class="actions">${foreign ? "" : btn("Dashboard…", dashboard, "sm")}</span></h2>
|
|
1734
|
+
<div class="body">${foreign ? `<div class="note">Sites go through <b class="mono">${esc(foreign)}</b>, a Traefik landfall did not install and never changes: its dashboard is set up in its own config.</div>` : ""}${rows([
|
|
1726
1735
|
["image", esc(x.traefik.summary)],
|
|
1727
1736
|
["e-mail", esc(x.traefik.email ?? "none")],
|
|
1728
1737
|
["dashboard", x.traefik.dashboard ? `<a href="https://${esc(x.traefik.dashboard)}" target="_blank" rel="noopener">${esc(x.traefik.dashboard)}</a>` : "off"],
|
|
@@ -1826,7 +1835,7 @@ function render() {
|
|
|
1826
1835
|
}
|
|
1827
1836
|
|
|
1828
1837
|
document.addEventListener("keydown", (e) => {
|
|
1829
|
-
if (e.key === "Escape" &&
|
|
1838
|
+
if (e.key === "Escape" && menuEl) return closeMenu();
|
|
1830
1839
|
if (e.key === "Escape" && !$("#dlg").open && S.job && !e.target.closest?.("input, textarea, select")) {
|
|
1831
1840
|
S.job = null;
|
|
1832
1841
|
render();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{A as e,B as t,C as n,D as r,E as i,F as a,I as o,L as s,M as c,N as l,O as u,P as d,R as ee,S as f,T as te,_ as p,a as m,b as h,c as g,d as ne,f as _,g as v,h as y,i as b,j as x,k as S,l as re,m as ie,n as ae,o as oe,p as se,r as ce,s as le,t as C,u as ue,v as de,w as fe,x as w,y as pe,z as T}from"./cli.mjs";import{spawn as me,spawnSync as E}from"node:child_process";import D from"node:fs";import{homedir as O}from"node:os";import{dirname as k,isAbsolute as A,join as j,resolve as M}from"node:path";import{randomBytes as N,timingSafeEqual as P}from"node:crypto";import{createServer as F}from"node:http";import{Readable as I}from"node:stream";const L=/^(github\.com|gitlab\.com|bitbucket\.org|codeberg\.org)$/;function R(){let e=a(),t=Object.keys(e.servers).map(t=>({name:t,registered:!0,isDefault:e.default===t})),n=f().filter(t=>!e.servers[t]&&!L.test(t)).map(e=>({name:e,registered:!1,isDefault:!1}));return[...t,...n]}function z(e){try{let t=w(s(e).server);return`${t.user}@${t.hostname}${t.port===22?``:`:${t.port}`}`}catch{return`cannot resolve host`}}function he(e){let[t,n]=e;return e.includes(`--check`)||e.includes(`--key-only`)?!1:t===`firewall`?[`enable`,`disable`,`allow`,`deny`,`update`,`rm`,`remove`].includes(n):t===`traefik`?n===`setup`||n===`use`:t===`env`?e.some(e=>e.includes(`=`)||e===`-u`||e===`--unset`):t===`domains`?e.some(e=>[`-a`,`--add`,`--rm`,`--set`].includes(e)):t===`auth`?e.some(e=>[`-u`,`--user`,`--rm-user`,`--off`].includes(e)):t===`update`||t===`upgrade`||[`setup`,`deploy`,`rollback`,`stop`,`start`,`restart`,`adopt`].includes(t)}function B(e){let t=M(!e||e===`~`?O():d(e)),n=D.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&V(j(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:k(t)===t?null:k(t),home:O(),dirs:n,detected:y(t)}}const V=e=>{try{return D.statSync(e).isDirectory()}catch{return!1}},H=j(l,`uploads`),U=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function W(e,t){D.mkdirSync(k(e),{recursive:!0}),D.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function ge(e){D.mkdirSync(H,{recursive:!0,mode:448});for(let e of D.readdirSync(H)){let t=j(H,e);Date.now()-D.statSync(t).mtimeMs>864e5&&D.rmSync(t,{recursive:!0,force:!0})}let t=D.mkdtempSync(j(H,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=j(t,`.upload-`+U(n[0].name).pop());await W(e,n[0]);let r=j(t,`files`);D.mkdirSync(r);let i=/\.zip$/i.test(e),a=i?E(`unzip`,[`-q`,e,`-d`,r]):E(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?E(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return D.rmSync(e),{path:G(r),files:_e(r)}}let i=j(t,`files`);for(let[e,t]of n.entries()){let n=U(r[e]||t.name);n.length&&await W(j(i,...n),t)}return{path:G(i),files:n.length}}function G(e){for(;;){let t=D.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!V(j(e,t[0])))return e;e=j(e,t[0])}}function _e(e){return D.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function ve(e){if(e!=null&&e!==``){if(typeof e!=`string`||!A(e)||!V(e))throw Error(`cwd must be an existing folder`);return e}}const K=()=>[process.execPath,...process.execArgv,process.argv[1]],q=5e3;function ye(e,t,n={}){let r={...process.env,NO_COLOR:`1`,...n.env};n.password&&Object.assign(r,g(n.password));let i=me(K()[0],[...K().slice(1),...e],{stdio:[`ignore`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0}),a={argv:e,lines:[],dropped:0,stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
2
|
+
`);e=n.pop();for(let e of n)a.lines.push(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``));a.lines.length>q&&(a.dropped+=a.lines.splice(0,a.lines.length-q).length),s()},flush:()=>e&&a.lines.push(e)}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const be=D.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),J=/* @__PURE__ */ new Map,xe=/* @__PURE__ */ new Set([`ui`,`web`]);function Se(){let e=j(k(x),`ui-token`);try{let t=D.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=N(24).toString(`hex`);return D.mkdirSync(k(e),{recursive:!0,mode:448}),D.writeFileSync(e,t+`
|
|
3
|
+
`,{mode:384}),t}const Y=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),X=(e,t=500)=>Y({error:e?.message??String(e)},t);function Z(e,t){let r=n(),i=/* @__PURE__ */ new Set,a=ye(e,()=>i.forEach(e=>e()),{password:t.password,cwd:t.cwd,env:{LANDFALL_RUN:r}}),o={id:r,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:he(e),job:a,listeners:i};J.set(r,o);for(let[e,t]of J){if(J.size<=50)break;t.job.code!==void 0&&J.delete(e)}return o}const Ce=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),Q=e=>({id:e.id,argv:Ce(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped});function we(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n)})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Q(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
4
|
+
|
|
5
|
+
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function Te(n,l){let[d,f,g]=l,y=n.method;if(d===`state`&&y===`GET`){let t=a();return Y({version:C,cwd:process.cwd(),project:o(process.cwd())??null,projects:v(),defaultImage:te,paths:{root:u,sites:S,traefik:e,journal:i,proxy:r},servers:R().map(e=>({...e,target:z(e.name),saved:t.servers[e.name]??null})),jobs:[...J.values()].reverse().map(Q)})}if(d===`servers`&&f){let{server:e}=s(f);if(y===`GET`&&!g)return Y(await ie(e));if(y===`GET`&&g===`firewall`){let{login:t,probe:n,...r}=await m(e);return Y({...r,asRoot:w(t).user===`root`})}if(y===`GET`&&g===`config`){let t=ce(f),[n,r]=await Promise.all([Promise.all(t.domains.map(t=>ae(e,t))),b(e)]);return Y({...t,dns:n,suggest:r})}if(y===`GET`&&g===`history`){let t=(await le(e)).reverse();return Y(t.map(e=>({...e,pending:oe(e).length})))}if(y===`GET`&&g===`facts`){let t=await de(e),n=await h(e,t);return Y({...t,ips:n,sslipIp:pe(n)??null})}if(y===`GET`&&g===`updates`){let t=new URL(n.url).searchParams.get(`refresh`)===`1`,r=await se(e,t),i=await ne(e);return Y({packages:r,images:await _(e),engine:{...i,...ue(i)}})}if(y===`GET`&&g===`dns`){let t=(new URL(n.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>c.test(e)).slice(0,50),r=await h(e),[i,a]=await Promise.all([p(t,r),Promise.all(t.map(re))]);return Y({ips:r,domains:i.map((e,t)=>({...e,https:a[t]}))})}if(y===`POST`&&g==="default")return ee(f),Y({ok:!0})}if(d===`jobs`){if(y===`POST`&&!f){let e=await n.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||xe.has(t[0]))return X(`argv must be a list of landfall arguments`,400);let r;try{r=ve(e.cwd)}catch(e){return X(e,400)}return Y(Q(Z(t,{server:e.server,password:e.password||void 0,cwd:r})))}let e=f?J.get(f):void 0;if(f&&!e)return X(`no job ${f}`,404);if(e&&y===`GET`&&g===`events`)return we(e,Number(new URL(n.url).searchParams.get(`from`))||0,n.signal);if(e&&y===`GET`&&!g)return Y({...Q(e),lines:e.job.lines,from:e.job.dropped});if(e&&y===`POST`&&g===`stop`)return e.job.stop(),Y(Q(e))}if(d===`fs`&&y===`GET`)return Y(B(new URL(n.url).searchParams.get(`path`)??void 0));if(d===`projects`&&y===`GET`)return Y(v());if(d===`upload`&&y===`POST`){let e=await ge(await n.formData());return Y({...e,detected:B(e.path).detected})}if(d===`open`&&y===`POST`){let{url:e}=await n.json();return!e||!/^https?:\/\//.test(e)?X(`not a web address`,400):(t(e),Y({ok:!0}))}return X(`not found`,404)}const $=(e,t)=>e.length===t.length&&P(Buffer.from(e),Buffer.from(t)),Ee=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``;async function De(e){fe(!0);let n=Se(),r=F(Oe(async e=>{let t=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(t.pathname===`/login`)return $(t.searchParams.get(`t`)??``,n)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${n}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!$(Ee(e),n))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(t.pathname===`/`)return new Response(be,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(t.pathname.startsWith(`/api/`))try{return await Te(e,t.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return X(e)}return new Response(`not found`,{status:404})}));r.requestTimeout=0;let i=await new Promise(t=>{r.once(`error`,t),r.listen(e.port,`127.0.0.1`,()=>t(void 0))});if(i){if(i.code!==`EADDRINUSE`)throw i;let r=`http://127.0.0.1:${e.port}/login?t=${n}`;return await fetch(`http://127.0.0.1:${e.port}/api/state`,{headers:{cookie:`landfall=${n}`}}).then(e=>e.ok,()=>!1)?(console.log(`lf ui already runs: ${T.cyan(r)}`),e.open&&t(r),0):(console.error(`${T.red(`error:`)} port ${e.port} is in use; pick another with --port`),1)}let a=`http://127.0.0.1:${r.address().port}/login?t=${n}`;return console.log(`${T.bold(`lf ui`)} ${T.dim(C)} on ${T.cyan(a)}`),console.log(T.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&t(a),new Promise(e=>{let t=()=>{for(let e of J.values())e.job.stop();r.closeAllConnections(),r.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function Oe(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let n=new Headers;for(let[e,r]of Object.entries(t.headers))for(let t of[r??[]].flat())n.append(e,t);let a=t.method===`GET`||t.method===`HEAD`?void 0:I.toWeb(t);i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:n,body:a,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=I.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{De as webUi};
|
package/package.json
CHANGED
package/dist/server-BrAinE0R.mjs
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{A as e,C as t,D as n,E as r,F as i,I as a,L as o,M as s,N as c,O as l,P as u,R as d,S as f,T as ee,_ as p,a as te,b as m,c as ne,d as h,f as g,g as _,h as v,i as y,j as b,k as re,l as ie,m as ae,n as oe,o as se,p as ce,r as le,s as ue,t as x,u as de,v as fe,w as pe,x as S,y as me,z as C}from"./cli.mjs";import{spawn as w,spawnSync as T}from"node:child_process";import E from"node:fs";import{homedir as D}from"node:os";import{dirname as O,isAbsolute as k,join as A,resolve as j}from"node:path";import{randomBytes as M,timingSafeEqual as N}from"node:crypto";import{createServer as P}from"node:http";import{Readable as F}from"node:stream";const I=/^(github\.com|gitlab\.com|bitbucket\.org|codeberg\.org)$/;function L(){let e=u(),t=Object.keys(e.servers).map(t=>({name:t,registered:!0,isDefault:e.default===t})),n=f().filter(t=>!e.servers[t]&&!I.test(t)).map(e=>({name:e,registered:!1,isDefault:!1}));return[...t,...n]}function R(e){try{let t=S(a(e).server);return`${t.user}@${t.hostname}${t.port===22?``:`:${t.port}`}`}catch{return`cannot resolve host`}}function z(e){let[t,n]=e;return e.includes(`--check`)||e.includes(`--key-only`)?!1:t===`firewall`?[`enable`,`disable`,`allow`,`deny`,`update`,`rm`,`remove`].includes(n):t===`traefik`?n===`setup`||n===`use`:t===`env`?e.some(e=>e.includes(`=`)||e===`-u`||e===`--unset`):t===`domains`?e.some(e=>[`-a`,`--add`,`--rm`,`--set`].includes(e)):t===`auth`?e.some(e=>[`-u`,`--user`,`--rm-user`,`--off`].includes(e)):t===`update`||t===`upgrade`||[`setup`,`deploy`,`rollback`,`stop`,`start`,`restart`,`adopt`].includes(t)}function B(e){let t=j(!e||e===`~`?D():c(e)),n=E.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&V(A(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:O(t)===t?null:O(t),home:D(),dirs:n,detected:v(t)}}const V=e=>{try{return E.statSync(e).isDirectory()}catch{return!1}},H=A(s,`uploads`),U=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function W(e,t){E.mkdirSync(O(e),{recursive:!0}),E.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function he(e){E.mkdirSync(H,{recursive:!0,mode:448});for(let e of E.readdirSync(H)){let t=A(H,e);Date.now()-E.statSync(t).mtimeMs>864e5&&E.rmSync(t,{recursive:!0,force:!0})}let t=E.mkdtempSync(A(H,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=A(t,`.upload-`+U(n[0].name).pop());await W(e,n[0]);let r=A(t,`files`);E.mkdirSync(r);let i=/\.zip$/i.test(e),a=i?T(`unzip`,[`-q`,e,`-d`,r]):T(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?T(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return E.rmSync(e),{path:G(r),files:ge(r)}}let i=A(t,`files`);for(let[e,t]of n.entries()){let n=U(r[e]||t.name);n.length&&await W(A(i,...n),t)}return{path:G(i),files:n.length}}function G(e){for(;;){let t=E.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!V(A(e,t[0])))return e;e=A(e,t[0])}}function ge(e){return E.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function _e(e){if(e!=null&&e!==``){if(typeof e!=`string`||!k(e)||!V(e))throw Error(`cwd must be an existing folder`);return e}}const K=()=>[process.execPath,...process.execArgv,process.argv[1]],q=5e3;function ve(e,t,n={}){let r={...process.env,NO_COLOR:`1`,...n.env};n.password&&Object.assign(r,ne(n.password));let i=w(K()[0],[...K().slice(1),...e],{stdio:[`ignore`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0}),a={argv:e,lines:[],dropped:0,stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
2
|
-
`);e=n.pop();for(let e of n)a.lines.push(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``));a.lines.length>q&&(a.dropped+=a.lines.splice(0,a.lines.length-q).length),s()},flush:()=>e&&a.lines.push(e)}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const ye=E.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),J=/* @__PURE__ */ new Map,be=/* @__PURE__ */ new Set([`ui`,`web`]);function xe(){let t=A(O(e),`ui-token`);try{let e=E.readFileSync(t,`utf8`).trim();if(e.length>=32)return e}catch{}let n=M(24).toString(`hex`);return E.mkdirSync(O(t),{recursive:!0,mode:448}),E.writeFileSync(t,n+`
|
|
3
|
-
`,{mode:384}),n}const Y=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),X=(e,t=500)=>Y({error:e?.message??String(e)},t);function Se(e,t){let n=`${Date.now().toString(36)}${Math.random().toString(36).slice(2,6)}`,r=/* @__PURE__ */ new Set,i=ve(e,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,env:{LANDFALL_RUN:n}}),a={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:z(e),job:i,listeners:r};J.set(n,a);for(let[e,t]of J){if(J.size<=50)break;t.job.code!==void 0&&J.delete(e)}return a}const Z=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),Q=e=>({id:e.id,argv:Z(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped});function Ce(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n)})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Q(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
4
|
-
|
|
5
|
-
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function we(e,t){let[s,c,d]=t,f=e.method;if(s===`state`&&f===`GET`){let e=u();return Y({version:x,cwd:process.cwd(),project:i(process.cwd())??null,projects:_(),defaultImage:pe,paths:{root:n,sites:l,traefik:re,journal:ee,proxy:r},servers:L().map(t=>({...t,target:R(t.name),saved:e.servers[t.name]??null})),jobs:[...J.values()].reverse().map(Q)})}if(s===`servers`&&c){let{server:t}=a(c);if(f===`GET`&&!d)return Y(await ae(t));if(f===`GET`&&d===`firewall`){let{login:e,probe:n,...r}=await te(t);return Y({...r,asRoot:S(e).user===`root`})}if(f===`GET`&&d===`config`){let e=le(c),[n,r]=await Promise.all([Promise.all(e.domains.map(e=>oe(t,e))),y(t)]);return Y({...e,dns:n,suggest:r})}if(f===`GET`&&d===`history`){let e=(await ue(t)).reverse();return Y(e.map(e=>({...e,pending:se(e).length})))}if(f===`GET`&&d===`facts`){let e=await fe(t),n=await m(t,e);return Y({...e,ips:n,sslipIp:me(n)??null})}if(f===`GET`&&d===`updates`){let n=new URL(e.url).searchParams.get(`refresh`)===`1`,r=await ce(t,n),i=await h(t);return Y({packages:r,images:await g(t),engine:{...i,...de(i)}})}if(f===`GET`&&d===`dns`){let n=(new URL(e.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>b.test(e)).slice(0,50),r=await m(t),[i,a]=await Promise.all([p(n,r),Promise.all(n.map(ie))]);return Y({ips:r,domains:i.map((e,t)=>({...e,https:a[t]}))})}if(f===`POST`&&d==="default"){let e=u();return e.servers[c]??={host:c},e.default=c,o(e),Y({ok:!0})}}if(s===`jobs`){if(f===`POST`&&!c){let t=await e.json(),n=t.argv;if(!Array.isArray(n)||!n.length||!n.every(e=>typeof e==`string`)||be.has(n[0]))return X(`argv must be a list of landfall arguments`,400);let r;try{r=_e(t.cwd)}catch(e){return X(e,400)}return Y(Q(Se(n,{server:t.server,password:t.password||void 0,cwd:r})))}let t=c?J.get(c):void 0;if(c&&!t)return X(`no job ${c}`,404);if(t&&f===`GET`&&d===`events`)return Ce(t,Number(new URL(e.url).searchParams.get(`from`))||0,e.signal);if(t&&f===`GET`&&!d)return Y({...Q(t),lines:t.job.lines,from:t.job.dropped});if(t&&f===`POST`&&d===`stop`)return t.job.stop(),Y(Q(t))}if(s===`fs`&&f===`GET`)return Y(B(new URL(e.url).searchParams.get(`path`)??void 0));if(s===`projects`&&f===`GET`)return Y(_());if(s===`upload`&&f===`POST`){let t=await he(await e.formData());return Y({...t,detected:B(t.path).detected})}if(s===`open`&&f===`POST`){let{url:t}=await e.json();return!t||!/^https?:\/\//.test(t)?X(`not a web address`,400):(C(t),Y({ok:!0}))}return X(`not found`,404)}const $=(e,t)=>e.length===t.length&&N(Buffer.from(e),Buffer.from(t)),Te=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``;async function Ee(e){t(!0);let n=xe(),r=P(De(async e=>{let t=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(t.pathname===`/login`)return $(t.searchParams.get(`t`)??``,n)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${n}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!$(Te(e),n))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(t.pathname===`/`)return new Response(ye,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(t.pathname.startsWith(`/api/`))try{return await we(e,t.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return X(e)}return new Response(`not found`,{status:404})}));r.requestTimeout=0;let i=await new Promise(t=>{r.once(`error`,t),r.listen(e.port,`127.0.0.1`,()=>t(void 0))});if(i){if(i.code!==`EADDRINUSE`)throw i;let t=`http://127.0.0.1:${e.port}/login?t=${n}`;return await fetch(`http://127.0.0.1:${e.port}/api/state`,{headers:{cookie:`landfall=${n}`}}).then(e=>e.ok,()=>!1)?(console.log(`lf ui already runs: ${d.cyan(t)}`),e.open&&C(t),0):(console.error(`${d.red(`error:`)} port ${e.port} is in use; pick another with --port`),1)}let a=`http://127.0.0.1:${r.address().port}/login?t=${n}`;return console.log(`${d.bold(`lf ui`)} ${d.dim(x)} on ${d.cyan(a)}`),console.log(d.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&C(a),new Promise(e=>{let t=()=>{for(let e of J.values())e.job.stop();r.closeAllConnections(),r.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function De(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let n=new Headers;for(let[e,r]of Object.entries(t.headers))for(let t of[r??[]].flat())n.append(e,t);let a=t.method===`GET`||t.method===`HEAD`?void 0:F.toWeb(t);i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:n,body:a,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=F.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{Ee as webUi};
|