{"version":3,"sources":["CopyCommand.tsx","LandingPageContent.tsx","constants.tsx","events.tsx","logo.png","TopBar.tsx","ErrorBoundary.tsx","BottomBar.tsx","App.tsx","xtermUtils.tsx","index.tsx","encryption.tsx","utils.tsx","websocketMessageHandler.tsx"],"names":["CopyCommand","props","useState","clicked","setClicked","hovering","setHovering","className","command","text","title","onMouseEnter","onMouseLeave","onClick","setTimeout","LandingPageContent","React","defaultTerminalId","localStorage","getItem","localStorageKeys","terminalId","terminalIdInput","setTerminalIdInput","host","customHostInput","setCustomHostInput","defaultBootstrapb64Key","bootstrapAesKeyB64","bootstrapAesKeyB64Input","setBootstrapAesKeyB64Input","submitForm","a","toast","dark","setItem","isStaticallyHosted","customServer","URL","termpairHttpServer","defaultTermpairServer","getAESKey","Buffer","from","bootstrapKey","termpairWebsocketServer","websocketUrlFromHttpUrl","connectToTerminalAndWebsocket","inputClass","terminalIdInputEl","name","type","onChange","event","target","value","placeholder","bootstrapCryptoKeyInputEl","terminalServerUrlEl","canConnect","length","connectButton","connectForm","onSubmit","e","preventDefault","staticLandingContent","isSecureContext","regularServerContent","termpairShareCommand","pipxTermpairShareCommand","termpairDemoContent","src","frameBorder","allow","allowFullScreen","href","secureContextHelp","TERMPAIR_VERSION","window","location","protocol","hostname","port","pathname","URLSearchParams","search","get","hash","substring","cannotTypeMsg","_port","xterm","Xterm","cursorBlink","macOptionIsMeta","scrollback","getSalt","crypto","getRandomValues","Uint8Array","toString","newBrowserConnected","JSON","stringify","payload","sendCommandToTerminal","secretEncryptionKey","data","messageCount","aesEncrypt","salt","githubLogo","width","height","fill","fillRule","clipRule","d","TopBar","logo","alt","ErrorBoundary","state","hasError","error","errorInfo","console","this","children","Component","BottomBar","connected","status","hasTerminalId","canType","terminalData","allow_browser_control","connectedClients","numClients","startTime","moment","broadcast_start_time_iso","format","terminalDimensions","terminalSize","rows","cols","App","setIsStaticallyHosted","terminalServerData","setTerminalServerData","setNumClients","aesKeys","useRef","browser","unix","ivCount","maxIvCount","xtermWasOpened","webSocket","setWebsocket","showTerminal","setTerminalSize","setStatus","prevStatus","setPrevStatus","setTerminalId","useEffect","fetch","mode","ret","json","pong","isTermpairServer","staticallyHosted","getBootstrapAESKey","termpairServerUrlParam","customTermpairServer","initialize","changeStatus","useLayoutEffect","current","el","document","getElementById","open","writeln","ensureXtermIsOpen","newStatus","toastStatus","_","Error","handleStatusChange","bootstrapAesKey","response","setupWebsocket","String","message","autoClose","close","connectWebsocketUrl","ws","WebSocket","terminal","sendInputToTerminal","onDataDispose","handleNewInput","newInput","send","isIvExhausted","getOnDataHandler","attachCustomKeyEventHandler","ctrlKey","shiftKey","key","toLowerCase","navigator","clipboard","readText","then","toPaste","toCopy","getSelection","writeText","focus","addEventListener","newBrowserMessage","onData","dispose","parse","handlers","new_output","resize","num_clients","aes_keys","aes_key_rotation","default","content","id","position","limit","hideProgressBar","newestOnTop","closeOnClick","rtl","pauseOnFocusLoss","draggable","pauseOnHover","ReactDOM","render","StrictMode","rawKeyData","usages","subtle","importKey","keyData","aesDecrypt","secretcryptoKey","encryptedPayload","iv","subarray","encryptedTerminalOutput","decrypt","decryptedTerminalOutput","ivFromInteger","unshift","set","browserSecretAESKey","utf8Payload","encrypt","TextEncoder","encode","encryptedArrayBuffer","ivAndEncryptedPayload","_combineBuffers","base64EncryptedString","_arrayBufferToBase64","buffer1","buffer2","tmp","byteLength","buffer","bytes","binary","len","i","fromCharCode","btoa","httpUrl","replace","debounce","decryptedJson","decryptedPayload","pty_output","write","b64_bootstrap_unix_aes_key","unixAesKeyData","b64_bootstrap_browser_aes_key","browserAesKeyData","iv_count","max_iv_count","startIvCount","parseInt","b64_aes_secret_unix_key","newUnixAesKeyData","b64_aes_secret_browser_key","newBrowserAesKeyData"],"mappings":"6LAIO,SAASA,EAAYC,GAC1B,MAA8BC,oBAAS,GAAvC,mBAAOC,EAAP,KAAgBC,EAAhB,KACA,EAAgCF,oBAAS,GAAzC,mBAAOG,EAAP,KAAiBC,EAAjB,KACA,OACE,sBAAKC,UAAU,OAAf,UACE,sBACEA,UAAS,UACPF,GAAYF,EAAU,gBAAkB,cADjC,uCADX,SAKGF,EAAMO,UAET,cAAC,IAAD,CAAiBC,KAAMR,EAAMO,QAA7B,SACE,wBACED,UAAU,OACVG,MAAM,4BACNC,aAAc,kBAAML,GAAY,IAChCM,aAAc,kBAAMN,GAAY,IAChCO,QAAS,WACPT,GAAW,GACXU,YAAW,kBAAMV,GAAW,KAAQ,OAPxC,SAUE,cAAC,IAAD,CAAeG,UAAU,2BAG7B,sBAAMA,UAAU,WAAhB,SAA4BJ,EAAU,UAAY,U,4KCdjD,SAASY,EAAmBd,GAS/B,IAAD,MACD,EAA8Ce,IAAMd,SAAN,iBAC5Ce,UAD4C,IAC5CA,QAAqBC,aAAaC,QAAQC,IAAiBC,mBADf,QAC8B,IAD5E,mBAAOC,EAAP,KAAwBC,EAAxB,KAGA,EAA8CP,IAAMd,SAAN,UAC5CgB,aAAaC,QAAQC,IAAiBI,aADM,QACG,IADjD,mBAAOC,EAAP,KAAwBC,EAAxB,KAGA,EAA8DV,IAAMd,SAAN,UAC3DyB,KACCT,aAAaC,QAAQC,IAAiBQ,2BAFoB,QAG1D,IAHJ,mBAAOC,EAAP,KAAgCC,EAAhC,KAMMC,EAAU,uCAAG,kCAAAC,EAAA,yDACZV,EADY,uBAEfW,IAAMC,KAAK,+BAFI,6BAKjBhB,aAAaiB,QAAQf,IAAiBC,WAAYC,GAC7CO,EANY,uBAOfI,IAAMC,KAAK,8BAPI,8BAWbjC,EAAMmC,mBAXO,oBAYVX,EAZU,wBAabQ,IAAMC,KAAK,6BAbE,qCAiBPG,EAAe,IAAIC,IAAIb,GAC7Bc,EAAqBF,EACrBnB,aAAaiB,QAAQf,IAAiBI,KAAMC,GAnB/B,0DAqBbQ,IAAMC,KAAN,UAAcT,EAAd,wBArBa,mDAyBfc,EAAqBC,IAzBN,mCA8BMC,YACnBC,EAAOC,KAAKd,EAAyB,UACrC,CAAC,YAhCY,QA8Bfe,EA9Be,OAkCf1B,aAAaiB,QACXf,IAAiBQ,mBACjBC,GApCa,0DAuCfI,IAAMC,KAAN,sCAvCe,kCA2CXW,EAA0BC,YAAwBP,GA3CvC,UA6CXtC,EAAM8C,8BACVzB,EACAuB,EACAN,EACAK,GAjDe,mEAAH,qDAoDVI,EAAa,4CAEbC,EACJ,sBACE1C,UAAU,oBACVG,MAAM,gGAFR,UAIE,sBAAMH,UAAU,qCAAhB,yBACA,uBACE2C,KAAK,kBACLC,KAAK,OACL5C,UAAWyC,EACXI,SAAU,SAACC,GACT9B,EAAmB8B,EAAMC,OAAOC,QAElCA,MAAOjC,EACPkC,YAAY,sCAIZC,EACJ,sBAAKlD,UAAU,oBAAoBG,MAAM,0BAAzC,UACE,sBAAMH,UAAU,qCAAhB,mCAGA,uBACE2C,KAAK,0BACLM,YAAY,2BACZL,KAAK,OACL5C,UAAWyC,EACXI,SAAU,SAACC,GACTvB,EAA2BuB,EAAMC,OAAOC,QAE1CA,MAAO1B,OAIP6B,EACJ,sBACEnD,UAAU,oBACVG,MAAM,kFAFR,UAIE,sBAAMH,UAAU,qCAAhB,iCAGA,uBACE2C,KAAK,kBACLC,KAAK,OACL5C,UAAWyC,EACXQ,YAAY,wBACZJ,SAAU,SAACC,GACT3B,EAAmB2B,EAAMC,OAAOC,QAElCA,MAAO9B,OAKPkC,IACuB,IAA3BrC,EAAgBsC,QAChB/B,EAAwB+B,OAAS,GACjC3D,EAAMmC,qBACyB,IAA3BX,EAAgBmC,OAGhBC,EACJ,qBAAKtD,UAAU,mBAAf,SACE,wBACE4C,KAAK,SACLzC,MAAM,oCACNH,UAAS,oFACPoD,EAAa,GAAK,sBAJtB,uBAWEG,EACJ,uBACEC,SAAU,SAACC,GACTA,EAAEC,iBACFlC,KAHJ,UAMGkB,EACAQ,EACAxD,EAAMmC,mBAAqBsB,EAAsB,KACjDG,KAGCK,EAAuBjE,EAAMkE,gBACjC,sBAAK5D,UAAU,OAAf,UACE,qBAAKA,UAAU,gBAAf,4CACA,gSAMA,qBAAKA,UAAU,OAAf,mGAICuD,KAED,KAEEM,EACJ,qCACE,sBAAK7D,UAAU,OAAf,UACE,qBAAKA,UAAU,gBAAf,yBACA,mGAGA,cAAC,IAAD,CAAaC,QAAS6D,MACtB,sFACA,cAAC,IAAD,CAAa7D,QAAS8D,SAExB,sBAAK/D,UAAU,OAAf,UACE,qBAAKA,UAAU,gBAAf,8BACA,oDACA,cAAC,IAAD,CAAaC,QAAQ,0BACrB,sDACA,cAAC,IAAD,CAAaA,QAAQ,mCAEvB,sBAAKD,UAAU,OAAf,UACE,qBAAKA,UAAU,gBAAf,uCADF,oKAKGuD,QAKDS,EACJ,sBAAKhE,UAAU,OAAf,UACE,qBAAKA,UAAU,eAAf,2BAEA,qBAAKA,UAAU,yBAAf,SACE,wBACEiE,IAAI,4CACJ9D,MAAM,uBACN+D,YAAY,IACZC,MAAM,2FACNC,iBAAe,SAMvB,OACE,qBAAKpE,UAAU,uCAAf,SACE,sBAAKA,UAAU,gBAAf,UACE,sBAAKA,UAAU,OAAf,UACE,qBAAKA,UAAU,YAAf,kCADF,4IAG2D,IACzD,mBAAGqE,KAAK,mCAAR,4BAEA3E,EAAMkE,gBAKJ,KAJF,sBAAK5D,UAAU,iBAAf,UACE,uCACCsE,OAGyB,OAA7B5E,EAAMmC,mBACH,MAC6B,IAA7BnC,EAAMmC,mBACN8B,EACAE,EAEJ,sBAAK7D,UAAU,QAAf,UACE,qBAAKA,UAAU,iBAAf,6BACA,sBAAKA,UAAU,YAAf,UACE,gCACE,qBAAKA,UAAU,WAAf,qDAGA,yFACyD,IACvD,uBAAMA,UAAU,YAAhB,cAA8BuE,OAFhC,uCAMF,gCACE,qBAAKvE,UAAU,WAAf,wDAGA,gCAAMsE,IAAN,gBAILN,U,wDC5RT,uWAEaO,EAAmB,UAEnBtC,EAAwB,IAAIF,IAAJ,UAChCyC,OAAOC,SAASC,SADgB,aACHF,OAAOC,SAASE,SADb,YACyBH,OAAOC,SAASG,MADzC,OACgDJ,OAAOC,SAASI,WAGxFnE,EAAoB,IAAIoE,gBACnCN,OAAOC,SAASM,QAChBC,IAAI,eAEO5D,EAAyBoD,OAAOC,SAASQ,KAAKC,UACzD,EACAV,OAAOC,SAASQ,KAAK5B,OAAS,GAGnB8B,EACX,kFAEWlE,EAAI,UAAMuD,OAAOC,SAASC,SAAtB,aAAmCF,OAAOC,SAASE,UAAnD,OAA8DH,OAAOC,SAASI,UAC3FO,EAAQZ,OAAOC,SAASG,KACvBJ,OAAOC,SAASG,OAEjBQ,EAD+B,WAA7BZ,OAAOC,SAASC,SACV,MAEA,MAGL,IACMZ,EAAoB,iCAA6B7C,EAA7B,oBAA6CmE,GACjErB,EAAwB,mBAAeD,GAEvCuB,EAAQ,IAAIC,WAAM,CAC7BC,aAAa,EACbC,iBAAiB,EACjBC,WAAY,MAGD5E,EAAmB,CAC9BQ,mBAAoB,6BACpBP,WAAY,qBACZG,KAAM,sBAGKqD,EACX,+IAEkC,yDAFlC,OAEyE,IACvE,mBAAGD,KAAK,wEAAR,mFAEK,IALP,4B,mKC5CF,SAASqB,IACP,OAAOlB,OAAOmB,OAAOC,gBAAgB,IAAIC,WAAW,KAAKC,WAMpD,SAAeC,IAAtB,+B,4CAAO,sBAAAtE,EAAA,+EACEuE,KAAKC,UAAU,CACpBnD,MAAO,wBACPoD,QAAS,MAHN,4C,sBAOA,SAAeC,EAAtB,sC,4CAAO,WACLC,EACAC,EACAC,GAHK,SAAA7E,EAAA,kEAKEuE,KALF,SAOYO,YACbH,EACAJ,KAAKC,UAAU,CAAEI,OAAMG,KAAMd,MAC7BY,GAVC,gCAMHxD,MAAO,UACPoD,QAPG,6BAKOD,UALP,6D,yCChBQ,MAA0B,iC,OCGnCQ,EACJ,qBACEC,MAAM,KACNC,OAAO,KACPC,KAAK,eACL5G,UAAU,sBAJZ,SAME,sBACE6G,SAAS,UACTC,SAAS,UACTC,EAAE,8tBAKD,SAASC,EAAOtH,GACrB,OACE,qBAAKM,UAAU,kBAAf,SACE,sBAAKA,UAAU,0DAAf,UACE,qBAAKA,UAAU,SAAf,SACE,mBAAGqE,KAAMG,OAAOC,SAASI,SAAzB,SACE,qBAAK7E,UAAU,SAASiE,IAAKgD,EAAMC,IAAI,aAG3C,sBAAKlH,UAAU,OAAf,UACE,uBAAMA,UAAU,qBAAhB,cAAuCuE,OACvC,mBAAGF,KAAK,mCAAmClE,MAAM,kBAAjD,SACGsG,Y,oCC5BAU,EAAb,kDACE,WAAYzH,GAAa,IAAD,8BACtB,cAAMA,IACD0H,MAAQ,CAAEC,UAAU,GAFH,EAD1B,qDAWE,SAAkBC,EAAYC,GAG5BC,QAAQF,MAAMA,GACdE,QAAQF,MAAMC,KAflB,oBAkBE,WACE,OAAIE,KAAKL,MAAMC,SAEN,oBAAIrH,UAAU,aAAd,mCAGFyH,KAAK/H,MAAMgI,YAxBtB,uCAME,SAAgCJ,GAE9B,MAAO,CAAED,UAAU,OARvB,GAAmC5G,IAAMkH,W,iBCClC,SAASC,EAAUlI,GAMtB,IAAD,IACKmI,EAA6B,2BAAjBnI,EAAMoI,OAClBC,EAAoC,MAApBrI,EAAMoB,WACtBgH,EAASC,EAAgB,8BAAMrI,EAAMoI,SAAgB,KAErDE,EAAUH,EACd,qBACE1H,MAAM,+KADR,UAKG,UAAAT,EAAMuI,oBAAN,eAAoBC,wBAAyBL,EAC1C,aACA,cAEJ,KAEEM,EAAmBN,EACvB,sBAAK1H,MAAM,sDAAX,UACGT,EAAM0I,WAAa1I,EAAM0I,WAAa,IADzC,0BAGE,KAEEC,EAAYR,EAChB,6CACa,IACVS,IAAM,UAAC5I,EAAMuI,oBAAP,aAAC,EAAoBM,0BAA0BC,OACpD,6BAGF,KAEEC,EAAqBZ,EACzB,sBAAK1H,MAAM,sCAAX,UACGT,EAAMgJ,aAAaC,KADtB,IAC6BjJ,EAAMgJ,aAAaE,QAE9C,KAEJ,OACE,gCACGb,EACC,sBACE/H,UAAS,qDACP6H,EAAY,eAAiB,aADtB,mCADX,UAKGC,EACAW,EACAT,EACAG,EACAE,KAED,KACJ,wBAAQrI,UAAU,mDAAlB,SACE,gCACE,mBAAGqE,KAAK,wBAAR,2BADF,KACsD,IACpD,mBAAGA,KAAK,mCAAR,4B,sBCwSKwE,MA/Pf,WACE,MACElJ,mBAA4B,MAD9B,mBAAOkC,EAAP,KAA2BiH,EAA3B,KAEA,EACEnJ,mBAAuC,MADzC,mBAAOoJ,EAAP,KAA2BC,EAA3B,KAEA,EAAoCrJ,mBAAS,GAA7C,mBAAOyI,EAAP,KAAmBa,EAAnB,KAEMC,EAAUC,iBAAmB,CACjCC,QAAS,KACTC,KAAM,KACNC,QAAS,KACTC,WAAY,OAERC,EAAiBL,kBAAO,GAC9B,EAAkCxJ,mBAA8B,MAAhE,mBAAO8J,EAAP,KAAkBC,EAAlB,KACMC,EAA6B,OAAdF,EACrB,EAAwC9J,mBAAuB,CAC7DgJ,KAAM,GACNC,KAAM,KAFR,mBAAOF,EAAP,KAAqBkB,EAArB,KAIA,EAA4BjK,mBAAiB,MAA7C,mBAAOmI,EAAP,KAAe+B,EAAf,KACA,EAAoClK,mBAAiB,MAArD,mBAAOmK,EAAP,KAAmBC,EAAnB,KACA,EAAoCpK,mBAASe,KAA7C,mBAAOI,EAAP,KAAmBkJ,EAAnB,KAEAC,qBAAU,WACHzF,OAAOZ,gBAKI,uCAAG,8CAAAnC,EAAA,+EAGGyI,MAAMjI,IAAsB6D,WAAa,OAAQ,CACjEqE,KAAM,gBAJO,cAGTC,EAHS,gBAMIA,EAAIC,OANR,OAMTnK,EANS,OAOToK,EAAgB,SAATpK,EACPqK,EAAkC,MAAfH,EAAItC,QAAkBwC,EAE/CxB,EADA0B,GAAoBD,GATL,kDAafzB,EADA0B,GAAmB,GAZJ,yBAeUC,cAfV,WAeXpI,EAfW,OAiBXqI,EAAyB,IAAI5F,gBACjCN,OAAOC,SAASM,QAChBC,IAAI,uBAEA2F,EAAuBD,EACzB,IAAI3I,IAAI2I,GACR,KAEE1I,EAAqBwI,EACvBG,EACA1I,MAEAnB,GAAckB,GAAsBK,GA7BvB,wBA8BTC,EACJC,YAAwBP,GA/BX,UAiCTQ,EACJ1B,EACAwB,EACAN,EACAK,GArCa,0DAAH,oDAyChBuI,GA7CEC,EAAa,gDA+Cd,IAEHC,2BAAgB,YA5FlB,SACEtB,EACAnE,GAEA,IAAImE,EAAeuB,QAAnB,CAGA,IAAMC,EAAKC,SAASC,eAAe,YAC9BF,IAGL3F,EAAM8F,KAAKH,GACXxB,EAAeuB,SAAU,EACzB1F,EAAM+F,QAAN,yDACA/F,EAAM+F,QAAQ,MA+EZC,CAAkB7B,EAAgBnE,OACjC,CAACsE,IAEJ,IAAMkB,EAAe,SAACS,GACpBzB,EAAUyB,GAhKd,SACExD,EACAgC,EACAC,GAGA,OADAA,EAAcjC,GACNA,GACN,KAAK,KACH,MACF,IAAK,yBACHyD,YAAYzD,GACZzC,IAAM+F,QAAQ,mEACd/F,IAAM+F,QACJ,sEAEF/F,IAAM+F,QAAQ,IACd/F,IAAM+F,QACJ,qFAEF/F,IAAM+F,QAAQ,IACd,MACF,IAAK,eACHG,YAAYzD,GACO,2BAAfgC,IACFzE,IAAM+F,QCgBL,+CDfD/F,IAAM+F,QAAQ,KAEhB,MACF,IAAK,yBACH1J,IAAMC,KACJ,8HAGF,MAEF,IAAK,mCACHD,IAAMC,KACJ,yGAGF,MAEF,IAAK,6CACHD,IAAMC,KAAK2C,KACX,MAEF,IAAK,gBAGL,IAAK,mBAGL,IAAK,gCACH,MAEF,SACE,SAAEkH,GACA,MAAMC,MADR,IAyGFC,CAAmBJ,EAAWxB,EAAYC,IAjF/B,SAoFEvH,EApFF,oFAoFb,WACE1B,EACAwB,EACAN,EACA2J,GAJF,iBAAAlK,EAAA,6DAMEuI,EAAclJ,GACdkI,EAAsB,MAPxB,kBAS2BkB,MACrB,IAAInI,IAAJ,mBAAoBjB,GAAckB,GAAoB8D,YAV5D,UAY4B,OAHlB8F,EATV,QAYiB9D,OAZjB,iCAa6C8D,EAASvB,OAbtD,OAaYhE,EAbZ,OAcM2C,EAAsB3C,GACtBwF,GACE/K,EACAuF,EACA/D,EACAqJ,GAnBR,wBAsBMd,EAAa,0BAtBnB,0DAyBIA,EAAa,iCACbnJ,IAAMC,KAAN,4CACuCK,EAAmB8D,WAD1D,gDAC4GgG,OACxG,KAAEC,UAGJ,CAAEC,WAAW,IA/BnB,2DApFa,sBAwHb,SAASH,GACP/K,EACAiI,EACAzG,EACAqJ,GAEIlC,IACF/H,IAAMC,KAAK,+BACX8H,EAAUwC,SAEZpB,EAAa,iBACb,IAAMqB,EAAsB,IAAInK,IAAJ,kDACiBjB,GAC3CwB,GAEI6J,EAAK,IAAIC,UAAUF,EAAoBpG,YAC7C4D,EAAayC,GACb,IChOFE,EACArE,EACAsE,EDsOMC,EAREC,EC9KH,SACLL,EACApD,EACAG,GAEA,8CAAO,WAAOuD,GAAP,SAAAhL,EAAA,mEAE8C,IAA7CsH,EAAmBb,sBAFpB,uBAGDqD,YAAYpG,KAHX,6BAO2B,OAA5B+D,EAAQ6B,QAAQ3B,SACY,OAA5BF,EAAQ6B,QAAQzB,SACe,OAA/BJ,EAAQ6B,QAAQxB,WATf,uBAWD7H,IAAMC,KAAN,6EAXC,sCAgBHwK,EAhBG,UAiBKhG,EACJ+C,EAAQ6B,QAAQ3B,QAChBqD,EACAvD,EAAQ6B,QAAQzB,WApBjB,yBAgBAoD,KAhBA,gBAuBCC,YAAczD,EAAQ6B,QAAQzB,QAASJ,EAAQ6B,QAAQxB,cACzD4C,EAAGO,KNlEF1G,KAAKC,UAAU,CACpBnD,MAAO,0BMkEHoG,EAAQ6B,QAAQxB,YAAc,KAzB7B,kDA4BH7H,IAAMC,KAAN,iDA5BG,0DAAP,sDDyKyBiL,CAAiBT,EAAIpD,EAAoBG,GAChE7D,IAAMwH,6BCjORR,EDmOMhH,IClON2C,EDiO4B,OAEtBe,QAFsB,IAEtBA,OAFsB,EAEtBA,EAAoBb,sBClO1BoE,EDmOME,ECxNN,SAA+B/I,GAC7B,GAAe,YAAXA,EAAEb,KACJ,OAAO,EAET,GAAIa,EAAEqJ,SAAWrJ,EAAEsJ,SAAU,CAC3B,IAAMC,EAAMvJ,EAAEuJ,IAAIC,cAClB,GAAY,MAARD,EACF,OAAKhF,GAILkF,UAAUC,UAAUC,WAAWC,MAAK,SAACC,GACnChB,EAAoBgB,OAEf,IANL/B,YAAYpG,MACL,GAMJ,GAAY,MAAR6H,GAAuB,MAARA,EAAa,CAMrC,IAAMO,EAASlB,EAASmB,eAGxB,OAFAN,UAAUC,UAAUM,UAAUF,GAC9BlB,EAASqB,SACF,GAGX,OAAO,KDiMPvB,EAAGwB,iBAAiB,OAApB,uCAA4B,WAAO7K,GAAP,eAAArB,EAAA,6DAC1BoJ,EAAa,0BACbsB,EAAGO,KLvPA1G,KAAKC,UAAU,CAAEnD,MAAO,iCKqPD,SAGMiD,IAHN,OAGpB6H,EAHoB,OAI1BzB,EAAGO,KAAKkB,GACRrB,EAAgBlH,IAAMwI,OAAOrB,GALH,2CAA5B,uDAOAL,EAAGwB,iBAAiB,SAAS,SAAC7K,GACxByJ,GAEFA,EAAcuB,UAEhBjD,EAAa,gBACb5B,EAAc,MAGhBkD,EAAGwB,iBAAiB,SAAS,SAAC7K,GACxByJ,GAEFA,EAAcuB,UAGhBtG,QAAQF,MAAMxE,GACdpB,IAAMC,KAAN,sCAA0CqE,KAAKC,UAAUnD,KACzD+H,EAAa,oBACb5B,EAAc,MAGhBkD,EAAGwB,iBAAiB,UAApB,uCAA+B,WAAO5B,GAAP,eAAAtK,EAAA,+DAG3B4E,EAAOL,KAAK+H,MAAMhC,EAAQ1F,MAHC,sDAK3B3E,IAAMC,KAAK,qCALgB,+BASrB0E,EAAKvD,MATgB,OAUtB,eAVsB,QAYtB,WAZsB,QActB,gBAdsB,QAgBtB,aAhBsB,QAuBtB,qBAvBsB,QAyBtB,UAzBsB,kDAWlBkL,IAASC,WAAW/E,EAAS7C,IAXX,iCAalB2H,IAASE,OAAO7H,EAAMuD,IAbJ,iCAelBoE,IAASG,YAAYlF,EAAe5C,IAflB,iCAiBlB2H,IAASI,SACdlF,EACAyC,EACAtF,EACAwE,IArBuB,iCAwBlBmD,IAASK,iBAAiBnF,EAAS7C,IAxBjB,iCA0BlB2H,IAAS1G,MAAMjB,IA1BG,eA4BzB,SAAEmF,GACA,MAAMC,MADR,CAEGpF,EAAKvD,OA9BiB,kBA+BlBkL,IAASM,QAAQjI,IA/BC,yDAA/B,uDAoCF,IAAMkI,GACJ,qBAAKvO,UAAU,yCAAf,SACG2J,EACC,qBACE6E,GAAG,WACHxO,UAAS,mDAGX,cAACQ,EAAA,EAAD,CACEoD,gBAAiBY,OAAOZ,gBACxB/B,mBAAoBA,EACpBW,8BAA+BA,MAKvC,OACE,cAAC,EAAD,UACE,sBAAKxC,UAAU,wDAAf,UACE,cAAC,IAAD,CACEyO,SAAS,eACTC,MAAO,EACP1C,UAAW,IACX2C,iBAAiB,EACjBC,aAAa,EACbC,cAAY,EACZC,KAAK,EACLC,kBAAkB,EAClBC,WAAS,EACTC,cAAY,IAEd,cAACjI,EAAD,IACCuH,GACD,cAAC3G,EAAD,CACEK,aAAcc,EACdjB,OAAQA,EACRhH,WAAYA,EACZ4H,aAAcA,EACdN,WAAYA,UE7VtB8G,IAASC,OACP,cAAC,IAAMC,WAAP,UACE,cAAC,EAAD,MAEFnE,SAASC,eAAe,U,wPCKnB,SAAehJ,EAAtB,oC,4CAAO,WACLmN,EACAC,GAFK,SAAA7N,EAAA,sEAIQ+C,OAAOmB,OAAO4J,OAAOC,UAChC,MACAH,EACA,CACE1M,KAAM,YAER,EACA2M,GAXG,oF,sBAeA,SAAe7E,IAAtB,+B,4CAAO,4BAAAhJ,EAAA,kEAEEL,IAFF,yCAGM,MAHN,cAKGqO,EAAUtN,EAAOC,KAAKhB,IAAwB,UALjD,SAMUc,EAAUuN,EAAS,CAAC,YAN9B,+EAQHjI,QAAQF,MAAR,MARG,kBASI,MATJ,0D,sBAaA,SAAeoI,EAAtB,oC,4CAAO,WACLC,EACAC,GAFK,mBAAAnO,EAAA,6DAKCoO,EAAKD,EAAiBE,SAAS,EA1CrB,IA6CVC,EAA0BH,EAAiBE,SA7CjC,IAqCX,KAU2B3N,EAV3B,SAWGqC,OAAOmB,OAAO4J,OAAOS,QACzB,CACErN,KAAM,UACNkN,GAAIA,GAENF,EACAI,GAjBC,0BAUCE,EAVD,KAUkC7N,KAVlC,kCAoBE6N,GApBF,4C,sBAwBP,SAASC,EAAc5G,GACrB,IAAMuG,EAAK,IAAIhK,WA9DC,IA+DVpE,EAAI,GAGV,IAFAA,EAAE0O,QAAkB,IAAV7G,GAEHA,GAAW,KAEhBA,KAAsB,EAEtB7H,EAAE0O,QAAkB,IAAV7G,GAKZ,OADAuG,EAAGO,IAAI3O,GACAoO,EAGF,SAAetJ,EAAtB,sC,4CAAO,WACL8J,EACAC,EACAhH,GAHK,qBAAA7H,EAAA,6DAMCoO,EAAKK,EAAc5G,GANpB,SAO8B9E,OAAOmB,OAAO4J,OAAOgB,QACtD,CACE5N,KAAM,UACNkN,GAAIA,GAENQ,GACA,IAAIG,aAAcC,OAAOH,IAbtB,cAOCI,EAPD,OAgBCC,EAAwBC,EAAgBf,EAAIa,GAE5CG,EAAwBC,EAAqBH,GAlB9C,kBAmBEE,GAnBF,4C,sBAsBA,SAASlE,EAAcrD,EAAiBC,GAC7C,OAAOD,GAAWC,EAGpB,SAASqH,EACPG,EACAC,GAEA,IAAMC,EAAM,IAAIpL,WAAWkL,EAAQG,WAAaF,EAAQE,YAGxD,OAFAD,EAAIb,IAAI,IAAIvK,WAAWkL,GAAU,GACjCE,EAAIb,IAAI,IAAIvK,WAAWmL,GAAUD,EAAQG,YAClCD,EAAIE,OAGb,SAASL,EAAqBK,GAI5B,IAHA,IAAMC,EAAQ,IAAIvL,WAAWsL,GACzBE,EAAS,GACPC,EAAMF,EAAMF,WACTK,EAAI,EAAGA,EAAID,EAAKC,IAEvBF,GAAUvF,OAAO0F,aAAaJ,EAAMG,IAGtC,OAAO/M,OAAOiN,KAAKJ,M,wDChIrB,uFAEO,SAAS9O,EAAwBmP,GACtC,OAAO,IAAI3P,IAAI2P,EAAQ5L,WAAW6L,QAAQ,QAAS,OAG9C,IAAMpG,EAAcqG,oBAAS,SAAC9J,GACnCpG,IAAMC,KAAKmG,KACV,M,sICQUkG,EAAW,CACtBC,WAAW,WAAD,4BAAE,WACV/E,EACA7C,GAFU,mBAAA5E,EAAA,yDAILyH,EAAQ6B,QAAQ1B,KAJX,uBAKR7B,QAAQF,MACN,oEANM,0CAUkBoI,YAC1BxG,EAAQ6B,QAAQ1B,KAChBlH,EAAOC,KAAKiE,EAAKH,QAAS,WAZlB,OAUJ2L,EAVI,OAcJC,EAAmB9L,KAAK+H,MAAM8D,EAAc/L,YAC5CiM,EAAa5P,EAAOC,KAAK0P,EAAiBC,WAAY,UAC5D1M,IAAM2M,MAAMD,GAhBF,2CAAF,qDAAC,GAkBX7D,OAAQ,SACN7H,EACAuD,GAEA,GAAIvD,EAAKH,QAAQ0C,MAAQvC,EAAKH,QAAQyC,KAAM,CAC1C,IAAMC,EAAOvC,EAAKH,QAAQ0C,KACpBD,EAAOtC,EAAKH,QAAQyC,KAC1BiB,EAAgB,CACdhB,OACAD,SAEFtD,IAAM6I,OAAOtF,EAAMD,KAGvBwF,YAAa,SACXlF,EACA5C,GAEA,IAAM8H,EAAc9H,EAAKH,QACzB+C,EAAckF,IAEhBC,SAAS,WAAD,4BAAE,WACRlF,EACAyC,EACAtF,EACAwE,GAJQ,qBAAApJ,EAAA,+EAOuBiO,YAC3B/D,EACAxJ,EAAOC,KAAKiE,EAAKH,QAAQ+L,2BAA4B,WATjD,cAOAC,EAPA,gBAWuBhQ,YAAUgQ,EAAgB,CAAC,YAXlD,cAWNhJ,EAAQ6B,QAAQ1B,KAXV,gBAa0BqG,YAC9B/D,EACAxJ,EAAOC,KAAKiE,EAAKH,QAAQiM,8BAA+B,WAfpD,cAaAC,EAbA,iBAiB0BlQ,YAAUkQ,EAAmB,CAAC,YAjBxD,WAiBNlJ,EAAQ6B,QAAQ3B,QAjBV,OAkBuB,MAAzB/C,EAAKH,QAAQmM,UAAiD,MAA7BhM,EAAKH,QAAQoM,aAlB5C,uBAmBJ9K,QAAQF,MAAM,kCACRmE,MAAM,kCApBR,WAsBA8G,EAAgBrJ,EAAQ6B,QAAQzB,QAAUkJ,SAC9CnM,EAAKH,QAAQmM,SACb,OAGI9I,EAAcL,EAAQ6B,QAAQxB,WAAaiJ,SAC/CnM,EAAKH,QAAQoM,aACb,KAEeC,GA/BX,uBAgCJ/K,QAAQF,MAAR,oDAC+CiL,EAD/C,eACkEhJ,IAElEL,EAAQ6B,QAAR,2BACK7B,EAAQ6B,SADb,IAEE3B,QAAS,KACTG,WAAY,KACZD,QAAS,KACTD,KAAM,OAEFoC,MA1CF,6DA8CuB,MAA3BvC,EAAQ6B,QAAQ3B,SACQ,MAAxBF,EAAQ6B,QAAQ1B,MACW,MAA3BH,EAAQ6B,QAAQzB,SACc,MAA9BJ,EAAQ6B,QAAQxB,WAjDZ,wBAmDJ/B,QAAQF,MAAR,MACAE,QAAQF,MAAMjB,GACdwE,EAAa,oCArDT,6EAAF,yDAAC,GA0DTwD,iBAAiB,WAAD,4BAAE,WAChBnF,EACA7C,GAFgB,iBAAA5E,EAAA,yDAIXyH,EAAQ6B,QAAQ1B,KAJL,uBAKd7B,QAAQF,MAAM,+BALA,mDASkBoI,YAC9BxG,EAAQ6B,QAAQ1B,KAChBhD,EAAKH,QAAQuM,yBAXD,cASRC,EATQ,gBAaqBhD,YACjCxG,EAAQ6B,QAAQ1B,KAChBlH,EAAOC,KAAKiE,EAAKH,QAAQyM,2BAA4B,WAfzC,cAaRC,EAbQ,iBAiBkB1Q,YAAU0Q,EAAsB,CAC9D,YAlBY,eAiBd1J,EAAQ6B,QAAQ3B,QAjBF,iBAoBelH,YAAUwQ,EAAmB,CAAC,YApB7C,QAoBdxJ,EAAQ6B,QAAQ1B,KApBF,yDAuBd7B,QAAQF,MAAR,MACA5F,IAAMC,KAAN,0CAxBc,0DAAF,qDAAC,GA2BjB2F,MAAO,SAAUjB,GACf3E,IAAMC,KAAN,iBAAqB0E,EAAKH,UAC1BsB,QAAQF,MAAMjB,IAEhBiI,QAAS,SAAUjI,GACjB3E,IAAMC,KAAN,kCAAsC0E,EAAKvD,QAC3C0E,QAAQF,MAAM,qBAAsBjB,O","file":"static/js/main.037f56e9.chunk.js","sourcesContent":["import { useState } from \"react\";\nimport CopyToClipboard from \"react-copy-to-clipboard\";\nimport { DuplicateIcon } from \"@heroicons/react/solid\";\n\nexport function CopyCommand(props: { command: string }) {\n const [clicked, setClicked] = useState(false);\n const [hovering, setHovering] = useState(false);\n return (\n
\n \n {props.command}\n \n \n setHovering(true)}\n onMouseLeave={() => setHovering(false)}\n onClick={() => {\n setClicked(true);\n setTimeout(() => setClicked(false), 1500);\n }}\n >\n \n \n \n {clicked ? \"Copied!\" : \"\"}\n
\n );\n}\n","import React from \"react\";\nimport { toast } from \"react-toastify\";\nimport {\n defaultBootstrapb64Key,\n defaultTerminalId,\n defaultTermpairServer,\n localStorageKeys,\n pipxTermpairShareCommand,\n secureContextHelp,\n termpairShareCommand,\n TERMPAIR_VERSION,\n} from \"./constants\";\nimport { CopyCommand } from \"./CopyCommand\";\nimport { getAESKey } from \"./encryption\";\nimport { websocketUrlFromHttpUrl } from \"./utils\";\n\nexport function LandingPageContent(props: {\n isSecureContext: boolean;\n isStaticallyHosted: Nullable;\n connectToTerminalAndWebsocket: (\n terminalId: string,\n termpairWebsocketServer: URL,\n termpairHttpServer: URL,\n bootstrapAesKey: CryptoKey\n ) => Promise;\n}) {\n const [terminalIdInput, setTerminalIdInput] = React.useState(\n defaultTerminalId ?? localStorage.getItem(localStorageKeys.terminalId) ?? \"\"\n );\n const [customHostInput, setCustomHostInput] = React.useState(\n localStorage.getItem(localStorageKeys.host) ?? \"\"\n );\n const [bootstrapAesKeyB64Input, setBootstrapAesKeyB64Input] = React.useState(\n (defaultBootstrapb64Key ||\n localStorage.getItem(localStorageKeys.bootstrapAesKeyB64)) ??\n \"\"\n );\n\n const submitForm = async () => {\n if (!terminalIdInput) {\n toast.dark(\"Terminal ID cannot be empty\");\n return;\n }\n localStorage.setItem(localStorageKeys.terminalId, terminalIdInput);\n if (!bootstrapAesKeyB64Input) {\n toast.dark(\"Secret key cannot be empty\");\n return;\n }\n let termpairHttpServer: URL;\n if (props.isStaticallyHosted) {\n if (!customHostInput) {\n toast.dark(\"Host name cannot be empty\");\n return;\n }\n try {\n const customServer = new URL(customHostInput);\n termpairHttpServer = customServer;\n localStorage.setItem(localStorageKeys.host, customHostInput);\n } catch (e) {\n toast.dark(`${customHostInput} is not a valid url`);\n return;\n }\n } else {\n termpairHttpServer = defaultTermpairServer;\n }\n\n let bootstrapKey;\n try {\n bootstrapKey = await getAESKey(\n Buffer.from(bootstrapAesKeyB64Input, \"base64\"),\n [\"decrypt\"]\n );\n localStorage.setItem(\n localStorageKeys.bootstrapAesKeyB64,\n bootstrapAesKeyB64Input\n );\n } catch (e) {\n toast.dark(`Secret encryption key is not valid`);\n return;\n }\n\n const termpairWebsocketServer = websocketUrlFromHttpUrl(termpairHttpServer);\n\n await props.connectToTerminalAndWebsocket(\n terminalIdInput,\n termpairWebsocketServer,\n termpairHttpServer,\n bootstrapKey\n );\n };\n const inputClass = \"text-black px-2 py-3 m-2 w-full font-mono\";\n\n const terminalIdInputEl = (\n \n Terminal ID\n {\n setTerminalIdInput(event.target.value);\n }}\n value={terminalIdInput}\n placeholder=\"abcdef123456789abcded123456789\"\n />\n \n );\n const bootstrapCryptoKeyInputEl = (\n
\n \n Secret encryption key\n \n {\n setBootstrapAesKeyB64Input(event.target.value);\n }}\n value={bootstrapAesKeyB64Input}\n />\n
\n );\n const terminalServerUrlEl = (\n \n \n TermPair Server URL\n \n {\n setCustomHostInput(event.target.value);\n }}\n value={customHostInput}\n />\n \n );\n\n const canConnect =\n terminalIdInput.length !== 0 &&\n bootstrapAesKeyB64Input.length > 0 &&\n props.isStaticallyHosted\n ? customHostInput.length !== 0\n : true;\n\n const connectButton = (\n
\n \n Connect\n \n
\n );\n const connectForm = (\n {\n e.preventDefault();\n submitForm();\n }}\n >\n {terminalIdInputEl}\n {bootstrapCryptoKeyInputEl}\n {props.isStaticallyHosted ? terminalServerUrlEl : null}\n {connectButton}\n \n );\n const staticLandingContent = props.isSecureContext ? (\n
\n
This page is statically hosted
\n
\n This is a static page serving the TermPair JavaScript app. It is\n optional to use a statically served TermPair webapp, but it facilitates\n easily building and self-serving to be certain the JavaScript app has\n not been tampered with by an untrusted server.\n
\n
\n Connect to a broadcasting terminal by entering the fields below and\n clicking Connect.\n
\n {connectForm}\n
\n ) : null;\n\n const regularServerContent = (\n <>\n
\n
Quick Start
\n
\n If you have TermPair installed, share a terminal with this host:\n
\n \n
Or if you have pipx, you can run TermPair via pipx:
\n \n
\n
\n
Install TermPair
\n
Install with pipx
\n \n
Or install with pip
\n \n
\n
\n
Connecting to a Terminal?
\n If a terminal is already broadcasting and you'd like to connect to it,\n you don't need to install or run anything. Just fill out the form below\n and click Connect.\n {connectForm}\n
\n \n );\n\n const termpairDemoContent = (\n
\n
TermPair Demo
\n {/* https://www.themes.dev/blog/easily-embed-responsive-youtube-video-with-tailwind-css/ */}\n
\n \n
\n
\n );\n\n return (\n
\n
\n
\n
Welcome to TermPair!
\n Easily share terminals with end-to-end encryption 🔒. Terminal data is\n always encrypted before being routed through the server.{\" \"}\n Learn more.\n
\n {!props.isSecureContext ? (\n
\n

Error

\n {secureContextHelp}\n
\n ) : null}\n {props.isStaticallyHosted === null\n ? null\n : props.isStaticallyHosted === true\n ? staticLandingContent\n : regularServerContent}\n\n
\n
Troubleshooting
\n
\n
\n
\n Initial connection fails or is rejected\n
\n
\n Ensure you are using a TermPair client compatible with{\" \"}\n v{TERMPAIR_VERSION} (the\n version of this webpage)\n
\n
\n
\n
\n Browser is not running in a secure context\n
\n
{secureContextHelp}
\n
\n
\n
\n {termpairDemoContent}\n
\n
\n );\n}\n","import { Terminal as Xterm } from \"xterm\";\n// this must match constants.py\nexport const TERMPAIR_VERSION = \"0.3.1.4\";\n\nexport const defaultTermpairServer = new URL(\n `${window.location.protocol}//${window.location.hostname}:${window.location.port}${window.location.pathname}`\n);\n\nexport const defaultTerminalId = new URLSearchParams(\n window.location.search\n).get(\"terminal_id\");\n\nexport const defaultBootstrapb64Key = window.location.hash.substring(\n 1, // skip the '#' symbol\n window.location.hash.length - 1\n);\n\nexport const cannotTypeMsg =\n \"Terminal was shared in read only mode. Unable to send data to terminal's input.\";\n\nexport const host = `${window.location.protocol}//${window.location.hostname}${window.location.pathname}`;\nlet _port = window.location.port;\nif (!window.location.port) {\n if (window.location.protocol === \"https:\") {\n _port = \"443\";\n } else {\n _port = \"80\";\n }\n}\nexport const port = _port;\nexport const termpairShareCommand = `termpair share --host \"${host}\" --port ${_port}`;\nexport const pipxTermpairShareCommand = `pipx run ${termpairShareCommand}`;\n\nexport const xterm = new Xterm({\n cursorBlink: true,\n macOptionIsMeta: true,\n scrollback: 1000,\n});\n\nexport const localStorageKeys = {\n bootstrapAesKeyB64: \"termpairBase64BootstrapKey\",\n terminalId: \"termpairTerminalId\",\n host: \"termpairCustomHost\",\n};\n\nexport const secureContextHelp = (\n
\n TermPair only works on secure connections. The server must be configured to\n serve this page over https. See termpair serve --help and{\" \"}\n \n https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts\n {\" \"}\n for more information.\n
\n);\n","import { aesEncrypt } from \"./encryption\";\n\nfunction getSalt(): string {\n return window.crypto.getRandomValues(new Uint8Array(12)).toString();\n}\n\nexport function requestTerminalDimensions() {\n return JSON.stringify({ event: \"request_terminal_dimensions\" });\n}\nexport async function newBrowserConnected(): Promise {\n return JSON.stringify({\n event: \"new_browser_connected\",\n payload: {},\n });\n}\n\nexport async function sendCommandToTerminal(\n secretEncryptionKey: CryptoKey,\n data: string,\n messageCount: number\n) {\n return JSON.stringify({\n event: \"command\",\n payload: await aesEncrypt(\n secretEncryptionKey,\n JSON.stringify({ data, salt: getSalt() }),\n messageCount\n ),\n });\n}\n\nexport function requestKeyRotation() {\n return JSON.stringify({\n event: \"request_key_rotation\",\n });\n}\n","export default __webpack_public_path__ + \"static/media/logo.0cb35f08.png\";","import { TERMPAIR_VERSION } from \"./constants\";\nimport logo from \"./logo.png\"; // logomakr.com/4N54oK\n\nconst githubLogo = (\n \n \n \n);\n\nexport function TopBar(props: any) {\n return (\n
\n
\n
\n \n \"logo\"\n \n
\n
\n v{TERMPAIR_VERSION}\n \n {githubLogo}\n \n
\n
\n
\n );\n}\n","import React from \"react\";\n\nexport class ErrorBoundary extends React.Component {\n constructor(props: any) {\n super(props);\n this.state = { hasError: false };\n }\n\n static getDerivedStateFromError(error: any) {\n // Update state so the next render will show the fallback UI.\n return { hasError: true };\n }\n\n componentDidCatch(error: any, errorInfo: any) {\n // You can also log the error to an error reporting service\n // logErrorToMyService(error, errorInfo);\n console.error(error);\n console.error(errorInfo);\n }\n\n render() {\n if (this.state.hasError) {\n // You can render any custom fallback UI\n return

