landfall 0.5.5 → 0.6.0

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.
@@ -0,0 +1,8 @@
1
+ import{A as e,B as t,C as n,D as r,E as i,F as a,H as o,I as s,L as c,M as l,N as u,O as ee,P as d,R as te,S as f,T as p,V as m,_ as h,a as g,b as _,c as v,d as ne,f as re,g as ie,h as ae,i as oe,j as se,k as ce,l as le,m as ue,n as de,o as fe,p as pe,r as me,s as he,t as y,u as ge,v as b,w as x,x as S,y as C,z as w}from"./cli.mjs";import{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{createHash as M,randomBytes as N,timingSafeEqual as P}from"node:crypto";import{createServer as F}from"node:http";import{Readable as I,Transform as L}from"node:stream";function R(e){let t=j(!e||e===`~`?D():s(e)),n=E.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&z(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:f(t)}}const z=e=>{try{return E.statSync(e).isDirectory()}catch{return!1}},B=A(a,`uploads`),V=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function H(e,t){E.mkdirSync(O(e),{recursive:!0}),E.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function _e(e){E.mkdirSync(B,{recursive:!0,mode:448});for(let e of E.readdirSync(B)){let t=A(B,e);Date.now()-E.statSync(t).mtimeMs>864e5&&E.rmSync(t,{recursive:!0,force:!0})}let t=E.mkdtempSync(A(B,`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-`+V(n[0].name).pop());await H(e,n[0]);let r=A(t,`files`);E.mkdirSync(r);let i=/\.zip$/i.test(e);if(ve(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let 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),ye(r),{path:U(r),files:be(r)}}let i=A(t,`files`);for(let[e,t]of n.entries()){let n=V(r[e]||t.name);n.length&&await H(A(i,...n),t)}return{path:U(i),files:n.length}}function ve(e,t){let n=(e,t)=>{let n=T(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
2
+ `).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function ye(e){for(let t of E.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&E.rmSync(A(t.parentPath,t.name),{force:!0})}function U(e){for(;;){let t=E.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!z(A(e,t[0])))return e;e=A(e,t[0])}}function be(e){return E.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function xe(e){if(e!=null&&e!==``){if(typeof e!=`string`||!k(e)||!z(e))throw Error(`cwd must be an existing folder`);return e}}const Se=E.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),Ce=512*1024**2,we=1024**2,W=/* @__PURE__ */ new Map,Te=/* @__PURE__ */ new Set([`ui`,`web`]);function Ee(){let e=A(O(l),`ui-token`);try{let t=E.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=N(24).toString(`hex`);return E.mkdirSync(O(e),{recursive:!0,mode:448}),E.writeFileSync(e,t+`
3
+ `,{mode:384}),t}const G=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),K=(e,t=500)=>G({error:e?.message??String(e)},t);function q(e,t){let n=x(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=De(e),s=de(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:h(e),job:s,listeners:r,opts:t};W.set(n,c);for(let[e,t]of W){if(W.size<=50)break;t.job.code!==void 0&&W.delete(e)}return c}const J=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,Y=/^[A-Za-z_][A-Za-z0-9_]*=/;function De(e){let n=[],r={},i=[];for(let a=0;a<e.length;a++){let o=e[a],s=o.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);s&&(s[2]!==void 0||a+1<e.length)?r[t(s[1])]=s[2]??e[++a]:J(e)&&Y.test(o)&&!o.includes(`
4
+ `)&&!e.includes(`--env-file`)?i.push(o):n.push(o)}return i.length&&n.push(`--env-file`,`-`),{args:n,env:r,stdin:i.length?i.join(`
5
+ `)+`
6
+ `:void 0}}const Oe=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:J(e)&&Y.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),X=e=>({id:e.id,argv:Oe(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function ke(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),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(X(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
7
+
8
+ `),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 Z(t,a){let[o,s,l]=a,f=t.method;if(o===`state`&&f===`GET`){let t=c();return G({version:y,cwd:process.cwd(),project:te(process.cwd())??null,projects:n(),defaultImage:i,rules:{loginName:d.source,minPassword:8},paths:{root:ce,sites:e,traefik:se,journal:r,proxy:ee},defaults:me(),servers:b().map(e=>({...e,target:_(e.name),saved:t.servers[e.name]??null})),jobs:[...W.values()].reverse().map(X)})}if(o===`reach`&&f===`GET`)return G(await C(b().map(e=>e.name),new URL(t.url).searchParams.get(`fresh`)===`1`));if(o===`servers`&&s){let{server:e}=w(s);if(f===`GET`&&!l)return G(await S(e));if(f===`GET`&&l===`firewall`)return G(await le(e));if(f===`GET`&&l===`config`)return G(await g(s,e));if(f===`GET`&&l===`history`)return G(await ge(e));if(f===`GET`&&l===`facts`)return G(await v(s,e));if(f===`GET`&&l===`stats`)return G(await re(e));if(f===`GET`&&l===`disk`)return G(await fe(e));if(f===`GET`&&l===`prune`)return G(await ne(e));if(f===`GET`&&l===`system`)return G(await pe(e));if(f===`GET`&&l===`updates`)return G(await ae(e,new URL(t.url).searchParams.get(`refresh`)===`1`));if(f===`GET`&&l===`traffic`)return G(await ue(e,ie(new URL(t.url).searchParams.get(`since`)??`24h`)));if(f===`GET`&&l===`certs`)return G(await oe(e));if(f===`GET`&&l===`dns`){let n=(new URL(t.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>u.test(e)).slice(0,50);return G(await he(e,n))}}if(o===`jobs`){if(f===`POST`&&!s){let e=await t.json(),n=e.argv;if(!Array.isArray(n)||!n.length||!n.every(e=>typeof e==`string`)||Te.has(n[0]))return K(`argv must be a list of landfall arguments`,400);if([...W.values()].filter(e=>e.job.code===void 0).length>=20)return K(`20 jobs are already running: wait for one to finish or stop one`,429);let r;try{r=xe(e.cwd)}catch(e){return K(e,400)}return G(X(q(n,{server:e.server,password:e.password||void 0,cwd:r})))}let e=s?W.get(s):void 0;if(s&&!e)return K(`no job ${s}`,404);if(e&&f===`GET`&&l===`events`)return ke(e,Number(new URL(t.url).searchParams.get(`from`))||0,t.signal);if(e&&f===`GET`&&!l)return G({...X(e),lines:e.job.lines,from:e.job.dropped});if(e&&f===`POST`&&l===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?K(`only a finished job without --force`,400):[...W.values()].filter(e=>e.job.code===void 0).length>=20?K(`too many jobs running`,429):G(X(q([...e.argv,`--force`],e.opts)));if(e&&f===`POST`&&l===`stop`)return e.job.stop(),G(X(e))}if(o===`fs`&&f===`GET`)return G(R(new URL(t.url).searchParams.get(`path`)??void 0));if(o===`upload`&&f===`POST`){let e=await _e(await t.formData());return G({...e,detected:R(e.path).detected})}return K(`not found`,404)}const Q=(e,t)=>e.length===t.length&&P(Buffer.from(e),Buffer.from(t)),Ae=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>M(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function je(e){p(!0);let t=Ee(),n=F(Ne(async e=>{let n=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(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(Ae(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(Se,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await Z(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return K(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),i=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),a=e.port??4777,s=e.port===void 0?[...Array.from({length:20},(e,t)=>a+t),0]:[a],c;for(let n of s){if(!(c=await r(n)))break;if(c.code!==`EADDRINUSE`)throw c;if(await i(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${m.cyan(r)}`),e.open&&o(r),0}}if(c)return console.error(`${m.red(`error:`)} port ${a} is in use by another program; pick another with --port`),1;let l=n.address().port;l!==a&&console.log(m.dim(`port ${a} is in use by another program: using ${l}`));let u=`http://127.0.0.1:${l}/login?t=${t}`;return console.log(`${m.bold(`lf ui`)} ${m.dim(y)} on ${m.cyan(u)}`),console.log(m.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&o(u),new Promise(e=>{let t=()=>{for(let e of W.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};for(let e of[`SIGINT`,`SIGTERM`,`SIGHUP`])process.once(e,t);if(process.execArgv.some(e=>e.includes(`tsx`))){let e=process.ppid;setInterval(()=>process.ppid!==e&&t(),1e3).unref()}})}function Me(e,t){let n=0;return e.pipe(new L({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function Ne(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?Ce:we;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:I.toWeb(Me(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,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{je as webUi};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "landfall",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "Set up servers and deploy sites behind Traefik with automatic HTTPS",
5
5
  "author": "ph1p <me@ph1p.dev>",
6
6
  "license": "MIT",
@@ -1,8 +0,0 @@
1
- import{A as e,C as t,D as n,E as r,F as i,M as a,N as o,O as s,P as c,S as l,T as u,_ as d,a as ee,b as te,c as ne,d as f,f as p,g as m,h,i as g,j as _,k as v,l as y,m as b,n as x,o as re,p as ie,r as ae,s as oe,t as S,u as se,v as ce,w as le,x as ue,y as de}from"./cli.mjs";import{spawnSync as C}from"node:child_process";import w from"node:fs";import{homedir as T}from"node:os";import{dirname as E,isAbsolute as D,join as O,resolve as k}from"node:path";import{createHash as A,randomBytes as j,timingSafeEqual as M}from"node:crypto";import{createServer as N}from"node:http";import{Readable as P,Transform as F}from"node:stream";function I(e){let t=k(!e||e===`~`?T():v(e)),n=w.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&L(O(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:E(t)===t?null:E(t),home:T(),dirs:n,detected:h(t)}}const L=e=>{try{return w.statSync(e).isDirectory()}catch{return!1}},R=O(s,`uploads`),z=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function B(e,t){w.mkdirSync(E(e),{recursive:!0}),w.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function V(e){w.mkdirSync(R,{recursive:!0,mode:448});for(let e of w.readdirSync(R)){let t=O(R,e);Date.now()-w.statSync(t).mtimeMs>864e5&&w.rmSync(t,{recursive:!0,force:!0})}let t=w.mkdtempSync(O(R,`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=O(t,`.upload-`+z(n[0].name).pop());await B(e,n[0]);let r=O(t,`files`);w.mkdirSync(r);let i=/\.zip$/i.test(e);if(H(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let a=i?C(`unzip`,[`-q`,e,`-d`,r]):C(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?C(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return w.rmSync(e),fe(r),{path:U(r),files:pe(r)}}let i=O(t,`files`);for(let[e,t]of n.entries()){let n=z(r[e]||t.name);n.length&&await B(O(i,...n),t)}return{path:U(i),files:n.length}}function H(e,t){let n=(e,t)=>{let n=C(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
2
- `).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function fe(e){for(let t of w.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&w.rmSync(O(t.parentPath,t.name),{force:!0})}function U(e){for(;;){let t=w.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!L(O(e,t[0])))return e;e=O(e,t[0])}}function pe(e){return w.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function me(e){if(e!=null&&e!==``){if(typeof e!=`string`||!D(e)||!L(e))throw Error(`cwd must be an existing folder`);return e}}const he=w.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),ge=512*1024**2,_e=1024**2,W=/* @__PURE__ */ new Map,ve=/* @__PURE__ */ new Set([`ui`,`web`]);function ye(){let e=O(E(u),`ui-token`);try{let t=w.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=j(24).toString(`hex`);return w.mkdirSync(E(e),{recursive:!0,mode:448}),w.writeFileSync(e,t+`
3
- `,{mode:384}),t}const G=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),K=(e,t=500)=>G({error:e?.message??String(e)},t);function q(e,t){let n=d(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=be(e),s=x(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:se(e),job:s,listeners:r,opts:t};W.set(n,c);for(let[e,t]of W){if(W.size<=50)break;t.job.code!==void 0&&W.delete(e)}return c}const J=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,Y=/^[A-Za-z_][A-Za-z0-9_]*=/;function be(e){let t=[],n={},r=[];for(let i=0;i<e.length;i++){let a=e[i],s=a.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);s&&(s[2]!==void 0||i+1<e.length)?n[o(s[1])]=s[2]??e[++i]:J(e)&&Y.test(a)&&!a.includes(`
4
- `)&&!e.includes(`--env-file`)?r.push(a):t.push(a)}return r.length&&t.push(`--env-file`,`-`),{args:t,env:n,stdin:r.length?r.join(`
5
- `)+`
6
- `:void 0}}const xe=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:J(e)&&Y.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),X=e=>({id:e.id,argv:xe(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function Se(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),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(X(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
7
-
8
- `),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 Z(i,o){let[s,c,u]=o,d=i.method;if(s===`state`&&d===`GET`){let r=e();return G({version:S,cwd:process.cwd(),project:_(process.cwd())??null,projects:m(),defaultImage:de,rules:{loginName:n.source,minPassword:8},paths:{root:l,sites:t,traefik:le,journal:te,proxy:ue},defaults:ae(),servers:f().map(e=>({...e,target:ie(e.name),saved:r.servers[e.name]??null})),jobs:[...W.values()].reverse().map(X)})}if(s===`reach`&&d===`GET`)return G(await p(f().map(e=>e.name),new URL(i.url).searchParams.get(`fresh`)===`1`));if(s===`servers`&&c){let{server:e}=a(c);if(d===`GET`&&!u)return G(await b(e));if(d===`GET`&&u===`firewall`)return G(await oe(e));if(d===`GET`&&u===`config`)return G(await g(c,e));if(d===`GET`&&u===`history`)return G(await ne(e));if(d===`GET`&&u===`facts`)return G(await re(c,e));if(d===`GET`&&u===`updates`)return G(await y(e,new URL(i.url).searchParams.get(`refresh`)===`1`));if(d===`GET`&&u===`dns`){let t=(new URL(i.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>r.test(e)).slice(0,50);return G(await ee(e,t))}}if(s===`jobs`){if(d===`POST`&&!c){let e=await i.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||ve.has(t[0]))return K(`argv must be a list of landfall arguments`,400);if([...W.values()].filter(e=>e.job.code===void 0).length>=20)return K(`20 jobs are already running: wait for one to finish or stop one`,429);let n;try{n=me(e.cwd)}catch(e){return K(e,400)}return G(X(q(t,{server:e.server,password:e.password||void 0,cwd:n})))}let e=c?W.get(c):void 0;if(c&&!e)return K(`no job ${c}`,404);if(e&&d===`GET`&&u===`events`)return Se(e,Number(new URL(i.url).searchParams.get(`from`))||0,i.signal);if(e&&d===`GET`&&!u)return G({...X(e),lines:e.job.lines,from:e.job.dropped});if(e&&d===`POST`&&u===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?K(`only a finished job without --force`,400):[...W.values()].filter(e=>e.job.code===void 0).length>=20?K(`too many jobs running`,429):G(X(q([...e.argv,`--force`],e.opts)));if(e&&d===`POST`&&u===`stop`)return e.job.stop(),G(X(e))}if(s===`fs`&&d===`GET`)return G(I(new URL(i.url).searchParams.get(`path`)??void 0));if(s===`upload`&&d===`POST`){let e=await V(await i.formData());return G({...e,detected:I(e.path).detected})}return K(`not found`,404)}const Q=(e,t)=>e.length===t.length&&M(Buffer.from(e),Buffer.from(t)),Ce=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>A(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function we(e){ce(!0);let t=ye(),n=N(Ee(async e=>{let n=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(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(Ce(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(he,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await Z(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return K(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),a=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),o=e.port??4777,s=e.port===void 0?[...Array.from({length:20},(e,t)=>o+t),0]:[o],l;for(let n of s){if(!(l=await r(n)))break;if(l.code!==`EADDRINUSE`)throw l;if(await a(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${c.cyan(r)}`),e.open&&i(r),0}}if(l)return console.error(`${c.red(`error:`)} port ${o} is in use by another program; pick another with --port`),1;let u=n.address().port;u!==o&&console.log(c.dim(`port ${o} is in use by another program: using ${u}`));let d=`http://127.0.0.1:${u}/login?t=${t}`;return console.log(`${c.bold(`lf ui`)} ${c.dim(S)} on ${c.cyan(d)}`),console.log(c.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&i(d),new Promise(e=>{let t=()=>{for(let e of W.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};for(let e of[`SIGINT`,`SIGTERM`,`SIGHUP`])process.once(e,t);if(process.execArgv.some(e=>e.includes(`tsx`))){let e=process.ppid;setInterval(()=>process.ppid!==e&&t(),1e3).unref()}})}function Te(e,t){let n=0;return e.pipe(new F({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function Ee(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?ge:_e;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:P.toWeb(Te(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,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=P.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{we as webUi};