@dylanpiercey/work 0.1.1 → 0.2.1
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 +28 -0
- package/bin/work.mjs +244 -4
- package/dist/{_Dzz52ZtI.js → _KRMk2duX.js} +378 -12
- package/dist/_ZwD1NY8D.js +2 -0
- package/dist/_h48l7A55.js +169 -0
- package/dist/index.mjs +8621 -7550
- package/dist/public/assets/_0vrYNi0q.js +1 -0
- package/dist/public/assets/{_DvKnakqk.js → _1nwEHbOU.js} +1 -1
- package/dist/public/assets/_1nwEHbOU.js.br +0 -0
- package/dist/public/assets/_3qg1gNHu.js +1 -0
- package/dist/public/assets/_6_cAsPFk.js +1 -0
- package/dist/public/assets/_8mmvS8Ic.js +1 -0
- package/dist/public/assets/{_DX5u6Wvm.js → _9s8_FJP0.js} +1 -1
- package/dist/public/assets/_9s8_FJP0.js.br +0 -0
- package/dist/public/assets/_B0cZNayr.js +1 -0
- package/dist/public/assets/_B23wGSFP.js +1 -0
- package/dist/public/assets/_B4BVKmh0.js +1 -0
- package/dist/public/assets/_B6oF-RIK.js +1 -0
- package/dist/public/assets/_B7SrX3Jp.js +1 -0
- package/dist/public/assets/{_BzGHediB.js → _B7cXT0VU.js} +1 -1
- package/dist/public/assets/_B7cXT0VU.js.br +0 -0
- package/dist/public/assets/{_DcOkTFth.js → _B8zhI-xd.js} +1 -1
- package/dist/public/assets/_B9epMiSr.js +1 -0
- package/dist/public/assets/{_B1D0PIz3.js → _BAYrWdA-.js} +1 -1
- package/dist/public/assets/_BAYrWdA-.js.br +0 -0
- package/dist/public/assets/{_pXG8fRON.js → _BAeMnj2l.js} +1 -1
- package/dist/public/assets/_BAeMnj2l.js.br +0 -0
- package/dist/public/assets/_BAs5Ke57.js +1 -0
- package/dist/public/assets/{_DomK7Scb.js → _BDkvoJS4.js} +1 -1
- package/dist/public/assets/_BDkvoJS4.js.br +0 -0
- package/dist/public/assets/{_TrcWRD8T.js → _BE2H3NPA.js} +7 -7
- package/dist/public/assets/_BE2H3NPA.js.br +0 -0
- package/dist/public/assets/{_-5UNb9-C.js → _BEL3zCP_.js} +1 -1
- package/dist/public/assets/_BEL3zCP_.js.br +0 -0
- package/dist/public/assets/{_FlITqTCo.js → _BGYXkARu.js} +1 -1
- package/dist/public/assets/_BGYXkARu.js.br +0 -0
- package/dist/public/assets/{_CxA3Yab0.js → _BHL0M4hx.js} +1 -1
- package/dist/public/assets/_BHL0M4hx.js.br +0 -0
- package/dist/public/assets/{_1JXC9nFp.js → _BIN7xPBA.js} +1 -1
- package/dist/public/assets/_BIN7xPBA.js.br +0 -0
- package/dist/public/assets/{_CFUREq1M.js → _BPa71So4.js} +1 -1
- package/dist/public/assets/_BPa71So4.js.br +0 -0
- package/dist/public/assets/_BQgqTOA6.js +1 -0
- package/dist/public/assets/_BVOSK21c.js +1 -0
- package/dist/public/assets/{_BVQosHjY.js → _BWdb49DN.js} +1 -1
- package/dist/public/assets/_BWdb49DN.js.br +0 -0
- package/dist/public/assets/{_C-EkQeP_.js → _BZMc12pa.js} +1 -1
- package/dist/public/assets/_BZMc12pa.js.br +0 -0
- package/dist/public/assets/{_BkcPY2j0.js → _BcS532Mh.js} +1 -1
- package/dist/public/assets/_BcS532Mh.js.br +0 -0
- package/dist/public/assets/_Bd3RfEz5.js +1 -0
- package/dist/public/assets/{_yUmRJIJ6.js → _BdS-SIP5.js} +1 -1
- package/dist/public/assets/_BdS-SIP5.js.br +0 -0
- package/dist/public/assets/{_B3NfNm8M.js → _BfU4-P5_.js} +13 -13
- package/dist/public/assets/_BfU4-P5_.js.br +0 -0
- package/dist/public/assets/{_DUs3ZGbj.js → _BidDqDX-.js} +1 -1
- package/dist/public/assets/_BidDqDX-.js.br +0 -0
- package/dist/public/assets/{_Cx_aJfDp.js → _Blw8tTg6.js} +1 -1
- package/dist/public/assets/_Blw8tTg6.js.br +0 -0
- package/dist/public/assets/{_CfGEGt_k.js → _BnIxui8m.js} +1 -1
- package/dist/public/assets/_BnIxui8m.js.br +0 -0
- package/dist/public/assets/{_DRHCy0UF.js → _BnnnKUB5.js} +1 -1
- package/dist/public/assets/_BnnnKUB5.js.br +0 -0
- package/dist/public/assets/_BoFnbJY1.js +1 -0
- package/dist/public/assets/{_Cb_k_JA_.js → _BotYlEwM.js} +1 -1
- package/dist/public/assets/_BotYlEwM.js.br +0 -0
- package/dist/public/assets/_BphDw0sE.js +1 -0
- package/dist/public/assets/_BqeBwjz2.js +1 -0
- package/dist/public/assets/{_BtZ2AatF.js → _Bs2mrt6D.js} +1 -1
- package/dist/public/assets/{_BRQ6iQ5p.js → _BtNwLErW.js} +1 -1
- package/dist/public/assets/_BtNwLErW.js.br +0 -0
- package/dist/public/assets/_Bv7YPgSd.js +1 -0
- package/dist/public/assets/{_CWG813ur.js → _BvrmMS2E.js} +1 -1
- package/dist/public/assets/_BvrmMS2E.js.br +0 -0
- package/dist/public/assets/{_DgWKSW-Y.js → _BwX_PmTl.js} +1 -1
- package/dist/public/assets/_BwX_PmTl.js.br +0 -0
- package/dist/public/assets/{_DEOryr7w.js → _C05-oJk8.js} +1 -1
- package/dist/public/assets/_C05-oJk8.js.br +0 -0
- package/dist/public/assets/{_CCVkYcN5.js → _C2yU1dLq.js} +1 -1
- package/dist/public/assets/_C2yU1dLq.js.br +0 -0
- package/dist/public/assets/{_CKLGAGKJ.js → _C2z40jpl.js} +1 -1
- package/dist/public/assets/_C2z40jpl.js.br +0 -0
- package/dist/public/assets/_C6HTijVG.js +1 -0
- package/dist/public/assets/{_1xOByopP.js → _C6lJdZzj.js} +1 -1
- package/dist/public/assets/_C6lJdZzj.js.br +0 -0
- package/dist/public/assets/_C7vMBzAL.js +1 -0
- package/dist/public/assets/_C7vMBzAL.js.br +0 -0
- package/dist/public/assets/{_B_0C2_RO.js → _C8pXYUZJ.js} +1 -1
- package/dist/public/assets/_C8pXYUZJ.js.br +0 -0
- package/dist/public/assets/_C9zPmOH2.js +2 -0
- package/dist/public/assets/_C9zPmOH2.js.br +0 -0
- package/dist/public/assets/_CA7mM4qX.js +1 -0
- package/dist/public/assets/{_DM60dKRs.js → _CBENFdjy.js} +1 -1
- package/dist/public/assets/_CBENFdjy.js.br +0 -0
- package/dist/public/assets/_CDlUndl3.js +1 -0
- package/dist/public/assets/_CDlUndl3.js.br +0 -0
- package/dist/public/assets/_CEPIxYBN.js +86 -0
- package/dist/public/assets/_CEPIxYBN.js.br +0 -0
- package/dist/public/assets/{_B7VYILLZ.js → _CFHivO5W.js} +1 -1
- package/dist/public/assets/_CFHivO5W.js.br +0 -0
- package/dist/public/assets/{_C98JZr09.js → _CGylDsvz.js} +1 -1
- package/dist/public/assets/_CGylDsvz.js.br +0 -0
- package/dist/public/assets/_CISImxw1.js +1 -0
- package/dist/public/assets/{_m2tRa0jk.js → _CJAgrixe.js} +1 -1
- package/dist/public/assets/_CJAgrixe.js.br +0 -0
- package/dist/public/assets/_CJEPkQp1.js +1 -0
- package/dist/public/assets/_CLF8kSpu.js +1 -0
- package/dist/public/assets/{_DxD_AC3a.js → _CMe89-TR.js} +1 -1
- package/dist/public/assets/_CMe89-TR.js.br +0 -0
- package/dist/public/assets/{_CY87Y7QL.js → _CQl6IKdI.js} +1 -1
- package/dist/public/assets/_CQl6IKdI.js.br +0 -0
- package/dist/public/assets/_CR1lhMQ3.js +1 -0
- package/dist/public/assets/_CRBiqW8E.js +1 -0
- package/dist/public/assets/{_UaKPY71M.js → _CRvIHvZK.js} +1 -1
- package/dist/public/assets/_CRvIHvZK.js.br +0 -0
- package/dist/public/assets/_CT-EgIiU.js +1 -0
- package/dist/public/assets/_CVXUwEfP.js +1 -0
- package/dist/public/assets/_CW5A7nA-.js +1 -0
- package/dist/public/assets/{_Do1ltY2a.js → _CWuvWHgA.js} +1 -1
- package/dist/public/assets/_CWuvWHgA.js.br +0 -0
- package/dist/public/assets/{_CfdK8EeQ.js → _CXSkYGoG.js} +1 -1
- package/dist/public/assets/_CXSkYGoG.js.br +0 -0
- package/dist/public/assets/_CYtwao4w.js +1 -0
- package/dist/public/assets/{_CUsrMpdL.js → _CZTHGPNy.js} +1 -1
- package/dist/public/assets/_CZTHGPNy.js.br +0 -0
- package/dist/public/assets/{_HM_QqtJi.js → _CappgGwi.js} +1 -1
- package/dist/public/assets/_CappgGwi.js.br +0 -0
- package/dist/public/assets/_CeARoEjl.js +1 -0
- package/dist/public/assets/_CeARoEjl.js.br +0 -0
- package/dist/public/assets/{_BDUjU8Ev.js → _Cf7js-r0.js} +1 -1
- package/dist/public/assets/{_Dbm6zAQS.js → _ChJB1hf1.js} +1 -1
- package/dist/public/assets/_ChJB1hf1.js.br +0 -0
- package/dist/public/assets/_ChkvWPVl.js +1 -0
- package/dist/public/assets/{_C0Ib7MZF.js → _CkLUNG6d.js} +1 -1
- package/dist/public/assets/_CkLUNG6d.js.br +0 -0
- package/dist/public/assets/_Cm2gzAKb.js +1 -0
- package/dist/public/assets/_CmCI2k_X.js +1 -0
- package/dist/public/assets/{_D9r_Fn0I.js → _CnIFQPqM.js} +1 -1
- package/dist/public/assets/_CnIFQPqM.js.br +0 -0
- package/dist/public/assets/_CpzkPBcd.js +1 -0
- package/dist/public/assets/{_DL3w8c_p.js → _Cqij7P1t.js} +1 -1
- package/dist/public/assets/_Cqij7P1t.js.br +0 -0
- package/dist/public/assets/_CsGBY_OR.js +1 -0
- package/dist/public/assets/_Cvq7oyHi.js +1 -0
- package/dist/public/assets/_CxQnabfk.js +6 -0
- package/dist/public/assets/_CxQnabfk.js.br +0 -0
- package/dist/public/assets/_Cxuln-KJ.js +1 -0
- package/dist/public/assets/{_DifjKObE.js → _CzjTMNNg.js} +1 -1
- package/dist/public/assets/_CzjTMNNg.js.br +0 -0
- package/dist/public/assets/{_CiYQhf81.js → _D-ewgB-g.js} +1 -1
- package/dist/public/assets/_D-ewgB-g.js.br +0 -0
- package/dist/public/assets/_D-lSuOqN.js +1 -0
- package/dist/public/assets/{_kWQ0dtYZ.js → _D0toMFV3.js} +1 -1
- package/dist/public/assets/_D0toMFV3.js.br +0 -0
- package/dist/public/assets/{_JEvx8ub4.js → _D1SAzhRo.js} +1 -1
- package/dist/public/assets/_D1SAzhRo.js.br +0 -0
- package/dist/public/assets/{_ClHPjF-1.js → _D2NPwpCK.js} +1 -1
- package/dist/public/assets/_D2NPwpCK.js.br +0 -0
- package/dist/public/assets/_D2pdXUO3.js +1 -0
- package/dist/public/assets/{_DzKW89Sx.js → _D36AHDIn.js} +1 -1
- package/dist/public/assets/_D36AHDIn.js.br +0 -0
- package/dist/public/assets/{_EefWj858.js → _D39T9Hc3.js} +1 -1
- package/dist/public/assets/_D39T9Hc3.js.br +0 -0
- package/dist/public/assets/{_DJqEBkNd.js → _D4iaGiBC.js} +1 -1
- package/dist/public/assets/_D4iaGiBC.js.br +0 -0
- package/dist/public/assets/{_BPSTqkJn.js → _D60_NLSK.js} +1 -1
- package/dist/public/assets/_D60_NLSK.js.br +0 -0
- package/dist/public/assets/{_Di1ccq_2.js → _D6O591m8.js} +1 -1
- package/dist/public/assets/_D6O591m8.js.br +0 -0
- package/dist/public/assets/_D6OrE8WN.js +1 -0
- package/dist/public/assets/_D6b9irVv.js +1 -0
- package/dist/public/assets/_D7e9Ia7P.js +1 -0
- package/dist/public/assets/_D95nSKbk.js +1 -0
- package/dist/public/assets/_D9qcWm-q.js +1 -0
- package/dist/public/assets/_D9uiExWV.js +1 -0
- package/dist/public/assets/{__cyRVmTO.js → _DAPIaEl7.js} +1 -1
- package/dist/public/assets/_DAPIaEl7.js.br +0 -0
- package/dist/public/assets/_DBOZe4SW.js +1 -0
- package/dist/public/assets/{_Cmmdd2e3.js → _DCfgIXDZ.js} +1 -1
- package/dist/public/assets/_DCfgIXDZ.js.br +0 -0
- package/dist/public/assets/_DDrn7T44.js +1 -0
- package/dist/public/assets/{_DQyjuXTi.js → _DDs3ube1.js} +1 -1
- package/dist/public/assets/_DDs3ube1.js.br +0 -0
- package/dist/public/assets/_DEKJL8_K.js +1 -0
- package/dist/public/assets/_DJOVMSj1.js +1 -0
- package/dist/public/assets/{_CqCenHOp.js → _DJSmALSy.js} +1 -1
- package/dist/public/assets/_DJSmALSy.js.br +0 -0
- package/dist/public/assets/_DLFpqsl0.js +1 -0
- package/dist/public/assets/_DOyEaX0U.js +1 -0
- package/dist/public/assets/{_B1-W0Sn2.js → _DQd4Gai0.js} +1 -1
- package/dist/public/assets/_DQd4Gai0.js.br +0 -0
- package/dist/public/assets/_DQlvpXhI.js +1 -0
- package/dist/public/assets/_DQlvpXhI.js.br +0 -0
- package/dist/public/assets/{_DZvkgS3V.js → _DRTaehFU.js} +1 -1
- package/dist/public/assets/_DRTaehFU.js.br +0 -0
- package/dist/public/assets/_DTKyMAAU.js +1 -0
- package/dist/public/assets/{_3aBl7SEn.js → _DWuVY-5K.js} +1 -1
- package/dist/public/assets/_DWuVY-5K.js.br +0 -0
- package/dist/public/assets/_DXAMCgpo.js +1 -0
- package/dist/public/assets/_DXAMCgpo.js.br +0 -0
- package/dist/public/assets/{_BSWN-v3P.js → _DXBIA5Jf.js} +1 -1
- package/dist/public/assets/_DXBIA5Jf.js.br +0 -0
- package/dist/public/assets/_Da14xaff.js +1 -0
- package/dist/public/assets/_Da14xaff.js.br +0 -0
- package/dist/public/assets/_DbXR2mlf.js +1 -0
- package/dist/public/assets/{_HmkI4Rfn.js → _Dbp_FIBr.js} +1 -1
- package/dist/public/assets/_Dbp_FIBr.js.br +0 -0
- package/dist/public/assets/_Dc_Ittsm.js +1 -0
- package/dist/public/assets/_DdYt8SWd.js +1 -0
- package/dist/public/assets/{_vcLBrU2m.js → _DgWkLH_d.js} +1 -1
- package/dist/public/assets/_DgWkLH_d.js.br +0 -0
- package/dist/public/assets/_Dh5xTE2G.js +1 -0
- package/dist/public/assets/{_CDY0RpYX.js → _DiE4MtP8.js} +1 -1
- package/dist/public/assets/_DiE4MtP8.js.br +0 -0
- package/dist/public/assets/_DjGr7GRl.js +1 -0
- package/dist/public/assets/_DjGr7GRl.js.br +0 -0
- package/dist/public/assets/_DotQ0YLh.js +2 -0
- package/dist/public/assets/_DotQ0YLh.js.br +0 -0
- package/dist/public/assets/_DqAfN3OQ.js +1 -0
- package/dist/public/assets/{_BKIx7IZP.js → _DqMNzDjo.js} +1 -1
- package/dist/public/assets/_DqMNzDjo.js.br +0 -0
- package/dist/public/assets/{_BsJZPHmz.js → _Dqqxf-zK.js} +1 -1
- package/dist/public/assets/_Dqqxf-zK.js.br +0 -0
- package/dist/public/assets/{_dsj7vMTW.js → _Dufbr-KL.js} +1 -1
- package/dist/public/assets/_Dufbr-KL.js.br +0 -0
- package/dist/public/assets/_Dv-q-Ej7.js +1 -0
- package/dist/public/assets/{_BHU5aikZ.js → _Dvfi_oP5.js} +1 -1
- package/dist/public/assets/_DvyJx5yW.js +1 -0
- package/dist/public/assets/_DvyJx5yW.js.br +0 -0
- package/dist/public/assets/_DxMb2QV8.js +1 -0
- package/dist/public/assets/{_BKHtiW96.js → _DyYpJ-H0.js} +1 -1
- package/dist/public/assets/_DyYpJ-H0.js.br +0 -0
- package/dist/public/assets/_DyhlHfZJ.js +1 -0
- package/dist/public/assets/_Dz053N5t.js +1 -0
- package/dist/public/assets/{_BLfzQY-3.js → _L54I8aS4.js} +1 -1
- package/dist/public/assets/_L54I8aS4.js.br +0 -0
- package/dist/public/assets/_Ly-2Xkdk.js +1 -0
- package/dist/public/assets/{_BSFJEuJX.js → _NN0jQCcG.js} +1 -1
- package/dist/public/assets/_NN0jQCcG.js.br +0 -0
- package/dist/public/assets/{_7YrArFMB.js → _OzsIhJyG.js} +1 -1
- package/dist/public/assets/_OzsIhJyG.js.br +0 -0
- package/dist/public/assets/_QJUmfZ1o.js +1 -0
- package/dist/public/assets/{_iutZ4EDe.js → _VdeOc79W.js} +1 -1
- package/dist/public/assets/_VdeOc79W.js.br +0 -0
- package/dist/public/assets/_VkEFwEn-.js +1 -0
- package/dist/public/assets/_ahp1rb5o.js +1 -0
- package/dist/public/assets/{_VPLoEBTS.js → _aqBpT1uJ.js} +1 -1
- package/dist/public/assets/{_DBD57l1T.js → _bgu_OKRO.js} +1 -1
- package/dist/public/assets/_bgu_OKRO.js.br +0 -0
- package/dist/public/assets/{_C81nSl09.js → _fqHAtYJz.js} +1 -1
- package/dist/public/assets/_fqHAtYJz.js.br +0 -0
- package/dist/public/assets/_hdoOlF1z.js +1 -0
- package/dist/public/assets/{_CtqGSS84.js → _iFV4welK.js} +1 -1
- package/dist/public/assets/_iFV4welK.js.br +0 -0
- package/dist/public/assets/_ihgadrBp.js +1 -0
- package/dist/public/assets/_lJgrZjR9.js +1 -0
- package/dist/public/assets/_lJgrZjR9.js.br +0 -0
- package/dist/public/assets/_oNShN50G.js +1 -0
- package/dist/public/assets/{_C8bqYk4J.js → _oR1qIlrw.js} +1 -1
- package/dist/public/assets/_oR1qIlrw.js.br +0 -0
- package/dist/public/assets/_pxC2PRJZ.js +1 -0
- package/dist/public/assets/_q6iLcLGY.js +1 -0
- package/dist/public/assets/_qnKDgwVg.js +1 -0
- package/dist/public/assets/_rYWHy2XX.js +1 -0
- package/dist/public/assets/{_CHdpLB95.js → _snlh9j-I.js} +1 -1
- package/dist/public/assets/_snlh9j-I.js.br +0 -0
- package/dist/public/assets/_uGH3cvIZ.js +1 -0
- package/dist/public/assets/_uRhnGFfv.js +1 -0
- package/dist/public/assets/{_DMJHTH74.js → _wOX1zfjU.js} +1 -1
- package/dist/public/assets/_wOX1zfjU.js.br +0 -0
- package/dist/public/assets/_xMc_vDaT.js +1 -0
- package/dist/public/assets/{_DSUBv-Bl.js → _zGPxyhe6.js} +1 -1
- package/dist/public/assets/_zGPxyhe6.js.br +0 -0
- package/dist/public/assets/files-view-dOlOeiFQ.js +2 -0
- package/dist/public/assets/manage-workspace-CO4jGUSH.js +2 -0
- package/dist/public/assets/new-hhTJEH33.js +1 -0
- package/dist/public/assets/new-workspace-w6SV5jo2.js +2 -0
- package/dist/public/assets/review-view-DhbS4oNh.js +2 -0
- package/dist/public/assets/settings-hhTJEH33.js +1 -0
- package/dist/public/assets/settings-page-DpRlMZTu.js +2 -0
- package/dist/public/assets/styles-BfKP6xyD.css +1 -0
- package/dist/public/assets/styles-BfKP6xyD.css.br +0 -0
- package/dist/public/assets/w-hhTJEH33.js +1 -0
- package/dist/public/assets/w._-DSFmri_t.js +1 -0
- package/dist/public/assets/w._-hhTJEH33.js +1 -0
- package/dist/public/assets/w._-kFbho4FJ.js +1 -0
- package/dist/rtc-helper.mjs +1170 -0
- package/lib/preview/media.mjs +5 -4
- package/lib/setup/doctor.d.mts +11 -0
- package/lib/setup/doctor.mjs +23 -2
- package/lib/setup/firewall.d.mts +76 -0
- package/lib/setup/firewall.mjs +598 -0
- package/package.json +3 -2
- package/dist/_D1C-XIqT.js +0 -168
- package/dist/_HbAW5Cbt.js +0 -2
- package/dist/public/assets/_-5UNb9-C.js.br +0 -0
- package/dist/public/assets/_06rtP_SB.js +0 -1
- package/dist/public/assets/_14yhXek7.js +0 -1
- package/dist/public/assets/_14yhXek7.js.br +0 -0
- package/dist/public/assets/_1JXC9nFp.js.br +0 -0
- package/dist/public/assets/_1Yit_Y5R.js +0 -1
- package/dist/public/assets/_1ZPfR2B8.js +0 -6
- package/dist/public/assets/_1ZPfR2B8.js.br +0 -0
- package/dist/public/assets/_1xOByopP.js.br +0 -0
- package/dist/public/assets/_3aBl7SEn.js.br +0 -0
- package/dist/public/assets/_5aMLV2Vy.js +0 -1
- package/dist/public/assets/_7YrArFMB.js.br +0 -0
- package/dist/public/assets/_B1-W0Sn2.js.br +0 -0
- package/dist/public/assets/_B1D0PIz3.js.br +0 -0
- package/dist/public/assets/_B1agoKUa.js +0 -1
- package/dist/public/assets/_B3NfNm8M.js.br +0 -0
- package/dist/public/assets/_B7VYILLZ.js.br +0 -0
- package/dist/public/assets/_B7sPBy0J.js +0 -1
- package/dist/public/assets/_B81p6DSw.js +0 -1
- package/dist/public/assets/_BAkSKCkz.js +0 -1
- package/dist/public/assets/_BHZPTv1U.js +0 -1
- package/dist/public/assets/_BKHtiW96.js.br +0 -0
- package/dist/public/assets/_BKIx7IZP.js.br +0 -0
- package/dist/public/assets/_BLfzQY-3.js.br +0 -0
- package/dist/public/assets/_BPSTqkJn.js.br +0 -0
- package/dist/public/assets/_BQZOimBB.js +0 -1
- package/dist/public/assets/_BRQ6iQ5p.js.br +0 -0
- package/dist/public/assets/_BRVURc4u.js +0 -1
- package/dist/public/assets/_BSFJEuJX.js.br +0 -0
- package/dist/public/assets/_BSWN-v3P.js.br +0 -0
- package/dist/public/assets/_BTVAQr2c.js +0 -1
- package/dist/public/assets/_BUD1pBC0.js +0 -1
- package/dist/public/assets/_BVGzCczb.js +0 -1
- package/dist/public/assets/_BVQosHjY.js.br +0 -0
- package/dist/public/assets/_B_0C2_RO.js.br +0 -0
- package/dist/public/assets/_BaA31ms5.js +0 -1
- package/dist/public/assets/_BgCHJGlO.js +0 -1
- package/dist/public/assets/_BhpRLDbL.js +0 -1
- package/dist/public/assets/_BhpRLDbL.js.br +0 -0
- package/dist/public/assets/_BkKcjMAC.js +0 -1
- package/dist/public/assets/_BkcPY2j0.js.br +0 -0
- package/dist/public/assets/_BlTVd_3d.js +0 -1
- package/dist/public/assets/_BlqlLmEY.js +0 -1
- package/dist/public/assets/_BoNfxc9P.js +0 -1
- package/dist/public/assets/_BoPk1gXL.js +0 -1
- package/dist/public/assets/_BqswW9B9.js +0 -1
- package/dist/public/assets/_BricqzsO.js +0 -1
- package/dist/public/assets/_BrreckIa.js +0 -1
- package/dist/public/assets/_BsJZPHmz.js.br +0 -0
- package/dist/public/assets/_BtRYwaBb.js +0 -1
- package/dist/public/assets/_BvPBKjRP.js +0 -1
- package/dist/public/assets/_BvPBKjRP.js.br +0 -0
- package/dist/public/assets/_ByrrjMU9.js +0 -1
- package/dist/public/assets/_BzGHediB.js.br +0 -0
- package/dist/public/assets/_BzvTKm92.js +0 -1
- package/dist/public/assets/_C-EkQeP_.js.br +0 -0
- package/dist/public/assets/_C0Ib7MZF.js.br +0 -0
- package/dist/public/assets/_C2p1Z5Lf.js +0 -86
- package/dist/public/assets/_C2p1Z5Lf.js.br +0 -0
- package/dist/public/assets/_C3iiOlxd.js +0 -1
- package/dist/public/assets/_C81nSl09.js.br +0 -0
- package/dist/public/assets/_C8bqYk4J.js.br +0 -0
- package/dist/public/assets/_C98JZr09.js.br +0 -0
- package/dist/public/assets/_CB4VkmGZ.js +0 -1
- package/dist/public/assets/_CCVkYcN5.js.br +0 -0
- package/dist/public/assets/_CDY0RpYX.js.br +0 -0
- package/dist/public/assets/_CE2GEqMx.js +0 -1
- package/dist/public/assets/_CE2GEqMx.js.br +0 -0
- package/dist/public/assets/_CFUREq1M.js.br +0 -0
- package/dist/public/assets/_CHdh6bII.js +0 -1
- package/dist/public/assets/_CHdpLB95.js.br +0 -0
- package/dist/public/assets/_CKLGAGKJ.js.br +0 -0
- package/dist/public/assets/_CMRt2lyQ.js +0 -1
- package/dist/public/assets/_CN1zW1AT.js +0 -1
- package/dist/public/assets/_CN1zW1AT.js.br +0 -0
- package/dist/public/assets/_CUILmj9R.js +0 -1
- package/dist/public/assets/_CUhP8KQ8.js +0 -1
- package/dist/public/assets/_CUsrMpdL.js.br +0 -0
- package/dist/public/assets/_CVG0_w9T.js +0 -1
- package/dist/public/assets/_CWG813ur.js.br +0 -0
- package/dist/public/assets/_CY87Y7QL.js.br +0 -0
- package/dist/public/assets/_CYAsKva_.js +0 -1
- package/dist/public/assets/_CajKYNSY.js +0 -1
- package/dist/public/assets/_Cb_k_JA_.js.br +0 -0
- package/dist/public/assets/_Ce8PlfMD.js +0 -1
- package/dist/public/assets/_CfGEGt_k.js.br +0 -0
- package/dist/public/assets/_CfdK8EeQ.js.br +0 -0
- package/dist/public/assets/_CgOKmAwr.js +0 -1
- package/dist/public/assets/_CiYQhf81.js.br +0 -0
- package/dist/public/assets/_Cj9DHPRL.js +0 -1
- package/dist/public/assets/_ClHPjF-1.js.br +0 -0
- package/dist/public/assets/_Cmmdd2e3.js.br +0 -0
- package/dist/public/assets/_Cn9Ia4SS.js +0 -1
- package/dist/public/assets/_Cn9Ia4SS.js.br +0 -0
- package/dist/public/assets/_CnTzFbuG.js +0 -1
- package/dist/public/assets/_CqCenHOp.js.br +0 -0
- package/dist/public/assets/_CtqGSS84.js.br +0 -0
- package/dist/public/assets/_CttfwM-c.js +0 -1
- package/dist/public/assets/_CxA3Yab0.js.br +0 -0
- package/dist/public/assets/_Cx_aJfDp.js.br +0 -0
- package/dist/public/assets/_D2L_ILOW.js +0 -1
- package/dist/public/assets/_D47lCu0c.js +0 -1
- package/dist/public/assets/_D5W-uGIK.js +0 -1
- package/dist/public/assets/_D6BbZLrc.js +0 -1
- package/dist/public/assets/_D6RVeCAn.js +0 -1
- package/dist/public/assets/_D75V4jV-.js +0 -1
- package/dist/public/assets/_D9r_Fn0I.js.br +0 -0
- package/dist/public/assets/_DBD57l1T.js.br +0 -0
- package/dist/public/assets/_DCWILZgL.js +0 -1
- package/dist/public/assets/_DEOryr7w.js.br +0 -0
- package/dist/public/assets/_DF54Iz7o.js +0 -1
- package/dist/public/assets/_DFamU2Uj.js +0 -2
- package/dist/public/assets/_DFamU2Uj.js.br +0 -0
- package/dist/public/assets/_DHmBuyCO.js +0 -1
- package/dist/public/assets/_DHmBuyCO.js.br +0 -0
- package/dist/public/assets/_DJqEBkNd.js.br +0 -0
- package/dist/public/assets/_DL3w8c_p.js.br +0 -0
- package/dist/public/assets/_DLVL-4MP.js +0 -1
- package/dist/public/assets/_DM60dKRs.js.br +0 -0
- package/dist/public/assets/_DMJHTH74.js.br +0 -0
- package/dist/public/assets/_DMLv6KA6.js +0 -1
- package/dist/public/assets/_DQYpgyzr.js +0 -1
- package/dist/public/assets/_DQYpgyzr.js.br +0 -0
- package/dist/public/assets/_DQyjuXTi.js.br +0 -0
- package/dist/public/assets/_DRHCy0UF.js.br +0 -0
- package/dist/public/assets/_DSUBv-Bl.js.br +0 -0
- package/dist/public/assets/_DUFM9qLV.js +0 -1
- package/dist/public/assets/_DUFM9qLV.js.br +0 -0
- package/dist/public/assets/_DUs3ZGbj.js.br +0 -0
- package/dist/public/assets/_DX5u6Wvm.js.br +0 -0
- package/dist/public/assets/_DZvkgS3V.js.br +0 -0
- package/dist/public/assets/_Dbm6zAQS.js.br +0 -0
- package/dist/public/assets/_Ddf1gumD.js +0 -1
- package/dist/public/assets/_De6E9M7R.js +0 -1
- package/dist/public/assets/_DgWKSW-Y.js.br +0 -0
- package/dist/public/assets/_DhEN6T4v.js +0 -1
- package/dist/public/assets/_Di1ccq_2.js.br +0 -0
- package/dist/public/assets/_DifjKObE.js.br +0 -0
- package/dist/public/assets/_Dn10ABa-.js +0 -1
- package/dist/public/assets/_Do1ltY2a.js.br +0 -0
- package/dist/public/assets/_DomK7Scb.js.br +0 -0
- package/dist/public/assets/_DtWggCxt.js +0 -1
- package/dist/public/assets/_DtqqybyQ.js +0 -1
- package/dist/public/assets/_DvKnakqk.js.br +0 -0
- package/dist/public/assets/_DxD_AC3a.js.br +0 -0
- package/dist/public/assets/_Dxo7m9cG.js +0 -1
- package/dist/public/assets/_DzKW89Sx.js.br +0 -0
- package/dist/public/assets/_DzTHCioH.js +0 -1
- package/dist/public/assets/_EefWj858.js.br +0 -0
- package/dist/public/assets/_FlITqTCo.js.br +0 -0
- package/dist/public/assets/_GkEw2S0T.js +0 -1
- package/dist/public/assets/_HM_QqtJi.js.br +0 -0
- package/dist/public/assets/_HmkI4Rfn.js.br +0 -0
- package/dist/public/assets/_JEvx8ub4.js.br +0 -0
- package/dist/public/assets/_JOb8B3tz.js +0 -1
- package/dist/public/assets/_LEIhjFrO.js +0 -1
- package/dist/public/assets/_STJhwy7U.js +0 -1
- package/dist/public/assets/_TrcWRD8T.js.br +0 -0
- package/dist/public/assets/_UaKPY71M.js.br +0 -0
- package/dist/public/assets/_UhK3lkm0.js +0 -1
- package/dist/public/assets/_Zeftz3t1.js +0 -1
- package/dist/public/assets/__cyRVmTO.js.br +0 -0
- package/dist/public/assets/_awfYTHit.js +0 -1
- package/dist/public/assets/_dsj7vMTW.js.br +0 -0
- package/dist/public/assets/_eqpiOhSM.js +0 -1
- package/dist/public/assets/_iutZ4EDe.js.br +0 -0
- package/dist/public/assets/_kWQ0dtYZ.js.br +0 -0
- package/dist/public/assets/_knvMqXQh.js +0 -1
- package/dist/public/assets/_lwC7sIgC.js +0 -1
- package/dist/public/assets/_m2tRa0jk.js.br +0 -0
- package/dist/public/assets/_pXG8fRON.js.br +0 -0
- package/dist/public/assets/_r541ENtr.js +0 -1
- package/dist/public/assets/_rrUx1wn3.js +0 -1
- package/dist/public/assets/_tJvcu7sD.js +0 -1
- package/dist/public/assets/_vcLBrU2m.js.br +0 -0
- package/dist/public/assets/_xZC4Vm70.js +0 -1
- package/dist/public/assets/_yAr5r-uL.js +0 -1
- package/dist/public/assets/_yUmRJIJ6.js.br +0 -0
- package/dist/public/assets/files-view-CTadakLK.js +0 -2
- package/dist/public/assets/manage-workspace-HUAS6qjy.js +0 -2
- package/dist/public/assets/new-CirJkJFl.js +0 -1
- package/dist/public/assets/new-workspace-C3t1nA6z.js +0 -2
- package/dist/public/assets/review-view-DKPNtgmc.js +0 -2
- package/dist/public/assets/settings-CirJkJFl.js +0 -1
- package/dist/public/assets/settings-page-DH-9cN-8.js +0 -2
- package/dist/public/assets/styles-CIeNhVxC.css +0 -1
- package/dist/public/assets/styles-CIeNhVxC.css.br +0 -0
- package/dist/public/assets/w-CirJkJFl.js +0 -1
- package/dist/public/assets/w._-BnShhzdq.js +0 -1
- package/dist/public/assets/w._-CirJkJFl.js +0 -1
- package/dist/public/assets/w._-aCTYfruH.js +0 -1
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
2
|
+
import childProcess from "node:child_process";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import zlib from "node:zlib";
|
|
5
|
+
import dgram from "node:dgram";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
//#region src/server/rtc/upnp.ts
|
|
8
|
+
/**
|
|
9
|
+
* A UPnP IGD client, just enough to open one UDP port on the LAN's router
|
|
10
|
+
* for direct connections (server/rtc): find the gateway (an SSDP
|
|
11
|
+
* M-SEARCH), read its description for the WAN connection service, then
|
|
12
|
+
* SOAP `GetExternalIPAddress`, `AddPortMapping` and `DeletePortMapping`. A
|
|
13
|
+
* phone behind carrier NAT cannot punch through to this machine by itself;
|
|
14
|
+
* with the router forwarding the port, its checks arrive as they would on
|
|
15
|
+
* the LAN.
|
|
16
|
+
*
|
|
17
|
+
* Only on demand: the user allowed it (Settings), and a page that could
|
|
18
|
+
* use it failed to connect without it (server/rtc). The mapping is leased
|
|
19
|
+
* (LEASE_S) and renewed while a peer uses it, given back once none has for
|
|
20
|
+
* IDLE_MS, and deleted when the permission goes or the helper this runs in
|
|
21
|
+
* exits (helper.ts); the lease is what closes it if the helper dies
|
|
22
|
+
* without deleting it.
|
|
23
|
+
*
|
|
24
|
+
* Whatever answers on the LAN is untrusted: only a default gateway is
|
|
25
|
+
* listened to, only its own address is fetched from and posted to, and
|
|
26
|
+
* nothing it sends is read past a small size.
|
|
27
|
+
*/
|
|
28
|
+
/** A fixed SSDP target (`WORK_UPNP_SSDP`, host:port) instead of the group
|
|
29
|
+
* and the default gateways: a fake gateway to test against, which is then
|
|
30
|
+
* the one gateway trusted. */
|
|
31
|
+
var OVERRIDE = hostPort(process.env.WORK_UPNP_SSDP);
|
|
32
|
+
var GROUP = "239.255.255.250";
|
|
33
|
+
var SSDP_PORT = OVERRIDE?.port ?? 1900;
|
|
34
|
+
var SEARCH = [
|
|
35
|
+
"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
36
|
+
"urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
37
|
+
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
38
|
+
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
|
39
|
+
"urn:schemas-upnp-org:service:WANPPPConnection:1"
|
|
40
|
+
];
|
|
41
|
+
/** The services that map ports, best first. */
|
|
42
|
+
var SERVICES = [
|
|
43
|
+
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
|
44
|
+
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
45
|
+
"urn:schemas-upnp-org:service:WANPPPConnection:1"
|
|
46
|
+
];
|
|
47
|
+
/** An hour: long enough that renewing costs nothing, short enough that a
|
|
48
|
+
* machine that vanished without deleting it does not leave the port open
|
|
49
|
+
* for good. `WORK_UPNP_LEASE` (seconds) shortens it, and the idle release
|
|
50
|
+
* with it, to watch them in a test. */
|
|
51
|
+
var LEASE_S = Number(process.env.WORK_UPNP_LEASE) || 3600;
|
|
52
|
+
/** Unused this long, the mapping is given back: a page that walks away
|
|
53
|
+
* does not leave the router's port open. */
|
|
54
|
+
var IDLE_MS = Math.min(12e4, LEASE_S * 1e3 / 4);
|
|
55
|
+
var CHECK_MS = Math.max(500, IDLE_MS / 4);
|
|
56
|
+
/** Every network step's limit: a LAN gateway answers in milliseconds. */
|
|
57
|
+
var TIMEOUT_MS = 3e3;
|
|
58
|
+
/** The most read of what a device on the LAN sends. */
|
|
59
|
+
var MAX_SSDP = 2048;
|
|
60
|
+
var MAX_XML = 65536;
|
|
61
|
+
var MAX_SOAP = 16384;
|
|
62
|
+
/** How long a found gateway is used before looking again. */
|
|
63
|
+
var GATEWAY_TTL_MS = 6e5;
|
|
64
|
+
var DESCRIPTION = "work direct connections";
|
|
65
|
+
var UpnpError = class extends Error {
|
|
66
|
+
code;
|
|
67
|
+
constructor(code, message) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.code = code;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var status = { state: "off" };
|
|
73
|
+
var mapping = null;
|
|
74
|
+
var gateway = null;
|
|
75
|
+
var busy = null;
|
|
76
|
+
var renewTimer;
|
|
77
|
+
var useTimer;
|
|
78
|
+
var lastUse = 0;
|
|
79
|
+
var inUse = () => false;
|
|
80
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
function upnpStatus() {
|
|
82
|
+
return status;
|
|
83
|
+
}
|
|
84
|
+
function onUpnpStatus(fn) {
|
|
85
|
+
listeners.add(fn);
|
|
86
|
+
return () => listeners.delete(fn);
|
|
87
|
+
}
|
|
88
|
+
function setStatus(next) {
|
|
89
|
+
status = next;
|
|
90
|
+
for (const fn of listeners) fn(next);
|
|
91
|
+
}
|
|
92
|
+
/** The router's address and the port it forwards to this machine's
|
|
93
|
+
* `port`, when mapped. */
|
|
94
|
+
function upnpCandidate() {
|
|
95
|
+
return mapping && status.state === "mapped" ? {
|
|
96
|
+
ip: mapping.externalIp,
|
|
97
|
+
port: mapping.externalPort
|
|
98
|
+
} : null;
|
|
99
|
+
}
|
|
100
|
+
/** Map UDP `port` on the router, or keep the mapping there is. `used`
|
|
101
|
+
* says, as it is checked, whether a peer still uses it. Settles when the
|
|
102
|
+
* mapping is made or has failed; the status says which. */
|
|
103
|
+
function upnpWant(port, used) {
|
|
104
|
+
lastUse = Date.now();
|
|
105
|
+
inUse = used;
|
|
106
|
+
if (busy) return busy;
|
|
107
|
+
if (mapping && mapping.internalPort === port && status.state === "mapped") return Promise.resolve();
|
|
108
|
+
busy = (async () => {
|
|
109
|
+
if (mapping) await unmap();
|
|
110
|
+
await map(port);
|
|
111
|
+
})().finally(() => {
|
|
112
|
+
busy = null;
|
|
113
|
+
});
|
|
114
|
+
return busy;
|
|
115
|
+
}
|
|
116
|
+
/** Delete the mapping, if there is one (the permission went, it is no
|
|
117
|
+
* longer used, or the server is stopping). */
|
|
118
|
+
async function upnpRelease() {
|
|
119
|
+
clearTimeout(renewTimer);
|
|
120
|
+
clearInterval(useTimer);
|
|
121
|
+
useTimer = void 0;
|
|
122
|
+
inUse = () => false;
|
|
123
|
+
await busy?.catch(() => {});
|
|
124
|
+
await unmap();
|
|
125
|
+
if (status.state !== "off") setStatus({ state: "off" });
|
|
126
|
+
}
|
|
127
|
+
async function map(port) {
|
|
128
|
+
setStatus({ state: "mapping" });
|
|
129
|
+
try {
|
|
130
|
+
const gw = await findGateway();
|
|
131
|
+
const lanIp = await localAddressFor(gw.host);
|
|
132
|
+
const externalIp = tag(await soap(gw, "GetExternalIPAddress", []), "NewExternalIPAddress");
|
|
133
|
+
if (!net.isIPv4(externalIp)) throw new Error(`the router gave no WAN address (${externalIp || "none"})`);
|
|
134
|
+
const ports = [port, ...[
|
|
135
|
+
1,
|
|
136
|
+
2,
|
|
137
|
+
3
|
|
138
|
+
].map(() => 49152 + randomInt(16e3))];
|
|
139
|
+
let lease = LEASE_S;
|
|
140
|
+
let made = null;
|
|
141
|
+
let last;
|
|
142
|
+
for (const external of ports) try {
|
|
143
|
+
await addMapping(gw, external, port, lanIp, lease);
|
|
144
|
+
made = external;
|
|
145
|
+
break;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
last = err;
|
|
148
|
+
if (err instanceof UpnpError && err.code === 725 && lease) {
|
|
149
|
+
lease = 0;
|
|
150
|
+
try {
|
|
151
|
+
await addMapping(gw, external, port, lanIp, lease);
|
|
152
|
+
made = external;
|
|
153
|
+
break;
|
|
154
|
+
} catch (again) {
|
|
155
|
+
last = again;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!(last instanceof UpnpError) || ![718, 729].includes(last.code)) break;
|
|
159
|
+
}
|
|
160
|
+
if (made === null) throw last;
|
|
161
|
+
mapping = {
|
|
162
|
+
gateway: gw,
|
|
163
|
+
externalIp,
|
|
164
|
+
externalPort: made,
|
|
165
|
+
internalPort: port,
|
|
166
|
+
lanIp,
|
|
167
|
+
lease
|
|
168
|
+
};
|
|
169
|
+
setStatus({
|
|
170
|
+
state: "mapped",
|
|
171
|
+
external: `${externalIp}:${made}`,
|
|
172
|
+
internal: `${lanIp}:${port}`,
|
|
173
|
+
expires: lease ? Date.now() + lease * 1e3 : 0,
|
|
174
|
+
note: privateV4(externalIp) ? `the router's own WAN address ${externalIp} is private: another NAT is in front of it, so this opens nothing to the internet` : void 0
|
|
175
|
+
});
|
|
176
|
+
scheduleRenew();
|
|
177
|
+
watchUse();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
gateway = null;
|
|
180
|
+
setStatus({
|
|
181
|
+
state: "error",
|
|
182
|
+
error: describe(err)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Given back once no peer has used it for IDLE_MS. */
|
|
187
|
+
function watchUse() {
|
|
188
|
+
clearInterval(useTimer);
|
|
189
|
+
useTimer = setInterval(() => {
|
|
190
|
+
if (inUse()) lastUse = Date.now();
|
|
191
|
+
else if (Date.now() - lastUse > IDLE_MS) upnpRelease();
|
|
192
|
+
}, CHECK_MS);
|
|
193
|
+
useTimer.unref?.();
|
|
194
|
+
}
|
|
195
|
+
function scheduleRenew() {
|
|
196
|
+
clearTimeout(renewTimer);
|
|
197
|
+
if (!mapping?.lease) return;
|
|
198
|
+
renewTimer = setTimeout(renew, LEASE_S * 1e3 / 2);
|
|
199
|
+
renewTimer.unref?.();
|
|
200
|
+
}
|
|
201
|
+
async function renew() {
|
|
202
|
+
const m = mapping;
|
|
203
|
+
if (!m) return;
|
|
204
|
+
try {
|
|
205
|
+
await addMapping(m.gateway, m.externalPort, m.internalPort, m.lanIp, m.lease);
|
|
206
|
+
setStatus({
|
|
207
|
+
...status,
|
|
208
|
+
expires: Date.now() + m.lease * 1e3
|
|
209
|
+
});
|
|
210
|
+
scheduleRenew();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
mapping = null;
|
|
213
|
+
gateway = null;
|
|
214
|
+
setStatus({
|
|
215
|
+
state: "error",
|
|
216
|
+
error: `renewing: ${describe(err)}`
|
|
217
|
+
});
|
|
218
|
+
upnpWant(m.internalPort, inUse);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function unmap() {
|
|
222
|
+
const m = mapping;
|
|
223
|
+
mapping = null;
|
|
224
|
+
if (!m) return;
|
|
225
|
+
await soap(m.gateway, "DeletePortMapping", [
|
|
226
|
+
["NewRemoteHost", ""],
|
|
227
|
+
["NewExternalPort", m.externalPort],
|
|
228
|
+
["NewProtocol", "UDP"]
|
|
229
|
+
]).catch(() => {});
|
|
230
|
+
}
|
|
231
|
+
function addMapping(gw, external, internal, lanIp, lease) {
|
|
232
|
+
return soap(gw, "AddPortMapping", [
|
|
233
|
+
["NewRemoteHost", ""],
|
|
234
|
+
["NewExternalPort", external],
|
|
235
|
+
["NewProtocol", "UDP"],
|
|
236
|
+
["NewInternalPort", internal],
|
|
237
|
+
["NewInternalClient", lanIp],
|
|
238
|
+
["NewEnabled", 1],
|
|
239
|
+
["NewPortMappingDescription", DESCRIPTION],
|
|
240
|
+
["NewLeaseDuration", lease]
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
async function soap(gw, action, args) {
|
|
244
|
+
const fields = args.map(([name, value]) => `<${name}>${escapeXml(String(value))}</${name}>`).join("");
|
|
245
|
+
const body = `<?xml version="1.0"?>\r
|
|
246
|
+
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:${action} xmlns:u="${gw.service}">${fields}</u:${action}></s:Body></s:Envelope>`;
|
|
247
|
+
const res = await fetch(gw.control, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: {
|
|
250
|
+
"content-type": "text/xml; charset=\"utf-8\"",
|
|
251
|
+
soapaction: `"${gw.service}#${action}"`
|
|
252
|
+
},
|
|
253
|
+
body,
|
|
254
|
+
redirect: "error",
|
|
255
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
256
|
+
});
|
|
257
|
+
const text = await readCapped(res, MAX_SOAP);
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
const code = Number(tag(text, "errorCode")) || 0;
|
|
260
|
+
const what = tag(text, "errorDescription") || `HTTP ${res.status}`;
|
|
261
|
+
throw new UpnpError(code, `${action}: ${code ? `${code} ` : ""}${what}`);
|
|
262
|
+
}
|
|
263
|
+
return text;
|
|
264
|
+
}
|
|
265
|
+
async function findGateway() {
|
|
266
|
+
if (gateway && Date.now() - gateway.found < GATEWAY_TTL_MS) return gateway;
|
|
267
|
+
const gateways = OVERRIDE ? [OVERRIDE.host] : await defaultGateways();
|
|
268
|
+
if (!gateways.length) throw new Error("this machine has no default gateway");
|
|
269
|
+
const answers = await search(gateways);
|
|
270
|
+
let last = /* @__PURE__ */ new Error("no UPnP gateway answered on the LAN");
|
|
271
|
+
for (const { location, from } of answers) try {
|
|
272
|
+
gateway = await describeGateway(location, from);
|
|
273
|
+
return gateway;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
last = err;
|
|
276
|
+
}
|
|
277
|
+
throw last;
|
|
278
|
+
}
|
|
279
|
+
/** The device descriptions the gateways point at, from answers to a search
|
|
280
|
+
* sent to the SSDP group and to each gateway directly — a host firewall
|
|
281
|
+
* (firewalld's default zones) drops a gateway's unicast answer to a
|
|
282
|
+
* multicast search, matching no flow this machine opened, where the answer
|
|
283
|
+
* to a query sent to it comes back on one. Only an answer from a gateway's
|
|
284
|
+
* own address, naming that address, is kept: anything else on the LAN can
|
|
285
|
+
* answer a search. The first answer ends the wait a moment later. */
|
|
286
|
+
function search(gateways) {
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
const found = /* @__PURE__ */ new Map();
|
|
289
|
+
const sock = dgram.createSocket({
|
|
290
|
+
type: "udp4",
|
|
291
|
+
reuseAddr: true
|
|
292
|
+
});
|
|
293
|
+
let timer;
|
|
294
|
+
const finish = () => {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
try {
|
|
297
|
+
sock.close();
|
|
298
|
+
} catch {}
|
|
299
|
+
resolve([...found].map(([location, from]) => ({
|
|
300
|
+
location,
|
|
301
|
+
from
|
|
302
|
+
})));
|
|
303
|
+
};
|
|
304
|
+
timer = setTimeout(finish, TIMEOUT_MS);
|
|
305
|
+
sock.on("error", finish);
|
|
306
|
+
sock.on("message", (msg, rinfo) => {
|
|
307
|
+
if (msg.length > MAX_SSDP || !gateways.includes(rinfo.address)) return;
|
|
308
|
+
const location = /^location:\s*(\S+)/im.exec(msg.toString())?.[1];
|
|
309
|
+
if (!location || !trustedUrl(location, rinfo.address)) return;
|
|
310
|
+
if (!found.size) {
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
timer = setTimeout(finish, 300);
|
|
313
|
+
}
|
|
314
|
+
found.set(location, rinfo.address);
|
|
315
|
+
});
|
|
316
|
+
sock.bind(0, () => {
|
|
317
|
+
const targets = OVERRIDE ? [OVERRIDE.host] : [GROUP, ...gateways];
|
|
318
|
+
try {
|
|
319
|
+
if (!OVERRIDE) sock.setMulticastTTL(2);
|
|
320
|
+
} catch {}
|
|
321
|
+
for (const host of targets) for (const st of SEARCH) {
|
|
322
|
+
const req = `M-SEARCH * HTTP/1.1\r\nHOST: ${host}:${SSDP_PORT}\r\nMAN: "ssdp:discover"\r\nMX: 2\r\nST: ${st}\r\n\r\n`;
|
|
323
|
+
sock.send(req, SSDP_PORT, host);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/** Where the default gateways come from when /proc/net/route does not
|
|
329
|
+
* say (macOS, whose `route` has to be run): this module runs in direct
|
|
330
|
+
* connections' helper, which never starts a process (helper.ts), so the
|
|
331
|
+
* server runs it and answers. */
|
|
332
|
+
var gatewayLookup = async () => [];
|
|
333
|
+
function setGatewayLookup(fn) {
|
|
334
|
+
gatewayLookup = fn;
|
|
335
|
+
}
|
|
336
|
+
/** The IPv4 default gateways on the LAN: Linux's routing table, else what
|
|
337
|
+
* the server's `route` says. */
|
|
338
|
+
async function defaultGateways() {
|
|
339
|
+
const lan = (ips) => ips.filter((ip) => privateV4(ip));
|
|
340
|
+
try {
|
|
341
|
+
return lan(fs.readFileSync("/proc/net/route", "utf8").split("\n").slice(1).map((line) => line.trim().split(/\s+/)).filter((f) => f[1] === "00000000" && /^[0-9A-F]{8}$/i.test(f[2] ?? "")).map((f) => [
|
|
342
|
+
3,
|
|
343
|
+
2,
|
|
344
|
+
1,
|
|
345
|
+
0
|
|
346
|
+
].map((i) => parseInt(f[2].slice(i * 2, i * 2 + 2), 16)).join(".")));
|
|
347
|
+
} catch {}
|
|
348
|
+
return lan((await gatewayLookup().catch(() => [])).filter(net.isIPv4));
|
|
349
|
+
}
|
|
350
|
+
/** Whether `url` is one to fetch from the gateway at `host`: http(s), at
|
|
351
|
+
* that very address, which is a LAN one. */
|
|
352
|
+
function trustedUrl(url, host) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = new URL(url);
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.hostname === host && lanAddress(host);
|
|
360
|
+
}
|
|
361
|
+
function lanAddress(host) {
|
|
362
|
+
if (!net.isIPv4(host)) return false;
|
|
363
|
+
if (OVERRIDE && host === OVERRIDE.host && /^127\./.test(host)) return true;
|
|
364
|
+
return privateV4(host);
|
|
365
|
+
}
|
|
366
|
+
/** The gateway's WAN connection service, from its description at
|
|
367
|
+
* `location`. Its control URL is taken relative to that address and must
|
|
368
|
+
* stay on it: a description's `URLBase` or an absolute control URL could
|
|
369
|
+
* send the requests anywhere. */
|
|
370
|
+
async function describeGateway(location, host) {
|
|
371
|
+
const res = await fetch(location, {
|
|
372
|
+
redirect: "error",
|
|
373
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
374
|
+
});
|
|
375
|
+
const xml = await readCapped(res, MAX_XML);
|
|
376
|
+
if (!res.ok) throw new Error(`${location}: HTTP ${res.status}`);
|
|
377
|
+
const services = [...xml.matchAll(/<service>([\s\S]*?)<\/service>/gi)].map((m) => ({
|
|
378
|
+
type: tag(m[1], "serviceType"),
|
|
379
|
+
control: tag(m[1], "controlURL")
|
|
380
|
+
}));
|
|
381
|
+
for (const type of SERVICES) {
|
|
382
|
+
const s = services.find((x) => x.type === type && x.control);
|
|
383
|
+
if (!s) continue;
|
|
384
|
+
let control;
|
|
385
|
+
try {
|
|
386
|
+
control = new URL(s.control, location).toString();
|
|
387
|
+
} catch {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!trustedUrl(control, host)) throw new Error(`${host}'s description points its control elsewhere (${control})`);
|
|
391
|
+
return {
|
|
392
|
+
control,
|
|
393
|
+
service: type,
|
|
394
|
+
host,
|
|
395
|
+
found: Date.now()
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
throw new Error(`${location} offers no WAN connection service`);
|
|
399
|
+
}
|
|
400
|
+
/** A response's text, refused past `max` bytes. */
|
|
401
|
+
async function readCapped(res, max) {
|
|
402
|
+
const reader = res.body?.getReader();
|
|
403
|
+
if (!reader) return "";
|
|
404
|
+
const chunks = [];
|
|
405
|
+
let size = 0;
|
|
406
|
+
for (;;) {
|
|
407
|
+
const { done, value } = await reader.read();
|
|
408
|
+
if (done) break;
|
|
409
|
+
size += value.length;
|
|
410
|
+
if (size > max) {
|
|
411
|
+
await reader.cancel().catch(() => {});
|
|
412
|
+
throw new Error(`the gateway's answer is over ${max} bytes`);
|
|
413
|
+
}
|
|
414
|
+
chunks.push(value);
|
|
415
|
+
}
|
|
416
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
417
|
+
}
|
|
418
|
+
/** This machine's address on the way to `host`: the one the router
|
|
419
|
+
* forwards to. A connected UDP socket sends nothing. */
|
|
420
|
+
function localAddressFor(host) {
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const sock = dgram.createSocket("udp4");
|
|
423
|
+
sock.on("error", (err) => {
|
|
424
|
+
sock.close();
|
|
425
|
+
reject(err);
|
|
426
|
+
});
|
|
427
|
+
sock.connect(9, host, () => {
|
|
428
|
+
try {
|
|
429
|
+
resolve(sock.address().address);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
reject(err);
|
|
432
|
+
} finally {
|
|
433
|
+
sock.close();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function tag(xml, name) {
|
|
439
|
+
const m = new RegExp(`<(?:\\w+:)?${name}>([^<]*)</(?:\\w+:)?${name}>`, "i").exec(xml);
|
|
440
|
+
return m ? unescapeXml(m[1].trim()) : "";
|
|
441
|
+
}
|
|
442
|
+
function escapeXml(s) {
|
|
443
|
+
return s.replace(/[<>&'"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
444
|
+
}
|
|
445
|
+
function unescapeXml(s) {
|
|
446
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
447
|
+
}
|
|
448
|
+
var PRIVATE = new net.BlockList();
|
|
449
|
+
for (const [ip, bits] of [
|
|
450
|
+
["10.0.0.0", 8],
|
|
451
|
+
["172.16.0.0", 12],
|
|
452
|
+
["192.168.0.0", 16],
|
|
453
|
+
["100.64.0.0", 10],
|
|
454
|
+
["169.254.0.0", 16]
|
|
455
|
+
]) PRIVATE.addSubnet(ip, bits, "ipv4");
|
|
456
|
+
function privateV4(ip) {
|
|
457
|
+
return net.isIPv4(ip) && PRIVATE.check(ip, "ipv4");
|
|
458
|
+
}
|
|
459
|
+
function hostPort(value) {
|
|
460
|
+
const m = /^\[?([^\]]+?)\]?:(\d+)$/.exec(value?.trim() || "");
|
|
461
|
+
return m ? {
|
|
462
|
+
host: m[1],
|
|
463
|
+
port: Number(m[2])
|
|
464
|
+
} : null;
|
|
465
|
+
}
|
|
466
|
+
function randomInt(n) {
|
|
467
|
+
return Math.floor(Math.random() * n);
|
|
468
|
+
}
|
|
469
|
+
function describe(err) {
|
|
470
|
+
if (err instanceof Error) {
|
|
471
|
+
if (err.name === "TimeoutError") return "the router did not answer";
|
|
472
|
+
return err.message;
|
|
473
|
+
}
|
|
474
|
+
return String(err);
|
|
475
|
+
}
|
|
476
|
+
var HEAD = 9;
|
|
477
|
+
/** Nothing either end sends comes near this: a larger length is a pipe
|
|
478
|
+
* out of step, and reading on would misread everything after it. */
|
|
479
|
+
var MAX_FRAME = 67108864;
|
|
480
|
+
/** One end of the pipe. */
|
|
481
|
+
var Wire = class {
|
|
482
|
+
sock;
|
|
483
|
+
onFrame;
|
|
484
|
+
pending = null;
|
|
485
|
+
corked = false;
|
|
486
|
+
broken = false;
|
|
487
|
+
constructor(sock, onFrame) {
|
|
488
|
+
this.sock = sock;
|
|
489
|
+
this.onFrame = onFrame;
|
|
490
|
+
sock.on("data", (data) => this.read(data));
|
|
491
|
+
}
|
|
492
|
+
send(kind, chan, payload) {
|
|
493
|
+
if (this.broken || this.sock.destroyed) return;
|
|
494
|
+
const head = Buffer.allocUnsafe(HEAD);
|
|
495
|
+
head.writeUInt32LE(payload.length + HEAD - 4, 0);
|
|
496
|
+
head.writeUInt8(kind, 4);
|
|
497
|
+
head.writeUInt32LE(chan, 5);
|
|
498
|
+
if (!this.corked) {
|
|
499
|
+
this.corked = true;
|
|
500
|
+
this.sock.cork();
|
|
501
|
+
process.nextTick(() => {
|
|
502
|
+
this.corked = false;
|
|
503
|
+
this.sock.uncork();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
this.sock.write(head);
|
|
507
|
+
this.sock.write(payload);
|
|
508
|
+
}
|
|
509
|
+
message(msg) {
|
|
510
|
+
this.send(0, 0, Buffer.from(JSON.stringify(msg)));
|
|
511
|
+
}
|
|
512
|
+
read(data) {
|
|
513
|
+
let buf = this.pending ? Buffer.concat([this.pending, data]) : data;
|
|
514
|
+
while (buf.length >= 4) {
|
|
515
|
+
const size = buf.readUInt32LE(0);
|
|
516
|
+
if (size < 5 || size > MAX_FRAME) {
|
|
517
|
+
this.broken = true;
|
|
518
|
+
this.sock.destroy(/* @__PURE__ */ new Error(`a frame of ${size} bytes`));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (buf.length < size + 4) break;
|
|
522
|
+
const kind = buf.readUInt8(4);
|
|
523
|
+
const chan = buf.readUInt32LE(5);
|
|
524
|
+
const payload = buf.subarray(HEAD, size + 4);
|
|
525
|
+
buf = buf.subarray(size + 4);
|
|
526
|
+
this.onFrame(kind, chan, payload);
|
|
527
|
+
}
|
|
528
|
+
this.pending = buf.length ? buf : null;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
/** `host 192.168.0.5:7171`: a candidate as the log and the page show it. */
|
|
532
|
+
function side(c) {
|
|
533
|
+
const host = c.address?.includes(":") ? `[${c.address}]` : c.address;
|
|
534
|
+
return `${c.type ?? "?"} ${host}:${c.port}`;
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/server/rtc/helper.ts
|
|
538
|
+
/**
|
|
539
|
+
* Direct connections' network half, a process of its own that the server
|
|
540
|
+
* starts at a page's first offer (link.ts): node-datachannel, the UDP
|
|
541
|
+
* port, every PeerConnection and DataChannel, the compression of what
|
|
542
|
+
* terminals send down them, and the router port (upnp.ts). The server
|
|
543
|
+
* keeps the rest — the signalling on its events sockets and the access
|
|
544
|
+
* and Origin checks they passed, the path verdict, the firewall, and the
|
|
545
|
+
* terminals themselves — and talks to this over a pipe (wire.ts): each
|
|
546
|
+
* terminal's DataChannel is a channel number there, which the server hands
|
|
547
|
+
* term-ws as a TermChannel.
|
|
548
|
+
*
|
|
549
|
+
* It never starts a process, and that is why it is one. libjuice opens the
|
|
550
|
+
* ICE socket without close-on-exec, so whatever is started from the
|
|
551
|
+
* process holding it inherits the port: a tmux server started from the
|
|
552
|
+
* server kept it bound across the server's restart, and the next bind
|
|
553
|
+
* failed (EADDRINUSE) for as long as tmux ran. Here nothing inherits it,
|
|
554
|
+
* and the port is free the moment this exits — so it can exit when idle.
|
|
555
|
+
* The spawning functions are replaced below so a change that starts one
|
|
556
|
+
* fails at once rather than leaking the socket; the gateway that macOS
|
|
557
|
+
* only names through `route`, the server looks up (`gateways?`).
|
|
558
|
+
*
|
|
559
|
+
* It exits when the pipe closes (the server stopped or was killed), on
|
|
560
|
+
* SIGTERM or SIGINT, when the server says so, and after IDLE_MS with no
|
|
561
|
+
* peer — each time giving back a router port it holds first. A native
|
|
562
|
+
* crash here costs the pages their peers, not the server: the server
|
|
563
|
+
* sees the pipe close, tells each page, and their terminals resume on
|
|
564
|
+
* their websockets.
|
|
565
|
+
*/
|
|
566
|
+
for (const name of [
|
|
567
|
+
"spawn",
|
|
568
|
+
"spawnSync",
|
|
569
|
+
"exec",
|
|
570
|
+
"execSync",
|
|
571
|
+
"execFile",
|
|
572
|
+
"execFileSync",
|
|
573
|
+
"fork"
|
|
574
|
+
]) childProcess[name] = () => {
|
|
575
|
+
throw new Error("direct connections' helper never starts a process: it would inherit the UDP port");
|
|
576
|
+
};
|
|
577
|
+
syncBuiltinESMExports();
|
|
578
|
+
/** A page pings every few seconds while it is open: one silent this long
|
|
579
|
+
* is gone (a phone put away, a laptop shut), and its terminals' attaches
|
|
580
|
+
* are held for it as a lost socket's are. */
|
|
581
|
+
var SILENT_MS = 6e4;
|
|
582
|
+
/** A peer that has not connected in this long never will. */
|
|
583
|
+
var CONNECT_MS = 3e4;
|
|
584
|
+
/** What this end accepts in one message, and so what a page may send. */
|
|
585
|
+
var MAX_MESSAGE = 262144;
|
|
586
|
+
/** Output is sent in pieces of at most this many characters (48 KiB of
|
|
587
|
+
* UTF-8 at worst), well under what any browser accepts in one message; a
|
|
588
|
+
* compressor's chunks are no larger. */
|
|
589
|
+
var PIECE = 16384;
|
|
590
|
+
/** How often a channel whose output is still queued says how much. */
|
|
591
|
+
var REPORT_MS = 50;
|
|
592
|
+
var pipe = new net.Socket({
|
|
593
|
+
fd: 3,
|
|
594
|
+
readable: true,
|
|
595
|
+
writable: true
|
|
596
|
+
});
|
|
597
|
+
var wire = new Wire(pipe, frame);
|
|
598
|
+
var peers = /* @__PURE__ */ new Map();
|
|
599
|
+
var chans = /* @__PURE__ */ new Map();
|
|
600
|
+
/** A page's candidates that came before its peer was made. */
|
|
601
|
+
var early = /* @__PURE__ */ new Map();
|
|
602
|
+
var ndc = null;
|
|
603
|
+
var port = 0;
|
|
604
|
+
var stun = [];
|
|
605
|
+
var idleMs = 6e5;
|
|
606
|
+
var chanCount = 0;
|
|
607
|
+
/** When it last had a peer, or an offer. */
|
|
608
|
+
var busyAt = Date.now();
|
|
609
|
+
var leaving = false;
|
|
610
|
+
/** The port's mux, held for this process's life: a peer of no page's,
|
|
611
|
+
* gathered on the port and never answered, whose ICE agent keeps the mux
|
|
612
|
+
* registered. Left to itself the mux closes with its last peer, and a
|
|
613
|
+
* peer made while it closes cannot bind the port. */
|
|
614
|
+
var anchor = null;
|
|
615
|
+
function say(msg) {
|
|
616
|
+
wire.message(msg);
|
|
617
|
+
}
|
|
618
|
+
pipe.on("error", () => {});
|
|
619
|
+
pipe.on("close", () => void leave("the server went"));
|
|
620
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => void leave(signal));
|
|
621
|
+
function frame(kind, chan, payload) {
|
|
622
|
+
if (kind !== 0) return chans.get(chan)?.output(kind, payload);
|
|
623
|
+
let msg;
|
|
624
|
+
try {
|
|
625
|
+
msg = JSON.parse(payload.toString());
|
|
626
|
+
} catch {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
switch (msg.t) {
|
|
630
|
+
case "init":
|
|
631
|
+
start(msg);
|
|
632
|
+
return;
|
|
633
|
+
case "offer":
|
|
634
|
+
offer(msg);
|
|
635
|
+
return;
|
|
636
|
+
case "candidate": {
|
|
637
|
+
const peer = peers.get(msg.id);
|
|
638
|
+
if (!peer) {
|
|
639
|
+
const list = early.get(msg.id);
|
|
640
|
+
if (list && list.length < 64) list.push(msg);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
peer.pc.addRemoteCandidate(msg.candidate, msg.mid);
|
|
645
|
+
} catch {}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
case "close":
|
|
649
|
+
closePeer(msg.id, null);
|
|
650
|
+
return;
|
|
651
|
+
case "end":
|
|
652
|
+
chans.get(msg.chan)?.close(msg.code);
|
|
653
|
+
return;
|
|
654
|
+
case "drop":
|
|
655
|
+
chans.get(msg.chan)?.terminate();
|
|
656
|
+
return;
|
|
657
|
+
case "upnp.release":
|
|
658
|
+
upnpRelease();
|
|
659
|
+
return;
|
|
660
|
+
case "gateways":
|
|
661
|
+
lookups.get(msg.n)?.(msg.ips);
|
|
662
|
+
return;
|
|
663
|
+
case "shutdown":
|
|
664
|
+
leave("the server is stopping");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
var lookups = /* @__PURE__ */ new Map();
|
|
669
|
+
var lookupCount = 0;
|
|
670
|
+
setGatewayLookup(() => new Promise((resolve) => {
|
|
671
|
+
const n = ++lookupCount;
|
|
672
|
+
const done = (ips) => {
|
|
673
|
+
lookups.delete(n);
|
|
674
|
+
resolve(ips);
|
|
675
|
+
};
|
|
676
|
+
lookups.set(n, done);
|
|
677
|
+
say({
|
|
678
|
+
t: "gateways?",
|
|
679
|
+
n
|
|
680
|
+
});
|
|
681
|
+
setTimeout(() => done([]), 3e3).unref();
|
|
682
|
+
}));
|
|
683
|
+
onUpnpStatus((status) => say({
|
|
684
|
+
t: "upnp",
|
|
685
|
+
status
|
|
686
|
+
}));
|
|
687
|
+
/** node-datachannel and the port, then ready; either failing is said, and
|
|
688
|
+
* this exits (the server starts another at a later offer). */
|
|
689
|
+
async function start(init) {
|
|
690
|
+
port = init.port;
|
|
691
|
+
stun = init.stun;
|
|
692
|
+
idleMs = init.idleMs;
|
|
693
|
+
try {
|
|
694
|
+
ndc = await import("node-datachannel");
|
|
695
|
+
} catch (err) {
|
|
696
|
+
say({
|
|
697
|
+
t: "failed",
|
|
698
|
+
error: message(err),
|
|
699
|
+
unavailable: true
|
|
700
|
+
});
|
|
701
|
+
return leave("node-datachannel did not load");
|
|
702
|
+
}
|
|
703
|
+
if (init.log) ndc.initLogger("Debug", (level, text) => console.error(`[rtc ${level}] ${text}`));
|
|
704
|
+
ndc.setSctpSettings({
|
|
705
|
+
sendBufferSize: 262144,
|
|
706
|
+
recvBufferSize: 262144
|
|
707
|
+
});
|
|
708
|
+
try {
|
|
709
|
+
anchor = await hold(ndc);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
say({
|
|
712
|
+
t: "failed",
|
|
713
|
+
error: message(err),
|
|
714
|
+
bind: true
|
|
715
|
+
});
|
|
716
|
+
return leave("the port could not be bound");
|
|
717
|
+
}
|
|
718
|
+
say({ t: "ready" });
|
|
719
|
+
setInterval(() => {
|
|
720
|
+
if (!peers.size && Date.now() - busyAt > idleMs) leave("idle");
|
|
721
|
+
}, Math.min(6e4, Math.max(1e3, idleMs / 4))).unref();
|
|
722
|
+
}
|
|
723
|
+
function hold(lib) {
|
|
724
|
+
return new Promise((resolve, reject) => {
|
|
725
|
+
const pc = new lib.PeerConnection("work-port", {
|
|
726
|
+
iceServers: [],
|
|
727
|
+
enableIceUdpMux: true,
|
|
728
|
+
portRangeBegin: port,
|
|
729
|
+
portRangeEnd: port
|
|
730
|
+
});
|
|
731
|
+
let settled = false;
|
|
732
|
+
const done = () => {
|
|
733
|
+
if (settled) return;
|
|
734
|
+
settled = true;
|
|
735
|
+
resolve(pc);
|
|
736
|
+
};
|
|
737
|
+
pc.onGatheringStateChange((state) => {
|
|
738
|
+
if (state === "complete") done();
|
|
739
|
+
});
|
|
740
|
+
setTimeout(done, 1e3);
|
|
741
|
+
try {
|
|
742
|
+
pc.createDataChannel("hold");
|
|
743
|
+
} catch (err) {
|
|
744
|
+
settled = true;
|
|
745
|
+
try {
|
|
746
|
+
pc.close();
|
|
747
|
+
} catch {}
|
|
748
|
+
reject(/* @__PURE__ */ new Error(`UDP port ${port} could not be bound (${message(err)})`));
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
async function offer(msg) {
|
|
753
|
+
const { id, sdp } = msg;
|
|
754
|
+
if (!ndc || leaving) return;
|
|
755
|
+
busyAt = Date.now();
|
|
756
|
+
early.set(id, early.get(id) ?? []);
|
|
757
|
+
if (msg.usesPort) await Promise.race([upnpWant(port, portInUse), new Promise((r) => setTimeout(r, 2500))]);
|
|
758
|
+
if (msg.needsPort) say({
|
|
759
|
+
t: "port",
|
|
760
|
+
id
|
|
761
|
+
});
|
|
762
|
+
const queued = early.get(id);
|
|
763
|
+
early.delete(id);
|
|
764
|
+
if (!queued) return;
|
|
765
|
+
closePeer(id, null);
|
|
766
|
+
let pc;
|
|
767
|
+
try {
|
|
768
|
+
pc = new ndc.PeerConnection(`work-${id}`, {
|
|
769
|
+
iceServers: stun,
|
|
770
|
+
enableIceUdpMux: true,
|
|
771
|
+
portRangeBegin: port,
|
|
772
|
+
portRangeEnd: port,
|
|
773
|
+
maxMessageSize: MAX_MESSAGE
|
|
774
|
+
});
|
|
775
|
+
} catch (err) {
|
|
776
|
+
say({
|
|
777
|
+
t: "closed",
|
|
778
|
+
id,
|
|
779
|
+
why: message(err)
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const now = Date.now();
|
|
784
|
+
const peer = {
|
|
785
|
+
id,
|
|
786
|
+
pc,
|
|
787
|
+
state: "new",
|
|
788
|
+
channels: /* @__PURE__ */ new Set(),
|
|
789
|
+
heard: now,
|
|
790
|
+
born: now,
|
|
791
|
+
usesPort: msg.usesPort,
|
|
792
|
+
timer: setInterval(() => {
|
|
793
|
+
if (peer.state !== "connected" && Date.now() - peer.born > CONNECT_MS) closePeer(id, "never connected");
|
|
794
|
+
else if (Date.now() - peer.heard > SILENT_MS) closePeer(id, "silent: its page stopped pinging");
|
|
795
|
+
}, 5e3)
|
|
796
|
+
};
|
|
797
|
+
peers.set(id, peer);
|
|
798
|
+
pc.onLocalDescription((answer, type) => {
|
|
799
|
+
say({
|
|
800
|
+
t: "answer",
|
|
801
|
+
id,
|
|
802
|
+
sdp: answer,
|
|
803
|
+
type
|
|
804
|
+
});
|
|
805
|
+
const ext = msg.routerCandidate ? upnpCandidate() : null;
|
|
806
|
+
if (ext) say({
|
|
807
|
+
t: "candidate",
|
|
808
|
+
id,
|
|
809
|
+
candidate: `candidate:upnp1 1 UDP 1694498815 ${ext.ip} ${ext.port} typ srflx raddr 0.0.0.0 rport 0`,
|
|
810
|
+
mid: /^a=mid:(\S+)/m.exec(sdp)?.[1] ?? "0"
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
pc.onLocalCandidate((candidate, mid) => say({
|
|
814
|
+
t: "candidate",
|
|
815
|
+
id,
|
|
816
|
+
candidate: candidate.replace(/^a=/, ""),
|
|
817
|
+
mid
|
|
818
|
+
}));
|
|
819
|
+
pc.onStateChange((state) => {
|
|
820
|
+
peer.state = state;
|
|
821
|
+
if (state === "connected") setImmediate(() => connected(peer));
|
|
822
|
+
if (state === "failed" || state === "closed") setImmediate(() => closePeer(id, state === "failed" ? "ICE failed" : "closed by the page's end"));
|
|
823
|
+
});
|
|
824
|
+
pc.onDataChannel((dc) => {
|
|
825
|
+
const label = dc.getLabel();
|
|
826
|
+
if (label === "ctl") control(peer, dc);
|
|
827
|
+
else if (label === "term") term(peer, dc);
|
|
828
|
+
else dc.close();
|
|
829
|
+
});
|
|
830
|
+
try {
|
|
831
|
+
pc.setRemoteDescription(sdp, "offer");
|
|
832
|
+
} catch (err) {
|
|
833
|
+
closePeer(id, message(err));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
for (const c of queued) try {
|
|
837
|
+
pc.addRemoteCandidate(c.candidate, c.mid);
|
|
838
|
+
} catch {}
|
|
839
|
+
}
|
|
840
|
+
/** Whether a peer that asked for the router's port is connected. */
|
|
841
|
+
function portInUse() {
|
|
842
|
+
for (const p of peers.values()) if (p.usesPort && p.state === "connected") return true;
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
/** Connected: the pair in use, as this end sees it — the page's address
|
|
846
|
+
* too, which the page itself cannot see (a browser keeps its own host
|
|
847
|
+
* address behind an mDNS name). */
|
|
848
|
+
function connected(peer, tries = 0) {
|
|
849
|
+
if (peers.get(peer.id) !== peer) return;
|
|
850
|
+
let pair = null;
|
|
851
|
+
try {
|
|
852
|
+
pair = peer.pc.getSelectedCandidatePair();
|
|
853
|
+
} catch {}
|
|
854
|
+
if (!pair) {
|
|
855
|
+
if (tries < 5) setTimeout(() => connected(peer, tries + 1), 200);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
say({
|
|
859
|
+
t: "pair",
|
|
860
|
+
id: peer.id,
|
|
861
|
+
page: side(pair.remote),
|
|
862
|
+
server: side(pair.local)
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/** Closed, and said so unless the server asked (`why` null). */
|
|
866
|
+
function closePeer(id, why) {
|
|
867
|
+
early.delete(id);
|
|
868
|
+
const peer = peers.get(id);
|
|
869
|
+
if (!peer) return;
|
|
870
|
+
peers.delete(id);
|
|
871
|
+
if (!peers.size) busyAt = Date.now();
|
|
872
|
+
if (why !== null) say({
|
|
873
|
+
t: "closed",
|
|
874
|
+
id,
|
|
875
|
+
why
|
|
876
|
+
});
|
|
877
|
+
clearInterval(peer.timer);
|
|
878
|
+
for (const chan of peer.channels) chan.lost();
|
|
879
|
+
try {
|
|
880
|
+
peer.pc.close();
|
|
881
|
+
} catch {}
|
|
882
|
+
}
|
|
883
|
+
/** The page's pings: echoed as they came, from here (the server is not
|
|
884
|
+
* asked), and heard as its liveness. */
|
|
885
|
+
function control(peer, dc) {
|
|
886
|
+
dc.onMessage((msg) => {
|
|
887
|
+
peer.heard = Date.now();
|
|
888
|
+
if (typeof msg !== "string") return;
|
|
889
|
+
try {
|
|
890
|
+
dc.sendMessage(msg);
|
|
891
|
+
} catch {}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/** A terminal: its first message is the /term path, which the server is
|
|
895
|
+
* handed with a channel number; from then on its frames go both ways. */
|
|
896
|
+
function term(peer, dc) {
|
|
897
|
+
let chan = null;
|
|
898
|
+
dc.onMessage((msg) => {
|
|
899
|
+
peer.heard = Date.now();
|
|
900
|
+
if (chan) return chan.receive(msg);
|
|
901
|
+
if (typeof msg !== "string" || !msg.startsWith("/term/")) {
|
|
902
|
+
dc.close();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const made = new Chan(++chanCount, peer, dc, codecOf(msg));
|
|
906
|
+
chan = made;
|
|
907
|
+
say({
|
|
908
|
+
t: "open",
|
|
909
|
+
chan: made.id,
|
|
910
|
+
peer: peer.id,
|
|
911
|
+
path: msg
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
dc.onClosed(() => chan?.lost());
|
|
915
|
+
}
|
|
916
|
+
function codecOf(path) {
|
|
917
|
+
const asked = new URLSearchParams(path.split("?")[1] ?? "").get("codec");
|
|
918
|
+
return asked === "brotli" || asked === "deflate-raw" ? asked : null;
|
|
919
|
+
}
|
|
920
|
+
/** One compressor for a channel's whole life, flushed at every message so
|
|
921
|
+
* nothing waits on the next: a terminal's output repeats itself across
|
|
922
|
+
* frames (a redrawn status line, a prompt), which a running context finds
|
|
923
|
+
* and a per-frame one cannot. Brotli at quality 5 with a 1 MiB window
|
|
924
|
+
* (lgwin 20: 22 made no recording smaller); deflate at its default level
|
|
925
|
+
* and full 32 KiB window. */
|
|
926
|
+
function compressor(codec) {
|
|
927
|
+
const c = zlib.constants;
|
|
928
|
+
return codec === "brotli" ? zlib.createBrotliCompress({
|
|
929
|
+
flush: c.BROTLI_OPERATION_FLUSH,
|
|
930
|
+
chunkSize: PIECE,
|
|
931
|
+
params: {
|
|
932
|
+
[c.BROTLI_PARAM_QUALITY]: 5,
|
|
933
|
+
[c.BROTLI_PARAM_LGWIN]: 20,
|
|
934
|
+
[c.BROTLI_PARAM_MODE]: c.BROTLI_MODE_TEXT
|
|
935
|
+
}
|
|
936
|
+
}) : zlib.createDeflateRaw({
|
|
937
|
+
flush: c.Z_SYNC_FLUSH,
|
|
938
|
+
windowBits: 15,
|
|
939
|
+
memLevel: 8,
|
|
940
|
+
chunkSize: PIECE
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/** Marks a message of the server's own (JSON) in a compressed stream: a
|
|
944
|
+
* byte UTF-8 never holds, so output needs no framing around it. */
|
|
945
|
+
var MARK = 255;
|
|
946
|
+
/** Channels whose queued output the server has not heard the last of. */
|
|
947
|
+
var reporting = /* @__PURE__ */ new Set();
|
|
948
|
+
var reportTimer;
|
|
949
|
+
var reportSoon = false;
|
|
950
|
+
/**
|
|
951
|
+
* A terminal's DataChannel, the network end of the server's TermChannel
|
|
952
|
+
* for it (index.ts `ipcChannel`). A DataChannel has no close code, so each
|
|
953
|
+
* end says its code in a last message (`{bye}`) before closing; one that
|
|
954
|
+
* closes without a word was lost. It has no pings either: its liveness is
|
|
955
|
+
* the peer's (ICE state, and the page's pings on its control channel).
|
|
956
|
+
*
|
|
957
|
+
* With a codec, everything the server sends on it is one compressed
|
|
958
|
+
* stream (keys up from the page are not: they are small, and the page's
|
|
959
|
+
* CPU is the scarcer): output as its UTF-8, the server's own messages each
|
|
960
|
+
* between two MARKs, so the page reads them back in the one order they
|
|
961
|
+
* were sent. The stream sits under the attach's resume: offsets and the
|
|
962
|
+
* replay are characters, and a new channel starts a new stream. It ends
|
|
963
|
+
* properly before a `bye`'d close, so the page can tell the end from a
|
|
964
|
+
* loss. Without one (a browser with no DecompressionStream), output is
|
|
965
|
+
* text frames and the server's messages binary, as on a websocket.
|
|
966
|
+
*
|
|
967
|
+
* The server's side of it counts what it sent and hears back what was
|
|
968
|
+
* taken and what is still queued here (`drain`), so the attach's read
|
|
969
|
+
* pauses on a slow page as it would on a slow websocket.
|
|
970
|
+
*/
|
|
971
|
+
var Chan = class {
|
|
972
|
+
id;
|
|
973
|
+
peer;
|
|
974
|
+
dc;
|
|
975
|
+
packed;
|
|
976
|
+
code = null;
|
|
977
|
+
done = false;
|
|
978
|
+
closing = false;
|
|
979
|
+
/** Output bytes taken off the pipe. */
|
|
980
|
+
taken = 0;
|
|
981
|
+
constructor(id, peer, dc, codec) {
|
|
982
|
+
this.id = id;
|
|
983
|
+
this.peer = peer;
|
|
984
|
+
this.dc = dc;
|
|
985
|
+
chans.set(id, this);
|
|
986
|
+
peer.channels.add(this);
|
|
987
|
+
this.packed = codec ? compressor(codec) : null;
|
|
988
|
+
this.packed?.on("data", (chunk) => {
|
|
989
|
+
try {
|
|
990
|
+
if (dc.isOpen()) dc.sendMessageBinary(chunk);
|
|
991
|
+
} catch {}
|
|
992
|
+
this.report();
|
|
993
|
+
});
|
|
994
|
+
this.packed?.on("error", () => this.shut());
|
|
995
|
+
}
|
|
996
|
+
get queued() {
|
|
997
|
+
try {
|
|
998
|
+
return this.dc.bufferedAmount() + (this.packed?.writableLength ?? 0);
|
|
999
|
+
} catch {
|
|
1000
|
+
return 0;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
get open() {
|
|
1004
|
+
if (this.done || this.closing) return false;
|
|
1005
|
+
try {
|
|
1006
|
+
return this.dc.isOpen();
|
|
1007
|
+
} catch {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/** From the server: output (text, its UTF-8) or its own word (binary). */
|
|
1012
|
+
output(kind, payload) {
|
|
1013
|
+
this.taken += payload.length;
|
|
1014
|
+
this.report();
|
|
1015
|
+
if (!this.open) return;
|
|
1016
|
+
if (this.packed) {
|
|
1017
|
+
this.packed.write(kind === 1 ? payload : this.mark(payload));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
if (kind === 2) this.dc.sendMessageBinary(payload);
|
|
1022
|
+
else for (const piece of pieces(payload.toString())) this.dc.sendMessage(piece);
|
|
1023
|
+
} catch {}
|
|
1024
|
+
}
|
|
1025
|
+
/** From the page: keys (binary) or its control (text), or its bye. */
|
|
1026
|
+
receive(msg) {
|
|
1027
|
+
if (this.done) return;
|
|
1028
|
+
if (typeof msg === "string") {
|
|
1029
|
+
const bye = byeOf(msg);
|
|
1030
|
+
if (bye !== null) {
|
|
1031
|
+
this.code = bye;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
wire.send(1, this.id, Buffer.from(msg));
|
|
1035
|
+
} else wire.send(2, this.id, Buffer.isBuffer(msg) ? msg : Buffer.from(msg));
|
|
1036
|
+
}
|
|
1037
|
+
close(code) {
|
|
1038
|
+
if (this.closing || this.done) return;
|
|
1039
|
+
const said = code !== void 0 && this.open;
|
|
1040
|
+
this.closing = true;
|
|
1041
|
+
if (!said) return this.shut();
|
|
1042
|
+
const bye = Buffer.from(JSON.stringify({ bye: code }));
|
|
1043
|
+
if (!this.packed) {
|
|
1044
|
+
try {
|
|
1045
|
+
this.dc.sendMessageBinary(bye);
|
|
1046
|
+
} catch {}
|
|
1047
|
+
return this.shut();
|
|
1048
|
+
}
|
|
1049
|
+
this.packed.once("end", () => this.shut());
|
|
1050
|
+
this.packed.end(this.mark(bye));
|
|
1051
|
+
setTimeout(() => this.shut(), 2e3).unref();
|
|
1052
|
+
}
|
|
1053
|
+
terminate() {
|
|
1054
|
+
this.shut();
|
|
1055
|
+
}
|
|
1056
|
+
/** Gone, with whatever code the page said before it went (1006 when it
|
|
1057
|
+
* said none: lost, and the attach is held). */
|
|
1058
|
+
lost() {
|
|
1059
|
+
if (this.done) return;
|
|
1060
|
+
this.done = true;
|
|
1061
|
+
chans.delete(this.id);
|
|
1062
|
+
reporting.delete(this);
|
|
1063
|
+
this.peer.channels.delete(this);
|
|
1064
|
+
this.packed?.destroy();
|
|
1065
|
+
say({
|
|
1066
|
+
t: "lost",
|
|
1067
|
+
chan: this.id,
|
|
1068
|
+
code: this.code ?? 1006
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
/** Tells the server how far its output has got: at once for the bytes
|
|
1072
|
+
* taken, then every REPORT_MS while any is still queued. */
|
|
1073
|
+
report() {
|
|
1074
|
+
reporting.add(this);
|
|
1075
|
+
if (reportSoon) return;
|
|
1076
|
+
reportSoon = true;
|
|
1077
|
+
setImmediate(sendReports);
|
|
1078
|
+
}
|
|
1079
|
+
shut() {
|
|
1080
|
+
if (this.done) return;
|
|
1081
|
+
try {
|
|
1082
|
+
this.dc.close();
|
|
1083
|
+
} catch {}
|
|
1084
|
+
setImmediate(() => this.lost());
|
|
1085
|
+
}
|
|
1086
|
+
mark(json) {
|
|
1087
|
+
return Buffer.concat([
|
|
1088
|
+
Buffer.of(MARK),
|
|
1089
|
+
json,
|
|
1090
|
+
Buffer.of(MARK)
|
|
1091
|
+
]);
|
|
1092
|
+
}
|
|
1093
|
+
/** What the last report said was queued, to stop reporting a channel
|
|
1094
|
+
* once it said nothing was. */
|
|
1095
|
+
said = -1;
|
|
1096
|
+
drain() {
|
|
1097
|
+
const buffered = this.queued;
|
|
1098
|
+
say({
|
|
1099
|
+
t: "drain",
|
|
1100
|
+
chan: this.id,
|
|
1101
|
+
done: this.taken,
|
|
1102
|
+
buffered
|
|
1103
|
+
});
|
|
1104
|
+
this.said = buffered;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
function sendReports() {
|
|
1108
|
+
reportSoon = false;
|
|
1109
|
+
for (const chan of reporting) {
|
|
1110
|
+
chan.drain();
|
|
1111
|
+
if (chan.said === 0) reporting.delete(chan);
|
|
1112
|
+
}
|
|
1113
|
+
if (reporting.size && !reportTimer) reportTimer = setInterval(() => {
|
|
1114
|
+
if (!reporting.size) {
|
|
1115
|
+
clearInterval(reportTimer);
|
|
1116
|
+
reportTimer = void 0;
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
sendReports();
|
|
1120
|
+
}, REPORT_MS);
|
|
1121
|
+
}
|
|
1122
|
+
function byeOf(text) {
|
|
1123
|
+
if (!text.startsWith("{\"bye\":")) return null;
|
|
1124
|
+
try {
|
|
1125
|
+
const bye = JSON.parse(text).bye;
|
|
1126
|
+
return Number.isInteger(bye) ? bye : null;
|
|
1127
|
+
} catch {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/** `text` in pieces of at most PIECE characters, never splitting a
|
|
1132
|
+
* surrogate pair: each piece is encoded on its own, and half a pair would
|
|
1133
|
+
* arrive as U+FFFD, a character the page's count would not agree with. */
|
|
1134
|
+
function* pieces(text) {
|
|
1135
|
+
if (text.length <= PIECE) {
|
|
1136
|
+
yield text;
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
for (let i = 0; i < text.length;) {
|
|
1140
|
+
let end = Math.min(i + PIECE, text.length);
|
|
1141
|
+
const last = text.charCodeAt(end - 1);
|
|
1142
|
+
if (end < text.length && last >= 55296 && last <= 56319) end--;
|
|
1143
|
+
yield text.slice(i, end);
|
|
1144
|
+
i = end;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/** Closes every peer (their pages fall back to websockets at once rather
|
|
1148
|
+
* than when their paths time out), gives back a router port it holds
|
|
1149
|
+
* (bounded: a router that does not answer cannot hold this up), and
|
|
1150
|
+
* exits, which frees the UDP port. */
|
|
1151
|
+
async function leave(why) {
|
|
1152
|
+
if (leaving) return;
|
|
1153
|
+
leaving = true;
|
|
1154
|
+
for (const id of [...peers.keys()]) closePeer(id, null);
|
|
1155
|
+
try {
|
|
1156
|
+
anchor?.close();
|
|
1157
|
+
} catch {}
|
|
1158
|
+
if (upnpStatus().state !== "off") await Promise.race([upnpRelease().catch(() => {}), new Promise((r) => setTimeout(r, 1500))]);
|
|
1159
|
+
say({
|
|
1160
|
+
t: "exit",
|
|
1161
|
+
why
|
|
1162
|
+
});
|
|
1163
|
+
pipe.end(() => process.exit(0));
|
|
1164
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
1165
|
+
}
|
|
1166
|
+
function message(err) {
|
|
1167
|
+
return err instanceof Error ? err.message : String(err);
|
|
1168
|
+
}
|
|
1169
|
+
//#endregion
|
|
1170
|
+
export {};
|