Something went wrong.

;\n }\n\n return this.props.children;\n }\n}\n","import { Status, TerminalServerData, TerminalSize } from \"./types\";\nimport moment from \"moment\";\n\nexport function BottomBar(props: {\n status: Status;\n terminalData: Nullable;\n terminalId: Nullable;\n terminalSize: TerminalSize;\n numClients: number;\n}) {\n const connected = props.status === \"Connection Established\";\n const hasTerminalId = props.terminalId != null;\n const status = hasTerminalId ?
{props.status}
: null;\n\n const canType = connected ? (\n \n {props.terminalData?.allow_browser_control && connected\n ? \"read/write\"\n : \"read only\"}\n \n ) : null;\n\n const connectedClients = connected ? (\n
\n {props.numClients ? props.numClients : \"0\"} Connected Client(s)\n
\n ) : null;\n\n const startTime = connected ? (\n
\n Started at{\" \"}\n {moment(props.terminalData?.broadcast_start_time_iso).format(\n \"h:mm a on MMM Do, YYYY\"\n )}\n
\n ) : null;\n\n const terminalDimensions = connected ? (\n
\n {props.terminalSize.rows}x{props.terminalSize.cols}\n
\n ) : null;\n\n return (\n
\n {hasTerminalId ? (\n \n {status}\n {terminalDimensions}\n {canType}\n {connectedClients}\n {startTime}\n
\n ) : null}\n \n \n );\n}\n","import React, { useEffect, useState, useRef, useLayoutEffect } from \"react\";\nimport \"xterm/css/xterm.css\";\nimport { Terminal as Xterm, IDisposable } from \"xterm\";\nimport { getBootstrapAESKey } from \"./encryption\";\nimport { ToastContainer, toast } from \"react-toastify\";\nimport \"react-toastify/dist/ReactToastify.css\";\nimport { newBrowserConnected, requestTerminalDimensions } from \"./events\";\nimport { LandingPageContent } from \"./LandingPageContent\";\nimport { AesKeysRef, Status, TerminalServerData, TerminalSize } from \"./types\";\nimport { TopBar } from \"./TopBar\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\nimport { BottomBar } from \"./BottomBar\";\nimport {\n defaultTerminalId,\n defaultTermpairServer,\n secureContextHelp,\n xterm,\n} from \"./constants\";\nimport { toastStatus, websocketUrlFromHttpUrl } from \"./utils\";\nimport {\n getCustomKeyEventHandler,\n getOnDataHandler,\n redXtermText,\n} from \"./xtermUtils\";\nimport { handlers, TermPairEvent } from \"./websocketMessageHandler\";\n\nfunction handleStatusChange(\n status: Status,\n prevStatus: Status,\n setPrevStatus: (prevStatus: Status) => void\n): void {\n setPrevStatus(status);\n switch (status) {\n case null:\n break;\n case \"Connection Established\":\n toastStatus(status);\n xterm.writeln(\"Connection established with end-to-end encryption 🔒.\");\n xterm.writeln(\n \"The termpair server and third parties can't read transmitted data.\"\n );\n xterm.writeln(\"\");\n xterm.writeln(\n \"You can copy text with ctrl+shift+c or ctrl+shift+x, and paste with ctrl+shift+v.\"\n );\n xterm.writeln(\"\");\n break;\n case \"Disconnected\":\n toastStatus(status);\n if (prevStatus === \"Connection Established\") {\n xterm.writeln(redXtermText(\"Terminal session has ended\"));\n xterm.writeln(\"\");\n }\n break;\n case \"Terminal ID is invalid\":\n toast.dark(\n `An invalid Terminal ID was provided. ` +\n `Check that the session is still being broadcast and that the ID is entered correctly.`\n );\n break;\n\n case \"Failed to obtain encryption keys\":\n toast.dark(\n `Failed to obtain secret encryption keys from the broadcasting terminal. ` +\n `Is your encryption key valid?`\n );\n break;\n\n case \"Browser is not running in a secure context\":\n toast.dark(secureContextHelp);\n break;\n\n case \"Connecting...\":\n break;\n\n case \"Connection Error\":\n break;\n\n case \"Failed to fetch terminal data\":\n break;\n\n default:\n ((_: \"Unhandled switch case\"): never => {\n throw Error;\n })(status);\n }\n return status as never;\n}\n\nfunction ensureXtermIsOpen(\n xtermWasOpened: React.MutableRefObject,\n xterm: Xterm\n) {\n if (xtermWasOpened.current) {\n return;\n }\n const el = document.getElementById(\"terminal\");\n if (!el) {\n return;\n }\n xterm.open(el);\n xtermWasOpened.current = true;\n xterm.writeln(`Welcome to TermPair! https://github.com/cs01/termpair`);\n xterm.writeln(\"\");\n}\n\nfunction App() {\n const [isStaticallyHosted, setIsStaticallyHosted] =\n useState>(null);\n const [terminalServerData, setTerminalServerData] =\n useState>(null);\n const [numClients, setNumClients] = useState(0);\n\n const aesKeys = useRef({\n browser: null,\n unix: null,\n ivCount: null,\n maxIvCount: null,\n });\n const xtermWasOpened = useRef(false);\n const [webSocket, setWebsocket] = useState>(null);\n const showTerminal = webSocket !== null;\n const [terminalSize, setTerminalSize] = useState({\n rows: 20,\n cols: 81,\n });\n const [status, setStatus] = useState(null);\n const [prevStatus, setPrevStatus] = useState(null);\n const [terminalId, setTerminalId] = useState(defaultTerminalId);\n\n useEffect(() => {\n if (!window.isSecureContext) {\n changeStatus(\"Browser is not running in a secure context\");\n return;\n }\n // run once when initially opened\n const initialize = async () => {\n let staticallyHosted;\n try {\n const ret = await fetch(defaultTermpairServer.toString() + \"ping\", {\n mode: \"same-origin\",\n });\n const text = await ret.json();\n const pong = text === \"pong\";\n const isTermpairServer = ret.status === 200 && pong;\n staticallyHosted = !isTermpairServer;\n setIsStaticallyHosted(staticallyHosted);\n } catch (e) {\n staticallyHosted = true;\n setIsStaticallyHosted(staticallyHosted);\n }\n const bootstrapKey = await getBootstrapAESKey();\n\n const termpairServerUrlParam = new URLSearchParams(\n window.location.search\n ).get(\"termpair_server_url\");\n\n const customTermpairServer = termpairServerUrlParam\n ? new URL(termpairServerUrlParam)\n : null;\n\n const termpairHttpServer = staticallyHosted\n ? customTermpairServer\n : defaultTermpairServer;\n\n if (terminalId && termpairHttpServer && bootstrapKey) {\n const termpairWebsocketServer =\n websocketUrlFromHttpUrl(termpairHttpServer);\n\n await connectToTerminalAndWebsocket(\n terminalId,\n termpairWebsocketServer,\n termpairHttpServer,\n bootstrapKey\n );\n }\n };\n initialize();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useLayoutEffect(() => {\n ensureXtermIsOpen(xtermWasOpened, xterm);\n }, [showTerminal]);\n\n const changeStatus = (newStatus: Status) => {\n setStatus(newStatus);\n handleStatusChange(newStatus, prevStatus, setPrevStatus);\n };\n\n async function connectToTerminalAndWebsocket(\n terminalId: string,\n termpairWebsocketServer: URL,\n termpairHttpServer: URL,\n bootstrapAesKey: CryptoKey\n ) {\n setTerminalId(terminalId);\n setTerminalServerData(null);\n try {\n const response = await fetch(\n new URL(`terminal/${terminalId}`, termpairHttpServer).toString()\n );\n if (response.status === 200) {\n const data: TerminalServerData = await response.json();\n setTerminalServerData(data);\n setupWebsocket(\n terminalId,\n data,\n termpairWebsocketServer,\n bootstrapAesKey\n );\n } else {\n changeStatus(\"Terminal ID is invalid\");\n }\n } catch (e) {\n changeStatus(`Failed to fetch terminal data`);\n toast.dark(\n `Error fetching terminal data from ${termpairHttpServer.toString()}. Is the URL correct? Error message: ${String(\n e.message\n )}`,\n\n { autoClose: false }\n );\n }\n }\n\n function setupWebsocket(\n terminalId: string,\n terminalServerData: TerminalServerData,\n termpairWebsocketServer: URL,\n bootstrapAesKey: CryptoKey\n ) {\n if (webSocket) {\n toast.dark(\"Closing existing connection\");\n webSocket.close();\n }\n changeStatus(\"Connecting...\");\n const connectWebsocketUrl = new URL(\n `connect_browser_to_terminal?terminal_id=${terminalId}`,\n termpairWebsocketServer\n );\n const ws = new WebSocket(connectWebsocketUrl.toString());\n setWebsocket(ws);\n const handleNewInput = getOnDataHandler(ws, terminalServerData, aesKeys);\n xterm.attachCustomKeyEventHandler(\n getCustomKeyEventHandler(\n xterm,\n terminalServerData?.allow_browser_control,\n handleNewInput\n )\n );\n let onDataDispose: Nullable;\n ws.addEventListener(\"open\", async (event) => {\n changeStatus(\"Connection Established\");\n ws.send(requestTerminalDimensions());\n const newBrowserMessage = await newBrowserConnected();\n ws.send(newBrowserMessage);\n onDataDispose = xterm.onData(handleNewInput);\n });\n ws.addEventListener(\"close\", (event) => {\n if (onDataDispose) {\n // stop trying to send data since the connection is closed\n onDataDispose.dispose();\n }\n changeStatus(\"Disconnected\");\n setNumClients(0);\n });\n\n ws.addEventListener(\"error\", (event) => {\n if (onDataDispose) {\n // stop trying to send data since the connection is closed\n onDataDispose.dispose();\n }\n\n console.error(event);\n toast.dark(`Websocket Connection Error: ${JSON.stringify(event)}`);\n changeStatus(\"Connection Error\");\n setNumClients(0);\n });\n\n ws.addEventListener(\"message\", async (message: { data: any }) => {\n let data: { event: TermPairEvent; [key: string]: any };\n try {\n data = JSON.parse(message.data);\n } catch (e) {\n toast.dark(\"Failed to parse websocket message\");\n return;\n }\n\n switch (data.event) {\n case \"new_output\":\n return handlers.new_output(aesKeys, data);\n case \"resize\":\n return handlers.resize(data, setTerminalSize);\n case \"num_clients\":\n return handlers.num_clients(setNumClients, data);\n case \"aes_keys\":\n return handlers.aes_keys(\n aesKeys,\n bootstrapAesKey,\n data,\n changeStatus\n );\n case \"aes_key_rotation\":\n return handlers.aes_key_rotation(aesKeys, data);\n case \"error\":\n return handlers.error(data);\n default:\n ((_: \"Unhandled switch case\"): never => {\n throw Error;\n })(data.event);\n return handlers.default(data);\n }\n });\n }\n\n const content = (\n
\n {showTerminal ? (\n
\n ) : (\n \n )}\n \n );\n return (\n \n
\n \n \n {content}\n \n
\n
\n );\n}\n\nexport default App;\n","import { toast } from \"react-toastify\";\nimport { Terminal as Xterm } from \"xterm\";\nimport { cannotTypeMsg } from \"./constants\";\nimport { isIvExhausted } from \"./encryption\";\nimport { sendCommandToTerminal, requestKeyRotation } from \"./events\";\nimport { AesKeysRef, TerminalServerData } from \"./types\";\nimport { toastStatus } from \"./utils\";\n/**\n * The API to xterm.attachCustomKeyEventHandler is hardcoded. This function\n * provides a closure so that other variables can be used inside it.\n *\n * https://github.com/xtermjs/xterm.js/blob/70babeacb62fe05264d64324ca1f4436997efa1b/typings/xterm.d.ts#L538-L547\n *\n * @param {*} terminal - xterm object\n * @param {*} canType - is user allowed to type (this is also enforced on the server)\n * @param {*} sendInputToTerminal - function to encode and send input over the websocket\n * @returns nothing\n */\nexport function getCustomKeyEventHandler(\n terminal: Xterm,\n canType: boolean | void,\n sendInputToTerminal: (input: string) => void\n) {\n /**\n * Custom key event handler which is run before keys are\n * processed, giving consumers of xterm.js ultimate control as to what keys\n * should be processed by the terminal and what keys should not.\n * @param customKeyEventHandler The custom KeyboardEvent handler to attach.\n * This is a function that takes a KeyboardEvent, allowing consumers to stop\n * propagation and/or prevent the default action. The function returns\n * whether the event should be processed by xterm.js.\n */\n function customKeyEventHandler(e: KeyboardEvent): boolean {\n if (e.type !== \"keydown\") {\n return true;\n }\n if (e.ctrlKey && e.shiftKey) {\n const key = e.key.toLowerCase();\n if (key === \"v\") {\n if (!canType) {\n toastStatus(cannotTypeMsg);\n return false;\n }\n navigator.clipboard.readText().then((toPaste) => {\n sendInputToTerminal(toPaste);\n });\n return false;\n } else if (key === \"c\" || key === \"x\") {\n // 'x' is used as an alternate to 'c' because ctrl+c is taken\n // by the terminal (SIGINT) and ctrl+shift+c is taken by the browser\n // (open devtools).\n // I'm not aware of ctrl+shift+x being used by anything in the terminal\n // or browser\n const toCopy = terminal.getSelection();\n navigator.clipboard.writeText(toCopy);\n terminal.focus();\n return false;\n }\n }\n return true;\n }\n\n return customKeyEventHandler;\n}\n\nexport function redXtermText(text: string): string {\n return \"\\x1b[1;31m\" + text + \"\\x1b[0m\";\n}\n\nexport function getOnDataHandler(\n ws: WebSocket,\n terminalServerData: TerminalServerData,\n aesKeys: React.MutableRefObject\n) {\n return async (newInput: any) => {\n try {\n if (terminalServerData.allow_browser_control === false) {\n toastStatus(cannotTypeMsg);\n return;\n }\n if (\n aesKeys.current.browser === null ||\n aesKeys.current.ivCount === null ||\n aesKeys.current.maxIvCount === null\n ) {\n toast.dark(\n `Cannot input because it cannot be encrypted. Encryption keys are missing.`\n );\n return;\n }\n ws.send(\n await sendCommandToTerminal(\n aesKeys.current.browser,\n newInput,\n aesKeys.current.ivCount++\n )\n );\n if (isIvExhausted(aesKeys.current.ivCount, aesKeys.current.maxIvCount)) {\n ws.send(requestKeyRotation());\n aesKeys.current.maxIvCount += 1000;\n }\n } catch (e) {\n toast.dark(`Failed to send data to terminal ${e}`);\n }\n };\n}\n","import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.render(\n \n \n ,\n document.getElementById(\"root\")\n);\n","// Symmetric encryption with aes gcm\n// https://github.com/mdn/dom-examples/blob/master/web-crypto/encrypt-decrypt/aes-gcm.js\n\nimport { defaultBootstrapb64Key } from \"./constants\";\n\nconst IV_LENGTH = 12;\ntype Base64String = string;\ntype EncryptedBase64String = string;\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction ab2str(buf: ArrayBuffer): string {\n // @ts-ignore\n return String.fromCharCode.apply(null, new Uint8Array(buf));\n}\n\nexport async function getAESKey(\n rawKeyData: Buffer,\n usages: Array<\"encrypt\" | \"decrypt\">\n): Promise {\n return await window.crypto.subtle.importKey(\n \"raw\",\n rawKeyData,\n {\n name: \"AES-GCM\",\n },\n false, // extractable\n usages\n );\n}\n\nexport async function getBootstrapAESKey(): Promise> {\n try {\n if (!defaultBootstrapb64Key) {\n return null;\n }\n const keyData = Buffer.from(defaultBootstrapb64Key, \"base64\");\n return await getAESKey(keyData, [\"decrypt\"]);\n } catch (e) {\n console.error(e);\n return null;\n }\n}\n\nexport async function aesDecrypt(\n secretcryptoKey: CryptoKey,\n encryptedPayload: Buffer\n): Promise {\n // iv is prepended to encrypted payload\n const iv = encryptedPayload.subarray(0, IV_LENGTH);\n\n // remaining bytes are encrypted utf-8 output of terminal\n const encryptedTerminalOutput = encryptedPayload.subarray(IV_LENGTH);\n\n const decryptedTerminalOutput = Buffer.from(\n await window.crypto.subtle.decrypt(\n {\n name: \"AES-GCM\",\n iv: iv,\n },\n secretcryptoKey,\n encryptedTerminalOutput\n )\n );\n return decryptedTerminalOutput;\n}\n\n// https://stackoverflow.com/a/65227338/2893090\nfunction ivFromInteger(ivCount: number) {\n const iv = new Uint8Array(IV_LENGTH);\n const a = [];\n a.unshift(ivCount & 255);\n // while some other byte still has data\n while (ivCount >= 256) {\n // shift 8 bits over (consume next byte)\n ivCount = ivCount >>> 8;\n // prepend current byte value to front of the array\n a.unshift(ivCount & 255);\n }\n // set the 12 byte array with the array we just\n // computed\n iv.set(a);\n return iv;\n}\n\nexport async function aesEncrypt(\n browserSecretAESKey: CryptoKey,\n utf8Payload: string,\n ivCount: number\n): Promise {\n // The same iv must never be reused with a given key\n const iv = ivFromInteger(ivCount);\n const encryptedArrayBuffer = await window.crypto.subtle.encrypt(\n {\n name: \"AES-GCM\",\n iv: iv,\n },\n browserSecretAESKey,\n new TextEncoder().encode(utf8Payload)\n );\n // prepend unencrypted iv to encrypted payload\n const ivAndEncryptedPayload = _combineBuffers(iv, encryptedArrayBuffer);\n\n const base64EncryptedString = _arrayBufferToBase64(ivAndEncryptedPayload);\n return base64EncryptedString;\n}\n\nexport function isIvExhausted(ivCount: number, maxIvCount: number): boolean {\n return ivCount >= maxIvCount;\n}\n\nfunction _combineBuffers(\n buffer1: Uint8Array,\n buffer2: ArrayBuffer\n): ArrayBufferLike {\n const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);\n tmp.set(new Uint8Array(buffer1), 0);\n tmp.set(new Uint8Array(buffer2), buffer1.byteLength);\n return tmp.buffer;\n}\n\nfunction _arrayBufferToBase64(buffer: ArrayBuffer): Base64String {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n const len = bytes.byteLength;\n for (let i = 0; i < len; i++) {\n // returns a utf-16 character, considered \"binary\"\n binary += String.fromCharCode(bytes[i]);\n }\n // \"binary to ascii\"\n return window.btoa(binary);\n}\n","import { toast } from \"react-toastify\";\nimport { debounce } from \"debounce\";\nexport function websocketUrlFromHttpUrl(httpUrl: URL) {\n return new URL(httpUrl.toString().replace(/^http/, \"ws\"));\n}\n\nexport const toastStatus = debounce((status: any) => {\n toast.dark(status);\n}, 100);\n","import { toast } from \"react-toastify\";\nimport { xterm } from \"./constants\";\nimport { aesDecrypt, getAESKey } from \"./encryption\";\nimport { AesKeysRef, Status, TerminalSize } from \"./types\";\n\n// This should be kept in sync with the Python client\nexport type TermPairEvent =\n | \"new_output\"\n | \"resize\"\n | \"num_clients\"\n | \"aes_keys\"\n | \"error\"\n | \"aes_key_rotation\";\n\n// TODO use something like https://www.npmjs.com/package/yup to\n// validate the json at runtime\nexport const handlers = {\n new_output: async function (\n aesKeys: React.MutableRefObject,\n data: any\n ) {\n if (!aesKeys.current.unix) {\n console.error(\n \"Missing AES CryptoKey for unix terminal. Cannot decrypt message.\"\n );\n return;\n }\n const decryptedJson = await aesDecrypt(\n aesKeys.current.unix,\n Buffer.from(data.payload, \"base64\")\n );\n const decryptedPayload = JSON.parse(decryptedJson.toString());\n const pty_output = Buffer.from(decryptedPayload.pty_output, \"base64\");\n xterm.write(pty_output);\n },\n resize: function (\n data: any,\n setTerminalSize: React.Dispatch>\n ) {\n if (data.payload.cols && data.payload.rows) {\n const cols = data.payload.cols;\n const rows = data.payload.rows;\n setTerminalSize({\n cols,\n rows,\n });\n xterm.resize(cols, rows);\n }\n },\n num_clients: function (\n setNumClients: React.Dispatch>,\n data: any\n ) {\n const num_clients = data.payload;\n setNumClients(num_clients);\n },\n aes_keys: async function (\n aesKeys: React.MutableRefObject,\n bootstrapAesKey: CryptoKey,\n data: any,\n changeStatus: (newStatus: Status) => void\n ) {\n try {\n const unixAesKeyData = await aesDecrypt(\n bootstrapAesKey,\n Buffer.from(data.payload.b64_bootstrap_unix_aes_key, \"base64\")\n );\n aesKeys.current.unix = await getAESKey(unixAesKeyData, [\"decrypt\"]);\n\n const browserAesKeyData = await aesDecrypt(\n bootstrapAesKey,\n Buffer.from(data.payload.b64_bootstrap_browser_aes_key, \"base64\")\n );\n aesKeys.current.browser = await getAESKey(browserAesKeyData, [\"encrypt\"]);\n if (data.payload.iv_count == null || data.payload.max_iv_count == null) {\n console.error(\"missing required iv parameters\");\n throw Error(\"missing required iv parameters\");\n }\n const startIvCount = (aesKeys.current.ivCount = parseInt(\n data.payload.iv_count,\n 10\n ));\n\n const maxIvCount = (aesKeys.current.maxIvCount = parseInt(\n data.payload.max_iv_count,\n 10\n ));\n if (maxIvCount < startIvCount) {\n console.error(\n `Initialized IV counter is below max value ${startIvCount} vs ${maxIvCount}`\n );\n aesKeys.current = {\n ...aesKeys.current,\n browser: null,\n maxIvCount: null,\n ivCount: null,\n unix: null,\n };\n throw Error;\n }\n } catch (e) {\n if (\n aesKeys.current.browser == null ||\n aesKeys.current.unix == null ||\n aesKeys.current.ivCount == null ||\n aesKeys.current.maxIvCount == null\n ) {\n console.error(e);\n console.error(data);\n changeStatus(\"Failed to obtain encryption keys\");\n return;\n }\n }\n },\n aes_key_rotation: async function (\n aesKeys: React.MutableRefObject,\n data: any\n ) {\n if (!aesKeys.current.unix) {\n console.error(\"Cannot decrypt new AES keys\");\n return;\n }\n try {\n const newUnixAesKeyData = await aesDecrypt(\n aesKeys.current.unix,\n data.payload.b64_aes_secret_unix_key\n );\n const newBrowserAesKeyData = await aesDecrypt(\n aesKeys.current.unix,\n Buffer.from(data.payload.b64_aes_secret_browser_key, \"base64\")\n );\n aesKeys.current.browser = await getAESKey(newBrowserAesKeyData, [\n \"encrypt\",\n ]);\n aesKeys.current.unix = await getAESKey(newUnixAesKeyData, [\"decrypt\"]);\n // toast.dark(\"AES keys have been rotated\");\n } catch (e) {\n console.error(e);\n toast.dark(`AES key rotation failed: ${e}`);\n }\n },\n error: function (data: any) {\n toast.dark(`Error: ${data.payload}`);\n console.error(data);\n },\n default: function (data: any) {\n toast.dark(`Unknown event received: ${data.event}`);\n console.error(\"unknown event type\", data);\n },\n};\n"],"sourceRoot":""}