webserver.js 655 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666
  1. /**
  2. * @description MeshCentral web server
  3. * @author Ylian Saint-Hilaire
  4. * @copyright Intel Corporation 2018-2022
  5. * @license Apache-2.0
  6. * @version v0.0.1
  7. */
  8. /*jslint node: true */
  9. /*jshint node: true */
  10. /*jshint strict:false */
  11. /*jshint -W097 */
  12. /*jshint esversion: 6 */
  13. 'use strict';
  14. // SerialTunnel object is used to embed TLS within another connection.
  15. function SerialTunnel(options) {
  16. var obj = new require('stream').Duplex(options);
  17. obj.forwardwrite = null;
  18. obj.updateBuffer = function (chunk) { this.push(chunk); };
  19. obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); }; // Pass data written to forward
  20. obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
  21. return obj;
  22. }
  23. // ExpressJS login sample
  24. // https://github.com/expressjs/express/blob/master/examples/auth/index.js
  25. // Polyfill startsWith/endsWith for older NodeJS
  26. if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; }
  27. if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.lastIndexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; }
  28. // Construct a HTTP server object
  29. module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) {
  30. var obj = {}, i = 0;
  31. // Modules
  32. obj.fs = require('fs');
  33. obj.net = require('net');
  34. obj.tls = require('tls');
  35. obj.path = require('path');
  36. obj.bodyParser = require('body-parser');
  37. obj.exphbs = require('express-handlebars');
  38. obj.crypto = require('crypto');
  39. obj.common = require('./common.js');
  40. obj.express = require('express');
  41. obj.meshAgentHandler = require('./meshagent.js');
  42. obj.meshRelayHandler = require('./meshrelay.js');
  43. obj.meshDeviceFileHandler = require('./meshdevicefile.js');
  44. obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js');
  45. obj.meshIderHandler = require('./amt/amt-ider.js');
  46. obj.meshUserHandler = require('./meshuser.js');
  47. obj.interceptor = require('./interceptor');
  48. obj.uaparser = require('ua-parser-js');
  49. const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
  50. // Setup WebAuthn / FIDO2
  51. obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
  52. // Variables
  53. obj.args = args;
  54. obj.parent = parent;
  55. obj.filespath = parent.filespath;
  56. obj.db = db;
  57. obj.app = obj.express();
  58. if (obj.args.agentport) { obj.agentapp = obj.express(); }
  59. if (args.compression !== false) { obj.app.use(require('compression')()); }
  60. obj.app.disable('x-powered-by');
  61. obj.tlsServer = null;
  62. obj.tcpServer = null;
  63. obj.certificates = certificates;
  64. obj.users = {}; // UserID --> User
  65. obj.meshes = {}; // MeshID --> Mesh (also called device group)
  66. obj.userGroups = {}; // UGrpID --> User Group
  67. obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node
  68. obj.tlsCiphers = args.tlsciphers; // List of TLS ciphers to use
  69. obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
  70. obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
  71. obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
  72. obj.tlsSniCredentials = null;
  73. obj.dnsDomains = {};
  74. obj.relaySessionCount = 0;
  75. obj.relaySessionErrorCount = 0;
  76. obj.blockedUsers = 0;
  77. obj.blockedAgents = 0;
  78. obj.renderPages = null;
  79. obj.renderLanguages = [];
  80. obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
  81. // Web relay sessions
  82. var webRelayNextSessionId = 1;
  83. var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
  84. var webRelayCleanupTimer = null;
  85. // Monitor web relay session removals
  86. parent.AddEventDispatch(['server-shareremove'], obj);
  87. obj.HandleEvent = function (source, event, ids, id) {
  88. if (event.action == 'removedDeviceShare') {
  89. for (var relaySessionId in webRelaySessions) {
  90. // A share was removed that matches an active session, close the web relay session.
  91. if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); }
  92. }
  93. }
  94. }
  95. // Mesh Rights
  96. const MESHRIGHT_EDITMESH = 0x00000001;
  97. const MESHRIGHT_MANAGEUSERS = 0x00000002;
  98. const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
  99. const MESHRIGHT_REMOTECONTROL = 0x00000008;
  100. const MESHRIGHT_AGENTCONSOLE = 0x00000010;
  101. const MESHRIGHT_SERVERFILES = 0x00000020;
  102. const MESHRIGHT_WAKEDEVICE = 0x00000040;
  103. const MESHRIGHT_SETNOTES = 0x00000080;
  104. const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
  105. const MESHRIGHT_NOTERMINAL = 0x00000200;
  106. const MESHRIGHT_NOFILES = 0x00000400;
  107. const MESHRIGHT_NOAMT = 0x00000800;
  108. const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
  109. const MESHRIGHT_LIMITEVENTS = 0x00002000;
  110. const MESHRIGHT_CHATNOTIFY = 0x00004000;
  111. const MESHRIGHT_UNINSTALL = 0x00008000;
  112. const MESHRIGHT_NODESKTOP = 0x00010000;
  113. const MESHRIGHT_REMOTECOMMAND = 0x00020000;
  114. const MESHRIGHT_RESETOFF = 0x00040000;
  115. const MESHRIGHT_GUESTSHARING = 0x00080000;
  116. const MESHRIGHT_ADMIN = 0xFFFFFFFF;
  117. // Site rights
  118. const SITERIGHT_SERVERBACKUP = 0x00000001;
  119. const SITERIGHT_MANAGEUSERS = 0x00000002;
  120. const SITERIGHT_SERVERRESTORE = 0x00000004;
  121. const SITERIGHT_FILEACCESS = 0x00000008;
  122. const SITERIGHT_SERVERUPDATE = 0x00000010;
  123. const SITERIGHT_LOCKED = 0x00000020;
  124. const SITERIGHT_NONEWGROUPS = 0x00000040;
  125. const SITERIGHT_NOMESHCMD = 0x00000080;
  126. const SITERIGHT_USERGROUPS = 0x00000100;
  127. const SITERIGHT_RECORDINGS = 0x00000200;
  128. const SITERIGHT_LOCKSETTINGS = 0x00000400;
  129. const SITERIGHT_ALLEVENTS = 0x00000800;
  130. const SITERIGHT_NONEWDEVICES = 0x00001000;
  131. const SITERIGHT_ADMIN = 0xFFFFFFFF;
  132. // Setup SSPI authentication if needed
  133. if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
  134. for (i in obj.parent.config.domains) { if (obj.parent.config.domains[i].auth == 'sspi') { var nodeSSPI = require('node-sspi'); obj.parent.config.domains[i].sspi = new nodeSSPI({ retrieveGroups: false, offerBasic: false }); } }
  135. }
  136. // Perform hash on web certificate and agent certificate
  137. obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
  138. obj.webCertificateHashs = { '': obj.webCertificateHash };
  139. obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
  140. obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
  141. obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
  142. obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) };
  143. obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
  144. obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
  145. obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
  146. obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null;
  147. obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null;
  148. // Compute the hash of all of the web certificates for each domain
  149. for (var i in obj.parent.config.domains) {
  150. if (obj.parent.config.domains[i].certhash != null) {
  151. // If the web certificate hash is provided, use it.
  152. obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
  153. if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
  154. delete obj.webCertificateExpire[i]; // Expire time is not provided
  155. } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
  156. // If the domain has a different DNS name, use a different certificate hash.
  157. // Hash the full certificate
  158. obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
  159. obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
  160. try {
  161. // Decode a RSA certificate and hash the public key.
  162. obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
  163. } catch (ex) {
  164. // This may be a ECDSA certificate, hash the entire cert.
  165. obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
  166. }
  167. } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
  168. // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
  169. obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
  170. obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
  171. obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
  172. } else if (i != '') {
  173. // For any other domain, use the default cert.
  174. obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
  175. obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
  176. obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
  177. }
  178. }
  179. // If we are running the legacy swarm server, compute the hash for that certificate
  180. if (parent.certificates.swarmserver != null) {
  181. obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
  182. obj.swarmCertificateHash384 = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.swarmserver.cert).publicKey, { md: parent.certificateOperations.forge.md.sha384.create(), encoding: 'binary' });
  183. obj.swarmCertificateHash256 = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.swarmserver.cert).publicKey, { md: parent.certificateOperations.forge.md.sha256.create(), encoding: 'binary' });
  184. }
  185. // Main lists
  186. obj.wsagents = {}; // NodeId --> Agent
  187. obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
  188. obj.wsagentsDisconnections = {};
  189. obj.wsagentsDisconnectionsTimer = null;
  190. obj.duplicateAgentsLog = {};
  191. obj.wssessions = {}; // UserId --> Array Of Sessions
  192. obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
  193. obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
  194. obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
  195. obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
  196. obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
  197. obj.wsrelays = {}; // Id -> Relay
  198. obj.desktoprelays = {}; // Id -> Desktop Multiplexer Relay
  199. obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
  200. var tlsSessionStore = {}; // Store TLS session information for quick resume.
  201. var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
  202. // Setup randoms
  203. obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
  204. obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
  205. obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
  206. // Get non-english web pages and emails
  207. getRenderList();
  208. getEmailLanguageList();
  209. // Setup DNS domain TLS SNI credentials
  210. {
  211. var dnscount = 0;
  212. obj.tlsSniCredentials = {};
  213. for (i in obj.certificates.dns) { if (obj.parent.config.domains[i].dns != null) { obj.dnsDomains[obj.parent.config.domains[i].dns.toLowerCase()] = obj.parent.config.domains[i]; obj.tlsSniCredentials[obj.parent.config.domains[i].dns] = obj.tls.createSecureContext(obj.certificates.dns[i]).context; dnscount++; } }
  214. if (dnscount > 0) { obj.tlsSniCredentials[''] = obj.tls.createSecureContext({ cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca }).context; } else { obj.tlsSniCredentials = null; }
  215. }
  216. function TlsSniCallback(name, cb) {
  217. var c = obj.tlsSniCredentials[name];
  218. if (c != null) {
  219. cb(null, c);
  220. } else {
  221. cb(null, obj.tlsSniCredentials['']);
  222. }
  223. }
  224. function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
  225. //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
  226. // Fetch all users from the database, keep this in memory
  227. obj.db.GetAllType('user', function (err, docs) {
  228. obj.common.unEscapeAllLinksFieldName(docs);
  229. var domainUserCount = {}, i = 0;
  230. for (i in parent.config.domains) { domainUserCount[i] = 0; }
  231. for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
  232. for (i in parent.config.domains) {
  233. if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
  234. // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation.
  235. //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
  236. console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
  237. }
  238. }
  239. // Fetch all device groups (meshes) from the database, keep this in memory
  240. // As we load things in memory, we will also be doing some cleaning up.
  241. // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
  242. obj.db.GetAllType('mesh', function (err, docs) {
  243. obj.common.unEscapeAllLinksFieldName(docs);
  244. for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
  245. // Fetch all user groups from the database, keep this in memory
  246. obj.db.GetAllType('ugrp', function (err, docs) {
  247. obj.common.unEscapeAllLinksFieldName(docs);
  248. // Perform user group link cleanup
  249. for (var i in docs) {
  250. const ugrp = docs[i];
  251. if (ugrp.links != null) {
  252. for (var j in ugrp.links) {
  253. if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
  254. else if (j.startsWith('mesh/') && ((obj.meshes[j] == null) || (obj.meshes[j].deleted != null))) { delete ugrp.links[j]; } // User has a link to a device group that does not exist
  255. }
  256. }
  257. obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
  258. }
  259. // Perform device group link cleanup
  260. for (var i in obj.meshes) {
  261. const mesh = obj.meshes[i];
  262. if (mesh.links != null) {
  263. for (var j in mesh.links) {
  264. if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete mesh.links[j]; } // Device group has a link to a user group that does not exist
  265. else if (j.startsWith('user/') && (obj.users[j] == null)) { delete mesh.links[j]; } // Device group has a link to a user that does not exist
  266. }
  267. }
  268. }
  269. // Perform user link cleanup
  270. for (var i in obj.users) {
  271. const user = obj.users[i];
  272. if (user.links != null) {
  273. for (var j in user.links) {
  274. if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
  275. else if (j.startsWith('mesh/') && ((obj.meshes[j] == null) || (obj.meshes[j].deleted != null))) { delete user.links[j]; } // User has a link to a device group that does not exist
  276. //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
  277. }
  278. //if (Object.keys(user.links).length == 0) { delete user.links; }
  279. }
  280. }
  281. // We loaded the users, device groups and user group state, start the server
  282. serverStart();
  283. });
  284. });
  285. });
  286. // Clean up a device, used before saving it in the database
  287. obj.cleanDevice = function (device) {
  288. // Check device links, if a link points to an unknown user, remove it.
  289. if (device.links != null) {
  290. for (var j in device.links) {
  291. if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
  292. delete device.links[j];
  293. if (Object.keys(device.links).length == 0) { delete device.links; }
  294. }
  295. }
  296. }
  297. return device;
  298. }
  299. // Return statistics about this web server
  300. obj.getStats = function () {
  301. return {
  302. users: Object.keys(obj.users).length,
  303. meshes: Object.keys(obj.meshes).length,
  304. dnsDomains: Object.keys(obj.dnsDomains).length,
  305. relaySessionCount: obj.relaySessionCount,
  306. relaySessionErrorCount: obj.relaySessionErrorCount,
  307. wsagents: Object.keys(obj.wsagents).length,
  308. wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
  309. wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
  310. wssessions: Object.keys(obj.wssessions).length,
  311. wssessions2: Object.keys(obj.wssessions2).length,
  312. wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
  313. wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
  314. wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
  315. sessionsCount: Object.keys(obj.sessionsCount).length,
  316. wsrelays: Object.keys(obj.wsrelays).length,
  317. wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
  318. tlsSessionStore: Object.keys(tlsSessionStore).length,
  319. blockedUsers: obj.blockedUsers,
  320. blockedAgents: obj.blockedAgents
  321. };
  322. }
  323. // Agent counters
  324. obj.agentStats = {
  325. createMeshAgentCount: 0,
  326. agentClose: 0,
  327. agentBinaryUpdate: 0,
  328. agentMeshCoreBinaryUpdate: 0,
  329. coreIsStableCount: 0,
  330. verifiedAgentConnectionCount: 0,
  331. clearingCoreCount: 0,
  332. updatingCoreCount: 0,
  333. recoveryCoreIsStableCount: 0,
  334. meshDoesNotExistCount: 0,
  335. invalidPkcsSignatureCount: 0,
  336. invalidRsaSignatureCount: 0,
  337. invalidJsonCount: 0,
  338. unknownAgentActionCount: 0,
  339. agentBadWebCertHashCount: 0,
  340. agentBadSignature1Count: 0,
  341. agentBadSignature2Count: 0,
  342. agentMaxSessionHoldCount: 0,
  343. invalidDomainMeshCount: 0,
  344. invalidMeshTypeCount: 0,
  345. invalidDomainMesh2Count: 0,
  346. invalidMeshType2Count: 0,
  347. duplicateAgentCount: 0,
  348. maxDomainDevicesReached: 0,
  349. agentInTrouble: 0,
  350. agentInBigTrouble: 0
  351. }
  352. obj.getAgentStats = function () { return obj.agentStats; }
  353. // Traffic counters
  354. obj.trafficStats = {
  355. httpRequestCount: 0,
  356. httpWebSocketCount: 0,
  357. httpIn: 0,
  358. httpOut: 0,
  359. relayCount: {},
  360. relayIn: {},
  361. relayOut: {},
  362. localRelayCount: {},
  363. localRelayIn: {},
  364. localRelayOut: {},
  365. AgentCtrlIn: 0,
  366. AgentCtrlOut: 0,
  367. LMSIn: 0,
  368. LMSOut: 0,
  369. CIRAIn: 0,
  370. CIRAOut: 0
  371. }
  372. obj.trafficStats.time = Date.now();
  373. obj.getTrafficStats = function () { return obj.trafficStats; }
  374. obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
  375. const data = obj.common.Clone(obj.trafficStats);
  376. data.time = Date.now();
  377. const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
  378. if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
  379. delta.time = data.time;
  380. return { current: data, delta: delta }
  381. }
  382. function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
  383. const r = {};
  384. for (var i in newData) {
  385. if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
  386. if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
  387. }
  388. return r;
  389. }
  390. // Keep a record of the last agent issues.
  391. obj.getAgentIssues = function () { return obj.agentIssues; }
  392. obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
  393. obj.agentIssues = [];
  394. // Authenticate the user
  395. obj.authenticate = function (name, pass, domain, fn) {
  396. if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
  397. if (name.startsWith('~t:')) {
  398. // Login token, try to fetch the token from the database
  399. obj.db.Get('logintoken-' + name, function (err, docs) {
  400. if (err != null) { fn(err); return; }
  401. if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
  402. const loginToken = docs[0];
  403. if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
  404. // Default strong password hashing (pbkdf2 SHA384)
  405. require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
  406. if (err) return fn(err);
  407. if (hash == loginToken.hash) {
  408. // Login username and password are valid.
  409. var user = obj.users[loginToken.userid];
  410. if (!user) { fn(new Error('cannot find user')); return; }
  411. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  412. // Successful login token authentication
  413. var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
  414. if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
  415. return fn(null, user._id, null, loginOptions);
  416. }
  417. fn(new Error('invalid password'));
  418. }, 0);
  419. });
  420. } else if (domain.auth == 'ldap') {
  421. // This method will handle LDAP login
  422. const ldapHandler = function ldapHandlerFunc(err, xxuser) {
  423. if (err) { parent.debug('ldap', 'LDAP Error: ' + err); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } fn(new Error('invalid password')); return; }
  424. // Save this LDAP user to file if needed
  425. if (typeof domain.ldapsaveusertofile == 'string') {
  426. obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
  427. }
  428. // Work on getting the userid for this LDAP user
  429. var shortname = null;
  430. var username = xxuser['displayName'];
  431. if (typeof domain.ldapusername == 'string') {
  432. if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; }
  433. } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; }
  434. if (domain.ldapuserbinarykey) {
  435. // Use a binary key as the userid
  436. if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
  437. } else if (domain.ldapuserkey) {
  438. // Use a string key as the userid
  439. if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
  440. } else {
  441. // Use the default key as the userid
  442. if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); }
  443. else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); }
  444. else if (xxuser['name']) { shortname = xxuser['name']; }
  445. else if (xxuser['cn']) { shortname = xxuser['cn']; }
  446. }
  447. if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; }
  448. if (username == null) { username = shortname; }
  449. var userid = 'user/' + domain.id + '/' + shortname;
  450. // Get the list of groups this user is a member of.
  451. var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'];
  452. if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
  453. if (Array.isArray(userMemberships) == false) { userMemberships = []; }
  454. // See if the user is required to be part of an LDAP user group in order to log into this server.
  455. if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
  456. if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
  457. // Look for a matching LDAP user group
  458. var userMembershipMatch = false;
  459. for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
  460. if (userMembershipMatch === false) { parent.authLog('ldapHandler', 'LDAP denying login to a user that is not a member of a LDAP required group.'); fn('denied'); return; } // If there is no match, deny the login
  461. }
  462. // Check if user is in an site administrator group
  463. var siteAdminGroup = null;
  464. if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
  465. if (Array.isArray(domain.ldapsiteadmingroups)) {
  466. siteAdminGroup = false;
  467. for (var i in domain.ldapsiteadmingroups) {
  468. if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; }
  469. }
  470. }
  471. // See if we need to sync LDAP user memberships with user groups
  472. if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; }
  473. if (typeof domain.ldapsyncwithusergroups == 'object') {
  474. // LDAP user memberships sync is enabled, see if there are any filters to apply
  475. if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; }
  476. if (Array.isArray(domain.ldapsyncwithusergroups.filter)) {
  477. const g = [];
  478. for (var i in userMemberships) {
  479. var match = false;
  480. for (var j in domain.ldapsyncwithusergroups.filter) {
  481. if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
  482. }
  483. if (match) { g.push(userMemberships[i]); }
  484. }
  485. userMemberships = g;
  486. }
  487. } else {
  488. // LDAP user memberships sync is disabled, sync the user with empty membership
  489. userMemberships = [];
  490. }
  491. // Get the email address for this LDAP user
  492. var email = null;
  493. if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default
  494. if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value.
  495. if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent.
  496. // Get the real name for this LDAP user
  497. var realname = null;
  498. if (typeof domain.ldapuserrealname == 'string') {
  499. if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
  500. }
  501. else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }
  502. // Get the phone number for this LDAP user
  503. var phonenumber = null;
  504. if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; }
  505. else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } }
  506. // Work on getting the image of this LDAP user
  507. var userimage = null, userImageBuffer = null;
  508. if (xxuser._raw) { // Using _raw allows us to get data directly as buffer.
  509. if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; }
  510. else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; }
  511. else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; }
  512. if (userImageBuffer != null) {
  513. if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); }
  514. if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); }
  515. }
  516. }
  517. // Display user information extracted from LDAP data
  518. parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
  519. // If there is a testing userid, use that
  520. if (ldapHandlerFunc.ldapShortName) {
  521. shortname = ldapHandlerFunc.ldapShortName;
  522. userid = 'user/' + domain.id + '/' + shortname;
  523. }
  524. // Save the user image
  525. if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }
  526. // Close the LDAP object
  527. if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }
  528. // Check if the user already exists
  529. var user = obj.users[userid];
  530. if (user == null) {
  531. // This user does not exist, create a new account.
  532. var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
  533. if (email) { user['email'] = email; user['emailVerified'] = true; }
  534. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
  535. if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
  536. var usercount = 0;
  537. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
  538. if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
  539. // Auto-join any user groups
  540. if (typeof domain.newaccountsusergroups == 'object') {
  541. for (var i in domain.newaccountsusergroups) {
  542. var ugrpid = domain.newaccountsusergroups[i];
  543. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  544. var ugroup = obj.userGroups[ugrpid];
  545. if (ugroup != null) {
  546. // Add group to the user
  547. if (user.links == null) { user.links = {}; }
  548. user.links[ugroup._id] = { rights: 1 };
  549. // Add user to the group
  550. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  551. db.Set(ugroup);
  552. // Notify user group change
  553. var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 71, msgArgs: [user.name, ugroup.name], msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
  554. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  555. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  556. }
  557. }
  558. }
  559. // Check the user real name
  560. if (realname) { user.realname = realname; }
  561. // Check the user phone number
  562. if (phonenumber) { user.phone = phonenumber; }
  563. // Indicate that this user has a image
  564. if (userimage != null) { user.flags = 1; }
  565. // See if the user is a member of the site admin group.
  566. if (typeof siteAdminGroup === 'string') {
  567. parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
  568. user.siteadmin = 0xFFFFFFFF;
  569. }
  570. // Sync the user with LDAP matching user groups
  571. if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
  572. obj.users[user._id] = user;
  573. obj.db.SetUser(user);
  574. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id };
  575. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
  576. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  577. return fn(null, user._id);
  578. } else {
  579. var userChanged = false;
  580. // This is an existing user
  581. // If the display username has changes, update it.
  582. if (user.name != username) { user.name = username; userChanged = true; }
  583. // Check if user email has changed
  584. if (user.email && !email) { // email unset in ldap => unset
  585. delete user.email;
  586. delete user.emailVerified;
  587. userChanged = true;
  588. } else if (user.email != email) { // update email
  589. user['email'] = email;
  590. user['emailVerified'] = true;
  591. userChanged = true;
  592. }
  593. // Check the user real name
  594. if (realname != user.realname) { user.realname = realname; userChanged = true; }
  595. // Check the user phone number
  596. if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }
  597. // Check the user image flag
  598. if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; }
  599. if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; }
  600. // See if the user is a member of the site admin group.
  601. if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
  602. parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
  603. user.siteadmin = 0xFFFFFFFF;
  604. userChanged = true;
  605. } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
  606. parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
  607. delete user.siteadmin;
  608. userChanged = true;
  609. }
  610. // Synd the user with LDAP matching user groups
  611. if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
  612. // If the user changed, save the changes to the database here
  613. if (userChanged) {
  614. obj.db.SetUser(user);
  615. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 154, msg: 'Account changed to sync with LDAP data.', domain: domain.id };
  616. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  617. parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  618. }
  619. // If user is locker out, block here.
  620. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  621. return fn(null, user._id);
  622. }
  623. }
  624. if (domain.ldapoptions.url == 'test') {
  625. // Test LDAP login
  626. var xxuser = domain.ldapoptions[name.toLowerCase()];
  627. if (xxuser == null) { fn(new Error('invalid password')); return; } else {
  628. ldapHandler.ldapShortName = name.toLowerCase();
  629. if (typeof xxuser == 'string') {
  630. // The test LDAP user points to a JSON file where the user information is, load it.
  631. ldapHandler(null, require(xxuser));
  632. } else {
  633. // The test user information is in the config.json, use it.
  634. ldapHandler(null, xxuser);
  635. }
  636. }
  637. } else {
  638. // LDAP login
  639. var LdapAuth = require('ldapauth-fork');
  640. if (domain.ldapoptions == null) { domain.ldapoptions = {}; }
  641. domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images.
  642. var ldap = new LdapAuth(domain.ldapoptions);
  643. ldapHandler.ldapobj = ldap;
  644. ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object
  645. ldap.authenticate(name, pass, ldapHandler);
  646. }
  647. } else {
  648. // Regular login
  649. var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
  650. // Query the db for the given username
  651. if (!user) { fn(new Error('cannot find user')); return; }
  652. // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
  653. if (user.salt == null) {
  654. fn(new Error('invalid password'));
  655. } else {
  656. if (user.passtype != null) {
  657. // IIS default clear or weak password hashing (SHA-1)
  658. require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
  659. if (err) return fn(err);
  660. if (hash == user.hash) {
  661. // Update the password to the stronger format.
  662. require('./pass').hash(pass, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }, 0);
  663. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  664. return fn(null, user._id);
  665. }
  666. fn(new Error('invalid password'), null, user.passhint);
  667. });
  668. } else {
  669. // Default strong password hashing (pbkdf2 SHA384)
  670. require('./pass').hash(pass, user.salt, function (err, hash, tag) {
  671. if (err) return fn(err);
  672. if (hash == user.hash) {
  673. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  674. return fn(null, user._id);
  675. }
  676. fn(new Error('invalid password'), null, user.passhint);
  677. }, 0);
  678. }
  679. }
  680. }
  681. };
  682. /*
  683. obj.restrict = function (req, res, next) {
  684. console.log('restrict', req.url);
  685. var domain = getDomain(req);
  686. if (req.session.userid) {
  687. next();
  688. } else {
  689. req.session.messageid = 111; // Access denied.
  690. res.redirect(domain.url + 'login');
  691. }
  692. };
  693. */
  694. // Check if the source IP address is in the IP list, return false if not.
  695. function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
  696. try {
  697. if (req.connection) {
  698. // HTTP(S) request
  699. if (req.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(req.clientIp, ipList[i])) { if (closeIfThis === true) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } } return true; } } }
  700. if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
  701. } else {
  702. // WebSocket request
  703. if (res.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(res.clientIp, ipList[i])) { if (closeIfThis === true) { try { req.close(); } catch (e) { } } return true; } } }
  704. if (closeIfThis === false) { try { req.close(); } catch (e) { } }
  705. }
  706. } catch (e) { console.log(e); } // Should never happen
  707. return false;
  708. }
  709. // Check if the source IP address is allowed, return domain if allowed
  710. // If there is a fail and null is returned, the request or connection is closed already.
  711. function checkUserIpAddress(req, res) {
  712. if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
  713. if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
  714. const domain = (req.url ? getDomain(req) : getDomain(res));
  715. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  716. if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
  717. if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
  718. return domain;
  719. }
  720. // Check if the source IP address is allowed, return domain if allowed
  721. // If there is a fail and null is returned, the request or connection is closed already.
  722. function checkAgentIpAddress(req, res) {
  723. if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
  724. if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
  725. const domain = (req.url ? getDomain(req) : getDomain(res));
  726. if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
  727. if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
  728. return domain;
  729. }
  730. // Return the current domain of the request
  731. // Request or connection says open regardless of the response
  732. function getDomain(req) {
  733. if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
  734. if ((req.hostname == 'localhost') && (req.query.domainid != null)) { const d = parent.config.domains[req.query.domainid]; if (d != null) return d; } // This is a localhost access with the domainid specified in the URL
  735. if (req.hostname != null) { const d = obj.dnsDomains[req.hostname.toLowerCase()]; if (d != null) return d; } // If this is a DNS name domain, return it here.
  736. const x = req.url.split('/');
  737. if (x.length < 2) return parent.config.domains[''];
  738. const y = parent.config.domains[x[1].toLowerCase()];
  739. if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
  740. return parent.config.domains[''];
  741. }
  742. function handleLogoutRequest(req, res) {
  743. const domain = checkUserIpAddress(req, res);
  744. if (domain == null) { return; }
  745. if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
  746. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  747. // If a HTTP header is required, check new UserRequiredHttpHeader
  748. if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
  749. res.set({ 'Cache-Control': 'no-store' });
  750. // Destroy the user's session to log them out will be re-created next request
  751. var userid = req.session.userid;
  752. if (req.session.userid) {
  753. var user = obj.users[req.session.userid];
  754. if (user != null) {
  755. obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
  756. obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
  757. }
  758. if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
  759. }
  760. req.session = null;
  761. parent.debug('web', 'handleLogoutRequest: success.');
  762. // If this user was logged in using an authentication strategy and there is a logout URL, use it.
  763. if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
  764. let logouturl = null;
  765. let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
  766. // Setup logout url for oidc
  767. if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
  768. if (typeof domain.authstrategies.oidc.logouturl == 'string') {
  769. logouturl = domain.authstrategies.oidc.logouturl;
  770. } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
  771. logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + '?post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri;
  772. } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
  773. logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
  774. }
  775. // Log out all other strategies
  776. } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
  777. // If custom logout was setup, use it
  778. if (logouturl != null) {
  779. parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
  780. res.redirect(logouturl);
  781. return;
  782. }
  783. }
  784. // This is the default logout redirect to the login page
  785. if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); }
  786. }
  787. // Return an object with 2FA type if 2-step auth can be skipped
  788. function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
  789. if (parent.config.settings.no2factorauth == true) return null;
  790. // If this login occurred using a login token, no 2FA needed.
  791. if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
  792. // Check if we can skip 2nd factor auth because of the source IP address
  793. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  794. for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
  795. }
  796. // Check if a 2nd factor cookie is present
  797. if (typeof req.headers.cookie == 'string') {
  798. const cookies = req.headers.cookie.split('; ');
  799. for (var i in cookies) {
  800. if (cookies[i].startsWith('twofactor=')) {
  801. var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
  802. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
  803. }
  804. }
  805. }
  806. return null;
  807. }
  808. // Return true if this user has 2-step auth active
  809. function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
  810. // If this login occurred using a login token, no 2FA needed.
  811. if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
  812. // Check if we can skip 2nd factor auth because of the source IP address
  813. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  814. for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
  815. }
  816. // Check if a 2nd factor cookie is present
  817. if (typeof req.headers.cookie == 'string') {
  818. const cookies = req.headers.cookie.split('; ');
  819. for (var i in cookies) {
  820. if (cookies[i].startsWith('twofactor=')) {
  821. var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
  822. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
  823. }
  824. }
  825. }
  826. // See if SMS 2FA is available
  827. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  828. // See if Messenger 2FA is available
  829. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  830. // Check if a 2nd factor is present
  831. return ((parent.config.settings.no2factorauth !== true) && (msg2fa || sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
  832. }
  833. // Check the 2-step auth token
  834. function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
  835. parent.debug('web', 'checkUserOneTimePassword()');
  836. const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
  837. if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
  838. // Check if we can use OTP tokens with email
  839. var otpemail = (domain.mailserver != null);
  840. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
  841. var otpsms = (parent.smsserver != null);
  842. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
  843. var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
  844. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
  845. // Check 2FA login cookie
  846. if ((token != null) && (token.startsWith('cookie='))) {
  847. var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
  848. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
  849. }
  850. // Check email key
  851. if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
  852. var deltaTime = (Date.now() - user.otpekey.d);
  853. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
  854. user.otpekey = {};
  855. obj.db.SetUser(user);
  856. parent.debug('web', 'checkUserOneTimePassword: success (email).');
  857. func(true, { twoFactorType: 'email' });
  858. return;
  859. }
  860. }
  861. // Check SMS key
  862. if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
  863. var deltaTime = (Date.now() - user.otpsms.d);
  864. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
  865. delete user.otpsms;
  866. obj.db.SetUser(user);
  867. parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
  868. func(true, { twoFactorType: 'sms' });
  869. return;
  870. }
  871. }
  872. // Check messenger key
  873. if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
  874. var deltaTime = (Date.now() - user.otpmsg.d);
  875. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
  876. delete user.otpmsg;
  877. obj.db.SetUser(user);
  878. parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
  879. func(true, { twoFactorType: 'messenger' });
  880. return;
  881. }
  882. }
  883. // Check hardware key
  884. if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
  885. var authResponse = null;
  886. try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
  887. if ((authResponse != null) && (authResponse.clientDataJSON)) {
  888. // Get all WebAuthn keys
  889. var webAuthnKeys = [];
  890. for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
  891. if (webAuthnKeys.length > 0) {
  892. // Decode authentication response
  893. var clientAssertionResponse = { response: {} };
  894. clientAssertionResponse.id = authResponse.id;
  895. clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
  896. clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
  897. clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
  898. clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
  899. clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
  900. // Look for the key with clientAssertionResponse.id
  901. var webAuthnKey = null;
  902. for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
  903. // If we found a valid key to use, let's validate the response
  904. if (webAuthnKey != null) {
  905. // Figure out the origin
  906. var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  907. var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
  908. if (httpport != 443) { origin += ':' + httpport; }
  909. var u2fchallenge = null;
  910. if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
  911. var assertionExpectations = {
  912. challenge: u2fchallenge,
  913. origin: origin,
  914. factor: 'either',
  915. fmt: 'fido-u2f',
  916. publicKey: webAuthnKey.publicKey,
  917. prevCounter: webAuthnKey.counter,
  918. userHandle: Buffer.from(user._id, 'binary').toString('base64')
  919. };
  920. var webauthnResponse = null;
  921. try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
  922. if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
  923. // Update the hardware key counter and accept the 2nd factor
  924. webAuthnKey.counter = webauthnResponse.counter;
  925. obj.db.SetUser(user);
  926. parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
  927. func(true, { twoFactorType: 'fido' });
  928. } else {
  929. parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
  930. func(false);
  931. }
  932. return;
  933. }
  934. }
  935. }
  936. }
  937. // Check Google Authenticator
  938. const otplib = require('otplib')
  939. otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
  940. if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
  941. parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
  942. func(true, { twoFactorType: 'otp' });
  943. return;
  944. };
  945. // Check written down keys
  946. if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
  947. var tokenNumber = parseInt(token);
  948. for (var i = 0; i < user.otpkeys.keys.length; i++) {
  949. if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
  950. parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
  951. user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
  952. }
  953. }
  954. }
  955. // Check OTP hardware key (Yubikey OTP)
  956. if ((domain.yubikey != null) && (domain.yubikey.id != null) && (domain.yubikey.secret != null) && (user.otphkeys != null) && (user.otphkeys.length > 0) && (typeof (token) == 'string') && (token.length == 44)) {
  957. var keyId = token.substring(0, 12);
  958. // Find a matching OTP key
  959. var match = false;
  960. for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
  961. // If we have a match, check the OTP
  962. if (match === true) {
  963. var yubikeyotp = require('yubikeyotp');
  964. var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
  965. if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
  966. yubikeyotp.verifyOTP(request, function (err, results) {
  967. if ((results != null) && (results.status == 'OK')) {
  968. parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
  969. func(true, { twoFactorType: 'hwotp' });
  970. } else {
  971. parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
  972. func(false);
  973. }
  974. });
  975. return;
  976. }
  977. }
  978. parent.debug('web', 'checkUserOneTimePassword: fail (2).');
  979. func(false);
  980. }
  981. // Return a U2F hardware key challenge
  982. function getHardwareKeyChallenge(req, domain, user, func) {
  983. var sec = {};
  984. if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
  985. if (user.otphkeys && (user.otphkeys.length > 0)) {
  986. // Get all WebAuthn keys
  987. var webAuthnKeys = [];
  988. for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
  989. if (webAuthnKeys.length > 0) {
  990. // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
  991. var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
  992. for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
  993. sec.u2f = authnOptions.challenge;
  994. req.session.e = parent.encryptSessionData(sec);
  995. parent.debug('web', 'getHardwareKeyChallenge: success');
  996. func(JSON.stringify(authnOptions));
  997. return;
  998. }
  999. }
  1000. // Remove the challenge if present
  1001. if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
  1002. parent.debug('web', 'getHardwareKeyChallenge: fail');
  1003. func('');
  1004. }
  1005. // Redirect a root request to a different page
  1006. function handleRootRedirect(req, res, direct) {
  1007. const domain = checkUserIpAddress(req, res);
  1008. if (domain == null) { return; }
  1009. res.redirect(domain.rootredirect + getQueryPortion(req));
  1010. }
  1011. function handleLoginRequest(req, res, direct) {
  1012. const domain = checkUserIpAddress(req, res);
  1013. if (domain == null) { return; }
  1014. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1015. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1016. if (req.session == null) { req.session = {}; }
  1017. // Check if this is a banned ip address
  1018. if (obj.checkAllowLogin(req) == false) {
  1019. // Wait and redirect the user
  1020. setTimeout(function () {
  1021. req.session.messageid = 114; // IP address blocked, try again later.
  1022. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1023. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1024. return;
  1025. }
  1026. // Normally, use the body username/password. If this is a token, use the username/password in the session.
  1027. var xusername = req.body.username, xpassword = req.body.password;
  1028. if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
  1029. const sec = parent.decryptSessionData(req.session.e);
  1030. xusername = sec.tuser; xpassword = sec.tpass;
  1031. }
  1032. // Authenticate the user
  1033. obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
  1034. if (userid) {
  1035. var user = obj.users[userid];
  1036. // Check if we are in maintenance mode
  1037. if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
  1038. req.session.messageid = 115; // Server under maintenance
  1039. req.session.loginmode = 1;
  1040. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1041. return;
  1042. }
  1043. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
  1044. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  1045. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  1046. var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  1047. // Check if two factor can be skipped
  1048. const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
  1049. // Check if this user has 2-step login active
  1050. if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
  1051. if ((req.body.hwtoken == '**timeout**')) {
  1052. delete req.session; // Clear the session
  1053. res.redirect(domain.url + getQueryPortion(req));
  1054. return;
  1055. }
  1056. if ((req.body.hwtoken == '**email**') && email2fa) {
  1057. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  1058. obj.db.SetUser(user);
  1059. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  1060. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  1061. req.session.messageid = 2; // "Email sent" message
  1062. req.session.loginmode = 4;
  1063. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1064. return;
  1065. }
  1066. if ((req.body.hwtoken == '**sms**') && sms2fa) {
  1067. // Cause a token to be sent to the user's phone number
  1068. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1069. obj.db.SetUser(user);
  1070. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  1071. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  1072. // Ask for a login token & confirm sms was sent
  1073. req.session.messageid = 4; // "SMS sent" message
  1074. req.session.loginmode = 4;
  1075. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1076. return;
  1077. }
  1078. if ((req.body.hwtoken == '**msg**') && msg2fa) {
  1079. // Cause a token to be sent to the user's messenger account
  1080. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1081. obj.db.SetUser(user);
  1082. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  1083. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  1084. // Ask for a login token & confirm message was sent
  1085. req.session.messageid = 6; // "Message sent" message
  1086. req.session.loginmode = 4;
  1087. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1088. return;
  1089. }
  1090. // Handle device push notification 2FA request
  1091. // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
  1092. if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
  1093. const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  1094. const sessioncode = obj.crypto.randomBytes(24).toString('base64');
  1095. // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
  1096. const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
  1097. // Get the HTTPS port
  1098. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
  1099. // Get the agent connection server name
  1100. var serverName = obj.getWebServerName(domain, req);
  1101. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  1102. // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  1103. var xdomain = (domain.dns == null) ? domain.id : '';
  1104. if (xdomain != '') xdomain += '/';
  1105. var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
  1106. // Request that the login page wait for device auth
  1107. req.session.messageid = 5; // "Sending notification..." message
  1108. req.session.passhint = url;
  1109. req.session.loginmode = 8;
  1110. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1111. return;
  1112. }
  1113. checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
  1114. if (result == false) {
  1115. var randomWaitTime = 0;
  1116. // Check if 2FA is allowed for this IP address
  1117. if (obj.checkAllow2Fa(req) == false) {
  1118. // Wait and redirect the user
  1119. setTimeout(function () {
  1120. req.session.messageid = 114; // IP address blocked, try again later.
  1121. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1122. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1123. return;
  1124. }
  1125. // 2-step auth is required, but the token is not present or not valid.
  1126. if ((req.body.token != null) || (req.body.hwtoken != null)) {
  1127. randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
  1128. req.session.messageid = 108; // Invalid token, try again.
  1129. obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.port, { useragent: req.headers['user-agent'] });
  1130. parent.debug('web', 'handleLoginRequest: invalid 2FA token');
  1131. const ua = obj.getUserAgentInfo(req);
  1132. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
  1133. obj.setbad2Fa(req);
  1134. } else {
  1135. parent.debug('web', 'handleLoginRequest: 2FA token required');
  1136. }
  1137. // Wait and redirect the user
  1138. setTimeout(function () {
  1139. req.session.loginmode = 4;
  1140. if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
  1141. if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
  1142. if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; }
  1143. if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
  1144. req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
  1145. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1146. }, randomWaitTime);
  1147. } else {
  1148. // Check if we need to remember this device
  1149. if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
  1150. var maxCookieAge = domain.twofactorcookiedurationdays;
  1151. if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
  1152. const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
  1153. res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
  1154. }
  1155. // Check if email address needs to be confirmed
  1156. const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
  1157. if (emailcheck && (user.emailVerified !== true)) {
  1158. parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
  1159. req.session.messageid = 3; // "Email verification required" message
  1160. req.session.loginmode = 7;
  1161. req.session.passhint = user.email;
  1162. req.session.cuserid = userid;
  1163. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1164. return;
  1165. }
  1166. // Login successful
  1167. parent.debug('web', 'handleLoginRequest: successful 2FA login');
  1168. if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
  1169. completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
  1170. }
  1171. });
  1172. return;
  1173. }
  1174. // Check if email address needs to be confirmed
  1175. const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
  1176. if (emailcheck && (user.emailVerified !== true)) {
  1177. parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
  1178. req.session.messageid = 3; // "Email verification required" message
  1179. req.session.loginmode = 7;
  1180. req.session.passhint = user.email;
  1181. req.session.cuserid = userid;
  1182. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1183. return;
  1184. }
  1185. // Login successful
  1186. parent.debug('web', 'handleLoginRequest: successful login');
  1187. if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
  1188. completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
  1189. } else {
  1190. // Login failed, log the error
  1191. obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
  1192. // Wait a random delay
  1193. setTimeout(function () {
  1194. // If the account is locked, display that.
  1195. if (typeof xusername == 'string') {
  1196. var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
  1197. if (err == 'locked') {
  1198. parent.debug('web', 'handleLoginRequest: login failed, locked account');
  1199. req.session.messageid = 110; // Account locked.
  1200. const ua = obj.getUserAgentInfo(req);
  1201. obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + req.clientIp, msgid: 109, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
  1202. obj.setbadLogin(req);
  1203. } else if (err == 'denied') {
  1204. parent.debug('web', 'handleLoginRequest: login failed, access denied');
  1205. req.session.messageid = 111; // Access denied.
  1206. const ua = obj.getUserAgentInfo(req);
  1207. obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Denied user login from ' + req.clientIp, msgid: 155, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
  1208. obj.setbadLogin(req);
  1209. } else {
  1210. parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
  1211. req.session.messageid = 112; // Login failed, check username and password.
  1212. const ua = obj.getUserAgentInfo(req);
  1213. obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp, msgid: 110, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
  1214. obj.setbadLogin(req);
  1215. }
  1216. }
  1217. // Clean up login mode and display password hint if present.
  1218. delete req.session.loginmode;
  1219. if ((passhint != null) && (passhint.length > 0)) {
  1220. req.session.passhint = passhint;
  1221. } else {
  1222. delete req.session.passhint;
  1223. }
  1224. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1225. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
  1226. }
  1227. });
  1228. }
  1229. function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
  1230. // Check if we need to change the password
  1231. if ((typeof user.passchange == 'number') && ((user.passchange == -1) || ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.reset == 'number') && (user.passchange + (domain.passwordrequirements.reset * 86400) < Math.floor(Date.now() / 1000))))) {
  1232. // Request a password change
  1233. parent.debug('web', 'handleLoginRequest: login ok, password change requested');
  1234. req.session.loginmode = 6;
  1235. req.session.messageid = 113; // Password change requested.
  1236. // Decrypt any session data
  1237. const sec = parent.decryptSessionData(req.session.e);
  1238. sec.rtuser = xusername;
  1239. sec.rtpass = xpassword;
  1240. req.session.e = parent.encryptSessionData(sec);
  1241. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1242. return;
  1243. }
  1244. // Save login time
  1245. user.pastlogin = user.login;
  1246. user.login = user.access = Math.floor(Date.now() / 1000);
  1247. obj.db.SetUser(user);
  1248. // Notify account login
  1249. const targets = ['*', 'server-users', user._id];
  1250. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  1251. const ua = obj.getUserAgentInfo(req);
  1252. const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login from ' + req.clientIp + ', ' + ua.browserStr + ', ' + ua.osStr, domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], rport: req.connection.remotePort };
  1253. if (loginOptions != null) {
  1254. if ((loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { loginEvent.tokenName = loginOptions.tokenName; loginEvent.tokenUser = loginOptions.tokenUser; } // If a login token was used, add it to the event.
  1255. if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
  1256. }
  1257. obj.parent.DispatchEvent(targets, obj, loginEvent);
  1258. // Regenerate session when signing in to prevent fixation
  1259. //req.session.regenerate(function () {
  1260. // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
  1261. delete req.session.e;
  1262. delete req.session.u2f;
  1263. delete req.session.loginmode;
  1264. delete req.session.tuserid;
  1265. delete req.session.tuser;
  1266. delete req.session.tpass;
  1267. delete req.session.temail;
  1268. delete req.session.tsms;
  1269. delete req.session.tmsg;
  1270. delete req.session.tpush;
  1271. delete req.session.messageid;
  1272. delete req.session.passhint;
  1273. delete req.session.cuserid;
  1274. delete req.session.expire;
  1275. delete req.session.currentNode;
  1276. req.session.userid = userid;
  1277. req.session.ip = req.clientIp;
  1278. setSessionRandom(req);
  1279. obj.parent.authLog('https', 'Accepted password for ' + (xusername ? xusername : userid) + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
  1280. // If a login token was used, add this information and expire time to the session.
  1281. if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
  1282. req.session.loginToken = loginOptions.tokenUser;
  1283. if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
  1284. }
  1285. if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
  1286. if (req.body.host) {
  1287. // TODO: This is a terrible search!!! FIX THIS.
  1288. /*
  1289. obj.db.GetAllType('node', function (err, docs) {
  1290. for (var i = 0; i < docs.length; i++) {
  1291. if (docs[i].name == req.body.host) {
  1292. req.session.currentNode = docs[i]._id;
  1293. break;
  1294. }
  1295. }
  1296. console.log("CurrentNode: " + req.session.currentNode);
  1297. // This redirect happens after finding node is completed
  1298. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1299. });
  1300. */
  1301. parent.debug('web', 'handleLoginRequest: login ok (1)');
  1302. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
  1303. } else {
  1304. parent.debug('web', 'handleLoginRequest: login ok (2)');
  1305. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1306. }
  1307. //});
  1308. }
  1309. function handleCreateAccountRequest(req, res, direct) {
  1310. const domain = checkUserIpAddress(req, res);
  1311. if (domain == null) { return; }
  1312. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
  1313. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1314. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1315. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1316. // Check if we are in maintenance mode
  1317. if (parent.config.settings.maintenancemode != null) {
  1318. req.session.messageid = 115; // Server under maintenance
  1319. req.session.loginmode = 1;
  1320. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1321. return;
  1322. }
  1323. // Always lowercase the email address
  1324. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1325. // If the email is the username, set this here.
  1326. if (domain.usernameisemail) { req.body.username = req.body.email; }
  1327. // Check if there is domain.newAccountToken, check if supplied token is valid
  1328. if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
  1329. parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
  1330. req.session.loginmode = 2;
  1331. req.session.messageid = 103; // Invalid account creation token.
  1332. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1333. return;
  1334. }
  1335. // If needed, check the new account creation CAPTCHA
  1336. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  1337. const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  1338. if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
  1339. req.session.loginmode = 2;
  1340. req.session.messageid = 117; // Invalid security check
  1341. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1342. return;
  1343. }
  1344. }
  1345. // Accounts that start with ~ are not allowed
  1346. if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
  1347. parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
  1348. req.session.loginmode = 2;
  1349. req.session.messageid = 100; // Unable to create account.
  1350. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1351. return;
  1352. }
  1353. // Count the number of users in this domain
  1354. var domainUserCount = 0;
  1355. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
  1356. // Check if we are allowed to create new users using the login screen
  1357. if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
  1358. parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
  1359. res.sendStatus(401);
  1360. return;
  1361. }
  1362. // Check if this request is for an allows email domain
  1363. if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
  1364. var i = -1;
  1365. if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
  1366. if (i == -1) {
  1367. parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
  1368. req.session.loginmode = 2;
  1369. req.session.messageid = 100; // Unable to create account.
  1370. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1371. return;
  1372. }
  1373. var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
  1374. for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
  1375. if (emailok == false) {
  1376. parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
  1377. req.session.loginmode = 2;
  1378. req.session.messageid = 100; // Unable to create account.
  1379. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1380. return;
  1381. }
  1382. }
  1383. // Check if we exceed the maximum number of user accounts
  1384. obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
  1385. if (maxExceed) {
  1386. parent.debug('web', 'handleCreateAccountRequest: account limit reached');
  1387. req.session.loginmode = 2;
  1388. req.session.messageid = 101; // Account limit reached.
  1389. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1390. } else {
  1391. if (!obj.common.validateUsername(req.body.username, 1, 64) || !obj.common.validateEmail(req.body.email, 1, 256) || !obj.common.validateString(req.body.password1, 1, 256) || !obj.common.validateString(req.body.password2, 1, 256) || (req.body.password1 != req.body.password2) || req.body.username == '~' || !obj.common.checkPasswordRequirements(req.body.password1, domain.passwordrequirements)) {
  1392. parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
  1393. req.session.loginmode = 2;
  1394. req.session.messageid = 100; // Unable to create account.
  1395. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1396. } else {
  1397. // Check if this email was already verified
  1398. obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
  1399. if ((docs != null) && (docs.length > 0)) {
  1400. parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
  1401. req.session.loginmode = 2;
  1402. req.session.messageid = 102; // Existing account with this email address.
  1403. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1404. } else {
  1405. // Check if user exists
  1406. if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
  1407. parent.debug('web', 'handleCreateAccountRequest: Username already exists');
  1408. req.session.loginmode = 2;
  1409. req.session.messageid = 104; // Username already exists.
  1410. } else {
  1411. var user = { type: 'user', _id: 'user/' + domain.id + '/' + req.body.username.toLowerCase(), name: req.body.username, email: req.body.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
  1412. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
  1413. if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
  1414. if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) { hint = hint.substring(0, 250); } user.passhint = hint; }
  1415. if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
  1416. // Auto-join any user groups
  1417. if (typeof domain.newaccountsusergroups == 'object') {
  1418. for (var i in domain.newaccountsusergroups) {
  1419. var ugrpid = domain.newaccountsusergroups[i];
  1420. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  1421. var ugroup = obj.userGroups[ugrpid];
  1422. if (ugroup != null) {
  1423. // Add group to the user
  1424. if (user.links == null) { user.links = {}; }
  1425. user.links[ugroup._id] = { rights: 1 };
  1426. // Add user to the group
  1427. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  1428. db.Set(ugroup);
  1429. // Notify user group change
  1430. var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
  1431. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  1432. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  1433. }
  1434. }
  1435. }
  1436. obj.users[user._id] = user;
  1437. req.session.userid = user._id;
  1438. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  1439. setSessionRandom(req);
  1440. // Create a user, generate a salt and hash the password
  1441. require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
  1442. if (err) throw err;
  1443. user.salt = salt;
  1444. user.hash = hash;
  1445. delete user.passtype;
  1446. obj.db.SetUser(user);
  1447. // Send the verification email
  1448. if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); }
  1449. }, 0);
  1450. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id };
  1451. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
  1452. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  1453. }
  1454. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1455. }
  1456. });
  1457. }
  1458. }
  1459. });
  1460. }
  1461. // Called to process an account password reset
  1462. function handleResetPasswordRequest(req, res, direct) {
  1463. const domain = checkUserIpAddress(req, res);
  1464. if (domain == null) { return; }
  1465. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1466. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1467. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1468. // Decrypt any session data
  1469. const sec = parent.decryptSessionData(req.session.e);
  1470. // Check everything is ok
  1471. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
  1472. if ((allowAccountReset === false) || (domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof sec.rtuser != 'string') || (typeof sec.rtpass != 'string')) {
  1473. parent.debug('web', 'handleResetPasswordRequest: checks failed');
  1474. delete req.session.e;
  1475. delete req.session.u2f;
  1476. delete req.session.loginmode;
  1477. delete req.session.tuserid;
  1478. delete req.session.tuser;
  1479. delete req.session.tpass;
  1480. delete req.session.temail;
  1481. delete req.session.tsms;
  1482. delete req.session.tmsg;
  1483. delete req.session.tpush;
  1484. delete req.session.messageid;
  1485. delete req.session.passhint;
  1486. delete req.session.cuserid;
  1487. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1488. return;
  1489. }
  1490. // Authenticate the user
  1491. obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
  1492. if (userid) {
  1493. // Login
  1494. var user = obj.users[userid];
  1495. // If we have password requirements, check this here.
  1496. if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
  1497. parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
  1498. req.session.loginmode = 6;
  1499. req.session.messageid = 105; // Password rejected, use a different one.
  1500. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1501. return;
  1502. }
  1503. // Check if the password is the same as a previous one
  1504. obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
  1505. if (result != 0) {
  1506. // This is the same password as an older one, request a password change again
  1507. parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
  1508. req.session.loginmode = 6;
  1509. req.session.messageid = 105; // Password rejected, use a different one.
  1510. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1511. } else {
  1512. // Update the password, use a different salt.
  1513. require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
  1514. const nowSeconds = Math.floor(Date.now() / 1000);
  1515. if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
  1516. if (domain.passwordrequirements != null) {
  1517. // Save password hint if this feature is enabled
  1518. if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
  1519. // Save previous password if this feature is enabled
  1520. if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  1521. if (user.oldpasswords == null) { user.oldpasswords = []; }
  1522. user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
  1523. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  1524. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  1525. }
  1526. }
  1527. user.salt = salt;
  1528. user.hash = hash;
  1529. user.passchange = user.access = nowSeconds;
  1530. delete user.passtype;
  1531. obj.db.SetUser(user);
  1532. // Event the account change
  1533. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
  1534. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  1535. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1536. // Login successful
  1537. parent.debug('web', 'handleResetPasswordRequest: success');
  1538. req.session.userid = userid;
  1539. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  1540. setSessionRandom(req);
  1541. const sec = parent.decryptSessionData(req.session.e);
  1542. completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
  1543. }, 0);
  1544. }
  1545. }, 0);
  1546. } else {
  1547. // Failed, error out.
  1548. parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
  1549. delete req.session.e;
  1550. delete req.session.u2f;
  1551. delete req.session.loginmode;
  1552. delete req.session.tuserid;
  1553. delete req.session.tuser;
  1554. delete req.session.tpass;
  1555. delete req.session.temail;
  1556. delete req.session.tsms;
  1557. delete req.session.tmsg;
  1558. delete req.session.tpush;
  1559. delete req.session.messageid;
  1560. delete req.session.passhint;
  1561. delete req.session.cuserid;
  1562. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1563. return;
  1564. }
  1565. });
  1566. }
  1567. // Called to process an account reset request
  1568. function handleResetAccountRequest(req, res, direct) {
  1569. const domain = checkUserIpAddress(req, res);
  1570. if (domain == null) { return; }
  1571. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
  1572. if ((allowAccountReset === false) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (obj.args.lanonly == true) || (obj.parent.certificates.CommonName == null) || (obj.parent.certificates.CommonName.indexOf('.') == -1)) { parent.debug('web', 'handleResetAccountRequest: check failed'); res.sendStatus(404); return; }
  1573. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1574. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1575. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1576. // Always lowercase the email address
  1577. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1578. // Get the email from the body or session.
  1579. var email = req.body.email;
  1580. if ((email == null) || (email == '')) { email = req.session.temail; }
  1581. // Check the email string format
  1582. if (!email || checkEmail(email) == false) {
  1583. parent.debug('web', 'handleResetAccountRequest: Invalid email');
  1584. req.session.loginmode = 3;
  1585. req.session.messageid = 106; // Invalid email.
  1586. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1587. } else {
  1588. obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
  1589. // Remove all accounts that start with ~ since they are special accounts.
  1590. var cleanDocs = [];
  1591. if ((err == null) && (docs.length > 0)) {
  1592. for (var i in docs) {
  1593. const user = docs[i];
  1594. const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
  1595. const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
  1596. if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
  1597. }
  1598. }
  1599. docs = cleanDocs;
  1600. // Check if we have any account that match this email address
  1601. if ((err != null) || (docs.length == 0)) {
  1602. parent.debug('web', 'handleResetAccountRequest: Account not found');
  1603. req.session.loginmode = 3;
  1604. req.session.messageid = 1; // If valid, reset mail sent. Instead of "Account not found" (107), we send this hold on message so users can't know if this account exists or not.
  1605. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1606. } else {
  1607. // If many accounts have the same validated e-mail, we are going to use the first one for display, but sent a reset email for all accounts.
  1608. var responseSent = false;
  1609. for (var i in docs) {
  1610. var user = docs[i];
  1611. if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
  1612. // Second factor setup, request it now.
  1613. checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
  1614. if (result == false) {
  1615. if (i == 0) {
  1616. // Check if 2FA is allowed for this IP address
  1617. if (obj.checkAllow2Fa(req) == false) {
  1618. // Wait and redirect the user
  1619. setTimeout(function () {
  1620. req.session.messageid = 114; // IP address blocked, try again later.
  1621. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1622. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1623. return;
  1624. }
  1625. // 2-step auth is required, but the token is not present or not valid.
  1626. parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
  1627. if ((req.body.token != null) || (req.body.hwtoken != null)) {
  1628. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  1629. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  1630. if ((req.body.hwtoken == '**sms**') && sms2fa) {
  1631. // Cause a token to be sent to the user's phone number
  1632. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1633. obj.db.SetUser(user);
  1634. parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
  1635. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  1636. req.session.messageid = 4; // SMS sent.
  1637. } else if ((req.body.hwtoken == '**msg**') && msg2fa) {
  1638. // Cause a token to be sent to the user's messager account
  1639. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1640. obj.db.SetUser(user);
  1641. parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
  1642. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  1643. req.session.messageid = 6; // Message sent.
  1644. } else {
  1645. req.session.messageid = 108; // Invalid token, try again.
  1646. const ua = obj.getUserAgentInfo(req);
  1647. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
  1648. obj.setbad2Fa(req);
  1649. }
  1650. }
  1651. req.session.loginmode = 5;
  1652. req.session.temail = email;
  1653. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1654. }
  1655. } else {
  1656. // Send email to perform recovery.
  1657. delete req.session.temail;
  1658. if (domain.mailserver != null) {
  1659. domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1660. if (i == 0) {
  1661. parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
  1662. req.session.loginmode = 1;
  1663. req.session.messageid = 1; // If valid, reset mail sent.
  1664. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1665. }
  1666. } else {
  1667. if (i == 0) {
  1668. parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
  1669. req.session.loginmode = 3;
  1670. req.session.messageid = 109; // Unable to sent email.
  1671. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1672. }
  1673. }
  1674. }
  1675. });
  1676. } else {
  1677. // No second factor, send email to perform recovery.
  1678. if (domain.mailserver != null) {
  1679. domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1680. if (i == 0) {
  1681. parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
  1682. req.session.loginmode = 1;
  1683. req.session.messageid = 1; // If valid, reset mail sent.
  1684. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1685. }
  1686. } else {
  1687. if (i == 0) {
  1688. parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
  1689. req.session.loginmode = 3;
  1690. req.session.messageid = 109; // Unable to sent email.
  1691. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1692. }
  1693. }
  1694. }
  1695. }
  1696. }
  1697. });
  1698. }
  1699. }
  1700. // Handle account email change and email verification request
  1701. function handleCheckAccountEmailRequest(req, res, direct) {
  1702. const domain = checkUserIpAddress(req, res);
  1703. if (domain == null) { return; }
  1704. if ((domain.mailserver == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.session.cuserid != 'string') || (obj.users[req.session.cuserid] == null) || (!obj.common.validateEmail(req.body.email, 1, 256))) { parent.debug('web', 'handleCheckAccountEmailRequest: failed checks.'); res.sendStatus(404); return; }
  1705. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1706. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1707. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1708. // Always lowercase the email address
  1709. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1710. // Get the email from the body or session.
  1711. var email = req.body.email;
  1712. if ((email == null) || (email == '')) { email = req.session.temail; }
  1713. // Check if this request is for an allows email domain
  1714. if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
  1715. var i = -1;
  1716. if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
  1717. if (i == -1) {
  1718. parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
  1719. req.session.loginmode = 7;
  1720. req.session.messageid = 106; // Invalid email.
  1721. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1722. return;
  1723. }
  1724. var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
  1725. for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
  1726. if (emailok == false) {
  1727. parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
  1728. req.session.loginmode = 7;
  1729. req.session.messageid = 106; // Invalid email.
  1730. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1731. return;
  1732. }
  1733. }
  1734. // Check the email string format
  1735. if (!email || checkEmail(email) == false) {
  1736. parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
  1737. req.session.loginmode = 7;
  1738. req.session.messageid = 106; // Invalid email.
  1739. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1740. } else {
  1741. // Check is email already exists
  1742. obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
  1743. if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
  1744. // Email already exists
  1745. req.session.messageid = 102; // Existing account with this email address.
  1746. } else {
  1747. // Update the user and notify of user email address change
  1748. var user = obj.users[req.session.cuserid];
  1749. if (user.email != email) {
  1750. user.email = email;
  1751. db.SetUser(user);
  1752. var targets = ['*', 'server-users', user._id];
  1753. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  1754. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
  1755. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  1756. parent.DispatchEvent(targets, obj, event);
  1757. }
  1758. // Send the verification email
  1759. domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1760. // Send the response
  1761. req.session.messageid = 2; // Email sent.
  1762. }
  1763. req.session.loginmode = 7;
  1764. delete req.session.cuserid;
  1765. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1766. });
  1767. }
  1768. }
  1769. // Called to process a web based email verification request
  1770. function handleCheckMailRequest(req, res) {
  1771. const domain = checkUserIpAddress(req, res);
  1772. if (domain == null) { return; }
  1773. if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
  1774. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1775. if (req.query.c != null) {
  1776. var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
  1777. if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
  1778. var idsplit = cookie.u.split('/');
  1779. if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
  1780. parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
  1781. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 1, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1782. } else {
  1783. obj.db.Get(cookie.u, function (err, docs) {
  1784. if (docs.length == 0) {
  1785. parent.debug('web', 'handleCheckMailRequest: Invalid username.');
  1786. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 2, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(idsplit[1]).replace(/'/g, '%27') }, req, domain));
  1787. } else {
  1788. var user = docs[0];
  1789. if (user.email != cookie.e) {
  1790. parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
  1791. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 3, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
  1792. } else {
  1793. if (cookie.a == 1) {
  1794. // Account email verification
  1795. if (user.emailVerified == true) {
  1796. parent.debug('web', 'handleCheckMailRequest: email already verified.');
  1797. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 4, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
  1798. } else {
  1799. obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
  1800. if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
  1801. parent.debug('web', 'handleCheckMailRequest: email already in use.');
  1802. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 5, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27') }, req, domain));
  1803. } else {
  1804. parent.debug('web', 'handleCheckMailRequest: email verification success.');
  1805. // Set the verified flag
  1806. obj.users[user._id].emailVerified = true;
  1807. user.emailVerified = true;
  1808. obj.db.SetUser(user);
  1809. // Event the change
  1810. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(user.email) + ')', domain: domain.id };
  1811. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  1812. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1813. // Send the confirmation page
  1814. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 6, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
  1815. // Send a notification
  1816. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
  1817. // Send to authLog
  1818. obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
  1819. }
  1820. });
  1821. }
  1822. } else if (cookie.a == 2) {
  1823. // Account reset
  1824. if (user.emailVerified != true) {
  1825. parent.debug('web', 'handleCheckMailRequest: email not verified.');
  1826. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 7, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.email), arg2: EscapeHtml(user.name) }, req, domain));
  1827. } else {
  1828. if (req.query.confirm == 1) {
  1829. // Set a temporary password
  1830. obj.crypto.randomBytes(16, function (err, buf) {
  1831. var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
  1832. require('./pass').hash(newpass, function (err, salt, hash, tag) {
  1833. if (err) throw err;
  1834. // Change the password
  1835. var userinfo = obj.users[user._id];
  1836. userinfo.salt = salt;
  1837. userinfo.hash = hash;
  1838. delete userinfo.passtype;
  1839. userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
  1840. delete userinfo.passhint;
  1841. obj.db.SetUser(userinfo);
  1842. // Event the change
  1843. var event = { etype: 'user', userid: user._id, username: userinfo.name, account: obj.CloneSafeUser(userinfo), action: 'accountchange', msg: 'Password reset for user ' + EscapeHtml(user.name), domain: domain.id };
  1844. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  1845. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1846. // Send the new password
  1847. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 8, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.name), arg2: EscapeHtml(newpass) }, req, domain));
  1848. parent.debug('web', 'handleCheckMailRequest: send temporary password.');
  1849. // Send to authLog
  1850. obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
  1851. }, 0);
  1852. });
  1853. } else {
  1854. // Display a link for the user to confirm password reset
  1855. // We must do this because GMail will also load this URL a few seconds after the user does and we don't want to cause two password resets.
  1856. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 14, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1857. }
  1858. }
  1859. } else {
  1860. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 9, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1861. }
  1862. }
  1863. }
  1864. });
  1865. }
  1866. } else {
  1867. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 10, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1868. }
  1869. }
  1870. }
  1871. // Called to process an agent invite GET/POST request
  1872. function handleInviteRequest(req, res) {
  1873. const domain = getDomain(req);
  1874. if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
  1875. if (domain.agentinvitecodes != true) { nice404(req, res); return; }
  1876. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1877. if ((req.body == null) || (req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
  1878. // Each for a device group that has this invite code.
  1879. for (var i in obj.meshes) {
  1880. if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].deleted == null) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) {
  1881. // Send invitation link, valid for 1 minute.
  1882. res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + (req.query.hide ? ('&hide=' + encodeURIComponent(req.query.hide)) : ''));
  1883. return;
  1884. }
  1885. }
  1886. render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
  1887. }
  1888. // Called to render the MSTSC (RDP) or SSH web page
  1889. function handleMSTSCRequest(req, res, page) {
  1890. const domain = getDomain(req);
  1891. if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
  1892. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1893. // Check if we are in maintenance mode
  1894. if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
  1895. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1896. return;
  1897. }
  1898. // Set features we want to send to this page
  1899. var features = 0;
  1900. if (domain.allowsavingdevicecredentials === false) { features |= 1; }
  1901. // Get the logged in user if present
  1902. var user = null;
  1903. // If there is a login token, use that
  1904. if (req.query.login != null) {
  1905. var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  1906. if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
  1907. }
  1908. // If no token, see if we have an active session
  1909. if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
  1910. // If still no user, see if we have a default user
  1911. if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
  1912. // No user login, exit now
  1913. if (user == null) { res.sendStatus(401); return; }
  1914. if (req.query.ws != null) {
  1915. // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
  1916. var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  1917. if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
  1918. // Fetch the node from the database
  1919. obj.db.Get(rcookie.nodeid, function (err, nodes) {
  1920. if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  1921. const node = nodes[0];
  1922. // Check if we have SSH/RDP credentials for this device
  1923. var serverCredentials = 0;
  1924. if (domain.allowsavingdevicecredentials !== false) {
  1925. if (page == 'ssh') {
  1926. if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
  1927. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
  1928. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
  1929. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
  1930. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format
  1931. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format
  1932. } else {
  1933. if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password in legacy format
  1934. if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
  1935. }
  1936. }
  1937. // Render the page
  1938. render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
  1939. });
  1940. return;
  1941. }
  1942. }
  1943. // Check the nodeid
  1944. if (req.query.node != null) {
  1945. var nodeidsplit = req.query.node.split('/');
  1946. if (nodeidsplit.length == 1) {
  1947. req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
  1948. } else if (nodeidsplit.length == 3) {
  1949. if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
  1950. } else {
  1951. req.query.node = null; // Bad nodeid
  1952. }
  1953. }
  1954. // If there is no nodeid, exit now
  1955. if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
  1956. // Fetch the node from the database
  1957. obj.db.Get(req.query.node, function (err, nodes) {
  1958. if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  1959. const node = nodes[0];
  1960. // Check access rights, must have remote control rights
  1961. if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
  1962. // Figure out the target port
  1963. var port = 0, serverCredentials = false;
  1964. if (page == 'ssh') {
  1965. // SSH port
  1966. port = 22;
  1967. if (typeof node.sshport == 'number') { port = node.sshport; }
  1968. // Check if we have SSH credentials for this device
  1969. if (domain.allowsavingdevicecredentials !== false) {
  1970. if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
  1971. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
  1972. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
  1973. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
  1974. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format
  1975. else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format
  1976. }
  1977. } else {
  1978. // RDP port
  1979. port = 3389;
  1980. if (typeof node.rdpport == 'number') { port = node.rdpport; }
  1981. // Check if we have RDP credentials for this device
  1982. if (domain.allowsavingdevicecredentials !== false) {
  1983. if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password
  1984. if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
  1985. }
  1986. }
  1987. if (req.query.port != null) { var qport = 0; try { qport = parseInt(req.query.port); } catch (ex) { } if ((typeof qport == 'number') && (qport > 0) && (qport < 65536)) { port = qport; } }
  1988. // Generate a cookie and respond
  1989. var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
  1990. render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
  1991. });
  1992. }
  1993. // Called to handle push-only requests
  1994. function handleFirebasePushOnlyRelayRequest(req, res) {
  1995. parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
  1996. if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
  1997. if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
  1998. if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
  1999. var data = null;
  2000. try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
  2001. if (typeof data != 'object') { res.sendStatus(404); return; }
  2002. if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
  2003. if (typeof data.payload != 'object') { res.sendStatus(404); return; }
  2004. if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
  2005. if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
  2006. if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
  2007. if (typeof data.options != 'object') { res.sendStatus(404); return; }
  2008. if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
  2009. if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
  2010. parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
  2011. obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
  2012. if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
  2013. });
  2014. }
  2015. // Called to handle two-way push notification relay request
  2016. function handleFirebaseRelayRequest(ws, req) {
  2017. parent.debug('email', 'handleFirebaseRelayRequest');
  2018. if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
  2019. if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
  2020. if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
  2021. if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; }
  2022. obj.parent.firebase.setupRelay(ws);
  2023. }
  2024. // Called to process an agent invite request
  2025. function handleAgentInviteRequest(req, res) {
  2026. const domain = getDomain(req);
  2027. if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
  2028. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2029. if (req.query.c != null) {
  2030. // A cookie is specified in the query string, use that
  2031. var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
  2032. if (cookie == null) { res.sendStatus(404); return; }
  2033. var mesh = obj.meshes[cookie.mid];
  2034. if (mesh == null) { res.sendStatus(404); return; }
  2035. var installflags = cookie.f;
  2036. if (typeof installflags != 'number') { installflags = 0; }
  2037. var showagents = cookie.ag;
  2038. if (typeof showagents != 'number') { showagents = 0; }
  2039. parent.debug('web', 'handleAgentInviteRequest using cookie.');
  2040. // Build the mobile agent URL, this is used to connect mobile devices
  2041. var agentServerName = obj.getWebServerName(domain, req);
  2042. if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
  2043. var xdomain = (domain.dns == null) ? domain.id : '';
  2044. var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2045. if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  2046. if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  2047. var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
  2048. var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
  2049. render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags, showagents: showagents, magenturl: magenturl, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
  2050. } else if (req.query.m != null) {
  2051. // The MeshId is specified in the query string, use that
  2052. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
  2053. if (mesh == null) { res.sendStatus(404); return; }
  2054. var installflags = 0;
  2055. if (req.query.f) { installflags = parseInt(req.query.f); }
  2056. if (typeof installflags != 'number') { installflags = 0; }
  2057. var showagents = 0;
  2058. if (req.query.f) { showagents = parseInt(req.query.ag); }
  2059. if (typeof showagents != 'number') { showagents = 0; }
  2060. parent.debug('web', 'handleAgentInviteRequest using meshid.');
  2061. // Build the mobile agent URL, this is used to connect mobile devices
  2062. var agentServerName = obj.getWebServerName(domain, req);
  2063. if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
  2064. var xdomain = (domain.dns == null) ? domain.id : '';
  2065. var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2066. if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  2067. if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  2068. var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
  2069. var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
  2070. render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags, showagents: showagents, magenturl: magenturl, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
  2071. }
  2072. }
  2073. // Called to process an agent invite request
  2074. function handleUserImageRequest(req, res) {
  2075. const domain = getDomain(req);
  2076. if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
  2077. if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
  2078. var imageUserId = req.session.userid;
  2079. if ((req.query.id != null)) {
  2080. var user = obj.users[req.session.userid];
  2081. if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
  2082. imageUserId = 'user/' + domain.id + '/' + req.query.id;
  2083. }
  2084. obj.db.Get('im' + imageUserId, function (err, docs) {
  2085. if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
  2086. var imagebase64 = docs[0].image;
  2087. if (imagebase64.startsWith('data:image/png;base64,')) {
  2088. res.set('Content-Type', 'image/png');
  2089. res.set({ 'Cache-Control': 'no-store' });
  2090. res.send(Buffer.from(imagebase64.substring(22), 'base64'));
  2091. } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
  2092. res.set('Content-Type', 'image/jpeg');
  2093. res.set({ 'Cache-Control': 'no-store' });
  2094. res.send(Buffer.from(imagebase64.substring(23), 'base64'));
  2095. } else {
  2096. res.sendStatus(404);
  2097. }
  2098. });
  2099. }
  2100. function handleDeleteAccountRequest(req, res, direct) {
  2101. parent.debug('web', 'handleDeleteAccountRequest()');
  2102. const domain = checkUserIpAddress(req, res);
  2103. if (domain == null) { return; }
  2104. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
  2105. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2106. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  2107. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  2108. var user = null;
  2109. if (req.body.authcookie) {
  2110. // If a authentication cookie is provided, decode it here
  2111. var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  2112. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
  2113. } else {
  2114. // Check if the user is logged and we have all required parameters
  2115. if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
  2116. parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
  2117. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2118. return;
  2119. } else {
  2120. user = obj.users[req.session.userid];
  2121. }
  2122. }
  2123. if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
  2124. if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
  2125. // Check if the password is correct
  2126. obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
  2127. var deluser = obj.users[userid];
  2128. if ((userid != null) && (deluser != null)) {
  2129. // Remove all links to this user
  2130. if (deluser.links != null) {
  2131. for (var i in deluser.links) {
  2132. if (i.startsWith('mesh/')) {
  2133. // Get the device group
  2134. var mesh = obj.meshes[i];
  2135. if (mesh) {
  2136. // Remove user from the mesh
  2137. if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
  2138. // Notify mesh change
  2139. var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
  2140. var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id, invite: mesh.invite };
  2141. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
  2142. parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
  2143. }
  2144. } else if (i.startsWith('node/')) {
  2145. // Get the node and the rights for this node
  2146. obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
  2147. if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
  2148. // Remove the link and save the node to the database
  2149. delete node.links[deluser._id];
  2150. if (Object.keys(node.links).length == 0) { delete node.links; }
  2151. db.Set(obj.cleanDevice(node));
  2152. // Event the node change
  2153. var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: ('Removed user device rights for ' + node.name), node: obj.CloneSafeNode(node) }
  2154. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
  2155. parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
  2156. });
  2157. } else if (i.startsWith('ugrp/')) {
  2158. // Get the device group
  2159. var ugroup = obj.userGroups[i];
  2160. if (ugroup) {
  2161. // Remove user from the user group
  2162. if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
  2163. // Notify user group change
  2164. var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
  2165. var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Removed user ' + deluser.name + ' from user group ' + ugroup.name, addUserDomain: domain.id };
  2166. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  2167. parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
  2168. }
  2169. }
  2170. }
  2171. }
  2172. obj.db.Remove('ws' + deluser._id); // Remove user web state
  2173. obj.db.Remove('nt' + deluser._id); // Remove notes for this user
  2174. obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
  2175. obj.db.Remove('im' + deluser._id); // Remove image for this user
  2176. // Delete any login tokens
  2177. parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
  2178. if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
  2179. });
  2180. // Delete all files on the server for this account
  2181. try {
  2182. var deluserpath = obj.getServerRootFilePath(deluser);
  2183. if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
  2184. } catch (e) { }
  2185. // Remove the user
  2186. obj.db.Remove(deluser._id);
  2187. delete obj.users[deluser._id];
  2188. req.session = null;
  2189. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2190. obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
  2191. parent.debug('web', 'handleDeleteAccountRequest: removed user.');
  2192. } else {
  2193. parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
  2194. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2195. }
  2196. });
  2197. }
  2198. // Check a user's password
  2199. obj.checkUserPassword = function (domain, user, password, func) {
  2200. // Check the old password
  2201. if (user.passtype != null) {
  2202. // IIS default clear or weak password hashing (SHA-1)
  2203. require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
  2204. if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
  2205. if (hash == user.hash) {
  2206. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
  2207. parent.debug('web', 'checkUserPassword: SHA-1 ok.');
  2208. return func(true); // Allow password change
  2209. }
  2210. func(false);
  2211. });
  2212. } else {
  2213. // Default strong password hashing (pbkdf2 SHA384)
  2214. require('./pass').hash(password, user.salt, function (err, hash, tag) {
  2215. if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
  2216. if (hash == user.hash) {
  2217. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
  2218. parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
  2219. return func(true); // Allow password change
  2220. }
  2221. func(false);
  2222. }, 0);
  2223. }
  2224. }
  2225. // Check a user's old passwords
  2226. // Callback: 0=OK, 1=OldPass, 2=CommonPass
  2227. obj.checkOldUserPasswords = function (domain, user, password, func) {
  2228. // Check how many old passwords we need to check
  2229. if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  2230. if (user.oldpasswords != null) {
  2231. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  2232. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  2233. }
  2234. } else {
  2235. delete user.oldpasswords;
  2236. }
  2237. // If there is no old passwords, exit now.
  2238. var oldPassCount = 1;
  2239. if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
  2240. var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
  2241. // Test against common passwords if this feature is enabled
  2242. // Example of common passwords: 123456789, password123
  2243. if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
  2244. oldPassCheckState.count++;
  2245. require('wildleek')(password).then(function (wild) {
  2246. if (wild == true) { oldPassCheckState.response = 2; }
  2247. if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
  2248. });
  2249. }
  2250. // Try current password
  2251. require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
  2252. if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
  2253. if (--tag.count == 0) { tag.func(tag.response); }
  2254. }, oldPassCheckState);
  2255. // Try each old password
  2256. if (user.oldpasswords != null) {
  2257. for (var i in user.oldpasswords) {
  2258. const oldpassword = user.oldpasswords[i];
  2259. // Default strong password hashing (pbkdf2 SHA384)
  2260. require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
  2261. if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
  2262. if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
  2263. }, { oldPassword: oldpassword, state: oldPassCheckState });
  2264. }
  2265. }
  2266. }
  2267. // Handle password changes
  2268. function handlePasswordChangeRequest(req, res, direct) {
  2269. const domain = checkUserIpAddress(req, res);
  2270. if (domain == null) { return; }
  2271. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
  2272. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2273. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  2274. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  2275. // Check if the user is logged and we have all required parameters
  2276. if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
  2277. parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
  2278. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2279. return;
  2280. }
  2281. // Get the current user
  2282. var user = obj.users[req.session.userid];
  2283. if (!user) {
  2284. parent.debug('web', 'handlePasswordChangeRequest: user not found.');
  2285. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2286. return;
  2287. }
  2288. // Check account settings locked
  2289. if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
  2290. parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
  2291. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2292. return;
  2293. }
  2294. // Check old password
  2295. obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
  2296. if (result == true) {
  2297. // Check if the new password is allowed, only do this if this feature is enabled.
  2298. parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
  2299. if (result == 1) {
  2300. parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
  2301. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2302. } else if (result == 2) {
  2303. parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
  2304. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2305. } else {
  2306. // Update the password
  2307. require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
  2308. const nowSeconds = Math.floor(Date.now() / 1000);
  2309. if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
  2310. if (domain.passwordrequirements != null) {
  2311. // Save password hint if this feature is enabled
  2312. if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
  2313. // Save previous password if this feature is enabled
  2314. if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  2315. if (user.oldpasswords == null) { user.oldpasswords = []; }
  2316. user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
  2317. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  2318. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  2319. }
  2320. }
  2321. user.salt = salt;
  2322. user.hash = hash;
  2323. user.passchange = user.access = nowSeconds;
  2324. delete user.passtype;
  2325. obj.db.SetUser(user);
  2326. req.session.viewmode = 2;
  2327. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2328. obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id });
  2329. }, 0);
  2330. }
  2331. });
  2332. }
  2333. });
  2334. }
  2335. // Called when a strategy login occurred
  2336. // This is called after a successful Oauth to Twitter, Google, GitHub...
  2337. function handleStrategyLogin(req, res) {
  2338. const domain = checkUserIpAddress(req, res);
  2339. if (domain == null) { return; }
  2340. if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
  2341. const strategy = domain.authstrategies[req.user.strategy];
  2342. const groups = { 'enabled': typeof strategy.groups == 'object' }
  2343. parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
  2344. if (groups.enabled) { // Groups only available for OIDC strategy currently
  2345. groups.userMemberships = obj.common.convertStrArray(req.user.groups);
  2346. groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false;
  2347. groups.syncMemberships = [];
  2348. groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false;
  2349. groups.grantAdmin = false;
  2350. groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true;
  2351. groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required);
  2352. groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin);
  2353. groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter);
  2354. // Fancy Logs
  2355. let groupMessage = '';
  2356. if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
  2357. else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
  2358. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
  2359. // Check user membership in required groups
  2360. if (groups.requiredGroups.length > 0) {
  2361. let match = false
  2362. for (var i in groups.requiredGroups) {
  2363. if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
  2364. match = true;
  2365. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
  2366. }
  2367. }
  2368. if (match === false) {
  2369. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`);
  2370. req.session.loginmode = 1;
  2371. req.session.messageid = 111; // Access Denied.
  2372. res.redirect(domain.url + getQueryPortion(req));
  2373. return;
  2374. }
  2375. }
  2376. // Check user membership in admin groups
  2377. if (groups.siteAdminEnabled === true) {
  2378. groups.grantAdmin = false;
  2379. for (var i in strategy.groups.siteadmin) {
  2380. if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
  2381. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
  2382. groups.siteAdmin = strategy.groups.siteadmin[i];
  2383. groups.grantAdmin = true;
  2384. break;
  2385. }
  2386. }
  2387. }
  2388. // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
  2389. if (groups.syncEnabled === true) {
  2390. if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it
  2391. for (var i in groups.syncFilter) {
  2392. if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
  2393. }
  2394. } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead
  2395. for (var i in groups.userMemberships) {
  2396. groups.syncMemberships.push(groups.userMemberships[i]);
  2397. }
  2398. }
  2399. if (groups.syncMemberships.length > 0) {
  2400. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`);
  2401. } else {
  2402. groups.syncMemberships = null;
  2403. groups.syncEnabled = false;
  2404. if (groups.syncFilter.length > 0){
  2405. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`);
  2406. } else {
  2407. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
  2408. }
  2409. }
  2410. }
  2411. }
  2412. // Check if the user already exists
  2413. const userid = 'user/' + domain.id + '/' + req.user.sid;
  2414. var user = obj.users[userid];
  2415. if (user == null) {
  2416. var newAccountAllowed = false;
  2417. var newAccountRealms = null;
  2418. if (domain.newaccounts === true) { newAccountAllowed = true; }
  2419. if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
  2420. if (domain.authstrategies[req.user.strategy]) {
  2421. if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
  2422. if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
  2423. }
  2424. if (newAccountAllowed === true) {
  2425. // Create the user
  2426. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
  2427. user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
  2428. if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
  2429. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
  2430. if (domain.authstrategies[req.user.strategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
  2431. if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
  2432. obj.users[userid] = user;
  2433. // Auto-join any user groups
  2434. var newaccountsusergroups = null;
  2435. if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
  2436. if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
  2437. if (newaccountsusergroups) {
  2438. for (var i in newaccountsusergroups) {
  2439. var ugrpid = newaccountsusergroups[i];
  2440. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  2441. var ugroup = obj.userGroups[ugrpid];
  2442. if (ugroup != null) {
  2443. // Add group to the user
  2444. if (user.links == null) { user.links = {}; }
  2445. user.links[ugroup._id] = { rights: 1 };
  2446. // Add user to the group
  2447. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  2448. db.Set(ugroup);
  2449. // Notify user group change
  2450. var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
  2451. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  2452. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  2453. }
  2454. }
  2455. }
  2456. if (groups.enabled === true) {
  2457. // Sync the user groups if enabled
  2458. if (groups.syncEnabled === true) {
  2459. // Set groupType to the preset name if it exists, otherwise use the strategy name
  2460. const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
  2461. syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
  2462. }
  2463. // See if the user is a member of the site admin group.
  2464. if (groups.grantAdmin === true) {
  2465. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
  2466. user.siteadmin = 0xFFFFFFFF;
  2467. }
  2468. }
  2469. // Save the user
  2470. obj.db.SetUser(user);
  2471. // Event user creation
  2472. var targets = ['*', 'server-users'];
  2473. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, username is ' + user.name, domain: domain.id };
  2474. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
  2475. parent.DispatchEvent(targets, obj, event);
  2476. req.session.userid = userid;
  2477. setSessionRandom(req);
  2478. // Notify account login using SSO
  2479. var targets = ['*', 'server-users', user._id];
  2480. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  2481. const ua = obj.getUserAgentInfo(req);
  2482. const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
  2483. obj.parent.DispatchEvent(targets, obj, loginEvent);
  2484. } else {
  2485. // New users not allowed
  2486. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
  2487. req.session.loginmode = 1;
  2488. req.session.messageid = 100; // Unable to create account.
  2489. res.redirect(domain.url + getQueryPortion(req));
  2490. return;
  2491. }
  2492. } else { // Login success
  2493. // Check for basic changes
  2494. var userChanged = false;
  2495. if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
  2496. if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
  2497. if (groups.enabled === true) {
  2498. // Sync the user groups if enabled
  2499. if (groups.syncEnabled === true) {
  2500. syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
  2501. }
  2502. // See if the user is a member of the site admin group.
  2503. if (groups.siteAdminEnabled === true) {
  2504. if (groups.grantAdmin === true) {
  2505. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
  2506. if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
  2507. } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
  2508. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
  2509. delete user.siteadmin;
  2510. userChanged = true;
  2511. }
  2512. }
  2513. }
  2514. // Update db record for user if there are changes detected
  2515. if (userChanged) {
  2516. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
  2517. obj.db.SetUser(user);
  2518. // Event user change
  2519. var targets = ['*', 'server-users'];
  2520. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
  2521. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
  2522. parent.DispatchEvent(targets, obj, event);
  2523. }
  2524. req.session.userid = userid;
  2525. setSessionRandom(req);
  2526. // Notify account login using SSO
  2527. var targets = ['*', 'server-users', user._id];
  2528. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  2529. const ua = obj.getUserAgentInfo(req);
  2530. const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
  2531. obj.parent.DispatchEvent(targets, obj, loginEvent);
  2532. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
  2533. }
  2534. } else if (req.session && req.session.userid && obj.users[req.session.userid]) {
  2535. parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`);
  2536. } else {
  2537. parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
  2538. }
  2539. //res.redirect(domain.url); // This does not handle cookie correctly.
  2540. res.set('Content-Type', 'text/html');
  2541. let url = domain.url;
  2542. if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
  2543. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
  2544. }
  2545. // Indicates that any request to "/" should render "default" or "login" depending on login state
  2546. function handleRootRequest(req, res, direct) {
  2547. const domain = checkUserIpAddress(req, res);
  2548. if (domain == null) { return; }
  2549. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2550. if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
  2551. // If a HTTP header is required, check new UserRequiredHttpHeader
  2552. if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
  2553. // If the session is expired, clear it.
  2554. if ((req.session != null) && (typeof req.session.expire == 'number') && ((req.session.expire - Date.now()) <= 0)) { for (var i in req.session) { delete req.session[i]; } }
  2555. // Check if we are in maintenance mode
  2556. if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
  2557. parent.debug('web', 'handleLoginRequest: Server under maintenance.');
  2558. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  2559. return;
  2560. }
  2561. // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
  2562. if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
  2563. var q = require('url').parse(req.url, true);
  2564. if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
  2565. }
  2566. if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
  2567. // Login using SSPI
  2568. domain.sspi.authenticate(req, res, function (err) {
  2569. if ((err != null) || (req.connection.user == null)) {
  2570. obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
  2571. parent.debug('web', 'handleRootRequest: SSPI auth required.');
  2572. try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
  2573. } else {
  2574. parent.debug('web', 'handleRootRequest: SSPI auth ok.');
  2575. handleRootRequestEx(req, res, domain, direct);
  2576. }
  2577. });
  2578. } else if (req.query.user && req.query.pass) {
  2579. // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
  2580. obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
  2581. // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
  2582. var user = obj.users[userid];
  2583. if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  2584. handleRootRequestEx(req, res, domain, direct);
  2585. } else if ((userid != null) && (err == null)) {
  2586. // Login success
  2587. parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
  2588. req.session.userid = userid;
  2589. delete req.session.currentNode;
  2590. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2591. setSessionRandom(req);
  2592. obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
  2593. handleRootRequestEx(req, res, domain, direct);
  2594. } else {
  2595. // Login failed
  2596. handleRootRequestEx(req, res, domain, direct);
  2597. }
  2598. });
  2599. } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
  2600. // Check if the loginToken is still valid
  2601. obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
  2602. if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
  2603. handleRootRequestEx(req, res, domain, direct); // Login using a different system
  2604. });
  2605. } else {
  2606. // Login using a different system
  2607. handleRootRequestEx(req, res, domain, direct);
  2608. }
  2609. }
  2610. function handleRootRequestEx(req, res, domain, direct) {
  2611. var nologout = false, user = null;
  2612. res.set({ 'Cache-Control': 'no-store' });
  2613. // Check if we have an incomplete domain name in the path
  2614. if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
  2615. parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
  2616. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2617. return;
  2618. }
  2619. if (obj.args.nousers == true) {
  2620. // If in single user mode, setup things here.
  2621. delete req.session.loginmode;
  2622. req.session.userid = 'user/' + domain.id + '/~';
  2623. delete req.session.currentNode;
  2624. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2625. setSessionRandom(req);
  2626. if (obj.users[req.session.userid] == null) {
  2627. // Create the dummy user ~ with impossible password
  2628. parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
  2629. obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
  2630. obj.db.SetUser(obj.users[req.session.userid]);
  2631. }
  2632. } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  2633. // If a default user is active, setup the session here.
  2634. parent.debug('web', 'handleRootRequestEx: auth using default user.');
  2635. delete req.session.loginmode;
  2636. req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
  2637. delete req.session.currentNode;
  2638. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2639. setSessionRandom(req);
  2640. } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
  2641. var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  2642. //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
  2643. if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
  2644. // If a login cookie was provided, setup the session here.
  2645. parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
  2646. delete req.session.loginmode;
  2647. req.session.userid = loginCookie.u;
  2648. delete req.session.currentNode;
  2649. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2650. setSessionRandom(req);
  2651. } else {
  2652. parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
  2653. }
  2654. } else if (domain.sspi != null) {
  2655. // SSPI login (Windows only)
  2656. //console.log(req.connection.user, req.connection.userSid);
  2657. if ((req.connection.user == null) || (req.connection.userSid == null)) {
  2658. parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
  2659. res.sendStatus(404); return;
  2660. } else {
  2661. nologout = true;
  2662. req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
  2663. req.session.usersid = req.connection.userSid;
  2664. req.session.usersGroups = req.connection.userGroups;
  2665. delete req.session.currentNode;
  2666. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2667. setSessionRandom(req);
  2668. obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
  2669. // Check if this user exists, create it if not.
  2670. user = obj.users[req.session.userid];
  2671. if ((user == null) || (user.sid != req.session.usersid)) {
  2672. // Create the domain user
  2673. var usercount = 0, user2 = { type: 'user', _id: req.session.userid, name: req.connection.user, domain: domain.id, sid: req.session.usersid, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000) };
  2674. if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
  2675. if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
  2676. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
  2677. if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
  2678. // Auto-join any user groups
  2679. if (typeof domain.newaccountsusergroups == 'object') {
  2680. for (var i in domain.newaccountsusergroups) {
  2681. var ugrpid = domain.newaccountsusergroups[i];
  2682. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  2683. var ugroup = obj.userGroups[ugrpid];
  2684. if (ugroup != null) {
  2685. // Add group to the user
  2686. if (user2.links == null) { user2.links = {}; }
  2687. user2.links[ugroup._id] = { rights: 1 };
  2688. // Add user to the group
  2689. ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
  2690. db.Set(ugroup);
  2691. // Notify user group change
  2692. var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user2.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
  2693. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  2694. parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
  2695. }
  2696. }
  2697. }
  2698. obj.users[req.session.userid] = user2;
  2699. obj.db.SetUser(user2);
  2700. var event = { etype: 'user', userid: req.session.userid, username: req.connection.user, account: obj.CloneSafeUser(user2), action: 'accountcreate', msg: 'Domain account created, user ' + req.connection.user, domain: domain.id };
  2701. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
  2702. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  2703. parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
  2704. }
  2705. }
  2706. }
  2707. // Figure out the minimal password requirement
  2708. var passRequirements = null;
  2709. if (domain.passwordrequirements != null) {
  2710. if (domain.passrequirementstr == null) {
  2711. var passRequirements = {};
  2712. if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
  2713. if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
  2714. if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
  2715. if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
  2716. if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
  2717. if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
  2718. domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
  2719. }
  2720. passRequirements = domain.passwordrequirementsstr;
  2721. }
  2722. // If a user exists and is logged in, serve the default app, otherwise server the login app.
  2723. if (req.session && req.session.userid && obj.users[req.session.userid]) {
  2724. const user = obj.users[req.session.userid];
  2725. // Check if we are in maintenance mode
  2726. if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
  2727. req.session.messageid = 115; // Server under maintenance
  2728. req.session.loginmode = 1;
  2729. res.redirect(domain.url);
  2730. return;
  2731. }
  2732. // If the request has a "meshmessengerid", redirect to MeshMessenger
  2733. // This situation happens when you get a push notification for a chat session, but are not logged in.
  2734. if (req.query.meshmessengerid != null) {
  2735. res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''));
  2736. return;
  2737. }
  2738. const xdbGetFunc = function dbGetFunc(err, states) {
  2739. if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
  2740. parent.debug('web', 'handleRootRequestEx: incorrect domain.');
  2741. dbGetFunc.req.session = null;
  2742. dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
  2743. return;
  2744. }
  2745. // Check if this is a locked account
  2746. if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
  2747. // Locked account
  2748. parent.debug('web', 'handleRootRequestEx: locked account.');
  2749. delete dbGetFunc.req.session.userid;
  2750. delete dbGetFunc.req.session.currentNode;
  2751. delete dbGetFunc.req.session.passhint;
  2752. delete dbGetFunc.req.session.cuserid;
  2753. dbGetFunc.req.session.messageid = 110; // Account locked.
  2754. dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
  2755. return;
  2756. }
  2757. var viewmode = 1;
  2758. if (dbGetFunc.req.session.viewmode) {
  2759. viewmode = dbGetFunc.req.session.viewmode;
  2760. delete dbGetFunc.req.session.viewmode;
  2761. } else if (dbGetFunc.req.query.viewmode) {
  2762. viewmode = dbGetFunc.req.query.viewmode;
  2763. }
  2764. var currentNode = '';
  2765. if (dbGetFunc.req.session.currentNode) {
  2766. currentNode = dbGetFunc.req.session.currentNode;
  2767. delete dbGetFunc.req.session.currentNode;
  2768. } else if (dbGetFunc.req.query.node) {
  2769. currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
  2770. }
  2771. var logoutcontrols = {};
  2772. if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
  2773. // Give the web page a list of supported server features for this domain and user
  2774. const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
  2775. // Create a authentication cookie
  2776. const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
  2777. const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
  2778. // Send the main web application
  2779. var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
  2780. if ((!obj.args.user) && (obj.args.nousers != true) && (nologout == false)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
  2781. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2782. // Clean up the U2F challenge if needed
  2783. if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
  2784. if (dbGetFunc.req.session.e) {
  2785. const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
  2786. if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
  2787. }
  2788. // Intel AMT Scanning options
  2789. var amtscanoptions = '';
  2790. if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
  2791. else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
  2792. // Fetch the web state
  2793. parent.debug('web', 'handleRootRequestEx: success.');
  2794. var webstate = '';
  2795. if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
  2796. if ((webstate == '') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
  2797. if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
  2798. var webstate2 = {};
  2799. try { if (webstate != '') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
  2800. for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
  2801. webstate = JSON.stringify(webstate2);
  2802. }
  2803. // Custom user interface
  2804. var customui = '';
  2805. if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
  2806. // Server features
  2807. var serverFeatures = 255;
  2808. if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
  2809. else if (typeof domain.myserver == 'object') {
  2810. if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
  2811. if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
  2812. if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
  2813. if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
  2814. if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
  2815. if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
  2816. if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
  2817. }
  2818. if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
  2819. if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
  2820. if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
  2821. }
  2822. // Get WebRTC configuration
  2823. var webRtcConfig = null;
  2824. if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
  2825. else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
  2826. // Refresh the session
  2827. render(dbGetFunc.req, dbGetFunc.res, getRenderPage((domain.sitestyle == 3 ? 'default3' : 'default'), dbGetFunc.req, domain), getRenderArgs({
  2828. authCookie: authCookie,
  2829. authRelayCookie: authRelayCookie,
  2830. viewmode: viewmode,
  2831. currentNode: currentNode,
  2832. logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
  2833. domain: domain.id,
  2834. debuglevel: parent.debugLevel,
  2835. serverDnsName: obj.getWebServerName(domain, req),
  2836. serverRedirPort: args.redirport,
  2837. serverPublicPort: httpsPort,
  2838. serverfeatures: serverFeatures,
  2839. features: allFeatures.features,
  2840. features2: allFeatures.features2,
  2841. sessiontime: (args.sessiontime) ? args.sessiontime : 60,
  2842. mpspass: args.mpspass,
  2843. passRequirements: passRequirements,
  2844. customui: customui,
  2845. webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
  2846. footer: (domain.footer == null) ? '' : domain.footer,
  2847. webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
  2848. amtscanoptions: amtscanoptions,
  2849. pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
  2850. webRelayPort: ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0)),
  2851. webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
  2852. hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
  2853. showNotesPanel: (domain.shownotespanel ? 'true' : 'false'),
  2854. userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'),
  2855. webrtcconfig: webRtcConfig
  2856. }, dbGetFunc.req, domain), user);
  2857. }
  2858. xdbGetFunc.req = req;
  2859. xdbGetFunc.res = res;
  2860. xdbGetFunc.user = user;
  2861. obj.db.Get('ws' + user._id, xdbGetFunc);
  2862. } else {
  2863. // Send back the login application
  2864. // If this is a 2 factor auth request, look for a hardware key challenge.
  2865. // Normal login 2 factor request
  2866. if (req.session && (req.session.loginmode == 4)) {
  2867. const sec = parent.decryptSessionData(req.session.e);
  2868. if ((sec != null) && (typeof sec.tuserid == 'string')) {
  2869. const user = obj.users[sec.tuserid];
  2870. if (user != null) {
  2871. parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
  2872. getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
  2873. return;
  2874. }
  2875. }
  2876. }
  2877. // Password recovery 2 factor request
  2878. if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
  2879. obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
  2880. if ((err != null) || (docs.length == 0)) {
  2881. parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
  2882. req.session = null;
  2883. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2884. } else {
  2885. var user = obj.users[docs[0]._id];
  2886. if (user != null) {
  2887. parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
  2888. getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
  2889. } else {
  2890. parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
  2891. req.session = null;
  2892. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2893. }
  2894. }
  2895. });
  2896. return;
  2897. }
  2898. handleRootRequestLogin(req, res, domain, '', passRequirements);
  2899. }
  2900. }
  2901. // Return a list of server supported features for a given domain and user
  2902. obj.getDomainUserFeatures = function (domain, user, req) {
  2903. var features = 0;
  2904. var features2 = 0;
  2905. if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
  2906. if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
  2907. if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
  2908. if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
  2909. if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
  2910. if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
  2911. if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
  2912. if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
  2913. // 0x00000100 --> This feature flag is free for future use.
  2914. if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
  2915. if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
  2916. if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
  2917. if ((parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (user._id.split('/')[2][0] != '~')) { features += 0x00001000; } // 2FA login supported
  2918. if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
  2919. if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
  2920. if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
  2921. if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
  2922. if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
  2923. if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
  2924. // Check if we can skip 2nd factor auth because of the source IP address
  2925. var skip2factor = false;
  2926. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  2927. for (var i in domain.passwordrequirements.skip2factor) {
  2928. if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
  2929. }
  2930. }
  2931. if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
  2932. }
  2933. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group.
  2934. if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
  2935. if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
  2936. if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
  2937. if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
  2938. if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
  2939. if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
  2940. if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
  2941. if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
  2942. if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
  2943. if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
  2944. if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
  2945. if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
  2946. if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
  2947. if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
  2948. if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
  2949. if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
  2950. if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
  2951. if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
  2952. if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
  2953. if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || (domain.passwordrequirements.logintokens.indexOf(user._id) >= 0)))) { features2 += 0x00000080; } // Indicates login tokens are allowed
  2954. if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
  2955. if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
  2956. if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
  2957. if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
  2958. if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
  2959. if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
  2960. if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
  2961. if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
  2962. if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
  2963. if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
  2964. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
  2965. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
  2966. if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
  2967. if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
  2968. if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
  2969. if ((typeof domain.files == 'object') && (domain.files.sftpconnect === false)) { features2 += 0x00800000; } // Remove the "SFTP Connect" button in the "Files" tab when the device is agent managed
  2970. if ((typeof domain.terminal == 'object') && (domain.terminal.sshconnect === false)) { features2 += 0x01000000; } // Remove the "SSH Connect" button in the "Terminal" tab when the device is agent managed
  2971. if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
  2972. if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
  2973. if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
  2974. if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
  2975. return { features: features, features2: features2 };
  2976. }
  2977. function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
  2978. parent.debug('web', 'handleRootRequestLogin()');
  2979. var features = 0;
  2980. if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string'))) { features += 32; } // Allow site within iframe
  2981. if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
  2982. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2983. var loginmode = 0;
  2984. if (req.session) { loginmode = req.session.loginmode; delete req.session.loginmode; } // Clear this state, if the user hits refresh, we want to go back to the login page.
  2985. // Format an error message if needed
  2986. var passhint = null, msgid = 0;
  2987. if (req.session != null) {
  2988. msgid = req.session.messageid;
  2989. if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
  2990. delete req.session.messageid;
  2991. delete req.session.passhint;
  2992. }
  2993. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
  2994. const emailcheck = (allowAccountReset && (domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
  2995. // Check if we are allowed to create new users using the login screen
  2996. var newAccountsAllowed = true;
  2997. if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
  2998. if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
  2999. // Encrypt the hardware key challenge state if needed
  3000. var hwstate = null;
  3001. if (hardwareKeyChallenge && req.session) {
  3002. const sec = parent.decryptSessionData(req.session.e);
  3003. hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
  3004. }
  3005. // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
  3006. var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
  3007. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
  3008. var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
  3009. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
  3010. var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
  3011. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
  3012. var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
  3013. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
  3014. const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
  3015. // See if we support two-factor trusted cookies
  3016. var twoFactorCookieDays = 30;
  3017. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  3018. // See what authentication strategies we have
  3019. var authStrategies = [];
  3020. if (typeof domain.authstrategies == 'object') {
  3021. if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
  3022. if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
  3023. if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
  3024. if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
  3025. if (typeof domain.authstrategies.oidc == 'object') {
  3026. if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
  3027. authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
  3028. } else {
  3029. authStrategies.push('oidc');
  3030. }
  3031. }
  3032. if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
  3033. if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
  3034. if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
  3035. }
  3036. // Custom user interface
  3037. var customui = '';
  3038. if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
  3039. // Get two-factor screen timeout
  3040. var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
  3041. if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
  3042. twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
  3043. }
  3044. // Setup CAPTCHA if needed
  3045. var newAccountCaptcha = '', newAccountCaptchaImage = '';
  3046. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  3047. newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
  3048. newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
  3049. }
  3050. // Check for flash errors from passport.js and make the array unique
  3051. var flashErrors = [];
  3052. if (req.session.flash && req.session.flash.error) {
  3053. flashErrors = obj.common.uniqueArray(req.session.flash.error);
  3054. req.session.flash = null;
  3055. }
  3056. // Render the login page
  3057. render(req, res,
  3058. getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'login2' : 'login', req, domain),
  3059. getRenderArgs({
  3060. loginmode: loginmode,
  3061. rootCertLink: getRootCertLink(domain),
  3062. newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
  3063. newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
  3064. newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
  3065. newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
  3066. serverDnsName: obj.getWebServerName(domain, req),
  3067. serverPublicPort: httpsPort,
  3068. passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
  3069. emailcheck: emailcheck,
  3070. features: features,
  3071. sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
  3072. passRequirements: passRequirements,
  3073. customui: customui,
  3074. footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
  3075. hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
  3076. messageid: msgid,
  3077. flashErrors: JSON.stringify(flashErrors),
  3078. passhint: passhint,
  3079. welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null,
  3080. welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
  3081. hwstate: hwstate,
  3082. otpemail: otpemail,
  3083. otpsms: otpsms,
  3084. otpmsg: otpmsg,
  3085. otppush: otppush,
  3086. autofido: autofido,
  3087. twoFactorCookieDays: twoFactorCookieDays,
  3088. authStrategies: authStrategies.join(','),
  3089. loginpicture: (typeof domain.loginpicture == 'string'),
  3090. tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
  3091. renderLanguages: obj.renderLanguages,
  3092. showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
  3093. }, req, domain, (domain.sitestyle == 2 || domain.sitestyle == 3) ? 'login2' : 'login'));
  3094. }
  3095. // Handle a post request on the root
  3096. function handleRootPostRequest(req, res) {
  3097. const domain = checkUserIpAddress(req, res);
  3098. if (domain == null) { return; }
  3099. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
  3100. if (req.body == null) { req.body = {}; }
  3101. parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
  3102. // If a HTTP header is required, check new UserRequiredHttpHeader
  3103. if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
  3104. switch (req.body.action) {
  3105. case 'login': { handleLoginRequest(req, res, true); break; }
  3106. case 'tokenlogin': {
  3107. if (req.body.hwstate) {
  3108. var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
  3109. if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
  3110. }
  3111. handleLoginRequest(req, res, true); break;
  3112. }
  3113. case 'pushlogin': {
  3114. if (req.body.hwstate) {
  3115. var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
  3116. if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
  3117. // Push authentication is a success, login the user
  3118. req.session = { userid: cookie.u };
  3119. // Check if we need to remember this device
  3120. if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
  3121. var maxCookieAge = domain.twofactorcookiedurationdays;
  3122. if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
  3123. const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
  3124. res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
  3125. }
  3126. handleRootRequestEx(req, res, domain);
  3127. return;
  3128. }
  3129. }
  3130. handleLoginRequest(req, res, true); break;
  3131. }
  3132. case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
  3133. case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
  3134. case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
  3135. case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
  3136. case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
  3137. case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
  3138. default: { handleLoginRequest(req, res, true); break; }
  3139. }
  3140. }
  3141. // Return true if it looks like we are using a real TLS certificate.
  3142. obj.isTrustedCert = function (domain) {
  3143. if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
  3144. if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
  3145. if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
  3146. if (obj.parent.config.letsencrypt != null) return (obj.parent.config.letsencrypt.production === true); // We are using Let's Encrypt, real cert in use if production is set to true.
  3147. if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
  3148. if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
  3149. return true; // This is a guess
  3150. }
  3151. // Get the link to the root certificate if needed
  3152. function getRootCertLink(domain) {
  3153. // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
  3154. if (obj.isTrustedCert(domain) == false) {
  3155. // Get the domain suffix
  3156. var xdomain = (domain.dns == null) ? domain.id : '';
  3157. if (xdomain != '') xdomain += '/';
  3158. return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
  3159. }
  3160. return '';
  3161. }
  3162. // Serve the xterm page
  3163. function handleXTermRequest(req, res) {
  3164. const domain = checkUserIpAddress(req, res);
  3165. if (domain == null) { return; }
  3166. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3167. parent.debug('web', 'handleXTermRequest: sending xterm');
  3168. res.set({ 'Cache-Control': 'no-store' });
  3169. if (req.session && req.session.userid) {
  3170. if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
  3171. var user = obj.users[req.session.userid];
  3172. if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
  3173. // Check permissions
  3174. obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
  3175. if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
  3176. var logoutcontrols = { name: user.name };
  3177. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3178. if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
  3179. // Create a authentication cookie
  3180. const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
  3181. const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
  3182. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  3183. render(req, res, getRenderPage('xterm', req, domain), getRenderArgs({ serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, authCookie: authCookie, authRelayCookie: authRelayCookie, logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'), name: EscapeHtml(node.name) }, req, domain));
  3184. });
  3185. } else {
  3186. res.redirect(domain.url + getQueryPortion(req));
  3187. return;
  3188. }
  3189. }
  3190. // Handle new account Captcha GET
  3191. function handleNewAccountCaptchaRequest(req, res) {
  3192. const domain = checkUserIpAddress(req, res);
  3193. if (domain == null) { return; }
  3194. if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
  3195. const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
  3196. if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
  3197. res.type('svg');
  3198. res.status(200).end(require('svg-captcha')(c.captcha, {}));
  3199. }
  3200. // Handle Captcha GET
  3201. function handleCaptchaGetRequest(req, res) {
  3202. const domain = checkUserIpAddress(req, res);
  3203. if (domain == null) { return; }
  3204. if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
  3205. parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
  3206. }
  3207. // Handle Captcha POST
  3208. function handleCaptchaPostRequest(req, res) {
  3209. if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
  3210. const domain = checkUserIpAddress(req, res);
  3211. if (domain == null) { return; }
  3212. req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
  3213. parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
  3214. }
  3215. // Render the terms of service.
  3216. function handleTermsRequest(req, res) {
  3217. const domain = checkUserIpAddress(req, res);
  3218. if (domain == null) { return; }
  3219. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3220. // See if term.txt was loaded from the database
  3221. if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
  3222. // Send the terms from the database
  3223. res.set({ 'Cache-Control': 'no-store' });
  3224. if (req.session && req.session.userid) {
  3225. if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
  3226. var user = obj.users[req.session.userid];
  3227. var logoutcontrols = { name: user.name };
  3228. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3229. if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
  3230. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
  3231. } else {
  3232. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
  3233. }
  3234. } else {
  3235. // See if there is a terms.txt file in meshcentral-data
  3236. var p = obj.path.join(obj.parent.datapath, 'terms.txt');
  3237. if (obj.fs.existsSync(p)) {
  3238. obj.fs.readFile(p, 'utf8', function (err, data) {
  3239. if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
  3240. // Send the terms from terms.txt
  3241. res.set({ 'Cache-Control': 'no-store' });
  3242. if (req.session && req.session.userid) {
  3243. if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
  3244. var user = obj.users[req.session.userid];
  3245. var logoutcontrols = { name: user.name };
  3246. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3247. if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
  3248. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
  3249. } else {
  3250. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
  3251. }
  3252. });
  3253. } else {
  3254. // Send the default terms
  3255. parent.debug('web', 'handleTermsRequest: sending default terms');
  3256. res.set({ 'Cache-Control': 'no-store' });
  3257. if (req.session && req.session.userid) {
  3258. if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
  3259. var user = obj.users[req.session.userid];
  3260. var logoutcontrols = { name: user.name };
  3261. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3262. if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
  3263. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
  3264. } else {
  3265. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
  3266. }
  3267. }
  3268. }
  3269. }
  3270. // Render the messenger application.
  3271. function handleMessengerRequest(req, res) {
  3272. const domain = getDomain(req);
  3273. if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
  3274. parent.debug('web', 'handleMessengerRequest()');
  3275. // Check if we are in maintenance mode
  3276. if (parent.config.settings.maintenancemode != null) {
  3277. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  3278. return;
  3279. }
  3280. // Check if this session is for a user
  3281. if (req.query.id == null) { res.sendStatus(404); return; }
  3282. var idSplit = decodeURIComponent(req.query.id).split('/');
  3283. if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
  3284. if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
  3285. // This is a user to user conversation, both users must be logged in.
  3286. var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
  3287. var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
  3288. if (!req.session || !req.session.userid) {
  3289. // Redirect to login page
  3290. if (req.query.key != null) { res.redirect(domain.url + '?key=' + encodeURIComponent(req.query.key) + '&meshmessengerid=' + encodeURIComponent(req.query.id)); } else { res.redirect(domain.url + '?meshmessengerid=' + encodeURIComponent(req.query.id)); }
  3291. return;
  3292. }
  3293. if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
  3294. }
  3295. // Get WebRTC configuration
  3296. var webRtcConfig = null;
  3297. if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
  3298. else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
  3299. // Setup other options
  3300. var options = { webrtcconfig: webRtcConfig };
  3301. if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
  3302. // Get the userid and name
  3303. if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
  3304. if (idSplit.length == 7) {
  3305. const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
  3306. if (user != null) {
  3307. if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
  3308. if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
  3309. }
  3310. }
  3311. }
  3312. // Render the page
  3313. res.set({ 'Cache-Control': 'no-store' });
  3314. render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
  3315. }
  3316. // Handle messenger image request
  3317. function handleMessengerImageRequest(req, res) {
  3318. const domain = getDomain(req);
  3319. if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
  3320. parent.debug('web', 'handleMessengerImageRequest()');
  3321. // Check if we are in maintenance mode
  3322. if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
  3323. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3324. if (domain.meshmessengerpicture) {
  3325. // Use the configured messenger logo picture
  3326. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
  3327. }
  3328. var imagefile = 'images/messenger.png';
  3329. if (domain.webpublicpath != null) {
  3330. obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
  3331. if (exists) {
  3332. // Use the domain logo picture
  3333. try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3334. } else {
  3335. // Use the default logo picture
  3336. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3337. }
  3338. });
  3339. } else if (parent.webPublicOverridePath) {
  3340. obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
  3341. if (exists) {
  3342. // Use the override logo picture
  3343. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3344. } else {
  3345. // Use the default logo picture
  3346. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3347. }
  3348. });
  3349. } else {
  3350. // Use the default logo picture
  3351. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3352. }
  3353. }
  3354. // Returns the server root certificate encoded in base64
  3355. function getRootCertBase64() {
  3356. var rootcert = obj.certificates.root.cert;
  3357. var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
  3358. if (i >= 0) { rootcert = rootcert.substring(i + 29); }
  3359. i = rootcert.indexOf('-----END CERTIFICATE-----');
  3360. if (i >= 0) { rootcert = rootcert.substring(i, 0); }
  3361. return Buffer.from(rootcert, 'base64').toString('base64');
  3362. }
  3363. // Returns the mesh server root certificate
  3364. function handleRootCertRequest(req, res) {
  3365. const domain = getDomain(req);
  3366. if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
  3367. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3368. if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
  3369. parent.debug('web', 'handleRootCertRequest()');
  3370. setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
  3371. res.send(Buffer.from(getRootCertBase64(), 'base64'));
  3372. }
  3373. // Return a customised mainifest.json for PWA
  3374. function handleManifestRequest(req, res){
  3375. const domain = getDomain(req);
  3376. if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; }
  3377. if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleManifestRequest: invalid ip'); return; } // Check server-wide IP filter only.
  3378. parent.debug('web', 'handleManifestRequest()');
  3379. var manifest = {
  3380. "name": (domain.title != null) ? domain.title : 'MeshCentral',
  3381. "short_name": (domain.title != null) ? domain.title : 'MeshCentral',
  3382. "description": "Open source web based, remote computer management.",
  3383. "scope": ".",
  3384. "start_url": "/",
  3385. "display": "fullscreen",
  3386. "orientation": "portrait",
  3387. "theme_color": "#ffffff",
  3388. "background_color": "#ffffff",
  3389. "icons": [{
  3390. "src": "pwalogo.png",
  3391. "sizes": "512x512",
  3392. "type": "image/png"
  3393. }]
  3394. };
  3395. res.json(manifest);
  3396. }
  3397. // Handle user public file downloads
  3398. function handleDownloadUserFiles(req, res) {
  3399. const domain = checkUserIpAddress(req, res);
  3400. if (domain == null) { return; }
  3401. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3402. if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
  3403. var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
  3404. if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns
  3405. if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
  3406. if (domain.id != '') { domainname = 'domain-' + domain.id; }
  3407. var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
  3408. for (var i = 3; i < spliturl.length; i++) { if (obj.common.IsFilenameValid(spliturl[i]) == true) { path += '/' + spliturl[i]; filename = spliturl[i]; } else { res.sendStatus(404); return; } }
  3409. var stat = null;
  3410. try { stat = obj.fs.statSync(path); } catch (e) { }
  3411. if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
  3412. if (req.query.download == 1) {
  3413. setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
  3414. try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
  3415. } else {
  3416. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
  3417. }
  3418. } else {
  3419. render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
  3420. }
  3421. }
  3422. // Handle device file request
  3423. function handleDeviceFile(req, res) {
  3424. const domain = checkUserIpAddress(req, res);
  3425. if (domain == null) { return; }
  3426. if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
  3427. // Check the inbound desktop sharing cookie
  3428. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  3429. if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
  3430. // Check userid
  3431. const user = obj.users[c.userid];
  3432. if ((c == user)) { res.sendStatus(404); return; }
  3433. // If this cookie has restricted usages, check that it's allowed to perform downloads
  3434. if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
  3435. if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
  3436. if (req.query.n == null) { res.sendStatus(404); return; }
  3437. // Check if this user has permission to manage this computer
  3438. obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
  3439. if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  3440. // All good, start the file transfer
  3441. req.query.id = getRandomLowerCase(12);
  3442. obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
  3443. });
  3444. }
  3445. // Handle download of a server file by an agent
  3446. function handleAgentDownloadFile(req, res) {
  3447. const domain = checkAgentIpAddress(req, res);
  3448. if (domain == null) { return; }
  3449. if (req.query.c == null) { res.sendStatus(404); return; }
  3450. // Check the inbound desktop sharing cookie
  3451. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
  3452. if ((c == null) || (c.a != 'tmpdl') || (c.d != domain.id) || (c.nid == null) || (c.f == null) || (obj.common.IsFilenameValid(c.f) == false)) { res.sendStatus(404); return; }
  3453. // Send the file back
  3454. try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
  3455. }
  3456. // Handle logo request
  3457. function handleLogoRequest(req, res) {
  3458. const domain = checkUserIpAddress(req, res);
  3459. if (domain == null) { return; }
  3460. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3461. if (domain.titlepicture) {
  3462. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
  3463. // Use the logo in the database
  3464. res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3465. res.send(parent.configurationFiles[domain.titlepicture]);
  3466. return;
  3467. } else {
  3468. // Use the logo on file
  3469. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
  3470. }
  3471. }
  3472. if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
  3473. // Use the domain logo picture
  3474. try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3475. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
  3476. // Use the override logo picture
  3477. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3478. } else {
  3479. // Use the default logo picture
  3480. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3481. }
  3482. }
  3483. // Handle login logo request
  3484. function handleLoginLogoRequest(req, res) {
  3485. const domain = checkUserIpAddress(req, res);
  3486. if (domain == null) { return; }
  3487. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3488. if (domain.loginpicture) {
  3489. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
  3490. // Use the logo in the database
  3491. res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3492. res.send(parent.configurationFiles[domain.loginpicture]);
  3493. return;
  3494. } else {
  3495. // Use the logo on file
  3496. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
  3497. }
  3498. } else {
  3499. res.sendStatus(404);
  3500. }
  3501. }
  3502. // Handle PWA logo request
  3503. function handlePWALogoRequest(req, res) {
  3504. const domain = checkUserIpAddress(req, res);
  3505. if (domain == null) { return; }
  3506. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3507. if (domain.pwalogo) {
  3508. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) {
  3509. // Use the logo in the database
  3510. res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3511. res.send(parent.configurationFiles[domain.pwalogo]);
  3512. return;
  3513. } else {
  3514. // Use the logo on file
  3515. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
  3516. }
  3517. }
  3518. if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) {
  3519. // Use the domain logo picture
  3520. try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3521. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) {
  3522. // Use the override logo picture
  3523. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3524. } else {
  3525. // Use the default logo picture
  3526. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3527. }
  3528. }
  3529. // Handle translation request
  3530. function handleTranslationsRequest(req, res) {
  3531. const domain = checkUserIpAddress(req, res);
  3532. if (domain == null) { return; }
  3533. //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3534. if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
  3535. var user = null;
  3536. if (obj.args.user != null) {
  3537. // A default user is active
  3538. user = obj.users['user/' + domain.id + '/' + obj.args.user];
  3539. if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
  3540. } else {
  3541. // Check if the user is logged and we have all required parameters
  3542. if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
  3543. // Get the current user
  3544. user = obj.users[req.session.userid];
  3545. if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
  3546. if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
  3547. }
  3548. var data = '';
  3549. req.setEncoding('utf8');
  3550. req.on('data', function (chunk) { data += chunk; });
  3551. req.on('end', function () {
  3552. try { data = JSON.parse(data); } catch (ex) { data = null; }
  3553. if (data == null) { res.sendStatus(404); return; }
  3554. if (data.action == 'getTranslations') {
  3555. if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
  3556. // Return the translation file (JSON)
  3557. try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
  3558. } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
  3559. // Return the default translation file (JSON)
  3560. try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
  3561. } else { res.sendStatus(404); }
  3562. } else if (data.action == 'setTranslations') {
  3563. obj.fs.writeFile(obj.path.join(obj.parent.datapath, 'translate.json'), obj.common.translationsToJson({ strings: data.strings }), function (err) { if (err == null) { res.send(JSON.stringify({ response: 'ok' })); } else { res.send(JSON.stringify({ response: err })); } });
  3564. } else if (data.action == 'translateServer') {
  3565. if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
  3566. const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
  3567. if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
  3568. var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
  3569. if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
  3570. if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
  3571. res.send(JSON.stringify({ response: 'ok' }));
  3572. console.log('Started server translation...');
  3573. obj.pendingTranslation = true;
  3574. require('child_process').exec('node translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
  3575. delete obj.pendingTranslation;
  3576. //console.log('error', error);
  3577. //console.log('stdout', stdout);
  3578. //console.log('stderr', stderr);
  3579. //console.log('Server restart...'); // Perform a server restart
  3580. //process.exit(0);
  3581. console.log('Server translation completed.');
  3582. });
  3583. } else {
  3584. // Unknown request
  3585. res.sendStatus(404);
  3586. }
  3587. });
  3588. }
  3589. // Handle welcome image request
  3590. function handleWelcomeImageRequest(req, res) {
  3591. const domain = checkUserIpAddress(req, res);
  3592. if (domain == null) { return; }
  3593. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3594. if (domain.welcomepicture) {
  3595. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
  3596. // Use the welcome image in the database
  3597. res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3598. res.send(parent.configurationFiles[domain.welcomepicture]);
  3599. return;
  3600. }
  3601. // Use the configured logo picture
  3602. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
  3603. }
  3604. var imagefile = 'images/mainwelcome.jpg';
  3605. if (domain.sitestyle == 2 || domain.sitestyle == 3) { imagefile = 'images/login/back.png'; }
  3606. if (domain.webpublicpath != null) {
  3607. obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
  3608. if (exists) {
  3609. // Use the domain logo picture
  3610. try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3611. } else {
  3612. // Use the default logo picture
  3613. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3614. }
  3615. });
  3616. } else if (parent.webPublicOverridePath) {
  3617. obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
  3618. if (exists) {
  3619. // Use the override logo picture
  3620. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3621. } else {
  3622. // Use the default logo picture
  3623. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3624. }
  3625. });
  3626. } else {
  3627. // Use the default logo picture
  3628. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3629. }
  3630. }
  3631. // Download a session recording
  3632. function handleGetRecordings(req, res) {
  3633. const domain = checkUserIpAddress(req, res);
  3634. if (domain == null) return;
  3635. // Check the query
  3636. if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; }
  3637. // Get the recording path
  3638. var recordingsPath = null;
  3639. if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
  3640. if (recordingsPath == null) { res.sendStatus(401); return; }
  3641. // Get the user and check user rights
  3642. var authUserid = null;
  3643. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3644. if (authUserid == null) { res.sendStatus(401); return; }
  3645. const user = obj.users[authUserid];
  3646. if (user == null) { res.sendStatus(401); return; }
  3647. if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
  3648. // Send the recorded file
  3649. setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
  3650. try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
  3651. }
  3652. // Stream a session recording
  3653. function handleGetRecordingsWebSocket(ws, req) {
  3654. var domain = checkAgentIpAddress(ws, req);
  3655. if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; }
  3656. // Check the query
  3657. if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; }
  3658. // Get the recording path
  3659. var recordingsPath = null;
  3660. if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
  3661. if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
  3662. // Get the user and check user rights
  3663. var authUserid = null;
  3664. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3665. if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
  3666. const user = obj.users[authUserid];
  3667. if (user == null) { try { ws.close(); } catch (ex) { } return; }
  3668. if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
  3669. const filefullpath = obj.path.join(recordingsPath, req.query.file);
  3670. obj.fs.stat(filefullpath, function (err, stats) {
  3671. if (err) {
  3672. try { ws.close(); } catch (ex) { } // File does not exist
  3673. } else {
  3674. obj.fs.open(filefullpath, 'r', function (err, fd) {
  3675. if (err == null) {
  3676. // When data is received from the web socket
  3677. ws.on('message', function (msg) {
  3678. if (typeof msg != 'string') return;
  3679. var command;
  3680. try { command = JSON.parse(msg); } catch (e) { return; }
  3681. if ((command == null) || (typeof command.action != 'string')) return;
  3682. switch (command.action) {
  3683. case 'get': {
  3684. const buffer = Buffer.alloc(8 + command.size);
  3685. //buffer.writeUInt32BE((command.ptr >> 32), 0);
  3686. buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
  3687. obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); });
  3688. break;
  3689. }
  3690. }
  3691. });
  3692. // If error, do nothing
  3693. ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
  3694. // If the web socket is closed
  3695. ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
  3696. ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
  3697. } else {
  3698. try { ws.close(); } catch (ex) { }
  3699. }
  3700. });
  3701. }
  3702. });
  3703. }
  3704. // Serve the player page
  3705. function handlePlayerRequest(req, res) {
  3706. const domain = checkUserIpAddress(req, res);
  3707. if (domain == null) { return; }
  3708. parent.debug('web', 'handlePlayerRequest: sending player');
  3709. res.set({ 'Cache-Control': 'no-store' });
  3710. render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
  3711. }
  3712. // Serve the guest sharing page
  3713. function handleSharingRequest(req, res) {
  3714. const domain = getDomain(req, res);
  3715. if (domain == null) { return; }
  3716. if (req.query.c == null) { res.sendStatus(404); return; }
  3717. if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
  3718. // Check the inbound guest sharing cookie
  3719. var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
  3720. if (c == null) { res.sendStatus(404); return; }
  3721. if (c.a === 5) {
  3722. // This is the older style sharing cookie with everything encoded within it.
  3723. // This cookie style gives a very large URL, so it's not used anymore.
  3724. if ((typeof c.p !== 'number') || (c.p < 1) || (c.p > 7) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
  3725. handleSharingRequestEx(req, res, domain, c);
  3726. return;
  3727. }
  3728. if (c.a === 6) {
  3729. // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
  3730. // Gives a much more compact URL.
  3731. if (typeof c.pid != 'string') { res.sendStatus(404); return; }
  3732. // Check the expired time, expire message.
  3733. if ((c.e != null) && (c.e <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3734. obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
  3735. if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
  3736. const doc = docs[0];
  3737. // If this is a recurrent share, check if we are at the correct time to make use of it
  3738. if (typeof doc.recurring == 'number') {
  3739. const now = Date.now();
  3740. if (now >= doc.startTime) { // We don't want to move the validity window before the start time
  3741. const deltaTime = (now - doc.startTime);
  3742. if (doc.recurring === 1) {
  3743. // This moves the start time to the next valid daily window
  3744. const oneDay = (24 * 60 * 60 * 1000);
  3745. var addition = Math.floor(deltaTime / oneDay);
  3746. if ((deltaTime - (addition * oneDay)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
  3747. doc.startTime += (addition * oneDay);
  3748. } else if (doc.recurring === 2) {
  3749. // This moves the start time to the next valid weekly window
  3750. const oneWeek = (7 * 24 * 60 * 60 * 1000);
  3751. var addition = Math.floor(deltaTime / oneWeek);
  3752. if ((deltaTime - (addition * oneWeek)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
  3753. doc.startTime += (addition * oneWeek);
  3754. }
  3755. }
  3756. }
  3757. // Generate an old style cookie from the information in the database
  3758. var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey ? doc.extrakey : null, port: doc.port };
  3759. if (doc.userid) { cookie.uid = doc.userid; }
  3760. if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
  3761. if (doc.startTime != null) {
  3762. if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
  3763. else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
  3764. }
  3765. if (doc.viewOnly === true) { cookie.vo = 1; }
  3766. handleSharingRequestEx(req, res, domain, cookie);
  3767. });
  3768. return;
  3769. }
  3770. res.sendStatus(404); return;
  3771. }
  3772. // Serve the guest sharing page
  3773. function handleSharingRequestEx(req, res, domain, c) {
  3774. // Check the expired time, expire message.
  3775. if ((c.expire != null) && (c.expire <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3776. // Check the public id
  3777. obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
  3778. // Check if any sharing links are present, expire message.
  3779. if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3780. // Search for the device share public identifier, expire message.
  3781. var found = false;
  3782. for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == c.pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === c.k))) { found = true; } }
  3783. if (found == false) { render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3784. // Get information about this node
  3785. obj.db.Get(c.nid, function (err, nodes) {
  3786. if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  3787. var node = nodes[0];
  3788. // Check the start time, not yet valid message.
  3789. if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { render(req, res, getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3790. // If this is a web relay share, check if this feature is active
  3791. if ((c.p == 8) || (c.p == 16)) {
  3792. // This is a HTTP or HTTPS share
  3793. var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0));
  3794. if (webRelayPort == 0) { res.sendStatus(404); return; }
  3795. // Create the authentication cookie
  3796. const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, r: 8, expire: c.expire, pid: c.pid, port: c.port };
  3797. if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
  3798. const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
  3799. // Redirect to a URL
  3800. var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
  3801. var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
  3802. if (c.addr != null) { url += '&addr=' + c.addr; }
  3803. if (c.pid != null) { url += '&relayid=' + c.pid; }
  3804. parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
  3805. res.redirect(url);
  3806. } else {
  3807. // Looks good, let's create the outbound session cookies.
  3808. // This is a desktop, terminal or files share. We need to display the sharing page.
  3809. // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
  3810. const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo };
  3811. if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
  3812. if (c.k != null) { authCookieData.k = c.k; }
  3813. const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
  3814. // Server features
  3815. var features2 = 0;
  3816. if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
  3817. // Lets respond by sending out the desktop viewer.
  3818. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  3819. parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
  3820. res.set({ 'Cache-Control': 'no-store' });
  3821. render(req, res, getRenderPage('sharing', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire, viewOnly: (c.vo == 1) ? 1 : 0, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain));
  3822. }
  3823. });
  3824. });
  3825. }
  3826. // Handle domain redirection
  3827. obj.handleDomainRedirect = function (req, res) {
  3828. const domain = checkUserIpAddress(req, res);
  3829. if (domain == null) { return; }
  3830. if (domain.redirects == null) { res.sendStatus(404); return; }
  3831. var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
  3832. if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
  3833. if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
  3834. if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
  3835. if (domain.redirects[urlName] == '~showversion') {
  3836. // Show the current version
  3837. res.end('MeshCentral v' + obj.parent.currentVer);
  3838. } else {
  3839. // Perform redirection
  3840. res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
  3841. }
  3842. }
  3843. // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
  3844. obj.getServerFilePath = function (user, domain, path) {
  3845. var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
  3846. if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
  3847. var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
  3848. if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
  3849. if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
  3850. if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
  3851. serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
  3852. for (var i = 3; i < splitpath.length; i++) { if (obj.common.IsFilenameValid(splitpath[i]) == true) { serverpath += '/' + splitpath[i]; filename = splitpath[i]; } else { return null; } } // Check that each folder is correct
  3853. return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
  3854. };
  3855. // Return the maximum number of bytes allowed in the user account "My Files".
  3856. obj.getQuota = function (objid, domain) {
  3857. if (objid == null) return 0;
  3858. if (objid.startsWith('user/')) {
  3859. var user = obj.users[objid];
  3860. if (user == null) return 0;
  3861. if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
  3862. if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
  3863. if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
  3864. return null; // By default, the user will have no limit
  3865. } else if (objid.startsWith('mesh/')) {
  3866. var mesh = obj.meshes[objid];
  3867. if (mesh == null) return 0;
  3868. if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
  3869. if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
  3870. return null; // By default, the mesh will have no limit
  3871. }
  3872. return 0;
  3873. };
  3874. // Download a file from the server
  3875. function handleDownloadFile(req, res) {
  3876. const domain = checkUserIpAddress(req, res);
  3877. if (domain == null) { return; }
  3878. if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
  3879. const user = obj.users[req.session.userid];
  3880. if (user == null) { res.sendStatus(404); return; }
  3881. const file = obj.getServerFilePath(user, domain, req.query.link);
  3882. if (file == null) { res.sendStatus(404); return; }
  3883. setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
  3884. obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
  3885. }
  3886. // Download the MeshCommander web page
  3887. function handleMeshCommander(req, res) {
  3888. const domain = checkUserIpAddress(req, res);
  3889. if (domain == null) { return; }
  3890. if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
  3891. // Find the correct MeshCommander language to send
  3892. const acceptableLanguages = obj.getLanguageCodes(req);
  3893. const commandLanguageTranslations = { 'en': '', 'de': '-de', 'es': '-es', 'fr': '-fr', 'it': '-it', 'ja': '-ja', 'ko': '-ko', 'nl': '-nl', 'pt': '-pt', 'ru': '-ru', 'zh-chs': '-zh-chs', 'zh-cht': '-zh-chs' };
  3894. for (var i in acceptableLanguages) {
  3895. const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]];
  3896. if (meshCommanderLanguage != null) {
  3897. try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { }
  3898. return;
  3899. }
  3900. }
  3901. // Send out the default english MeshCommander
  3902. try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
  3903. }
  3904. // Upload a MeshCore.js file to the server
  3905. function handleUploadMeshCoreFile(req, res) {
  3906. const domain = checkUserIpAddress(req, res);
  3907. if (domain == null) { return; }
  3908. if (domain.id !== '') { res.sendStatus(401); return; }
  3909. var authUserid = null;
  3910. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3911. const multiparty = require('multiparty');
  3912. const form = new multiparty.Form();
  3913. form.parse(req, function (err, fields, files) {
  3914. // If an authentication cookie is embedded in the form, use that.
  3915. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  3916. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  3917. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  3918. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  3919. }
  3920. if (authUserid == null) { res.sendStatus(401); return; }
  3921. if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
  3922. // Get the user
  3923. const user = obj.users[authUserid];
  3924. if (user == null) { res.sendStatus(401); return; } // Check this user exists
  3925. // Get the node and check node rights
  3926. const nodeid = fields.attrib[0];
  3927. obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
  3928. if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  3929. for (var i in files.files) {
  3930. var file = files.files[i];
  3931. obj.fs.readFile(file.path, 'utf8', function (err, data) {
  3932. if (err != null) return;
  3933. data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
  3934. obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
  3935. try { obj.fs.unlinkSync(file.path); } catch (e) { }
  3936. });
  3937. }
  3938. res.send('');
  3939. });
  3940. });
  3941. }
  3942. // Upload a MeshCore.js file to the server
  3943. function handleOneClickRecoveryFile(req, res) {
  3944. const domain = checkUserIpAddress(req, res);
  3945. if (domain == null) { return; }
  3946. if (domain.id !== '') { res.sendStatus(401); return; }
  3947. var authUserid = null;
  3948. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3949. const multiparty = require('multiparty');
  3950. const form = new multiparty.Form();
  3951. form.parse(req, function (err, fields, files) {
  3952. // If an authentication cookie is embedded in the form, use that.
  3953. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  3954. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  3955. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  3956. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  3957. }
  3958. if (authUserid == null) { res.sendStatus(401); return; }
  3959. if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
  3960. // Get the user
  3961. const user = obj.users[authUserid];
  3962. if (user == null) { res.sendStatus(401); return; } // Check this user exists
  3963. // Get the node and check node rights
  3964. const nodeid = fields.attrib[0];
  3965. obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
  3966. if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  3967. for (var i in files.files) {
  3968. var file = files.files[i];
  3969. // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
  3970. parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
  3971. //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
  3972. }
  3973. res.send('');
  3974. });
  3975. });
  3976. }
  3977. // Upload a file to the server
  3978. function handleUploadFile(req, res) {
  3979. const domain = checkUserIpAddress(req, res);
  3980. if (domain == null) { return; }
  3981. if (domain.userQuota == -1) { res.sendStatus(401); return; }
  3982. var authUserid = null;
  3983. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3984. const multiparty = require('multiparty');
  3985. const form = new multiparty.Form();
  3986. form.parse(req, function (err, fields, files) {
  3987. // If an authentication cookie is embedded in the form, use that.
  3988. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  3989. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  3990. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  3991. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  3992. }
  3993. if (authUserid == null) { res.sendStatus(401); return; }
  3994. // Get the user
  3995. const user = obj.users[authUserid];
  3996. if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
  3997. if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
  3998. var xfile = null;
  3999. try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
  4000. if (xfile == null) { res.sendStatus(404); return; }
  4001. // Get total bytes in the path
  4002. var totalsize = readTotalFileSize(xfile.fullpath);
  4003. if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
  4004. if (fields.name != null) {
  4005. // See if we need to create the folder
  4006. var domainx = 'domain';
  4007. if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
  4008. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
  4009. try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
  4010. try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
  4011. // Upload method where all the file data is within the fields.
  4012. var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
  4013. if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
  4014. for (var i = 0; i < names.length; i++) {
  4015. if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
  4016. var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
  4017. if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
  4018. // Create the user folder if needed
  4019. (function (fullpath, filename, filedata) {
  4020. obj.fs.mkdir(xfile.fullpath, function () {
  4021. // Write the file
  4022. obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
  4023. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4024. });
  4025. });
  4026. })(xfile.fullpath, names[i], filedata);
  4027. } else {
  4028. // Send a notification
  4029. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
  4030. }
  4031. }
  4032. }
  4033. } else {
  4034. // More typical upload method, the file data is in a multipart mime post.
  4035. for (var i in files.files) {
  4036. var file = files.files[i], fpath = obj.path.join(xfile.fullpath, file.originalFilename);
  4037. if (obj.common.IsFilenameValid(file.originalFilename) && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
  4038. // See if we need to create the folder
  4039. var domainx = 'domain';
  4040. if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
  4041. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
  4042. try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
  4043. try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
  4044. // Rename the file
  4045. obj.fs.rename(file.path, fpath, function (err) {
  4046. if (err && (err.code === 'EXDEV')) {
  4047. // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
  4048. obj.common.copyFile(file.path, fpath, function (err) {
  4049. obj.fs.unlink(file.path, function (err) {
  4050. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4051. });
  4052. });
  4053. } else {
  4054. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4055. }
  4056. });
  4057. } else {
  4058. // Send a notification
  4059. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
  4060. try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
  4061. }
  4062. }
  4063. }
  4064. } else {
  4065. // Send a notification
  4066. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
  4067. }
  4068. res.send('');
  4069. });
  4070. }
  4071. // Upload a file to the server and then batch upload to many agents
  4072. function handleUploadFileBatch(req, res) {
  4073. const domain = checkUserIpAddress(req, res);
  4074. if (domain == null) { return; }
  4075. var authUserid = null;
  4076. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4077. const multiparty = require('multiparty');
  4078. const form = new multiparty.Form();
  4079. form.parse(req, function (err, fields, files) {
  4080. // If an authentication cookie is embedded in the form, use that.
  4081. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4082. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4083. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4084. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4085. }
  4086. if (authUserid == null) { res.sendStatus(401); return; }
  4087. // Get the user
  4088. const user = obj.users[authUserid];
  4089. if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
  4090. // Get fields
  4091. if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
  4092. var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
  4093. if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
  4094. if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
  4095. if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
  4096. if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
  4097. // Check if we have at least one target path
  4098. if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
  4099. parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
  4100. res.send('');
  4101. return;
  4102. }
  4103. // Get server temporary path
  4104. var serverpath = obj.path.join(obj.filespath, 'tmp')
  4105. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
  4106. try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
  4107. // More typical upload method, the file data is in a multipart mime post.
  4108. for (var i in files.files) {
  4109. var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
  4110. cmd.files.push({ name: file.originalFilename, target: ftarget });
  4111. // Rename the file
  4112. obj.fs.rename(file.path, fpath, function (err) {
  4113. if (err && (err.code === 'EXDEV')) {
  4114. // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
  4115. obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
  4116. }
  4117. });
  4118. }
  4119. // Instruct one of more agents to download a URL to a given local drive location.
  4120. var tlsCertHash = null;
  4121. if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
  4122. tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
  4123. if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
  4124. }
  4125. for (var i in cmd.nodeids) {
  4126. obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
  4127. if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
  4128. var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
  4129. if (agentPath == null) return;
  4130. // Compute user consent
  4131. var consent = 0;
  4132. var mesh = obj.meshes[node.meshid];
  4133. if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
  4134. if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
  4135. if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
  4136. if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
  4137. // Check if we need to add consent flags because of a user group link
  4138. if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
  4139. // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
  4140. for (var i in user.links) {
  4141. var ugrp = obj.userGroups[i];
  4142. if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
  4143. consent |= ugrp.consent; // Add user group consent flags
  4144. }
  4145. }
  4146. }
  4147. // Event that this operation is being performed.
  4148. var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
  4149. var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
  4150. var event = { etype: 'node', userid: cmd.user._id, username: cmd.user.name, nodeid: node._id, action: 'batchupload', msg: 'Performing batch upload of ' + cmd.files.length + ' file(s) to ' + agentPath, msgid: msgid, msgArgs: [cmd.files.length, agentPath], domain: cmd.domain.id };
  4151. parent.DispatchEvent(targets, obj, event);
  4152. // Send the agent commands to perform the batch upload operation
  4153. for (var f in cmd.files) {
  4154. if (cmd.files[f].name != null) {
  4155. const acmd = { action: 'wget', userid: user._id, username: user.name, realname: user.realname, remoteaddr: req.clientIp, consent: consent, rights: rights, overwrite: cmd.overwrite, createFolder: cmd.createFolder, urlpath: '/agentdownload.ashx?c=' + obj.parent.encodeCookie({ a: 'tmpdl', d: cmd.domain.id, nid: node._id, f: cmd.files[f].target }, obj.parent.loginCookieEncryptionKey), path: obj.path.join(agentPath, cmd.files[f].name), folder: agentPath, servertlshash: tlsCertHash };
  4156. var agent = obj.wsagents[node._id];
  4157. if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
  4158. // TODO: Add support for peer servers.
  4159. }
  4160. }
  4161. });
  4162. }
  4163. res.send('');
  4164. });
  4165. }
  4166. // Subscribe to all events we are allowed to receive
  4167. obj.subscribe = function (userid, target) {
  4168. const user = obj.users[userid];
  4169. if (user == null) return;
  4170. const subscriptions = [userid, 'server-allusers'];
  4171. if (user.siteadmin != null) {
  4172. // Allow full site administrators of users with all events rights to see all events.
  4173. if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
  4174. else if ((user.siteadmin & 2) != 0) {
  4175. if ((user.groups == null) || (user.groups.length == 0)) {
  4176. // Subscribe to all user changes
  4177. subscriptions.push('server-users');
  4178. } else {
  4179. // Subscribe to user changes for some groups
  4180. for (var i in user.groups) { subscriptions.push('server-users:' + i); }
  4181. }
  4182. }
  4183. }
  4184. if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
  4185. obj.parent.RemoveAllEventDispatch(target);
  4186. obj.parent.AddEventDispatch(subscriptions, target);
  4187. return subscriptions;
  4188. };
  4189. // Handle a web socket relay request
  4190. function handleRelayWebSocket(ws, req, domain, user, cookie) {
  4191. if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4192. parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
  4193. try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
  4194. // Fetch information about the target
  4195. obj.db.Get(req.query.host, function (err, docs) {
  4196. if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4197. var node = docs[0];
  4198. if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4199. // Check if this user has permission to manage this computer
  4200. if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
  4201. // Check what connectivity is available for this node
  4202. var state = parent.GetConnectivityState(req.query.host);
  4203. var conn = 0;
  4204. if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
  4205. // Check what server needs to handle this connection
  4206. if ((obj.parent.multiServer != null) && ((cookie == null) || (cookie.ps != 1))) { // If a cookie is provided and is from a peer server, don't allow the connection to jump again to a different server
  4207. var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
  4208. if (server != null) {
  4209. if (server.serverid != obj.parent.serverId) {
  4210. // Do local Intel CIRA routing using a different server
  4211. parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
  4212. obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
  4213. return;
  4214. }
  4215. } else {
  4216. server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
  4217. if ((server != null) && (server.serverid != obj.parent.serverId)) {
  4218. // Do local Intel AMT routing using a different server
  4219. parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
  4220. obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
  4221. return;
  4222. }
  4223. }
  4224. }
  4225. // Setup session recording if needed
  4226. if (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf((req.query.p == 2) ? 101 : 100) >= 0)))) { // TODO 100
  4227. // Check again if we need to do recording
  4228. var record = true;
  4229. // Check user or device group recording
  4230. if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
  4231. record = false;
  4232. // Check device group recording
  4233. if (domain.sessionrecording.onlyselecteddevicegroups === true) {
  4234. var mesh = obj.meshes[node.meshid];
  4235. if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
  4236. }
  4237. // Check user recording
  4238. if (domain.sessionrecording.onlyselectedusers === true) {
  4239. if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
  4240. }
  4241. }
  4242. if (record == true) {
  4243. var now = new Date(Date.now());
  4244. var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth() + 1, 2) + '-' + obj.common.zeroPad(now.getUTCDate(), 2) + '-' + obj.common.zeroPad(now.getUTCHours(), 2) + '-' + obj.common.zeroPad(now.getUTCMinutes(), 2) + '-' + obj.common.zeroPad(now.getUTCSeconds(), 2) + '-' + getRandomPassword() + '.mcrec'
  4245. var recFullFilename = null;
  4246. if (domain.sessionrecording.filepath) {
  4247. try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
  4248. recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
  4249. } else {
  4250. try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
  4251. recFullFilename = obj.path.join(parent.recordpath, recFilename);
  4252. }
  4253. var fd = obj.fs.openSync(recFullFilename, 'w');
  4254. if (fd != null) {
  4255. // Write the recording file header
  4256. var firstBlock = JSON.stringify({ magic: 'MeshCentralRelaySession', ver: 1, userid: user._id, username: user.name, ipaddr: req.clientIp, nodeid: node._id, intelamt: true, protocol: (req.query.p == 2) ? 101 : 100, time: new Date().toLocaleString() })
  4257. recordingEntry(fd, 1, 0, firstBlock, function () { });
  4258. ws.logfile = { fd: fd, lock: false };
  4259. if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
  4260. }
  4261. }
  4262. }
  4263. // If Intel AMT CIRA connection is available, use it
  4264. var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
  4265. if (ciraconn != null) {
  4266. parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
  4267. // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
  4268. // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
  4269. var port = 16993;
  4270. //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
  4271. if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
  4272. if (req.query.p == 2) port += 2;
  4273. // Setup a new CIRA channel
  4274. if ((port == 16993) || (port == 16995)) {
  4275. // Perform TLS
  4276. var ser = new SerialTunnel();
  4277. var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
  4278. // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
  4279. // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
  4280. ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
  4281. // When APF tunnel return something, update SerialTunnel buffer
  4282. chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
  4283. // Handle CIRA tunnel state change
  4284. chnl.onStateChange = function (ciraconn, state) {
  4285. parent.debug('webrelay', 'Relay TLS CIRA state change', state);
  4286. if (state == 0) { try { ws.close(); } catch (e) { } }
  4287. if (state == 2) {
  4288. // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
  4289. const tlsoptions = { socket: ser, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false };
  4290. if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
  4291. var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
  4292. tlsock.chnl = chnl;
  4293. tlsock.setEncoding('binary');
  4294. tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
  4295. // Decrypted tunnel from TLS communication to be forwarded to websocket
  4296. tlsock.on('data', function (data) {
  4297. // AMT/TLS ---> WS
  4298. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
  4299. try { ws.send(data); } catch (ex) { }
  4300. });
  4301. // If TLS is on, forward it through TLSSocket
  4302. ws.forwardclient = tlsock;
  4303. ws.forwardclient.xtls = 1;
  4304. ws.forwardclient.onStateChange = function (ciraconn, state) {
  4305. parent.debug('webrelay', 'Relay CIRA state change', state);
  4306. if (state == 0) { try { ws.close(); } catch (e) { } }
  4307. };
  4308. ws.forwardclient.onData = function (ciraconn, data) {
  4309. // Run data thru interceptor
  4310. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
  4311. if (data.length > 0) {
  4312. if (ws.logfile == null) {
  4313. try { ws.send(data); } catch (e) { }
  4314. } else {
  4315. // Log to recording file
  4316. recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
  4317. }
  4318. }
  4319. };
  4320. // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
  4321. ws.forwardclient.onSendOk = function (ciraconn) { };
  4322. }
  4323. };
  4324. } else {
  4325. // Without TLS
  4326. ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
  4327. ws.forwardclient.xtls = 0;
  4328. ws._socket.resume();
  4329. ws.forwardclient.onStateChange = function (ciraconn, state) {
  4330. parent.debug('webrelay', 'Relay CIRA state change', state);
  4331. if (state == 0) { try { ws.close(); } catch (e) { } }
  4332. };
  4333. ws.forwardclient.onData = function (ciraconn, data) {
  4334. //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
  4335. // Run data thru interceptor
  4336. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
  4337. //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
  4338. if (data.length > 0) {
  4339. if (ws.logfile == null) {
  4340. try { ws.send(data); } catch (e) { }
  4341. } else {
  4342. // Log to recording file
  4343. recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
  4344. }
  4345. }
  4346. };
  4347. // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
  4348. ws.forwardclient.onSendOk = function (ciraconn) { };
  4349. }
  4350. // When data is received from the web socket, forward the data into the associated CIRA channel.
  4351. // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
  4352. ws.on('message', function (data) {
  4353. //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
  4354. if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
  4355. // WS ---> AMT/TLS
  4356. if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
  4357. // Log to recording file
  4358. if (ws.logfile == null) {
  4359. // Forward data to the associated TCP connection.
  4360. try { ws.forwardclient.write(data); } catch (ex) { }
  4361. } else {
  4362. // Log to recording file
  4363. recordingEntry(ws.logfile.fd, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
  4364. }
  4365. });
  4366. // If error, close the associated TCP connection.
  4367. ws.on('error', function (err) {
  4368. console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
  4369. parent.debug('webrelay', 'Websocket relay closed on error.');
  4370. // Websocket closed, close the CIRA channel and TLS session.
  4371. if (ws.forwardclient) {
  4372. if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
  4373. if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
  4374. if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
  4375. delete ws.forwardclient;
  4376. }
  4377. // Close the recording file
  4378. if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
  4379. });
  4380. // If the web socket is closed, close the associated TCP connection.
  4381. ws.on('close', function (req) {
  4382. parent.debug('webrelay', 'Websocket relay closed.');
  4383. // Websocket closed, close the CIRA channel and TLS session.
  4384. if (ws.forwardclient) {
  4385. if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
  4386. if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
  4387. if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
  4388. delete ws.forwardclient;
  4389. }
  4390. // Close the recording file
  4391. if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
  4392. });
  4393. // Note that here, req.query.p: 1 = WSMAN with server auth, 2 = REDIR with server auth, 3 = WSMAN without server auth, 4 = REDIR with server auth
  4394. // Fetch Intel AMT credentials & Setup interceptor
  4395. if (req.query.p == 1) {
  4396. parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
  4397. ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
  4398. ws.interceptor.blockAmtStorage = true;
  4399. } else if (req.query.p == 2) {
  4400. parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
  4401. ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
  4402. ws.interceptor.blockAmtStorage = true;
  4403. }
  4404. return;
  4405. }
  4406. // If Intel AMT direct connection is possible, option a direct socket
  4407. if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
  4408. parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
  4409. // When data is received from the web socket, forward the data into the associated TCP connection.
  4410. ws.on('message', function (msg) {
  4411. //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
  4412. if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
  4413. if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
  4414. // Log to recording file
  4415. if (ws.logfile == null) {
  4416. // Forward data to the associated TCP connection.
  4417. try { ws.forwardclient.write(msg); } catch (ex) { }
  4418. } else {
  4419. // Log to recording file
  4420. recordingEntry(ws.logfile.fd, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
  4421. }
  4422. });
  4423. // If error, close the associated TCP connection.
  4424. ws.on('error', function (err) {
  4425. console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
  4426. parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
  4427. if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
  4428. // Close the recording file
  4429. if (ws.logfile != null) {
  4430. recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
  4431. obj.fs.close(fd);
  4432. ws.logfile = null;
  4433. });
  4434. }
  4435. });
  4436. // If the web socket is closed, close the associated TCP connection.
  4437. ws.on('close', function () {
  4438. parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
  4439. if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
  4440. // Close the recording file
  4441. if (ws.logfile != null) {
  4442. recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
  4443. obj.fs.close(fd);
  4444. ws.logfile = null;
  4445. });
  4446. }
  4447. });
  4448. // Compute target port
  4449. var port = 16992;
  4450. if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
  4451. if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
  4452. if (node.intelamt.tls == 0) {
  4453. // If this is TCP (without TLS) set a normal TCP socket
  4454. ws.forwardclient = new obj.net.Socket();
  4455. ws.forwardclient.setEncoding('binary');
  4456. ws.forwardclient.xstate = 0;
  4457. ws.forwardclient.forwardwsocket = ws;
  4458. ws._socket.resume();
  4459. } else {
  4460. // If TLS is going to be used, setup a TLS socket
  4461. var tlsoptions = { ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, rejectUnauthorized: false };
  4462. if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
  4463. ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
  4464. // The TLS connection method is the same as TCP, but located a bit differently.
  4465. parent.debug('webrelay', 'TLS connected to ' + node.host + ':' + port + '.');
  4466. ws.forwardclient.xstate = 1;
  4467. ws._socket.resume();
  4468. });
  4469. ws.forwardclient.setEncoding('binary');
  4470. ws.forwardclient.xstate = 0;
  4471. ws.forwardclient.forwardwsocket = ws;
  4472. }
  4473. // When we receive data on the TCP connection, forward it back into the web socket connection.
  4474. ws.forwardclient.on('data', function (data) {
  4475. if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
  4476. if (obj.parent.debugLevel >= 1) { // DEBUG
  4477. parent.debug('webrelaydata', 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
  4478. //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
  4479. }
  4480. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
  4481. if (ws.logfile == null) {
  4482. // No logging
  4483. try { ws.send(data); } catch (e) { }
  4484. } else {
  4485. // Log to recording file
  4486. recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
  4487. }
  4488. });
  4489. // If the TCP connection closes, disconnect the associated web socket.
  4490. ws.forwardclient.on('close', function () {
  4491. parent.debug('webrelay', 'TCP relay disconnected from ' + node.host + ':' + port + '.');
  4492. try { ws.close(); } catch (e) { }
  4493. });
  4494. // If the TCP connection causes an error, disconnect the associated web socket.
  4495. ws.forwardclient.on('error', function (err) {
  4496. parent.debug('webrelay', 'TCP relay error from ' + node.host + ':' + port + ': ' + err);
  4497. try { ws.close(); } catch (e) { }
  4498. });
  4499. // Fetch Intel AMT credentials & Setup interceptor
  4500. if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
  4501. else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
  4502. if (node.intelamt.tls == 0) {
  4503. // A TCP connection to Intel AMT just connected, start forwarding.
  4504. ws.forwardclient.connect(port, node.host, function () {
  4505. parent.debug('webrelay', 'TCP relay connected to ' + node.host + ':' + port + '.');
  4506. ws.forwardclient.xstate = 1;
  4507. ws._socket.resume();
  4508. });
  4509. }
  4510. return;
  4511. }
  4512. });
  4513. }
  4514. // Setup agent to/from server file transfer handler
  4515. function handleAgentFileTransfer(ws, req) {
  4516. var domain = checkAgentIpAddress(ws, req);
  4517. if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4518. if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4519. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  4520. if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4521. ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
  4522. ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
  4523. ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
  4524. if (ws.xcmd == 'coredump') {
  4525. // Check the agent core dump folder if not already present.
  4526. var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
  4527. if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
  4528. ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
  4529. ws.xid = 'coredump';
  4530. ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
  4531. }
  4532. // When data is received from the web socket, echo it back
  4533. ws.on('message', function (data) {
  4534. if (typeof data == 'string') {
  4535. // Control message
  4536. var cmd = null;
  4537. try { cmd = JSON.parse(data); } catch (ex) { }
  4538. if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
  4539. switch (cmd.sub) {
  4540. case 'start': {
  4541. // Perform an async file open
  4542. var callback = function onFileOpen(err, fd) {
  4543. onFileOpen.xws.xfile = fd;
  4544. try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
  4545. };
  4546. callback.xws = this;
  4547. obj.fs.open(this.xfilepath + '.part', 'w', callback);
  4548. break;
  4549. }
  4550. }
  4551. } else {
  4552. // Binary message
  4553. if (data.length < 4) return;
  4554. var flags = data.readInt32BE(0);
  4555. if ((data.length > 4)) {
  4556. // Write the file
  4557. this.xfilelen += (data.length - 4);
  4558. try {
  4559. var callback = function onFileDataWritten(err, bytesWritten, buffer) {
  4560. if (onFileDataWritten.xflags & 1) {
  4561. // End of file
  4562. parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
  4563. if (onFileDataWritten.xws.xfile) {
  4564. obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
  4565. obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
  4566. onFileDataWritten.xws.xfile = null;
  4567. }
  4568. try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
  4569. try { onFileDataWritten.xws.close(); } catch (ex) { }
  4570. } else {
  4571. // Send ack
  4572. try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
  4573. }
  4574. };
  4575. callback.xws = this;
  4576. callback.xflags = flags;
  4577. obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
  4578. } catch (ex) { }
  4579. } else {
  4580. if (flags & 1) {
  4581. // End of file
  4582. parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
  4583. if (this.xfile) {
  4584. obj.fs.close(this.xfile, function (err) { });
  4585. obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
  4586. this.xfile = null;
  4587. }
  4588. this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
  4589. try { this.close(); } catch (ex) { }
  4590. } else {
  4591. // Send ack
  4592. this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
  4593. }
  4594. }
  4595. }
  4596. });
  4597. // If error, do nothing.
  4598. ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
  4599. // If closed, do nothing
  4600. ws.on('close', function (req) {
  4601. if (this.xfile) {
  4602. obj.fs.close(this.xfile, function (err) { });
  4603. obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
  4604. }
  4605. });
  4606. }
  4607. // Handle the web socket echo request, just echo back the data sent
  4608. function handleEchoWebSocket(ws, req) {
  4609. const domain = checkUserIpAddress(ws, req);
  4610. if (domain == null) { return; }
  4611. ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
  4612. // When data is received from the web socket, echo it back
  4613. ws.on('message', function (data) {
  4614. if (data.toString('utf8') == 'close') {
  4615. try { ws.close(); } catch (e) { console.log(e); }
  4616. } else {
  4617. try { ws.send(data); } catch (e) { console.log(e); }
  4618. }
  4619. });
  4620. // If error, do nothing.
  4621. ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
  4622. // If closed, do nothing
  4623. ws.on('close', function (req) { });
  4624. }
  4625. // Handle the 2FA hold web socket
  4626. // Accept an hold a web socket connection until the 2FA response is received.
  4627. function handle2faHoldWebSocket(ws, req) {
  4628. const domain = checkUserIpAddress(ws, req);
  4629. if (domain == null) { return; }
  4630. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
  4631. if (typeof req.query.c !== 'string') { ws.close(); return; }
  4632. const cookie = parent.decodeCookie(req.query.c, null, 1);
  4633. if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
  4634. var user = obj.users[cookie.u];
  4635. if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
  4636. ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
  4637. // 2FA event subscription
  4638. obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
  4639. ws.cookie = cookie;
  4640. ws.HandleEvent = function (source, event, ids, id) {
  4641. obj.parent.RemoveAllEventDispatch(this);
  4642. if ((event.approved === true) && (event.userid == this.cookie.u)) {
  4643. // Create a login cookie
  4644. const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
  4645. try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
  4646. } else {
  4647. // Reject the login
  4648. try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
  4649. }
  4650. }
  4651. // We do not accept any data on this connection.
  4652. ws.on('message', function (data) { this.close(); });
  4653. // If error, do nothing.
  4654. ws.on('error', function (err) { });
  4655. // If closed, unsubscribe
  4656. ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
  4657. // Perform push notification to device
  4658. try {
  4659. const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
  4660. var code = Buffer.from(cookie.c, 'base64').toString();
  4661. var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
  4662. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  4663. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  4664. if (err == null) {
  4665. try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
  4666. } else {
  4667. try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
  4668. }
  4669. });
  4670. } catch (ex) { console.log(ex); }
  4671. }
  4672. // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
  4673. function readTotalFileSize(path) {
  4674. var r = 0, dir;
  4675. try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
  4676. for (var i in dir) {
  4677. var stat = obj.fs.statSync(path + '/' + dir[i]);
  4678. if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
  4679. }
  4680. return r;
  4681. }
  4682. // Delete a folder and all sub items. (TODO: try to make all async version)
  4683. function deleteFolderRec(path) {
  4684. if (obj.fs.existsSync(path) == false) return;
  4685. try {
  4686. obj.fs.readdirSync(path).forEach(function (file, index) {
  4687. var pathx = path + '/' + file;
  4688. if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
  4689. });
  4690. obj.fs.rmdirSync(path);
  4691. } catch (ex) { }
  4692. }
  4693. // Handle Intel AMT events
  4694. // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
  4695. obj.handleAmtEventRequest = function (req, res) {
  4696. const domain = getDomain(req);
  4697. try {
  4698. if (req.headers.authorization) {
  4699. var authstr = req.headers.authorization;
  4700. if (authstr.substring(0, 7) == 'Digest ') {
  4701. var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
  4702. if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
  4703. // Read the data, we need to get the arg field
  4704. var eventData = '';
  4705. req.on('data', function (chunk) { eventData += chunk; });
  4706. req.on('end', function () {
  4707. // Completed event read, let get the argument that must contain the nodeid
  4708. var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
  4709. if (i > 0) {
  4710. var nodeid = eventData.substring(i + 30, i + 30 + 64);
  4711. if (nodeid.length == 64) {
  4712. var nodekey = 'node/' + domain.id + '/' + nodeid;
  4713. // See if this node exists in the database
  4714. obj.db.Get(nodekey, function (err, nodes) {
  4715. if (nodes.length == 1) {
  4716. // Yes, the node exists, compute Intel AMT digest password
  4717. var node = nodes[0];
  4718. var amtpass = obj.crypto.createHash('sha384').update(auth.username.toLowerCase() + ':' + nodeid + ":" + obj.parent.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
  4719. // Check the MD5 hash
  4720. if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
  4721. // This is an authenticated Intel AMT event, update the host address
  4722. var amthost = req.clientIp;
  4723. if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
  4724. if (node.host != amthost) {
  4725. // Get the mesh for this device
  4726. var mesh = obj.meshes[node.meshid];
  4727. if (mesh) {
  4728. // Update the database
  4729. var oldname = node.host;
  4730. node.host = amthost;
  4731. obj.db.Set(obj.cleanDevice(node));
  4732. // Event the node change
  4733. var event = { etype: 'node', action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Intel(R) AMT host change ' + node.name + ' from group ' + mesh.name + ': ' + oldname + ' to ' + amthost };
  4734. // Remove the Intel AMT password before eventing this.
  4735. event.node = node;
  4736. if (event.node.intelamt && event.node.intelamt.pass) {
  4737. event.node = Object.assign({}, event.node); // Shallow clone
  4738. event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
  4739. delete event.node.intelamt.pass;
  4740. }
  4741. if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
  4742. obj.parent.DispatchEvent(['*', node.meshid], obj, event);
  4743. }
  4744. }
  4745. if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
  4746. //res.send('OK');
  4747. return;
  4748. }
  4749. }
  4750. });
  4751. }
  4752. }
  4753. });
  4754. }
  4755. }
  4756. }
  4757. } catch (e) { console.log(e); }
  4758. // Send authentication response
  4759. obj.crypto.randomBytes(48, function (err, buf) {
  4760. var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
  4761. res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
  4762. res.sendStatus(401);
  4763. });
  4764. };
  4765. // Handle a server backup request
  4766. function handleBackupRequest(req, res) {
  4767. const domain = checkUserIpAddress(req, res);
  4768. if (domain == null) { return; }
  4769. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  4770. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  4771. if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
  4772. var user = obj.users[req.session.userid];
  4773. if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
  4774. // Require modules
  4775. const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
  4776. // Good practice to catch this error explicitly
  4777. archive.on('error', function (err) { throw err; });
  4778. // Set the archive name
  4779. res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
  4780. // Pipe archive data to the file
  4781. archive.pipe(res);
  4782. // Append files from a glob pattern
  4783. archive.directory(obj.parent.datapath, false);
  4784. // Finalize the archive (ie we are done appending files but streams have to finish yet)
  4785. archive.finalize();
  4786. }
  4787. // Handle a server restore request
  4788. function handleRestoreRequest(req, res) {
  4789. const domain = checkUserIpAddress(req, res);
  4790. if (domain == null) { return; }
  4791. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  4792. if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
  4793. var authUserid = null;
  4794. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4795. const multiparty = require('multiparty');
  4796. const form = new multiparty.Form();
  4797. form.parse(req, function (err, fields, files) {
  4798. // If an authentication cookie is embedded in the form, use that.
  4799. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4800. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4801. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4802. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4803. }
  4804. if (authUserid == null) { res.sendStatus(401); return; }
  4805. // Get the user
  4806. const user = obj.users[req.session.userid];
  4807. if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
  4808. res.set('Content-Type', 'text/html');
  4809. res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
  4810. parent.Stop(files.datafile[0].path);
  4811. });
  4812. }
  4813. // Handle a request to download a mesh agent
  4814. obj.handleMeshAgentRequest = function (req, res) {
  4815. var domain = getDomain(req, res);
  4816. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  4817. // If required, check if this user has rights to do this
  4818. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
  4819. if ((req.query.meshinstall != null) && (req.query.id != null)) {
  4820. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  4821. // Send meshagent with included self installer for a specific platform back
  4822. // Start by getting the .msh for this request
  4823. var meshsettings = getMshFromRequest(req, res, domain);
  4824. if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
  4825. // Get the interactive install script, this only works for non-Windows agents
  4826. var agentid = parseInt(req.query.meshinstall);
  4827. var argentInfo = obj.parent.meshAgentBinaries[agentid];
  4828. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  4829. var scriptInfo = obj.parent.meshAgentInstallScripts[6];
  4830. if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
  4831. // Change the .msh file into JSON format and merge it into the install script
  4832. var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
  4833. for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
  4834. var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
  4835. // Get the agent filename
  4836. var meshagentFilename = 'meshagent';
  4837. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  4838. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
  4839. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  4840. res.statusCode = 200;
  4841. obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
  4842. } else if (req.query.id != null) {
  4843. // Send a specific mesh agent back
  4844. var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
  4845. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
  4846. if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  4847. // Download PDB debug files, only allowed for administrator or accounts with agent dump access
  4848. if (req.query.pdb == 1) {
  4849. if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  4850. var user = obj.users[req.session.userid];
  4851. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  4852. if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
  4853. if (argentInfo.id == 3) {
  4854. setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
  4855. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  4856. try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
  4857. return;
  4858. }
  4859. if (argentInfo.id == 4) {
  4860. setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
  4861. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  4862. try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
  4863. return;
  4864. }
  4865. }
  4866. try { res.sendStatus(404); } catch (ex) { }
  4867. return;
  4868. }
  4869. if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
  4870. // Get the agent filename
  4871. var meshagentFilename = argentInfo.rname;
  4872. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  4873. if (argentInfo.rname.endsWith('.apk') && !meshagentFilename.endsWith('.apk')) { meshagentFilename = meshagentFilename + '.apk'; }
  4874. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  4875. if (req.query.zip == 1) { if (argentInfo.zdata != null) { setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.zip', null, 'meshagent.zip'); res.send(argentInfo.zdata); } else { try { res.sendStatus(404); } catch (ex) { } } return; } // Send compressed agent
  4876. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
  4877. if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
  4878. return;
  4879. } else {
  4880. // Check if the meshid is a time limited, encrypted cookie
  4881. var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
  4882. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
  4883. // We are going to embed the .msh file into the Windows executable (signed or not).
  4884. // First, fetch the mesh object to build the .msh file
  4885. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
  4886. if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
  4887. // If required, check if this user has rights to do this
  4888. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  4889. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
  4890. }
  4891. var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  4892. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  4893. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
  4894. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  4895. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  4896. // Prepare a mesh agent file name using the device group name.
  4897. var meshfilename = mesh.name
  4898. meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
  4899. if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
  4900. // Customize the mesh agent file name
  4901. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
  4902. meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
  4903. }
  4904. // Get the agent connection server name
  4905. var serverName = obj.getWebServerName(domain, req);
  4906. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  4907. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  4908. var xdomain = (domain.dns == null) ? domain.id : '';
  4909. if (xdomain != '') xdomain += '/';
  4910. var meshsettings = '';
  4911. if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
  4912. meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  4913. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  4914. meshsettings += 'MeshServer=local\r\n';
  4915. if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
  4916. }
  4917. if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
  4918. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  4919. }
  4920. if (req.query.id == '10006') { // Assistant settings and customizations
  4921. if ((req.query.ac != null)) { meshsettings += 'AutoConnect=' + req.query.ac + '\r\n'; } // Set MeshCentral Assistant flags if needed. 0x01 = Always Connected, 0x02 = Not System Tray
  4922. if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
  4923. if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
  4924. if ((domain.assistantnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  4925. if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
  4926. if (typeof domain.assistantcustomization.title == 'string') { meshsettings += 'Title=' + domain.assistantcustomization.title + '\r\n'; }
  4927. if (typeof domain.assistantcustomization.image == 'string') {
  4928. try { meshsettings += 'Image=' + Buffer.from(obj.fs.readFileSync(parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64') + '\r\n'; } catch (ex) { console.log(ex); }
  4929. }
  4930. if (req.query.ac != '4') {
  4931. // Send with custom filename followed by device group name
  4932. if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = meshfilename.split('MeshCentralAssistant').join(domain.assistantcustomization.filename); }
  4933. } else {
  4934. // Send with custom filename, no device group name
  4935. if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = domain.assistantcustomization.filename + '.exe'; } else { meshfilename = 'MeshCentralAssistant.exe'; }
  4936. }
  4937. }
  4938. } else { // Add agent customization, not for Assistant
  4939. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  4940. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  4941. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  4942. if (domain.agentcustomization != null) {
  4943. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  4944. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  4945. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  4946. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  4947. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  4948. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  4949. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  4950. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  4951. }
  4952. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; } // Translation strings, not for MeshCentral Assistant
  4953. }
  4954. setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
  4955. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  4956. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) {
  4957. obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: domain.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: domain.meshAgentBinaries[req.query.id].pe });
  4958. } else {
  4959. obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: obj.parent.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: obj.parent.meshAgentBinaries[req.query.id].pe });
  4960. }
  4961. return;
  4962. }
  4963. } else if (req.query.script != null) {
  4964. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  4965. // Send a specific mesh install script back
  4966. var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
  4967. if (scriptInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  4968. setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
  4969. var data = scriptInfo.data;
  4970. var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
  4971. if (obj.isTrustedCert(domain) != true) {
  4972. cmdoptions.wgetoptionshttps += '--no-check-certificate ';
  4973. cmdoptions.curloptionshttps += '-k ';
  4974. }
  4975. if (domain.agentnoproxy === true) {
  4976. cmdoptions.wgetoptionshttp += '--no-proxy ';
  4977. cmdoptions.wgetoptionshttps += '--no-proxy ';
  4978. cmdoptions.curloptionshttp += '--noproxy \'*\' ';
  4979. cmdoptions.curloptionshttps += '--noproxy \'*\' ';
  4980. }
  4981. for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
  4982. res.send(data);
  4983. return;
  4984. } else if (req.query.meshcmd != null) {
  4985. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  4986. // Send meshcmd for a specific platform back
  4987. var agentid = parseInt(req.query.meshcmd);
  4988. // If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
  4989. if ((agentid == 3) && (obj.parent.meshAgentBinaries[11000] != null)) { // Signed Windows MeshCmd.exe x86-32
  4990. var stats = null, meshCmdPath = obj.parent.meshAgentBinaries[11000].path;
  4991. try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
  4992. if ((stats != null)) {
  4993. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
  4994. res.sendFile(meshCmdPath); return;
  4995. }
  4996. } else if ((agentid == 4) && (obj.parent.meshAgentBinaries[11001] != null)) { // Signed Windows MeshCmd64.exe x86-64
  4997. var stats = null, meshCmd64Path = obj.parent.meshAgentBinaries[11001].path;
  4998. try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
  4999. if ((stats != null)) {
  5000. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
  5001. res.sendFile(meshCmd64Path); return;
  5002. }
  5003. } else if ((agentid == 43) && (obj.parent.meshAgentBinaries[11002] != null)) { // Signed Windows MeshCmd64.exe ARM-64
  5004. var stats = null, meshCmdAMR64Path = obj.parent.meshAgentBinaries[11002].path;
  5005. try { stats = obj.fs.statSync(meshCmdAMR64Path); } catch (e) { }
  5006. if ((stats != null)) {
  5007. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd-arm64.exe', null, 'meshcmd');
  5008. res.sendFile(meshCmdAMR64Path); return;
  5009. }
  5010. }
  5011. // No signed agents, we are going to merge a new MeshCmd.
  5012. if (((agentid == 3) || (agentid == 4)) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
  5013. var argentInfo = obj.parent.meshAgentBinaries[agentid];
  5014. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5015. if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5016. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
  5017. res.statusCode = 200;
  5018. if (argentInfo.signedMeshCmdPath != null) {
  5019. // If we have a pre-signed MeshCmd, send that.
  5020. res.sendFile(argentInfo.signedMeshCmdPath);
  5021. } else {
  5022. // Merge JavaScript to a unsigned agent and send that.
  5023. obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(obj.parent.defaultMeshCmd, 'utf8'), peinfo: argentInfo.pe });
  5024. }
  5025. return;
  5026. } else if (req.query.meshaction != null) {
  5027. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5028. var user = obj.users[req.session.userid];
  5029. if (user == null) {
  5030. // Check if we have an authentication cookie
  5031. var c = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey);
  5032. if (c == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5033. // Download tools using a cookie
  5034. if (c.download == req.query.meshaction) {
  5035. if (req.query.meshaction == 'winrouter') {
  5036. var p = null;
  5037. if (obj.meshToolsBinaries['MeshCentralRouter']) { p = obj.meshToolsBinaries['MeshCentralRouter'].path; }
  5038. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
  5039. if (obj.fs.existsSync(p)) {
  5040. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
  5041. try { res.sendFile(p); } catch (ex) { }
  5042. } else { try { res.sendStatus(404); } catch (ex) { } }
  5043. return;
  5044. } else if (req.query.meshaction == 'winassistant') {
  5045. var p = null;
  5046. if (obj.meshToolsBinaries['MeshCentralAssistant']) { p = obj.meshToolsBinaries['MeshCentralAssistant'].path; }
  5047. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
  5048. if (obj.fs.existsSync(p)) {
  5049. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
  5050. try { res.sendFile(p); } catch (ex) { }
  5051. } else { try { res.sendStatus(404); } catch (ex) { } }
  5052. return;
  5053. } else if (req.query.meshaction == 'macrouter') {
  5054. var p = null;
  5055. if (obj.meshToolsBinaries['MeshCentralRouterMacOS']) { p = obj.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
  5056. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
  5057. if (obj.fs.existsSync(p)) {
  5058. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
  5059. try { res.sendFile(p); } catch (ex) { }
  5060. } else { try { res.sendStatus(404); } catch (ex) { } }
  5061. return;
  5062. }
  5063. return;
  5064. }
  5065. // Check if the cookie authenticates a user
  5066. if (c.userid == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5067. user = obj.users[c.userid];
  5068. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5069. }
  5070. if ((req.query.meshaction == 'route') && (req.query.nodeid != null)) {
  5071. var nodeIdSplit = req.query.nodeid.split('/');
  5072. if ((nodeIdSplit[0] != 'node') || (nodeIdSplit[1] != domain.id)) { try { res.sendStatus(401); } catch (ex) { } return; }
  5073. obj.db.Get(req.query.nodeid, function (err, nodes) {
  5074. if ((err != null) || (nodes.length != 1)) { try { res.sendStatus(401); } catch (ex) { } return; }
  5075. var node = nodes[0];
  5076. // Create the meshaction.txt file for meshcmd.exe
  5077. var meshaction = {
  5078. action: req.query.meshaction,
  5079. localPort: 1234,
  5080. remoteName: node.name,
  5081. remoteNodeId: node._id,
  5082. remoteTarget: null,
  5083. remotePort: 3389,
  5084. username: '',
  5085. password: '',
  5086. serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
  5087. serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
  5088. debugLevel: 0
  5089. };
  5090. if (user != null) { meshaction.username = user.name; }
  5091. if (req.query.key != null) { meshaction.loginKey = req.query.key; }
  5092. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5093. if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : (domain.id + '/')) + 'meshrelay.ashx'; }
  5094. setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
  5095. res.send(JSON.stringify(meshaction, null, ' '));
  5096. return;
  5097. });
  5098. } else if (req.query.meshaction == 'generic') {
  5099. var meshaction = {
  5100. username: user.name,
  5101. password: '',
  5102. serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
  5103. serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
  5104. debugLevel: 0
  5105. };
  5106. if (user != null) { meshaction.username = user.name; }
  5107. if (req.query.key != null) { meshaction.loginKey = req.query.key; }
  5108. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5109. if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
  5110. setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
  5111. res.send(JSON.stringify(meshaction, null, ' '));
  5112. return;
  5113. } else if (req.query.meshaction == 'winrouter') {
  5114. var p = null;
  5115. if (parent.meshToolsBinaries['MeshCentralRouter']) { p = parent.meshToolsBinaries['MeshCentralRouter'].path; }
  5116. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
  5117. if (obj.fs.existsSync(p)) {
  5118. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
  5119. try { res.sendFile(p); } catch (ex) { }
  5120. } else { try { res.sendStatus(404); } catch (ex) { } }
  5121. return;
  5122. } else if (req.query.meshaction == 'winassistant') {
  5123. var p = null;
  5124. if (parent.meshToolsBinaries['MeshCentralAssistant']) { p = parent.meshToolsBinaries['MeshCentralAssistant'].path; }
  5125. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
  5126. if (obj.fs.existsSync(p)) {
  5127. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
  5128. try { res.sendFile(p); } catch (ex) { }
  5129. } else { try { res.sendStatus(404); } catch (ex) { } }
  5130. return;
  5131. } else if (req.query.meshaction == 'macrouter') {
  5132. var p = null;
  5133. if (parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
  5134. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
  5135. if (obj.fs.existsSync(p)) {
  5136. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
  5137. try { res.sendFile(p); } catch (ex) { }
  5138. } else { try { res.sendStatus(404); } catch (ex) { } }
  5139. return;
  5140. } else {
  5141. try { res.sendStatus(401); } catch (ex) { }
  5142. return;
  5143. }
  5144. } else {
  5145. domain = checkUserIpAddress(req, res); // Recheck the domain to apply user IP filtering.
  5146. if (domain == null) return;
  5147. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5148. if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5149. var user = null, coreDumpsAllowed = false;
  5150. if (typeof req.session.userid == 'string') { user = obj.users[req.session.userid]; }
  5151. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5152. // Check if this user has access to agent core dumps
  5153. if ((obj.parent.config.settings.agentcoredump === true) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
  5154. coreDumpsAllowed = true;
  5155. if ((req.query.dldump != null) && obj.common.IsFilenameValid(req.query.dldump)) {
  5156. // Download a dump file
  5157. var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
  5158. if (obj.fs.existsSync(dumpFile)) {
  5159. setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
  5160. res.sendFile(dumpFile); return;
  5161. } else {
  5162. try { res.sendStatus(404); } catch (ex) { } return;
  5163. }
  5164. }
  5165. if ((req.query.deldump != null) && obj.common.IsFilenameValid(req.query.deldump)) {
  5166. // Delete a dump file
  5167. try { obj.fs.unlinkSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.deldump)); } catch (ex) { console.log(ex); }
  5168. }
  5169. if ((req.query.dumps != null) || (req.query.deldump != null)) {
  5170. // Send list of agent core dumps
  5171. var response = '<html><head><title>Mesh Agents Core Dumps</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
  5172. response += '<tr style="background-color:lightgray"><th>ID</th><th>Upload Date</th><th>Description</th><th>Current</th><th>Dump</th><th>Size</th><th>Agent</th><th>Agent SHA384</th><th>NodeID</th><th></th></tr>';
  5173. var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
  5174. if (obj.fs.existsSync(coreDumpPath)) {
  5175. var files = obj.fs.readdirSync(coreDumpPath);
  5176. var coredumps = [];
  5177. for (var i in files) {
  5178. var file = files[i];
  5179. if (file.endsWith('.dmp')) {
  5180. var fileSplit = file.substring(0, file.length - 4).split('-');
  5181. if (fileSplit.length == 3) {
  5182. var agentid = parseInt(fileSplit[0]);
  5183. if ((isNaN(agentid) == false) && (obj.parent.meshAgentBinaries[agentid] != null)) {
  5184. var agentinfo = obj.parent.meshAgentBinaries[agentid];
  5185. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5186. var filestats = obj.fs.statSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', file));
  5187. coredumps.push({
  5188. fileSplit: fileSplit,
  5189. agentinfo: agentinfo,
  5190. filestats: filestats,
  5191. currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()),
  5192. downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5193. deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5194. agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5195. time: new Date(filestats.ctime)
  5196. });
  5197. }
  5198. }
  5199. }
  5200. }
  5201. coredumps.sort(function (a, b) { if (a.time > b.time) return -1; if (a.time < b.time) return 1; return 0; });
  5202. for (var i in coredumps) {
  5203. var d = coredumps[i];
  5204. response += '<tr><td>' + d.agentinfo.id + '</td><td>' + d.time.toDateString().split(' ').join('&nbsp;') + '</td><td>' + d.agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
  5205. response += '<td style=text-align:center>' + d.currentAgent + '</td><td><a download href="' + d.downloadUrl + '">Download</a></td><td style=text-align:right>' + d.filestats.size + '</td>';
  5206. if (d.currentAgent) { response += '<td><a download href="' + d.agentUrl + '">Download</a></td>'; } else { response += '<td></td>'; }
  5207. response += '<td>' + d.fileSplit[1].toLowerCase() + '</td><td>' + d.fileSplit[2] + '</td><td><a href="' + d.deleteUrl + '">Delete</a></td></tr>';
  5208. }
  5209. }
  5210. response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
  5211. res.send(response);
  5212. return;
  5213. }
  5214. }
  5215. if (req.query.cores != null) {
  5216. // Send list of agent cores
  5217. var response = '<html><head><title>Mesh Agents Cores</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
  5218. response += '<tr style="background-color:lightgray"><th>Name</th><th>Size</th><th>Comp</th><th>Decompressed Hash SHA384</th></tr>';
  5219. for (var i in parent.defaultMeshCores) {
  5220. response += '<tr><td>' + i.split(' ').join('&nbsp;') + '</td><td style="text-align:right"><a download href="/meshagents?dlcore=' + i + '">' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '</a></td><td style="text-align:right"><a download href="/meshagents?dlccore=' + i + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">' + parent.defaultMeshCoresDeflate[i].length + '</a></td><td>' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '</td></tr>';
  5221. }
  5222. response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
  5223. res.send(response);
  5224. return;
  5225. }
  5226. if (req.query.dlcore != null) {
  5227. // Download mesh core
  5228. var bin = parent.defaultMeshCores[req.query.dlcore];
  5229. if ((bin == null) || (bin.length < 5)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5230. setContentDispositionHeader(res, 'application/octet-stream', encodeURIComponent(req.query.dlcore) + '.js', null, 'meshcore.js');
  5231. res.send(bin.slice(4));
  5232. return;
  5233. }
  5234. if (req.query.dlccore != null) {
  5235. // Download compressed mesh core
  5236. var bin = parent.defaultMeshCoresDeflate[req.query.dlccore];
  5237. if (bin == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5238. setContentDispositionHeader(res, 'application/octet-stream', req.query.dlccore + '.js.deflate', null, 'meshcore.js.deflate');
  5239. res.send(bin);
  5240. return;
  5241. }
  5242. // Send a list of available mesh agents
  5243. var response = '<html><head><title>Mesh Agents</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
  5244. response += '<tr style="background-color:lightgray"><th>ID</th><th>Description</th><th>Link</th><th>Size</th><th>SHA384</th><th>MeshCmd</th></tr>';
  5245. var originalUrl = req.originalUrl.split('?')[0];
  5246. for (var agentid in obj.parent.meshAgentBinaries) {
  5247. if ((agentid >= 10000) && (agentid != 10005)) continue;
  5248. var agentinfo = obj.parent.meshAgentBinaries[agentid];
  5249. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5250. response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
  5251. response += '<td><a download href="' + originalUrl + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">' + agentinfo.rname + '</a>';
  5252. if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) {
  5253. if ((agentid == 3) || (agentid == 4)) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&pdb=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">PDB</a>'; }
  5254. }
  5255. if (agentinfo.zdata != null) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&zip=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">ZIP</a>'; }
  5256. response += '</td>';
  5257. response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hashhex + '</td>';
  5258. response += '<td><a download href="' + originalUrl + '?meshcmd=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">' + agentinfo.rname.replace('agent', 'cmd') + '</a></td></tr>';
  5259. }
  5260. response += '</table>';
  5261. response += '<a href="' + originalUrl + '?cores=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshCores</a> ';
  5262. if (coreDumpsAllowed) { response += '<a href="' + originalUrl + '?dumps=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshAgent Crash Dumps</a>'; }
  5263. response += '</body></html>';
  5264. res.send(response);
  5265. return;
  5266. }
  5267. };
  5268. // Get the web server hostname. This may change if using a domain with a DNS name.
  5269. obj.getWebServerName = function (domain, req) {
  5270. if (domain.dns != null) return domain.dns;
  5271. if ((obj.certificates.CommonName == 'un-configured') && (req != null) && (req.headers != null) && (typeof req.headers.host == 'string')) { return req.headers.host.split(':')[0]; }
  5272. return obj.certificates.CommonName;
  5273. }
  5274. // Return true if this is an allowed HTTP request origin hostname.
  5275. obj.CheckWebServerOriginName = function (domain, req) {
  5276. if (domain.allowedorigin === true) return true; // Ignore origin
  5277. if (typeof req.headers.origin != 'string') return true; // No origin in the header, this is a desktop app
  5278. const originUrl = require('url').parse(req.headers.origin, true);
  5279. if (typeof originUrl.hostname != 'string') return false; // Origin hostname is not valid
  5280. if (Array.isArray(domain.allowedorigin)) return (domain.allowedorigin.indexOf(originUrl.hostname) >= 0); // Check if this is an allowed origin from an explicit list
  5281. if (obj.isTrustedCert(domain) === false) return true; // This server does not have a trusted certificate.
  5282. if (domain.dns != null) return (domain.dns == originUrl.hostname); // Match the domain DNS
  5283. return (obj.certificates.CommonName == originUrl.hostname); // Match the default server name
  5284. }
  5285. // Create a OSX mesh agent installer
  5286. obj.handleMeshOsxAgentRequest = function (req, res) {
  5287. const domain = getDomain(req, res);
  5288. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  5289. if (req.query.id == null) { res.sendStatus(404); return; }
  5290. // If required, check if this user has rights to do this
  5291. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
  5292. // Send a specific mesh agent back
  5293. var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
  5294. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
  5295. if ((argentInfo == null) || (req.query.meshid == null)) { res.sendStatus(404); return; }
  5296. // Check if the meshid is a time limited, encrypted cookie
  5297. var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
  5298. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
  5299. // We are going to embed the .msh file into the Windows executable (signed or not).
  5300. // First, fetch the mesh object to build the .msh file
  5301. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
  5302. if (mesh == null) { res.sendStatus(401); return; }
  5303. // If required, check if this user has rights to do this
  5304. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  5305. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
  5306. }
  5307. var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5308. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5309. // Get the agent connection server name
  5310. var serverName = obj.getWebServerName(domain, req);
  5311. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  5312. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  5313. var xdomain = (domain.dns == null) ? domain.id : '';
  5314. if (xdomain != '') xdomain += '/';
  5315. var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  5316. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5317. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  5318. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  5319. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  5320. meshsettings += 'MeshServer=local\r\n';
  5321. if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
  5322. }
  5323. if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
  5324. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  5325. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5326. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  5327. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  5328. if (domain.agentcustomization != null) { // Add agent customization
  5329. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  5330. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  5331. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  5332. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  5333. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  5334. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  5335. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  5336. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  5337. }
  5338. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
  5339. // Setup the response output
  5340. var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
  5341. archive.on('error', function (err) { throw err; });
  5342. // Customize the mesh agent file name
  5343. var meshfilename = 'MeshAgent-' + mesh.name + '.zip';
  5344. var meshexecutablename = 'meshagent';
  5345. var meshmpkgname = 'MeshAgent.mpkg';
  5346. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
  5347. meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
  5348. meshexecutablename = meshexecutablename.split('meshagent').join(domain.agentcustomization.filename);
  5349. meshmpkgname = meshmpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
  5350. }
  5351. // Customise the mesh agent display name
  5352. var meshdisplayname = 'Mesh Agent';
  5353. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.displayname == 'string')) {
  5354. meshdisplayname = meshdisplayname.split('Mesh Agent').join(domain.agentcustomization.displayname);
  5355. }
  5356. // Customise the mesh agent service name
  5357. var meshservicename = 'meshagent';
  5358. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.servicename == 'string')) {
  5359. meshservicename = meshservicename.split('meshagent').join(domain.agentcustomization.servicename);
  5360. }
  5361. // Customise the mesh agent company name
  5362. var meshcompanyname = 'meshagent';
  5363. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.companyname == 'string')) {
  5364. meshcompanyname = meshcompanyname.split('meshagent').join(domain.agentcustomization.companyname);
  5365. }
  5366. // Set the agent download including the mesh name.
  5367. setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
  5368. archive.pipe(res);
  5369. // Opens the "MeshAgentOSXPackager.zip"
  5370. var yauzl = require('yauzl');
  5371. yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
  5372. if (err) { res.sendStatus(500); return; }
  5373. zipfile.readEntry();
  5374. zipfile.on('entry', function (entry) {
  5375. if (/\/$/.test(entry.fileName)) {
  5376. // Skip all folder entries
  5377. zipfile.readEntry();
  5378. } else {
  5379. if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
  5380. // This is a special file entry, we need to fix it.
  5381. zipfile.openReadStream(entry, function (err, readStream) {
  5382. readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
  5383. readStream.on('end', function () {
  5384. var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
  5385. var welcomemsg = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + meshname + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
  5386. var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
  5387. archive.append(readStream.xxdata.toString().split('###DISPLAYNAME###').join(meshdisplayname).split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) });
  5388. zipfile.readEntry();
  5389. });
  5390. });
  5391. } else if (entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchAgent.plist' ||
  5392. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchDaemon.plist' ||
  5393. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Info.plist' ||
  5394. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/postflight' ||
  5395. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/Postflight.sh' ||
  5396. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Uninstall.command' ||
  5397. entry.fileName == 'MeshAgent.mpkg/Uninstall.command') {
  5398. // This is a special file entry, we need to fix it.
  5399. zipfile.openReadStream(entry, function (err, readStream) {
  5400. readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
  5401. readStream.on('end', function () {
  5402. var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
  5403. if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
  5404. archive.append(readStream.xxdata.toString().split('###SERVICENAME###').join(meshservicename).split('###COMPANYNAME###').join(meshcompanyname).split('###EXECUTABLENAME###').join(meshexecutablename), options);
  5405. zipfile.readEntry();
  5406. });
  5407. });
  5408. } else {
  5409. // Normal file entry
  5410. zipfile.openReadStream(entry, function (err, readStream) {
  5411. if (err) { throw err; }
  5412. var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
  5413. if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
  5414. archive.append(readStream, options);
  5415. readStream.on('end', function () { zipfile.readEntry(); });
  5416. });
  5417. }
  5418. }
  5419. });
  5420. zipfile.on('end', function () {
  5421. archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin'.replace('MeshAgent.mpkg',meshmpkgname) });
  5422. archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh'.replace('MeshAgent.mpkg',meshmpkgname) });
  5423. archive.finalize();
  5424. });
  5425. });
  5426. }
  5427. // Return a .msh file from a given request, id is the device group identifier or encrypted cookie with the identifier.
  5428. function getMshFromRequest(req, res, domain) {
  5429. // If required, check if this user has rights to do this
  5430. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { return null; }
  5431. // Check if the meshid is a time limited, encrypted cookie
  5432. var meshcookie = obj.parent.decodeCookie(req.query.id, obj.parent.invitationLinkEncryptionKey);
  5433. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.id = meshcookie.m; }
  5434. // Fetch the mesh object
  5435. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
  5436. if (mesh == null) { return null; }
  5437. // If needed, check if this user has rights to do this
  5438. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  5439. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { return null; }
  5440. }
  5441. var meshidhex = Buffer.from(req.query.id.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5442. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5443. // Get the agent connection server name
  5444. var serverName = obj.getWebServerName(domain, req);
  5445. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  5446. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  5447. var xdomain = (domain.dns == null) ? domain.id : '';
  5448. if (xdomain != '') xdomain += '/';
  5449. var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  5450. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5451. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  5452. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  5453. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  5454. meshsettings += 'MeshServer=local\r\n';
  5455. if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
  5456. }
  5457. if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
  5458. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  5459. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5460. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  5461. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  5462. if (domain.agentcustomization != null) { // Add agent customization
  5463. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  5464. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  5465. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  5466. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  5467. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  5468. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  5469. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  5470. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  5471. }
  5472. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
  5473. return meshsettings;
  5474. }
  5475. // Handle a request to download a mesh settings
  5476. obj.handleMeshSettingsRequest = function (req, res) {
  5477. const domain = getDomain(req);
  5478. if (domain == null) { return; }
  5479. //if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5480. var meshsettings = getMshFromRequest(req, res, domain);
  5481. if (meshsettings == null) { res.sendStatus(401); return; }
  5482. // Get the agent filename
  5483. var meshagentFilename = 'meshagent';
  5484. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  5485. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
  5486. res.send(meshsettings);
  5487. };
  5488. // Handle a request for power events
  5489. obj.handleDevicePowerEvents = function (req, res) {
  5490. const domain = checkUserIpAddress(req, res);
  5491. if (domain == null) { return; }
  5492. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  5493. if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (req.query.id == null) || (typeof req.query.id != 'string')) { res.sendStatus(401); return; }
  5494. var x = req.query.id.split('/');
  5495. var user = obj.users[req.session.userid];
  5496. if ((x.length != 3) || (x[0] != 'node') || (x[1] != domain.id) || (user == null) || (user.links == null)) { res.sendStatus(401); return; }
  5497. obj.db.Get(req.query.id, function (err, docs) {
  5498. if (docs.length != 1) {
  5499. res.sendStatus(401);
  5500. } else {
  5501. var node = docs[0];
  5502. // Check if we have right to this node
  5503. if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
  5504. // See how we will convert UTC time to local time
  5505. var localTimeOffset = 0;
  5506. var timeConversionSystem = 0;
  5507. if ((req.query.l != null) && (req.query.tz != null)) {
  5508. timeConversionSystem = 1;
  5509. } else if (req.query.tf != null) {
  5510. // Get local time offset (bad way)
  5511. timeConversionSystem = 2;
  5512. localTimeOffset = parseInt(req.query.tf);
  5513. if (isNaN(localTimeOffset)) { localTimeOffset = 0; }
  5514. }
  5515. // Get the list of power events and send them
  5516. setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
  5517. obj.db.getPowerTimeline(node._id, function (err, docs) {
  5518. var xevents = ['UTC Time, Local Time, State, Previous State'], prevState = 0;
  5519. for (var i in docs) {
  5520. if (docs[i].power != prevState) {
  5521. var timedoc = docs[i].time;
  5522. if (typeof timedoc == 'string') {
  5523. timedoc = new Date(timedoc);
  5524. }
  5525. prevState = docs[i].power;
  5526. var localTime = '';
  5527. if (timeConversionSystem == 1) { // Good way
  5528. localTime = new Date(timedoc.getTime()).toLocaleString(req.query.l, { timeZone: req.query.tz })
  5529. } else if (timeConversionSystem == 2) { // Bad way
  5530. localTime = new Date(timedoc.getTime() + (localTimeOffset * 60000)).toISOString();
  5531. localTime = localTime.substring(0, localTime.length - 1);
  5532. }
  5533. if (docs[i].oldPower != null) {
  5534. xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power + ',' + docs[i].oldPower);
  5535. } else {
  5536. xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power);
  5537. }
  5538. }
  5539. }
  5540. res.send(xevents.join('\r\n'));
  5541. });
  5542. }
  5543. });
  5544. }
  5545. if (parent.pluginHandler != null) {
  5546. // Handle a plugin admin request
  5547. obj.handlePluginAdminReq = function (req, res) {
  5548. const domain = checkUserIpAddress(req, res);
  5549. if (domain == null) { return; }
  5550. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5551. var user = obj.users[req.session.userid];
  5552. if (user == null) { res.sendStatus(401); return; }
  5553. parent.pluginHandler.handleAdminReq(req, res, user, obj);
  5554. }
  5555. obj.handlePluginAdminPostReq = function (req, res) {
  5556. const domain = checkUserIpAddress(req, res);
  5557. if (domain == null) { return; }
  5558. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5559. var user = obj.users[req.session.userid];
  5560. if (user == null) { res.sendStatus(401); return; }
  5561. parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
  5562. }
  5563. obj.handlePluginJS = function (req, res) {
  5564. const domain = checkUserIpAddress(req, res);
  5565. if (domain == null) { return; }
  5566. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5567. var user = obj.users[req.session.userid];
  5568. if (user == null) { res.sendStatus(401); return; }
  5569. parent.pluginHandler.refreshJS(req, res);
  5570. }
  5571. }
  5572. // Starts the HTTPS server, this should be called after the user/mesh tables are loaded
  5573. function serverStart() {
  5574. // Start the server, only after users and meshes are loaded from the database.
  5575. if (obj.args.tlsoffload) {
  5576. // Setup the HTTP server without TLS
  5577. obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5578. } else {
  5579. var ciphers = [
  5580. 'TLS_AES_256_GCM_SHA384',
  5581. 'TLS_AES_128_GCM_SHA256',
  5582. 'TLS_AES_128_CCM_8_SHA256',
  5583. 'TLS_AES_128_CCM_SHA256',
  5584. 'TLS_CHACHA20_POLY1305_SHA256',
  5585. 'ECDHE-RSA-AES256-GCM-SHA384',
  5586. 'ECDHE-ECDSA-AES256-GCM-SHA384',
  5587. 'ECDHE-RSA-AES128-GCM-SHA256',
  5588. 'ECDHE-ECDSA-AES128-GCM-SHA256',
  5589. 'DHE-RSA-AES128-GCM-SHA256',
  5590. 'ECDHE-RSA-CHACHA20-POLY1305', // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
  5591. 'ECDHE-ARIA128-GCM-SHA256',
  5592. 'ECDHE-ARIA256-GCM-SHA384',
  5593. 'ECDHE-RSA-AES128-SHA256', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
  5594. 'ECDHE-RSA-AES256-SHA384', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
  5595. '!aNULL',
  5596. '!eNULL',
  5597. '!EXPORT',
  5598. '!DES',
  5599. '!RC4',
  5600. '!MD5',
  5601. '!PSK',
  5602. '!SRP',
  5603. '!CAMELLIA'
  5604. ].join(':');
  5605. if (obj.useNodeDefaultTLSCiphers) {
  5606. ciphers = require("tls").DEFAULT_CIPHERS;
  5607. }
  5608. if (obj.tlsCiphers) {
  5609. ciphers = obj.tlsCiphers;
  5610. if (Array.isArray(obj.tlsCiphers)) {
  5611. ciphers = obj.tlsCiphers.join(":");
  5612. }
  5613. }
  5614. // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
  5615. //const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:!aNULL:!eNULL:!EXPORT:!RSA:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; // This does not work with TLS 1.3
  5616. const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: ciphers, secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
  5617. if (obj.tlsSniCredentials != null) { tlsOptions.SNICallback = TlsSniCallback; } // We have multiple web server certificate used depending on the domain name
  5618. obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
  5619. obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
  5620. obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
  5621. //obj.tlsServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
  5622. obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
  5623. obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
  5624. obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5625. }
  5626. // Start a second agent-only server if needed
  5627. if (obj.args.agentport) {
  5628. var agentPortTls = true;
  5629. if (obj.args.tlsoffload != null) { agentPortTls = false; }
  5630. if (typeof obj.args.agentporttls == 'boolean') { agentPortTls = obj.args.agentporttls; }
  5631. if (obj.certificates.webdefault == null) { agentPortTls = false; }
  5632. if (agentPortTls == false) {
  5633. // Setup the HTTP server without TLS
  5634. obj.expressWsAlt = require('express-ws')(obj.agentapp, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5635. } else {
  5636. // Setup the agent HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
  5637. // If TLS is used on the agent port, we always use the default TLS certificate.
  5638. const tlsOptions = { cert: obj.certificates.webdefault.cert, key: obj.certificates.webdefault.key, ca: obj.certificates.webdefault.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
  5639. obj.tlsAltServer = require('https').createServer(tlsOptions, obj.agentapp);
  5640. obj.tlsAltServer.on('secureConnection', function () { /*console.log('tlsAltServer secureConnection');*/ });
  5641. obj.tlsAltServer.on('error', function (err) { console.log('tlsAltServer error', err); });
  5642. //obj.tlsAltServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
  5643. obj.tlsAltServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
  5644. obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
  5645. obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5646. }
  5647. }
  5648. // Setup middleware
  5649. obj.app.engine('handlebars', obj.exphbs.engine({ defaultLayout: false }));
  5650. obj.app.set('view engine', 'handlebars');
  5651. if (obj.args.trustedproxy) {
  5652. // Reverse proxy should add the "X-Forwarded-*" headers
  5653. try {
  5654. obj.app.set('trust proxy', obj.args.trustedproxy);
  5655. } catch (ex) {
  5656. // If there is an error, try to resolve the string
  5657. if ((obj.args.trustedproxy.length == 1) && (typeof obj.args.trustedproxy[0] == 'string')) {
  5658. require('dns').lookup(obj.args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.trustedproxy = [address]; } });
  5659. }
  5660. }
  5661. }
  5662. else if (typeof obj.args.tlsoffload == 'object') {
  5663. // Reverse proxy should add the "X-Forwarded-*" headers
  5664. try {
  5665. obj.app.set('trust proxy', obj.args.tlsoffload);
  5666. } catch (ex) {
  5667. // If there is an error, try to resolve the string
  5668. if ((Array.isArray(obj.args.tlsoffload)) && (obj.args.tlsoffload.length == 1) && (typeof obj.args.tlsoffload[0] == 'string')) {
  5669. require('dns').lookup(obj.args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.tlsoffload = [address]; } });
  5670. }
  5671. }
  5672. }
  5673. // Setup a keygrip instance with higher default security, default hash is SHA1, we want to bump that up with SHA384
  5674. // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
  5675. // If args.sessionkey is a string, use it as a single key, but args.sessionkey can also be used as an array of keys.
  5676. const keygrip = require('keygrip')((typeof obj.args.sessionkey == 'string') ? [obj.args.sessionkey] : obj.args.sessionkey, 'sha384', 'base64');
  5677. // Setup the cookie session
  5678. const sessionOptions = {
  5679. name: 'xid', // Recommended security practice to not use the default cookie name
  5680. httpOnly: true,
  5681. keys: keygrip,
  5682. secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
  5683. sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax')
  5684. }
  5685. if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60000); } // sessiontime is minutes
  5686. obj.app.use(require('cookie-session')(sessionOptions));
  5687. obj.app.use(function (request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
  5688. if (request.session && !request.session.regenerate) {
  5689. request.session.regenerate = function (cb) {
  5690. cb()
  5691. }
  5692. }
  5693. if (request.session && !request.session.save) {
  5694. request.session.save = function (cb) {
  5695. cb()
  5696. }
  5697. }
  5698. next()
  5699. });
  5700. // Handle all incoming web sockets, see if some need to be handled as web relays
  5701. obj.app.ws('/*', function (ws, req, next) {
  5702. if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) { handleWebRelayWebSocket(ws, req); return; }
  5703. return next();
  5704. });
  5705. // Add HTTP security headers to all responses
  5706. obj.app.use(async function (req, res, next) {
  5707. // Check if a session is destroyed
  5708. if (typeof req.session.userid == 'string') {
  5709. if (typeof req.session.x == 'string') {
  5710. if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) {
  5711. delete req.session.userid;
  5712. delete req.session.ip;
  5713. delete req.session.t;
  5714. delete req.session.x;
  5715. }
  5716. } else {
  5717. // Legacy session without a random, add one.
  5718. setSessionRandom(req);
  5719. }
  5720. }
  5721. // Remove legacy values from the session to keep the session as small as possible
  5722. delete req.session.u2f;
  5723. delete req.session.domainid;
  5724. delete req.session.nowInMinutes;
  5725. delete req.session.tokenuserid;
  5726. delete req.session.tokenusername;
  5727. delete req.session.tokenpassword;
  5728. delete req.session.tokenemail;
  5729. delete req.session.tokensms;
  5730. delete req.session.tokenpush;
  5731. delete req.session.tusername;
  5732. delete req.session.tpassword;
  5733. // Useful for debugging reverse proxy issues
  5734. parent.debug('httpheaders', req.method, req.url, req.headers);
  5735. // If this request came over HTTP, redirect to HTTPS
  5736. if (req.headers['x-forwarded-proto'] == 'http') {
  5737. var host = req.headers.host;
  5738. if (typeof host == 'string') { host = host.split(':')[0]; }
  5739. if ((host == null) && (obj.certificates != null)) { host = obj.certificates.CommonName; if (obj.certificates.CommonName.indexOf('.') == -1) { host = req.headers.host; } }
  5740. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5741. res.redirect('https://' + host + ':' + httpsPort + req.url);
  5742. return;
  5743. }
  5744. // Perform traffic accounting
  5745. if (req.headers.upgrade == 'websocket') {
  5746. // We don't count traffic on WebSockets since it's counted by the handling modules.
  5747. obj.trafficStats.httpWebSocketCount++;
  5748. } else {
  5749. // Normal HTTP traffic is counted
  5750. obj.trafficStats.httpRequestCount++;
  5751. if (typeof req.socket.xbytesRead != 'number') {
  5752. req.socket.xbytesRead = 0;
  5753. req.socket.xbytesWritten = 0;
  5754. req.socket.on('close', function () {
  5755. // Perform final accounting
  5756. obj.trafficStats.httpIn += (this.bytesRead - this.xbytesRead);
  5757. obj.trafficStats.httpOut += (this.bytesWritten - this.xbytesWritten);
  5758. this.xbytesRead = this.bytesRead;
  5759. this.xbytesWritten = this.bytesWritten;
  5760. });
  5761. } else {
  5762. // Update counters
  5763. obj.trafficStats.httpIn += (req.socket.bytesRead - req.socket.xbytesRead);
  5764. obj.trafficStats.httpOut += (req.socket.bytesWritten - req.socket.xbytesWritten);
  5765. req.socket.xbytesRead = req.socket.bytesRead;
  5766. req.socket.xbytesWritten = req.socket.bytesWritten;
  5767. }
  5768. }
  5769. // Set the real IP address of the request
  5770. // If a trusted reverse-proxy is sending us the remote IP address, use it.
  5771. var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
  5772. if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
  5773. if (
  5774. (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
  5775. ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
  5776. ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
  5777. ) {
  5778. // Get client IP
  5779. if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
  5780. req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
  5781. } else if (req.headers['x-forwarded-for']) {
  5782. req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
  5783. } else if (req.headers['x-real-ip']) {
  5784. req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
  5785. } else {
  5786. req.clientIp = ipex;
  5787. }
  5788. // If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
  5789. const clientIpSplit = req.clientIp.split(':');
  5790. if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
  5791. // Get server host
  5792. if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host'].split(',')[0]; } // If multiple hosts are specified with a comma, take the first one.
  5793. } else {
  5794. req.clientIp = ipex;
  5795. }
  5796. // If this is a web relay connection, handle it here.
  5797. if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) {
  5798. if (['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; }
  5799. }
  5800. // Get the domain for this request
  5801. const domain = req.xdomain = getDomain(req);
  5802. parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
  5803. // Skip the rest if this is an agent connection
  5804. if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
  5805. // Setup security headers
  5806. const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
  5807. var selfurl = ' wss://' + req.headers.host;
  5808. if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
  5809. const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
  5810. // If the web relay port is enabled, allow the web page to redirect to it
  5811. var extraFrameSrc = '';
  5812. if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
  5813. extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
  5814. if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
  5815. }
  5816. // Finish setup security headers
  5817. const headers = {
  5818. 'Referrer-Policy': 'no-referrer',
  5819. 'X-XSS-Protection': '1; mode=block',
  5820. 'X-Content-Type-Options': 'nosniff',
  5821. 'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' blob: mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self'; manifest-src 'self'"
  5822. };
  5823. if (req.headers['user-agent'] && (req.headers['user-agent'].indexOf('Chrome') >= 0)) { headers['Permissions-Policy'] = 'interest-cohort=()'; } // Remove Google's FLoC Network, only send this if Chrome browser
  5824. if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
  5825. if ((parent.config.settings.stricttransportsecurity === true) || ((parent.config.settings.stricttransportsecurity !== false) && (obj.isTrustedCert(domain)))) { if (typeof parent.config.settings.stricttransportsecurity == 'string') { headers['Strict-Transport-Security'] = parent.config.settings.stricttransportsecurity; } else { headers['Strict-Transport-Security'] = 'max-age=63072000'; } }
  5826. // If this domain has configured headers, add them. If a header is set to null, remove it.
  5827. if ((domain != null) && (domain.httpheaders != null) && (typeof domain.httpheaders == 'object')) {
  5828. for (var i in domain.httpheaders) { if (domain.httpheaders[i] === null) { delete headers[i]; } else { headers[i] = domain.httpheaders[i]; } }
  5829. }
  5830. res.set(headers);
  5831. // Check the session if bound to the external IP address
  5832. if ((req.session.ip != null) && (req.clientIp != null) && !checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
  5833. // Extend the session time by forcing a change to the session every minute.
  5834. if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
  5835. // Check CrowdSec Bounser if configured
  5836. if ((parent.crowdSecBounser != null) && (req.headers['upgrade'] != 'websocket') && (req.session.userid == null)) { if ((await parent.crowdSecBounser.process(domain, req, res, next)) == true) { return; } }
  5837. // Debugging code, this will stop the agent from crashing if two responses are made to the same request.
  5838. const render = res.render;
  5839. const send = res.send;
  5840. res.render = function renderWrapper(...args) {
  5841. Error.captureStackTrace(this);
  5842. return render.apply(this, args);
  5843. };
  5844. res.send = function sendWrapper(...args) {
  5845. try {
  5846. send.apply(this, args);
  5847. } catch (err) {
  5848. console.error(`Error in res.send | ${err.code} | ${err.message} | ${res.stack}`);
  5849. try {
  5850. var errlogpath = null;
  5851. if (typeof parent.args.mesherrorlogpath == 'string') { errlogpath = parent.path.join(parent.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = parent.getConfigFilePath('mesherrors.txt'); }
  5852. parent.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + `Error in res.send | ${err.code} | ${err.message} | ${res.stack}` + '\r\n');
  5853. } catch (ex) { parent.debug('error', 'Unable to write to mesherrors.txt.'); }
  5854. }
  5855. };
  5856. // Continue processing the request
  5857. return next();
  5858. });
  5859. if (obj.agentapp) {
  5860. // Add HTTP security headers to all responses
  5861. obj.agentapp.use(function (req, res, next) {
  5862. // Set the real IP address of the request
  5863. // If a trusted reverse-proxy is sending us the remote IP address, use it.
  5864. var ipex = '0.0.0.0';
  5865. if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
  5866. if (
  5867. (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
  5868. ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
  5869. ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
  5870. ) {
  5871. if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
  5872. req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
  5873. } else if (req.headers['x-forwarded-for']) {
  5874. req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
  5875. } else if (req.headers['x-real-ip']) {
  5876. req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
  5877. } else {
  5878. req.clientIp = ipex;
  5879. }
  5880. } else {
  5881. req.clientIp = ipex;
  5882. }
  5883. // Get the domain for this request
  5884. const domain = req.xdomain = getDomain(req);
  5885. parent.debug('webrequest', '(' + req.clientIp + ') AgentPort: ' + req.url);
  5886. res.removeHeader('X-Powered-By');
  5887. return next();
  5888. });
  5889. }
  5890. // Setup all sharing domains and check if auth strategies need setup
  5891. var setupSSO = false
  5892. for (var i in parent.config.domains) {
  5893. if ((parent.config.domains[i].dns == null) && (parent.config.domains[i].share != null)) { obj.app.use(parent.config.domains[i].url, obj.express.static(parent.config.domains[i].share)); }
  5894. if (typeof parent.config.domains[i].authstrategies == 'object') { setupSSO = true };
  5895. }
  5896. if (setupSSO) {
  5897. setupAllDomainAuthStrategies().then(() => finalizeWebserver());
  5898. } else {
  5899. finalizeWebserver()
  5900. }
  5901. // Setup all domain auth strategy passport.js
  5902. async function setupAllDomainAuthStrategies() {
  5903. for (var i in parent.config.domains) {
  5904. if (parent.config.domains[i].dns != null) {
  5905. if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { 'authStrategyFlags': 0 }; }
  5906. parent.config.domains[''].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
  5907. } else {
  5908. if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { 'authStrategyFlags': 0 }; }
  5909. parent.config.domains[i].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
  5910. }
  5911. }
  5912. }
  5913. function setupHTTPHandlers() {
  5914. // Setup all HTTP handlers
  5915. if (parent.pluginHandler != null) {
  5916. parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
  5917. }
  5918. if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
  5919. for (var i in parent.config.domains) {
  5920. if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
  5921. var domain = parent.config.domains[i];
  5922. var url = domain.url;
  5923. if (typeof domain.rootredirect == 'string') {
  5924. // Root page redirects the user to a different URL
  5925. obj.app.get(url, handleRootRedirect);
  5926. } else {
  5927. // Present the login page as the root page
  5928. obj.app.get(url, handleRootRequest);
  5929. obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
  5930. }
  5931. obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
  5932. if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
  5933. if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
  5934. obj.app.get(url + 'terms', handleTermsRequest);
  5935. obj.app.get(url + 'xterm', handleXTermRequest);
  5936. obj.app.get(url + 'login', handleRootRequest);
  5937. obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
  5938. obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
  5939. obj.app.get(url + 'logout', handleLogoutRequest);
  5940. obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
  5941. obj.app.get(url + 'manifest.json', handleManifestRequest);
  5942. obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
  5943. obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
  5944. obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
  5945. obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
  5946. obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
  5947. obj.app.get(url + 'checkmail', handleCheckMailRequest);
  5948. obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
  5949. obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
  5950. obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
  5951. obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
  5952. obj.app.get(url + 'messenger', handleMessengerRequest);
  5953. obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
  5954. obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
  5955. obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
  5956. obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
  5957. obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
  5958. obj.app.get(url + 'commander.ashx', handleMeshCommander);
  5959. obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
  5960. obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
  5961. obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
  5962. obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
  5963. obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
  5964. obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
  5965. obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
  5966. obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
  5967. obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
  5968. obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
  5969. obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
  5970. obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie, authData) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
  5971. obj.app.ws(url + 'control.ashx', function (ws, req) {
  5972. getWebsocketArgs(ws, req, function (ws, req) {
  5973. const domain = getDomain(req);
  5974. if (obj.CheckWebServerOriginName(domain, req) == false) {
  5975. try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
  5976. try { ws.close(); } catch (ex) { }
  5977. return;
  5978. }
  5979. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { // Check 3FA URL key
  5980. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'nokey' })); } catch (ex) { }
  5981. try { ws.close(); } catch (ex) { }
  5982. return;
  5983. }
  5984. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  5985. if (user == null) { // User is not authenticated, perform inner server authentication
  5986. if (req.headers['x-meshauth'] === '*') {
  5987. PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); }); // User is authenticated
  5988. } else {
  5989. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth' })); } catch (ex) { }
  5990. try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
  5991. }
  5992. } else {
  5993. obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
  5994. }
  5995. });
  5996. });
  5997. });
  5998. obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
  5999. obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
  6000. obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
  6001. obj.app.get(url + 'logo.png', handleLogoRequest);
  6002. obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
  6003. obj.app.get(url + 'pwalogo.png', handlePWALogoRequest);
  6004. obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
  6005. obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
  6006. obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
  6007. obj.app.get(url + 'recordings.ashx', handleGetRecordings);
  6008. obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
  6009. obj.app.get(url + 'player.htm', handlePlayerRequest);
  6010. obj.app.get(url + 'player', handlePlayerRequest);
  6011. obj.app.get(url + 'sharing', handleSharingRequest);
  6012. obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
  6013. obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
  6014. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6015. if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
  6016. obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
  6017. } else {
  6018. obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
  6019. }
  6020. });
  6021. });
  6022. if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
  6023. obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
  6024. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6025. if ((user == null) || (cookie == null)) {
  6026. try { ws1.close(); } catch (ex) { }
  6027. } else {
  6028. obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
  6029. }
  6030. });
  6031. });
  6032. }
  6033. obj.app.get(url + 'invite', handleInviteRequest);
  6034. obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
  6035. if (parent.pluginHandler != null) {
  6036. obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
  6037. obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
  6038. obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
  6039. }
  6040. // New account CAPTCHA request
  6041. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  6042. obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
  6043. }
  6044. // Check CrowdSec Bounser if configured
  6045. if (parent.crowdSecBounser != null) {
  6046. obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
  6047. obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
  6048. }
  6049. // Setup IP-KVM relay if supported
  6050. if (domain.ipkvm) {
  6051. obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
  6052. const domain = getDomain(req);
  6053. if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
  6054. parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
  6055. });
  6056. obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
  6057. const domain = getDomain(req);
  6058. if (domain == null) return;
  6059. parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
  6060. });
  6061. }
  6062. // Setup RDP unless indicated as disabled
  6063. if (domain.mstsc !== false) {
  6064. obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
  6065. obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
  6066. const domain = getDomain(req);
  6067. if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
  6068. // If no user is logged in and we have a default user, set it now.
  6069. if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
  6070. try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6071. });
  6072. }
  6073. // Setup SSH if needed
  6074. if (domain.ssh === true) {
  6075. obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
  6076. obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
  6077. const domain = getDomain(req);
  6078. if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
  6079. // If no user is logged in and we have a default user, set it now.
  6080. if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
  6081. try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6082. });
  6083. obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
  6084. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6085. require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
  6086. });
  6087. });
  6088. obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
  6089. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6090. require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
  6091. });
  6092. });
  6093. }
  6094. // Setup firebase push only server
  6095. if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
  6096. if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
  6097. if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
  6098. }
  6099. // Setup auth strategies using passport if needed
  6100. if (typeof domain.authstrategies == 'object') {
  6101. parent.authLog('setupHTTPHandlers', `Setting up authentication strategies login and callback URLs for ${domain.id == '' ? 'root' : '"' + domain.id + '"'} domain.`);
  6102. // Twitter
  6103. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
  6104. obj.app.get(url + 'auth-twitter', function (req, res, next) {
  6105. var domain = getDomain(req);
  6106. if (domain.passport == null) { next(); return; }
  6107. domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
  6108. });
  6109. obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
  6110. var domain = getDomain(req);
  6111. if (domain.passport == null) { next(); return; }
  6112. if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
  6113. // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
  6114. var url = req.url;
  6115. if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
  6116. res.set('Content-Type', 'text/html');
  6117. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
  6118. } else {
  6119. domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
  6120. }
  6121. }, handleStrategyLogin);
  6122. }
  6123. // Google
  6124. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
  6125. obj.app.get(url + 'auth-google', function (req, res, next) {
  6126. var domain = getDomain(req);
  6127. if (domain.passport == null) { next(); return; }
  6128. domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
  6129. });
  6130. obj.app.get(url + 'auth-google-callback', function (req, res, next) {
  6131. var domain = getDomain(req);
  6132. if (domain.passport == null) { next(); return; }
  6133. domain.passport.authenticate('google-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
  6134. }, handleStrategyLogin);
  6135. }
  6136. // GitHub
  6137. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
  6138. obj.app.get(url + 'auth-github', function (req, res, next) {
  6139. var domain = getDomain(req);
  6140. if (domain.passport == null) { next(); return; }
  6141. domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
  6142. });
  6143. obj.app.get(url + 'auth-github-callback', function (req, res, next) {
  6144. var domain = getDomain(req);
  6145. if (domain.passport == null) { next(); return; }
  6146. domain.passport.authenticate('github-' + domain.id, { failureRedirect: domain.url })(req, res, next);
  6147. }, handleStrategyLogin);
  6148. }
  6149. // Azure
  6150. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
  6151. obj.app.get(url + 'auth-azure', function (req, res, next) {
  6152. var domain = getDomain(req);
  6153. if (domain.passport == null) { next(); return; }
  6154. domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
  6155. });
  6156. obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
  6157. var domain = getDomain(req);
  6158. if (domain.passport == null) { next(); return; }
  6159. if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
  6160. // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
  6161. var url = req.url;
  6162. if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
  6163. res.set('Content-Type', 'text/html');
  6164. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
  6165. } else {
  6166. if (req.query.state != null) {
  6167. var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  6168. if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: domain.url })(req, res, next); return; }
  6169. }
  6170. next();
  6171. }
  6172. }, handleStrategyLogin);
  6173. }
  6174. // Setup OpenID Connect URLs
  6175. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
  6176. let authURL = url + 'auth-oidc'
  6177. parent.authLog('setupHTTPHandlers', `OIDC: Authorization URL: ${authURL}`);
  6178. obj.app.get(authURL, function (req, res, next) {
  6179. var domain = getDomain(req);
  6180. if (domain.passport == null) { next(); return; }
  6181. domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6182. });
  6183. let redirectPath;
  6184. if (typeof domain.authstrategies.oidc.client.redirect_uri == 'string') {
  6185. redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uri)).pathname;
  6186. } else if (Array.isArray(domain.authstrategies.oidc.client.redirect_uris)) {
  6187. redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uris[0])).pathname;
  6188. } else {
  6189. redirectPath = url + 'auth-oidc-callback';
  6190. }
  6191. parent.authLog('setupHTTPHandlers', `OIDC: Callback URL: ${redirectPath}`);
  6192. obj.app.get(redirectPath, obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6193. var domain = getDomain(req);
  6194. if (domain.passport == null) { next(); return; }
  6195. if (req.session && req.session.userid) { next(); return; } // already logged in so dont authenticate just carry on
  6196. if (req.session && req.session['oidc-' + domain.id]) { // we have a request to login so do authenticate
  6197. domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6198. } else { // no idea so carry on
  6199. next(); return;
  6200. }
  6201. }, handleStrategyLogin);
  6202. }
  6203. // Generic SAML
  6204. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
  6205. obj.app.get(url + 'auth-saml', function (req, res, next) {
  6206. var domain = getDomain(req);
  6207. if (domain.passport == null) { next(); return; }
  6208. domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6209. });
  6210. obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6211. var domain = getDomain(req);
  6212. if (domain.passport == null) { next(); return; }
  6213. domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6214. }, handleStrategyLogin);
  6215. }
  6216. // Intel SAML
  6217. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
  6218. obj.app.get(url + 'auth-intel', function (req, res, next) {
  6219. var domain = getDomain(req);
  6220. if (domain.passport == null) { next(); return; }
  6221. domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6222. });
  6223. obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6224. var domain = getDomain(req);
  6225. if (domain.passport == null) { next(); return; }
  6226. domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6227. }, handleStrategyLogin);
  6228. }
  6229. // JumpCloud SAML
  6230. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
  6231. obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
  6232. var domain = getDomain(req);
  6233. if (domain.passport == null) { next(); return; }
  6234. domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6235. });
  6236. obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6237. var domain = getDomain(req);
  6238. if (domain.passport == null) { next(); return; }
  6239. domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6240. }, handleStrategyLogin);
  6241. }
  6242. }
  6243. // Server redirects
  6244. if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
  6245. // Server picture
  6246. obj.app.get(url + 'serverpic.ashx', function (req, res) {
  6247. // Check if we have "server.jpg" in the data folder, if so, use that.
  6248. if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
  6249. res.set({ 'Content-Type': 'image/png' });
  6250. res.send(parent.configurationFiles['server.png']);
  6251. } else {
  6252. // Check if we have "server.jpg" in the data folder, if so, use that.
  6253. var p = obj.path.join(obj.parent.datapath, 'server.png');
  6254. if (obj.fs.existsSync(p)) {
  6255. // Use the data folder server picture
  6256. try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
  6257. } else {
  6258. var domain = getDomain(req);
  6259. if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
  6260. // Use the domain server picture
  6261. try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6262. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
  6263. // Use the override server picture
  6264. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6265. } else {
  6266. // Use the default server picture
  6267. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6268. }
  6269. }
  6270. }
  6271. });
  6272. // Receive mesh agent connections
  6273. obj.app.ws(url + 'agent.ashx', function (ws, req) {
  6274. var domain = checkAgentIpAddress(ws, req);
  6275. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6276. if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
  6277. //console.log('Agent connect: ' + req.clientIp);
  6278. try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6279. });
  6280. // Setup MQTT broker over websocket
  6281. if (obj.parent.mqttbroker != null) {
  6282. obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
  6283. var domain = checkAgentIpAddress(ws, req);
  6284. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6285. var serialtunnel = SerialTunnel();
  6286. serialtunnel.xtransport = 'ws';
  6287. serialtunnel.xdomain = domain;
  6288. serialtunnel.xip = req.clientIp;
  6289. ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
  6290. serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
  6291. ws.on('close', function () { serialtunnel.emit('end'); });
  6292. obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
  6293. });
  6294. }
  6295. // Setup any .well-known folders
  6296. var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
  6297. if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
  6298. // Setup the alternative agent-only port
  6299. if (obj.agentapp) {
  6300. // Receive mesh agent connections on alternate port
  6301. obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
  6302. var domain = checkAgentIpAddress(ws, req);
  6303. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6304. if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
  6305. try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
  6306. });
  6307. // Setup mesh relay on alternative agent-only port
  6308. obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
  6309. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6310. if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
  6311. obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
  6312. } else {
  6313. obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
  6314. }
  6315. });
  6316. });
  6317. // Allows agents to transfer files
  6318. obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
  6319. // Setup agent to/from server file transfer handler
  6320. obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
  6321. // Setup agent downloads for meshcore updates
  6322. obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
  6323. // Setup agent file downloads
  6324. obj.agentapp.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
  6325. }
  6326. // Setup web relay on this web server if needed
  6327. // We set this up when a DNS name is used as a web relay instead of a port
  6328. if (obj.args.relaydns != null) {
  6329. obj.webRelayRouter = require('express').Router();
  6330. // This is the magic URL that will setup the relay session
  6331. obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
  6332. if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
  6333. if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
  6334. res.set({ 'Cache-Control': 'no-store' });
  6335. parent.debug('web', 'webRelaySetup');
  6336. // Decode the relay cookie
  6337. if (req.query.c == null) { res.sendStatus(404); return; }
  6338. // Decode and check if this relay cookie is valid
  6339. var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
  6340. const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey, 32); // Allow cookies up to 32 minutes old. The web page will renew this cookie every 30 minutes.
  6341. if (urlCookie == null) { res.sendStatus(404); return; }
  6342. // Decode the incoming cookie
  6343. if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
  6344. if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
  6345. // This is a standard user, figure out what our web relay will be.
  6346. if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
  6347. if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
  6348. if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
  6349. userid = req.session.userid;
  6350. domainid = userid.split('/')[1];
  6351. domain = parent.config.domains[domainid];
  6352. nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
  6353. addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
  6354. port = parseInt(req.query.p);
  6355. appid = parseInt(req.query.appid);
  6356. webSessionId = req.session.userid + '/' + req.session.x;
  6357. // Check that all the required arguments are present
  6358. if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
  6359. } else if (urlCookie.r == 8) {
  6360. // This is a guest user, figure out what our web relay will be.
  6361. userid = urlCookie.userid;
  6362. domainid = userid.split('/')[1];
  6363. domain = parent.config.domains[domainid];
  6364. nodeid = urlCookie.nid;
  6365. addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
  6366. port = urlCookie.port;
  6367. appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
  6368. webSessionId = userid + '/' + urlCookie.pid;
  6369. publicid = urlCookie.pid;
  6370. if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
  6371. if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
  6372. if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
  6373. expire = urlCookie.expire;
  6374. if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
  6375. }
  6376. // No session identifier was setup, exit now
  6377. if (webSessionId == null) { res.sendStatus(404); return; }
  6378. // Check that we have an exact session on any of the relay DNS names
  6379. var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
  6380. for (var hostIndex in obj.args.relaydns) {
  6381. const host = obj.args.relaydns[hostIndex];
  6382. xrelaySessionId = webSessionId + '/' + host;
  6383. xrelaySession = webRelaySessions[xrelaySessionId];
  6384. if (xrelaySession == null) {
  6385. // We found an unused hostname, save this as it could be useful.
  6386. if (freeRelayHost == null) { freeRelayHost = host; }
  6387. } else {
  6388. // Check if we already have a relay session that matches exactly what we want
  6389. if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
  6390. // We found an exact match, we are all setup already, redirect to root of that DNS name
  6391. if (host == req.hostname) {
  6392. // Request was made on the same host, redirect to root.
  6393. res.redirect('/');
  6394. } else {
  6395. // Request was made to a different host
  6396. const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  6397. res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
  6398. }
  6399. return;
  6400. }
  6401. // Keep a record of the oldest web relay session, this could be useful.
  6402. if (oldestRelayHost == null) {
  6403. // Oldest host not set yet, set it
  6404. oldestRelayHost = host;
  6405. oldestRelayTime = xrelaySession.lastOperation;
  6406. } else {
  6407. // Check if this host is older then oldest so far
  6408. if (oldestRelayTime > xrelaySession.lastOperation) {
  6409. oldestRelayHost = host;
  6410. oldestRelayTime = xrelaySession.lastOperation;
  6411. }
  6412. }
  6413. }
  6414. }
  6415. // Check that the user has rights to access this device
  6416. parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
  6417. // If there is no remote control or relay rights, reject this web relay
  6418. if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
  6419. // Check if there is a free relay DNS name we can use
  6420. var selectedHost = null;
  6421. if (freeRelayHost != null) {
  6422. // There is a free one, use it.
  6423. selectedHost = freeRelayHost;
  6424. } else {
  6425. // No free ones, close the oldest one
  6426. selectedHost = oldestRelayHost;
  6427. }
  6428. xrelaySessionId = webSessionId + '/' + selectedHost;
  6429. if (selectedHost == req.hostname) {
  6430. // If this web relay session id is not free, close it now
  6431. xrelaySession = webRelaySessions[xrelaySessionId];
  6432. if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
  6433. // Create a web relay session
  6434. const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
  6435. relaySession.xpublicid = publicid;
  6436. relaySession.onclose = function (sessionId) {
  6437. // Remove the relay session
  6438. delete webRelaySessions[sessionId];
  6439. // If there are not more relay sessions, clear the cleanup timer
  6440. if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
  6441. }
  6442. // Set the multi-tunnel session
  6443. webRelaySessions[xrelaySessionId] = relaySession;
  6444. // Setup the cleanup timer if needed
  6445. if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
  6446. // Redirect to root.
  6447. res.redirect('/');
  6448. } else {
  6449. if (req.query.noredirect != null) {
  6450. // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
  6451. res.sendStatus(404);
  6452. } else {
  6453. // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
  6454. const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  6455. res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
  6456. }
  6457. }
  6458. });
  6459. });
  6460. // Handle all incoming requests as web relays
  6461. obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6462. // Handle all incoming requests as web relays
  6463. obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6464. // Handle all incoming requests as web relays
  6465. obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6466. // Handle all incoming requests as web relays
  6467. obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6468. // Handle all incoming requests as web relays
  6469. obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6470. // Handle all incoming requests as web relays
  6471. obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6472. }
  6473. // Indicates to ExpressJS that the override public folder should be used to serve static files.
  6474. obj.app.use(url, function(req, res, next){
  6475. var domain = getDomain(req);
  6476. if (domain.webpublicpath != null) { // Use domain public path
  6477. obj.express.static(domain.webpublicpath)(req, res, next);
  6478. } else if (obj.parent.webPublicOverridePath != null) { // Use override path
  6479. obj.express.static(obj.parent.webPublicOverridePath)(req, res, next);
  6480. } else { // carry on and use default public path
  6481. next();
  6482. }
  6483. });
  6484. // Indicates to ExpressJS that the default public folder should be used to serve static files.
  6485. obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
  6486. // Start regular disconnection list flush every 2 minutes.
  6487. obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
  6488. }
  6489. }
  6490. function finalizeWebserver() {
  6491. // Setup all HTTP handlers
  6492. setupHTTPHandlers()
  6493. // Handle 404 error
  6494. if (obj.args.nice404 !== false) {
  6495. obj.app.use(function (req, res, next) {
  6496. parent.debug('web', '404 Error ' + req.url);
  6497. var domain = getDomain(req);
  6498. if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
  6499. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
  6500. const cspNonce = obj.crypto.randomBytes(15).toString('base64');
  6501. res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
  6502. res.status(404).render(getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
  6503. });
  6504. }
  6505. // Start server on a free port.
  6506. CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
  6507. // Start on a second agent-only alternative port if needed.
  6508. if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
  6509. // We are done starting the web server.
  6510. if (doneFunc) doneFunc();
  6511. }
  6512. }
  6513. function nice404(req, res) {
  6514. parent.debug('web', '404 Error ' + req.url);
  6515. var domain = getDomain(req);
  6516. if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
  6517. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
  6518. if (obj.args.nice404 == false) { res.sendStatus(404); return; }
  6519. const cspNonce = obj.crypto.randomBytes(15).toString('base64');
  6520. res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
  6521. res.status(404).render(getRenderPage((domain.sitestyle == 2 || domain.sitestyle == 3) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
  6522. }
  6523. // Auth strategy flags
  6524. const domainAuthStrategyConsts = {
  6525. twitter: 1,
  6526. google: 2,
  6527. github: 3,
  6528. reddit: 8, // Deprecated
  6529. azure: 16,
  6530. oidc: 32,
  6531. saml: 64,
  6532. intelSaml: 128,
  6533. jumpCloudSaml: 256
  6534. }
  6535. // Setup auth strategies for a domain
  6536. async function setupDomainAuthStrategy(domain) {
  6537. // Return binary flags representing all auth strategies that have been setup
  6538. let authStrategyFlags = 0;
  6539. // Setup auth strategies using passport if needed
  6540. if (typeof domain.authstrategies != 'object') return authStrategyFlags;
  6541. const url = domain.url
  6542. const passport = domain.passport = require('passport');
  6543. passport.serializeUser(function (user, done) { done(null, user.sid); });
  6544. passport.deserializeUser(function (sid, done) { done(null, { sid: sid }); });
  6545. obj.app.use(passport.initialize());
  6546. obj.app.use(require('connect-flash')());
  6547. // Twitter
  6548. if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) {
  6549. const TwitterStrategy = require('passport-twitter');
  6550. let options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
  6551. if (typeof domain.authstrategies.twitter.callbackurl == 'string') { options.callbackURL = domain.authstrategies.twitter.callbackurl; } else { options.callbackURL = url + 'auth-twitter-callback'; }
  6552. parent.authLog('setupDomainAuthStrategy', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
  6553. passport.use('twitter-' + domain.id, new TwitterStrategy(options,
  6554. function (token, tokenSecret, profile, cb) {
  6555. parent.authLog('setupDomainAuthStrategy', 'Twitter profile: ' + JSON.stringify(profile));
  6556. var user = { sid: '~twitter:' + profile.id, name: profile.displayName, strategy: 'twitter' };
  6557. if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
  6558. return cb(null, user);
  6559. }
  6560. ));
  6561. authStrategyFlags |= domainAuthStrategyConsts.twitter;
  6562. }
  6563. // Google
  6564. if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) {
  6565. const GoogleStrategy = require('passport-google-oauth20');
  6566. let options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
  6567. if (typeof domain.authstrategies.google.callbackurl == 'string') { options.callbackURL = domain.authstrategies.google.callbackurl; } else { options.callbackURL = url + 'auth-google-callback'; }
  6568. parent.authLog('setupDomainAuthStrategy', 'Adding Google SSO with options: ' + JSON.stringify(options));
  6569. passport.use('google-' + domain.id, new GoogleStrategy(options,
  6570. function (token, tokenSecret, profile, cb) {
  6571. parent.authLog('setupDomainAuthStrategy', 'Google profile: ' + JSON.stringify(profile));
  6572. var user = { sid: '~google:' + profile.id, name: profile.displayName, strategy: 'google' };
  6573. if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string') && (profile.emails[0].verified == true)) { user.email = profile.emails[0].value; }
  6574. return cb(null, user);
  6575. }
  6576. ));
  6577. authStrategyFlags |= domainAuthStrategyConsts.google;
  6578. }
  6579. // Github
  6580. if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) {
  6581. const GitHubStrategy = require('passport-github2');
  6582. let options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
  6583. if (typeof domain.authstrategies.github.callbackurl == 'string') { options.callbackURL = domain.authstrategies.github.callbackurl; } else { options.callbackURL = url + 'auth-github-callback'; }
  6584. parent.authLog('setupDomainAuthStrategy', 'Adding Github SSO with options: ' + JSON.stringify(options));
  6585. passport.use('github-' + domain.id, new GitHubStrategy(options,
  6586. function (token, tokenSecret, profile, cb) {
  6587. parent.authLog('setupDomainAuthStrategy', 'Github profile: ' + JSON.stringify(profile));
  6588. var user = { sid: '~github:' + profile.id, name: profile.displayName, strategy: 'github' };
  6589. if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
  6590. return cb(null, user);
  6591. }
  6592. ));
  6593. authStrategyFlags |= domainAuthStrategyConsts.github;
  6594. }
  6595. // Azure
  6596. if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
  6597. const AzureOAuth2Strategy = require('passport-azure-oauth2');
  6598. let options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
  6599. if (typeof domain.authstrategies.azure.callbackurl == 'string') { options.callbackURL = domain.authstrategies.azure.callbackurl; } else { options.callbackURL = url + 'auth-azure-callback'; }
  6600. parent.authLog('setupDomainAuthStrategy', 'Adding Azure SSO with options: ' + JSON.stringify(options));
  6601. passport.use('azure-' + domain.id, new AzureOAuth2Strategy(options,
  6602. function (accessToken, refreshtoken, params, profile, done) {
  6603. var userex = null;
  6604. try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
  6605. parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
  6606. var user = null;
  6607. if (userex != null) {
  6608. var user = { sid: '~azure:' + userex.unique_name, name: userex.name, strategy: 'azure' };
  6609. if (typeof userex.email == 'string') { user.email = userex.email; }
  6610. }
  6611. return done(null, user);
  6612. }
  6613. ));
  6614. authStrategyFlags |= domainAuthStrategyConsts.azure;
  6615. }
  6616. // Generic SAML
  6617. if (typeof domain.authstrategies.saml == 'object') {
  6618. if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
  6619. parent.debug('error', 'Missing SAML configuration.');
  6620. } else {
  6621. const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
  6622. var cert = obj.fs.readFileSync(certPath);
  6623. if (cert == null) {
  6624. parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
  6625. } else {
  6626. var options = { entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
  6627. if (typeof domain.authstrategies.saml.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.saml.callbackurl; } else { options.callbackUrl = url + 'auth-saml-callback'; }
  6628. if (domain.authstrategies.saml.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.saml.disablerequestedauthncontext; }
  6629. if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
  6630. parent.authLog('setupDomainAuthStrategy', 'Adding SAML SSO with options: ' + JSON.stringify(options));
  6631. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  6632. const SamlStrategy = require('passport-saml').Strategy;
  6633. passport.use('saml-' + domain.id, new SamlStrategy(options,
  6634. function (profile, done) {
  6635. parent.authLog('setupDomainAuthStrategy', 'SAML profile: ' + JSON.stringify(profile));
  6636. if (typeof profile.nameID != 'string') { return done(); }
  6637. var user = { sid: '~saml:' + profile.nameID, name: profile.nameID, strategy: 'saml' };
  6638. if (typeof profile.displayname == 'string') {
  6639. user.name = profile.displayname;
  6640. } else if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) {
  6641. user.name = profile.firstname + ' ' + profile.lastname;
  6642. }
  6643. if (typeof profile.email == 'string') { user.email = profile.email; }
  6644. return done(null, user);
  6645. }
  6646. ));
  6647. authStrategyFlags |= domainAuthStrategyConsts.saml
  6648. }
  6649. }
  6650. }
  6651. // Intel SAML
  6652. if (typeof domain.authstrategies.intel == 'object') {
  6653. if ((typeof domain.authstrategies.intel.cert != 'string') || (typeof domain.authstrategies.intel.idpurl != 'string')) {
  6654. parent.debug('error', 'Missing Intel SAML configuration.');
  6655. } else {
  6656. var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
  6657. if (cert == null) {
  6658. parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
  6659. } else {
  6660. var options = { entryPoint: domain.authstrategies.intel.idpurl, issuer: 'meshcentral' };
  6661. if (typeof domain.authstrategies.intel.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.intel.callbackurl; } else { options.callbackUrl = url + 'auth-intel-callback'; }
  6662. if (domain.authstrategies.intel.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.intel.disablerequestedauthncontext; }
  6663. if (typeof domain.authstrategies.intel.entityid == 'string') { options.issuer = domain.authstrategies.intel.entityid; }
  6664. parent.authLog('setupDomainAuthStrategy', 'Adding Intel SSO with options: ' + JSON.stringify(options));
  6665. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  6666. const SamlStrategy = require('passport-saml').Strategy;
  6667. passport.use('isaml-' + domain.id, new SamlStrategy(options,
  6668. function (profile, done) {
  6669. parent.authLog('setupDomainAuthStrategy', 'Intel profile: ' + JSON.stringify(profile));
  6670. if (typeof profile.nameID != 'string') { return done(); }
  6671. var user = { sid: '~intel:' + profile.nameID, name: profile.nameID, strategy: 'intel' };
  6672. if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
  6673. else if ((typeof profile.FirstName == 'string') && (typeof profile.LastName == 'string')) { user.name = profile.FirstName + ' ' + profile.LastName; }
  6674. if (typeof profile.email == 'string') { user.email = profile.email; }
  6675. else if (typeof profile.EmailAddress == 'string') { user.email = profile.EmailAddress; }
  6676. return done(null, user);
  6677. }
  6678. ));
  6679. authStrategyFlags |= domainAuthStrategyConsts.intelSaml
  6680. }
  6681. }
  6682. }
  6683. // JumpCloud SAML
  6684. if (typeof domain.authstrategies.jumpcloud == 'object') {
  6685. if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
  6686. parent.debug('error', 'Missing JumpCloud SAML configuration.');
  6687. } else {
  6688. var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
  6689. if (cert == null) {
  6690. parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
  6691. } else {
  6692. var options = { entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
  6693. if (typeof domain.authstrategies.jumpcloud.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.jumpcloud.callbackurl; } else { options.callbackUrl = url + 'auth-jumpcloud-callback'; }
  6694. if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
  6695. parent.authLog('setupDomainAuthStrategy', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options));
  6696. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  6697. const SamlStrategy = require('passport-saml').Strategy;
  6698. passport.use('jumpcloud-' + domain.id, new SamlStrategy(options,
  6699. function (profile, done) {
  6700. parent.authLog('setupDomainAuthStrategy', 'JumpCloud profile: ' + JSON.stringify(profile));
  6701. if (typeof profile.nameID != 'string') { return done(); }
  6702. var user = { sid: '~jumpcloud:' + profile.nameID, name: profile.nameID, strategy: 'jumpcloud' };
  6703. if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
  6704. if (typeof profile.email == 'string') { user.email = profile.email; }
  6705. return done(null, user);
  6706. }
  6707. ));
  6708. authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
  6709. }
  6710. }
  6711. }
  6712. // Setup OpenID Connect Authentication Strategy
  6713. if (obj.common.validateObject(domain.authstrategies.oidc)) {
  6714. parent.authLog('setupDomainAuthStrategy', `OIDC: Setting up strategy for domain: ${domain.id == null ? 'default' : domain.id}`);
  6715. // Ensure required objects exist
  6716. let initStrategy = domain.authstrategies.oidc
  6717. if (typeof initStrategy.issuer == 'string') { initStrategy.issuer = { 'issuer': initStrategy.issuer } }
  6718. let strategy = migrateOldConfigs(Object.assign({ 'client': {}, 'issuer': {}, 'options': {}, 'custom': {}, 'obj': { 'openidClient': require('openid-client') } }, initStrategy))
  6719. let preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
  6720. if (!preset) {
  6721. if (typeof strategy.custom.tenant_id == 'string') { strategy.custom.preset = preset = 'azure' }
  6722. if (strategy.custom.customer_id || strategy.custom.identitysource || strategy.client.client_id.split('.')[2] == 'googleusercontent') { strategy.custom.preset = preset = 'google' }
  6723. }
  6724. // Check issuer url
  6725. let presetIssuer
  6726. if (preset == 'azure') { presetIssuer = 'https://login.microsoftonline.com/' + strategy.custom.tenant_id + '/v2.0'; }
  6727. if (preset == 'google') { presetIssuer = 'https://accounts.google.com'; }
  6728. if (!obj.common.validateString(strategy.issuer.issuer)) {
  6729. if (!preset) {
  6730. let error = new Error('OIDC: Missing issuer URI.');
  6731. parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
  6732. throw error;
  6733. } else {
  6734. strategy.issuer.issuer = presetIssuer
  6735. parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
  6736. }
  6737. } else if ((typeof strategy.issuer.issuer == 'string') && (typeof strategy.custom.preset == 'string')) {
  6738. let error = new Error(`OIDC: PRESET: ${strategy.custom.preset.toUpperCase()}: PRESET OVERRIDDEN: CONFIG ISSUER: ${strategy.issuer.issuer} PRESET ISSUER: ${presetIssuer}`);
  6739. parent.authLog('setupDomainAuthStrategy', error.message);
  6740. console.warn(error)
  6741. }
  6742. // Setup Strategy Options
  6743. strategy.custom.scope = obj.common.convertStrArray(strategy.custom.scope, ' ')
  6744. if (strategy.custom.scope.length > 1) {
  6745. strategy.options = Object.assign(strategy.options, { 'params': { 'scope': strategy.custom.scope } })
  6746. } else {
  6747. strategy.options = Object.assign(strategy.options, { 'params': { 'scope': ['openid', 'profile', 'email'] } })
  6748. }
  6749. if (typeof strategy.groups == 'object') {
  6750. let groupScope = strategy.groups.scope || null
  6751. if (groupScope == null) {
  6752. if (preset == 'azure') { groupScope = 'Group.Read.All' }
  6753. if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
  6754. if (typeof preset != 'string') { groupScope = 'groups' }
  6755. }
  6756. strategy.options.params.scope.push(groupScope)
  6757. }
  6758. strategy.options.params.scope = strategy.options.params.scope.join(' ')
  6759. // Discover additional information if available, use endpoints from config if present
  6760. let issuer
  6761. try {
  6762. parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
  6763. issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
  6764. } catch (err) {
  6765. let error = new Error('OIDC: Discovery failed.', { cause: err });
  6766. parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
  6767. throw error
  6768. }
  6769. if (Object.keys(strategy.issuer).length > 1) {
  6770. parent.authLog('setupDomainAuthStrategy', `OIDC: Adding Issuer Metadata: ${JSON.stringify(strategy.issuer)}`);
  6771. issuer = new strategy.obj.openidClient.Issuer(Object.assign(issuer?.metadata, strategy.issuer));
  6772. }
  6773. strategy.issuer = issuer?.metadata;
  6774. strategy.obj.issuer = issuer;
  6775. var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  6776. var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
  6777. if (httpport != 443) { origin += ':' + httpport; }
  6778. // Make sure redirect_uri and post_logout_redirect_uri exist before continuing
  6779. if (!strategy.client.redirect_uri) {
  6780. strategy.client.redirect_uri = origin + url + 'auth-oidc-callback';
  6781. }
  6782. if (!strategy.client.post_logout_redirect_uri) {
  6783. strategy.client.post_logout_redirect_uri = origin + url + 'login';
  6784. }
  6785. // Create client and overwrite in options
  6786. let client = new issuer.Client(strategy.client)
  6787. strategy.options = Object.assign(strategy.options, { 'client': client, sessionKey: 'oidc-' + domain.id });
  6788. strategy.client = client.metadata
  6789. strategy.obj.client = client
  6790. // Setup strategy and save configs for later
  6791. passport.use('oidc-' + domain.id, new strategy.obj.openidClient.Strategy(strategy.options, oidcCallback));
  6792. parent.config.domains[domain.id].authstrategies.oidc = strategy;
  6793. parent.debug('verbose', 'OIDC: Saved Configuration: ' + JSON.stringify(strategy));
  6794. if (preset) { parent.authLog('setupDomainAuthStrategy', 'OIDC: ' + preset.toUpperCase() + ': Setup Complete'); }
  6795. else { parent.authLog('setupDomainAuthStrategy', 'OIDC: Setup Complete'); }
  6796. authStrategyFlags |= domainAuthStrategyConsts.oidc
  6797. function migrateOldConfigs(strategy) {
  6798. let oldConfigs = {
  6799. 'client': {
  6800. 'clientid': 'client_id',
  6801. 'clientsecret': 'client_secret',
  6802. 'callbackurl': 'redirect_uri'
  6803. },
  6804. 'issuer': {
  6805. 'authorizationurl': 'authorization_endpoint',
  6806. 'tokenurl': 'token_endpoint',
  6807. 'userinfourl': 'userinfo_endpoint'
  6808. },
  6809. 'custom': {
  6810. 'tenantid': 'tenant_id',
  6811. 'customerid': 'customer_id'
  6812. }
  6813. }
  6814. for (var type in oldConfigs) {
  6815. for (const [key, value] of Object.entries(oldConfigs[type])) {
  6816. if (Object.hasOwn(strategy, key)) {
  6817. if (strategy[type][value] && obj.common.validateString(strategy[type][value])) {
  6818. let error = new Error('OIDC: OLD CONFIG: Config conflict, new config overrides old config');
  6819. parent.authLog('migrateOldConfigs', `${JSON.stringify(error)} OLD CONFIG: ${key}: ${strategy[key]} NEW CONFIG: ${value}:${strategy[type][value]}`);
  6820. } else {
  6821. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
  6822. strategy[type][value] = strategy[key];
  6823. }
  6824. delete strategy[key]
  6825. }
  6826. }
  6827. }
  6828. if (typeof strategy.scope == 'string') {
  6829. if (!strategy.custom.scope) {
  6830. strategy.custom.scope = strategy.scope;
  6831. strategy.options.params = { 'scope': strategy.scope };
  6832. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.scope => strategy.custom.scope`);
  6833. } else {
  6834. let error = new Error('OIDC: OLD CONFIG: Config conflict, using new config values.');
  6835. parent.authLog('migrateOldConfigs', `${error.message} OLD CONFIG: strategy.scope: ${strategy.scope} NEW CONFIG: strategy.custom.scope:${strategy.custom.scope}`);
  6836. parent.debug('warning', error.message)
  6837. }
  6838. delete strategy.scope
  6839. }
  6840. if (strategy.groups && strategy.groups.sync && strategy.groups.sync.enabled && strategy.groups.sync.enabled === true) {
  6841. if (strategy.groups.sync.filter) {
  6842. delete strategy.groups.sync.enabled;
  6843. } else {
  6844. strategy.groups.sync = true;
  6845. }
  6846. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.groups.sync.enabled => strategy.groups.sync`);
  6847. }
  6848. return strategy
  6849. }
  6850. // Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
  6851. function oidcCallback(tokenset, profile, verified) {
  6852. // Initialize user object
  6853. let user = { 'strategy': 'oidc' }
  6854. let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null;
  6855. user.sid = obj.common.validateString(profile.sub) ? '~oidc:' + profile.sub : null;
  6856. user.name = obj.common.validateString(profile.name) ? profile.name : null;
  6857. user.email = obj.common.validateString(profile.email) ? profile.email : null;
  6858. if (claims != null) {
  6859. user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid;
  6860. user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name;
  6861. user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email;
  6862. }
  6863. user.emailVerified = profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
  6864. user.groups = obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
  6865. user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null;
  6866. if (strategy.groups && obj.common.validateString(strategy.groups.claim)) {
  6867. user.groups = obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
  6868. }
  6869. // Setup end session enpoint if not already configured this requires an auth token
  6870. try {
  6871. if (!strategy.issuer.end_session_endpoint) {
  6872. strategy.issuer.end_session_endpoint = strategy.obj.client.endSessionUrl({ 'id_token_hint': tokenset })
  6873. parent.authLog('oidcCallback', `OIDC: Discovered end_session_endpoint: ${strategy.issuer.end_session_endpoint}`);
  6874. }
  6875. } catch (err) {
  6876. let error = new Error('OIDC: Discovering end_session_endpoint failed. Using Default.', { cause: err });
  6877. strategy.issuer.end_session_endpoint = strategy.issuer.issuer + '/logout';
  6878. parent.debug('error', `${error.message} end_session_endpoint: ${strategy.issuer.end_session_endpoint} post_logout_redirect_uri: ${strategy.client.post_logout_redirect_uri} TOKENSET: ${JSON.stringify(tokenset)}`);
  6879. parent.authLog('oidcCallback', error.message);
  6880. }
  6881. // Setup presets and groups, get groups from API if needed then return
  6882. if (strategy.groups && typeof user.preset == 'string') {
  6883. getGroups(user.preset, tokenset).then((groups) => {
  6884. user = Object.assign(user, { 'groups': groups });
  6885. return verified(null, user);
  6886. }).catch((err) => {
  6887. let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
  6888. parent.debug('error', `${JSON.stringify(error)}`);
  6889. parent.authLog('oidcCallback', error.message);
  6890. user.groups = [];
  6891. return verified(null, user);
  6892. });
  6893. } else {
  6894. return verified(null, user);
  6895. }
  6896. async function getGroups(preset, tokenset) {
  6897. let url = '';
  6898. if (preset == 'azure') { url = strategy.groups.recursive == true ? 'https://graph.microsoft.com/v1.0/me/transitiveMemberOf' : 'https://graph.microsoft.com/v1.0/me/memberOf'; }
  6899. if (preset == 'google') { url = strategy.custom.customer_id ? 'https://cloudidentity.googleapis.com/v1/groups?parent=customers/' + strategy.custom.customer_id : strategy.custom.identitysource ? 'https://cloudidentity.googleapis.com/v1/groups?parent=identitysources/' + strategy.custom.identitysource : null; }
  6900. return new Promise((resolve, reject) => {
  6901. const options = {
  6902. 'headers': { authorization: 'Bearer ' + tokenset.access_token }
  6903. }
  6904. const req = require('https').get(url, options, (res) => {
  6905. let data = []
  6906. res.on('data', (chunk) => {
  6907. data.push(chunk);
  6908. });
  6909. res.on('end', () => {
  6910. if (res.statusCode < 200 || res.statusCode >= 300) {
  6911. let error = new Error('OIDC: GROUPS: Bad response code from API, statusCode: ' + res.statusCode);
  6912. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  6913. console.error(error);
  6914. reject(error);
  6915. }
  6916. if (data.length == 0) {
  6917. let error = new Error('OIDC: GROUPS: Getting groups from API failed, request returned no data in response.');
  6918. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  6919. console.error(error);
  6920. reject(error);
  6921. }
  6922. try {
  6923. if (Buffer.isBuffer(data[0])) {
  6924. data = Buffer.concat(data);
  6925. data = data.toString();
  6926. } else { // else if (typeof data[0] == 'string')
  6927. data = data.join();
  6928. }
  6929. } catch (err) {
  6930. let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: err });
  6931. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  6932. console.error(error);
  6933. reject(error);
  6934. }
  6935. if (preset == 'azure') {
  6936. data = JSON.parse(data);
  6937. if (data.error) {
  6938. let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: data.error });
  6939. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  6940. console.error(error);
  6941. reject(error);
  6942. }
  6943. data = data.value;
  6944. }
  6945. if (preset == 'google') {
  6946. data = data.split('\n');
  6947. data = data.join('');
  6948. data = JSON.parse(data);
  6949. data = data.groups;
  6950. }
  6951. let groups = []
  6952. for (var i in data) {
  6953. if (typeof data[i].displayName == 'string') {
  6954. groups.push(data[i].displayName);
  6955. }
  6956. }
  6957. if (groups.length == 0) {
  6958. let warn = new Error('OIDC: GROUPS: No groups returned from API.');
  6959. parent.authLog('getGroups', `WARN: ${warn.message} DATA: ${data}`);
  6960. console.warn(warn);
  6961. resolve(groups);
  6962. } else {
  6963. resolve(groups);
  6964. }
  6965. });
  6966. });
  6967. req.on('error', (err) => {
  6968. let error = new Error('OIDC: GROUPS: Request error.', { cause: err });
  6969. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  6970. console.error(error);
  6971. reject(error);
  6972. });
  6973. req.end();
  6974. });
  6975. }
  6976. }
  6977. }
  6978. return authStrategyFlags;
  6979. }
  6980. // Handle an incoming request as a web relay
  6981. function handleWebRelayRequest(req, res) {
  6982. var webRelaySessionId = null;
  6983. if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
  6984. else if (req.session.z != null) { webRelaySessionId = req.session.z; }
  6985. if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
  6986. var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
  6987. if (relaySession != null) {
  6988. // The web relay session is valid, use it
  6989. relaySession.handleRequest(req, res);
  6990. } else {
  6991. // No web relay session with this relay identifier, close the HTTP request.
  6992. res.sendStatus(404);
  6993. }
  6994. } else {
  6995. // The user is not logged in or does not have a relay identifier, close the HTTP request.
  6996. res.sendStatus(404);
  6997. }
  6998. }
  6999. // Handle an incoming websocket connection as a web relay
  7000. function handleWebRelayWebSocket(ws, req) {
  7001. var webRelaySessionId = null;
  7002. if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
  7003. else if (req.session.z != null) { webRelaySessionId = req.session.z; }
  7004. if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
  7005. var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
  7006. if (relaySession != null) {
  7007. // The multi-tunnel session is valid, use it
  7008. relaySession.handleWebSocket(ws, req);
  7009. } else {
  7010. // No multi-tunnel session with this relay identifier, close the websocket.
  7011. ws.close();
  7012. }
  7013. } else {
  7014. // The user is not logged in or does not have a relay identifier, close the websocket.
  7015. ws.close();
  7016. }
  7017. }
  7018. // Perform server inner authentication
  7019. // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
  7020. // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username
  7021. // and password or authentication cookie.
  7022. function PerformWSSessionInnerAuth(ws, req, domain, func) {
  7023. // When data is received from the web socket
  7024. ws.on('message', function (data) {
  7025. var command;
  7026. try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; }
  7027. if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
  7028. switch (command.action) {
  7029. case 'serverAuth': { // This command is used to perform server "inner" authentication.
  7030. // Check the client nonce and TLS hash
  7031. if ((obj.common.validateString(command.cnonce, 1, 256) == false) || (obj.common.validateString(command.tlshash, 1, 512) == false)) {
  7032. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badargs' })); } catch (ex) { }
  7033. try { ws.close(); } catch (ex) { }
  7034. break;
  7035. }
  7036. // Check that the TLS hash is an acceptable one.
  7037. var h = Buffer.from(command.tlshash, 'hex').toString('binary');
  7038. if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) {
  7039. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badtlscert' })); } catch (ex) { }
  7040. try { ws.close(); } catch (ex) { }
  7041. return;
  7042. }
  7043. // TLS hash check is a success, sign the request.
  7044. // Perform the hash signature using the server agent certificate
  7045. var nonce = obj.crypto.randomBytes(48);
  7046. var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
  7047. parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
  7048. // Send back our certificate + nonce + signature
  7049. ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(obj.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature, 'binary').toString('base64') }));
  7050. });
  7051. break;
  7052. }
  7053. case 'userAuth': { // This command is used to perform user authentication.
  7054. // Check username and password authentication
  7055. if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
  7056. obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) {
  7057. if ((err != null) || (userid == null)) {
  7058. // Invalid authentication
  7059. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
  7060. try { ws.close(); } catch (ex) { }
  7061. } else {
  7062. var user = obj.users[userid];
  7063. if ((err == null) && (user)) {
  7064. // Check if a 2nd factor is needed
  7065. const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
  7066. // See if we support two-factor trusted cookies
  7067. var twoFactorCookieDays = 30;
  7068. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7069. // Check if two factor can be skipped
  7070. const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
  7071. if ((twoFactorSkip == null) && (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true)) {
  7072. // Figure out if email 2FA is allowed
  7073. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7074. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7075. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7076. //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  7077. if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) {
  7078. if ((command.token == '**email**') && (email2fa == true)) {
  7079. // Cause a token to be sent to the user's registered email
  7080. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7081. obj.db.SetUser(user);
  7082. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7083. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7084. // Ask for a login token & confirm email was sent
  7085. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7086. } else if ((command.token == '**sms**') && (sms2fa == true)) {
  7087. // Cause a token to be sent to the user's phone number
  7088. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7089. obj.db.SetUser(user);
  7090. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7091. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7092. // Ask for a login token & confirm sms was sent
  7093. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7094. } else if ((command.token == '**msg**') && (msg2fa == true)) {
  7095. // Cause a token to be sent to the user's messenger account
  7096. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7097. obj.db.SetUser(user);
  7098. parent.debug('web', 'Sending 2FA message to: ' + user.phone);
  7099. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7100. // Ask for a login token & confirm sms was sent
  7101. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7102. /*
  7103. } else if ((command.token == '**push**') && (push2fa == true)) {
  7104. // Cause push notification to device
  7105. const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  7106. const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
  7107. var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
  7108. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  7109. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  7110. if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
  7111. });
  7112. */
  7113. } else {
  7114. // Ask for a login token
  7115. parent.debug('web', 'Asking for login token');
  7116. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (ex) { console.log(ex); }
  7117. }
  7118. } else {
  7119. checkUserOneTimePassword(req, domain, user, command.token, null, function (result, authData) {
  7120. if (result == false) {
  7121. // Failed, ask for a login token again
  7122. parent.debug('web', 'Invalid login token, asking again');
  7123. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7124. } else {
  7125. // We are authenticated with 2nd factor.
  7126. // Check email verification
  7127. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7128. parent.debug('web', 'Invalid login, asking for email validation');
  7129. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7130. } else {
  7131. // We are authenticated
  7132. ws._socket.pause();
  7133. ws.removeAllListeners(['message', 'close', 'error']);
  7134. func(ws, req, domain, user, authData);
  7135. }
  7136. }
  7137. });
  7138. }
  7139. } else {
  7140. // Check email verification
  7141. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7142. parent.debug('web', 'Invalid login, asking for email validation');
  7143. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7144. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7145. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7146. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7147. } else {
  7148. // We are authenticated
  7149. ws._socket.pause();
  7150. ws.removeAllListeners(['message', 'close', 'error']);
  7151. func(ws, req, domain, user, twoFactorSkip);
  7152. }
  7153. }
  7154. }
  7155. }
  7156. });
  7157. } else {
  7158. // Invalid authentication
  7159. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
  7160. try { ws.close(); } catch (ex) { }
  7161. }
  7162. break;
  7163. }
  7164. }
  7165. });
  7166. // If error, do nothing
  7167. ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
  7168. // If the web socket is closed
  7169. ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
  7170. // Resume the socket to perform inner authentication
  7171. try { ws._socket.resume(); } catch (ex) { }
  7172. }
  7173. // Authenticates a session and forwards
  7174. function PerformWSSessionAuth(ws, req, noAuthOk, func) {
  7175. // Check if the session expired
  7176. if ((req.session != null) && (typeof req.session.expire == 'number') && (req.session.expire <= Date.now())) {
  7177. parent.debug('web', 'WSERROR: Session expired.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'expired', msg: 'expired-1' })); ws.close(); } catch (e) { } return;
  7178. }
  7179. // Check if this is a banned ip address
  7180. if (obj.checkAllowLogin(req) == false) { parent.debug('web', 'WSERROR: Banned connection.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'banned', msg: 'banned-1' })); ws.close(); } catch (e) { } return; }
  7181. try {
  7182. // Hold this websocket until we are ready.
  7183. ws._socket.pause();
  7184. // Check IP filtering and domain
  7185. var domain = null;
  7186. if (noAuthOk == true) {
  7187. domain = getDomain(req);
  7188. if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, no auth ok.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-1' })); ws.close(); return; } catch (e) { } return; }
  7189. } else {
  7190. // If authentication is required, enforce IP address filtering.
  7191. domain = checkUserIpAddress(ws, req);
  7192. if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, user auth required.'); return; }
  7193. }
  7194. // Check if inner authentication is requested
  7195. if (req.headers['x-meshauth'] === '*') { func(ws, req, domain, null); return; }
  7196. const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
  7197. // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
  7198. if ((req.query.user != null) && (req.query.pass != null)) {
  7199. // A user/pass is provided in URL arguments
  7200. obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
  7201. var user = obj.users[userid];
  7202. // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
  7203. if ((user != null) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
  7204. // No tools allowed, close the websocket connection
  7205. parent.debug('web', 'ERR: Websocket no tools allowed');
  7206. try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
  7207. return;
  7208. }
  7209. // See if we support two-factor trusted cookies
  7210. var twoFactorCookieDays = 30;
  7211. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7212. if ((err == null) && (user)) {
  7213. // Check if a 2nd factor is needed
  7214. if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  7215. // Figure out if email 2FA is allowed
  7216. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7217. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7218. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7219. //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  7220. if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) {
  7221. if ((req.query.token == '**email**') && (email2fa == true)) {
  7222. // Cause a token to be sent to the user's registered email
  7223. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7224. obj.db.SetUser(user);
  7225. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7226. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7227. // Ask for a login token & confirm email was sent
  7228. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7229. } else if ((req.query.token == '**sms**') && (sms2fa == true)) {
  7230. // Cause a token to be sent to the user's phone number
  7231. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7232. obj.db.SetUser(user);
  7233. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7234. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7235. // Ask for a login token & confirm sms was sent
  7236. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7237. } else if ((req.query.token == '**msg**') && (msg2fa == true)) {
  7238. // Cause a token to be sent to the user's messenger account
  7239. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7240. obj.db.SetUser(user);
  7241. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  7242. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7243. // Ask for a login token & confirm message was sent
  7244. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7245. /*
  7246. } else if ((command.token == '**push**') && (push2fa == true)) {
  7247. // Cause push notification to device
  7248. const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  7249. const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
  7250. var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
  7251. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  7252. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  7253. if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
  7254. });
  7255. */
  7256. } else {
  7257. // Ask for a login token
  7258. parent.debug('web', 'Asking for login token');
  7259. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7260. }
  7261. } else {
  7262. checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result, authData) {
  7263. if (result == false) {
  7264. // Failed, ask for a login token again
  7265. parent.debug('web', 'Invalid login token, asking again');
  7266. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7267. } else {
  7268. // We are authenticated with 2nd factor.
  7269. // Check email verification
  7270. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7271. parent.debug('web', 'Invalid login, asking for email validation');
  7272. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7273. } else {
  7274. func(ws, req, domain, user, null, authData);
  7275. }
  7276. }
  7277. });
  7278. }
  7279. } else {
  7280. // Check email verification
  7281. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7282. parent.debug('web', 'Invalid login, asking for email validation');
  7283. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7284. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7285. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7286. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7287. } else {
  7288. // We are authenticated
  7289. func(ws, req, domain, user);
  7290. }
  7291. }
  7292. } else {
  7293. // Failed to authenticate, see if a default user is active
  7294. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7295. // A default user is active
  7296. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7297. } else {
  7298. // If not authenticated, close the websocket connection
  7299. parent.debug('web', 'ERR: Websocket bad user/pass auth');
  7300. //obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp });
  7301. //obj.setbadLogin(req);
  7302. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2a' })); ws.close(); } catch (e) { }
  7303. }
  7304. }
  7305. });
  7306. return;
  7307. }
  7308. if ((req.query.auth != null) && (req.query.auth != '')) {
  7309. // This is a encrypted cookie authentication
  7310. var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  7311. if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
  7312. if ((cookie != null) && (cookie.ip != null) && !checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
  7313. parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
  7314. cookie = null;
  7315. }
  7316. if ((cookie != null) && (cookie.userid != null) && (obj.users[cookie.userid]) && (cookie.domainid == domain.id) && (cookie.userid.split('/')[1] == domain.id)) {
  7317. // Valid cookie, we are authenticated. Cookie of format { userid: 'user//name', domain: '' }
  7318. func(ws, req, domain, obj.users[cookie.userid], cookie);
  7319. return;
  7320. } else if ((cookie != null) && (cookie.a === 3) && (typeof cookie.u == 'string') && (obj.users[cookie.u]) && (cookie.u.split('/')[1] == domain.id)) {
  7321. // Valid cookie, we are authenticated. Cookie of format { u: 'user//name', a: 3 }
  7322. func(ws, req, domain, obj.users[cookie.u], cookie);
  7323. return;
  7324. } else if ((cookie != null) && (cookie.nouser === 1)) {
  7325. // This is a valid cookie, but no user. This is used for agent self-sharing.
  7326. func(ws, req, domain, null, cookie);
  7327. return;
  7328. } /*else {
  7329. // This is a bad cookie, keep going anyway, maybe we have a active session that will save us.
  7330. if ((cookie != null) && (cookie.domainid != domain.id)) { parent.debug('web', 'ERR: Invalid domain, got \"' + cookie.domainid + '\", expected \"' + domain.id + '\".'); }
  7331. parent.debug('web', 'ERR: Websocket bad cookie auth (Cookie:' + (cookie != null) + '): ' + req.query.auth);
  7332. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2b' })); ws.close(); } catch (e) { }
  7333. return;
  7334. }
  7335. */
  7336. }
  7337. if (req.headers['x-meshauth'] != null) {
  7338. // This is authentication using a custom HTTP header
  7339. var s = req.headers['x-meshauth'].split(',');
  7340. for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); }
  7341. if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; }
  7342. obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) {
  7343. var user = obj.users[userid];
  7344. if ((err == null) && (user)) {
  7345. // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
  7346. if ((user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
  7347. // No tools allowed, close the websocket connection
  7348. parent.debug('web', 'ERR: Websocket no tools allowed');
  7349. try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
  7350. return;
  7351. }
  7352. // Check if a 2nd factor is needed
  7353. if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  7354. // See if we support two-factor trusted cookies
  7355. var twoFactorCookieDays = 30;
  7356. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7357. // Figure out if email 2FA is allowed
  7358. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7359. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7360. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7361. if (s.length != 3) {
  7362. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7363. } else {
  7364. checkUserOneTimePassword(req, domain, user, s[2], null, function (result, authData) {
  7365. if (result == false) {
  7366. if ((s[2] == '**email**') && (email2fa == true)) {
  7367. // Cause a token to be sent to the user's registered email
  7368. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7369. obj.db.SetUser(user);
  7370. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7371. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7372. // Ask for a login token & confirm email was sent
  7373. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7374. } else if ((s[2] == '**sms**') && (sms2fa == true)) {
  7375. // Cause a token to be sent to the user's phone number
  7376. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7377. obj.db.SetUser(user);
  7378. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7379. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7380. // Ask for a login token & confirm sms was sent
  7381. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7382. } else if ((s[2] == '**msg**') && (msg2fa == true)) {
  7383. // Cause a token to be sent to the user's phone number
  7384. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7385. obj.db.SetUser(user);
  7386. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  7387. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7388. // Ask for a login token & confirm sms was sent
  7389. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7390. } else {
  7391. // Ask for a login token
  7392. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7393. }
  7394. } else {
  7395. // We are authenticated with 2nd factor.
  7396. // Check email verification
  7397. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7398. parent.debug('web', 'Invalid login, asking for email validation');
  7399. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7400. } else {
  7401. func(ws, req, domain, user, null, authData);
  7402. }
  7403. }
  7404. });
  7405. }
  7406. } else {
  7407. // We are authenticated
  7408. // Check email verification
  7409. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7410. parent.debug('web', 'Invalid login, asking for email validation');
  7411. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7412. } else {
  7413. func(ws, req, domain, user);
  7414. }
  7415. }
  7416. } else {
  7417. // Failed to authenticate, see if a default user is active
  7418. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7419. // A default user is active
  7420. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7421. } else {
  7422. // If not authenticated, close the websocket connection
  7423. parent.debug('web', 'ERR: Websocket bad user/pass auth');
  7424. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2d' })); ws.close(); } catch (e) { }
  7425. }
  7426. }
  7427. });
  7428. return;
  7429. }
  7430. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7431. // A default user is active
  7432. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7433. return;
  7434. }
  7435. if (req.session && (req.session.userid != null) && (req.session.userid.split('/')[1] == domain.id) && (obj.users[req.session.userid])) {
  7436. // This user is logged in using the ExpressJS session
  7437. func(ws, req, domain, obj.users[req.session.userid]);
  7438. return;
  7439. }
  7440. if (noAuthOk != true) {
  7441. // If not authenticated, close the websocket connection
  7442. parent.debug('web', 'ERR: Websocket no auth');
  7443. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-4' })); ws.close(); } catch (e) { }
  7444. } else {
  7445. // Continue this session without user authentication,
  7446. // this is expected if the agent is connecting for a tunnel.
  7447. func(ws, req, domain, null);
  7448. }
  7449. } catch (e) { console.log(e); }
  7450. }
  7451. // Find a free port starting with the specified one and going up.
  7452. function CheckListenPort(port, addr, func) {
  7453. var s = obj.net.createServer(function (socket) { });
  7454. obj.tcpServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on('error', function (err) {
  7455. if (args.exactports) { console.error('ERROR: MeshCentral HTTPS server port ' + port + ' not available.'); process.exit(); }
  7456. else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } }
  7457. });
  7458. }
  7459. // Start the ExpressJS web server
  7460. function StartWebServer(port, addr) {
  7461. if ((port < 1) || (port > 65535)) return;
  7462. obj.args.port = port;
  7463. if (obj.tlsServer != null) {
  7464. if (obj.args.lanonly == true) {
  7465. obj.tcpServer = obj.tlsServer.listen(port, addr, function () { console.log('MeshCentral HTTPS server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); });
  7466. } else {
  7467. obj.tcpServer = obj.tlsServer.listen(port, addr, function () {
  7468. console.log('MeshCentral HTTPS server running on ' + certificates.CommonName + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
  7469. if (args.relaydns != null) { console.log('MeshCentral HTTPS relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
  7470. });
  7471. obj.parent.updateServerState('servername', certificates.CommonName);
  7472. }
  7473. obj.parent.debug('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
  7474. obj.parent.updateServerState('https-port', port);
  7475. if (args.aliasport != null) { obj.parent.updateServerState('https-aliasport', args.aliasport); }
  7476. } else {
  7477. obj.tcpServer = obj.app.listen(port, addr, function () {
  7478. console.log('MeshCentral HTTP server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
  7479. if (args.relaydns != null) { console.log('MeshCentral HTTP relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
  7480. });
  7481. obj.parent.updateServerState('http-port', port);
  7482. if (args.aliasport != null) { obj.parent.updateServerState('http-aliasport', args.aliasport); }
  7483. }
  7484. // Check if there is a permissions problem with the ports.
  7485. if (require('os').platform() != 'win32') {
  7486. var expectedPort = obj.parent.config.settings.port ? obj.parent.config.settings.port : 443;
  7487. if ((expectedPort != port) && (port >= 1024) && (port < 1034)) {
  7488. console.log('');
  7489. console.log('WARNING: MeshCentral is running without permissions to use ports below 1025.');
  7490. console.log(' Use setcap to grant access to lower ports, or read installation guide.');
  7491. console.log('');
  7492. console.log(' sudo setcap \'cap_net_bind_service=+ep\' `which node` \r\n');
  7493. obj.parent.addServerWarning('Server running without permissions to use ports below 1025.', false);
  7494. }
  7495. }
  7496. }
  7497. // Start the ExpressJS web server on agent-only alternative port
  7498. function StartAltWebServer(port, addr) {
  7499. if ((port < 1) || (port > 65535)) return;
  7500. var agentAliasPort = null;
  7501. var agentAliasDns = null;
  7502. if (args.agentaliasport != null) { agentAliasPort = args.agentaliasport; }
  7503. if (args.agentaliasdns != null) { agentAliasDns = args.agentaliasdns; }
  7504. if (obj.tlsAltServer != null) {
  7505. if (obj.args.lanonly == true) {
  7506. obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
  7507. } else {
  7508. obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on ' + ((agentAliasDns != null) ? agentAliasDns : certificates.CommonName) + ':' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
  7509. }
  7510. obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
  7511. obj.parent.updateServerState('https-agent-port', port);
  7512. } else {
  7513. obj.tcpAltServer = obj.agentapp.listen(port, addr, function () { console.log('MeshCentral HTTP agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
  7514. obj.parent.updateServerState('http-agent-port', port);
  7515. }
  7516. }
  7517. // Force mesh agent disconnection
  7518. obj.forceMeshAgentDisconnect = function (user, domain, nodeid, disconnectMode) {
  7519. if (nodeid == null) return;
  7520. var splitnode = nodeid.split('/');
  7521. if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
  7522. var agent = obj.wsagents[nodeid];
  7523. if (agent == null) return;
  7524. // Check we have agent rights
  7525. if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
  7526. };
  7527. // Send the core module to the mesh agent
  7528. obj.sendMeshAgentCore = function (user, domain, nodeid, coretype, coredata) {
  7529. if (nodeid == null) return;
  7530. const splitnode = nodeid.split('/');
  7531. if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
  7532. // TODO: This command only works if the agent is connected on the same server. Will not work with multi server peering.
  7533. const agent = obj.wsagents[nodeid];
  7534. if (agent == null) return;
  7535. // Check we have agent rights
  7536. if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) {
  7537. if (coretype == 'clear') {
  7538. // Clear the mesh agent core
  7539. agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
  7540. agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0));
  7541. } else if (coretype == 'default') {
  7542. // Reset to default code
  7543. agent.agentCoreCheck = 0; // Tell the agent object we are using a default code
  7544. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  7545. } else if (coretype == 'recovery') {
  7546. // Reset to recovery core
  7547. agent.agentCoreCheck = 1001; // Tell the agent object we are using the recovery core.
  7548. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  7549. } else if (coretype == 'tiny') {
  7550. // Reset to tiny core
  7551. agent.agentCoreCheck = 1011; // Tell the agent object we are using the tiny core.
  7552. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  7553. } else if (coretype == 'custom') {
  7554. agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
  7555. var buf = Buffer.from(coredata, 'utf8');
  7556. const hash = obj.crypto.createHash('sha384').update(buf).digest().toString('binary'); // Perform a SHA384 hash on the core module
  7557. agent.sendBinary(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + buf.toString('binary')); // Send the code module to the agent
  7558. }
  7559. }
  7560. };
  7561. // Get the server path of a user or mesh object
  7562. function getServerRootFilePath(obj) {
  7563. if ((typeof obj != 'object') || (obj.domain == null) || (obj._id == null)) return null;
  7564. var domainname = 'domain', splitname = obj._id.split('/');
  7565. if (splitname.length != 3) return null;
  7566. if (obj.domain !== '') domainname = 'domain-' + obj.domain;
  7567. return obj.path.join(obj.filespath, domainname + "/" + splitname[0] + "-" + splitname[2]);
  7568. }
  7569. // Return true is the input string looks like an email address
  7570. function checkEmail(str) {
  7571. var x = str.split('@');
  7572. var ok = ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2));
  7573. if (ok == true) { var y = x[1].split('.'); for (var i in y) { if (y[i].length == 0) { ok = false; } } }
  7574. return ok;
  7575. }
  7576. /*
  7577. obj.wssessions = {}; // UserId --> Array Of Sessions
  7578. obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
  7579. obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
  7580. obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
  7581. obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
  7582. */
  7583. // Count sessions and event any changes
  7584. obj.recountSessions = function (changedSessionId) {
  7585. var userid, oldcount, newcount, x, serverid;
  7586. if (changedSessionId == null) {
  7587. // Recount all sessions
  7588. // Calculate the session count for all userid's
  7589. var newSessionsCount = {};
  7590. for (userid in obj.wssessions) { newSessionsCount[userid] = obj.wssessions[userid].length; }
  7591. for (serverid in obj.wsPeerSessions3) {
  7592. for (userid in obj.wsPeerSessions3[serverid]) {
  7593. x = obj.wsPeerSessions3[serverid][userid].length;
  7594. if (newSessionsCount[userid] == null) { newSessionsCount[userid] = x; } else { newSessionsCount[userid] += x; }
  7595. }
  7596. }
  7597. // See what session counts have changed, event any changes
  7598. for (userid in newSessionsCount) {
  7599. newcount = newSessionsCount[userid];
  7600. oldcount = obj.sessionsCount[userid];
  7601. if (oldcount == null) { oldcount = 0; } else { delete obj.sessionsCount[userid]; }
  7602. if (newcount != oldcount) {
  7603. x = userid.split('/');
  7604. var u = obj.users[userid];
  7605. if (u) {
  7606. var targets = ['*', 'server-users'];
  7607. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  7608. obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
  7609. }
  7610. }
  7611. }
  7612. // If there are any counts left in the old counts, event to zero
  7613. for (userid in obj.sessionsCount) {
  7614. oldcount = obj.sessionsCount[userid];
  7615. if ((oldcount != null) && (oldcount != 0)) {
  7616. x = userid.split('/');
  7617. var u = obj.users[userid];
  7618. if (u) {
  7619. var targets = ['*', 'server-users'];
  7620. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  7621. obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', userid: userid, username: x[2], count: 0, domain: x[1], nolog: 1, nopeers: 1 })
  7622. }
  7623. }
  7624. }
  7625. // Set the new session counts
  7626. obj.sessionsCount = newSessionsCount;
  7627. } else {
  7628. // Figure out the userid
  7629. userid = changedSessionId.split('/').slice(0, 3).join('/');
  7630. // Recount only changedSessionId
  7631. newcount = 0;
  7632. if (obj.wssessions[userid] != null) { newcount = obj.wssessions[userid].length; }
  7633. for (serverid in obj.wsPeerSessions3) { if (obj.wsPeerSessions3[serverid][userid] != null) { newcount += obj.wsPeerSessions3[serverid][userid].length; } }
  7634. oldcount = obj.sessionsCount[userid];
  7635. if (oldcount == null) { oldcount = 0; }
  7636. // If the count changed, update and event
  7637. if (newcount != oldcount) {
  7638. x = userid.split('/');
  7639. var u = obj.users[userid];
  7640. if (u) {
  7641. var targets = ['*', 'server-users'];
  7642. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  7643. obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
  7644. obj.sessionsCount[userid] = newcount;
  7645. }
  7646. }
  7647. }
  7648. };
  7649. /* Access Control Functions */
  7650. // Remove user rights
  7651. function removeUserRights(rights, user) {
  7652. if (user.removeRights == null) return rights;
  7653. var add = 0, substract = 0;
  7654. if ((user.removeRights & 0x00000008) != 0) { substract += 0x00000008; } // No Remote Control
  7655. if ((user.removeRights & 0x00010000) != 0) { add += 0x00010000; } // No Desktop
  7656. if ((user.removeRights & 0x00000100) != 0) { add += 0x00000100; } // Desktop View Only
  7657. if ((user.removeRights & 0x00000200) != 0) { add += 0x00000200; } // No Terminal
  7658. if ((user.removeRights & 0x00000400) != 0) { add += 0x00000400; } // No Files
  7659. if ((user.removeRights & 0x00000010) != 0) { substract += 0x00000010; } // No Console
  7660. if ((user.removeRights & 0x00008000) != 0) { substract += 0x00008000; } // No Uninstall
  7661. if ((user.removeRights & 0x00020000) != 0) { substract += 0x00020000; } // No Remote Command
  7662. if ((user.removeRights & 0x00000040) != 0) { substract += 0x00000040; } // No Wake
  7663. if ((user.removeRights & 0x00040000) != 0) { substract += 0x00040000; } // No Reset/Off
  7664. if (rights != 0xFFFFFFFF) {
  7665. // If not administrator, add and subsctract restrictions
  7666. rights |= add;
  7667. rights &= (0xFFFFFFFF - substract);
  7668. } else {
  7669. // If administrator for a device group, start with permissions and add and subsctract restrictions
  7670. rights = 1 + 2 + 4 + 8 + 32 + 64 + 128 + 16384 + 32768 + 131072 + 262144 + 524288 + 1048576;
  7671. rights |= add;
  7672. rights &= (0xFFFFFFFF - substract);
  7673. }
  7674. return rights;
  7675. }
  7676. // Return the node and rights for a array of nodeids
  7677. obj.GetNodesWithRights = function (domain, user, nodeids, func) {
  7678. var rc = nodeids.length, r = {};
  7679. for (var i in nodeids) {
  7680. obj.GetNodeWithRights(domain, user, nodeids[i], function (node, rights, visible) {
  7681. if ((node != null) && (visible == true)) { r[node._id] = { node: node, rights: rights }; if (--rc == 0) { func(r); } }
  7682. });
  7683. }
  7684. }
  7685. // Return the node and rights for a given nodeid
  7686. obj.GetNodeWithRights = function (domain, user, nodeid, func) {
  7687. // Perform user pre-validation
  7688. if ((user == null) || (nodeid == null)) { func(null, 0, false); return; } // Invalid user
  7689. if (typeof user == 'string') { user = obj.users[user]; }
  7690. if (user == null) { func(null, 0, false); return; } // No rights
  7691. // Perform node pre-validation
  7692. if (obj.common.validateString(nodeid, 0, 128) == false) { func(null, 0, false); return; } // Invalid nodeid
  7693. const snode = nodeid.split('/');
  7694. if ((snode.length != 3) || (snode[0] != 'node')) { func(null, 0, false); return; } // Invalid nodeid
  7695. if ((domain != null) && (snode[1] != domain.id)) { func(null, 0, false); return; } // Invalid domain
  7696. // Check that we have permissions for this node.
  7697. db.Get(nodeid, function (err, nodes) {
  7698. if ((nodes == null) || (nodes.length != 1)) { func(null, 0, false); return; } // No such nodeid
  7699. // This is a super user that can see all device groups for a given domain
  7700. if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (nodes[0].domain == user.domain)) {
  7701. func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return;
  7702. }
  7703. // If no links, stop here.
  7704. if (user.links == null) { func(null, 0, false); return; }
  7705. // Check device link
  7706. var rights = 0, visible = false, r = user.links[nodeid];
  7707. if (r != null) {
  7708. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device link, stop here.
  7709. rights |= r.rights;
  7710. visible = true;
  7711. }
  7712. // Check device group link
  7713. r = user.links[nodes[0].meshid];
  7714. if (r != null) {
  7715. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device group link, stop here.
  7716. rights |= r.rights;
  7717. visible = true;
  7718. }
  7719. // Check user group links
  7720. for (var i in user.links) {
  7721. if (i.startsWith('ugrp/')) {
  7722. const g = obj.userGroups[i];
  7723. if (g && (g.links != null)) {
  7724. r = g.links[nodes[0].meshid];
  7725. if (r != null) {
  7726. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group link, stop here.
  7727. rights |= r.rights; // TODO: Deal with reverse rights
  7728. visible = true;
  7729. }
  7730. r = g.links[nodeid];
  7731. if (r != null) {
  7732. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group direct link, stop here.
  7733. rights |= r.rights; // TODO: Deal with reverse rights
  7734. visible = true;
  7735. }
  7736. }
  7737. }
  7738. }
  7739. // Remove any user rights
  7740. rights = removeUserRights(rights, user);
  7741. // Return the rights we found
  7742. func(nodes[0], rights, visible);
  7743. });
  7744. }
  7745. // Returns a list of all meshes that this user has some rights too
  7746. obj.GetAllMeshWithRights = function (user, rights) {
  7747. if (typeof user == 'string') { user = obj.users[user]; }
  7748. if (user == null) { return []; }
  7749. var r = [];
  7750. if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0)) {
  7751. // This is a super user that can see all device groups for a given domain
  7752. var meshStartStr = 'mesh/' + user.domain + '/';
  7753. for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]); } }
  7754. return r;
  7755. }
  7756. if (user.links == null) { return []; }
  7757. for (var i in user.links) {
  7758. if (i.startsWith('mesh/')) {
  7759. // Grant access to a device group thru a direct link
  7760. const m = obj.meshes[i];
  7761. if ((m) && (r.indexOf(m) == -1) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) { r.push(m); }
  7762. } else if (i.startsWith('ugrp/')) {
  7763. // Grant access to a device group thru a user group
  7764. const g = obj.userGroups[i];
  7765. for (var j in g.links) {
  7766. if (j.startsWith('mesh/') && ((rights == null) || ((g.links[j].rights != null) && (g.links[j].rights & rights) != 0))) {
  7767. const m = obj.meshes[j];
  7768. if ((m) && (m.deleted == null) && (r.indexOf(m) == -1)) { r.push(m); }
  7769. }
  7770. }
  7771. }
  7772. }
  7773. return r;
  7774. }
  7775. // Returns a list of all mesh id's that this user has some rights too
  7776. obj.GetAllMeshIdWithRights = function (user, rights) {
  7777. if (typeof user == 'string') { user = obj.users[user]; }
  7778. if (user == null) { return []; }
  7779. var r = [];
  7780. if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0)) {
  7781. // This is a super user that can see all device groups for a given domain
  7782. var meshStartStr = 'mesh/' + user.domain + '/';
  7783. for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]._id); } }
  7784. return r;
  7785. }
  7786. if (user.links == null) { return []; }
  7787. for (var i in user.links) {
  7788. if (i.startsWith('mesh/')) {
  7789. // Grant access to a device group thru a direct link
  7790. const m = obj.meshes[i];
  7791. if ((m) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
  7792. if (r.indexOf(m._id) == -1) { r.push(m._id); }
  7793. }
  7794. } else if (i.startsWith('ugrp/')) {
  7795. // Grant access to a device group thru a user group
  7796. const g = obj.userGroups[i];
  7797. if (g && (g.links != null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
  7798. for (var j in g.links) {
  7799. if (j.startsWith('mesh/')) {
  7800. const m = obj.meshes[j];
  7801. if ((m) && (m.deleted == null)) {
  7802. if (r.indexOf(m._id) == -1) { r.push(m._id); }
  7803. }
  7804. }
  7805. }
  7806. }
  7807. }
  7808. }
  7809. return r;
  7810. }
  7811. // Get the rights of a user on a given device group
  7812. obj.GetMeshRights = function (user, mesh) {
  7813. if ((user == null) || (mesh == null)) { return 0; }
  7814. if (typeof user == 'string') { user = obj.users[user]; }
  7815. if (user == null) { return 0; }
  7816. var r, meshid;
  7817. if (typeof mesh == 'string') {
  7818. meshid = mesh;
  7819. } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
  7820. meshid = mesh._id;
  7821. } else return 0;
  7822. // Check if this is a super user that can see all device groups for a given domain
  7823. if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return removeUserRights(0xFFFFFFFF, user); }
  7824. // Check direct user to device group permissions
  7825. if (user.links == null) return 0;
  7826. var rights = 0;
  7827. r = user.links[meshid];
  7828. if (r != null) {
  7829. var rights = r.rights;
  7830. if (rights == 0xFFFFFFFF) { return removeUserRights(rights, user); } // If the user has full access thru direct link, stop here.
  7831. }
  7832. // Check if we are part of any user groups that would give this user more access.
  7833. for (var i in user.links) {
  7834. if (i.startsWith('ugrp')) {
  7835. const g = obj.userGroups[i];
  7836. if (g) {
  7837. r = g.links[meshid];
  7838. if (r != null) {
  7839. if (r.rights == 0xFFFFFFFF) {
  7840. return removeUserRights(r.rights, user); // If the user hash full access thru a user group link, stop here.
  7841. } else {
  7842. rights |= r.rights; // Add to existing rights (TODO: Deal with reverse rights)
  7843. }
  7844. }
  7845. }
  7846. }
  7847. }
  7848. return removeUserRights(rights, user);
  7849. }
  7850. // Returns true if the user can view the given device group
  7851. obj.IsMeshViewable = function (user, mesh) {
  7852. if ((user == null) || (mesh == null)) { return false; }
  7853. if (typeof user == 'string') { user = obj.users[user]; }
  7854. if (user == null) { return false; }
  7855. var meshid;
  7856. if (typeof mesh == 'string') {
  7857. meshid = mesh;
  7858. } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
  7859. meshid = mesh._id;
  7860. } else return false;
  7861. // Check if this is a super user that can see all device groups for a given domain
  7862. if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return true; }
  7863. // Check direct user to device group permissions
  7864. if (user.links == null) { return false; }
  7865. if (user.links[meshid] != null) { return true; } // If the user has a direct link, stop here.
  7866. // Check if we are part of any user groups that would give this user visibility to this device group.
  7867. for (var i in user.links) {
  7868. if (i.startsWith('ugrp')) {
  7869. const g = obj.userGroups[i];
  7870. if (g && (g.links[meshid] != null)) { return true; } // If the user has a user group link, stop here.
  7871. }
  7872. }
  7873. return false;
  7874. }
  7875. var GetNodeRightsCache = {};
  7876. var GetNodeRightsCacheCount = 0;
  7877. // Return the user rights for a given node
  7878. obj.GetNodeRights = function (user, mesh, nodeid) {
  7879. if ((user == null) || (mesh == null) || (nodeid == null)) { return 0; }
  7880. if (typeof user == 'string') { user = obj.users[user]; }
  7881. if (user == null) { return 0; }
  7882. var meshid;
  7883. if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; } else return 0;
  7884. // Check if we have this in the cache
  7885. const cacheid = user._id + '/' + meshid + '/' + nodeid;
  7886. const cache = GetNodeRightsCache[cacheid];
  7887. if (cache != null) { if (cache.t > Date.now()) { return cache.o; } else { GetNodeRightsCacheCount--; } } // Cache hit, or we need to update the cache
  7888. if (GetNodeRightsCacheCount > 2000) { GetNodeRightsCache = {}; GetNodeRightsCacheCount = 0; } // From time to time, flush the cache
  7889. var r = obj.GetMeshRights(user, mesh);
  7890. if (r == 0xFFFFFFFF) {
  7891. const out = removeUserRights(r, user);
  7892. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  7893. GetNodeRightsCacheCount++;
  7894. return out;
  7895. }
  7896. // Check direct device rights using device data
  7897. if ((user.links != null) && (user.links[nodeid] != null)) { r |= user.links[nodeid].rights; } // TODO: Deal with reverse permissions
  7898. if (r == 0xFFFFFFFF) {
  7899. const out = removeUserRights(r, user);
  7900. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  7901. GetNodeRightsCacheCount++;
  7902. return out;
  7903. }
  7904. // Check direct device rights thru a user group
  7905. for (var i in user.links) {
  7906. if (i.startsWith('ugrp')) {
  7907. const g = obj.userGroups[i];
  7908. if (g && (g.links[nodeid] != null)) { r |= g.links[nodeid].rights; }
  7909. }
  7910. }
  7911. const out = removeUserRights(r, user);
  7912. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  7913. GetNodeRightsCacheCount++;
  7914. return out;
  7915. }
  7916. // Returns a list of displatch targets for a given mesh
  7917. // We have to target the meshid and all user groups for this mesh, plus any added targets
  7918. obj.CreateMeshDispatchTargets = function (mesh, addedTargets) {
  7919. var targets = (addedTargets != null) ? addedTargets : [];
  7920. if (targets.indexOf('*') == -1) { targets.push('*'); }
  7921. if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
  7922. if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
  7923. return targets;
  7924. }
  7925. // Returns a list of displatch targets for a given mesh
  7926. // We have to target the meshid and all user groups for this mesh, plus any added targets
  7927. obj.CreateNodeDispatchTargets = function (mesh, nodeid, addedTargets) {
  7928. var targets = (addedTargets != null) ? addedTargets : [];
  7929. targets.push(nodeid);
  7930. if (targets.indexOf('*') == -1) { targets.push('*'); }
  7931. if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
  7932. if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
  7933. for (var i in obj.userGroups) { const g = obj.userGroups[i]; if ((g != null) && (g.links != null) && (g.links[nodeid] != null)) { targets.push(i); } }
  7934. return targets;
  7935. }
  7936. // Clone a safe version of a user object, remove everything that is secret.
  7937. obj.CloneSafeUser = function (user) {
  7938. if (typeof user != 'object') { return user; }
  7939. var user2 = Object.assign({}, user); // Shallow clone
  7940. delete user2.hash;
  7941. delete user2.passhint;
  7942. delete user2.salt;
  7943. delete user2.type;
  7944. delete user2.domain;
  7945. delete user2.subscriptions;
  7946. delete user2.passtype;
  7947. delete user2.otpsms;
  7948. delete user2.otpmsg;
  7949. if ((typeof user2.otpekey == 'object') && (user2.otpekey != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
  7950. if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
  7951. if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
  7952. if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
  7953. if ((typeof user2.otpdev == 'string') && (user2.otpdev != null)) { user2.otpdev = 1; } // Indicates device for 2FA push notification
  7954. if ((typeof user2.webpush == 'object') && (user2.webpush != null)) { user2.webpush = user2.webpush.length; } // Indicates the number of web push sessions we have
  7955. return user2;
  7956. }
  7957. // Clone a safe version of a node object, remove everything that is secret.
  7958. obj.CloneSafeNode = function (node) {
  7959. if (typeof node != 'object') { return node; }
  7960. var r = node;
  7961. if ((r.pmt != null) || (r.ssh != null) || (r.rdp != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
  7962. r = Object.assign({}, r); // Shallow clone
  7963. if (r.pmt != null) { r.pmt = 1; }
  7964. if (r.ssh != null) {
  7965. var n = {};
  7966. for (var i in r.ssh) {
  7967. if (i.startsWith('user/')) {
  7968. if (r.ssh[i].p) { n[i] = 1; } // Username and password
  7969. else if (r.ssh[i].k && r.ssh[i].kp) { n[i] = 2; } // Username, key and password
  7970. else if (r.ssh[i].k) { n[i] = 3; } // Username and key. No password.
  7971. }
  7972. }
  7973. r.ssh = n;
  7974. }
  7975. if (r.rdp != null) { var n = {}; for (var i in r.rdp) { if (i.startsWith('user/')) { n[i] = 1; } } r.rdp = n; }
  7976. if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
  7977. r.intelamt = Object.assign({}, r.intelamt); // Shallow clone
  7978. if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node
  7979. if (r.intelamt.mpspass != null) { r.intelamt.mpspass = 1; }; // Remove the Intel AMT MPS password from the node
  7980. }
  7981. }
  7982. return r;
  7983. }
  7984. // Clone a safe version of a mesh object, remove everything that is secret.
  7985. obj.CloneSafeMesh = function (mesh) {
  7986. if (typeof mesh != 'object') { return mesh; }
  7987. var r = mesh;
  7988. if (((r.amt != null) && (r.amt.password != null)) || ((r.kvm != null) && (r.kvm.pass != null))) {
  7989. r = Object.assign({}, r); // Shallow clone
  7990. if ((r.amt != null) && (r.amt.password != null)) {
  7991. r.amt = Object.assign({}, r.amt); // Shallow clone
  7992. if ((r.amt.password != null) && (r.amt.password != '')) { r.amt.password = 1; } // Remove the Intel AMT password from the policy
  7993. }
  7994. if ((r.kvm != null) && (r.kvm.pass != null)) {
  7995. r.kvm = Object.assign({}, r.kvm); // Shallow clone
  7996. if ((r.kvm.pass != null) && (r.kvm.pass != '')) { r.kvm.pass = 1; } // Remove the IP KVM device password
  7997. }
  7998. }
  7999. return r;
  8000. }
  8001. // Filter the user web site and only output state that we need to keep
  8002. const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag'];
  8003. const acceptableUserWebStateDesktopStrings = ['encoding', 'showfocus', 'showmouse', 'showcad', 'limitFrameRate', 'noMouseRotate', 'quality', 'scaling', 'agentencoding']
  8004. obj.filterUserWebState = function (state) {
  8005. if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }
  8006. if ((state == null) || (typeof state != 'object')) { return null; }
  8007. var out = {};
  8008. for (var i in acceptableUserWebStateStrings) {
  8009. var n = acceptableUserWebStateStrings[i];
  8010. if ((state[n] != null) && ((typeof state[n] == 'number') || (typeof state[n] == 'boolean') || ((typeof state[n] == 'string') && (state[n].length < 64)))) { out[n] = state[n]; }
  8011. }
  8012. if ((typeof state.stars == 'string') && (state.stars.length < 2048)) { out.stars = state.stars; }
  8013. if (typeof state.desktopsettings == 'string') { try { state.desktopsettings = JSON.parse(state.desktopsettings); } catch (ex) { delete state.desktopsettings; } }
  8014. if (state.desktopsettings != null) {
  8015. out.desktopsettings = {};
  8016. for (var i in acceptableUserWebStateDesktopStrings) {
  8017. var n = acceptableUserWebStateDesktopStrings[i];
  8018. if ((state.desktopsettings[n] != null) && ((typeof state.desktopsettings[n] == 'number') || (typeof state.desktopsettings[n] == 'boolean') || ((typeof state.desktopsettings[n] == 'string') && (state.desktopsettings[n].length < 32)))) { out.desktopsettings[n] = state.desktopsettings[n]; }
  8019. }
  8020. out.desktopsettings = JSON.stringify(out.desktopsettings);
  8021. }
  8022. if ((typeof state.deskKeyShortcuts == 'string') && (state.deskKeyShortcuts.length < 2048)) { out.deskKeyShortcuts = state.deskKeyShortcuts; }
  8023. if ((typeof state.deskStrings == 'string') && (state.deskStrings.length < 10000)) { out.deskStrings = state.deskStrings; }
  8024. if ((typeof state.runopt == 'string') && (state.runopt.length < 30000)) { out.runopt = state.runopt; }
  8025. return JSON.stringify(out);
  8026. }
  8027. // Return the correct render page given mobile, minify and override path.
  8028. function getRenderPage(pagename, req, domain) {
  8029. var mobile = isMobileBrowser(req), minify = (domain.minify == true), p;
  8030. if (req.query.mobile == '1') { mobile = true; } else if (req.query.mobile == '0') { mobile = false; }
  8031. if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
  8032. if ((domain != null) && (domain.mobilesite === false)) { mobile = false; }
  8033. if (mobile) {
  8034. if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
  8035. if (minify) {
  8036. p = obj.path.join(domain.webviewspath, pagename + '-mobile-min');
  8037. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
  8038. }
  8039. p = obj.path.join(domain.webviewspath, pagename + '-mobile');
  8040. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
  8041. }
  8042. if (obj.parent.webViewsOverridePath != null) {
  8043. if (minify) {
  8044. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile-min');
  8045. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
  8046. }
  8047. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile');
  8048. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
  8049. }
  8050. if (minify) {
  8051. p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile-min');
  8052. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify document
  8053. }
  8054. p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile');
  8055. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile document
  8056. }
  8057. if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
  8058. if (minify) {
  8059. p = obj.path.join(domain.webviewspath, pagename + '-min');
  8060. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
  8061. }
  8062. p = obj.path.join(domain.webviewspath, pagename);
  8063. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
  8064. }
  8065. if (obj.parent.webViewsOverridePath != null) {
  8066. if (minify) {
  8067. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-min');
  8068. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
  8069. }
  8070. p = obj.path.join(obj.parent.webViewsOverridePath, pagename);
  8071. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
  8072. }
  8073. if (minify) {
  8074. p = obj.path.join(obj.parent.webViewsPath, pagename + '-min');
  8075. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify document
  8076. }
  8077. p = obj.path.join(obj.parent.webViewsPath, pagename);
  8078. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Default document
  8079. return null;
  8080. }
  8081. // Return the correct render page arguments.
  8082. function getRenderArgs(xargs, req, domain, page) {
  8083. var minify = (domain.minify == true);
  8084. if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
  8085. xargs.min = minify ? '-min' : '';
  8086. xargs.titlehtml = domain.titlehtml;
  8087. xargs.title = (domain.title != null) ? domain.title : 'MeshCentral';
  8088. if (
  8089. ((page == 'login2') && (domain.loginpicture == null) && (domain.titlehtml == null)) ||
  8090. ((page != 'login2') && (domain.titlepicture == null) && (domain.titlehtml == null))
  8091. ) {
  8092. if (domain.title == null) {
  8093. xargs.title1 = 'MeshCentral';
  8094. xargs.title2 = '';
  8095. } else {
  8096. xargs.title1 = domain.title;
  8097. xargs.title2 = domain.title2 ? domain.title2 : '';
  8098. }
  8099. } else {
  8100. xargs.title1 = domain.title1 ? domain.title1 : '';
  8101. xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
  8102. }
  8103. xargs.extitle = encodeURIComponent(xargs.title).split('\'').join('\\\'');
  8104. xargs.domainurl = domain.url;
  8105. xargs.autocomplete = (domain.autocomplete === false) ? 'autocomplete=off x' : 'autocomplete'; // This option allows autocomplete to be turned off on the login page.
  8106. if (typeof domain.hide == 'number') { xargs.hide = domain.hide; }
  8107. // To mitigate any possible BREACH attack, we generate a random 0 to 255 bytes length string here.
  8108. xargs.randomlength = (args.webpagelengthrandomization !== false) ? parent.crypto.randomBytes(parent.crypto.randomBytes(1)[0]).toString('base64') : '';
  8109. return xargs;
  8110. }
  8111. // Route a command from a agent. domainid, nodeid and meshid are the values of the source agent.
  8112. obj.routeAgentCommand = function (command, domainid, nodeid, meshid) {
  8113. // Route a message.
  8114. // If this command has a sessionid, that is the target.
  8115. if (command.sessionid != null) {
  8116. if (typeof command.sessionid != 'string') return;
  8117. var splitsessionid = command.sessionid.split('/');
  8118. // Check that we are in the same domain and the user has rights over this node.
  8119. if ((splitsessionid.length == 4) && (splitsessionid[0] == 'user') && (splitsessionid[1] == domainid)) {
  8120. // Check if this user has rights to get this message
  8121. if (obj.GetNodeRights(splitsessionid[0] + '/' + splitsessionid[1] + '/' + splitsessionid[2], meshid, nodeid) == 0) return; // TODO: Check if this is ok
  8122. // See if the session is connected. If so, go ahead and send this message to the target node
  8123. var ws = obj.wssessions2[command.sessionid];
  8124. if (ws != null) {
  8125. command.nodeid = nodeid; // Set the nodeid, required for responses.
  8126. delete command.sessionid; // Remove the sessionid, since we are sending to that sessionid, so it's implyed.
  8127. try { ws.send(JSON.stringify(command)); } catch (ex) { }
  8128. } else if (parent.multiServer != null) {
  8129. // See if we can send this to a peer server
  8130. var serverid = obj.wsPeerSessions2[command.sessionid];
  8131. if (serverid != null) {
  8132. command.fromNodeid = nodeid;
  8133. parent.multiServer.DispatchMessageSingleServer(command, serverid);
  8134. }
  8135. }
  8136. }
  8137. } else if (command.userid != null) { // If this command has a userid, that is the target.
  8138. if (typeof command.userid != 'string') return;
  8139. var splituserid = command.userid.split('/');
  8140. // Check that we are in the same domain and the user has rights over this node.
  8141. if ((splituserid[0] == 'user') && (splituserid[1] == domainid)) {
  8142. // Check if this user has rights to get this message
  8143. if (obj.GetNodeRights(command.userid, meshid, nodeid) == 0) return; // TODO: Check if this is ok
  8144. // See if the session is connected
  8145. var sessions = obj.wssessions[command.userid];
  8146. // Go ahead and send this message to the target node
  8147. if (sessions != null) {
  8148. command.nodeid = nodeid; // Set the nodeid, required for responses.
  8149. delete command.userid; // Remove the userid, since we are sending to that userid, so it's implyed.
  8150. for (i in sessions) { sessions[i].send(JSON.stringify(command)); }
  8151. }
  8152. if (parent.multiServer != null) {
  8153. // TODO: Add multi-server support
  8154. }
  8155. }
  8156. } else { // Route this command to all users with MESHRIGHT_AGENTCONSOLE rights to this device group
  8157. command.nodeid = nodeid;
  8158. var cmdstr = JSON.stringify(command);
  8159. // Find all connected user sessions with access to this device
  8160. for (var userid in obj.wssessions) {
  8161. var xsessions = obj.wssessions[userid];
  8162. if (obj.GetNodeRights(userid, meshid, nodeid) != 0) {
  8163. // Send the message to all sessions for this user on this server
  8164. for (i in xsessions) { try { xsessions[i].send(cmdstr); } catch (e) { } }
  8165. }
  8166. }
  8167. // Send the message to all users of other servers
  8168. if (parent.multiServer != null) {
  8169. delete command.nodeid;
  8170. command.fromNodeid = nodeid;
  8171. command.meshid = meshid;
  8172. parent.multiServer.DispatchMessage(command);
  8173. }
  8174. }
  8175. }
  8176. // Returns a list of acceptable languages in order
  8177. obj.getLanguageCodes = function (req) {
  8178. // If a user set a localization, use that
  8179. if ((req.query.lang == null) && (req.session != null) && (req.session.userid)) {
  8180. var user = obj.users[req.session.userid];
  8181. if ((user != null) && (user.lang != null)) { req.query.lang = user.lang; }
  8182. };
  8183. // Get a list of acceptable languages in order
  8184. var acceptLanguages = [];
  8185. if (req.query.lang != null) {
  8186. acceptLanguages.push(req.query.lang.toLowerCase());
  8187. } else {
  8188. if (req.headers['accept-language'] != null) {
  8189. var acceptLanguageSplit = req.headers['accept-language'].split(';');
  8190. for (var i in acceptLanguageSplit) {
  8191. var acceptLanguageSplitEx = acceptLanguageSplit[i].split(',');
  8192. for (var j in acceptLanguageSplitEx) { if (acceptLanguageSplitEx[j].startsWith('q=') == false) { acceptLanguages.push(acceptLanguageSplitEx[j].toLowerCase()); } }
  8193. }
  8194. }
  8195. }
  8196. return acceptLanguages;
  8197. }
  8198. // Render a page using the proper language
  8199. function render(req, res, filename, args, user) {
  8200. if (obj.renderPages != null) {
  8201. // Get the list of acceptable languages in order
  8202. var acceptLanguages = obj.getLanguageCodes(req);
  8203. var domain = getDomain(req);
  8204. // Take a look at the options we have for this file
  8205. var fileOptions = obj.renderPages[domain.id][obj.path.basename(filename)];
  8206. if (fileOptions != null) {
  8207. for (var i in acceptLanguages) {
  8208. if ((acceptLanguages[i] == 'en') || (acceptLanguages[i].startsWith('en-'))) {
  8209. // English requested
  8210. args.lang = 'en';
  8211. if (user && user.llang) { delete user.llang; obj.db.SetUser(user); } // Clear user 'last language' used if needed. Since English is the default, remove "last language".
  8212. break;
  8213. }
  8214. // See if a language (like "fr-ca") or short-language (like "fr") matches an available translation file.
  8215. var foundLanguage = null;
  8216. if (fileOptions[acceptLanguages[i]] != null) { foundLanguage = acceptLanguages[i]; } else {
  8217. const ptr = acceptLanguages[i].indexOf('-');
  8218. if (ptr >= 0) {
  8219. const shortAcceptedLanguage = acceptLanguages[i].substring(0, ptr);
  8220. if (fileOptions[shortAcceptedLanguage] != null) { foundLanguage = shortAcceptedLanguage; }
  8221. }
  8222. }
  8223. // If a language is found, render it.
  8224. if (foundLanguage != null) {
  8225. // Found a match. If the file no longer exists, default to English.
  8226. obj.fs.exists(fileOptions[foundLanguage] + '.handlebars', function (exists) {
  8227. if (exists) { args.lang = foundLanguage; res.render(fileOptions[foundLanguage], args); } else { args.lang = 'en'; res.render(filename, args); }
  8228. });
  8229. if (user && (user.llang != foundLanguage)) { user.llang = foundLanguage; obj.db.SetUser(user); } // Set user 'last language' used if needed.
  8230. return;
  8231. }
  8232. }
  8233. }
  8234. }
  8235. // No matches found, render the default English page.
  8236. res.render(filename, args);
  8237. }
  8238. // Get the list of pages with different languages that can be rendered
  8239. function getRenderList() {
  8240. // Fetch default rendeing pages
  8241. var translateFolder = null;
  8242. if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
  8243. if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
  8244. if (translateFolder != null) {
  8245. obj.renderPages = {};
  8246. obj.renderLanguages = ['en'];
  8247. for (var i in parent.config.domains) {
  8248. if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
  8249. if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
  8250. var files = obj.fs.readdirSync(translateFolder);
  8251. var domain = parent.config.domains[i].id;
  8252. obj.renderPages[domain] = {};
  8253. for (var i in files) {
  8254. var name = files[i];
  8255. if (name.endsWith('.handlebars')) {
  8256. name = name.substring(0, name.length - 11);
  8257. var xname = name.split('_');
  8258. if (xname.length == 2) {
  8259. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8260. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8261. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8262. }
  8263. }
  8264. }
  8265. // See if there are any custom rending pages that will override the default ones
  8266. if ((obj.parent.webViewsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webViewsOverridePath, 'translations')))) {
  8267. translateFolder = obj.path.join(obj.parent.webViewsOverridePath, 'translations');
  8268. var files = obj.fs.readdirSync(translateFolder);
  8269. for (var i in files) {
  8270. var name = files[i];
  8271. if (name.endsWith('.handlebars')) {
  8272. name = name.substring(0, name.length - 11);
  8273. var xname = name.split('_');
  8274. if (xname.length == 2) {
  8275. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8276. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8277. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8278. }
  8279. }
  8280. }
  8281. }
  8282. // See if there is a custom meshcentral-web-domain folder as that will override the default ones
  8283. if (obj.fs.existsSync(obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations'))) {
  8284. translateFolder = obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations');
  8285. var files = obj.fs.readdirSync(translateFolder);
  8286. for (var i in files) {
  8287. var name = files[i];
  8288. if (name.endsWith('.handlebars')) {
  8289. name = name.substring(0, name.length - 11);
  8290. var xname = name.split('_');
  8291. if (xname.length == 2) {
  8292. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8293. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8294. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8295. }
  8296. }
  8297. }
  8298. }
  8299. }
  8300. }
  8301. }
  8302. // Get the list of pages with different languages that can be rendered
  8303. function getEmailLanguageList() {
  8304. // Fetch default rendeing pages
  8305. var translateFolder = null;
  8306. if (obj.fs.existsSync('emails/translations')) { translateFolder = 'emails/translations'; }
  8307. if (obj.fs.existsSync(obj.path.join(__dirname, 'emails', 'translations'))) { translateFolder = obj.path.join(__dirname, 'emails', 'translations'); }
  8308. if (translateFolder != null) {
  8309. obj.emailLanguages = ['en'];
  8310. var files = obj.fs.readdirSync(translateFolder);
  8311. for (var i in files) {
  8312. var name = files[i];
  8313. if (name.endsWith('.html')) {
  8314. name = name.substring(0, name.length - 5);
  8315. var xname = name.split('_');
  8316. if (xname.length == 2) {
  8317. if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
  8318. }
  8319. }
  8320. }
  8321. // See if there are any custom rending pages that will override the default ones
  8322. if ((obj.parent.webEmailsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webEmailsOverridePath, 'translations')))) {
  8323. translateFolder = obj.path.join(obj.parent.webEmailsOverridePath, 'translations');
  8324. var files = obj.fs.readdirSync(translateFolder);
  8325. for (var i in files) {
  8326. var name = files[i];
  8327. if (name.endsWith('.html')) {
  8328. name = name.substring(0, name.length - 5);
  8329. var xname = name.split('_');
  8330. if (xname.length == 2) {
  8331. if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
  8332. }
  8333. }
  8334. }
  8335. }
  8336. }
  8337. }
  8338. // Perform a web push to a user
  8339. // If any of the push fail, remove the subscription from the user's webpush subscription list.
  8340. obj.performWebPush = function (domain, user, payload, options) {
  8341. if ((parent.webpush == null) || (Array.isArray(user.webpush) == false) || (user.webpush.length == 0)) return;
  8342. var completionFunc = function pushCompletionFunc(sub, fail) {
  8343. pushCompletionFunc.failCount += fail;
  8344. if (--pushCompletionFunc.pushCount == 0) {
  8345. if (pushCompletionFunc.failCount > 0) {
  8346. var user = pushCompletionFunc.user, newwebpush = [];
  8347. for (var i in user.webpush) { if (user.webpush[i].fail == null) { newwebpush.push(user.webpush[i]); } }
  8348. user.webpush = newwebpush;
  8349. // Update the database
  8350. obj.db.SetUser(user);
  8351. // Event the change
  8352. var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
  8353. if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  8354. var targets = ['*', 'server-users', user._id];
  8355. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  8356. parent.DispatchEvent(targets, obj, message);
  8357. }
  8358. }
  8359. }
  8360. completionFunc.pushCount = user.webpush.length;
  8361. completionFunc.user = user;
  8362. completionFunc.domain = domain;
  8363. completionFunc.failCount = 0;
  8364. for (var i in user.webpush) {
  8365. var errorFunc = function pushErrorFunc(error) { pushErrorFunc.sub.fail = 1; pushErrorFunc.call(pushErrorFunc.sub, 1); }
  8366. errorFunc.sub = user.webpush[i];
  8367. errorFunc.call = completionFunc;
  8368. var successFunc = function pushSuccessFunc(value) { pushSuccessFunc.call(pushSuccessFunc.sub, 0); }
  8369. successFunc.sub = user.webpush[i];
  8370. successFunc.call = completionFunc;
  8371. parent.webpush.sendNotification(user.webpush[i], JSON.stringify(payload), options).then(successFunc, errorFunc);
  8372. }
  8373. }
  8374. // Ensure exclusivity of a push messaging token for Android device
  8375. obj.removePmtFromAllOtherNodes = function (node) {
  8376. if (typeof node.pmt != 'string') return;
  8377. db.Get('pmt_' + node.pmt, function (err, docs) {
  8378. if ((err == null) && (docs.length == 1)) {
  8379. var oldNodeId = docs[0].nodeid;
  8380. db.Get(oldNodeId, function (nerr, ndocs) {
  8381. if ((nerr == null) && (ndocs.length == 1)) {
  8382. var oldNode = ndocs[0];
  8383. if (oldNode.pmt == node.pmt) {
  8384. // Remove the push messaging token and save the node.
  8385. delete oldNode.pmt;
  8386. db.Set(oldNode);
  8387. // Event the node change
  8388. var event = { etype: 'node', action: 'changenode', nodeid: oldNode._id, domain: oldNode.domain, node: obj.CloneSafeNode(oldNode) }
  8389. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
  8390. parent.DispatchEvent(['*', oldNode.meshid, oldNode._id], obj, event);
  8391. }
  8392. }
  8393. });
  8394. }
  8395. db.Set({ _id: 'pmt_' + node.pmt, type: 'pmt', domain: node.domain, time: Date.now(), nodeid: node._id })
  8396. });
  8397. }
  8398. // Return true if a mobile browser is detected.
  8399. // This code comes from "http://detectmobilebrowsers.com/" and was modified, This is free and unencumbered software released into the public domain. For more information, please refer to the http://unlicense.org/
  8400. function isMobileBrowser(req) {
  8401. //var ua = req.headers['user-agent'].toLowerCase();
  8402. //return (/(android|bb\d+|meego).+mobile|mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(ua) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(ua.substr(0, 4)));
  8403. if (typeof req.headers['user-agent'] != 'string') return false;
  8404. return (req.headers['user-agent'].toLowerCase().indexOf('mobile') >= 0);
  8405. }
  8406. // Return decoded user agent information
  8407. obj.getUserAgentInfo = function (req) {
  8408. var browser = 'Unknown', os = 'Unknown';
  8409. try {
  8410. const ua = obj.uaparser((typeof req == 'string') ? req : req.headers['user-agent']);
  8411. if (ua.browser && ua.browser.name) { ua.browserStr = ua.browser.name; if (ua.browser.version) { ua.browserStr += '/' + ua.browser.version } }
  8412. if (ua.os && ua.os.name) { ua.osStr = ua.os.name; if (ua.os.version) { ua.osStr += '/' + ua.os.version } }
  8413. return ua;
  8414. } catch (ex) { return { browserStr: browser, osStr: os } }
  8415. }
  8416. // Return the query string portion of the URL, the ? and anything after.
  8417. function getQueryPortion(req) { var s = req.url.indexOf('?'); if (s == -1) { if (req.body && req.body.urlargs) { return req.body.urlargs; } return ''; } return req.url.substring(s); }
  8418. // Generate a random Intel AMT password
  8419. function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
  8420. function getRandomAmtPassword() { var p; do { p = Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
  8421. function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
  8422. function getRandomLowerCase(len) { var r = '', random = obj.crypto.randomBytes(len); for (var i = 0; i < len; i++) { r += String.fromCharCode(97 + (random[i] % 26)); } return r; }
  8423. // Generate a 8 digit integer with even random probability for each value.
  8424. function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
  8425. function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
  8426. // Clean a IPv6 address that encodes a IPv4 address
  8427. function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
  8428. // Set the content disposition header for a HTTP response.
  8429. // Because the filename can't have any special characters in it, we need to be extra careful.
  8430. function setContentDispositionHeader(res, type, name, size, altname) {
  8431. var name = require('path').basename(name).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split('\'').join('');
  8432. try {
  8433. var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(name) + '"' };
  8434. if (typeof size == 'number') { x['Content-Length'] = size; }
  8435. res.set(x);
  8436. } catch (ex) {
  8437. var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
  8438. if (typeof size == 'number') { x['Content-Length'] = size; }
  8439. res.set(x);
  8440. }
  8441. }
  8442. // Record a new entry in a recording log
  8443. function recordingEntry(fd, type, flags, data, func, tag) {
  8444. try {
  8445. if (typeof data == 'string') {
  8446. // String write
  8447. var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
  8448. header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
  8449. header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User)
  8450. header.writeInt32BE(blockData.length, 4); // Size
  8451. header.writeIntBE(new Date(), 10, 6); // Time
  8452. var block = Buffer.concat([header, blockData]);
  8453. obj.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
  8454. } else {
  8455. // Binary write
  8456. var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
  8457. header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
  8458. header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User)
  8459. header.writeInt32BE(data.length, 4); // Size
  8460. header.writeIntBE(new Date(), 10, 6); // Time
  8461. var block = Buffer.concat([header, data]);
  8462. obj.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
  8463. }
  8464. } catch (ex) { console.log(ex); func(fd, tag); }
  8465. }
  8466. // Perform a IP match against a list
  8467. function isIPMatch(ip, matchList) {
  8468. const ipcheck = require('ipcheck');
  8469. for (var i in matchList) { if (ipcheck.match(ip, matchList[i]) == true) return true; }
  8470. return false;
  8471. }
  8472. // This is the invalid login throttling code
  8473. obj.badLoginTable = {};
  8474. obj.badLoginTableLastClean = 0;
  8475. if (parent.config.settings == null) { parent.config.settings = {}; }
  8476. if (parent.config.settings.maxinvalidlogin !== false) {
  8477. if (typeof parent.config.settings.maxinvalidlogin != 'object') { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
  8478. if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
  8479. if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
  8480. if ((typeof parent.config.settings.maxinvalidlogin.coolofftime != 'number') || (parent.config.settings.maxinvalidlogin.coolofftime < 1)) { parent.config.settings.maxinvalidlogin.coolofftime = null; }
  8481. }
  8482. obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
  8483. if (parent.config.settings.maxinvalidlogin === false) return;
  8484. if (typeof ip == 'object') { ip = ip.clientIp; }
  8485. if (parent.config.settings.maxinvalidlogin != null) {
  8486. if (typeof parent.config.settings.maxinvalidlogin.exclude == 'string') {
  8487. const excludeSplit = parent.config.settings.maxinvalidlogin.exclude.split(',');
  8488. for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
  8489. } else if (Array.isArray(parent.config.settings.maxinvalidlogin.exclude)) {
  8490. for (var i in parent.config.settings.maxinvalidlogin.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalidlogin.exclude[i])) return; }
  8491. }
  8492. }
  8493. var splitip = ip.split('.');
  8494. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
  8495. if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
  8496. if (typeof obj.badLoginTable[ip] == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return; } } // Check cooloff period
  8497. if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
  8498. if ((obj.badLoginTable[ip].length >= parent.config.settings.maxinvalidlogin.count) && (parent.config.settings.maxinvalidlogin.coolofftime != null)) {
  8499. obj.badLoginTable[ip] = Date.now() + (parent.config.settings.maxinvalidlogin.coolofftime * 60000); // Move to cooloff period
  8500. }
  8501. }
  8502. obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
  8503. if (parent.config.settings.maxinvalidlogin === false) return true;
  8504. if (typeof ip == 'object') { ip = ip.clientIp; }
  8505. var splitip = ip.split('.');
  8506. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
  8507. var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
  8508. var ipTable = obj.badLoginTable[ip];
  8509. if (ipTable == null) return true;
  8510. if (typeof ipTable == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return false; } } // Check cooloff period
  8511. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  8512. if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
  8513. return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
  8514. }
  8515. obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
  8516. if (parent.config.settings.maxinvalidlogin === false) return;
  8517. var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
  8518. for (var ip in obj.badLoginTable) {
  8519. var ipTable = obj.badLoginTable[ip];
  8520. if (typeof ipTable == 'number') {
  8521. if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } // Check cooloff period
  8522. } else {
  8523. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  8524. if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
  8525. }
  8526. }
  8527. obj.badLoginTableLastClean = 0;
  8528. }
  8529. // This is the invalid 2FA throttling code
  8530. obj.bad2faTable = {};
  8531. obj.bad2faTableLastClean = 0;
  8532. if (parent.config.settings == null) { parent.config.settings = {}; }
  8533. if (parent.config.settings.maxinvalid2fa !== false) {
  8534. if (typeof parent.config.settings.maxinvalid2fa != 'object') { parent.config.settings.maxinvalid2fa = { time: 10, count: 10 }; }
  8535. if (typeof parent.config.settings.maxinvalid2fa.time != 'number') { parent.config.settings.maxinvalid2fa.time = 10; }
  8536. if (typeof parent.config.settings.maxinvalid2fa.count != 'number') { parent.config.settings.maxinvalid2fa.count = 10; }
  8537. if ((typeof parent.config.settings.maxinvalid2fa.coolofftime != 'number') || (parent.config.settings.maxinvalid2fa.coolofftime < 1)) { parent.config.settings.maxinvalid2fa.coolofftime = null; }
  8538. }
  8539. obj.setbad2Fa = function (ip) { // Set an IP address that just did a bad 2FA request
  8540. if (parent.config.settings.maxinvalid2fa === false) return;
  8541. if (typeof ip == 'object') { ip = ip.clientIp; }
  8542. if (parent.config.settings.maxinvalid2fa != null) {
  8543. if (typeof parent.config.settings.maxinvalid2fa.exclude == 'string') {
  8544. const excludeSplit = parent.config.settings.maxinvalid2fa.exclude.split(',');
  8545. for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
  8546. } else if (Array.isArray(parent.config.settings.maxinvalid2fa.exclude)) {
  8547. for (var i in parent.config.settings.maxinvalid2fa.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalid2fa.exclude[i])) return; }
  8548. }
  8549. }
  8550. var splitip = ip.split('.');
  8551. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
  8552. if (++obj.bad2faTableLastClean > 100) { obj.cleanBad2faTable(); }
  8553. if (typeof obj.bad2faTable[ip] == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return; } } // Check cooloff period
  8554. if (obj.bad2faTable[ip] == null) { obj.bad2faTable[ip] = [Date.now()]; } else { obj.bad2faTable[ip].push(Date.now()); }
  8555. if ((obj.bad2faTable[ip].length >= parent.config.settings.maxinvalid2fa.count) && (parent.config.settings.maxinvalid2fa.coolofftime != null)) {
  8556. obj.bad2faTable[ip] = Date.now() + (parent.config.settings.maxinvalid2fa.coolofftime * 60000); // Move to cooloff period
  8557. }
  8558. }
  8559. obj.checkAllow2Fa = function (ip) { // Check if an IP address is allowed to perform 2FA
  8560. if (parent.config.settings.maxinvalid2fa === false) return true;
  8561. if (typeof ip == 'object') { ip = ip.clientIp; }
  8562. var splitip = ip.split('.');
  8563. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
  8564. var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
  8565. var ipTable = obj.bad2faTable[ip];
  8566. if (ipTable == null) return true;
  8567. if (typeof ipTable == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return false; } } // Check cooloff period
  8568. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  8569. if (ipTable.length == 0) { delete obj.bad2faTable[ip]; return true; }
  8570. return (ipTable.length < parent.config.settings.maxinvalid2fa.count); // No more than x bad 2FAs in x minutes
  8571. }
  8572. obj.cleanBad2faTable = function () { // Clean up the IP address 2FA blockage table, we do this occasionaly.
  8573. if (parent.config.settings.maxinvalid2fa === false) return;
  8574. var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
  8575. for (var ip in obj.bad2faTable) {
  8576. var ipTable = obj.bad2faTable[ip];
  8577. if (typeof ipTable == 'number') {
  8578. if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } // Check cooloff period
  8579. } else {
  8580. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  8581. if (ipTable.length == 0) { delete obj.bad2faTable[ip]; }
  8582. }
  8583. }
  8584. obj.bad2faTableLastClean = 0;
  8585. }
  8586. // Hold a websocket until additional arguments are provided within the socket.
  8587. // This is a generic function that can be used for any websocket to avoid passing arguments in the URL.
  8588. function getWebsocketArgs(ws, req, func) {
  8589. if (req.query.moreargs != '1') {
  8590. // No more arguments needed, pass the websocket thru
  8591. func(ws, req);
  8592. } else {
  8593. // More arguments are needed
  8594. delete req.query.moreargs;
  8595. const xfunc = function getWebsocketArgsEx(msg) {
  8596. var command = null;
  8597. try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
  8598. if ((command != null) && (command.action === 'urlargs') && (typeof command.args == 'object')) {
  8599. for (var i in command.args) { getWebsocketArgsEx.req.query[i] = command.args[i]; }
  8600. ws.removeEventListener('message', getWebsocketArgsEx);
  8601. getWebsocketArgsEx.func(getWebsocketArgsEx.ws, getWebsocketArgsEx.req);
  8602. }
  8603. }
  8604. xfunc.ws = ws;
  8605. xfunc.req = req;
  8606. xfunc.func = func;
  8607. ws.on('message', xfunc);
  8608. }
  8609. }
  8610. // Set a random value to this session. Only works if the session has a userid.
  8611. // This random value along with the userid is used to destroy the session when logging out.
  8612. function setSessionRandom(req) {
  8613. if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return;
  8614. var x = obj.crypto.randomBytes(6).toString('base64');
  8615. while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); }
  8616. req.session.x = x;
  8617. }
  8618. // Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway.
  8619. function clearDestroyedSessions() {
  8620. var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000);
  8621. for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } }
  8622. for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; }
  8623. }
  8624. // Check and/or convert the agent color value into a correct string or return empty string.
  8625. function checkAgentColorString(header, value) {
  8626. if ((typeof header !== 'string') || (typeof value !== 'string')) return '';
  8627. if (value.startsWith('#') && (value.length == 7)) {
  8628. // Convert color in hex format
  8629. value = parseInt(value.substring(1, 3), 16) + ',' + parseInt(value.substring(3, 5), 16) + ',' + parseInt(value.substring(5, 7), 16);
  8630. } else {
  8631. // Check color in decimal format
  8632. const valueSplit = value.split(',');
  8633. if (valueSplit.length != 3) return '';
  8634. const r = parseInt(valueSplit[0]), g = parseInt(valueSplit[1]), b = parseInt(valueSplit[2]);
  8635. if (isNaN(r) || (r < 0) || (r > 255) || isNaN(g) || (g < 0) || (g > 255) || isNaN(b) || (b < 0) || (b > 255)) return '';
  8636. value = r + ',' + g + ',' + b;
  8637. }
  8638. return header + value + '\r\n';
  8639. }
  8640. // Check that everything is cleaned up
  8641. function checkWebRelaySessionsTimeout() {
  8642. for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
  8643. }
  8644. // Return true if this is a private IP address
  8645. function isPrivateAddress(ip_addr) {
  8646. // If this is a loopback address, return true
  8647. if ((ip_addr == '127.0.0.1') || (ip_addr == '::1')) return true;
  8648. // Check IPv4 private addresses
  8649. const ipcheck = require('ipcheck');
  8650. const IPv4PrivateRanges = ['0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24', '192.0.0.0/29', '192.0.0.8/32', '192.0.0.9/32', '192.0.0.10/32', '192.0.0.170/32', '192.0.0.171/32', '192.0.2.0/24', '192.31.196.0/24', '192.52.193.0/24', '192.88.99.0/24', '192.168.0.0/16', '192.175.48.0/24', '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24', '240.0.0.0/4', '255.255.255.255/32']
  8651. for (var i in IPv4PrivateRanges) { if (ipcheck.match(ip_addr, IPv4PrivateRanges[i])) return true; }
  8652. // Check IPv6 private addresses
  8653. return /^::$/.test(ip_addr) ||
  8654. /^::1$/.test(ip_addr) ||
  8655. /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  8656. /^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  8657. /^64:ff9b::([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  8658. /^100::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
  8659. /^2001::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
  8660. /^2001:2[0-9a-fA-F]:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
  8661. /^2001:db8:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
  8662. /^2002:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
  8663. /^f[c-d]([0-9a-fA-F]{2,2}):/i.test(ip_addr) ||
  8664. /^fe[8-9a-bA-B][0-9a-fA-F]:/i.test(ip_addr) ||
  8665. /^ff([0-9a-fA-F]{2,2}):/i.test(ip_addr)
  8666. }
  8667. // Check that a cookie IP is within the correct range depending on the active policy
  8668. function checkCookieIp(cookieip, ip) {
  8669. if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking
  8670. if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers.
  8671. if (require('ipcheck').match(cookieip, ip + '/24')) return true; // 'lax' - IP address need to be in the some range
  8672. return (isPrivateAddress(cookieip) && isPrivateAddress(ip)); // 'lax' - If both IP addresses are private or loopback, accept it. This is needed because sometimes browsers will resolve IP addresses oddly on private networks.
  8673. }
  8674. // Takes a formating string like "this {{{a}}} is an {{{b}}} example" and fills the a and b with input o.a and o.b
  8675. function assembleStringFromObject(format, o) {
  8676. var r = '', i = format.indexOf('{{{');
  8677. if (i > 0) { r = format.substring(0, i); format = format.substring(i); }
  8678. const cmd = format.split('{{{');
  8679. for (var j in cmd) { if (j == 0) continue; i = cmd[j].indexOf('}}}'); r += o[cmd[j].substring(0, i)] + cmd[j].substring(i + 3); }
  8680. return r;
  8681. }
  8682. // Sync an account with an external user group.
  8683. // Return true if the user was changed
  8684. function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) {
  8685. var userChanged = false;
  8686. if (user.links == null) { user.links = {}; }
  8687. // Create a user of memberships for this user that type
  8688. var existingUserMemberships = {};
  8689. for (var i in user.links) {
  8690. if (i.startsWith('ugrp/') && (obj.userGroups[i] != null) && (obj.userGroups[i].membershipType == userMembershipType)) { existingUserMemberships[i] = obj.userGroups[i]; }
  8691. }
  8692. // Go thru the list user memberships and create and add to any user groups as needed
  8693. for (var i in userMemberships) {
  8694. const membership = userMemberships[i];
  8695. var ugrpid = 'ugrp/' + domain.id + '/' + obj.crypto.createHash('sha384').update(membership).digest('base64').replace(/\+/g, '@').replace(/\//g, '$');
  8696. var ugrp = obj.userGroups[ugrpid];
  8697. if (ugrp == null) {
  8698. // This user group does not exist, create it
  8699. ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} };
  8700. // Save the new group
  8701. db.Set(ugrp);
  8702. if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; }
  8703. // Event the user group creation
  8704. var event = { etype: 'ugrp', ugrpid: ugrpid, name: ugrp.name, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id };
  8705. parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
  8706. // Log in the auth log
  8707. parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
  8708. }
  8709. if (existingUserMemberships[ugrpid] == null) {
  8710. // This user is not part of the user group, add it.
  8711. if (user.links == null) { user.links = {}; }
  8712. user.links[ugrp._id] = { rights: 1 };
  8713. userChanged = true;
  8714. db.SetUser(user);
  8715. parent.DispatchEvent([user._id], obj, 'resubscribe');
  8716. // Notify user change
  8717. var targets = ['*', 'server-users', user._id];
  8718. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
  8719. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  8720. parent.DispatchEvent(targets, obj, event);
  8721. // Add a user to the user group
  8722. ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  8723. db.Set(ugrp);
  8724. // Notify user group change
  8725. var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 71, msgArgs: [user.name, ugrp.name], msg: 'Added user(s) ' + user.name + ' to user group ' + ugrp.name, addUserDomain: domain.id };
  8726. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  8727. parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
  8728. // Log in the auth log
  8729. parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
  8730. } else {
  8731. // User is already part of this user group
  8732. delete existingUserMemberships[ugrpid];
  8733. }
  8734. }
  8735. // Remove the user from any memberships they don't belong to anymore
  8736. for (var ugrpid in existingUserMemberships) {
  8737. var ugrp = obj.userGroups[ugrpid];
  8738. parent.authLog('https', userMembershipType.toUpperCase() + ': Removing ' + user.name + ' from user group ' + ugrp.name + '.');
  8739. if ((user.links != null) && (user.links[ugrpid] != null)) {
  8740. delete user.links[ugrpid];
  8741. // Notify user change
  8742. var targets = ['*', 'server-users', user._id, user._id];
  8743. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
  8744. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  8745. parent.DispatchEvent(targets, obj, event);
  8746. db.SetUser(user);
  8747. parent.DispatchEvent([user._id], obj, 'resubscribe');
  8748. }
  8749. if (ugrp != null) {
  8750. // Remove the user from the group
  8751. if ((ugrp.links != null) && (ugrp.links[user._id] != null)) {
  8752. delete ugrp.links[user._id];
  8753. db.Set(ugrp);
  8754. // Notify user group change
  8755. var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 72, msgArgs: [user.name, ugrp.name], msg: 'Removed user ' + user.name + ' from user group ' + ugrp.name, domain: domain.id };
  8756. if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
  8757. parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
  8758. }
  8759. }
  8760. }
  8761. return userChanged;
  8762. }
  8763. return obj;
  8764. };