webserver.js 706 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357
  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. obj.uaclienthints = require('ua-client-hints-js');
  50. const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
  51. // Setup WebAuthn / FIDO2
  52. obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
  53. if (process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) {
  54. obj.httpsProxyAgent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
  55. }
  56. // Variables
  57. obj.args = args;
  58. obj.parent = parent;
  59. obj.filespath = parent.filespath;
  60. obj.db = db;
  61. obj.app = obj.express();
  62. if (obj.args.agentport) { obj.agentapp = obj.express(); }
  63. if (args.compression === true) {
  64. obj.app.use(require('compression')({ filter: function (req, res) {
  65. if (req.path == '/devicefile.ashx') return false; // Don't compress device file transfers to show file sizes
  66. if ((args.relaydns != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) return false; // Don't compress DNS relay requests
  67. return require('compression').filter(req, res);
  68. }}));
  69. }
  70. obj.app.disable('x-powered-by');
  71. obj.tlsServer = null;
  72. obj.tcpServer = null;
  73. obj.certificates = certificates;
  74. obj.users = {}; // UserID --> User
  75. obj.meshes = {}; // MeshID --> Mesh (also called device group)
  76. obj.userGroups = {}; // UGrpID --> User Group
  77. obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node
  78. obj.tlsCiphers = args.tlsciphers; // List of TLS ciphers to use
  79. obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
  80. obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
  81. obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
  82. obj.tlsSniCredentials = null;
  83. obj.dnsDomains = {};
  84. obj.relaySessionCount = 0;
  85. obj.relaySessionErrorCount = 0;
  86. obj.blockedUsers = 0;
  87. obj.blockedAgents = 0;
  88. obj.renderPages = null;
  89. obj.renderLanguages = [];
  90. obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
  91. // Web relay sessions
  92. var webRelayNextSessionId = 1;
  93. var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
  94. var webRelayCleanupTimer = null;
  95. // Monitor web relay session removals
  96. parent.AddEventDispatch(['server-shareremove'], obj);
  97. obj.HandleEvent = function (source, event, ids, id) {
  98. if (event.action == 'removedDeviceShare') {
  99. for (var relaySessionId in webRelaySessions) {
  100. // A share was removed that matches an active session, close the web relay session.
  101. if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); }
  102. }
  103. }
  104. }
  105. // Mesh Rights
  106. const MESHRIGHT_EDITMESH = 0x00000001;
  107. const MESHRIGHT_MANAGEUSERS = 0x00000002;
  108. const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
  109. const MESHRIGHT_REMOTECONTROL = 0x00000008;
  110. const MESHRIGHT_AGENTCONSOLE = 0x00000010;
  111. const MESHRIGHT_SERVERFILES = 0x00000020;
  112. const MESHRIGHT_WAKEDEVICE = 0x00000040;
  113. const MESHRIGHT_SETNOTES = 0x00000080;
  114. const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
  115. const MESHRIGHT_NOTERMINAL = 0x00000200;
  116. const MESHRIGHT_NOFILES = 0x00000400;
  117. const MESHRIGHT_NOAMT = 0x00000800;
  118. const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
  119. const MESHRIGHT_LIMITEVENTS = 0x00002000;
  120. const MESHRIGHT_CHATNOTIFY = 0x00004000;
  121. const MESHRIGHT_UNINSTALL = 0x00008000;
  122. const MESHRIGHT_NODESKTOP = 0x00010000;
  123. const MESHRIGHT_REMOTECOMMAND = 0x00020000;
  124. const MESHRIGHT_RESETOFF = 0x00040000;
  125. const MESHRIGHT_GUESTSHARING = 0x00080000;
  126. const MESHRIGHT_ADMIN = 0xFFFFFFFF;
  127. // Site rights
  128. const SITERIGHT_SERVERBACKUP = 0x00000001;
  129. const SITERIGHT_MANAGEUSERS = 0x00000002;
  130. const SITERIGHT_SERVERRESTORE = 0x00000004;
  131. const SITERIGHT_FILEACCESS = 0x00000008;
  132. const SITERIGHT_SERVERUPDATE = 0x00000010;
  133. const SITERIGHT_LOCKED = 0x00000020;
  134. const SITERIGHT_NONEWGROUPS = 0x00000040;
  135. const SITERIGHT_NOMESHCMD = 0x00000080;
  136. const SITERIGHT_USERGROUPS = 0x00000100;
  137. const SITERIGHT_RECORDINGS = 0x00000200;
  138. const SITERIGHT_LOCKSETTINGS = 0x00000400;
  139. const SITERIGHT_ALLEVENTS = 0x00000800;
  140. const SITERIGHT_NONEWDEVICES = 0x00001000;
  141. const SITERIGHT_ADMIN = 0xFFFFFFFF;
  142. // Setup SSPI authentication if needed
  143. if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
  144. 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 }); } }
  145. }
  146. // Perform hash on web certificate and agent certificate
  147. obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
  148. obj.webCertificateHashs = { '': obj.webCertificateHash };
  149. obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
  150. obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
  151. obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
  152. obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) };
  153. obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
  154. obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
  155. obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
  156. obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null;
  157. obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null;
  158. // Compute the hash of all of the web certificates for each domain
  159. for (var i in obj.parent.config.domains) {
  160. if (obj.parent.config.domains[i].certhash != null) {
  161. // If the web certificate hash is provided, use it.
  162. obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
  163. if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
  164. delete obj.webCertificateExpire[i]; // Expire time is not provided
  165. } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
  166. // If the domain has a different DNS name, use a different certificate hash.
  167. // Hash the full certificate
  168. obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
  169. obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
  170. try {
  171. // Decode a RSA certificate and hash the public key.
  172. obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
  173. } catch (ex) {
  174. // This may be a ECDSA certificate, hash the entire cert.
  175. obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
  176. }
  177. } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
  178. // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
  179. obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
  180. obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
  181. obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
  182. } else if (i != '') {
  183. // For any other domain, use the default cert.
  184. obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
  185. obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
  186. obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
  187. }
  188. }
  189. // If we are running the legacy swarm server, compute the hash for that certificate
  190. if (parent.certificates.swarmserver != null) {
  191. obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
  192. 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' });
  193. 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' });
  194. }
  195. // Main lists
  196. obj.wsagents = {}; // NodeId --> Agent
  197. obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
  198. obj.wsagentsDisconnections = {};
  199. obj.wsagentsDisconnectionsTimer = null;
  200. obj.duplicateAgentsLog = {};
  201. obj.wssessions = {}; // UserId --> Array Of Sessions
  202. obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
  203. obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
  204. obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
  205. obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
  206. obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
  207. obj.wsrelays = {}; // Id -> Relay
  208. obj.desktoprelays = {}; // Id -> Desktop Multiplexer Relay
  209. obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
  210. var tlsSessionStore = {}; // Store TLS session information for quick resume.
  211. var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
  212. // Setup randoms
  213. obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
  214. obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
  215. obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
  216. // Get non-english web pages and emails
  217. getRenderList();
  218. getEmailLanguageList();
  219. // Setup DNS domain TLS SNI credentials
  220. {
  221. var dnscount = 0;
  222. obj.tlsSniCredentials = {};
  223. 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++; } }
  224. 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; }
  225. }
  226. function TlsSniCallback(name, cb) {
  227. var c = obj.tlsSniCredentials[name];
  228. if (c != null) {
  229. cb(null, c);
  230. } else {
  231. cb(null, obj.tlsSniCredentials['']);
  232. }
  233. }
  234. 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; }
  235. //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; }
  236. // Fetch all users from the database, keep this in memory
  237. obj.db.GetAllType('user', function (err, docs) {
  238. obj.common.unEscapeAllLinksFieldName(docs);
  239. var domainUserCount = {}, i = 0;
  240. for (i in parent.config.domains) { domainUserCount[i] = 0; }
  241. for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
  242. for (i in parent.config.domains) {
  243. if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
  244. // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation.
  245. //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
  246. console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
  247. }
  248. }
  249. // Fetch all device groups (meshes) from the database, keep this in memory
  250. // As we load things in memory, we will also be doing some cleaning up.
  251. // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
  252. obj.db.GetAllType('mesh', function (err, docs) {
  253. obj.common.unEscapeAllLinksFieldName(docs);
  254. for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
  255. // Fetch all user groups from the database, keep this in memory
  256. obj.db.GetAllType('ugrp', function (err, docs) {
  257. obj.common.unEscapeAllLinksFieldName(docs);
  258. // Perform user group link cleanup
  259. for (var i in docs) {
  260. const ugrp = docs[i];
  261. if (ugrp.links != null) {
  262. for (var j in ugrp.links) {
  263. if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
  264. 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
  265. }
  266. }
  267. obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
  268. }
  269. // Mapping between users and groups
  270. for (var ugrpId in obj.userGroups) {
  271. const ugrp = obj.userGroups[ugrpId];
  272. if (ugrp.links != null) {
  273. for (var userId in ugrp.links) {
  274. if (userId.startsWith('user/') && (obj.users[userId] != null)) {
  275. const user = obj.users[userId];
  276. if (user.links == null) { user.links = {}; }
  277. if (user.links[ugrpId] == null) {
  278. // Adding group link to user
  279. user.links[ugrpId] = { rights: ugrp.links[userId].rights || 1 };
  280. }
  281. }
  282. }
  283. }
  284. }
  285. // Perform device group link cleanup
  286. for (var i in obj.meshes) {
  287. const mesh = obj.meshes[i];
  288. if (mesh.links != null) {
  289. for (var j in mesh.links) {
  290. 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
  291. 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
  292. }
  293. }
  294. }
  295. // Perform user link cleanup
  296. for (var i in obj.users) {
  297. const user = obj.users[i];
  298. if (user.links != null) {
  299. for (var j in user.links) {
  300. if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
  301. 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
  302. //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
  303. }
  304. //if (Object.keys(user.links).length == 0) { delete user.links; }
  305. }
  306. }
  307. // We loaded the users, device groups and user group state, start the server
  308. serverStart();
  309. });
  310. });
  311. });
  312. // Clean up a device, used before saving it in the database
  313. obj.cleanDevice = function (device) {
  314. // Check device links, if a link points to an unknown user, remove it.
  315. if (device.links != null) {
  316. for (var j in device.links) {
  317. if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
  318. delete device.links[j];
  319. if (Object.keys(device.links).length == 0) { delete device.links; }
  320. }
  321. }
  322. }
  323. return device;
  324. }
  325. // Return statistics about this web server
  326. obj.getStats = function () {
  327. return {
  328. users: Object.keys(obj.users).length,
  329. meshes: Object.keys(obj.meshes).length,
  330. dnsDomains: Object.keys(obj.dnsDomains).length,
  331. relaySessionCount: obj.relaySessionCount,
  332. relaySessionErrorCount: obj.relaySessionErrorCount,
  333. wsagents: Object.keys(obj.wsagents).length,
  334. wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
  335. wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
  336. wssessions: Object.keys(obj.wssessions).length,
  337. wssessions2: Object.keys(obj.wssessions2).length,
  338. wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
  339. wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
  340. wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
  341. sessionsCount: Object.keys(obj.sessionsCount).length,
  342. wsrelays: Object.keys(obj.wsrelays).length,
  343. wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
  344. tlsSessionStore: Object.keys(tlsSessionStore).length,
  345. blockedUsers: obj.blockedUsers,
  346. blockedAgents: obj.blockedAgents
  347. };
  348. }
  349. // Agent counters
  350. obj.agentStats = {
  351. createMeshAgentCount: 0,
  352. agentClose: 0,
  353. agentBinaryUpdate: 0,
  354. agentMeshCoreBinaryUpdate: 0,
  355. coreIsStableCount: 0,
  356. verifiedAgentConnectionCount: 0,
  357. clearingCoreCount: 0,
  358. updatingCoreCount: 0,
  359. recoveryCoreIsStableCount: 0,
  360. meshDoesNotExistCount: 0,
  361. invalidPkcsSignatureCount: 0,
  362. invalidRsaSignatureCount: 0,
  363. invalidJsonCount: 0,
  364. unknownAgentActionCount: 0,
  365. agentBadWebCertHashCount: 0,
  366. agentBadSignature1Count: 0,
  367. agentBadSignature2Count: 0,
  368. agentMaxSessionHoldCount: 0,
  369. invalidDomainMeshCount: 0,
  370. invalidMeshTypeCount: 0,
  371. invalidDomainMesh2Count: 0,
  372. invalidMeshType2Count: 0,
  373. duplicateAgentCount: 0,
  374. maxDomainDevicesReached: 0,
  375. agentInTrouble: 0,
  376. agentInBigTrouble: 0
  377. }
  378. obj.getAgentStats = function () { return obj.agentStats; }
  379. // Traffic counters
  380. obj.trafficStats = {
  381. httpRequestCount: 0,
  382. httpWebSocketCount: 0,
  383. httpIn: 0,
  384. httpOut: 0,
  385. relayCount: {},
  386. relayIn: {},
  387. relayOut: {},
  388. localRelayCount: {},
  389. localRelayIn: {},
  390. localRelayOut: {},
  391. AgentCtrlIn: 0,
  392. AgentCtrlOut: 0,
  393. LMSIn: 0,
  394. LMSOut: 0,
  395. CIRAIn: 0,
  396. CIRAOut: 0
  397. }
  398. obj.trafficStats.time = Date.now();
  399. obj.getTrafficStats = function () { return obj.trafficStats; }
  400. obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
  401. const data = obj.common.Clone(obj.trafficStats);
  402. data.time = Date.now();
  403. const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
  404. if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
  405. delta.time = data.time;
  406. return { current: data, delta: delta }
  407. }
  408. function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
  409. const r = {};
  410. for (var i in newData) {
  411. if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
  412. if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
  413. }
  414. return r;
  415. }
  416. // Keep a record of the last agent issues.
  417. obj.getAgentIssues = function () { return obj.agentIssues; }
  418. obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
  419. obj.agentIssues = [];
  420. // Authenticate the user
  421. obj.authenticate = function (name, pass, domain, fn) {
  422. if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
  423. if (name.startsWith('~t:')) {
  424. // Login token, try to fetch the token from the database
  425. obj.db.Get('logintoken-' + name, function (err, docs) {
  426. if (err != null) { fn(err); return; }
  427. if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
  428. const loginToken = docs[0];
  429. if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
  430. // Default strong password hashing (pbkdf2 SHA384)
  431. require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
  432. if (err) return fn(err);
  433. if (hash == loginToken.hash) {
  434. // Login username and password are valid.
  435. var user = obj.users[loginToken.userid];
  436. if (!user) { fn(new Error('cannot find user')); return; }
  437. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  438. // Successful login token authentication
  439. var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
  440. if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
  441. return fn(null, user._id, null, loginOptions);
  442. }
  443. fn(new Error('invalid password'));
  444. }, 0);
  445. });
  446. } else if (domain.auth == 'ldap') {
  447. // This method will handle LDAP login
  448. const ldapHandler = function ldapHandlerFunc(err, xxuser) {
  449. 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; }
  450. // Save this LDAP user to file if needed
  451. if (typeof domain.ldapsaveusertofile == 'string') {
  452. obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
  453. }
  454. // Work on getting the userid for this LDAP user
  455. var shortname = null;
  456. var username = xxuser['displayName'];
  457. if (typeof domain.ldapusername == 'string') {
  458. if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; }
  459. } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; }
  460. if (domain.ldapuserbinarykey) {
  461. // Use a binary key as the userid
  462. if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
  463. } else if (domain.ldapuserkey) {
  464. // Use a string key as the userid
  465. if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
  466. } else {
  467. // Use the default key as the userid
  468. if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); }
  469. else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); }
  470. else if (xxuser['name']) { shortname = xxuser['name']; }
  471. else if (xxuser['cn']) { shortname = xxuser['cn']; }
  472. }
  473. if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; }
  474. if (username == null) { username = shortname; }
  475. var userid = 'user/' + domain.id + '/' + shortname;
  476. // Get the list of groups this user is a member of.
  477. var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'];
  478. if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
  479. if (Array.isArray(userMemberships) == false) { userMemberships = []; }
  480. // See if the user is required to be part of an LDAP user group in order to log into this server.
  481. if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
  482. if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
  483. // Look for a matching LDAP user group
  484. var userMembershipMatch = false;
  485. for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
  486. 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
  487. }
  488. // Check if user is in an site administrator group
  489. var siteAdminGroup = null;
  490. if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
  491. if (Array.isArray(domain.ldapsiteadmingroups)) {
  492. siteAdminGroup = false;
  493. for (var i in domain.ldapsiteadmingroups) {
  494. if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; }
  495. }
  496. }
  497. // See if we need to sync LDAP user memberships with user groups
  498. if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; }
  499. if (typeof domain.ldapsyncwithusergroups == 'object') {
  500. // LDAP user memberships sync is enabled, see if there are any filters to apply
  501. if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; }
  502. if (Array.isArray(domain.ldapsyncwithusergroups.filter)) {
  503. const g = [];
  504. for (var i in userMemberships) {
  505. var match = false;
  506. for (var j in domain.ldapsyncwithusergroups.filter) {
  507. if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
  508. }
  509. if (match) { g.push(userMemberships[i]); }
  510. }
  511. userMemberships = g;
  512. }
  513. } else {
  514. // LDAP user memberships sync is disabled, sync the user with empty membership
  515. userMemberships = [];
  516. }
  517. // Get the email address for this LDAP user
  518. var email = null;
  519. if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default
  520. if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value.
  521. if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent.
  522. // Get the real name for this LDAP user
  523. var realname = null;
  524. if (typeof domain.ldapuserrealname == 'string') {
  525. if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
  526. }
  527. else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }
  528. // Get the phone number for this LDAP user
  529. var phonenumber = null;
  530. if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; }
  531. else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } }
  532. // Work on getting the image of this LDAP user
  533. var userimage = null, userImageBuffer = null;
  534. if (xxuser._raw) { // Using _raw allows us to get data directly as buffer.
  535. if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; }
  536. else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; }
  537. else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; }
  538. if (userImageBuffer != null) {
  539. if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); }
  540. if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); }
  541. }
  542. }
  543. // Display user information extracted from LDAP data
  544. parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
  545. // If there is a testing userid, use that
  546. if (ldapHandlerFunc.ldapShortName) {
  547. shortname = ldapHandlerFunc.ldapShortName;
  548. userid = 'user/' + domain.id + '/' + shortname;
  549. }
  550. // Save the user image
  551. if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }
  552. // Close the LDAP object
  553. if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }
  554. // Check if the user already exists
  555. var user = obj.users[userid];
  556. if (user == null) {
  557. // This user does not exist, create a new account.
  558. 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 };
  559. if (email) { user['email'] = email; user['emailVerified'] = true; }
  560. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
  561. if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
  562. var usercount = 0;
  563. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
  564. if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
  565. // Auto-join any user groups
  566. if (typeof domain.newaccountsusergroups == 'object') {
  567. for (var i in domain.newaccountsusergroups) {
  568. var ugrpid = domain.newaccountsusergroups[i];
  569. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  570. var ugroup = obj.userGroups[ugrpid];
  571. if (ugroup != null) {
  572. // Add group to the user
  573. if (user.links == null) { user.links = {}; }
  574. user.links[ugroup._id] = { rights: 1 };
  575. // Add user to the group
  576. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  577. db.Set(ugroup);
  578. // Notify user group change
  579. 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 };
  580. 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.
  581. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  582. }
  583. }
  584. }
  585. // Check the user real name
  586. if (realname) { user.realname = realname; }
  587. // Check the user phone number
  588. if (phonenumber) { user.phone = phonenumber; }
  589. // Indicate that this user has a image
  590. if (userimage != null) { user.flags = 1; }
  591. // See if the user is a member of the site admin group.
  592. if (typeof siteAdminGroup === 'string') {
  593. parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
  594. user.siteadmin = 0xFFFFFFFF;
  595. }
  596. // Sync the user with LDAP matching user groups
  597. if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
  598. obj.users[user._id] = user;
  599. obj.db.SetUser(user);
  600. 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 };
  601. 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.
  602. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  603. return fn(null, user._id);
  604. } else {
  605. var userChanged = false;
  606. // This is an existing user
  607. // If the display username has changes, update it.
  608. if (user.name != username) { user.name = username; userChanged = true; }
  609. // Check if user email has changed
  610. if (user.email && !email) { // email unset in ldap => unset
  611. delete user.email;
  612. delete user.emailVerified;
  613. userChanged = true;
  614. } else if (user.email != email) { // update email
  615. user['email'] = email;
  616. user['emailVerified'] = true;
  617. userChanged = true;
  618. }
  619. // Check the user real name
  620. if (realname != user.realname) { user.realname = realname; userChanged = true; }
  621. // Check the user phone number
  622. if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }
  623. // Check the user image flag
  624. if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; }
  625. if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; }
  626. // See if the user is a member of the site admin group.
  627. if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
  628. parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
  629. user.siteadmin = 0xFFFFFFFF;
  630. userChanged = true;
  631. } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
  632. parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
  633. delete user.siteadmin;
  634. userChanged = true;
  635. }
  636. // Synd the user with LDAP matching user groups
  637. if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
  638. // If the user changed, save the changes to the database here
  639. if (userChanged) {
  640. obj.db.SetUser(user);
  641. 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 };
  642. 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.
  643. parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  644. }
  645. // If user is locker out, block here.
  646. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  647. return fn(null, user._id);
  648. }
  649. }
  650. if (domain.ldapoptions.url == 'test') {
  651. // Test LDAP login
  652. var xxuser = domain.ldapoptions[name.toLowerCase()];
  653. if (xxuser == null) { fn(new Error('invalid password')); return; } else {
  654. ldapHandler.ldapShortName = name.toLowerCase();
  655. if (typeof xxuser == 'string') {
  656. // The test LDAP user points to a JSON file where the user information is, load it.
  657. ldapHandler(null, require(xxuser));
  658. } else {
  659. // The test user information is in the config.json, use it.
  660. ldapHandler(null, xxuser);
  661. }
  662. }
  663. } else {
  664. // LDAP login
  665. var LdapAuth = require('ldapauth-fork');
  666. if (domain.ldapoptions == null) { domain.ldapoptions = {}; }
  667. domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images.
  668. var ldap = new LdapAuth(domain.ldapoptions);
  669. ldapHandler.ldapobj = ldap;
  670. ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object
  671. ldap.authenticate(name, pass, ldapHandler);
  672. }
  673. } else {
  674. // Regular login
  675. var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
  676. // Query the db for the given username
  677. if (!user) { fn(new Error('cannot find user')); return; }
  678. // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
  679. if (user.salt == null) {
  680. fn(new Error('invalid password'));
  681. } else {
  682. if (user.passtype != null) {
  683. // IIS default clear or weak password hashing (SHA-1)
  684. require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
  685. if (err) return fn(err);
  686. if (hash == user.hash) {
  687. // Update the password to the stronger format.
  688. 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);
  689. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  690. return fn(null, user._id);
  691. }
  692. fn(new Error('invalid password'), null, user.passhint);
  693. });
  694. } else {
  695. // Default strong password hashing (pbkdf2 SHA384)
  696. require('./pass').hash(pass, user.salt, function (err, hash, tag) {
  697. if (err) return fn(err);
  698. if (hash == user.hash) {
  699. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
  700. return fn(null, user._id);
  701. }
  702. fn(new Error('invalid password'), null, user.passhint);
  703. }, 0);
  704. }
  705. }
  706. }
  707. };
  708. /*
  709. obj.restrict = function (req, res, next) {
  710. console.log('restrict', req.url);
  711. var domain = getDomain(req);
  712. if (req.session.userid) {
  713. next();
  714. } else {
  715. req.session.messageid = 111; // Access denied.
  716. res.redirect(domain.url + 'login');
  717. }
  718. };
  719. */
  720. // Check if the source IP address is in the IP list, return false if not.
  721. function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
  722. try {
  723. if (req.connection) {
  724. // HTTP(S) request
  725. 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; } } }
  726. if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
  727. } else {
  728. // WebSocket request
  729. 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; } } }
  730. if (closeIfThis === false) { try { req.close(); } catch (e) { } }
  731. }
  732. } catch (e) { console.log(e); } // Should never happen
  733. return false;
  734. }
  735. // Check if the source IP address is allowed, return domain if allowed
  736. // If there is a fail and null is returned, the request or connection is closed already.
  737. function checkUserIpAddress(req, res) {
  738. if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
  739. if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
  740. const domain = (req.url ? getDomain(req) : getDomain(res));
  741. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  742. if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
  743. if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
  744. return domain;
  745. }
  746. // Check if the source IP address is allowed, return domain if allowed
  747. // If there is a fail and null is returned, the request or connection is closed already.
  748. function checkAgentIpAddress(req, res) {
  749. if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
  750. if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
  751. const domain = (req.url ? getDomain(req) : getDomain(res));
  752. if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
  753. if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
  754. return domain;
  755. }
  756. // Return the current domain of the request
  757. // Request or connection says open regardless of the response
  758. function getDomain(req) {
  759. if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
  760. 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
  761. 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.
  762. const x = req.url.split('/');
  763. if (x.length < 2) return parent.config.domains[''];
  764. const y = parent.config.domains[x[1].toLowerCase()];
  765. if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
  766. return parent.config.domains[''];
  767. }
  768. function handleLogoutRequest(req, res) {
  769. const domain = checkUserIpAddress(req, res);
  770. if (domain == null) { return; }
  771. if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
  772. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  773. // If a HTTP header is required, check new UserRequiredHttpHeader
  774. 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; } }
  775. res.set({ 'Cache-Control': 'no-store' });
  776. // Destroy the user's session to log them out will be re-created next request
  777. var userid = req.session.userid;
  778. if (req.session.userid) {
  779. var user = obj.users[req.session.userid];
  780. if (user != null) {
  781. obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
  782. obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
  783. }
  784. if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
  785. }
  786. req.session = null;
  787. parent.debug('web', 'handleLogoutRequest: success.');
  788. // If this user was logged in using an authentication strategy and there is a logout URL, use it.
  789. if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
  790. let logouturl = null;
  791. let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
  792. // Setup logout url for oidc
  793. if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
  794. if (typeof domain.authstrategies.oidc.logouturl == 'string') {
  795. logouturl = domain.authstrategies.oidc.logouturl;
  796. } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
  797. logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + '?post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri;
  798. } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
  799. logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
  800. }
  801. // Log out all other strategies
  802. } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
  803. // If custom logout was setup, use it
  804. if (logouturl != null) {
  805. parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
  806. res.redirect(logouturl);
  807. return;
  808. }
  809. }
  810. // This is the default logout redirect to the login page
  811. if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); }
  812. }
  813. // Return an object with 2FA type if 2-step auth can be skipped
  814. function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
  815. if (parent.config.settings.no2factorauth == true) return null;
  816. // If this login occurred using a login token, no 2FA needed.
  817. if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
  818. // Check if we can skip 2nd factor auth because of the source IP address
  819. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  820. for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
  821. }
  822. // Check if a 2nd factor cookie is present
  823. if (typeof req.headers.cookie == 'string') {
  824. const cookies = req.headers.cookie.split('; ');
  825. for (var i in cookies) {
  826. if (cookies[i].startsWith('twofactor=')) {
  827. 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.
  828. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
  829. }
  830. }
  831. }
  832. return null;
  833. }
  834. // Return true if this user has 2-step auth active
  835. function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
  836. // If this login occurred using a login token, no 2FA needed.
  837. if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
  838. // Check if we can skip 2nd factor auth because of the source IP address
  839. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  840. for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
  841. }
  842. // Check if a 2nd factor cookie is present
  843. if (typeof req.headers.cookie == 'string') {
  844. const cookies = req.headers.cookie.split('; ');
  845. for (var i in cookies) {
  846. if (cookies[i].startsWith('twofactor=')) {
  847. 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.
  848. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
  849. }
  850. }
  851. }
  852. // See if SMS 2FA is available
  853. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  854. // See if Messenger 2FA is available
  855. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  856. // Check if a 2nd factor is present
  857. return ((parent.config.settings.no2factorauth !== true) && (msg2fa || sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || (user.otpduo != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
  858. }
  859. // Check the 2-step auth token
  860. function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
  861. parent.debug('web', 'checkUserOneTimePassword()');
  862. const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
  863. if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
  864. // Check if we can use OTP tokens with email
  865. var otpemail = (domain.mailserver != null);
  866. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
  867. var otpsms = (parent.smsserver != null);
  868. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
  869. var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
  870. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
  871. // Check 2FA login cookie
  872. if ((token != null) && (token.startsWith('cookie='))) {
  873. 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.
  874. if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
  875. }
  876. // Check email key
  877. if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
  878. var deltaTime = (Date.now() - user.otpekey.d);
  879. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
  880. user.otpekey = {};
  881. obj.db.SetUser(user);
  882. parent.debug('web', 'checkUserOneTimePassword: success (email).');
  883. func(true, { twoFactorType: 'email' });
  884. return;
  885. }
  886. }
  887. // Check SMS key
  888. if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
  889. var deltaTime = (Date.now() - user.otpsms.d);
  890. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
  891. delete user.otpsms;
  892. obj.db.SetUser(user);
  893. parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
  894. func(true, { twoFactorType: 'sms' });
  895. return;
  896. }
  897. }
  898. // Check messenger key
  899. if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
  900. var deltaTime = (Date.now() - user.otpmsg.d);
  901. if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
  902. delete user.otpmsg;
  903. obj.db.SetUser(user);
  904. parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
  905. func(true, { twoFactorType: 'messenger' });
  906. return;
  907. }
  908. }
  909. // Check hardware key
  910. if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
  911. var authResponse = null;
  912. try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
  913. if ((authResponse != null) && (authResponse.clientDataJSON)) {
  914. // Get all WebAuthn keys
  915. var webAuthnKeys = [];
  916. for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
  917. if (webAuthnKeys.length > 0) {
  918. // Decode authentication response
  919. var clientAssertionResponse = { response: {} };
  920. clientAssertionResponse.id = authResponse.id;
  921. clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
  922. clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
  923. clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
  924. clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
  925. clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
  926. // Look for the key with clientAssertionResponse.id
  927. var webAuthnKey = null;
  928. for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
  929. // If we found a valid key to use, let's validate the response
  930. if (webAuthnKey != null) {
  931. // Figure out the origin
  932. var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  933. var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
  934. if (httpport != 443) { origin += ':' + httpport; }
  935. var u2fchallenge = null;
  936. if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
  937. var assertionExpectations = {
  938. challenge: u2fchallenge,
  939. origin: origin,
  940. factor: 'either',
  941. fmt: 'fido-u2f',
  942. publicKey: webAuthnKey.publicKey,
  943. prevCounter: webAuthnKey.counter,
  944. userHandle: Buffer.from(user._id, 'binary').toString('base64')
  945. };
  946. var webauthnResponse = null;
  947. try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
  948. if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
  949. // Update the hardware key counter and accept the 2nd factor
  950. webAuthnKey.counter = webauthnResponse.counter;
  951. obj.db.SetUser(user);
  952. parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
  953. func(true, { twoFactorType: 'fido' });
  954. } else {
  955. parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
  956. func(false);
  957. }
  958. return;
  959. }
  960. }
  961. }
  962. }
  963. // Check Google Authenticator
  964. const otplib = require('otplib')
  965. otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
  966. if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
  967. parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
  968. func(true, { twoFactorType: 'otp' });
  969. return;
  970. };
  971. // Check written down keys
  972. if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
  973. var tokenNumber = parseInt(token);
  974. for (var i = 0; i < user.otpkeys.keys.length; i++) {
  975. if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
  976. parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
  977. user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
  978. }
  979. }
  980. }
  981. // Check OTP hardware key (Yubikey OTP)
  982. 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)) {
  983. var keyId = token.substring(0, 12);
  984. // Find a matching OTP key
  985. var match = false;
  986. for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
  987. // If we have a match, check the OTP
  988. if (match === true) {
  989. var yub = require('yub');
  990. yub.init(domain.yubikey.id, domain.yubikey.secret);
  991. yub.verify(token, function (err, results) {
  992. if ((results != null) && (results.status == 'OK')) {
  993. parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
  994. func(true, { twoFactorType: 'hwotp' });
  995. } else {
  996. parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
  997. func(false);
  998. }
  999. });
  1000. return;
  1001. }
  1002. }
  1003. parent.debug('web', 'checkUserOneTimePassword: fail (2).');
  1004. func(false);
  1005. }
  1006. // Return a U2F hardware key challenge
  1007. function getHardwareKeyChallenge(req, domain, user, func) {
  1008. var sec = {};
  1009. if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
  1010. if (user.otphkeys && (user.otphkeys.length > 0)) {
  1011. // Get all WebAuthn keys
  1012. var webAuthnKeys = [];
  1013. for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
  1014. if (webAuthnKeys.length > 0) {
  1015. // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
  1016. var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
  1017. // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin.
  1018. authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{
  1019. for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
  1020. sec.u2f = authnOptions.challenge;
  1021. req.session.e = parent.encryptSessionData(sec);
  1022. parent.debug('web', 'getHardwareKeyChallenge: success');
  1023. func(JSON.stringify(authnOptions));
  1024. return;
  1025. }
  1026. }
  1027. // Remove the challenge if present
  1028. if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
  1029. parent.debug('web', 'getHardwareKeyChallenge: fail');
  1030. func('');
  1031. }
  1032. // Redirect a root request to a different page
  1033. function handleRootRedirect(req, res, direct) {
  1034. const domain = checkUserIpAddress(req, res);
  1035. if (domain == null) { return; }
  1036. res.redirect(domain.rootredirect + getQueryPortion(req));
  1037. }
  1038. function handleLoginRequest(req, res, direct) {
  1039. const domain = checkUserIpAddress(req, res);
  1040. if (domain == null) { return; }
  1041. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1042. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1043. if (req.session == null) { req.session = {}; }
  1044. // Check if this is a banned ip address
  1045. if (obj.checkAllowLogin(req) == false) {
  1046. // Wait and redirect the user
  1047. setTimeout(function () {
  1048. req.session.messageid = 114; // IP address blocked, try again later.
  1049. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1050. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1051. return;
  1052. }
  1053. // Normally, use the body username/password. If this is a token, use the username/password in the session.
  1054. var xusername = req.body.username, xpassword = req.body.password;
  1055. if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
  1056. const sec = parent.decryptSessionData(req.session.e);
  1057. xusername = sec.tuser; xpassword = sec.tpass;
  1058. }
  1059. // Authenticate the user
  1060. obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
  1061. if (userid) {
  1062. var user = obj.users[userid];
  1063. // Check if we are in maintenance mode
  1064. if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
  1065. req.session.messageid = 115; // Server under maintenance
  1066. req.session.loginmode = 1;
  1067. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1068. return;
  1069. }
  1070. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
  1071. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  1072. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  1073. var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  1074. var duo2fa = ((((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) || ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor != false))) && (user.otpduo != null));
  1075. // Check if two factor can be skipped
  1076. const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
  1077. // Check if this user has 2-step login active
  1078. if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
  1079. if ((req.body.hwtoken == '**timeout**')) {
  1080. delete req.session; // Clear the session
  1081. res.redirect(domain.url + getQueryPortion(req));
  1082. return;
  1083. }
  1084. if ((req.body.hwtoken == '**email**') && email2fa) {
  1085. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  1086. obj.db.SetUser(user);
  1087. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  1088. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  1089. req.session.messageid = 2; // "Email sent" message
  1090. req.session.loginmode = 4;
  1091. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1092. return;
  1093. }
  1094. if ((req.body.hwtoken == '**sms**') && sms2fa) {
  1095. // Cause a token to be sent to the user's phone number
  1096. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1097. obj.db.SetUser(user);
  1098. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  1099. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  1100. // Ask for a login token & confirm sms was sent
  1101. req.session.messageid = 4; // "SMS sent" message
  1102. req.session.loginmode = 4;
  1103. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1104. return;
  1105. }
  1106. if ((req.body.hwtoken == '**msg**') && msg2fa) {
  1107. // Cause a token to be sent to the user's messenger account
  1108. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1109. obj.db.SetUser(user);
  1110. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  1111. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  1112. // Ask for a login token & confirm message was sent
  1113. req.session.messageid = 6; // "Message sent" message
  1114. req.session.loginmode = 4;
  1115. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1116. return;
  1117. }
  1118. if ((req.body.hwtoken == '**duo**') && duo2fa && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
  1119. // Redirect to duo here
  1120. const duo = require('@duosecurity/duo_universal');
  1121. const client = new duo.Client({
  1122. clientId: domain.duo2factor.integrationkey,
  1123. clientSecret: domain.duo2factor.secretkey,
  1124. apiHost: domain.duo2factor.apihostname,
  1125. redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
  1126. });
  1127. // Decrypt any session data
  1128. const sec = parent.decryptSessionData(req.session.e);
  1129. sec.duostate = client.generateState();
  1130. req.session.e = parent.encryptSessionData(sec);
  1131. parent.debug('web', 'Redirecting user ' + user._id + ' to Duo');
  1132. res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate));
  1133. return;
  1134. }
  1135. // Handle device push notification 2FA request
  1136. // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
  1137. if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
  1138. const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  1139. const sessioncode = obj.crypto.randomBytes(24).toString('base64');
  1140. // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
  1141. const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
  1142. // Get the HTTPS port
  1143. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
  1144. // Get the agent connection server name
  1145. var serverName = obj.getWebServerName(domain, req);
  1146. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  1147. // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  1148. var xdomain = (domain.dns == null) ? domain.id : '';
  1149. if (xdomain != '') xdomain += '/';
  1150. var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
  1151. // Request that the login page wait for device auth
  1152. req.session.messageid = 5; // "Sending notification..." message
  1153. req.session.passhint = url;
  1154. req.session.loginmode = 8;
  1155. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1156. return;
  1157. }
  1158. checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
  1159. if (result == false) {
  1160. var randomWaitTime = 0;
  1161. // Check if 2FA is allowed for this IP address
  1162. if (obj.checkAllow2Fa(req) == false) {
  1163. // Wait and redirect the user
  1164. setTimeout(function () {
  1165. req.session.messageid = 114; // IP address blocked, try again later.
  1166. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1167. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1168. return;
  1169. }
  1170. // 2-step auth is required, but the token is not present or not valid.
  1171. if ((req.body.token != null) || (req.body.hwtoken != null)) {
  1172. randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
  1173. req.session.messageid = 108; // Invalid token, try again.
  1174. obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
  1175. parent.debug('web', 'handleLoginRequest: invalid 2FA token');
  1176. const ua = obj.getUserAgentInfo(req);
  1177. 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] });
  1178. obj.setbad2Fa(req);
  1179. } else {
  1180. parent.debug('web', 'handleLoginRequest: 2FA token required');
  1181. }
  1182. // Wait and redirect the user
  1183. setTimeout(function () {
  1184. req.session.loginmode = 4;
  1185. if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
  1186. if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
  1187. if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; }
  1188. if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
  1189. if ((user.otpduo != null)) { req.session.tduo = 1; }
  1190. req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
  1191. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1192. }, randomWaitTime);
  1193. } else {
  1194. // Check if we need to remember this device
  1195. if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
  1196. var maxCookieAge = domain.twofactorcookiedurationdays;
  1197. if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
  1198. const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
  1199. res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
  1200. }
  1201. // Check if email address needs to be confirmed
  1202. 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'))
  1203. if (emailcheck && (user.emailVerified !== true)) {
  1204. parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
  1205. req.session.messageid = 3; // "Email verification required" message
  1206. req.session.loginmode = 7;
  1207. req.session.passhint = user.email;
  1208. req.session.cuserid = userid;
  1209. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1210. return;
  1211. }
  1212. // Login successful
  1213. parent.debug('web', 'handleLoginRequest: successful 2FA login');
  1214. if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
  1215. completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
  1216. }
  1217. });
  1218. return;
  1219. }
  1220. // Check if email address needs to be confirmed
  1221. 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'))
  1222. if (emailcheck && (user.emailVerified !== true)) {
  1223. parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
  1224. req.session.messageid = 3; // "Email verification required" message
  1225. req.session.loginmode = 7;
  1226. req.session.passhint = user.email;
  1227. req.session.cuserid = userid;
  1228. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1229. return;
  1230. }
  1231. // Login successful
  1232. parent.debug('web', 'handleLoginRequest: successful login');
  1233. if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
  1234. completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
  1235. } else {
  1236. // Login failed, log the error
  1237. obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
  1238. // Wait a random delay
  1239. setTimeout(function () {
  1240. // If the account is locked, display that.
  1241. if (typeof xusername == 'string') {
  1242. var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
  1243. if (err == 'locked') {
  1244. parent.debug('web', 'handleLoginRequest: login failed, locked account');
  1245. req.session.messageid = 110; // Account locked.
  1246. const ua = obj.getUserAgentInfo(req);
  1247. 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] });
  1248. obj.setbadLogin(req);
  1249. } else if (err == 'denied') {
  1250. parent.debug('web', 'handleLoginRequest: login failed, access denied');
  1251. req.session.messageid = 111; // Access denied.
  1252. const ua = obj.getUserAgentInfo(req);
  1253. 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] });
  1254. obj.setbadLogin(req);
  1255. } else {
  1256. parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
  1257. req.session.messageid = 112; // Login failed, check username and password.
  1258. const ua = obj.getUserAgentInfo(req);
  1259. 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] });
  1260. obj.setbadLogin(req);
  1261. }
  1262. }
  1263. // Clean up login mode and display password hint if present.
  1264. delete req.session.loginmode;
  1265. if ((passhint != null) && (passhint.length > 0)) {
  1266. req.session.passhint = passhint;
  1267. } else {
  1268. delete req.session.passhint;
  1269. }
  1270. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1271. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
  1272. }
  1273. });
  1274. }
  1275. function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
  1276. // Check if we need to change the password
  1277. 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))))) {
  1278. // Request a password change
  1279. parent.debug('web', 'handleLoginRequest: login ok, password change requested');
  1280. req.session.loginmode = 6;
  1281. req.session.messageid = 113; // Password change requested.
  1282. // Decrypt any session data
  1283. const sec = parent.decryptSessionData(req.session.e);
  1284. sec.rtuser = xusername;
  1285. sec.rtpass = xpassword;
  1286. sec.rtreset = true;
  1287. req.session.e = parent.encryptSessionData(sec);
  1288. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1289. return;
  1290. }
  1291. // Save login time
  1292. user.pastlogin = user.login;
  1293. user.login = user.access = Math.floor(Date.now() / 1000);
  1294. obj.db.SetUser(user);
  1295. // Notify account login
  1296. const targets = ['*', 'server-users', user._id];
  1297. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  1298. const ua = obj.getUserAgentInfo(req);
  1299. 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 };
  1300. if (loginOptions != null) {
  1301. 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.
  1302. if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
  1303. }
  1304. obj.parent.DispatchEvent(targets, obj, loginEvent);
  1305. // Regenerate session when signing in to prevent fixation
  1306. //req.session.regenerate(function () {
  1307. // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
  1308. delete req.session.e;
  1309. delete req.session.u2f;
  1310. delete req.session.loginmode;
  1311. delete req.session.tuserid;
  1312. delete req.session.tuser;
  1313. delete req.session.tpass;
  1314. delete req.session.temail;
  1315. delete req.session.tsms;
  1316. delete req.session.tmsg;
  1317. delete req.session.tduo;
  1318. delete req.session.tpush;
  1319. delete req.session.messageid;
  1320. delete req.session.passhint;
  1321. delete req.session.cuserid;
  1322. delete req.session.expire;
  1323. delete req.session.currentNode;
  1324. req.session.userid = userid;
  1325. req.session.ip = req.clientIp;
  1326. setSessionRandom(req);
  1327. 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 });
  1328. // If a login token was used, add this information and expire time to the session.
  1329. if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
  1330. req.session.loginToken = loginOptions.tokenUser;
  1331. if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
  1332. }
  1333. if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
  1334. if (req.body.host) {
  1335. // TODO: This is a terrible search!!! FIX THIS.
  1336. /*
  1337. obj.db.GetAllType('node', function (err, docs) {
  1338. for (var i = 0; i < docs.length; i++) {
  1339. if (docs[i].name == req.body.host) {
  1340. req.session.currentNode = docs[i]._id;
  1341. break;
  1342. }
  1343. }
  1344. console.log("CurrentNode: " + req.session.currentNode);
  1345. // This redirect happens after finding node is completed
  1346. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1347. });
  1348. */
  1349. parent.debug('web', 'handleLoginRequest: login ok (1)');
  1350. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
  1351. } else {
  1352. parent.debug('web', 'handleLoginRequest: login ok (2)');
  1353. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1354. }
  1355. //});
  1356. }
  1357. function handleCreateAccountRequest(req, res, direct) {
  1358. const domain = checkUserIpAddress(req, res);
  1359. if (domain == null) { return; }
  1360. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
  1361. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1362. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1363. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1364. // Check if we are in maintenance mode
  1365. if (parent.config.settings.maintenancemode != null) {
  1366. req.session.messageid = 115; // Server under maintenance
  1367. req.session.loginmode = 1;
  1368. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1369. return;
  1370. }
  1371. // Always lowercase the email address
  1372. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1373. // If the email is the username, set this here.
  1374. if (domain.usernameisemail) { req.body.username = req.body.email; }
  1375. // Check if there is domain.newAccountToken, check if supplied token is valid
  1376. if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
  1377. parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
  1378. req.session.loginmode = 2;
  1379. req.session.messageid = 103; // Invalid account creation token.
  1380. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1381. return;
  1382. }
  1383. // If needed, check the new account creation CAPTCHA
  1384. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  1385. const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  1386. if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
  1387. req.session.loginmode = 2;
  1388. req.session.messageid = 117; // Invalid security check
  1389. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1390. return;
  1391. }
  1392. }
  1393. // Accounts that start with ~ are not allowed
  1394. if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
  1395. parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
  1396. req.session.loginmode = 2;
  1397. req.session.messageid = 100; // Unable to create account.
  1398. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1399. return;
  1400. }
  1401. // Count the number of users in this domain
  1402. var domainUserCount = 0;
  1403. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
  1404. // Check if we are allowed to create new users using the login screen
  1405. if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
  1406. parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
  1407. res.sendStatus(401);
  1408. return;
  1409. }
  1410. // Check if this request is for an allows email domain
  1411. if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
  1412. var i = -1;
  1413. if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
  1414. if (i == -1) {
  1415. parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
  1416. req.session.loginmode = 2;
  1417. req.session.messageid = 100; // Unable to create account.
  1418. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1419. return;
  1420. }
  1421. var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
  1422. for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
  1423. if (emailok == false) {
  1424. parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
  1425. req.session.loginmode = 2;
  1426. req.session.messageid = 100; // Unable to create account.
  1427. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1428. return;
  1429. }
  1430. }
  1431. // Check if we exceed the maximum number of user accounts
  1432. obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
  1433. if (maxExceed) {
  1434. parent.debug('web', 'handleCreateAccountRequest: account limit reached');
  1435. req.session.loginmode = 2;
  1436. req.session.messageid = 101; // Account limit reached.
  1437. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1438. } else {
  1439. 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)) {
  1440. parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
  1441. req.session.loginmode = 2;
  1442. req.session.messageid = 100; // Unable to create account.
  1443. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1444. } else {
  1445. // Check if this email was already verified
  1446. obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
  1447. if ((docs != null) && (docs.length > 0)) {
  1448. parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
  1449. req.session.loginmode = 2;
  1450. req.session.messageid = 102; // Existing account with this email address.
  1451. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1452. } else {
  1453. // Check if user exists
  1454. if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
  1455. parent.debug('web', 'handleCreateAccountRequest: Username already exists');
  1456. req.session.loginmode = 2;
  1457. req.session.messageid = 104; // Username already exists.
  1458. } else {
  1459. 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 };
  1460. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
  1461. if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
  1462. 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; }
  1463. if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
  1464. // Auto-join any user groups
  1465. if (typeof domain.newaccountsusergroups == 'object') {
  1466. for (var i in domain.newaccountsusergroups) {
  1467. var ugrpid = domain.newaccountsusergroups[i];
  1468. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  1469. var ugroup = obj.userGroups[ugrpid];
  1470. if (ugroup != null) {
  1471. // Add group to the user
  1472. if (user.links == null) { user.links = {}; }
  1473. user.links[ugroup._id] = { rights: 1 };
  1474. // Add user to the group
  1475. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  1476. db.Set(ugroup);
  1477. // Notify user group change
  1478. 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 };
  1479. 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.
  1480. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  1481. }
  1482. }
  1483. }
  1484. obj.users[user._id] = user;
  1485. req.session.userid = user._id;
  1486. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  1487. setSessionRandom(req);
  1488. // Create a user, generate a salt and hash the password
  1489. require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
  1490. if (err) throw err;
  1491. user.salt = salt;
  1492. user.hash = hash;
  1493. delete user.passtype;
  1494. obj.db.SetUser(user);
  1495. // Send the verification email
  1496. 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); }
  1497. }, 0);
  1498. 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 };
  1499. 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.
  1500. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  1501. }
  1502. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1503. }
  1504. });
  1505. }
  1506. }
  1507. });
  1508. }
  1509. // Called to process an account password reset
  1510. function handleResetPasswordRequest(req, res, direct) {
  1511. const domain = checkUserIpAddress(req, res);
  1512. if (domain == null) { return; }
  1513. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1514. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1515. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1516. // Decrypt any session data
  1517. const sec = parent.decryptSessionData(req.session.e);
  1518. // Check everything is ok
  1519. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false) || (sec.rtreset === true));
  1520. 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')) {
  1521. parent.debug('web', 'handleResetPasswordRequest: checks failed');
  1522. delete req.session.e;
  1523. delete req.session.u2f;
  1524. delete req.session.loginmode;
  1525. delete req.session.tuserid;
  1526. delete req.session.tuser;
  1527. delete req.session.tpass;
  1528. delete req.session.temail;
  1529. delete req.session.tsms;
  1530. delete req.session.tmsg;
  1531. delete req.session.tpush;
  1532. delete req.session.messageid;
  1533. delete req.session.passhint;
  1534. delete req.session.cuserid;
  1535. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1536. return;
  1537. }
  1538. // Authenticate the user
  1539. obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
  1540. if (userid) {
  1541. // Login
  1542. var user = obj.users[userid];
  1543. // If we have password requirements, check this here.
  1544. if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
  1545. parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
  1546. req.session.loginmode = 6;
  1547. req.session.messageid = 105; // Password rejected, use a different one.
  1548. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1549. return;
  1550. }
  1551. // Check if the password is the same as a previous one
  1552. obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
  1553. if (result != 0) {
  1554. // This is the same password as an older one, request a password change again
  1555. parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
  1556. req.session.loginmode = 6;
  1557. req.session.messageid = 105; // Password rejected, use a different one.
  1558. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1559. } else {
  1560. // Update the password, use a different salt.
  1561. require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
  1562. const nowSeconds = Math.floor(Date.now() / 1000);
  1563. if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
  1564. if (domain.passwordrequirements != null) {
  1565. // Save password hint if this feature is enabled
  1566. 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; }
  1567. // Save previous password if this feature is enabled
  1568. if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  1569. if (user.oldpasswords == null) { user.oldpasswords = []; }
  1570. user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
  1571. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  1572. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  1573. }
  1574. }
  1575. user.salt = salt;
  1576. user.hash = hash;
  1577. user.passchange = user.access = nowSeconds;
  1578. delete user.passtype;
  1579. obj.db.SetUser(user);
  1580. // Event the account change
  1581. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
  1582. 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.
  1583. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1584. // Login successful
  1585. parent.debug('web', 'handleResetPasswordRequest: success');
  1586. req.session.userid = userid;
  1587. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  1588. setSessionRandom(req);
  1589. const sec = parent.decryptSessionData(req.session.e);
  1590. completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
  1591. }, 0);
  1592. }
  1593. }, 0);
  1594. } else {
  1595. // Failed, error out.
  1596. parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
  1597. delete req.session.e;
  1598. delete req.session.u2f;
  1599. delete req.session.loginmode;
  1600. delete req.session.tuserid;
  1601. delete req.session.tuser;
  1602. delete req.session.tpass;
  1603. delete req.session.temail;
  1604. delete req.session.tsms;
  1605. delete req.session.tmsg;
  1606. delete req.session.tpush;
  1607. delete req.session.messageid;
  1608. delete req.session.passhint;
  1609. delete req.session.cuserid;
  1610. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1611. return;
  1612. }
  1613. });
  1614. }
  1615. // Called to process an account reset request
  1616. function handleResetAccountRequest(req, res, direct) {
  1617. const domain = checkUserIpAddress(req, res);
  1618. if (domain == null) { return; }
  1619. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
  1620. 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; }
  1621. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1622. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1623. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1624. // Always lowercase the email address
  1625. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1626. // Get the email from the body or session.
  1627. var email = req.body.email;
  1628. if ((email == null) || (email == '')) { email = req.session.temail; }
  1629. // Check the email string format
  1630. if (!email || checkEmail(email) == false) {
  1631. parent.debug('web', 'handleResetAccountRequest: Invalid email');
  1632. req.session.loginmode = 3;
  1633. req.session.messageid = 106; // Invalid email.
  1634. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1635. } else {
  1636. obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
  1637. // Remove all accounts that start with ~ since they are special accounts.
  1638. var cleanDocs = [];
  1639. if ((err == null) && (docs.length > 0)) {
  1640. for (var i in docs) {
  1641. const user = docs[i];
  1642. const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
  1643. const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
  1644. if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
  1645. }
  1646. }
  1647. docs = cleanDocs;
  1648. // Check if we have any account that match this email address
  1649. if ((err != null) || (docs.length == 0)) {
  1650. parent.debug('web', 'handleResetAccountRequest: Account not found');
  1651. req.session.loginmode = 3;
  1652. 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.
  1653. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1654. } else {
  1655. // 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.
  1656. var responseSent = false;
  1657. for (var i in docs) {
  1658. var user = docs[i];
  1659. if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
  1660. // Second factor setup, request it now.
  1661. checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
  1662. if (result == false) {
  1663. if (i == 0) {
  1664. // Check if 2FA is allowed for this IP address
  1665. if (obj.checkAllow2Fa(req) == false) {
  1666. // Wait and redirect the user
  1667. setTimeout(function () {
  1668. req.session.messageid = 114; // IP address blocked, try again later.
  1669. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1670. }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
  1671. return;
  1672. }
  1673. // 2-step auth is required, but the token is not present or not valid.
  1674. parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
  1675. if ((req.body.token != null) || (req.body.hwtoken != null)) {
  1676. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  1677. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  1678. if ((req.body.hwtoken == '**sms**') && sms2fa) {
  1679. // Cause a token to be sent to the user's phone number
  1680. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1681. obj.db.SetUser(user);
  1682. parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
  1683. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  1684. req.session.messageid = 4; // SMS sent.
  1685. } else if ((req.body.hwtoken == '**msg**') && msg2fa) {
  1686. // Cause a token to be sent to the user's messager account
  1687. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  1688. obj.db.SetUser(user);
  1689. parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
  1690. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  1691. req.session.messageid = 6; // Message sent.
  1692. } else {
  1693. req.session.messageid = 108; // Invalid token, try again.
  1694. const ua = obj.getUserAgentInfo(req);
  1695. 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] });
  1696. obj.setbad2Fa(req);
  1697. }
  1698. }
  1699. req.session.loginmode = 5;
  1700. req.session.temail = email;
  1701. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1702. }
  1703. } else {
  1704. // Send email to perform recovery.
  1705. delete req.session.temail;
  1706. if (domain.mailserver != null) {
  1707. domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1708. if (i == 0) {
  1709. parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
  1710. req.session.loginmode = 1;
  1711. req.session.messageid = 1; // If valid, reset mail sent.
  1712. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1713. }
  1714. } else {
  1715. if (i == 0) {
  1716. parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
  1717. req.session.loginmode = 3;
  1718. req.session.messageid = 109; // Unable to sent email.
  1719. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1720. }
  1721. }
  1722. }
  1723. });
  1724. } else {
  1725. // No second factor, send email to perform recovery.
  1726. if (domain.mailserver != null) {
  1727. domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1728. if (i == 0) {
  1729. parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
  1730. req.session.loginmode = 1;
  1731. req.session.messageid = 1; // If valid, reset mail sent.
  1732. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1733. }
  1734. } else {
  1735. if (i == 0) {
  1736. parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
  1737. req.session.loginmode = 3;
  1738. req.session.messageid = 109; // Unable to sent email.
  1739. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1740. }
  1741. }
  1742. }
  1743. }
  1744. }
  1745. });
  1746. }
  1747. }
  1748. // Handle account email change and email verification request
  1749. function handleCheckAccountEmailRequest(req, res, direct) {
  1750. const domain = checkUserIpAddress(req, res);
  1751. if (domain == null) { return; }
  1752. 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; }
  1753. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1754. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  1755. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  1756. // Always lowercase the email address
  1757. if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
  1758. // Get the email from the body or session.
  1759. var email = req.body.email;
  1760. if ((email == null) || (email == '')) { email = req.session.temail; }
  1761. // Check if this request is for an allows email domain
  1762. if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
  1763. var i = -1;
  1764. if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
  1765. if (i == -1) {
  1766. parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
  1767. req.session.loginmode = 7;
  1768. req.session.messageid = 106; // Invalid email.
  1769. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1770. return;
  1771. }
  1772. var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
  1773. for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
  1774. if (emailok == false) {
  1775. parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
  1776. req.session.loginmode = 7;
  1777. req.session.messageid = 106; // Invalid email.
  1778. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1779. return;
  1780. }
  1781. }
  1782. // Check the email string format
  1783. if (!email || checkEmail(email) == false) {
  1784. parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
  1785. req.session.loginmode = 7;
  1786. req.session.messageid = 106; // Invalid email.
  1787. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1788. } else {
  1789. // Check is email already exists
  1790. obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
  1791. if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
  1792. // Email already exists
  1793. req.session.messageid = 102; // Existing account with this email address.
  1794. } else {
  1795. // Update the user and notify of user email address change
  1796. var user = obj.users[req.session.cuserid];
  1797. if (user.email != email) {
  1798. user.email = email;
  1799. db.SetUser(user);
  1800. var targets = ['*', 'server-users', user._id];
  1801. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  1802. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
  1803. 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.
  1804. parent.DispatchEvent(targets, obj, event);
  1805. }
  1806. // Send the verification email
  1807. domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
  1808. // Send the response
  1809. req.session.messageid = 2; // Email sent.
  1810. }
  1811. req.session.loginmode = 7;
  1812. delete req.session.cuserid;
  1813. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  1814. });
  1815. }
  1816. }
  1817. // Called to process a web based email verification request
  1818. function handleCheckMailRequest(req, res) {
  1819. const domain = checkUserIpAddress(req, res);
  1820. if (domain == null) { return; }
  1821. if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
  1822. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1823. if (req.query.c != null) {
  1824. var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
  1825. if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
  1826. var idsplit = cookie.u.split('/');
  1827. if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
  1828. parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
  1829. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 1, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1830. } else {
  1831. obj.db.Get(cookie.u, function (err, docs) {
  1832. if (docs.length == 0) {
  1833. parent.debug('web', 'handleCheckMailRequest: Invalid username.');
  1834. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1835. } else {
  1836. var user = docs[0];
  1837. if (user.email != cookie.e) {
  1838. parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
  1839. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1840. } else {
  1841. if (cookie.a == 1) {
  1842. // Account email verification
  1843. if (user.emailVerified == true) {
  1844. parent.debug('web', 'handleCheckMailRequest: email already verified.');
  1845. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1846. } else {
  1847. obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
  1848. if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
  1849. parent.debug('web', 'handleCheckMailRequest: email already in use.');
  1850. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1851. } else {
  1852. parent.debug('web', 'handleCheckMailRequest: email verification success.');
  1853. // Set the verified flag
  1854. obj.users[user._id].emailVerified = true;
  1855. user.emailVerified = true;
  1856. obj.db.SetUser(user);
  1857. // Event the change
  1858. 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 };
  1859. 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.
  1860. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1861. // Send the confirmation page
  1862. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1863. // Send a notification
  1864. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
  1865. // Send to authLog
  1866. obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
  1867. }
  1868. });
  1869. }
  1870. } else if (cookie.a == 2) {
  1871. // Account reset
  1872. if (user.emailVerified != true) {
  1873. parent.debug('web', 'handleCheckMailRequest: email not verified.');
  1874. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1875. } else {
  1876. if (req.query.confirm == 1) {
  1877. // Set a temporary password
  1878. obj.crypto.randomBytes(16, function (err, buf) {
  1879. var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
  1880. require('./pass').hash(newpass, function (err, salt, hash, tag) {
  1881. if (err) throw err;
  1882. // Change the password
  1883. var userinfo = obj.users[user._id];
  1884. userinfo.salt = salt;
  1885. userinfo.hash = hash;
  1886. delete userinfo.passtype;
  1887. userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
  1888. delete userinfo.passhint;
  1889. obj.db.SetUser(userinfo);
  1890. // Event the change
  1891. 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 };
  1892. 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.
  1893. obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
  1894. // Send the new password
  1895. render(req, res, getRenderPage((domain.sitestyle >= 2) ? '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));
  1896. parent.debug('web', 'handleCheckMailRequest: send temporary password.');
  1897. // Send to authLog
  1898. obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
  1899. }, 0);
  1900. });
  1901. } else {
  1902. // Display a link for the user to confirm password reset
  1903. // 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.
  1904. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 14, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1905. }
  1906. }
  1907. } else {
  1908. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 9, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1909. }
  1910. }
  1911. }
  1912. });
  1913. }
  1914. } else {
  1915. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 10, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1916. }
  1917. }
  1918. }
  1919. // Called to process an agent invite GET/POST request
  1920. function handleInviteRequest(req, res) {
  1921. const domain = getDomain(req);
  1922. if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
  1923. if (domain.agentinvitecodes != true) { nice404(req, res); return; }
  1924. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1925. 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
  1926. // Each for a device group that has this invite code.
  1927. for (var i in obj.meshes) {
  1928. 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)) {
  1929. // Send invitation link, valid for 1 minute.
  1930. 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)) : ''));
  1931. return;
  1932. }
  1933. }
  1934. render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
  1935. }
  1936. // Called to render the MSTSC (RDP) or SSH web page
  1937. function handleMSTSCRequest(req, res, page) {
  1938. const domain = getDomain(req);
  1939. if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
  1940. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  1941. // Check if we are in maintenance mode
  1942. if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
  1943. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  1944. return;
  1945. }
  1946. // Set features we want to send to this page
  1947. var features = 0;
  1948. if (domain.allowsavingdevicecredentials === false) { features |= 1; }
  1949. // Get the logged in user if present
  1950. var user = null;
  1951. // If there is a login token, use that
  1952. if (req.query.login != null) {
  1953. var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  1954. if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
  1955. }
  1956. // If no token, see if we have an active session
  1957. if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
  1958. // If still no user, see if we have a default user
  1959. if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
  1960. // No user login, exit now
  1961. if (user == null) { res.sendStatus(401); return; }
  1962. if (req.query.ws != null) {
  1963. // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
  1964. var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  1965. if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
  1966. // Fetch the node from the database
  1967. obj.db.Get(rcookie.nodeid, function (err, nodes) {
  1968. if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  1969. const node = nodes[0];
  1970. // Check if we have SSH/RDP credentials for this device
  1971. var serverCredentials = 0;
  1972. if (domain.allowsavingdevicecredentials !== false) {
  1973. if (page == 'ssh') {
  1974. if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
  1975. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
  1976. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
  1977. 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
  1978. 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
  1979. 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
  1980. } else {
  1981. 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
  1982. 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
  1983. }
  1984. }
  1985. // Render the page
  1986. 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));
  1987. });
  1988. return;
  1989. }
  1990. }
  1991. // Check the nodeid
  1992. if (req.query.node != null) {
  1993. var nodeidsplit = req.query.node.split('/');
  1994. if (nodeidsplit.length == 1) {
  1995. req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
  1996. } else if (nodeidsplit.length == 3) {
  1997. if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
  1998. } else {
  1999. req.query.node = null; // Bad nodeid
  2000. }
  2001. }
  2002. // If there is no nodeid, exit now
  2003. if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
  2004. // Fetch the node from the database
  2005. obj.db.Get(req.query.node, function (err, nodes) {
  2006. if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  2007. const node = nodes[0];
  2008. // Check access rights, must have remote control rights
  2009. if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
  2010. // Figure out the target port
  2011. var port = 0, serverCredentials = false;
  2012. if (page == 'ssh') {
  2013. // SSH port
  2014. port = 22;
  2015. if (typeof node.sshport == 'number') { port = node.sshport; }
  2016. // Check if we have SSH credentials for this device
  2017. if (domain.allowsavingdevicecredentials !== false) {
  2018. if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
  2019. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
  2020. else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
  2021. 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
  2022. 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
  2023. 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
  2024. }
  2025. } else {
  2026. // RDP port
  2027. port = 3389;
  2028. if (typeof node.rdpport == 'number') { port = node.rdpport; }
  2029. // Check if we have RDP credentials for this device
  2030. if (domain.allowsavingdevicecredentials !== false) {
  2031. 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
  2032. 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
  2033. }
  2034. }
  2035. 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; } }
  2036. // Generate a cookie and respond
  2037. var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
  2038. render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
  2039. });
  2040. }
  2041. // Called to handle push-only requests
  2042. function handleFirebasePushOnlyRelayRequest(req, res) {
  2043. parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
  2044. if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
  2045. if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
  2046. if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
  2047. var data = null;
  2048. try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
  2049. if (typeof data != 'object') { res.sendStatus(404); return; }
  2050. if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
  2051. if (typeof data.payload != 'object') { res.sendStatus(404); return; }
  2052. if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
  2053. if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
  2054. if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
  2055. if (typeof data.options != 'object') { res.sendStatus(404); return; }
  2056. if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
  2057. if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
  2058. parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
  2059. obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
  2060. if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
  2061. });
  2062. }
  2063. // Called to handle two-way push notification relay request
  2064. function handleFirebaseRelayRequest(ws, req) {
  2065. parent.debug('email', 'handleFirebaseRelayRequest');
  2066. if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
  2067. if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
  2068. if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
  2069. 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; }
  2070. obj.parent.firebase.setupRelay(ws);
  2071. }
  2072. // Called to process an agent invite request
  2073. function handleAgentInviteRequest(req, res) {
  2074. const domain = getDomain(req);
  2075. if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
  2076. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2077. if (req.query.c != null) {
  2078. // A cookie is specified in the query string, use that
  2079. var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
  2080. if (cookie == null) { res.sendStatus(404); return; }
  2081. var mesh = obj.meshes[cookie.mid];
  2082. if (mesh == null) { res.sendStatus(404); return; }
  2083. var installflags = cookie.f;
  2084. if (typeof installflags != 'number') { installflags = 0; }
  2085. var showagents = cookie.ag;
  2086. if (typeof showagents != 'number') { showagents = 0; }
  2087. parent.debug('web', 'handleAgentInviteRequest using cookie.');
  2088. // Build the mobile agent URL, this is used to connect mobile devices
  2089. var agentServerName = obj.getWebServerName(domain, req);
  2090. if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
  2091. var xdomain = (domain.dns == null) ? domain.id : '';
  2092. var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2093. if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  2094. if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  2095. var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
  2096. var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
  2097. 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));
  2098. } else if (req.query.m != null) {
  2099. // The MeshId is specified in the query string, use that
  2100. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
  2101. if (mesh == null) { res.sendStatus(404); return; }
  2102. var installflags = 0;
  2103. if (req.query.f) { installflags = parseInt(req.query.f); }
  2104. if (typeof installflags != 'number') { installflags = 0; }
  2105. var showagents = 0;
  2106. if (req.query.f) { showagents = parseInt(req.query.ag); }
  2107. if (typeof showagents != 'number') { showagents = 0; }
  2108. parent.debug('web', 'handleAgentInviteRequest using meshid.');
  2109. // Build the mobile agent URL, this is used to connect mobile devices
  2110. var agentServerName = obj.getWebServerName(domain, req);
  2111. if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
  2112. var xdomain = (domain.dns == null) ? domain.id : '';
  2113. var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2114. if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  2115. if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  2116. var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
  2117. var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
  2118. 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));
  2119. }
  2120. }
  2121. // Called to process an agent invite request
  2122. function handleUserImageRequest(req, res) {
  2123. const domain = getDomain(req);
  2124. if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
  2125. if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
  2126. var imageUserId = req.session.userid;
  2127. if ((req.query.id != null)) {
  2128. var user = obj.users[req.session.userid];
  2129. if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
  2130. imageUserId = 'user/' + domain.id + '/' + req.query.id;
  2131. }
  2132. obj.db.Get('im' + imageUserId, function (err, docs) {
  2133. if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
  2134. var imagebase64 = docs[0].image;
  2135. if (imagebase64.startsWith('data:image/png;base64,')) {
  2136. res.set('Content-Type', 'image/png');
  2137. res.set({ 'Cache-Control': 'no-store' });
  2138. res.send(Buffer.from(imagebase64.substring(22), 'base64'));
  2139. } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
  2140. res.set('Content-Type', 'image/jpeg');
  2141. res.set({ 'Cache-Control': 'no-store' });
  2142. res.send(Buffer.from(imagebase64.substring(23), 'base64'));
  2143. } else {
  2144. res.sendStatus(404);
  2145. }
  2146. });
  2147. }
  2148. function handleDeleteAccountRequest(req, res, direct) {
  2149. parent.debug('web', 'handleDeleteAccountRequest()');
  2150. const domain = checkUserIpAddress(req, res);
  2151. if (domain == null) { return; }
  2152. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
  2153. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2154. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  2155. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  2156. var user = null;
  2157. if (req.body.authcookie) {
  2158. // If a authentication cookie is provided, decode it here
  2159. var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  2160. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
  2161. } else {
  2162. // Check if the user is logged and we have all required parameters
  2163. if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
  2164. parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
  2165. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2166. return;
  2167. } else {
  2168. user = obj.users[req.session.userid];
  2169. }
  2170. }
  2171. if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
  2172. if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
  2173. // Check if the password is correct
  2174. obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
  2175. var deluser = obj.users[userid];
  2176. if ((userid != null) && (deluser != null)) {
  2177. // Remove all links to this user
  2178. if (deluser.links != null) {
  2179. for (var i in deluser.links) {
  2180. if (i.startsWith('mesh/')) {
  2181. // Get the device group
  2182. var mesh = obj.meshes[i];
  2183. if (mesh) {
  2184. // Remove user from the mesh
  2185. if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
  2186. // Notify mesh change
  2187. var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
  2188. 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 };
  2189. 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.
  2190. parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
  2191. }
  2192. } else if (i.startsWith('node/')) {
  2193. // Get the node and the rights for this node
  2194. obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
  2195. if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
  2196. // Remove the link and save the node to the database
  2197. delete node.links[deluser._id];
  2198. if (Object.keys(node.links).length == 0) { delete node.links; }
  2199. db.Set(obj.cleanDevice(node));
  2200. // Event the node change
  2201. 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) }
  2202. 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.
  2203. parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
  2204. });
  2205. } else if (i.startsWith('ugrp/')) {
  2206. // Get the device group
  2207. var ugroup = obj.userGroups[i];
  2208. if (ugroup) {
  2209. // Remove user from the user group
  2210. if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
  2211. // Notify user group change
  2212. var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
  2213. 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 };
  2214. 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.
  2215. parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
  2216. }
  2217. }
  2218. }
  2219. }
  2220. obj.db.Remove('ws' + deluser._id); // Remove user web state
  2221. obj.db.Remove('nt' + deluser._id); // Remove notes for this user
  2222. obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
  2223. obj.db.Remove('im' + deluser._id); // Remove image for this user
  2224. // Delete any login tokens
  2225. parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
  2226. if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
  2227. });
  2228. // Delete all files on the server for this account
  2229. try {
  2230. var deluserpath = obj.getServerRootFilePath(deluser);
  2231. if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
  2232. } catch (e) { }
  2233. // Remove the user
  2234. obj.db.Remove(deluser._id);
  2235. delete obj.users[deluser._id];
  2236. req.session = null;
  2237. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2238. obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
  2239. parent.debug('web', 'handleDeleteAccountRequest: removed user.');
  2240. } else {
  2241. parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
  2242. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2243. }
  2244. });
  2245. }
  2246. // Check a user's password
  2247. obj.checkUserPassword = function (domain, user, password, func) {
  2248. // Check the old password
  2249. if (user.passtype != null) {
  2250. // IIS default clear or weak password hashing (SHA-1)
  2251. require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
  2252. if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
  2253. if (hash == user.hash) {
  2254. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
  2255. parent.debug('web', 'checkUserPassword: SHA-1 ok.');
  2256. return func(true); // Allow password change
  2257. }
  2258. func(false);
  2259. });
  2260. } else {
  2261. // Default strong password hashing (pbkdf2 SHA384)
  2262. require('./pass').hash(password, user.salt, function (err, hash, tag) {
  2263. if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
  2264. if (hash == user.hash) {
  2265. if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
  2266. parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
  2267. return func(true); // Allow password change
  2268. }
  2269. func(false);
  2270. }, 0);
  2271. }
  2272. }
  2273. // Check a user's old passwords
  2274. // Callback: 0=OK, 1=OldPass, 2=CommonPass
  2275. obj.checkOldUserPasswords = function (domain, user, password, func) {
  2276. // Check how many old passwords we need to check
  2277. if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  2278. if (user.oldpasswords != null) {
  2279. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  2280. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  2281. }
  2282. } else {
  2283. delete user.oldpasswords;
  2284. }
  2285. // If there is no old passwords, exit now.
  2286. var oldPassCount = 1;
  2287. if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
  2288. var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
  2289. // Test against common passwords if this feature is enabled
  2290. // Example of common passwords: 123456789, password123
  2291. if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
  2292. oldPassCheckState.count++;
  2293. require('wildleek')(password).then(function (wild) {
  2294. if (wild == true) { oldPassCheckState.response = 2; }
  2295. if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
  2296. });
  2297. }
  2298. // Try current password
  2299. require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
  2300. if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
  2301. if (--tag.count == 0) { tag.func(tag.response); }
  2302. }, oldPassCheckState);
  2303. // Try each old password
  2304. if (user.oldpasswords != null) {
  2305. for (var i in user.oldpasswords) {
  2306. const oldpassword = user.oldpasswords[i];
  2307. // Default strong password hashing (pbkdf2 SHA384)
  2308. require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
  2309. if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
  2310. if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
  2311. }, { oldPassword: oldpassword, state: oldPassCheckState });
  2312. }
  2313. }
  2314. }
  2315. // Handle password changes
  2316. function handlePasswordChangeRequest(req, res, direct) {
  2317. const domain = checkUserIpAddress(req, res);
  2318. if (domain == null) { return; }
  2319. if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
  2320. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2321. if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
  2322. if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
  2323. // Check if the user is logged and we have all required parameters
  2324. 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)) {
  2325. parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
  2326. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2327. return;
  2328. }
  2329. // Get the current user
  2330. var user = obj.users[req.session.userid];
  2331. if (!user) {
  2332. parent.debug('web', 'handlePasswordChangeRequest: user not found.');
  2333. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2334. return;
  2335. }
  2336. // Check account settings locked
  2337. if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
  2338. parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
  2339. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2340. return;
  2341. }
  2342. // Check old password
  2343. obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
  2344. if (result == true) {
  2345. // Check if the new password is allowed, only do this if this feature is enabled.
  2346. parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
  2347. if (result == 1) {
  2348. parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
  2349. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2350. } else if (result == 2) {
  2351. parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
  2352. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2353. } else {
  2354. // Update the password
  2355. require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
  2356. const nowSeconds = Math.floor(Date.now() / 1000);
  2357. if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
  2358. if (domain.passwordrequirements != null) {
  2359. // Save password hint if this feature is enabled
  2360. 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; }
  2361. // Save previous password if this feature is enabled
  2362. if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
  2363. if (user.oldpasswords == null) { user.oldpasswords = []; }
  2364. user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
  2365. const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
  2366. if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
  2367. }
  2368. }
  2369. user.salt = salt;
  2370. user.hash = hash;
  2371. user.passchange = user.access = nowSeconds;
  2372. delete user.passtype;
  2373. obj.db.SetUser(user);
  2374. req.session.viewmode = 2;
  2375. if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
  2376. 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 });
  2377. }, 0);
  2378. }
  2379. });
  2380. }
  2381. });
  2382. }
  2383. // Called when a strategy login occurred
  2384. // This is called after a successful Oauth to Twitter, Google, GitHub...
  2385. function handleStrategyLogin(req, res) {
  2386. const domain = checkUserIpAddress(req, res);
  2387. if (domain == null) { return; }
  2388. if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
  2389. const strategy = domain.authstrategies[req.user.strategy];
  2390. const groups = { 'enabled': typeof strategy.groups == 'object' }
  2391. parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
  2392. if (groups.enabled) { // Groups only available for OIDC strategy currently
  2393. groups.userMemberships = obj.common.convertStrArray(req.user.groups);
  2394. groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false;
  2395. groups.syncMemberships = [];
  2396. groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false;
  2397. groups.grantAdmin = false;
  2398. groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true;
  2399. groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required);
  2400. groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin);
  2401. groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter);
  2402. // Fancy Logs
  2403. let groupMessage = '';
  2404. if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
  2405. else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
  2406. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
  2407. // Check user membership in required groups
  2408. if (groups.requiredGroups.length > 0) {
  2409. let match = false
  2410. for (var i in groups.requiredGroups) {
  2411. if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
  2412. match = true;
  2413. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
  2414. }
  2415. }
  2416. if (match === false) {
  2417. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`);
  2418. req.session.loginmode = 1;
  2419. req.session.messageid = 111; // Access Denied.
  2420. res.redirect(domain.url + getQueryPortion(req));
  2421. return;
  2422. }
  2423. }
  2424. // Check user membership in admin groups
  2425. if (groups.siteAdminEnabled === true) {
  2426. groups.grantAdmin = false;
  2427. for (var i in strategy.groups.siteadmin) {
  2428. if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
  2429. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
  2430. groups.siteAdmin = strategy.groups.siteadmin[i];
  2431. groups.grantAdmin = true;
  2432. break;
  2433. }
  2434. }
  2435. }
  2436. // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
  2437. if (groups.syncEnabled === true) {
  2438. if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it
  2439. for (var i in groups.syncFilter) {
  2440. if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
  2441. }
  2442. } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead
  2443. for (var i in groups.userMemberships) {
  2444. groups.syncMemberships.push(groups.userMemberships[i]);
  2445. }
  2446. }
  2447. if (groups.syncMemberships.length > 0) {
  2448. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`);
  2449. } else {
  2450. groups.syncMemberships = null;
  2451. groups.syncEnabled = false;
  2452. if (groups.syncFilter.length > 0){
  2453. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`);
  2454. } else {
  2455. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
  2456. }
  2457. }
  2458. }
  2459. }
  2460. // Check if the user already exists
  2461. const userid = 'user/' + domain.id + '/' + req.user.sid;
  2462. var user = obj.users[userid];
  2463. if (user == null) {
  2464. var newAccountAllowed = false;
  2465. var newAccountRealms = null;
  2466. if (domain.newaccounts === true) { newAccountAllowed = true; }
  2467. if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
  2468. if (domain.authstrategies[req.user.strategy]) {
  2469. if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
  2470. if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
  2471. }
  2472. if (newAccountAllowed === true) {
  2473. // Create the user
  2474. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
  2475. 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 };
  2476. if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
  2477. if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
  2478. 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.
  2479. if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
  2480. obj.users[userid] = user;
  2481. // Auto-join any user groups
  2482. var newaccountsusergroups = null;
  2483. if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
  2484. if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
  2485. if (newaccountsusergroups) {
  2486. for (var i in newaccountsusergroups) {
  2487. var ugrpid = newaccountsusergroups[i];
  2488. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  2489. var ugroup = obj.userGroups[ugrpid];
  2490. if (ugroup != null) {
  2491. // Add group to the user
  2492. if (user.links == null) { user.links = {}; }
  2493. user.links[ugroup._id] = { rights: 1 };
  2494. // Add user to the group
  2495. ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  2496. db.Set(ugroup);
  2497. // Notify user group change
  2498. 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 };
  2499. 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.
  2500. parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
  2501. }
  2502. }
  2503. }
  2504. if (groups.enabled === true) {
  2505. // Sync the user groups if enabled
  2506. if (groups.syncEnabled === true) {
  2507. // Set groupType to the preset name if it exists, otherwise use the strategy name
  2508. const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
  2509. syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
  2510. }
  2511. // See if the user is a member of the site admin group.
  2512. if (groups.grantAdmin === true) {
  2513. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
  2514. user.siteadmin = 0xFFFFFFFF;
  2515. }
  2516. }
  2517. // Save the user
  2518. obj.db.SetUser(user);
  2519. // Event user creation
  2520. var targets = ['*', 'server-users'];
  2521. 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 };
  2522. 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.
  2523. parent.DispatchEvent(targets, obj, event);
  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. } else {
  2533. // New users not allowed
  2534. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
  2535. req.session.loginmode = 1;
  2536. req.session.messageid = 100; // Unable to create account.
  2537. res.redirect(domain.url + getQueryPortion(req));
  2538. return;
  2539. }
  2540. } else { // Login success
  2541. // Check for basic changes
  2542. var userChanged = false;
  2543. if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
  2544. if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
  2545. if (groups.enabled === true) {
  2546. // Sync the user groups if enabled
  2547. if (groups.syncEnabled === true) {
  2548. syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
  2549. }
  2550. // See if the user is a member of the site admin group.
  2551. if (groups.siteAdminEnabled === true) {
  2552. if (groups.grantAdmin === true) {
  2553. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
  2554. if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
  2555. } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
  2556. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
  2557. delete user.siteadmin;
  2558. userChanged = true;
  2559. }
  2560. }
  2561. }
  2562. // Update db record for user if there are changes detected
  2563. if (userChanged) {
  2564. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
  2565. obj.db.SetUser(user);
  2566. // Event user change
  2567. var targets = ['*', 'server-users'];
  2568. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
  2569. 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.
  2570. parent.DispatchEvent(targets, obj, event);
  2571. }
  2572. req.session.userid = userid;
  2573. setSessionRandom(req);
  2574. // Notify account login using SSO
  2575. var targets = ['*', 'server-users', user._id];
  2576. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  2577. const ua = obj.getUserAgentInfo(req);
  2578. 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' };
  2579. obj.parent.DispatchEvent(targets, obj, loginEvent);
  2580. parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
  2581. }
  2582. } else if (req.session && req.session.userid && obj.users[req.session.userid]) {
  2583. parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`);
  2584. } else {
  2585. parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
  2586. }
  2587. //res.redirect(domain.url); // This does not handle cookie correctly.
  2588. res.set('Content-Type', 'text/html');
  2589. let url = domain.url;
  2590. if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
  2591. // check for relaystate is set, test against configured server name and accepted query params
  2592. if(req.body && req.body.RelayState !== undefined){
  2593. var relayState = decodeURIComponent(req.body.RelayState);
  2594. var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.');
  2595. var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` +
  2596. `.*((?<=([\\?&])gotodevicename=(.{64})|` +
  2597. `gotonode=(.{64})|` +
  2598. `gotodeviceip=(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})|` +
  2599. `gotodeviceip=(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:)` +
  2600. `lang=(.{5})|` +
  2601. `sitestyle=(\\d+)|` +
  2602. `user=(.{64})|` +
  2603. `pass=(.{256})|` +
  2604. `key=|` +
  2605. `locale=|` +
  2606. `gotomesh=(.{64})|` +
  2607. `gotouser=(.{0,64})|` +
  2608. `gotougrp=(.{64})|` +
  2609. `debug=|` +
  2610. `filter=|` +
  2611. `webrtc=|` +
  2612. `hide=|` +
  2613. `viewmode=(\\d+)(?=[\\&]|\\b)))`;
  2614. var regex = new RegExp(regexstr);
  2615. if(regex.test(relayState)){
  2616. url = relayState;
  2617. }
  2618. }
  2619. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
  2620. }
  2621. // Indicates that any request to "/" should render "default" or "login" depending on login state
  2622. function handleRootRequest(req, res, direct) {
  2623. const domain = checkUserIpAddress(req, res);
  2624. if (domain == null) { return; }
  2625. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  2626. if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
  2627. // If a HTTP header is required, check new UserRequiredHttpHeader
  2628. 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; } }
  2629. // If the session is expired, clear it.
  2630. 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]; } }
  2631. // Check if we are in maintenance mode
  2632. if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
  2633. parent.debug('web', 'handleLoginRequest: Server under maintenance.');
  2634. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  2635. return;
  2636. }
  2637. // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
  2638. if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
  2639. var q = require('url').parse(req.url, true);
  2640. if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
  2641. }
  2642. if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
  2643. // Login using SSPI
  2644. domain.sspi.authenticate(req, res, function (err) {
  2645. if ((err != null) || (req.connection.user == null)) {
  2646. obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
  2647. parent.debug('web', 'handleRootRequest: SSPI auth required.');
  2648. try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
  2649. } else {
  2650. parent.debug('web', 'handleRootRequest: SSPI auth ok.');
  2651. handleRootRequestEx(req, res, domain, direct);
  2652. }
  2653. });
  2654. } else if (req.query.user && req.query.pass) {
  2655. // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
  2656. obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
  2657. // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
  2658. var user = obj.users[userid];
  2659. if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  2660. handleRootRequestEx(req, res, domain, direct);
  2661. } else if ((userid != null) && (err == null)) {
  2662. // Login success
  2663. parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
  2664. req.session.userid = userid;
  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 password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
  2669. handleRootRequestEx(req, res, domain, direct);
  2670. } else {
  2671. // Login failed
  2672. handleRootRequestEx(req, res, domain, direct);
  2673. }
  2674. });
  2675. } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
  2676. // Check if the loginToken is still valid
  2677. obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
  2678. if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
  2679. handleRootRequestEx(req, res, domain, direct); // Login using a different system
  2680. });
  2681. } else {
  2682. // Login using a different system
  2683. handleRootRequestEx(req, res, domain, direct);
  2684. }
  2685. }
  2686. function handleRootRequestEx(req, res, domain, direct) {
  2687. var nologout = false, user = null;
  2688. res.set({ 'Cache-Control': 'no-store' });
  2689. // Check if we have an incomplete domain name in the path
  2690. if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
  2691. parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
  2692. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2693. return;
  2694. }
  2695. if (obj.args.nousers == true) {
  2696. // If in single user mode, setup things here.
  2697. delete req.session.loginmode;
  2698. req.session.userid = 'user/' + domain.id + '/~';
  2699. delete req.session.currentNode;
  2700. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2701. setSessionRandom(req);
  2702. if (obj.users[req.session.userid] == null) {
  2703. // Create the dummy user ~ with impossible password
  2704. parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
  2705. obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
  2706. obj.db.SetUser(obj.users[req.session.userid]);
  2707. }
  2708. } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  2709. // If a default user is active, setup the session here.
  2710. parent.debug('web', 'handleRootRequestEx: auth using default user.');
  2711. delete req.session.loginmode;
  2712. req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
  2713. delete req.session.currentNode;
  2714. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2715. setSessionRandom(req);
  2716. } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
  2717. var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  2718. //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
  2719. if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
  2720. // If a login cookie was provided, setup the session here.
  2721. parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
  2722. delete req.session.loginmode;
  2723. req.session.userid = loginCookie.u;
  2724. delete req.session.currentNode;
  2725. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2726. setSessionRandom(req);
  2727. } else {
  2728. parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
  2729. }
  2730. } else if (domain.sspi != null) {
  2731. // SSPI login (Windows only)
  2732. //console.log(req.connection.user, req.connection.userSid);
  2733. if ((req.connection.user == null) || (req.connection.userSid == null)) {
  2734. parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
  2735. res.sendStatus(404); return;
  2736. } else {
  2737. nologout = true;
  2738. req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
  2739. req.session.usersid = req.connection.userSid;
  2740. req.session.usersGroups = req.connection.userGroups;
  2741. delete req.session.currentNode;
  2742. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  2743. setSessionRandom(req);
  2744. 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 });
  2745. // Check if this user exists, create it if not.
  2746. user = obj.users[req.session.userid];
  2747. if ((user == null) || (user.sid != req.session.usersid)) {
  2748. // Create the domain user
  2749. 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) };
  2750. if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
  2751. if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
  2752. for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
  2753. if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
  2754. // Auto-join any user groups
  2755. if (typeof domain.newaccountsusergroups == 'object') {
  2756. for (var i in domain.newaccountsusergroups) {
  2757. var ugrpid = domain.newaccountsusergroups[i];
  2758. if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
  2759. var ugroup = obj.userGroups[ugrpid];
  2760. if (ugroup != null) {
  2761. // Add group to the user
  2762. if (user2.links == null) { user2.links = {}; }
  2763. user2.links[ugroup._id] = { rights: 1 };
  2764. // Add user to the group
  2765. ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
  2766. db.Set(ugroup);
  2767. // Notify user group change
  2768. 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 };
  2769. 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.
  2770. parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
  2771. }
  2772. }
  2773. }
  2774. obj.users[req.session.userid] = user2;
  2775. obj.db.SetUser(user2);
  2776. 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 };
  2777. 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.
  2778. obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
  2779. parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
  2780. }
  2781. }
  2782. }
  2783. // Figure out the minimal password requirement
  2784. var passRequirements = null;
  2785. if (domain.passwordrequirements != null) {
  2786. if (domain.passrequirementstr == null) {
  2787. var passRequirements = {};
  2788. if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
  2789. if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
  2790. if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
  2791. if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
  2792. if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
  2793. if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
  2794. domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
  2795. }
  2796. passRequirements = domain.passwordrequirementsstr;
  2797. }
  2798. // If a user exists and is logged in, serve the default app, otherwise server the login app.
  2799. if (req.session && req.session.userid && obj.users[req.session.userid]) {
  2800. const user = obj.users[req.session.userid];
  2801. // Check if we are in maintenance mode
  2802. if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
  2803. req.session.messageid = 115; // Server under maintenance
  2804. req.session.loginmode = 1;
  2805. res.redirect(domain.url);
  2806. return;
  2807. }
  2808. // If the request has a "meshmessengerid", redirect to MeshMessenger
  2809. // This situation happens when you get a push notification for a chat session, but are not logged in.
  2810. if (req.query.meshmessengerid != null) {
  2811. res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''));
  2812. return;
  2813. }
  2814. const xdbGetFunc = function dbGetFunc(err, states) {
  2815. if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
  2816. parent.debug('web', 'handleRootRequestEx: incorrect domain.');
  2817. dbGetFunc.req.session = null;
  2818. dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
  2819. return;
  2820. }
  2821. // Check if this is a locked account
  2822. if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
  2823. // Locked account
  2824. parent.debug('web', 'handleRootRequestEx: locked account.');
  2825. delete dbGetFunc.req.session.userid;
  2826. delete dbGetFunc.req.session.currentNode;
  2827. delete dbGetFunc.req.session.passhint;
  2828. delete dbGetFunc.req.session.cuserid;
  2829. dbGetFunc.req.session.messageid = 110; // Account locked.
  2830. dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
  2831. return;
  2832. }
  2833. var viewmode = 1;
  2834. if (dbGetFunc.req.session.viewmode) {
  2835. viewmode = dbGetFunc.req.session.viewmode;
  2836. delete dbGetFunc.req.session.viewmode;
  2837. } else if (dbGetFunc.req.query.viewmode) {
  2838. viewmode = dbGetFunc.req.query.viewmode;
  2839. }
  2840. var currentNode = '';
  2841. if (dbGetFunc.req.session.currentNode) {
  2842. currentNode = dbGetFunc.req.session.currentNode;
  2843. delete dbGetFunc.req.session.currentNode;
  2844. } else if (dbGetFunc.req.query.node) {
  2845. currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
  2846. }
  2847. var logoutcontrols = {};
  2848. if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
  2849. // Give the web page a list of supported server features for this domain and user
  2850. const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
  2851. // Create a authentication cookie
  2852. const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
  2853. const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
  2854. // Send the main web application
  2855. var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
  2856. 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
  2857. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  2858. // Clean up the U2F challenge if needed
  2859. if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
  2860. if (dbGetFunc.req.session.e) {
  2861. const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
  2862. if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
  2863. }
  2864. // Intel AMT Scanning options
  2865. var amtscanoptions = '';
  2866. if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
  2867. else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
  2868. // Fetch the web state
  2869. parent.debug('web', 'handleRootRequestEx: success.');
  2870. var webstate = '{}';
  2871. if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
  2872. if ((webstate == '{}') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
  2873. if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
  2874. var webstate2 = {};
  2875. try { if (webstate != '{}') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
  2876. for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
  2877. webstate = JSON.stringify(webstate2);
  2878. }
  2879. // Custom user interface
  2880. var customui = '';
  2881. if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
  2882. // Custom files (CSS and JS)
  2883. var customFiles = '';
  2884. if (domain.customFiles != null) {
  2885. customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
  2886. } else if (domain.customfiles != null) {
  2887. customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
  2888. }
  2889. // Server features
  2890. var serverFeatures = 255;
  2891. if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
  2892. else if (typeof domain.myserver == 'object') {
  2893. if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
  2894. if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
  2895. if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
  2896. if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
  2897. if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
  2898. if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
  2899. if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
  2900. }
  2901. if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
  2902. if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
  2903. if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
  2904. }
  2905. // Get WebRTC configuration
  2906. var webRtcConfig = null;
  2907. 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'); }
  2908. else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
  2909. // Load default page style or new modern ui
  2910. var uiViewMode = 'default';
  2911. var webstateJSON = JSON.parse(webstate);
  2912. if (req.query.sitestyle != null) {
  2913. if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
  2914. } else if (webstateJSON && webstateJSON.uiViewMode == 3) {
  2915. uiViewMode = 'default3';
  2916. } else if (domain.sitestyle == 3) {
  2917. uiViewMode = 'default3';
  2918. }
  2919. // Refresh the session
  2920. render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
  2921. authCookie: authCookie,
  2922. authRelayCookie: authRelayCookie,
  2923. viewmode: viewmode,
  2924. currentNode: currentNode,
  2925. logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
  2926. domain: domain.id,
  2927. debuglevel: parent.debugLevel,
  2928. serverDnsName: obj.getWebServerName(domain, req),
  2929. serverRedirPort: args.redirport,
  2930. serverPublicPort: httpsPort,
  2931. serverfeatures: serverFeatures,
  2932. features: allFeatures.features,
  2933. features2: allFeatures.features2,
  2934. sessiontime: (args.sessiontime) ? args.sessiontime : 60,
  2935. mpspass: args.mpspass,
  2936. passRequirements: passRequirements,
  2937. customui: customui,
  2938. customFiles: customFiles,
  2939. webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
  2940. footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
  2941. 'serverversion': obj.parent.currentVer,
  2942. 'servername': obj.getWebServerName(domain, req),
  2943. 'agentsessions': Object.keys(parent.webserver.wsagents).length,
  2944. 'connectedusers': Object.keys(parent.webserver.wssessions).length,
  2945. 'userssessions': Object.keys(parent.webserver.wssessions2).length,
  2946. 'relaysessions': parent.webserver.relaySessionCount,
  2947. 'relaycount': Object.keys(parent.webserver.wsrelays).length
  2948. }),
  2949. webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
  2950. amtscanoptions: amtscanoptions,
  2951. pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
  2952. 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)),
  2953. webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
  2954. hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
  2955. showNotesPanel: (domain.shownotespanel ? 'true' : 'false'),
  2956. userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'),
  2957. webrtcconfig: webRtcConfig,
  2958. collapseGroups: (domain.collapsegroups ? 'true' : 'false')
  2959. }, dbGetFunc.req, domain, uiViewMode), user);
  2960. }
  2961. xdbGetFunc.req = req;
  2962. xdbGetFunc.res = res;
  2963. xdbGetFunc.user = user;
  2964. obj.db.Get('ws' + user._id, xdbGetFunc);
  2965. } else {
  2966. // Send back the login application
  2967. // If this is a 2 factor auth request, look for a hardware key challenge.
  2968. // Normal login 2 factor request
  2969. if (req.session && (req.session.loginmode == 4)) {
  2970. const sec = parent.decryptSessionData(req.session.e);
  2971. if ((sec != null) && (typeof sec.tuserid == 'string')) {
  2972. const user = obj.users[sec.tuserid];
  2973. if (user != null) {
  2974. parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
  2975. getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
  2976. return;
  2977. }
  2978. }
  2979. }
  2980. // Password recovery 2 factor request
  2981. if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
  2982. obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
  2983. if ((err != null) || (docs.length == 0)) {
  2984. parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
  2985. req.session = null;
  2986. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2987. } else {
  2988. var user = obj.users[docs[0]._id];
  2989. if (user != null) {
  2990. parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
  2991. getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
  2992. } else {
  2993. parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
  2994. req.session = null;
  2995. res.redirect(domain.url + getQueryPortion(req)); // BAD***
  2996. }
  2997. }
  2998. });
  2999. return;
  3000. }
  3001. handleRootRequestLogin(req, res, domain, '', passRequirements);
  3002. }
  3003. }
  3004. // Return a list of server supported features for a given domain and user
  3005. obj.getDomainUserFeatures = function (domain, user, req) {
  3006. var features = 0;
  3007. var features2 = 0;
  3008. if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
  3009. if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
  3010. if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
  3011. if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
  3012. if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
  3013. if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
  3014. if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
  3015. if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
  3016. // 0x00000100 --> This feature flag is free for future use.
  3017. if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
  3018. if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
  3019. if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
  3020. 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
  3021. if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
  3022. if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
  3023. if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
  3024. if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
  3025. if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
  3026. if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
  3027. // Check if we can skip 2nd factor auth because of the source IP address
  3028. var skip2factor = false;
  3029. if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
  3030. for (var i in domain.passwordrequirements.skip2factor) {
  3031. if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
  3032. }
  3033. }
  3034. if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
  3035. }
  3036. 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.
  3037. if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
  3038. if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
  3039. if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
  3040. if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
  3041. if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
  3042. if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
  3043. if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
  3044. if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
  3045. if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
  3046. if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
  3047. if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
  3048. if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
  3049. if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
  3050. if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
  3051. if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
  3052. if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
  3053. if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
  3054. if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
  3055. if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
  3056. if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || ((domain.passwordrequirements.logintokens.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => domain.passwordrequirements.logintokens.indexOf(key) >= 0)) )))) { features2 += 0x00000080; } // Indicates login tokens are allowed
  3057. if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
  3058. if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
  3059. if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
  3060. if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
  3061. if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
  3062. if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
  3063. if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
  3064. if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
  3065. if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
  3066. if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
  3067. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
  3068. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
  3069. if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
  3070. if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
  3071. if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
  3072. 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
  3073. 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
  3074. if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
  3075. if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
  3076. if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
  3077. if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
  3078. if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed
  3079. if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
  3080. if (domain.sitestyle === 3) { features2 |= 0x80000000; } // Indicates that Modern UI is forced (siteStyle = 3)
  3081. return { features: features, features2: features2 };
  3082. }
  3083. function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
  3084. parent.debug('web', 'handleRootRequestLogin()');
  3085. var features = 0;
  3086. 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
  3087. if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
  3088. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  3089. var loginmode = 0;
  3090. 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.
  3091. // Format an error message if needed
  3092. var passhint = null, msgid = 0;
  3093. if (req.session != null) {
  3094. msgid = req.session.messageid;
  3095. if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
  3096. delete req.session.messageid;
  3097. delete req.session.passhint;
  3098. }
  3099. const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
  3100. 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'))
  3101. // Check if we are allowed to create new users using the login screen
  3102. var newAccountsAllowed = true;
  3103. if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
  3104. if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
  3105. // Encrypt the hardware key challenge state if needed
  3106. var hwstate = null;
  3107. if (hardwareKeyChallenge && req.session) {
  3108. const sec = parent.decryptSessionData(req.session.e);
  3109. hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
  3110. }
  3111. // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
  3112. var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
  3113. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
  3114. var otpduo = (req.session != null) && (req.session.tduo === 1);
  3115. if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; }
  3116. var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
  3117. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
  3118. var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
  3119. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
  3120. var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
  3121. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
  3122. const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
  3123. // See if we support two-factor trusted cookies
  3124. var twoFactorCookieDays = 30;
  3125. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  3126. // See what authentication strategies we have
  3127. var authStrategies = [];
  3128. if (typeof domain.authstrategies == 'object') {
  3129. if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
  3130. if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
  3131. if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
  3132. if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
  3133. if (typeof domain.authstrategies.oidc == 'object') {
  3134. if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
  3135. authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
  3136. } else {
  3137. authStrategies.push('oidc');
  3138. }
  3139. }
  3140. if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
  3141. if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
  3142. if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
  3143. }
  3144. // Custom user interface
  3145. var customui = '';
  3146. if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
  3147. // Custom files (CSS and JS)
  3148. var customFiles = '';
  3149. if (domain.customFiles != null) {
  3150. customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
  3151. } else if (domain.customfiles != null) {
  3152. customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
  3153. }
  3154. // Get two-factor screen timeout
  3155. var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
  3156. if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
  3157. twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
  3158. }
  3159. // Setup CAPTCHA if needed
  3160. var newAccountCaptcha = '', newAccountCaptchaImage = '';
  3161. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  3162. newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
  3163. newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
  3164. }
  3165. // Check for flash errors from passport.js and make the array unique
  3166. var flashErrors = [];
  3167. if (req.session.flash && req.session.flash.error) {
  3168. flashErrors = obj.common.uniqueArray(req.session.flash.error);
  3169. req.session.flash = null;
  3170. }
  3171. // Render the login page
  3172. render(req, res,
  3173. getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain),
  3174. getRenderArgs({
  3175. loginmode: loginmode,
  3176. rootCertLink: getRootCertLink(domain),
  3177. newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
  3178. newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
  3179. newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
  3180. newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
  3181. serverDnsName: obj.getWebServerName(domain, req),
  3182. serverPublicPort: httpsPort,
  3183. passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
  3184. emailcheck: emailcheck,
  3185. features: features,
  3186. sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
  3187. passRequirements: passRequirements,
  3188. customui: customui,
  3189. customFiles: customFiles,
  3190. footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, {
  3191. 'serverversion': obj.parent.currentVer,
  3192. 'servername': obj.getWebServerName(domain, req),
  3193. 'agentsessions': Object.keys(parent.webserver.wsagents).length,
  3194. 'connectedusers': Object.keys(parent.webserver.wssessions).length,
  3195. 'userssessions': Object.keys(parent.webserver.wssessions2).length,
  3196. 'relaysessions': parent.webserver.relaySessionCount,
  3197. 'relaycount': Object.keys(parent.webserver.wsrelays).length
  3198. }),
  3199. hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
  3200. messageid: msgid,
  3201. flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
  3202. passhint: passhint,
  3203. welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
  3204. 'serverversion': obj.parent.currentVer,
  3205. 'servername': obj.getWebServerName(domain, req),
  3206. 'agentsessions': Object.keys(parent.webserver.wsagents).length,
  3207. 'connectedusers': Object.keys(parent.webserver.wssessions).length,
  3208. 'userssessions': Object.keys(parent.webserver.wssessions2).length,
  3209. 'relaysessions': parent.webserver.relaySessionCount,
  3210. 'relaycount': Object.keys(parent.webserver.wsrelays).length
  3211. })).split('\'').join('\\\'') : null,
  3212. welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
  3213. hwstate: hwstate,
  3214. otpemail: otpemail,
  3215. otpduo: otpduo,
  3216. otpsms: otpsms,
  3217. otpmsg: otpmsg,
  3218. otppush: otppush,
  3219. autofido: autofido,
  3220. twoFactorCookieDays: twoFactorCookieDays,
  3221. authStrategies: authStrategies.join(','),
  3222. loginpicture: (typeof domain.loginpicture == 'string'),
  3223. tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
  3224. renderLanguages: obj.renderLanguages,
  3225. showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
  3226. }, req, domain, (domain.sitestyle >= 2) ? 'login2' : 'login'));
  3227. }
  3228. // Handle a post request on the root
  3229. function handleRootPostRequest(req, res) {
  3230. const domain = checkUserIpAddress(req, res);
  3231. if (domain == null) { return; }
  3232. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
  3233. if (req.body == null) { req.body = {}; }
  3234. parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
  3235. // If a HTTP header is required, check new UserRequiredHttpHeader
  3236. 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; } }
  3237. switch (req.body.action) {
  3238. case 'login': { handleLoginRequest(req, res, true); break; }
  3239. case 'tokenlogin': {
  3240. if (req.body.hwstate) {
  3241. var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
  3242. if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
  3243. }
  3244. handleLoginRequest(req, res, true); break;
  3245. }
  3246. case 'pushlogin': {
  3247. if (req.body.hwstate) {
  3248. var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
  3249. if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
  3250. // Push authentication is a success, login the user
  3251. req.session = { userid: cookie.u };
  3252. // Check if we need to remember this device
  3253. if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
  3254. var maxCookieAge = domain.twofactorcookiedurationdays;
  3255. if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
  3256. const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
  3257. res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
  3258. }
  3259. var user = obj.users[cookie.u];
  3260. // Notify account login
  3261. var targets = ['*', 'server-users', user._id];
  3262. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  3263. const ua = obj.getUserAgentInfo(req);
  3264. 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: 'pushlogin' };
  3265. obj.parent.DispatchEvent(targets, obj, loginEvent);
  3266. handleRootRequestEx(req, res, domain);
  3267. return;
  3268. }
  3269. }
  3270. handleLoginRequest(req, res, true); break;
  3271. }
  3272. case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
  3273. case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
  3274. case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
  3275. case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
  3276. case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
  3277. case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
  3278. default: { handleLoginRequest(req, res, true); break; }
  3279. }
  3280. }
  3281. // Return true if it looks like we are using a real TLS certificate.
  3282. obj.isTrustedCert = function (domain) {
  3283. if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
  3284. if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
  3285. if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
  3286. 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.
  3287. if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
  3288. if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
  3289. return true; // This is a guess
  3290. }
  3291. // Get the link to the root certificate if needed
  3292. function getRootCertLink(domain) {
  3293. // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
  3294. if (obj.isTrustedCert(domain) == false) {
  3295. // Get the domain suffix
  3296. var xdomain = (domain.dns == null) ? domain.id : '';
  3297. if (xdomain != '') xdomain += '/';
  3298. return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
  3299. }
  3300. return '';
  3301. }
  3302. // Serve the xterm page
  3303. function handleXTermRequest(req, res) {
  3304. const domain = checkUserIpAddress(req, res);
  3305. if (domain == null) { return; }
  3306. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3307. parent.debug('web', 'handleXTermRequest: sending xterm');
  3308. res.set({ 'Cache-Control': 'no-store' });
  3309. if (req.session && req.session.userid) {
  3310. if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
  3311. var user = obj.users[req.session.userid];
  3312. if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
  3313. // Check permissions
  3314. obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
  3315. if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
  3316. var logoutcontrols = { name: user.name };
  3317. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3318. 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
  3319. // Create a authentication cookie
  3320. const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
  3321. const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
  3322. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  3323. 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));
  3324. });
  3325. } else {
  3326. res.redirect(domain.url + getQueryPortion(req));
  3327. return;
  3328. }
  3329. }
  3330. // Handle new account Captcha GET
  3331. function handleNewAccountCaptchaRequest(req, res) {
  3332. const domain = checkUserIpAddress(req, res);
  3333. if (domain == null) { return; }
  3334. if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
  3335. const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
  3336. if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
  3337. res.type('svg');
  3338. res.status(200).end(require('svg-captcha')(c.captcha, {}));
  3339. }
  3340. // Handle Captcha GET
  3341. function handleCaptchaGetRequest(req, res) {
  3342. const domain = checkUserIpAddress(req, res);
  3343. if (domain == null) { return; }
  3344. if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
  3345. parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
  3346. }
  3347. // Handle Captcha POST
  3348. function handleCaptchaPostRequest(req, res) {
  3349. if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
  3350. const domain = checkUserIpAddress(req, res);
  3351. if (domain == null) { return; }
  3352. req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
  3353. parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
  3354. }
  3355. // Render the terms of service.
  3356. function handleTermsRequest(req, res) {
  3357. const domain = checkUserIpAddress(req, res);
  3358. if (domain == null) { return; }
  3359. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3360. // See if term.txt was loaded from the database
  3361. if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
  3362. // Send the terms from the database
  3363. res.set({ 'Cache-Control': 'no-store' });
  3364. if (req.session && req.session.userid) {
  3365. 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
  3366. var user = obj.users[req.session.userid];
  3367. var logoutcontrols = { name: user.name };
  3368. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3369. 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
  3370. 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));
  3371. } else {
  3372. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
  3373. }
  3374. } else {
  3375. // See if there is a terms.txt file in meshcentral-data
  3376. var p = obj.path.join(obj.parent.datapath, 'terms.txt');
  3377. if (obj.fs.existsSync(p)) {
  3378. obj.fs.readFile(p, 'utf8', function (err, data) {
  3379. if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
  3380. // Send the terms from terms.txt
  3381. res.set({ 'Cache-Control': 'no-store' });
  3382. if (req.session && req.session.userid) {
  3383. 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
  3384. var user = obj.users[req.session.userid];
  3385. var logoutcontrols = { name: user.name };
  3386. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3387. 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
  3388. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
  3389. } else {
  3390. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
  3391. }
  3392. });
  3393. } else {
  3394. // Send the default terms
  3395. parent.debug('web', 'handleTermsRequest: sending default terms');
  3396. res.set({ 'Cache-Control': 'no-store' });
  3397. if (req.session && req.session.userid) {
  3398. 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
  3399. var user = obj.users[req.session.userid];
  3400. var logoutcontrols = { name: user.name };
  3401. var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
  3402. 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
  3403. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
  3404. } else {
  3405. render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
  3406. }
  3407. }
  3408. }
  3409. }
  3410. // Render the messenger application.
  3411. function handleMessengerRequest(req, res) {
  3412. const domain = getDomain(req);
  3413. if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
  3414. parent.debug('web', 'handleMessengerRequest()');
  3415. // Check if we are in maintenance mode
  3416. if (parent.config.settings.maintenancemode != null) {
  3417. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
  3418. return;
  3419. }
  3420. // Check if this session is for a user
  3421. if (req.query.id == null) { res.sendStatus(404); return; }
  3422. var idSplit = decodeURIComponent(req.query.id).split('/');
  3423. if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
  3424. if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
  3425. // This is a user to user conversation, both users must be logged in.
  3426. var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
  3427. var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
  3428. if (!req.session || !req.session.userid) {
  3429. // Redirect to login page
  3430. 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)); }
  3431. return;
  3432. }
  3433. if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
  3434. }
  3435. // Get WebRTC configuration
  3436. var webRtcConfig = null;
  3437. 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'); }
  3438. else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
  3439. // Setup other options
  3440. var options = { webrtcconfig: webRtcConfig };
  3441. if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
  3442. // Get the userid and name
  3443. if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
  3444. if (idSplit.length == 7) {
  3445. const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
  3446. if (user != null) {
  3447. if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
  3448. if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
  3449. }
  3450. }
  3451. }
  3452. // Render the page
  3453. res.set({ 'Cache-Control': 'no-store' });
  3454. render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
  3455. }
  3456. // Handle messenger image request
  3457. function handleMessengerImageRequest(req, res) {
  3458. const domain = getDomain(req);
  3459. if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
  3460. parent.debug('web', 'handleMessengerImageRequest()');
  3461. // Check if we are in maintenance mode
  3462. if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
  3463. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3464. if (domain.meshmessengerpicture) {
  3465. // Use the configured messenger logo picture
  3466. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
  3467. }
  3468. var imagefile = 'images/messenger.png';
  3469. if (domain.webpublicpath != null) {
  3470. obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
  3471. if (exists) {
  3472. // Use the domain logo picture
  3473. try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3474. } else {
  3475. // Use the default logo picture
  3476. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3477. }
  3478. });
  3479. } else if (parent.webPublicOverridePath) {
  3480. obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
  3481. if (exists) {
  3482. // Use the override logo picture
  3483. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3484. } else {
  3485. // Use the default logo picture
  3486. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3487. }
  3488. });
  3489. } else {
  3490. // Use the default logo picture
  3491. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3492. }
  3493. }
  3494. // Returns the server root certificate encoded in base64
  3495. function getRootCertBase64() {
  3496. var rootcert = obj.certificates.root.cert;
  3497. var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
  3498. if (i >= 0) { rootcert = rootcert.substring(i + 29); }
  3499. i = rootcert.indexOf('-----END CERTIFICATE-----');
  3500. if (i >= 0) { rootcert = rootcert.substring(i, 0); }
  3501. return Buffer.from(rootcert, 'base64').toString('base64');
  3502. }
  3503. // Returns the mesh server root certificate
  3504. function handleRootCertRequest(req, res) {
  3505. const domain = getDomain(req);
  3506. if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
  3507. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3508. if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
  3509. parent.debug('web', 'handleRootCertRequest()');
  3510. setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
  3511. res.send(Buffer.from(getRootCertBase64(), 'base64'));
  3512. }
  3513. // Return a customised mainifest.json for PWA
  3514. function handleManifestRequest(req, res){
  3515. const domain = checkUserIpAddress(req);
  3516. if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; }
  3517. parent.debug('web', 'handleManifestRequest()');
  3518. var manifest = {
  3519. "name": (domain.title != null) ? domain.title : 'MeshCentral',
  3520. "short_name": (domain.title != null) ? domain.title : 'MeshCentral',
  3521. "description": "Open source web based, remote computer management.",
  3522. "scope": ".",
  3523. "start_url": "/",
  3524. "display": "fullscreen",
  3525. "orientation": "any",
  3526. "theme_color": "#ffffff",
  3527. "background_color": "#ffffff",
  3528. "icons": [{
  3529. "src": "pwalogo.png",
  3530. "sizes": "512x512",
  3531. "type": "image/png"
  3532. }]
  3533. };
  3534. res.json(manifest);
  3535. }
  3536. // Handle user public file downloads
  3537. function handleDownloadUserFiles(req, res) {
  3538. const domain = checkUserIpAddress(req, res);
  3539. if (domain == null) { return; }
  3540. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3541. if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
  3542. var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
  3543. if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns
  3544. if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
  3545. if (domain.id != '') { domainname = 'domain-' + domain.id; }
  3546. var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
  3547. 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; } }
  3548. var stat = null;
  3549. try { stat = obj.fs.statSync(path); } catch (e) { }
  3550. if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
  3551. if (req.query.download == 1) {
  3552. setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
  3553. try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
  3554. } else {
  3555. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
  3556. }
  3557. } else {
  3558. render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
  3559. }
  3560. }
  3561. // Handle device file request
  3562. function handleDeviceFile(req, res) {
  3563. const domain = getDomain(req, res);
  3564. if (domain == null) { return; }
  3565. if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
  3566. // Check the inbound desktop sharing cookie
  3567. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  3568. if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
  3569. // Check userid
  3570. const user = obj.users[c.userid];
  3571. if ((c == user)) { res.sendStatus(404); return; }
  3572. // If this cookie has restricted usages, check that it's allowed to perform downloads
  3573. if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
  3574. if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
  3575. if (req.query.n == null) { res.sendStatus(404); return; }
  3576. // Check if this user has permission to manage this computer
  3577. obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
  3578. if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  3579. // All good, start the file transfer
  3580. req.query.id = getRandomLowerCase(12);
  3581. obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
  3582. });
  3583. }
  3584. // Handle download of a server file by an agent
  3585. function handleAgentDownloadFile(req, res) {
  3586. const domain = checkAgentIpAddress(req, res);
  3587. if (domain == null) { return; }
  3588. if (req.query.c == null) { res.sendStatus(404); return; }
  3589. // Check the inbound desktop sharing cookie
  3590. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
  3591. 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; }
  3592. // Send the file back
  3593. try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
  3594. }
  3595. // Handle logo request
  3596. function handleLogoRequest(req, res) {
  3597. const domain = checkUserIpAddress(req, res);
  3598. if (domain == null) { return; }
  3599. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3600. if (domain.titlepicture) {
  3601. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
  3602. // Use the logo in the database
  3603. res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3604. res.send(parent.configurationFiles[domain.titlepicture]);
  3605. return;
  3606. } else {
  3607. // Use the logo on file
  3608. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
  3609. }
  3610. }
  3611. if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
  3612. // Use the domain logo picture
  3613. try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3614. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
  3615. // Use the override logo picture
  3616. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3617. } else {
  3618. // Use the default logo picture
  3619. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
  3620. }
  3621. }
  3622. // Handle login logo request
  3623. function handleLoginLogoRequest(req, res) {
  3624. const domain = checkUserIpAddress(req, res);
  3625. if (domain == null) { return; }
  3626. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3627. if (domain.loginpicture) {
  3628. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
  3629. // Use the logo in the database
  3630. res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3631. res.send(parent.configurationFiles[domain.loginpicture]);
  3632. return;
  3633. } else {
  3634. // Use the logo on file
  3635. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
  3636. }
  3637. } else {
  3638. res.sendStatus(404);
  3639. }
  3640. }
  3641. // Handle PWA logo request
  3642. function handlePWALogoRequest(req, res) {
  3643. const domain = checkUserIpAddress(req, res);
  3644. if (domain == null) { return; }
  3645. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3646. if (domain.pwalogo) {
  3647. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) {
  3648. // Use the logo in the database
  3649. res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3650. res.send(parent.configurationFiles[domain.pwalogo]);
  3651. return;
  3652. } else {
  3653. // Use the logo on file
  3654. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
  3655. }
  3656. }
  3657. if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) {
  3658. // Use the domain logo picture
  3659. try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3660. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) {
  3661. // Use the override logo picture
  3662. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3663. } else {
  3664. // Use the default logo picture
  3665. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
  3666. }
  3667. }
  3668. // Handle translation request
  3669. function handleTranslationsRequest(req, res) {
  3670. const domain = checkUserIpAddress(req, res);
  3671. if (domain == null) { return; }
  3672. //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  3673. if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
  3674. var user = null;
  3675. if (obj.args.user != null) {
  3676. // A default user is active
  3677. user = obj.users['user/' + domain.id + '/' + obj.args.user];
  3678. if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
  3679. } else {
  3680. // Check if the user is logged and we have all required parameters
  3681. if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
  3682. // Get the current user
  3683. user = obj.users[req.session.userid];
  3684. if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
  3685. if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
  3686. }
  3687. var data = '';
  3688. req.setEncoding('utf8');
  3689. req.on('data', function (chunk) { data += chunk; });
  3690. req.on('end', function () {
  3691. try { data = JSON.parse(data); } catch (ex) { data = null; }
  3692. if (data == null) { res.sendStatus(404); return; }
  3693. if (data.action == 'getTranslations') {
  3694. if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
  3695. // Return the translation file (JSON)
  3696. try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
  3697. } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
  3698. // Return the default translation file (JSON)
  3699. try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
  3700. } else { res.sendStatus(404); }
  3701. } else if (data.action == 'setTranslations') {
  3702. 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 })); } });
  3703. } else if (data.action == 'translateServer') {
  3704. if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
  3705. const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
  3706. if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
  3707. var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
  3708. if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
  3709. if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
  3710. res.send(JSON.stringify({ response: 'ok' }));
  3711. console.log('Started server translation...');
  3712. obj.pendingTranslation = true;
  3713. require('child_process').exec(process.argv[0] + ' translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 300000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
  3714. delete obj.pendingTranslation;
  3715. if (error) { console.log('Server translation error', error); }
  3716. // console.log('stdout', stdout);
  3717. if (stderr) { console.log('Server translation stderr', stderr); }
  3718. //console.log('Server restart...'); // Perform a server restart
  3719. //process.exit(0);
  3720. console.log('Server translation completed.');
  3721. });
  3722. } else {
  3723. // Unknown request
  3724. res.sendStatus(404);
  3725. }
  3726. });
  3727. }
  3728. // Handle welcome image request
  3729. function handleWelcomeImageRequest(req, res) {
  3730. const domain = checkUserIpAddress(req, res);
  3731. if (domain == null) { return; }
  3732. //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
  3733. if (domain.welcomepicture) {
  3734. if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
  3735. // Use the welcome image in the database
  3736. res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
  3737. res.send(parent.configurationFiles[domain.welcomepicture]);
  3738. return;
  3739. }
  3740. // Use the configured logo picture
  3741. try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
  3742. }
  3743. var imagefile = 'images/mainwelcome.jpg';
  3744. if (domain.sitestyle >= 2) { imagefile = 'images/login/back.png'; }
  3745. if (domain.webpublicpath != null) {
  3746. obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
  3747. if (exists) {
  3748. // Use the domain logo picture
  3749. try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3750. } else {
  3751. // Use the default logo picture
  3752. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3753. }
  3754. });
  3755. } else if (parent.webPublicOverridePath) {
  3756. obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
  3757. if (exists) {
  3758. // Use the override logo picture
  3759. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3760. } else {
  3761. // Use the default logo picture
  3762. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3763. }
  3764. });
  3765. } else {
  3766. // Use the default logo picture
  3767. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
  3768. }
  3769. }
  3770. // Download a session recording
  3771. function handleGetRecordings(req, res) {
  3772. const domain = checkUserIpAddress(req, res);
  3773. if (domain == null) return;
  3774. // Check the query
  3775. if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (!req.query.file.endsWith('.mcrec') && !req.query.file.endsWith('.txt'))) { res.sendStatus(401); return; }
  3776. // Get the recording path
  3777. var recordingsPath = null;
  3778. if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
  3779. if (recordingsPath == null) { res.sendStatus(401); return; }
  3780. // Get the user and check user rights
  3781. var authUserid = null;
  3782. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3783. if (authUserid == null) { res.sendStatus(401); return; }
  3784. const user = obj.users[authUserid];
  3785. if (user == null) { res.sendStatus(401); return; }
  3786. if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
  3787. // Send the recorded file
  3788. setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
  3789. try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
  3790. }
  3791. // Stream a session recording
  3792. function handleGetRecordingsWebSocket(ws, req) {
  3793. var domain = checkAgentIpAddress(ws, req);
  3794. 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; }
  3795. // Check the query
  3796. 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; }
  3797. // Get the recording path
  3798. var recordingsPath = null;
  3799. if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
  3800. if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
  3801. // Get the user and check user rights
  3802. var authUserid = null;
  3803. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  3804. if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
  3805. const user = obj.users[authUserid];
  3806. if (user == null) { try { ws.close(); } catch (ex) { } return; }
  3807. if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
  3808. const filefullpath = obj.path.join(recordingsPath, req.query.file);
  3809. obj.fs.stat(filefullpath, function (err, stats) {
  3810. if (err) {
  3811. try { ws.close(); } catch (ex) { } // File does not exist
  3812. } else {
  3813. obj.fs.open(filefullpath, 'r', function (err, fd) {
  3814. if (err == null) {
  3815. // When data is received from the web socket
  3816. ws.on('message', function (msg) {
  3817. if (typeof msg != 'string') return;
  3818. var command;
  3819. try { command = JSON.parse(msg); } catch (e) { return; }
  3820. if ((command == null) || (typeof command.action != 'string')) return;
  3821. switch (command.action) {
  3822. case 'get': {
  3823. const buffer = Buffer.alloc(8 + command.size);
  3824. //buffer.writeUInt32BE((command.ptr >> 32), 0);
  3825. buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
  3826. 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); });
  3827. break;
  3828. }
  3829. }
  3830. });
  3831. // If error, do nothing
  3832. ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
  3833. // If the web socket is closed
  3834. ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
  3835. ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
  3836. } else {
  3837. try { ws.close(); } catch (ex) { }
  3838. }
  3839. });
  3840. }
  3841. });
  3842. }
  3843. // Serve the player page
  3844. function handlePlayerRequest(req, res) {
  3845. const domain = checkUserIpAddress(req, res);
  3846. if (domain == null) { return; }
  3847. parent.debug('web', 'handlePlayerRequest: sending player');
  3848. res.set({ 'Cache-Control': 'no-store' });
  3849. render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
  3850. }
  3851. // Serve the guest sharing page
  3852. function handleSharingRequest(req, res) {
  3853. const domain = getDomain(req, res);
  3854. if (domain == null) { return; }
  3855. if (req.query.c == null) { res.sendStatus(404); return; }
  3856. if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
  3857. // Check the inbound guest sharing cookie
  3858. var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
  3859. if (c == null) { res.sendStatus(404); return; }
  3860. if (c.a === 5) {
  3861. // This is the older style sharing cookie with everything encoded within it.
  3862. // This cookie style gives a very large URL, so it's not used anymore.
  3863. 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; }
  3864. handleSharingRequestEx(req, res, domain, c);
  3865. return;
  3866. }
  3867. if (c.a === 6) {
  3868. // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
  3869. // Gives a much more compact URL.
  3870. if (typeof c.pid != 'string') { res.sendStatus(404); return; }
  3871. // Check the expired time, expire message.
  3872. if ((c.e != null) && (c.e <= Date.now())) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3873. obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
  3874. if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
  3875. const doc = docs[0];
  3876. // If this is a recurrent share, check if we are at the correct time to make use of it
  3877. if (typeof doc.recurring == 'number') {
  3878. const now = Date.now();
  3879. if (now >= doc.startTime) { // We don't want to move the validity window before the start time
  3880. const deltaTime = (now - doc.startTime);
  3881. if (doc.recurring === 1) {
  3882. // This moves the start time to the next valid daily window
  3883. const oneDay = (24 * 60 * 60 * 1000);
  3884. var addition = Math.floor(deltaTime / oneDay);
  3885. 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.
  3886. doc.startTime += (addition * oneDay);
  3887. } else if (doc.recurring === 2) {
  3888. // This moves the start time to the next valid weekly window
  3889. const oneWeek = (7 * 24 * 60 * 60 * 1000);
  3890. var addition = Math.floor(deltaTime / oneWeek);
  3891. 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.
  3892. doc.startTime += (addition * oneWeek);
  3893. }
  3894. }
  3895. }
  3896. // Generate an old style cookie from the information in the database
  3897. 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 };
  3898. if (doc.userid) { cookie.uid = doc.userid; }
  3899. if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
  3900. if (doc.startTime != null) {
  3901. if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
  3902. else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
  3903. }
  3904. if (doc.viewOnly === true) { cookie.vo = 1; }
  3905. handleSharingRequestEx(req, res, domain, cookie);
  3906. });
  3907. return;
  3908. }
  3909. res.sendStatus(404); return;
  3910. }
  3911. // Serve the guest sharing page
  3912. function handleSharingRequestEx(req, res, domain, c) {
  3913. // Check the expired time, expire message.
  3914. if ((c.expire != null) && (c.expire <= Date.now())) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3915. // Check the public id
  3916. obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
  3917. // Check if any sharing links are present, expire message.
  3918. if ((err != null) || (docs.length == 0)) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3919. // Search for the device share public identifier, expire message.
  3920. var found = false;
  3921. 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; } }
  3922. if (found == false) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3923. // Get information about this node
  3924. obj.db.Get(c.nid, function (err, nodes) {
  3925. if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
  3926. var node = nodes[0];
  3927. // Check the start time, not yet valid message.
  3928. if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
  3929. // If this is a web relay share, check if this feature is active
  3930. if ((c.p == 8) || (c.p == 16)) {
  3931. // This is a HTTP or HTTPS share
  3932. 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));
  3933. if (webRelayPort == 0) { res.sendStatus(404); return; }
  3934. // Create the authentication cookie
  3935. 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 };
  3936. if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
  3937. const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
  3938. // Redirect to a URL
  3939. var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
  3940. var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
  3941. if (c.addr != null) { url += '&addr=' + c.addr; }
  3942. if (c.pid != null) { url += '&relayid=' + c.pid; }
  3943. parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
  3944. res.redirect(url);
  3945. } else {
  3946. // Looks good, let's create the outbound session cookies.
  3947. // This is a desktop, terminal or files share. We need to display the sharing page.
  3948. // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
  3949. 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 };
  3950. if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
  3951. if (c.k != null) { authCookieData.k = c.k; }
  3952. const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
  3953. // Server features
  3954. var features2 = 0;
  3955. if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
  3956. // Lets respond by sending out the desktop viewer.
  3957. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  3958. parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
  3959. res.set({ 'Cache-Control': 'no-store' });
  3960. 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));
  3961. }
  3962. });
  3963. });
  3964. }
  3965. // Handle domain redirection
  3966. obj.handleDomainRedirect = function (req, res) {
  3967. const domain = checkUserIpAddress(req, res);
  3968. if (domain == null) { return; }
  3969. if (domain.redirects == null) { res.sendStatus(404); return; }
  3970. var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
  3971. if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
  3972. if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
  3973. if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
  3974. if (domain.redirects[urlName] == '~showversion') {
  3975. // Show the current version
  3976. res.end('MeshCentral v' + obj.parent.currentVer);
  3977. } else {
  3978. // Perform redirection
  3979. res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
  3980. }
  3981. }
  3982. // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
  3983. obj.getServerFilePath = function (user, domain, path) {
  3984. var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
  3985. if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
  3986. var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
  3987. if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
  3988. if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
  3989. if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
  3990. serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
  3991. 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
  3992. return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
  3993. };
  3994. // Return the maximum number of bytes allowed in the user account "My Files".
  3995. obj.getQuota = function (objid, domain) {
  3996. if (objid == null) return 0;
  3997. if (objid.startsWith('user/')) {
  3998. var user = obj.users[objid];
  3999. if (user == null) return 0;
  4000. if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
  4001. if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
  4002. if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
  4003. return null; // By default, the user will have no limit
  4004. } else if (objid.startsWith('mesh/')) {
  4005. var mesh = obj.meshes[objid];
  4006. if (mesh == null) return 0;
  4007. if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
  4008. if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
  4009. return null; // By default, the mesh will have no limit
  4010. }
  4011. return 0;
  4012. };
  4013. // Download a file from the server
  4014. function handleDownloadFile(req, res) {
  4015. const domain = checkUserIpAddress(req, res);
  4016. if (domain == null) { return; }
  4017. if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
  4018. const user = obj.users[req.session.userid];
  4019. if (user == null) { res.sendStatus(404); return; }
  4020. const file = obj.getServerFilePath(user, domain, req.query.link);
  4021. if (file == null) { res.sendStatus(404); return; }
  4022. setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
  4023. obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
  4024. }
  4025. // Download the MeshCommander web page
  4026. function handleMeshCommander(req, res) {
  4027. const domain = checkUserIpAddress(req, res);
  4028. if (domain == null) { return; }
  4029. if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
  4030. // Find the correct MeshCommander language to send
  4031. const acceptableLanguages = obj.getLanguageCodes(req);
  4032. 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' };
  4033. for (var i in acceptableLanguages) {
  4034. const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]];
  4035. if (meshCommanderLanguage != null) {
  4036. try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { }
  4037. return;
  4038. }
  4039. }
  4040. // Send out the default english MeshCommander
  4041. try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
  4042. }
  4043. // Upload a MeshCore.js file to the server
  4044. function handleUploadMeshCoreFile(req, res) {
  4045. const domain = checkUserIpAddress(req, res);
  4046. if (domain == null) { return; }
  4047. if (domain.id !== '') { res.sendStatus(401); return; }
  4048. var authUserid = null;
  4049. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4050. const multiparty = require('multiparty');
  4051. const form = new multiparty.Form();
  4052. form.parse(req, function (err, fields, files) {
  4053. // If an authentication cookie is embedded in the form, use that.
  4054. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4055. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4056. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4057. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4058. }
  4059. if (authUserid == null) { res.sendStatus(401); return; }
  4060. if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
  4061. // Get the user
  4062. const user = obj.users[authUserid];
  4063. if (user == null) { res.sendStatus(401); return; } // Check this user exists
  4064. // Get the node and check node rights
  4065. const nodeid = fields.attrib[0];
  4066. obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
  4067. if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  4068. files.files.forEach(function (file) {
  4069. obj.fs.readFile(file.path, 'utf8', function (err, data) {
  4070. if (err != null) return;
  4071. data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
  4072. obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
  4073. try { obj.fs.unlinkSync(file.path); } catch (e) { }
  4074. });
  4075. });
  4076. res.send('');
  4077. });
  4078. });
  4079. }
  4080. // Upload a MeshCore.js file to the server
  4081. function handleOneClickRecoveryFile(req, res) {
  4082. const domain = checkUserIpAddress(req, res);
  4083. if (domain == null) { return; }
  4084. if (domain.id !== '') { res.sendStatus(401); return; }
  4085. var authUserid = null;
  4086. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4087. const multiparty = require('multiparty');
  4088. const form = new multiparty.Form();
  4089. form.parse(req, function (err, fields, files) {
  4090. // If an authentication cookie is embedded in the form, use that.
  4091. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4092. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4093. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4094. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4095. }
  4096. if (authUserid == null) { res.sendStatus(401); return; }
  4097. if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
  4098. // Get the user
  4099. const user = obj.users[authUserid];
  4100. if (user == null) { res.sendStatus(401); return; } // Check this user exists
  4101. // Get the node and check node rights
  4102. const nodeid = fields.attrib[0];
  4103. obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
  4104. if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
  4105. files.files.forEach(function (file) {
  4106. // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
  4107. parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
  4108. //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
  4109. });
  4110. res.send('');
  4111. });
  4112. });
  4113. }
  4114. // Upload a file to the server
  4115. function handleUploadFile(req, res) {
  4116. const domain = checkUserIpAddress(req, res);
  4117. if (domain == null) { return; }
  4118. if (domain.userQuota == -1) { res.sendStatus(401); return; }
  4119. var authUserid = null;
  4120. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4121. const multiparty = require('multiparty');
  4122. const form = new multiparty.Form();
  4123. form.parse(req, function (err, fields, files) {
  4124. // If an authentication cookie is embedded in the form, use that.
  4125. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4126. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4127. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4128. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4129. }
  4130. if (authUserid == null) { res.sendStatus(401); return; }
  4131. // Get the user
  4132. const user = obj.users[authUserid];
  4133. if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
  4134. if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
  4135. var xfile = null;
  4136. try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
  4137. if (xfile == null) { res.sendStatus(404); return; }
  4138. // Get total bytes in the path
  4139. var totalsize = readTotalFileSize(xfile.fullpath);
  4140. if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
  4141. if (fields.name != null) {
  4142. // See if we need to create the folder
  4143. var domainx = 'domain';
  4144. if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
  4145. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
  4146. try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
  4147. try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
  4148. // Upload method where all the file data is within the fields.
  4149. var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
  4150. if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
  4151. for (var i = 0; i < names.length; i++) {
  4152. if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
  4153. var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
  4154. if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
  4155. // Create the user folder if needed
  4156. (function (fullpath, filename, filedata) {
  4157. obj.fs.mkdir(xfile.fullpath, function () {
  4158. // Write the file
  4159. obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
  4160. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4161. });
  4162. });
  4163. })(xfile.fullpath, names[i], filedata);
  4164. } else {
  4165. // Send a notification
  4166. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
  4167. }
  4168. }
  4169. }
  4170. } else {
  4171. // More typical upload method, the file data is in a multipart mime post.
  4172. files.files.forEach(function (file) {
  4173. var fpath = obj.path.join(xfile.fullpath, file.originalFilename);
  4174. 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
  4175. // See if we need to create the folder
  4176. var domainx = 'domain';
  4177. if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
  4178. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
  4179. try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
  4180. try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
  4181. // Rename the file
  4182. obj.fs.rename(file.path, fpath, function (err) {
  4183. if (err && (err.code === 'EXDEV')) {
  4184. // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
  4185. obj.common.copyFile(file.path, fpath, function (err) {
  4186. obj.fs.unlink(file.path, function (err) {
  4187. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4188. });
  4189. });
  4190. } else {
  4191. obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
  4192. }
  4193. });
  4194. } else {
  4195. // Send a notification
  4196. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
  4197. try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
  4198. }
  4199. });
  4200. }
  4201. } else {
  4202. // Send a notification
  4203. obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
  4204. }
  4205. res.send('');
  4206. });
  4207. }
  4208. // Upload a file to the server and then batch upload to many agents
  4209. function handleUploadFileBatch(req, res) {
  4210. const domain = checkUserIpAddress(req, res);
  4211. if (domain == null) { return; }
  4212. var authUserid = null;
  4213. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  4214. const multiparty = require('multiparty');
  4215. const form = new multiparty.Form();
  4216. form.parse(req, function (err, fields, files) {
  4217. // If an authentication cookie is embedded in the form, use that.
  4218. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  4219. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  4220. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  4221. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  4222. }
  4223. if (authUserid == null) { res.sendStatus(401); return; }
  4224. // Get the user
  4225. const user = obj.users[authUserid];
  4226. if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
  4227. // Get fields
  4228. if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
  4229. var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
  4230. if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
  4231. if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
  4232. if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
  4233. if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
  4234. // Check if we have at least one target path
  4235. if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
  4236. parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
  4237. res.send('');
  4238. return;
  4239. }
  4240. // Get server temporary path
  4241. var serverpath = obj.path.join(obj.filespath, 'tmp')
  4242. try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
  4243. try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
  4244. // More typical upload method, the file data is in a multipart mime post.
  4245. files.files.forEach(function (file) {
  4246. var ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
  4247. cmd.files.push({ name: file.originalFilename, target: ftarget });
  4248. // Rename the file
  4249. obj.fs.rename(file.path, fpath, function (err) {
  4250. if (err && (err.code === 'EXDEV')) {
  4251. // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
  4252. obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
  4253. }
  4254. });
  4255. });
  4256. // Instruct one of more agents to download a URL to a given local drive location.
  4257. var tlsCertHash = null;
  4258. if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
  4259. tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
  4260. if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
  4261. }
  4262. for (var i in cmd.nodeids) {
  4263. obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
  4264. if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
  4265. var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
  4266. if (agentPath == null) return;
  4267. // Compute user consent
  4268. var consent = 0;
  4269. var mesh = obj.meshes[node.meshid];
  4270. if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
  4271. if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
  4272. if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
  4273. if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
  4274. // Check if we need to add consent flags because of a user group link
  4275. if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
  4276. // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
  4277. for (var i in user.links) {
  4278. var ugrp = obj.userGroups[i];
  4279. if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
  4280. consent |= ugrp.consent; // Add user group consent flags
  4281. }
  4282. }
  4283. }
  4284. // Event that this operation is being performed.
  4285. var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
  4286. var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
  4287. 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 };
  4288. parent.DispatchEvent(targets, obj, event);
  4289. // Send the agent commands to perform the batch upload operation
  4290. for (var f in cmd.files) {
  4291. if (cmd.files[f].name != null) {
  4292. 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 };
  4293. var agent = obj.wsagents[node._id];
  4294. if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
  4295. // TODO: Add support for peer servers.
  4296. }
  4297. }
  4298. });
  4299. }
  4300. res.send('');
  4301. });
  4302. }
  4303. // Subscribe to all events we are allowed to receive
  4304. obj.subscribe = function (userid, target) {
  4305. const user = obj.users[userid];
  4306. if (user == null) return;
  4307. const subscriptions = [userid, 'server-allusers'];
  4308. if (user.siteadmin != null) {
  4309. // Allow full site administrators of users with all events rights to see all events.
  4310. if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
  4311. else if ((user.siteadmin & 2) != 0) {
  4312. if ((user.groups == null) || (user.groups.length == 0)) {
  4313. // Subscribe to all user changes
  4314. subscriptions.push('server-users');
  4315. } else {
  4316. // Subscribe to user changes for some groups
  4317. for (var i in user.groups) { subscriptions.push('server-users:' + i); }
  4318. }
  4319. }
  4320. }
  4321. if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
  4322. obj.parent.RemoveAllEventDispatch(target);
  4323. obj.parent.AddEventDispatch(subscriptions, target);
  4324. return subscriptions;
  4325. };
  4326. // Handle a web socket relay request
  4327. function handleRelayWebSocket(ws, req, domain, user, cookie) {
  4328. if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4329. parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
  4330. try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
  4331. // Fetch information about the target
  4332. obj.db.Get(req.query.host, function (err, docs) {
  4333. if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4334. var xusername = '', xdevicename = '', xdevicename2 = null, node = null;
  4335. node = docs[0]; xdevicename2 = node.name; xdevicename = '-' + parent.common.makeFilename(node.name); ws.id = getRandomPassword(); ws.time = Date.now();
  4336. if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
  4337. var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
  4338. // Check if this user has permission to manage this computer
  4339. if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
  4340. // Check what connectivity is available for this node
  4341. var state = parent.GetConnectivityState(req.query.host);
  4342. var conn = 0;
  4343. if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
  4344. // Check what server needs to handle this connection
  4345. 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
  4346. var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
  4347. if (server != null) {
  4348. if (server.serverid != obj.parent.serverId) {
  4349. // Do local Intel CIRA routing using a different server
  4350. parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
  4351. obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
  4352. return;
  4353. }
  4354. } else {
  4355. server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
  4356. if ((server != null) && (server.serverid != obj.parent.serverId)) {
  4357. // Do local Intel AMT routing using a different server
  4358. parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
  4359. obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
  4360. return;
  4361. }
  4362. }
  4363. }
  4364. // Setup session recording if needed
  4365. 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
  4366. // Check again if we need to do recording
  4367. var record = true;
  4368. // Check user or device group recording
  4369. if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
  4370. record = false;
  4371. // Check device group recording
  4372. if (domain.sessionrecording.onlyselecteddevicegroups === true) {
  4373. var mesh = obj.meshes[node.meshid];
  4374. if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
  4375. }
  4376. // Check user recording
  4377. if (domain.sessionrecording.onlyselectedusers === true) {
  4378. if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
  4379. }
  4380. }
  4381. if (record == true) {
  4382. var now = new Date(Date.now());
  4383. // Get the username and make it acceptable as a filename
  4384. if (user._id) { xusername = '-' + parent.common.makeFilename(user._id.split('/')[2]); }
  4385. var xsessionid = ws.id;
  4386. 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) + xusername + xdevicename + '-' + xsessionid + '.mcrec';
  4387. var recFullFilename = null;
  4388. if (domain.sessionrecording.filepath) {
  4389. try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
  4390. recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
  4391. } else {
  4392. try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
  4393. recFullFilename = obj.path.join(parent.recordpath, recFilename);
  4394. }
  4395. var fd = obj.fs.openSync(recFullFilename, 'w');
  4396. if (fd != null) {
  4397. // Write the recording file header
  4398. parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename);
  4399. var metadata = {
  4400. magic: 'MeshCentralRelaySession',
  4401. ver: 1,
  4402. userid: user._id,
  4403. username: user.name,
  4404. sessionid: ws.id,
  4405. ipaddr1: req.clientIp,
  4406. time: new Date().toLocaleString(),
  4407. protocol: (req.query.p == 2) ? 101 : 100,
  4408. nodeid: node._id,
  4409. intelamt: true
  4410. };
  4411. if (ciraconn != null) { metadata.ipaddr2 = ciraconn.remoteAddr; }
  4412. else if ((conn & 4) != 0) { metadata.ipaddr2 = node.host; }
  4413. if (xdevicename2 != null) { metadata.devicename = xdevicename2; }
  4414. var firstBlock = JSON.stringify(metadata)
  4415. ws.logfile = { fd: fd, lock: false, filename: recFullFilename, startTime: Date.now(), size: 0, text: 0, req: req };
  4416. obj.meshRelayHandler.recordingEntry(ws.logfile, 1, 0, firstBlock, function () { });
  4417. if (node != null) { ws.logfile.nodeid = node._id; ws.logfile.meshid = node.meshid; ws.logfile.name = node.name; ws.logfile.icon = node.icon; }
  4418. if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
  4419. }
  4420. }
  4421. }
  4422. // If Intel AMT CIRA connection is available, use it
  4423. if (ciraconn != null) {
  4424. parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
  4425. // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
  4426. // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
  4427. var port = 16993;
  4428. //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
  4429. if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
  4430. if (req.query.p == 2) port += 2;
  4431. // Setup a new CIRA channel
  4432. if ((port == 16993) || (port == 16995)) {
  4433. // Perform TLS
  4434. var ser = new SerialTunnel();
  4435. var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
  4436. // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
  4437. // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
  4438. ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
  4439. // When APF tunnel return something, update SerialTunnel buffer
  4440. chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
  4441. // Handle CIRA tunnel state change
  4442. chnl.onStateChange = function (ciraconn, state) {
  4443. parent.debug('webrelay', 'Relay TLS CIRA state change', state);
  4444. if (state == 0) { try { ws.close(); } catch (e) { } }
  4445. if (state == 2) {
  4446. // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
  4447. 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 | constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, rejectUnauthorized: false };
  4448. if (req.query.tls1only == 1) {
  4449. tlsoptions.secureProtocol = 'TLSv1_method';
  4450. } else {
  4451. tlsoptions.minVersion = 'TLSv1';
  4452. }
  4453. var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
  4454. tlsock.chnl = chnl;
  4455. tlsock.setEncoding('binary');
  4456. tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
  4457. // Decrypted tunnel from TLS communication to be forwarded to websocket
  4458. tlsock.on('data', function (data) {
  4459. // AMT/TLS ---> WS
  4460. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
  4461. try { ws.send(data); } catch (ex) { }
  4462. });
  4463. // If TLS is on, forward it through TLSSocket
  4464. ws.forwardclient = tlsock;
  4465. ws.forwardclient.xtls = 1;
  4466. ws.forwardclient.onStateChange = function (ciraconn, state) {
  4467. parent.debug('webrelay', 'Relay CIRA state change', state);
  4468. if (state == 0) { try { ws.close(); } catch (e) { } }
  4469. };
  4470. ws.forwardclient.onData = function (ciraconn, data) {
  4471. // Run data thru interceptor
  4472. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
  4473. if (data.length > 0) {
  4474. if (ws.logfile == null) {
  4475. try { ws.send(data); } catch (e) { }
  4476. } else {
  4477. // Log to recording file
  4478. obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
  4479. }
  4480. }
  4481. };
  4482. // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
  4483. ws.forwardclient.onSendOk = function (ciraconn) { };
  4484. }
  4485. };
  4486. } else {
  4487. // Without TLS
  4488. ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
  4489. ws.forwardclient.xtls = 0;
  4490. ws._socket.resume();
  4491. ws.forwardclient.onStateChange = function (ciraconn, state) {
  4492. parent.debug('webrelay', 'Relay CIRA state change', state);
  4493. if (state == 0) { try { ws.close(); } catch (e) { } }
  4494. };
  4495. ws.forwardclient.onData = function (ciraconn, data) {
  4496. //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
  4497. // Run data thru interceptor
  4498. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
  4499. //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
  4500. if (data.length > 0) {
  4501. if (ws.logfile == null) {
  4502. try { ws.send(data); } catch (e) { }
  4503. } else {
  4504. // Log to recording file
  4505. obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
  4506. }
  4507. }
  4508. };
  4509. // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
  4510. ws.forwardclient.onSendOk = function (ciraconn) { };
  4511. }
  4512. // When data is received from the web socket, forward the data into the associated CIRA channel.
  4513. // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
  4514. ws.on('message', function (data) {
  4515. //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
  4516. if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
  4517. // WS ---> AMT/TLS
  4518. if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
  4519. // Log to recording file
  4520. if (ws.logfile == null) {
  4521. // Forward data to the associated TCP connection.
  4522. try { ws.forwardclient.write(data); } catch (ex) { }
  4523. } else {
  4524. // Log to recording file
  4525. obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
  4526. }
  4527. });
  4528. // If error, close the associated TCP connection.
  4529. ws.on('error', function (err) {
  4530. console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
  4531. parent.debug('webrelay', 'Websocket relay closed on error.');
  4532. // Log the disconnection
  4533. if (ws.time) {
  4534. if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
  4535. var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
  4536. if (user) {
  4537. var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: 101, nodeid: node._id };
  4538. obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
  4539. }
  4540. }
  4541. }
  4542. // Websocket closed, close the CIRA channel and TLS session.
  4543. if (ws.forwardclient) {
  4544. if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
  4545. if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
  4546. if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
  4547. delete ws.forwardclient;
  4548. }
  4549. // Close the recording file
  4550. if (ws.logfile != null) {
  4551. setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
  4552. obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
  4553. obj.fs.close(logfile.fd);
  4554. parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
  4555. // Compute session length
  4556. var sessionLength = null;
  4557. if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000) - 5; }
  4558. // Add a event entry about this recording
  4559. var basefile = parent.path.basename(ws.logfile.filename);
  4560. var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
  4561. if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
  4562. var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
  4563. if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
  4564. var mesh = obj.meshes[ws.logfile.meshid];
  4565. if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
  4566. if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
  4567. if (ws.logfile.name) { event.name = ws.logfile.name; }
  4568. if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
  4569. obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
  4570. delete ws.logfile;
  4571. }, ws);
  4572. }, 5000);
  4573. }
  4574. });
  4575. // If the web socket is closed, close the associated TCP connection.
  4576. ws.on('close', function () {
  4577. parent.debug('webrelay', 'Websocket relay closed.');
  4578. // Log the disconnection
  4579. if (ws.time) {
  4580. if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
  4581. var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
  4582. var nodeid = node._id;
  4583. var meshid = node.meshid;
  4584. if (user) {
  4585. var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: nodeid };
  4586. obj.parent.DispatchEvent(['*', user._id, nodeid, meshid], obj, event);
  4587. }
  4588. }
  4589. }
  4590. // Websocket closed, close the CIRA channel and TLS session.
  4591. if (ws.forwardclient) {
  4592. if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
  4593. if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
  4594. if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
  4595. delete ws.forwardclient;
  4596. }
  4597. // Close the recording file
  4598. if (ws.logfile != null) {
  4599. setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
  4600. obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
  4601. obj.fs.close(logfile.fd);
  4602. parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
  4603. // Compute session length
  4604. var sessionLength = null;
  4605. if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000) - 5; }
  4606. // Add a event entry about this recording
  4607. var basefile = parent.path.basename(ws.logfile.filename);
  4608. var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
  4609. if (user) { event.userids = [user._id]; }
  4610. var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
  4611. if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
  4612. var mesh = obj.meshes[ws.logfile.meshid];
  4613. if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
  4614. if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
  4615. if (ws.logfile.name) { event.name = ws.logfile.name; }
  4616. if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
  4617. obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
  4618. delete ws.logfile;
  4619. }, ws);
  4620. }, 5000);
  4621. }
  4622. });
  4623. // 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
  4624. // Fetch Intel AMT credentials & Setup interceptor
  4625. if (req.query.p == 1) {
  4626. parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
  4627. ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
  4628. ws.interceptor.blockAmtStorage = true;
  4629. } else if (req.query.p == 2) {
  4630. parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
  4631. ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
  4632. ws.interceptor.blockAmtStorage = true;
  4633. }
  4634. } else if ((conn & 4) != 0) { // If Intel AMT direct connection is possible, option a direct socket
  4635. // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
  4636. parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
  4637. // When data is received from the web socket, forward the data into the associated TCP connection.
  4638. ws.on('message', function (msg) {
  4639. //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
  4640. if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
  4641. if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
  4642. // Log to recording file
  4643. if (ws.logfile == null) {
  4644. // Forward data to the associated TCP connection.
  4645. try { ws.forwardclient.write(msg); } catch (ex) { }
  4646. } else {
  4647. // Log to recording file
  4648. obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
  4649. }
  4650. });
  4651. // If error, close the associated TCP connection.
  4652. ws.on('error', function (err) {
  4653. console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
  4654. parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
  4655. // Log the disconnection
  4656. if (ws.time) {
  4657. if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
  4658. var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
  4659. if (user) {
  4660. var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: node._id };
  4661. obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
  4662. }
  4663. }
  4664. }
  4665. if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
  4666. // Close the recording file
  4667. if (ws.logfile != null) {
  4668. setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
  4669. obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
  4670. obj.fs.close(logfile.fd);
  4671. parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
  4672. // Compute session length
  4673. var sessionLength = null;
  4674. if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000); }
  4675. // Add a event entry about this recording
  4676. var basefile = parent.path.basename(ws.logfile.filename);
  4677. var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
  4678. if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
  4679. var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
  4680. if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
  4681. var mesh = obj.meshes[ws.logfile.meshid];
  4682. if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
  4683. if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
  4684. if (ws.logfile.name) { event.name = ws.logfile.name; }
  4685. if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
  4686. obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
  4687. delete ws.logfile;
  4688. }, ws);
  4689. }, 5000);
  4690. }
  4691. });
  4692. // If the web socket is closed, close the associated TCP connection.
  4693. ws.on('close', function () {
  4694. parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
  4695. // Log the disconnection
  4696. if (ws.time) {
  4697. if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
  4698. var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
  4699. if (user) {
  4700. var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: node._id };
  4701. obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
  4702. }
  4703. }
  4704. }
  4705. if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
  4706. // Close the recording file
  4707. if (ws.logfile != null) {
  4708. setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
  4709. obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
  4710. obj.fs.close(logfile.fd);
  4711. parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
  4712. // Compute session length
  4713. var sessionLength = null;
  4714. if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000); }
  4715. // Add a event entry about this recording
  4716. var basefile = parent.path.basename(ws.logfile.filename);
  4717. var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
  4718. if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
  4719. var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
  4720. if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
  4721. var mesh = obj.meshes[ws.logfile.meshid];
  4722. if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
  4723. if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
  4724. if (ws.logfile.name) { event.name = ws.logfile.name; }
  4725. if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
  4726. obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
  4727. delete ws.logfile;
  4728. }, ws);
  4729. }, 5000);
  4730. }
  4731. });
  4732. // Compute target port
  4733. var port = 16992;
  4734. if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
  4735. if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
  4736. if (node.intelamt.tls == 0) {
  4737. // If this is TCP (without TLS) set a normal TCP socket
  4738. ws.forwardclient = new obj.net.Socket();
  4739. ws.forwardclient.setEncoding('binary');
  4740. ws.forwardclient.xstate = 0;
  4741. ws.forwardclient.forwardwsocket = ws;
  4742. ws._socket.resume();
  4743. } else {
  4744. // If TLS is going to be used, setup a TLS socket
  4745. 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 };
  4746. if (req.query.tls1only == 1) {
  4747. tlsoptions.secureProtocol = 'TLSv1_method';
  4748. } else {
  4749. tlsoptions.minVersion = 'TLSv1';
  4750. }
  4751. ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
  4752. // The TLS connection method is the same as TCP, but located a bit differently.
  4753. parent.debug('webrelay', user.name + ' - TLS connected to ' + node.host + ':' + port + '.');
  4754. ws.forwardclient.xstate = 1;
  4755. ws._socket.resume();
  4756. });
  4757. ws.forwardclient.setEncoding('binary');
  4758. ws.forwardclient.xstate = 0;
  4759. ws.forwardclient.forwardwsocket = ws;
  4760. }
  4761. // When we receive data on the TCP connection, forward it back into the web socket connection.
  4762. ws.forwardclient.on('data', function (data) {
  4763. if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
  4764. if (obj.parent.debugLevel >= 1) { // DEBUG
  4765. parent.debug('webrelaydata', user.name + ' - TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
  4766. //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
  4767. }
  4768. if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
  4769. if (ws.logfile == null) {
  4770. // No logging
  4771. try { ws.send(data); } catch (e) { }
  4772. } else {
  4773. // Log to recording file
  4774. obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
  4775. }
  4776. });
  4777. // If the TCP connection closes, disconnect the associated web socket.
  4778. ws.forwardclient.on('close', function () {
  4779. parent.debug('webrelay', user.name + ' - TCP relay disconnected from ' + node.host + ':' + port + '.');
  4780. try { ws.close(); } catch (e) { }
  4781. });
  4782. // If the TCP connection causes an error, disconnect the associated web socket.
  4783. ws.forwardclient.on('error', function (err) {
  4784. parent.debug('webrelay', user.name + ' - TCP relay error from ' + node.host + ':' + port + ': ' + err);
  4785. try { ws.close(); } catch (e) { }
  4786. });
  4787. // Fetch Intel AMT credentials & Setup interceptor
  4788. if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
  4789. else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
  4790. if (node.intelamt.tls == 0) {
  4791. // A TCP connection to Intel AMT just connected, start forwarding.
  4792. ws.forwardclient.connect(port, node.host, function () {
  4793. parent.debug('webrelay', user.name + ' - TCP relay connected to ' + node.host + ':' + port + '.');
  4794. ws.forwardclient.xstate = 1;
  4795. ws._socket.resume();
  4796. });
  4797. }
  4798. }
  4799. // Log the connection
  4800. if (user != null) {
  4801. if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
  4802. var msg = 'Started relay session', msgid = 13, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
  4803. var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip, protocol: 101, nodeid: node._id };
  4804. obj.parent.DispatchEvent(['*', user._id], obj, event);
  4805. }
  4806. // Update user last access time
  4807. if ((user != null)) {
  4808. const timeNow = Math.floor(Date.now() / 1000);
  4809. if (user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes
  4810. user.access = timeNow;
  4811. obj.parent.db.SetUser(user);
  4812. // Event the change
  4813. var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
  4814. if (parent.db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  4815. var targets = ['*', 'server-users', user._id];
  4816. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  4817. obj.parent.DispatchEvent(targets, obj, message);
  4818. }
  4819. }
  4820. }
  4821. });
  4822. }
  4823. // Setup agent to/from server file transfer handler
  4824. function handleAgentFileTransfer(ws, req) {
  4825. var domain = checkAgentIpAddress(ws, req);
  4826. if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4827. if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4828. var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  4829. if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
  4830. ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
  4831. ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
  4832. ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
  4833. if (ws.xcmd == 'coredump') {
  4834. // Check the agent core dump folder if not already present.
  4835. var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
  4836. if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
  4837. ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
  4838. ws.xid = 'coredump';
  4839. ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
  4840. }
  4841. // When data is received from the web socket, echo it back
  4842. ws.on('message', function (data) {
  4843. if (typeof data == 'string') {
  4844. // Control message
  4845. var cmd = null;
  4846. try { cmd = JSON.parse(data); } catch (ex) { }
  4847. if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
  4848. switch (cmd.sub) {
  4849. case 'start': {
  4850. // Perform an async file open
  4851. var callback = function onFileOpen(err, fd) {
  4852. onFileOpen.xws.xfile = fd;
  4853. try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
  4854. };
  4855. callback.xws = this;
  4856. obj.fs.open(this.xfilepath + '.part', 'w', callback);
  4857. break;
  4858. }
  4859. }
  4860. } else {
  4861. // Binary message
  4862. if (data.length < 4) return;
  4863. var flags = data.readInt32BE(0);
  4864. if ((data.length > 4)) {
  4865. // Write the file
  4866. this.xfilelen += (data.length - 4);
  4867. try {
  4868. var callback = function onFileDataWritten(err, bytesWritten, buffer) {
  4869. if (onFileDataWritten.xflags & 1) {
  4870. // End of file
  4871. parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
  4872. if (onFileDataWritten.xws.xfile) {
  4873. obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
  4874. obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
  4875. onFileDataWritten.xws.xfile = null;
  4876. }
  4877. try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
  4878. try { onFileDataWritten.xws.close(); } catch (ex) { }
  4879. } else {
  4880. // Send ack
  4881. try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
  4882. }
  4883. };
  4884. callback.xws = this;
  4885. callback.xflags = flags;
  4886. obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
  4887. } catch (ex) { }
  4888. } else {
  4889. if (flags & 1) {
  4890. // End of file
  4891. parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
  4892. if (this.xfile) {
  4893. obj.fs.close(this.xfile, function (err) { });
  4894. obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
  4895. this.xfile = null;
  4896. }
  4897. this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
  4898. try { this.close(); } catch (ex) { }
  4899. } else {
  4900. // Send ack
  4901. this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
  4902. }
  4903. }
  4904. }
  4905. });
  4906. // If error, do nothing.
  4907. ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
  4908. // If closed, do nothing
  4909. ws.on('close', function (req) {
  4910. if (this.xfile) {
  4911. obj.fs.close(this.xfile, function (err) { });
  4912. obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
  4913. }
  4914. });
  4915. }
  4916. // Handle the web socket echo request, just echo back the data sent
  4917. function handleEchoWebSocket(ws, req) {
  4918. const domain = checkUserIpAddress(ws, req);
  4919. if (domain == null) { return; }
  4920. ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
  4921. // When data is received from the web socket, echo it back
  4922. ws.on('message', function (data) {
  4923. if (data.toString('utf8') == 'close') {
  4924. try { ws.close(); } catch (e) { console.log(e); }
  4925. } else {
  4926. try { ws.send(data); } catch (e) { console.log(e); }
  4927. }
  4928. });
  4929. // If error, do nothing.
  4930. ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
  4931. // If closed, do nothing
  4932. ws.on('close', function (req) { });
  4933. }
  4934. // Handle the 2FA hold web socket
  4935. // Accept an hold a web socket connection until the 2FA response is received.
  4936. function handle2faHoldWebSocket(ws, req) {
  4937. const domain = checkUserIpAddress(ws, req);
  4938. if (domain == null) { return; }
  4939. if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
  4940. if (typeof req.query.c !== 'string') { ws.close(); return; }
  4941. const cookie = parent.decodeCookie(req.query.c, null, 1);
  4942. if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
  4943. var user = obj.users[cookie.u];
  4944. if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
  4945. ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
  4946. // 2FA event subscription
  4947. obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
  4948. ws.cookie = cookie;
  4949. ws.HandleEvent = function (source, event, ids, id) {
  4950. obj.parent.RemoveAllEventDispatch(this);
  4951. if ((event.approved === true) && (event.userid == this.cookie.u)) {
  4952. // Create a login cookie
  4953. const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
  4954. try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
  4955. } else {
  4956. // Reject the login
  4957. try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
  4958. }
  4959. }
  4960. // We do not accept any data on this connection.
  4961. ws.on('message', function (data) { this.close(); });
  4962. // If error, do nothing.
  4963. ws.on('error', function (err) { });
  4964. // If closed, unsubscribe
  4965. ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
  4966. // Perform push notification to device
  4967. try {
  4968. const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
  4969. var code = Buffer.from(cookie.c, 'base64').toString();
  4970. var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
  4971. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  4972. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  4973. if (err == null) {
  4974. try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
  4975. } else {
  4976. try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
  4977. }
  4978. });
  4979. } catch (ex) { console.log(ex); }
  4980. }
  4981. // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
  4982. function readTotalFileSize(path) {
  4983. var r = 0, dir;
  4984. try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
  4985. for (var i in dir) {
  4986. var stat = obj.fs.statSync(path + '/' + dir[i]);
  4987. if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
  4988. }
  4989. return r;
  4990. }
  4991. // Delete a folder and all sub items. (TODO: try to make all async version)
  4992. function deleteFolderRec(path) {
  4993. if (obj.fs.existsSync(path) == false) return;
  4994. try {
  4995. obj.fs.readdirSync(path).forEach(function (file, index) {
  4996. var pathx = path + '/' + file;
  4997. if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
  4998. });
  4999. obj.fs.rmdirSync(path);
  5000. } catch (ex) { }
  5001. }
  5002. // Handle Intel AMT events
  5003. // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
  5004. obj.handleAmtEventRequest = function (req, res) {
  5005. const domain = getDomain(req);
  5006. try {
  5007. if (req.headers.authorization) {
  5008. var authstr = req.headers.authorization;
  5009. if (authstr.substring(0, 7) == 'Digest ') {
  5010. var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
  5011. if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
  5012. // Read the data, we need to get the arg field
  5013. var eventData = '';
  5014. req.on('data', function (chunk) { eventData += chunk; });
  5015. req.on('end', function () {
  5016. // Completed event read, let get the argument that must contain the nodeid
  5017. var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
  5018. if (i > 0) {
  5019. var nodeid = eventData.substring(i + 30, i + 30 + 64);
  5020. if (nodeid.length == 64) {
  5021. var nodekey = 'node/' + domain.id + '/' + nodeid;
  5022. // See if this node exists in the database
  5023. obj.db.Get(nodekey, function (err, nodes) {
  5024. if (nodes.length == 1) {
  5025. // Yes, the node exists, compute Intel AMT digest password
  5026. var node = nodes[0];
  5027. 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');
  5028. // Check the MD5 hash
  5029. if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
  5030. // This is an authenticated Intel AMT event, update the host address
  5031. var amthost = req.clientIp;
  5032. if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
  5033. if (node.host != amthost) {
  5034. // Get the mesh for this device
  5035. var mesh = obj.meshes[node.meshid];
  5036. if (mesh) {
  5037. // Update the database
  5038. var oldname = node.host;
  5039. node.host = amthost;
  5040. obj.db.Set(obj.cleanDevice(node));
  5041. // Event the node change
  5042. 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 };
  5043. // Remove the Intel AMT password before eventing this.
  5044. event.node = node;
  5045. if (event.node.intelamt && event.node.intelamt.pass) {
  5046. event.node = Object.assign({}, event.node); // Shallow clone
  5047. event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
  5048. delete event.node.intelamt.pass;
  5049. }
  5050. 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.
  5051. obj.parent.DispatchEvent(['*', node.meshid], obj, event);
  5052. }
  5053. }
  5054. if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
  5055. //res.send('OK');
  5056. return;
  5057. }
  5058. }
  5059. });
  5060. }
  5061. }
  5062. });
  5063. }
  5064. }
  5065. }
  5066. } catch (e) { console.log(e); }
  5067. // Send authentication response
  5068. obj.crypto.randomBytes(48, function (err, buf) {
  5069. var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
  5070. res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
  5071. res.sendStatus(401);
  5072. });
  5073. };
  5074. // Handle a server backup request
  5075. function handleBackupRequest(req, res) {
  5076. const domain = checkUserIpAddress(req, res);
  5077. if (domain == null) { return; }
  5078. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  5079. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5080. if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
  5081. var user = obj.users[req.session.userid];
  5082. if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
  5083. // Require modules
  5084. const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
  5085. // Good practice to catch this error explicitly
  5086. archive.on('error', function (err) { throw err; });
  5087. // Set the archive name
  5088. res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
  5089. // Pipe archive data to the file
  5090. archive.pipe(res);
  5091. // Append files from a glob pattern
  5092. archive.directory(obj.parent.datapath, false);
  5093. // Finalize the archive (ie we are done appending files but streams have to finish yet)
  5094. archive.finalize();
  5095. }
  5096. // Handle a server restore request
  5097. function handleRestoreRequest(req, res) {
  5098. const domain = checkUserIpAddress(req, res);
  5099. if (domain == null) { return; }
  5100. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  5101. if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
  5102. var authUserid = null;
  5103. if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
  5104. const multiparty = require('multiparty');
  5105. const form = new multiparty.Form();
  5106. form.parse(req, function (err, fields, files) {
  5107. // If an authentication cookie is embedded in the form, use that.
  5108. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
  5109. var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
  5110. if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
  5111. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
  5112. }
  5113. if (authUserid == null) { res.sendStatus(401); return; }
  5114. // Get the user
  5115. const user = obj.users[req.session.userid];
  5116. if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
  5117. res.set('Content-Type', 'text/html');
  5118. res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
  5119. parent.Stop(files.datafile[0].path);
  5120. });
  5121. }
  5122. // Handle a request to download a mesh agent
  5123. obj.handleMeshAgentRequest = function (req, res) {
  5124. var domain = getDomain(req, res);
  5125. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  5126. // If required, check if this user has rights to do this
  5127. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
  5128. if ((req.query.meshinstall != null) && (req.query.id != null)) {
  5129. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5130. // Send meshagent with included self installer for a specific platform back
  5131. // Start by getting the .msh for this request
  5132. var meshsettings = getMshFromRequest(req, res, domain);
  5133. if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
  5134. // Get the interactive install script, this only works for non-Windows agents
  5135. var agentid = parseInt(req.query.meshinstall);
  5136. var argentInfo = obj.parent.meshAgentBinaries[agentid];
  5137. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5138. var scriptInfo = obj.parent.meshAgentInstallScripts[6];
  5139. if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
  5140. // Change the .msh file into JSON format and merge it into the install script
  5141. var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
  5142. for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
  5143. var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
  5144. // Get the agent filename
  5145. var meshagentFilename = 'meshagent';
  5146. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  5147. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
  5148. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  5149. res.statusCode = 200;
  5150. obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
  5151. } else if (req.query.id != null) {
  5152. // Send a specific mesh agent back
  5153. var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
  5154. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
  5155. if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5156. // Download PDB debug files, only allowed for administrator or accounts with agent dump access
  5157. if (req.query.pdb == 1) {
  5158. if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5159. var user = obj.users[req.session.userid];
  5160. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5161. if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
  5162. if (argentInfo.id == 3) {
  5163. setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
  5164. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  5165. try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
  5166. return;
  5167. }
  5168. if (argentInfo.id == 4) {
  5169. setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
  5170. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  5171. try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
  5172. return;
  5173. }
  5174. }
  5175. try { res.sendStatus(404); } catch (ex) { }
  5176. return;
  5177. }
  5178. if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
  5179. // Get the agent filename
  5180. var meshagentFilename = argentInfo.rname;
  5181. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  5182. if (argentInfo.rname.endsWith('.apk') && !meshagentFilename.endsWith('.apk')) { meshagentFilename = meshagentFilename + '.apk'; }
  5183. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  5184. 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
  5185. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
  5186. if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
  5187. return;
  5188. } else {
  5189. // Check if the meshid is a time limited, encrypted cookie
  5190. var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
  5191. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
  5192. // We are going to embed the .msh file into the Windows executable (signed or not).
  5193. // First, fetch the mesh object to build the .msh file
  5194. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
  5195. if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
  5196. // If required, check if this user has rights to do this
  5197. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  5198. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
  5199. }
  5200. var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5201. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5202. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
  5203. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  5204. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  5205. // Prepare a mesh agent file name using the device group name.
  5206. var meshfilename = mesh.name
  5207. meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
  5208. if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
  5209. // Customize the mesh agent file name
  5210. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
  5211. meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
  5212. }
  5213. // Get the agent connection server name
  5214. var serverName = obj.getWebServerName(domain, req);
  5215. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  5216. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  5217. var xdomain = (domain.dns == null) ? domain.id : '';
  5218. if (xdomain != '') xdomain += '/';
  5219. var meshsettings = '';
  5220. if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
  5221. meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  5222. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  5223. meshsettings += 'MeshServer=local\r\n';
  5224. 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'; }
  5225. }
  5226. 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'; }
  5227. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  5228. }
  5229. if (req.query.id == '10006') { // Assistant settings and customizations
  5230. 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
  5231. if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
  5232. if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
  5233. if ((domain.assistantnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5234. if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
  5235. if (typeof domain.assistantcustomization.title == 'string') { meshsettings += 'Title=' + domain.assistantcustomization.title + '\r\n'; }
  5236. if (typeof domain.assistantcustomization.image == 'string') {
  5237. try { meshsettings += 'Image=' + Buffer.from(obj.fs.readFileSync(parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64') + '\r\n'; } catch (ex) { console.log(ex); }
  5238. }
  5239. if (req.query.ac != '4') {
  5240. // Send with custom filename followed by device group name
  5241. if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = meshfilename.split('MeshCentralAssistant').join(domain.assistantcustomization.filename); }
  5242. } else {
  5243. // Send with custom filename, no device group name
  5244. if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = domain.assistantcustomization.filename + '.exe'; } else { meshfilename = 'MeshCentralAssistant.exe'; }
  5245. }
  5246. }
  5247. } else { // Add agent customization, not for Assistant
  5248. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  5249. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  5250. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5251. if (domain.agentcustomization != null) {
  5252. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  5253. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  5254. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  5255. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  5256. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  5257. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  5258. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  5259. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  5260. }
  5261. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; } // Translation strings, not for MeshCentral Assistant
  5262. }
  5263. setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
  5264. if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
  5265. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) {
  5266. obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: domain.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: domain.meshAgentBinaries[req.query.id].pe });
  5267. } else {
  5268. 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 });
  5269. }
  5270. return;
  5271. }
  5272. } else if (req.query.script != null) {
  5273. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5274. // Send a specific mesh install script back
  5275. var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
  5276. if (scriptInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5277. setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
  5278. var data = scriptInfo.data;
  5279. var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
  5280. if (obj.isTrustedCert(domain) != true) {
  5281. cmdoptions.wgetoptionshttps += '--no-check-certificate ';
  5282. cmdoptions.curloptionshttps += '-k ';
  5283. }
  5284. if (domain.agentnoproxy === true) {
  5285. cmdoptions.wgetoptionshttp += '--no-proxy ';
  5286. cmdoptions.wgetoptionshttps += '--no-proxy ';
  5287. cmdoptions.curloptionshttp += '--noproxy \'*\' ';
  5288. cmdoptions.curloptionshttps += '--noproxy \'*\' ';
  5289. }
  5290. for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
  5291. res.send(data);
  5292. return;
  5293. } else if (req.query.meshcmd != null) {
  5294. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5295. // Send meshcmd for a specific platform back
  5296. var agentid = parseInt(req.query.meshcmd);
  5297. // If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
  5298. if ((agentid == 3) && (obj.parent.meshAgentBinaries[11000] != null)) { // Signed Windows MeshCmd.exe x86-32
  5299. var stats = null, meshCmdPath = obj.parent.meshAgentBinaries[11000].path;
  5300. try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
  5301. if ((stats != null)) {
  5302. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
  5303. res.sendFile(meshCmdPath); return;
  5304. }
  5305. } else if ((agentid == 4) && (obj.parent.meshAgentBinaries[11001] != null)) { // Signed Windows MeshCmd64.exe x86-64
  5306. var stats = null, meshCmd64Path = obj.parent.meshAgentBinaries[11001].path;
  5307. try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
  5308. if ((stats != null)) {
  5309. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
  5310. res.sendFile(meshCmd64Path); return;
  5311. }
  5312. } else if ((agentid == 43) && (obj.parent.meshAgentBinaries[11002] != null)) { // Signed Windows MeshCmd64.exe ARM-64
  5313. var stats = null, meshCmdAMR64Path = obj.parent.meshAgentBinaries[11002].path;
  5314. try { stats = obj.fs.statSync(meshCmdAMR64Path); } catch (e) { }
  5315. if ((stats != null)) {
  5316. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd-arm64.exe', null, 'meshcmd');
  5317. res.sendFile(meshCmdAMR64Path); return;
  5318. }
  5319. }
  5320. // No signed agents, we are going to merge a new MeshCmd.
  5321. if (((agentid == 3) || (agentid == 4)) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
  5322. var argentInfo = obj.parent.meshAgentBinaries[agentid];
  5323. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5324. if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5325. setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
  5326. res.statusCode = 200;
  5327. if (argentInfo.signedMeshCmdPath != null) {
  5328. // If we have a pre-signed MeshCmd, send that.
  5329. res.sendFile(argentInfo.signedMeshCmdPath);
  5330. } else {
  5331. // Merge JavaScript to a unsigned agent and send that.
  5332. obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(obj.parent.defaultMeshCmd, 'utf8'), peinfo: argentInfo.pe });
  5333. }
  5334. return;
  5335. } else if (req.query.meshaction != null) {
  5336. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5337. var user = obj.users[req.session.userid];
  5338. if (user == null) {
  5339. // Check if we have an authentication cookie
  5340. var c = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey);
  5341. if (c == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5342. // Download tools using a cookie
  5343. if (c.download == req.query.meshaction) {
  5344. if (req.query.meshaction == 'winrouter') {
  5345. var p = null;
  5346. if (obj.parent.meshToolsBinaries['MeshCentralRouter']) { p = obj.parent.meshToolsBinaries['MeshCentralRouter'].path; }
  5347. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
  5348. if (obj.fs.existsSync(p)) {
  5349. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
  5350. try { res.sendFile(p); } catch (ex) { }
  5351. } else { try { res.sendStatus(404); } catch (ex) { } }
  5352. return;
  5353. } else if (req.query.meshaction == 'winassistant') {
  5354. var p = null;
  5355. if (obj.parent.meshToolsBinaries['MeshCentralAssistant']) { p = obj.parent.meshToolsBinaries['MeshCentralAssistant'].path; }
  5356. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
  5357. if (obj.fs.existsSync(p)) {
  5358. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
  5359. try { res.sendFile(p); } catch (ex) { }
  5360. } else { try { res.sendStatus(404); } catch (ex) { } }
  5361. return;
  5362. } else if (req.query.meshaction == 'macrouter') {
  5363. var p = null;
  5364. if (obj.parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = obj.parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
  5365. if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
  5366. if (obj.fs.existsSync(p)) {
  5367. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
  5368. try { res.sendFile(p); } catch (ex) { }
  5369. } else { try { res.sendStatus(404); } catch (ex) { } }
  5370. return;
  5371. }
  5372. return;
  5373. }
  5374. // Check if the cookie authenticates a user
  5375. if (c.userid == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5376. user = obj.users[c.userid];
  5377. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5378. }
  5379. if ((req.query.meshaction == 'route') && (req.query.nodeid != null)) {
  5380. var nodeIdSplit = req.query.nodeid.split('/');
  5381. if ((nodeIdSplit[0] != 'node') || (nodeIdSplit[1] != domain.id)) { try { res.sendStatus(401); } catch (ex) { } return; }
  5382. obj.db.Get(req.query.nodeid, function (err, nodes) {
  5383. if ((err != null) || (nodes.length != 1)) { try { res.sendStatus(401); } catch (ex) { } return; }
  5384. var node = nodes[0];
  5385. // Create the meshaction.txt file for meshcmd.exe
  5386. var meshaction = {
  5387. action: req.query.meshaction,
  5388. localPort: 1234,
  5389. remoteName: node.name,
  5390. remoteNodeId: node._id,
  5391. remoteTarget: null,
  5392. remotePort: 3389,
  5393. username: '',
  5394. password: '',
  5395. serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
  5396. serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
  5397. debugLevel: 0
  5398. };
  5399. if (user != null) { meshaction.username = user.name; }
  5400. if (req.query.key != null) { meshaction.loginKey = req.query.key; }
  5401. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5402. if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : (domain.id + '/')) + 'meshrelay.ashx'; }
  5403. setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
  5404. res.send(JSON.stringify(meshaction, null, ' '));
  5405. return;
  5406. });
  5407. } else if (req.query.meshaction == 'generic') {
  5408. var meshaction = {
  5409. username: user.name,
  5410. password: '',
  5411. serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
  5412. serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
  5413. debugLevel: 0
  5414. };
  5415. if (user != null) { meshaction.username = user.name; }
  5416. if (req.query.key != null) { meshaction.loginKey = req.query.key; }
  5417. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5418. if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
  5419. setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
  5420. res.send(JSON.stringify(meshaction, null, ' '));
  5421. return;
  5422. } else if (req.query.meshaction == 'winrouter') {
  5423. var p = null;
  5424. if (parent.meshToolsBinaries['MeshCentralRouter']) { p = parent.meshToolsBinaries['MeshCentralRouter'].path; }
  5425. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
  5426. if (obj.fs.existsSync(p)) {
  5427. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
  5428. try { res.sendFile(p); } catch (ex) { }
  5429. } else { try { res.sendStatus(404); } catch (ex) { } }
  5430. return;
  5431. } else if (req.query.meshaction == 'winassistant') {
  5432. var p = null;
  5433. if (parent.meshToolsBinaries['MeshCentralAssistant']) { p = parent.meshToolsBinaries['MeshCentralAssistant'].path; }
  5434. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
  5435. if (obj.fs.existsSync(p)) {
  5436. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
  5437. try { res.sendFile(p); } catch (ex) { }
  5438. } else { try { res.sendStatus(404); } catch (ex) { } }
  5439. return;
  5440. } else if (req.query.meshaction == 'macrouter') {
  5441. var p = null;
  5442. if (parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
  5443. if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
  5444. if (obj.fs.existsSync(p)) {
  5445. setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
  5446. try { res.sendFile(p); } catch (ex) { }
  5447. } else { try { res.sendStatus(404); } catch (ex) { } }
  5448. return;
  5449. } else {
  5450. try { res.sendStatus(401); } catch (ex) { }
  5451. return;
  5452. }
  5453. } else {
  5454. domain = checkUserIpAddress(req, res); // Recheck the domain to apply user IP filtering.
  5455. if (domain == null) return;
  5456. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
  5457. if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5458. var user = null, coreDumpsAllowed = false;
  5459. if (typeof req.session.userid == 'string') { user = obj.users[req.session.userid]; }
  5460. if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5461. // Check if this user has access to agent core dumps
  5462. 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)))) {
  5463. coreDumpsAllowed = true;
  5464. if ((req.query.dldump != null) && obj.common.IsFilenameValid(req.query.dldump)) {
  5465. // Download a dump file
  5466. var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
  5467. if (obj.fs.existsSync(dumpFile)) {
  5468. setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
  5469. res.sendFile(dumpFile); return;
  5470. } else {
  5471. try { res.sendStatus(404); } catch (ex) { } return;
  5472. }
  5473. }
  5474. if ((req.query.deldump != null) && obj.common.IsFilenameValid(req.query.deldump)) {
  5475. // Delete a dump file
  5476. try { obj.fs.unlinkSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.deldump)); } catch (ex) { console.log(ex); }
  5477. }
  5478. if ((req.query.dumps != null) || (req.query.deldump != null)) {
  5479. // Send list of agent core dumps
  5480. 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>';
  5481. 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>';
  5482. var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
  5483. if (obj.fs.existsSync(coreDumpPath)) {
  5484. var files = obj.fs.readdirSync(coreDumpPath);
  5485. var coredumps = [];
  5486. for (var i in files) {
  5487. var file = files[i];
  5488. if (file.endsWith('.dmp')) {
  5489. var fileSplit = file.substring(0, file.length - 4).split('-');
  5490. if (fileSplit.length == 3) {
  5491. var agentid = parseInt(fileSplit[0]);
  5492. if ((isNaN(agentid) == false) && (obj.parent.meshAgentBinaries[agentid] != null)) {
  5493. var agentinfo = obj.parent.meshAgentBinaries[agentid];
  5494. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5495. var filestats = obj.fs.statSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', file));
  5496. coredumps.push({
  5497. fileSplit: fileSplit,
  5498. agentinfo: agentinfo,
  5499. filestats: filestats,
  5500. currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()),
  5501. downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5502. deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5503. agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
  5504. time: new Date(filestats.ctime)
  5505. });
  5506. }
  5507. }
  5508. }
  5509. }
  5510. coredumps.sort(function (a, b) { if (a.time > b.time) return -1; if (a.time < b.time) return 1; return 0; });
  5511. for (var i in coredumps) {
  5512. var d = coredumps[i];
  5513. response += '<tr><td>' + d.agentinfo.id + '</td><td>' + d.time.toDateString().split(' ').join('&nbsp;') + '</td><td>' + d.agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
  5514. 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>';
  5515. if (d.currentAgent) { response += '<td><a download href="' + d.agentUrl + '">Download</a></td>'; } else { response += '<td></td>'; }
  5516. response += '<td>' + d.fileSplit[1].toLowerCase() + '</td><td>' + d.fileSplit[2] + '</td><td><a href="' + d.deleteUrl + '">Delete</a></td></tr>';
  5517. }
  5518. }
  5519. response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
  5520. res.send(response);
  5521. return;
  5522. }
  5523. }
  5524. if (req.query.cores != null) {
  5525. // Send list of agent cores
  5526. 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>';
  5527. response += '<tr style="background-color:lightgray"><th>Name</th><th>Size</th><th>Comp</th><th>Decompressed Hash SHA384</th></tr>';
  5528. for (var i in parent.defaultMeshCores) {
  5529. 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>';
  5530. }
  5531. response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
  5532. res.send(response);
  5533. return;
  5534. }
  5535. if (req.query.dlcore != null) {
  5536. // Download mesh core
  5537. var bin = parent.defaultMeshCores[req.query.dlcore];
  5538. if ((bin == null) || (bin.length < 5)) { try { res.sendStatus(404); } catch (ex) { } return; }
  5539. setContentDispositionHeader(res, 'application/octet-stream', encodeURIComponent(req.query.dlcore) + '.js', null, 'meshcore.js');
  5540. res.send(bin.slice(4));
  5541. return;
  5542. }
  5543. if (req.query.dlccore != null) {
  5544. // Download compressed mesh core
  5545. var bin = parent.defaultMeshCoresDeflate[req.query.dlccore];
  5546. if (bin == null) { try { res.sendStatus(404); } catch (ex) { } return; }
  5547. setContentDispositionHeader(res, 'application/octet-stream', req.query.dlccore + '.js.deflate', null, 'meshcore.js.deflate');
  5548. res.send(bin);
  5549. return;
  5550. }
  5551. // Send a list of available mesh agents
  5552. 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>';
  5553. 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>';
  5554. var originalUrl = req.originalUrl.split('?')[0];
  5555. for (var agentid in obj.parent.meshAgentBinaries) {
  5556. if ((agentid >= 10000) && (agentid != 10005)) continue;
  5557. var agentinfo = obj.parent.meshAgentBinaries[agentid];
  5558. if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
  5559. response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
  5560. response += '<td><a download href="' + originalUrl + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">' + agentinfo.rname + '</a>';
  5561. if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) {
  5562. 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>'; }
  5563. }
  5564. if (agentinfo.zdata != null) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&zip=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">ZIP</a>'; }
  5565. response += '</td>';
  5566. response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hashhex + '</td>';
  5567. 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>';
  5568. }
  5569. response += '</table>';
  5570. response += '<a href="' + originalUrl + '?cores=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshCores</a> ';
  5571. if (coreDumpsAllowed) { response += '<a href="' + originalUrl + '?dumps=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshAgent Crash Dumps</a>'; }
  5572. response += '</body></html>';
  5573. res.send(response);
  5574. return;
  5575. }
  5576. };
  5577. // generate the server url
  5578. obj.generateBaseURL = function (domain, req) {
  5579. var serverName = obj.getWebServerName(domain, req);
  5580. var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
  5581. var xdomain = (domain.dns == null) ? domain.id : '';
  5582. if (xdomain != '') xdomain += '/';
  5583. return ('https://' + serverName + ':' + httpsPort + '/' + xdomain);
  5584. }
  5585. // Get the web server hostname. This may change if using a domain with a DNS name.
  5586. obj.getWebServerName = function (domain, req) {
  5587. if (domain.dns != null) return domain.dns;
  5588. if ((obj.certificates.CommonName == 'un-configured') && (req != null) && (req.headers != null) && (typeof req.headers.host == 'string')) { return req.headers.host.split(':')[0]; }
  5589. return obj.certificates.CommonName;
  5590. }
  5591. // Return true if this is an allowed HTTP request origin hostname.
  5592. obj.CheckWebServerOriginName = function (domain, req) {
  5593. if (domain.allowedorigin === true) return true; // Ignore origin
  5594. if (typeof req.headers.origin != 'string') return true; // No origin in the header, this is a desktop app
  5595. const originUrl = require('url').parse(req.headers.origin, true);
  5596. if (typeof originUrl.hostname != 'string') return false; // Origin hostname is not valid
  5597. if (Array.isArray(domain.allowedorigin)) return (domain.allowedorigin.indexOf(originUrl.hostname) >= 0); // Check if this is an allowed origin from an explicit list
  5598. if (obj.isTrustedCert(domain) === false) return true; // This server does not have a trusted certificate.
  5599. if (domain.dns != null) return (domain.dns == originUrl.hostname); // Match the domain DNS
  5600. return (obj.certificates.CommonName == originUrl.hostname); // Match the default server name
  5601. }
  5602. // Create a OSX mesh agent installer
  5603. obj.handleMeshOsxAgentRequest = function (req, res) {
  5604. const domain = getDomain(req, res);
  5605. if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
  5606. if (req.query.id == null) { res.sendStatus(404); return; }
  5607. // If required, check if this user has rights to do this
  5608. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
  5609. // Send a specific mesh agent back
  5610. var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
  5611. if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
  5612. if ((argentInfo == null) || (req.query.meshid == null)) { res.sendStatus(404); return; }
  5613. // Check if the meshid is a time limited, encrypted cookie
  5614. var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
  5615. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
  5616. // We are going to embed the .msh file into the Windows executable (signed or not).
  5617. // First, fetch the mesh object to build the .msh file
  5618. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
  5619. if (mesh == null) { res.sendStatus(401); return; }
  5620. // If required, check if this user has rights to do this
  5621. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  5622. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
  5623. }
  5624. var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5625. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5626. // Get the agent connection server name
  5627. var serverName = obj.getWebServerName(domain, req);
  5628. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  5629. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  5630. var xdomain = (domain.dns == null) ? domain.id : '';
  5631. if (xdomain != '') xdomain += '/';
  5632. var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  5633. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5634. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  5635. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  5636. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  5637. meshsettings += 'MeshServer=local\r\n';
  5638. 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'; }
  5639. }
  5640. 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'; }
  5641. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  5642. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5643. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  5644. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  5645. if (domain.agentcustomization != null) { // Add agent customization
  5646. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  5647. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  5648. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  5649. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  5650. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  5651. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  5652. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  5653. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  5654. }
  5655. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
  5656. // Setup the response output
  5657. var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
  5658. archive.on('error', function (err) { throw err; });
  5659. // Customize the mesh agent file name
  5660. var meshfilename = 'MeshAgent-' + mesh.name + '.zip';
  5661. var meshexecutablename = 'meshagent';
  5662. var meshmpkgname = 'MeshAgent.mpkg';
  5663. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
  5664. meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
  5665. meshexecutablename = meshexecutablename.split('meshagent').join(domain.agentcustomization.filename);
  5666. meshmpkgname = meshmpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
  5667. }
  5668. // Customise the mesh agent display name
  5669. var meshdisplayname = 'Mesh Agent';
  5670. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.displayname == 'string')) {
  5671. meshdisplayname = meshdisplayname.split('Mesh Agent').join(domain.agentcustomization.displayname);
  5672. }
  5673. // Customise the mesh agent service name
  5674. var meshservicename = 'meshagent';
  5675. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.servicename == 'string')) {
  5676. meshservicename = meshservicename.split('meshagent').join(domain.agentcustomization.servicename);
  5677. }
  5678. // Customise the mesh agent company name
  5679. var meshcompanyname = 'meshagent';
  5680. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.companyname == 'string')) {
  5681. meshcompanyname = meshcompanyname.split('meshagent').join(domain.agentcustomization.companyname);
  5682. }
  5683. // Set the agent download including the mesh name.
  5684. setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
  5685. archive.pipe(res);
  5686. // Opens the "MeshAgentOSXPackager.zip"
  5687. var yauzl = require('yauzl');
  5688. yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
  5689. if (err) { res.sendStatus(500); return; }
  5690. zipfile.readEntry();
  5691. zipfile.on('entry', function (entry) {
  5692. if (/\/$/.test(entry.fileName)) {
  5693. // Skip all folder entries
  5694. zipfile.readEntry();
  5695. } else {
  5696. if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
  5697. // This is a special file entry, we need to fix it.
  5698. zipfile.openReadStream(entry, function (err, readStream) {
  5699. readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
  5700. readStream.on('end', function () {
  5701. var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
  5702. 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';
  5703. var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
  5704. archive.append(readStream.xxdata.toString().split('###DISPLAYNAME###').join(meshdisplayname).split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) });
  5705. zipfile.readEntry();
  5706. });
  5707. });
  5708. } else if (entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchAgent.plist' ||
  5709. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchDaemon.plist' ||
  5710. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Info.plist' ||
  5711. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/postflight' ||
  5712. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/Postflight.sh' ||
  5713. entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Uninstall.command' ||
  5714. entry.fileName == 'MeshAgent.mpkg/Uninstall.command') {
  5715. // This is a special file entry, we need to fix it.
  5716. zipfile.openReadStream(entry, function (err, readStream) {
  5717. readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
  5718. readStream.on('end', function () {
  5719. var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
  5720. if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
  5721. archive.append(readStream.xxdata.toString().split('###SERVICENAME###').join(meshservicename).split('###COMPANYNAME###').join(meshcompanyname).split('###EXECUTABLENAME###').join(meshexecutablename), options);
  5722. zipfile.readEntry();
  5723. });
  5724. });
  5725. } else {
  5726. // Normal file entry
  5727. zipfile.openReadStream(entry, function (err, readStream) {
  5728. if (err) { throw err; }
  5729. var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
  5730. if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
  5731. archive.append(readStream, options);
  5732. readStream.on('end', function () { zipfile.readEntry(); });
  5733. });
  5734. }
  5735. }
  5736. });
  5737. zipfile.on('end', function () {
  5738. archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin'.replace('MeshAgent.mpkg',meshmpkgname) });
  5739. archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh'.replace('MeshAgent.mpkg',meshmpkgname) });
  5740. archive.finalize();
  5741. });
  5742. });
  5743. }
  5744. // Return a .msh file from a given request, id is the device group identifier or encrypted cookie with the identifier.
  5745. function getMshFromRequest(req, res, domain) {
  5746. // If required, check if this user has rights to do this
  5747. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { return null; }
  5748. // Check if the meshid is a time limited, encrypted cookie
  5749. var meshcookie = obj.parent.decodeCookie(req.query.id, obj.parent.invitationLinkEncryptionKey);
  5750. if ((meshcookie != null) && (meshcookie.m != null)) { req.query.id = meshcookie.m; }
  5751. // Fetch the mesh object
  5752. var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
  5753. if (mesh == null) { return null; }
  5754. // If needed, check if this user has rights to do this
  5755. if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
  5756. if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { return null; }
  5757. }
  5758. var meshidhex = Buffer.from(req.query.id.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5759. var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
  5760. // Get the agent connection server name
  5761. var serverName = obj.getWebServerName(domain, req);
  5762. if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
  5763. // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
  5764. var xdomain = (domain.dns == null) ? domain.id : '';
  5765. if (xdomain != '') xdomain += '/';
  5766. var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
  5767. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  5768. if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
  5769. if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
  5770. if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
  5771. meshsettings += 'MeshServer=local\r\n';
  5772. 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'; }
  5773. }
  5774. 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'; }
  5775. if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
  5776. if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
  5777. if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
  5778. if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
  5779. if (domain.agentcustomization != null) { // Add agent customization
  5780. if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
  5781. if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
  5782. if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
  5783. if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
  5784. if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
  5785. if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
  5786. if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
  5787. if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
  5788. }
  5789. if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
  5790. return meshsettings;
  5791. }
  5792. // Handle a request to download a mesh settings
  5793. obj.handleMeshSettingsRequest = function (req, res) {
  5794. const domain = getDomain(req);
  5795. if (domain == null) { return; }
  5796. //if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5797. var meshsettings = getMshFromRequest(req, res, domain);
  5798. if (meshsettings == null) { res.sendStatus(401); return; }
  5799. // Get the agent filename
  5800. var meshagentFilename = 'meshagent';
  5801. if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
  5802. setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
  5803. res.send(meshsettings);
  5804. };
  5805. // Handle a request for power events
  5806. obj.handleDevicePowerEvents = function (req, res) {
  5807. const domain = checkUserIpAddress(req, res);
  5808. if (domain == null) { return; }
  5809. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
  5810. if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (req.query.id == null) || (typeof req.query.id != 'string')) { res.sendStatus(401); return; }
  5811. var x = req.query.id.split('/');
  5812. var user = obj.users[req.session.userid];
  5813. if ((x.length != 3) || (x[0] != 'node') || (x[1] != domain.id) || (user == null) || (user.links == null)) { res.sendStatus(401); return; }
  5814. obj.db.Get(req.query.id, function (err, docs) {
  5815. if (docs.length != 1) {
  5816. res.sendStatus(401);
  5817. } else {
  5818. var node = docs[0];
  5819. // Check if we have right to this node
  5820. if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
  5821. // See how we will convert UTC time to local time
  5822. var localTimeOffset = 0;
  5823. var timeConversionSystem = 0;
  5824. if ((req.query.l != null) && (req.query.tz != null)) {
  5825. timeConversionSystem = 1;
  5826. } else if (req.query.tf != null) {
  5827. // Get local time offset (bad way)
  5828. timeConversionSystem = 2;
  5829. localTimeOffset = parseInt(req.query.tf);
  5830. if (isNaN(localTimeOffset)) { localTimeOffset = 0; }
  5831. }
  5832. // Get the list of power events and send them
  5833. setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
  5834. obj.db.getPowerTimeline(node._id, function (err, docs) {
  5835. var xevents = ['UTC Time, Local Time, State, Previous State'], prevState = 0;
  5836. for (var i in docs) {
  5837. if (docs[i].power != prevState) {
  5838. var timedoc = docs[i].time;
  5839. if (typeof timedoc == 'string') {
  5840. timedoc = new Date(timedoc);
  5841. }
  5842. prevState = docs[i].power;
  5843. var localTime = '';
  5844. if (timeConversionSystem == 1) { // Good way
  5845. localTime = new Date(timedoc.getTime()).toLocaleString(req.query.l, { timeZone: req.query.tz })
  5846. } else if (timeConversionSystem == 2) { // Bad way
  5847. localTime = new Date(timedoc.getTime() + (localTimeOffset * 60000)).toISOString();
  5848. localTime = localTime.substring(0, localTime.length - 1);
  5849. }
  5850. if (docs[i].oldPower != null) {
  5851. xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power + ',' + docs[i].oldPower);
  5852. } else {
  5853. xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power);
  5854. }
  5855. }
  5856. }
  5857. res.send(xevents.join('\r\n'));
  5858. });
  5859. }
  5860. });
  5861. }
  5862. if (parent.pluginHandler != null) {
  5863. // Handle a plugin admin request
  5864. obj.handlePluginAdminReq = function (req, res) {
  5865. const domain = checkUserIpAddress(req, res);
  5866. if (domain == null) { return; }
  5867. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5868. var user = obj.users[req.session.userid];
  5869. if (user == null) { res.sendStatus(401); return; }
  5870. parent.pluginHandler.handleAdminReq(req, res, user, obj);
  5871. }
  5872. obj.handlePluginAdminPostReq = function (req, res) {
  5873. const domain = checkUserIpAddress(req, res);
  5874. if (domain == null) { return; }
  5875. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5876. var user = obj.users[req.session.userid];
  5877. if (user == null) { res.sendStatus(401); return; }
  5878. parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
  5879. }
  5880. obj.handlePluginJS = function (req, res) {
  5881. const domain = checkUserIpAddress(req, res);
  5882. if (domain == null) { return; }
  5883. if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
  5884. var user = obj.users[req.session.userid];
  5885. if (user == null) { res.sendStatus(401); return; }
  5886. parent.pluginHandler.refreshJS(req, res);
  5887. }
  5888. }
  5889. // Starts the HTTPS server, this should be called after the user/mesh tables are loaded
  5890. function serverStart() {
  5891. // Start the server, only after users and meshes are loaded from the database.
  5892. if (obj.args.tlsoffload) {
  5893. // Setup the HTTP server without TLS
  5894. obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5895. } else {
  5896. var ciphers = [
  5897. 'TLS_AES_256_GCM_SHA384',
  5898. 'TLS_AES_128_GCM_SHA256',
  5899. 'TLS_AES_128_CCM_8_SHA256',
  5900. 'TLS_AES_128_CCM_SHA256',
  5901. 'TLS_CHACHA20_POLY1305_SHA256',
  5902. 'ECDHE-RSA-AES256-GCM-SHA384',
  5903. 'ECDHE-ECDSA-AES256-GCM-SHA384',
  5904. 'ECDHE-RSA-AES128-GCM-SHA256',
  5905. 'ECDHE-ECDSA-AES128-GCM-SHA256',
  5906. 'DHE-RSA-AES128-GCM-SHA256',
  5907. 'ECDHE-RSA-CHACHA20-POLY1305', // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
  5908. 'ECDHE-ARIA128-GCM-SHA256',
  5909. 'ECDHE-ARIA256-GCM-SHA384',
  5910. 'ECDHE-RSA-AES128-SHA256', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
  5911. 'ECDHE-RSA-AES256-SHA384', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
  5912. '!aNULL',
  5913. '!eNULL',
  5914. '!EXPORT',
  5915. '!DES',
  5916. '!RC4',
  5917. '!MD5',
  5918. '!PSK',
  5919. '!SRP',
  5920. '!CAMELLIA'
  5921. ].join(':');
  5922. if (obj.useNodeDefaultTLSCiphers) {
  5923. ciphers = require("tls").DEFAULT_CIPHERS;
  5924. }
  5925. if (obj.tlsCiphers) {
  5926. ciphers = obj.tlsCiphers;
  5927. if (Array.isArray(obj.tlsCiphers)) {
  5928. ciphers = obj.tlsCiphers.join(":");
  5929. }
  5930. }
  5931. // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
  5932. //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
  5933. 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 };
  5934. if (obj.tlsSniCredentials != null) { tlsOptions.SNICallback = TlsSniCallback; } // We have multiple web server certificate used depending on the domain name
  5935. obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
  5936. obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
  5937. obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
  5938. //obj.tlsServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
  5939. obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
  5940. obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
  5941. obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5942. }
  5943. // Start a second agent-only server if needed
  5944. if (obj.args.agentport) {
  5945. var agentPortTls = true;
  5946. if (obj.args.tlsoffload != null && obj.args.tlsoffload != false) { agentPortTls = false; }
  5947. if (typeof obj.args.agentporttls == 'boolean') { agentPortTls = obj.args.agentporttls; }
  5948. if (obj.certificates.webdefault == null) { agentPortTls = false; }
  5949. if (agentPortTls == false) {
  5950. // Setup the HTTP server without TLS
  5951. obj.expressWsAlt = require('express-ws')(obj.agentapp, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5952. } else {
  5953. // Setup the agent HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
  5954. // If TLS is used on the agent port, we always use the default TLS certificate.
  5955. 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 };
  5956. obj.tlsAltServer = require('https').createServer(tlsOptions, obj.agentapp);
  5957. obj.tlsAltServer.on('secureConnection', function () { /*console.log('tlsAltServer secureConnection');*/ });
  5958. obj.tlsAltServer.on('error', function (err) { console.log('tlsAltServer error', err); });
  5959. //obj.tlsAltServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
  5960. obj.tlsAltServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
  5961. obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
  5962. obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
  5963. }
  5964. }
  5965. // Setup middleware
  5966. obj.app.engine('handlebars', obj.exphbs.engine({ defaultLayout: false }));
  5967. obj.app.set('view engine', 'handlebars');
  5968. if (obj.args.trustedproxy) {
  5969. // Reverse proxy should add the "X-Forwarded-*" headers
  5970. try {
  5971. obj.app.set('trust proxy', obj.args.trustedproxy);
  5972. } catch (ex) {
  5973. // If there is an error, try to resolve the string
  5974. if ((obj.args.trustedproxy.length == 1) && (typeof obj.args.trustedproxy[0] == 'string')) {
  5975. require('dns').lookup(obj.args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.trustedproxy = [address]; } });
  5976. }
  5977. }
  5978. }
  5979. else if (typeof obj.args.tlsoffload == 'object') {
  5980. // Reverse proxy should add the "X-Forwarded-*" headers
  5981. try {
  5982. obj.app.set('trust proxy', obj.args.tlsoffload);
  5983. } catch (ex) {
  5984. // If there is an error, try to resolve the string
  5985. if ((Array.isArray(obj.args.tlsoffload)) && (obj.args.tlsoffload.length == 1) && (typeof obj.args.tlsoffload[0] == 'string')) {
  5986. require('dns').lookup(obj.args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.tlsoffload = [address]; } });
  5987. }
  5988. }
  5989. }
  5990. // Setup a keygrip instance with higher default security, default hash is SHA1, we want to bump that up with SHA384
  5991. // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
  5992. // If args.sessionkey is a string, use it as a single key, but args.sessionkey can also be used as an array of keys.
  5993. const keygrip = require('keygrip')((typeof obj.args.sessionkey == 'string') ? [obj.args.sessionkey] : obj.args.sessionkey, 'sha384', 'base64');
  5994. // Setup the cookie session
  5995. const sessionOptions = {
  5996. name: 'xid', // Recommended security practice to not use the default cookie name
  5997. httpOnly: true,
  5998. keys: keygrip,
  5999. secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
  6000. sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax')
  6001. }
  6002. if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60000); } // sessiontime is minutes
  6003. obj.app.use(require('cookie-session')(sessionOptions));
  6004. obj.app.use(function (request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
  6005. if (request.session && !request.session.regenerate) {
  6006. request.session.regenerate = function (cb) {
  6007. cb()
  6008. }
  6009. }
  6010. if (request.session && !request.session.save) {
  6011. request.session.save = function (cb) {
  6012. cb()
  6013. }
  6014. }
  6015. // Special Client Hint Headers for Browser Detection on every request - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers#client_hints
  6016. // note: only works in a secure context (localhost or https://)
  6017. if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(request.hostname) == -1)) {
  6018. const secCH = [
  6019. 'Sec-CH-UA-Arch', 'Sec-CH-UA-Bitness', 'Sec-CH-UA-Form-Factors', 'Sec-CH-UA-Full-Version',
  6020. 'Sec-CH-UA-Full-Version-List', 'Sec-CH-UA-Mobile', 'Sec-CH-UA-Model', 'Sec-CH-UA-Platform',
  6021. 'Sec-CH-UA-Platform-Version', 'Sec-CH-UA-WoW64'
  6022. ];
  6023. response.setHeader('Accept-CH', secCH.join(', '));
  6024. response.setHeader('Critical-CH', secCH.join(', '));
  6025. }
  6026. next();
  6027. });
  6028. // Handle all incoming web sockets, see if some need to be handled as web relays
  6029. obj.app.ws('/*', function (ws, req, next) {
  6030. // Global error catcher
  6031. ws.on('error', function (err) { parent.debug('web', 'GENERAL WSERR: ' + err); console.log(err); });
  6032. if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) { handleWebRelayWebSocket(ws, req); return; }
  6033. return next();
  6034. });
  6035. // Add HTTP security headers to all responses
  6036. obj.app.use(async function (req, res, next) {
  6037. // Check if a session is destroyed
  6038. if (typeof req.session.userid == 'string') {
  6039. if (typeof req.session.x == 'string') {
  6040. if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) {
  6041. delete req.session.userid;
  6042. delete req.session.ip;
  6043. delete req.session.t;
  6044. delete req.session.x;
  6045. }
  6046. } else {
  6047. // Legacy session without a random, add one.
  6048. setSessionRandom(req);
  6049. }
  6050. }
  6051. // Remove legacy values from the session to keep the session as small as possible
  6052. delete req.session.u2f;
  6053. delete req.session.domainid;
  6054. delete req.session.nowInMinutes;
  6055. delete req.session.tokenuserid;
  6056. delete req.session.tokenusername;
  6057. delete req.session.tokenpassword;
  6058. delete req.session.tokenemail;
  6059. delete req.session.tokensms;
  6060. delete req.session.tokenpush;
  6061. delete req.session.tusername;
  6062. delete req.session.tpassword;
  6063. // Useful for debugging reverse proxy issues
  6064. parent.debug('httpheaders', req.method, req.url, req.headers);
  6065. // If this request came over HTTP, redirect to HTTPS
  6066. if (req.headers['x-forwarded-proto'] == 'http') {
  6067. var host = req.headers.host;
  6068. if (typeof host == 'string') { host = host.split(':')[0]; }
  6069. if ((host == null) && (obj.certificates != null)) { host = obj.certificates.CommonName; if (obj.certificates.CommonName.indexOf('.') == -1) { host = req.headers.host; } }
  6070. var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
  6071. res.redirect('https://' + host + ':' + httpsPort + req.url);
  6072. return;
  6073. }
  6074. // Perform traffic accounting
  6075. if (req.headers.upgrade == 'websocket') {
  6076. // We don't count traffic on WebSockets since it's counted by the handling modules.
  6077. obj.trafficStats.httpWebSocketCount++;
  6078. } else {
  6079. // Normal HTTP traffic is counted
  6080. obj.trafficStats.httpRequestCount++;
  6081. if (typeof req.socket.xbytesRead != 'number') {
  6082. req.socket.xbytesRead = 0;
  6083. req.socket.xbytesWritten = 0;
  6084. req.socket.on('close', function () {
  6085. // Perform final accounting
  6086. obj.trafficStats.httpIn += (this.bytesRead - this.xbytesRead);
  6087. obj.trafficStats.httpOut += (this.bytesWritten - this.xbytesWritten);
  6088. this.xbytesRead = this.bytesRead;
  6089. this.xbytesWritten = this.bytesWritten;
  6090. });
  6091. } else {
  6092. // Update counters
  6093. obj.trafficStats.httpIn += (req.socket.bytesRead - req.socket.xbytesRead);
  6094. obj.trafficStats.httpOut += (req.socket.bytesWritten - req.socket.xbytesWritten);
  6095. req.socket.xbytesRead = req.socket.bytesRead;
  6096. req.socket.xbytesWritten = req.socket.bytesWritten;
  6097. }
  6098. }
  6099. // Set the real IP address of the request
  6100. // If a trusted reverse-proxy is sending us the remote IP address, use it.
  6101. var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
  6102. if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
  6103. if (
  6104. (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
  6105. ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
  6106. ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
  6107. ) {
  6108. // Get client IP
  6109. if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
  6110. req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
  6111. } else if (req.headers['x-forwarded-for']) {
  6112. req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
  6113. } else if (req.headers['x-real-ip']) {
  6114. req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
  6115. } else {
  6116. req.clientIp = ipex;
  6117. }
  6118. // 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.
  6119. const clientIpSplit = req.clientIp.split(':');
  6120. if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
  6121. // Get server host
  6122. 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.
  6123. } else {
  6124. req.clientIp = ipex;
  6125. }
  6126. // If this is a web relay connection, handle it here.
  6127. if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) {
  6128. if (['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; }
  6129. }
  6130. // Get the domain for this request
  6131. const domain = req.xdomain = getDomain(req);
  6132. parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
  6133. // Skip the rest if this is an agent connection
  6134. if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
  6135. // Setup security headers
  6136. const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
  6137. var selfurl = ' wss://' + req.headers.host;
  6138. if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
  6139. const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
  6140. // If the web relay port is enabled, allow the web page to redirect to it
  6141. var extraFrameSrc = '';
  6142. if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
  6143. extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
  6144. if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
  6145. }
  6146. // If using duo add apihostname to CSP
  6147. var duoSrc = '';
  6148. if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.apihostname == 'string')) {
  6149. duoSrc = domain.duo2factor.apihostname;
  6150. }
  6151. // Finish setup security headers
  6152. const headers = {
  6153. 'Referrer-Policy': 'no-referrer',
  6154. 'X-XSS-Protection': '1; mode=block',
  6155. 'X-Content-Type-Options': 'nosniff',
  6156. 'Content-Security-Policy': "default-src 'none'; font-src 'self' fonts.gstatic.com data:; script-src 'self' 'unsafe-inline' " + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-src 'self' blob: mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self' " + duoSrc + "; manifest-src 'self'"
  6157. };
  6158. 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
  6159. if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
  6160. 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'; } }
  6161. // If this domain has configured headers, add them. If a header is set to null, remove it.
  6162. if ((domain != null) && (domain.httpheaders != null) && (typeof domain.httpheaders == 'object')) {
  6163. for (var i in domain.httpheaders) { if (domain.httpheaders[i] === null) { delete headers[i]; } else { headers[i] = domain.httpheaders[i]; } }
  6164. }
  6165. res.set(headers);
  6166. // Check the session if bound to the external IP address
  6167. if ((req.session.ip != null) && (req.clientIp != null) && !checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
  6168. // Extend the session time by forcing a change to the session every minute.
  6169. if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
  6170. // Check CrowdSec Bounser if configured
  6171. if ((parent.crowdSecBounser != null) && (req.headers['upgrade'] != 'websocket') && (req.session.userid == null)) { if ((await parent.crowdSecBounser.process(domain, req, res, next)) == true) { return; } }
  6172. // Debugging code, this will stop the agent from crashing if two responses are made to the same request.
  6173. const render = res.render;
  6174. const send = res.send;
  6175. res.render = function renderWrapper(...args) {
  6176. Error.captureStackTrace(this);
  6177. return render.apply(this, args);
  6178. };
  6179. res.send = function sendWrapper(...args) {
  6180. try {
  6181. send.apply(this, args);
  6182. } catch (err) {
  6183. console.error(`Error in res.send | ${err.code} | ${err.message} | ${res.stack}`);
  6184. try {
  6185. var errlogpath = null;
  6186. if (typeof parent.args.mesherrorlogpath == 'string') { errlogpath = parent.path.join(parent.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = parent.getConfigFilePath('mesherrors.txt'); }
  6187. parent.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + `Error in res.send | ${err.code} | ${err.message} | ${res.stack}` + '\r\n');
  6188. } catch (ex) { parent.debug('error', 'Unable to write to mesherrors.txt.'); }
  6189. }
  6190. };
  6191. // Continue processing the request
  6192. return next();
  6193. });
  6194. if (obj.agentapp) {
  6195. // Add HTTP security headers to all responses
  6196. obj.agentapp.use(function (req, res, next) {
  6197. // Set the real IP address of the request
  6198. // If a trusted reverse-proxy is sending us the remote IP address, use it.
  6199. var ipex = '0.0.0.0';
  6200. if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
  6201. if (
  6202. (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
  6203. ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
  6204. ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
  6205. ) {
  6206. if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
  6207. req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
  6208. } else if (req.headers['x-forwarded-for']) {
  6209. req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
  6210. } else if (req.headers['x-real-ip']) {
  6211. req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
  6212. } else {
  6213. req.clientIp = ipex;
  6214. }
  6215. } else {
  6216. req.clientIp = ipex;
  6217. }
  6218. // Get the domain for this request
  6219. const domain = req.xdomain = getDomain(req);
  6220. parent.debug('webrequest', '(' + req.clientIp + ') AgentPort: ' + req.url);
  6221. res.removeHeader('X-Powered-By');
  6222. return next();
  6223. });
  6224. }
  6225. // Setup all sharing domains and check if auth strategies need setup
  6226. var setupSSO = false
  6227. for (var i in parent.config.domains) {
  6228. 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)); }
  6229. if (typeof parent.config.domains[i].authstrategies == 'object') { setupSSO = true };
  6230. }
  6231. if (setupSSO) {
  6232. setupAllDomainAuthStrategies().then(() => finalizeWebserver());
  6233. } else {
  6234. finalizeWebserver()
  6235. }
  6236. // Setup all domain auth strategy passport.js
  6237. async function setupAllDomainAuthStrategies() {
  6238. for (var i in parent.config.domains) {
  6239. if (parent.config.domains[i].dns != null) {
  6240. if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { 'authStrategyFlags': 0 }; }
  6241. parent.config.domains[''].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
  6242. } else {
  6243. if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { 'authStrategyFlags': 0 }; }
  6244. parent.config.domains[i].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
  6245. }
  6246. }
  6247. }
  6248. function setupHTTPHandlers() {
  6249. // Setup all HTTP handlers
  6250. if (parent.pluginHandler != null) {
  6251. parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
  6252. }
  6253. if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
  6254. for (var i in parent.config.domains) {
  6255. 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.
  6256. var domain = parent.config.domains[i];
  6257. var url = domain.url;
  6258. if (typeof domain.rootredirect == 'string') {
  6259. // Root page redirects the user to a different URL
  6260. obj.app.get(url, handleRootRedirect);
  6261. } else {
  6262. // Present the login page as the root page
  6263. obj.app.get(url, handleRootRequest);
  6264. obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
  6265. }
  6266. obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
  6267. if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
  6268. if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
  6269. obj.app.get(url + 'terms', handleTermsRequest);
  6270. obj.app.get(url + 'xterm', handleXTermRequest);
  6271. obj.app.get(url + 'login', handleRootRequest);
  6272. obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
  6273. obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
  6274. obj.app.get(url + 'logout', handleLogoutRequest);
  6275. obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
  6276. obj.app.get(url + 'manifest.json', handleManifestRequest);
  6277. obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
  6278. obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
  6279. obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
  6280. obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
  6281. obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
  6282. obj.app.get(url + 'checkmail', handleCheckMailRequest);
  6283. obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
  6284. obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
  6285. obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
  6286. obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
  6287. obj.app.get(url + 'messenger', handleMessengerRequest);
  6288. obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
  6289. obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
  6290. obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
  6291. obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
  6292. obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
  6293. obj.app.get(url + 'commander.ashx', handleMeshCommander);
  6294. obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
  6295. obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
  6296. obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
  6297. obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
  6298. obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
  6299. obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
  6300. obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
  6301. obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
  6302. obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
  6303. obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
  6304. obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
  6305. 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); }); });
  6306. obj.app.ws(url + 'control.ashx', function (ws, req) {
  6307. getWebsocketArgs(ws, req, function (ws, req) {
  6308. const domain = getDomain(req);
  6309. if (obj.CheckWebServerOriginName(domain, req) == false) {
  6310. try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
  6311. try { ws.close(); } catch (ex) { }
  6312. return;
  6313. }
  6314. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { // Check 3FA URL key
  6315. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'nokey' })); } catch (ex) { }
  6316. try { ws.close(); } catch (ex) { }
  6317. return;
  6318. }
  6319. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6320. if (user == null) { // User is not authenticated, perform inner server authentication
  6321. if (req.headers['x-meshauth'] === '*') {
  6322. 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
  6323. } else {
  6324. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth' })); } catch (ex) { }
  6325. try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
  6326. }
  6327. } else {
  6328. obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
  6329. }
  6330. });
  6331. });
  6332. });
  6333. obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
  6334. obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
  6335. obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
  6336. obj.app.get(url + 'logo.png', handleLogoRequest);
  6337. obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
  6338. obj.app.get(url + 'pwalogo.png', handlePWALogoRequest);
  6339. obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
  6340. obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
  6341. obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
  6342. obj.app.get(url + 'recordings.ashx', handleGetRecordings);
  6343. obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
  6344. obj.app.get(url + 'player.htm', handlePlayerRequest);
  6345. obj.app.get(url + 'player', handlePlayerRequest);
  6346. obj.app.get(url + 'sharing', handleSharingRequest);
  6347. obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
  6348. obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
  6349. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6350. if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
  6351. obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
  6352. } else {
  6353. obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
  6354. }
  6355. });
  6356. });
  6357. if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
  6358. obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
  6359. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6360. if ((user == null) || (cookie == null)) {
  6361. try { ws1.close(); } catch (ex) { }
  6362. } else {
  6363. obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
  6364. }
  6365. });
  6366. });
  6367. }
  6368. obj.app.get(url + 'invite', handleInviteRequest);
  6369. obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
  6370. if (parent.pluginHandler != null) {
  6371. obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
  6372. obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
  6373. obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
  6374. }
  6375. // New account CAPTCHA request
  6376. if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
  6377. obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
  6378. }
  6379. // Check CrowdSec Bounser if configured
  6380. if (parent.crowdSecBounser != null) {
  6381. obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
  6382. obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
  6383. }
  6384. // Setup IP-KVM relay if supported
  6385. if (domain.ipkvm) {
  6386. obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
  6387. const domain = getDomain(req);
  6388. if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
  6389. parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
  6390. });
  6391. obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
  6392. const domain = getDomain(req);
  6393. if (domain == null) return;
  6394. parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
  6395. });
  6396. }
  6397. // Setup RDP unless indicated as disabled
  6398. if (domain.mstsc !== false) {
  6399. obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
  6400. obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
  6401. const domain = getDomain(req);
  6402. if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
  6403. // If no user is logged in and we have a default user, set it now.
  6404. 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(); }
  6405. try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6406. });
  6407. }
  6408. // Setup SSH if needed
  6409. if (domain.ssh === true) {
  6410. obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
  6411. obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
  6412. const domain = getDomain(req);
  6413. if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
  6414. // If no user is logged in and we have a default user, set it now.
  6415. 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(); }
  6416. try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6417. });
  6418. obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
  6419. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6420. require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
  6421. });
  6422. });
  6423. obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
  6424. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6425. require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
  6426. });
  6427. });
  6428. }
  6429. // Setup firebase push only server
  6430. if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
  6431. if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
  6432. if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
  6433. }
  6434. // Setup auth strategies using passport if needed
  6435. if (typeof domain.authstrategies == 'object') {
  6436. parent.authLog('setupHTTPHandlers', `Setting up authentication strategies login and callback URLs for ${domain.id == '' ? 'root' : '"' + domain.id + '"'} domain.`);
  6437. // Twitter
  6438. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
  6439. obj.app.get(url + 'auth-twitter', function (req, res, next) {
  6440. var domain = getDomain(req);
  6441. if (domain.passport == null) { next(); return; }
  6442. domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
  6443. });
  6444. obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
  6445. var domain = getDomain(req);
  6446. if (domain.passport == null) { next(); return; }
  6447. if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
  6448. // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
  6449. var url = req.url;
  6450. if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
  6451. res.set('Content-Type', 'text/html');
  6452. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
  6453. } else {
  6454. domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
  6455. }
  6456. }, handleStrategyLogin);
  6457. }
  6458. // Google
  6459. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
  6460. obj.app.get(url + 'auth-google', function (req, res, next) {
  6461. var domain = getDomain(req);
  6462. if (domain.passport == null) { next(); return; }
  6463. domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
  6464. });
  6465. obj.app.get(url + 'auth-google-callback', function (req, res, next) {
  6466. var domain = getDomain(req);
  6467. if (domain.passport == null) { next(); return; }
  6468. domain.passport.authenticate('google-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
  6469. }, handleStrategyLogin);
  6470. }
  6471. // GitHub
  6472. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
  6473. obj.app.get(url + 'auth-github', function (req, res, next) {
  6474. var domain = getDomain(req);
  6475. if (domain.passport == null) { next(); return; }
  6476. domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
  6477. });
  6478. obj.app.get(url + 'auth-github-callback', function (req, res, next) {
  6479. var domain = getDomain(req);
  6480. if (domain.passport == null) { next(); return; }
  6481. domain.passport.authenticate('github-' + domain.id, { failureRedirect: domain.url })(req, res, next);
  6482. }, handleStrategyLogin);
  6483. }
  6484. // Azure
  6485. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
  6486. obj.app.get(url + 'auth-azure', function (req, res, next) {
  6487. var domain = getDomain(req);
  6488. if (domain.passport == null) { next(); return; }
  6489. domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
  6490. });
  6491. obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
  6492. var domain = getDomain(req);
  6493. if (domain.passport == null) { next(); return; }
  6494. if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
  6495. // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
  6496. var url = req.url;
  6497. if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
  6498. res.set('Content-Type', 'text/html');
  6499. res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
  6500. } else {
  6501. if (req.query.state != null) {
  6502. var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
  6503. if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: domain.url })(req, res, next); return; }
  6504. }
  6505. next();
  6506. }
  6507. }, handleStrategyLogin);
  6508. }
  6509. // Setup OpenID Connect URLs
  6510. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
  6511. let authURL = url + 'auth-oidc'
  6512. parent.authLog('setupHTTPHandlers', `OIDC: Authorization URL: ${authURL}`);
  6513. obj.app.get(authURL, function (req, res, next) {
  6514. var domain = getDomain(req);
  6515. if (domain.passport == null) { next(); return; }
  6516. domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6517. });
  6518. let redirectPath;
  6519. if (typeof domain.authstrategies.oidc.client.redirect_uri == 'string') {
  6520. redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uri)).pathname;
  6521. } else if (Array.isArray(domain.authstrategies.oidc.client.redirect_uris)) {
  6522. redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uris[0])).pathname;
  6523. } else {
  6524. redirectPath = url + 'auth-oidc-callback';
  6525. }
  6526. parent.authLog('setupHTTPHandlers', `OIDC: Callback URL: ${redirectPath}`);
  6527. obj.app.get(redirectPath, obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6528. var domain = getDomain(req);
  6529. if (domain.passport == null) { next(); return; }
  6530. if (req.session && req.session.userid) { next(); return; } // already logged in so dont authenticate just carry on
  6531. if (req.session && req.session['oidc-' + domain.id]) { // we have a request to login so do authenticate
  6532. domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6533. } else { // no idea so carry on
  6534. next(); return;
  6535. }
  6536. }, handleStrategyLogin);
  6537. }
  6538. // Generic SAML
  6539. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
  6540. obj.app.get(url + 'auth-saml', function (req, res, next) {
  6541. var domain = getDomain(req);
  6542. if (domain.passport == null) { next(); return; }
  6543. //set RelayState when queries are passed
  6544. if (Object.keys(req.query).length != 0){
  6545. req.query.RelayState = encodeURIComponent(`${req.protocol}://${req.hostname}${req.originalUrl}`.replace('auth-saml/',''))
  6546. }
  6547. domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6548. });
  6549. obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6550. var domain = getDomain(req);
  6551. if (domain.passport == null) { next(); return; }
  6552. domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6553. }, handleStrategyLogin);
  6554. }
  6555. // Intel SAML
  6556. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
  6557. obj.app.get(url + 'auth-intel', function (req, res, next) {
  6558. var domain = getDomain(req);
  6559. if (domain.passport == null) { next(); return; }
  6560. domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6561. });
  6562. obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6563. var domain = getDomain(req);
  6564. if (domain.passport == null) { next(); return; }
  6565. domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6566. }, handleStrategyLogin);
  6567. }
  6568. // JumpCloud SAML
  6569. if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
  6570. obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
  6571. var domain = getDomain(req);
  6572. if (domain.passport == null) { next(); return; }
  6573. domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6574. });
  6575. obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
  6576. var domain = getDomain(req);
  6577. if (domain.passport == null) { next(); return; }
  6578. domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
  6579. }, handleStrategyLogin);
  6580. }
  6581. }
  6582. // Setup Duo HTTP handlers if supported
  6583. if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
  6584. // Duo authentication handler
  6585. obj.app.get(url + 'auth-duo', function (req, res){
  6586. var domain = getDomain(req);
  6587. const sec = parent.decryptSessionData(req.session.e);
  6588. if ((req.query.state !== sec.duostate) || (req.query.duo_code == null)) {
  6589. // The state returned from Duo is not the same as what was in the session, so must fail
  6590. parent.debug('web', 'handleRootRequest: Duo 2FA state failed.');
  6591. req.session.loginmode = 1;
  6592. req.session.messageid = 117; // Invalid security check
  6593. res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page
  6594. return;
  6595. } else {
  6596. const duo = require('@duosecurity/duo_universal');
  6597. const client = new duo.Client({
  6598. clientId: domain.duo2factor.integrationkey,
  6599. clientSecret: domain.duo2factor.secretkey,
  6600. apiHost: domain.duo2factor.apihostname,
  6601. redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
  6602. });
  6603. if (sec.duoconfig == 1) {
  6604. // Login data correct, now exchange authorization code for 2FA
  6605. var userid = req.session.userid;
  6606. client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid.split('/')[2]).then(function (data) {
  6607. // Duo 2FA exchange success
  6608. parent.debug('web', 'handleRootRequest: Duo 2FA configuration success.');
  6609. // Enable Duo for this user
  6610. var user = obj.users[userid];
  6611. if (user.otpduo == null) {
  6612. user.otpduo = {};
  6613. db.SetUser(user);
  6614. // Notify change
  6615. var targets = ['*', 'server-users', user._id];
  6616. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  6617. var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 160, msg: "Enabled duo two-factor authentication.", domain: domain.id };
  6618. 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.
  6619. parent.DispatchEvent(targets, obj, event);
  6620. }
  6621. // Clear the Duo state
  6622. delete sec.duostate;
  6623. delete sec.duoconfig;
  6624. req.session.e = parent.encryptSessionData(sec);
  6625. var url = req.session.duorurl;
  6626. delete req.session.duorurl;
  6627. res.redirect(url ? url : domain.url); // Redirect back to the user's original page
  6628. }).catch(function (err) {
  6629. const sec = parent.decryptSessionData(req.session.e);
  6630. // Duo 2FA exchange success
  6631. parent.debug('web', 'handleRootRequest: Duo 2FA configuration failed.');
  6632. // Clear the Duo state
  6633. delete sec.duostate;
  6634. delete sec.duoconfig;
  6635. req.session.e = parent.encryptSessionData(sec);
  6636. var url = req.session.duorurl;
  6637. delete req.session.duorurl;
  6638. res.redirect(url ? url : domain.url); // Redirect back to the user's original page
  6639. });
  6640. } else {
  6641. // User credentials are stored in session, just check again and get userid
  6642. obj.authenticate(sec.tuser, sec.tpass, domain, function (err, userid, passhint, loginOptions) {
  6643. if ((userid != null) && (err == null)) {
  6644. var user = obj.users[userid]; // Get user object
  6645. // Login data correct, now exchange authorization code for 2FA
  6646. client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid.split('/')[2]).then(function (data) {
  6647. const sec = parent.decryptSessionData(req.session.e);
  6648. // Duo 2FA exchange success
  6649. parent.debug('web', 'handleRootRequest: Duo 2FA authorization success.');
  6650. req.session.userid = userid;
  6651. delete req.session.currentNode;
  6652. req.session.ip = req.clientIp; // Bind this session to the IP address of the request
  6653. setSessionRandom(req);
  6654. // Clear the Duo state and user/pass
  6655. delete sec.duostate;
  6656. delete sec.tuser;
  6657. delete sec.tpass;
  6658. req.session.e = parent.encryptSessionData(sec);
  6659. obj.parent.authLog('https', 'Accepted Duo authentication for ' + userid + ' from ' + req.clientIp + ':' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
  6660. // Notify account login
  6661. var targets = ['*', 'server-users', user._id];
  6662. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  6663. const ua = obj.getUserAgentInfo(req);
  6664. 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: 'duo' };
  6665. obj.parent.DispatchEvent(targets, obj, loginEvent);
  6666. res.redirect(domain.url + getQueryPortion(req));
  6667. }).catch(function (err) {
  6668. const sec = parent.decryptSessionData(req.session.e);
  6669. // Duo 2FA exchange failed
  6670. parent.debug('web', 'handleRootRequest: Duo 2FA authorization failed.');
  6671. // Clear the Duo state
  6672. delete sec.duostate;
  6673. req.session.e = parent.encryptSessionData(sec);
  6674. req.session.loginmode = 1;
  6675. req.session.messageid = 117; // Invalid security check
  6676. // Notify account 2fa failed login
  6677. const ua = obj.getUserAgentInfo(req);
  6678. 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] });
  6679. obj.setbad2Fa(req);
  6680. res.redirect(domain.url + getQueryPortion(req));
  6681. });
  6682. } else {
  6683. // Login failed
  6684. parent.debug('web', 'handleRootRequest: login authorization failed when returning from Duo 2FA.');
  6685. req.session.loginmode = 1;
  6686. res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page
  6687. return;
  6688. }
  6689. });
  6690. }
  6691. }
  6692. });
  6693. // Configure Duo handler
  6694. obj.app.get(url + 'add-duo', function (req, res) {
  6695. var domain = getDomain(req);
  6696. if (req.session.userid == null) {
  6697. res.sendStatus(404);
  6698. } else {
  6699. // Redirect to Duo here
  6700. const duo = require('@duosecurity/duo_universal');
  6701. const client = new duo.Client({
  6702. clientId: domain.duo2factor.integrationkey,
  6703. clientSecret: domain.duo2factor.secretkey,
  6704. apiHost: domain.duo2factor.apihostname,
  6705. redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('&key=' + domain.loginkey) : '')
  6706. });
  6707. // Setup the Duo configuration
  6708. if (req.query.rurl) { req.session.duorurl = req.query.rurl; } // Set Duo return URL
  6709. const sec = parent.decryptSessionData(req.session.e);
  6710. sec.duostate = client.generateState();
  6711. sec.duoconfig = 1;
  6712. req.session.e = parent.encryptSessionData(sec);
  6713. parent.debug('web', 'Redirecting user ' + req.session.userid + ' to Duo for configuration');
  6714. res.redirect(client.createAuthUrl(req.session.userid.split('/')[2], sec.duostate));
  6715. }
  6716. });
  6717. }
  6718. // Server redirects
  6719. 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); } } }
  6720. // Server picture
  6721. obj.app.get(url + 'serverpic.ashx', function (req, res) {
  6722. // Check if we have "server.jpg" in the data folder, if so, use that.
  6723. if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
  6724. res.set({ 'Content-Type': 'image/png' });
  6725. res.send(parent.configurationFiles['server.png']);
  6726. } else {
  6727. // Check if we have "server.jpg" in the data folder, if so, use that.
  6728. var p = obj.path.join(obj.parent.datapath, 'server.png');
  6729. if (obj.fs.existsSync(p)) {
  6730. // Use the data folder server picture
  6731. try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
  6732. } else {
  6733. var domain = getDomain(req);
  6734. if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
  6735. // Use the domain server picture
  6736. try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6737. } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
  6738. // Use the override server picture
  6739. try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6740. } else {
  6741. // Use the default server picture
  6742. try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
  6743. }
  6744. }
  6745. }
  6746. });
  6747. // Receive mesh agent connections
  6748. obj.app.ws(url + 'agent.ashx', function (ws, req) {
  6749. var domain = checkAgentIpAddress(ws, req);
  6750. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6751. 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.
  6752. //console.log('Agent connect: ' + req.clientIp);
  6753. try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
  6754. });
  6755. // Setup MQTT broker over websocket
  6756. if (obj.parent.mqttbroker != null) {
  6757. obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
  6758. var domain = checkAgentIpAddress(ws, req);
  6759. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6760. var serialtunnel = SerialTunnel();
  6761. serialtunnel.xtransport = 'ws';
  6762. serialtunnel.xdomain = domain;
  6763. serialtunnel.xip = req.clientIp;
  6764. ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
  6765. serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
  6766. ws.on('close', function () { serialtunnel.emit('end'); });
  6767. obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
  6768. });
  6769. }
  6770. // Setup any .well-known folders
  6771. var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
  6772. if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
  6773. // Setup the alternative agent-only port
  6774. if (obj.agentapp) {
  6775. // Receive mesh agent connections on alternate port
  6776. obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
  6777. var domain = checkAgentIpAddress(ws, req);
  6778. if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
  6779. 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.
  6780. try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
  6781. });
  6782. // Setup mesh relay on alternative agent-only port
  6783. obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
  6784. PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
  6785. if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
  6786. obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
  6787. } else {
  6788. obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
  6789. }
  6790. });
  6791. });
  6792. // Allows agents to transfer files
  6793. obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
  6794. // Setup agent to/from server file transfer handler
  6795. obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
  6796. // Setup agent downloads for meshcore updates
  6797. obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
  6798. // Setup agent file downloads
  6799. obj.agentapp.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
  6800. // Setup APF.ashx for AMT communication
  6801. if (obj.parent.mpsserver != null) {
  6802. obj.agentapp.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
  6803. }
  6804. }
  6805. // Setup web relay on this web server if needed
  6806. // We set this up when a DNS name is used as a web relay instead of a port
  6807. if (obj.args.relaydns != null) {
  6808. obj.webRelayRouter = require('express').Router();
  6809. // This is the magic URL that will setup the relay session
  6810. obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
  6811. if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
  6812. 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
  6813. res.set({ 'Cache-Control': 'no-store' });
  6814. parent.debug('web', 'webRelaySetup');
  6815. // Decode the relay cookie
  6816. if (req.query.c == null) { res.sendStatus(404); return; }
  6817. // Decode and check if this relay cookie is valid
  6818. var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
  6819. 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.
  6820. if (urlCookie == null) { res.sendStatus(404); return; }
  6821. // Decode the incoming cookie
  6822. if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
  6823. if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
  6824. // This is a standard user, figure out what our web relay will be.
  6825. if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
  6826. if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
  6827. if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
  6828. userid = req.session.userid;
  6829. domainid = userid.split('/')[1];
  6830. domain = parent.config.domains[domainid];
  6831. nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
  6832. addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
  6833. port = parseInt(req.query.p);
  6834. appid = parseInt(req.query.appid);
  6835. webSessionId = req.session.userid + '/' + req.session.x;
  6836. // Check that all the required arguments are present
  6837. 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; }
  6838. } else if (urlCookie.r == 8) {
  6839. // This is a guest user, figure out what our web relay will be.
  6840. userid = urlCookie.userid;
  6841. domainid = userid.split('/')[1];
  6842. domain = parent.config.domains[domainid];
  6843. nodeid = urlCookie.nid;
  6844. addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
  6845. port = urlCookie.port;
  6846. appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
  6847. webSessionId = userid + '/' + urlCookie.pid;
  6848. publicid = urlCookie.pid;
  6849. if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
  6850. if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
  6851. if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
  6852. expire = urlCookie.expire;
  6853. if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
  6854. }
  6855. // No session identifier was setup, exit now
  6856. if (webSessionId == null) { res.sendStatus(404); return; }
  6857. // Check that we have an exact session on any of the relay DNS names
  6858. var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
  6859. for (var hostIndex in obj.args.relaydns) {
  6860. const host = obj.args.relaydns[hostIndex];
  6861. xrelaySessionId = webSessionId + '/' + host;
  6862. xrelaySession = webRelaySessions[xrelaySessionId];
  6863. if (xrelaySession == null) {
  6864. // We found an unused hostname, save this as it could be useful.
  6865. if (freeRelayHost == null) { freeRelayHost = host; }
  6866. } else {
  6867. // Check if we already have a relay session that matches exactly what we want
  6868. if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
  6869. // We found an exact match, we are all setup already, redirect to root of that DNS name
  6870. if (host == req.hostname) {
  6871. // Request was made on the same host, redirect to root.
  6872. res.redirect('/');
  6873. } else {
  6874. // Request was made to a different host
  6875. const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  6876. res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
  6877. }
  6878. return;
  6879. }
  6880. // Keep a record of the oldest web relay session, this could be useful.
  6881. if (oldestRelayHost == null) {
  6882. // Oldest host not set yet, set it
  6883. oldestRelayHost = host;
  6884. oldestRelayTime = xrelaySession.lastOperation;
  6885. } else {
  6886. // Check if this host is older then oldest so far
  6887. if (oldestRelayTime > xrelaySession.lastOperation) {
  6888. oldestRelayHost = host;
  6889. oldestRelayTime = xrelaySession.lastOperation;
  6890. }
  6891. }
  6892. }
  6893. }
  6894. // Check that the user has rights to access this device
  6895. parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
  6896. // If there is no remote control or relay rights, reject this web relay
  6897. if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
  6898. // Check if there is a free relay DNS name we can use
  6899. var selectedHost = null;
  6900. if (freeRelayHost != null) {
  6901. // There is a free one, use it.
  6902. selectedHost = freeRelayHost;
  6903. } else {
  6904. // No free ones, close the oldest one
  6905. selectedHost = oldestRelayHost;
  6906. }
  6907. xrelaySessionId = webSessionId + '/' + selectedHost;
  6908. if (selectedHost == req.hostname) {
  6909. // If this web relay session id is not free, close it now
  6910. xrelaySession = webRelaySessions[xrelaySessionId];
  6911. if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
  6912. // Create a web relay session
  6913. const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
  6914. relaySession.xpublicid = publicid;
  6915. relaySession.onclose = function (sessionId) {
  6916. // Remove the relay session
  6917. delete webRelaySessions[sessionId];
  6918. // If there are not more relay sessions, clear the cleanup timer
  6919. if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
  6920. }
  6921. // Set the multi-tunnel session
  6922. webRelaySessions[xrelaySessionId] = relaySession;
  6923. // Setup the cleanup timer if needed
  6924. if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
  6925. // Redirect to root.
  6926. res.redirect('/');
  6927. } else {
  6928. if (req.query.noredirect != null) {
  6929. // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
  6930. res.sendStatus(404);
  6931. } else {
  6932. // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
  6933. const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  6934. res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
  6935. }
  6936. }
  6937. });
  6938. });
  6939. // Handle all incoming requests as web relays
  6940. obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6941. // Handle all incoming requests as web relays
  6942. obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6943. // Handle all incoming requests as web relays
  6944. obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6945. // Handle all incoming requests as web relays
  6946. obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6947. // Handle all incoming requests as web relays
  6948. obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6949. // Handle all incoming requests as web relays
  6950. obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
  6951. }
  6952. // Indicates to ExpressJS that the override public folder should be used to serve static files.
  6953. obj.app.use(url, function(req, res, next){
  6954. var domain = getDomain(req);
  6955. if (domain.webpublicpath != null) { // Use domain public path
  6956. obj.express.static(domain.webpublicpath)(req, res, next);
  6957. } else if (obj.parent.webPublicOverridePath != null) { // Use override path
  6958. obj.express.static(obj.parent.webPublicOverridePath)(req, res, next);
  6959. } else { // carry on and use default public path
  6960. next();
  6961. }
  6962. });
  6963. // Indicates to ExpressJS that the default public folder should be used to serve static files.
  6964. obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
  6965. // Start regular disconnection list flush every 2 minutes.
  6966. obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
  6967. }
  6968. }
  6969. function finalizeWebserver() {
  6970. // Setup all HTTP handlers
  6971. setupHTTPHandlers()
  6972. // Handle 404 error
  6973. if (obj.args.nice404 !== false) {
  6974. obj.app.use(function (req, res, next) {
  6975. parent.debug('web', '404 Error ' + req.url);
  6976. var domain = getDomain(req);
  6977. if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
  6978. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
  6979. const cspNonce = obj.crypto.randomBytes(15).toString('base64');
  6980. 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
  6981. res.status(404).render(getRenderPage((domain.sitestyle >= 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
  6982. });
  6983. }
  6984. // Start server on a free port.
  6985. CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
  6986. // Start on a second agent-only alternative port if needed.
  6987. if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
  6988. // We are done starting the web server.
  6989. if (doneFunc) doneFunc();
  6990. }
  6991. }
  6992. function nice404(req, res) {
  6993. parent.debug('web', '404 Error ' + req.url);
  6994. var domain = getDomain(req);
  6995. if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
  6996. if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
  6997. if (obj.args.nice404 == false) { res.sendStatus(404); return; }
  6998. const cspNonce = obj.crypto.randomBytes(15).toString('base64');
  6999. 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
  7000. res.status(404).render(getRenderPage((domain.sitestyle >= 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
  7001. }
  7002. // Auth strategy flags
  7003. const domainAuthStrategyConsts = {
  7004. twitter: 1,
  7005. google: 2,
  7006. github: 4,
  7007. reddit: 8, // Deprecated
  7008. azure: 16,
  7009. oidc: 32,
  7010. saml: 64,
  7011. intelSaml: 128,
  7012. jumpCloudSaml: 256
  7013. }
  7014. // Setup auth strategies for a domain
  7015. async function setupDomainAuthStrategy(domain) {
  7016. // Return binary flags representing all auth strategies that have been setup
  7017. let authStrategyFlags = 0;
  7018. // Setup auth strategies using passport if needed
  7019. if (typeof domain.authstrategies != 'object') return authStrategyFlags;
  7020. const url = domain.url
  7021. const passport = domain.passport = require('passport');
  7022. passport.serializeUser(function (user, done) { done(null, user.sid); });
  7023. passport.deserializeUser(function (sid, done) { done(null, { sid: sid }); });
  7024. obj.app.use(passport.initialize());
  7025. obj.app.use(require('connect-flash')());
  7026. // Twitter
  7027. if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) {
  7028. const TwitterStrategy = require('passport-twitter');
  7029. let options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
  7030. if (typeof domain.authstrategies.twitter.callbackurl == 'string') { options.callbackURL = domain.authstrategies.twitter.callbackurl; } else { options.callbackURL = url + 'auth-twitter-callback'; }
  7031. parent.authLog('setupDomainAuthStrategy', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
  7032. passport.use('twitter-' + domain.id, new TwitterStrategy(options,
  7033. function (token, tokenSecret, profile, cb) {
  7034. parent.authLog('setupDomainAuthStrategy', 'Twitter profile: ' + JSON.stringify(profile));
  7035. var user = { sid: '~twitter:' + profile.id, name: profile.displayName, strategy: 'twitter' };
  7036. if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
  7037. return cb(null, user);
  7038. }
  7039. ));
  7040. authStrategyFlags |= domainAuthStrategyConsts.twitter;
  7041. }
  7042. // Google
  7043. if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) {
  7044. const GoogleStrategy = require('passport-google-oauth20');
  7045. let options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
  7046. if (typeof domain.authstrategies.google.callbackurl == 'string') { options.callbackURL = domain.authstrategies.google.callbackurl; } else { options.callbackURL = url + 'auth-google-callback'; }
  7047. parent.authLog('setupDomainAuthStrategy', 'Adding Google SSO with options: ' + JSON.stringify(options));
  7048. passport.use('google-' + domain.id, new GoogleStrategy(options,
  7049. function (token, tokenSecret, profile, cb) {
  7050. parent.authLog('setupDomainAuthStrategy', 'Google profile: ' + JSON.stringify(profile));
  7051. var user = { sid: '~google:' + profile.id, name: profile.displayName, strategy: 'google' };
  7052. 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; }
  7053. return cb(null, user);
  7054. }
  7055. ));
  7056. authStrategyFlags |= domainAuthStrategyConsts.google;
  7057. }
  7058. // Github
  7059. if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) {
  7060. const GitHubStrategy = require('passport-github2');
  7061. let options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
  7062. if (typeof domain.authstrategies.github.callbackurl == 'string') { options.callbackURL = domain.authstrategies.github.callbackurl; } else { options.callbackURL = url + 'auth-github-callback'; }
  7063. //override passport-github2 defaults that point to github.com with urls specified by user
  7064. if (typeof domain.authstrategies.github.authorizationurl == 'string') { options.authorizationURL = domain.authstrategies.github.authorizationurl; }
  7065. if (typeof domain.authstrategies.github.tokenurl == 'string') { options.tokenURL = domain.authstrategies.github.tokenurl; }
  7066. if (typeof domain.authstrategies.github.userprofileurl == 'string') { options.userProfileURL = domain.authstrategies.github.userprofileurl; }
  7067. if (typeof domain.authstrategies.github.useremailurl == 'string') { options.userEmailURL = domain.authstrategies.github.useremailurl; }
  7068. parent.authLog('setupDomainAuthStrategy', 'Adding Github SSO with options: ' + JSON.stringify(options));
  7069. passport.use('github-' + domain.id, new GitHubStrategy(options,
  7070. function (token, tokenSecret, profile, cb) {
  7071. parent.authLog('setupDomainAuthStrategy', 'Github profile: ' + JSON.stringify(profile));
  7072. var user = { sid: '~github:' + profile.id, name: profile.displayName, strategy: 'github' };
  7073. if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
  7074. return cb(null, user);
  7075. }
  7076. ));
  7077. authStrategyFlags |= domainAuthStrategyConsts.github;
  7078. }
  7079. // Azure
  7080. if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
  7081. const AzureOAuth2Strategy = require('passport-azure-oauth2');
  7082. let options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
  7083. if (typeof domain.authstrategies.azure.callbackurl == 'string') { options.callbackURL = domain.authstrategies.azure.callbackurl; } else { options.callbackURL = url + 'auth-azure-callback'; }
  7084. parent.authLog('setupDomainAuthStrategy', 'Adding Azure SSO with options: ' + JSON.stringify(options));
  7085. passport.use('azure-' + domain.id, new AzureOAuth2Strategy(options,
  7086. function (accessToken, refreshtoken, params, profile, done) {
  7087. var userex = null;
  7088. try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
  7089. parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
  7090. var user = null;
  7091. if (userex != null) {
  7092. var user = { sid: '~azure:' + userex.unique_name.toLowerCase(), name: userex.name, strategy: 'azure' };
  7093. if (typeof userex.email == 'string') { user.email = userex.email.toLowerCase(); }
  7094. }
  7095. return done(null, user);
  7096. }
  7097. ));
  7098. authStrategyFlags |= domainAuthStrategyConsts.azure;
  7099. }
  7100. // Generic SAML
  7101. if (typeof domain.authstrategies.saml == 'object') {
  7102. if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
  7103. parent.debug('error', 'Missing SAML configuration.');
  7104. } else {
  7105. const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
  7106. var cert = obj.fs.readFileSync(certPath);
  7107. if (cert == null) {
  7108. parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
  7109. } else {
  7110. var options = { entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
  7111. if (typeof domain.authstrategies.saml.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.saml.callbackurl; } else { options.callbackUrl = url + 'auth-saml-callback'; }
  7112. if (domain.authstrategies.saml.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.saml.disablerequestedauthncontext; }
  7113. if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
  7114. parent.authLog('setupDomainAuthStrategy', 'Adding SAML SSO with options: ' + JSON.stringify(options));
  7115. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  7116. const SamlStrategy = require('passport-saml').Strategy;
  7117. passport.use('saml-' + domain.id, new SamlStrategy(options,
  7118. function (profile, done) {
  7119. parent.authLog('setupDomainAuthStrategy', 'SAML profile: ' + JSON.stringify(profile));
  7120. if (typeof profile.nameID != 'string') { return done(); }
  7121. var user = { sid: '~saml:' + profile.nameID, name: profile.nameID, strategy: 'saml' };
  7122. if (typeof profile.displayname == 'string') {
  7123. user.name = profile.displayname;
  7124. } else if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) {
  7125. user.name = profile.firstname + ' ' + profile.lastname;
  7126. }
  7127. if (typeof profile.email == 'string') { user.email = profile.email; }
  7128. return done(null, user);
  7129. }
  7130. ));
  7131. authStrategyFlags |= domainAuthStrategyConsts.saml
  7132. }
  7133. }
  7134. }
  7135. // Intel SAML
  7136. if (typeof domain.authstrategies.intel == 'object') {
  7137. if ((typeof domain.authstrategies.intel.cert != 'string') || (typeof domain.authstrategies.intel.idpurl != 'string')) {
  7138. parent.debug('error', 'Missing Intel SAML configuration.');
  7139. } else {
  7140. var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
  7141. if (cert == null) {
  7142. parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
  7143. } else {
  7144. var options = { entryPoint: domain.authstrategies.intel.idpurl, issuer: 'meshcentral' };
  7145. if (typeof domain.authstrategies.intel.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.intel.callbackurl; } else { options.callbackUrl = url + 'auth-intel-callback'; }
  7146. if (domain.authstrategies.intel.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.intel.disablerequestedauthncontext; }
  7147. if (typeof domain.authstrategies.intel.entityid == 'string') { options.issuer = domain.authstrategies.intel.entityid; }
  7148. parent.authLog('setupDomainAuthStrategy', 'Adding Intel SSO with options: ' + JSON.stringify(options));
  7149. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  7150. const SamlStrategy = require('passport-saml').Strategy;
  7151. passport.use('isaml-' + domain.id, new SamlStrategy(options,
  7152. function (profile, done) {
  7153. parent.authLog('setupDomainAuthStrategy', 'Intel profile: ' + JSON.stringify(profile));
  7154. if (typeof profile.nameID != 'string') { return done(); }
  7155. var user = { sid: '~intel:' + profile.nameID, name: profile.nameID, strategy: 'intel' };
  7156. if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
  7157. else if ((typeof profile.FirstName == 'string') && (typeof profile.LastName == 'string')) { user.name = profile.FirstName + ' ' + profile.LastName; }
  7158. if (typeof profile.email == 'string') { user.email = profile.email; }
  7159. else if (typeof profile.EmailAddress == 'string') { user.email = profile.EmailAddress; }
  7160. return done(null, user);
  7161. }
  7162. ));
  7163. authStrategyFlags |= domainAuthStrategyConsts.intelSaml
  7164. }
  7165. }
  7166. }
  7167. // JumpCloud SAML
  7168. if (typeof domain.authstrategies.jumpcloud == 'object') {
  7169. if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
  7170. parent.debug('error', 'Missing JumpCloud SAML configuration.');
  7171. } else {
  7172. var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
  7173. if (cert == null) {
  7174. parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
  7175. } else {
  7176. var options = { entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
  7177. if (typeof domain.authstrategies.jumpcloud.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.jumpcloud.callbackurl; } else { options.callbackUrl = url + 'auth-jumpcloud-callback'; }
  7178. if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
  7179. parent.authLog('setupDomainAuthStrategy', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options));
  7180. options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
  7181. const SamlStrategy = require('passport-saml').Strategy;
  7182. passport.use('jumpcloud-' + domain.id, new SamlStrategy(options,
  7183. function (profile, done) {
  7184. parent.authLog('setupDomainAuthStrategy', 'JumpCloud profile: ' + JSON.stringify(profile));
  7185. if (typeof profile.nameID != 'string') { return done(); }
  7186. var user = { sid: '~jumpcloud:' + profile.nameID, name: profile.nameID, strategy: 'jumpcloud' };
  7187. if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
  7188. if (typeof profile.email == 'string') { user.email = profile.email; }
  7189. return done(null, user);
  7190. }
  7191. ));
  7192. authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
  7193. }
  7194. }
  7195. }
  7196. // Setup OpenID Connect Authentication Strategy
  7197. if (obj.common.validateObject(domain.authstrategies.oidc)) {
  7198. parent.authLog('setupDomainAuthStrategy', `OIDC: Setting up strategy for domain: ${domain.id == null ? 'default' : domain.id}`);
  7199. // Ensure required objects exist
  7200. let initStrategy = domain.authstrategies.oidc
  7201. if (typeof initStrategy.issuer == 'string') { initStrategy.issuer = { 'issuer': initStrategy.issuer } }
  7202. let strategy = migrateOldConfigs(Object.assign({ 'client': {}, 'issuer': {}, 'options': {}, 'custom': {}, 'obj': { 'openidClient': require('openid-client') } }, initStrategy))
  7203. let preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
  7204. if (!preset) {
  7205. if (typeof strategy.custom.tenant_id == 'string') { strategy.custom.preset = preset = 'azure' }
  7206. if (strategy.custom.customer_id || strategy.custom.identitysource || strategy.client.client_id.split('.')[2] == 'googleusercontent') { strategy.custom.preset = preset = 'google' }
  7207. }
  7208. // Check issuer url
  7209. let presetIssuer
  7210. if (preset == 'azure') { presetIssuer = 'https://login.microsoftonline.com/' + strategy.custom.tenant_id + '/v2.0'; }
  7211. if (preset == 'google') { presetIssuer = 'https://accounts.google.com'; }
  7212. if (!obj.common.validateString(strategy.issuer.issuer)) {
  7213. if (!preset) {
  7214. let error = new Error('OIDC: Missing issuer URI.');
  7215. parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
  7216. throw error;
  7217. } else {
  7218. strategy.issuer.issuer = presetIssuer
  7219. parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
  7220. }
  7221. } else if ((typeof strategy.issuer.issuer == 'string') && (typeof strategy.custom.preset == 'string')) {
  7222. let error = new Error(`OIDC: PRESET: ${strategy.custom.preset.toUpperCase()}: PRESET OVERRIDDEN: CONFIG ISSUER: ${strategy.issuer.issuer} PRESET ISSUER: ${presetIssuer}`);
  7223. parent.authLog('setupDomainAuthStrategy', error.message);
  7224. console.warn(error)
  7225. }
  7226. // Setup Strategy Options
  7227. strategy.custom.scope = obj.common.convertStrArray(strategy.custom.scope, ' ')
  7228. if (strategy.custom.scope.length > 0) {
  7229. strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': strategy.custom.scope });
  7230. } else {
  7231. strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': ['openid', 'profile', 'email'] });
  7232. }
  7233. if (typeof strategy.groups == 'object') {
  7234. let groupScope = strategy.groups.scope || null
  7235. if (groupScope == null) {
  7236. if (preset == 'azure') { groupScope = 'Group.Read.All' }
  7237. if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
  7238. if (typeof preset != 'string') { groupScope = 'groups' }
  7239. }
  7240. strategy.options.params.scope.push(groupScope)
  7241. }
  7242. strategy.options.params.scope = strategy.options.params.scope.join(' ')
  7243. if (obj.httpsProxyAgent) {
  7244. // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // add using environment variables if needs be not here
  7245. strategy.obj.openidClient.custom.setHttpOptionsDefaults({ agent: obj.httpsProxyAgent });
  7246. }
  7247. // Discover additional information if available, use endpoints from config if present
  7248. let issuer
  7249. try {
  7250. parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
  7251. issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
  7252. } catch (err) {
  7253. let error = new Error('OIDC: Discovery failed.', { cause: err });
  7254. parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
  7255. throw error
  7256. }
  7257. if (Object.keys(strategy.issuer).length > 1) {
  7258. parent.authLog('setupDomainAuthStrategy', `OIDC: Adding Issuer Metadata: ${JSON.stringify(strategy.issuer)}`);
  7259. issuer = new strategy.obj.openidClient.Issuer(Object.assign(issuer?.metadata, strategy.issuer));
  7260. }
  7261. strategy.issuer = issuer?.metadata;
  7262. strategy.obj.issuer = issuer;
  7263. var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
  7264. var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
  7265. if (httpport != 443) { origin += ':' + httpport; }
  7266. // Make sure redirect_uri and post_logout_redirect_uri exist before continuing
  7267. if (!strategy.client.redirect_uri) {
  7268. strategy.client.redirect_uri = origin + url + 'auth-oidc-callback';
  7269. }
  7270. if (!strategy.client.post_logout_redirect_uri) {
  7271. strategy.client.post_logout_redirect_uri = origin + url + 'login';
  7272. }
  7273. // Create client and overwrite in options
  7274. let client = new issuer.Client(strategy.client)
  7275. strategy.options = Object.assign(strategy.options, { 'client': client, sessionKey: 'oidc-' + domain.id });
  7276. strategy.client = client.metadata
  7277. strategy.obj.client = client
  7278. // Setup strategy and save configs for later
  7279. passport.use('oidc-' + domain.id, new strategy.obj.openidClient.Strategy(strategy.options, oidcCallback));
  7280. parent.config.domains[domain.id].authstrategies.oidc = strategy;
  7281. parent.debug('verbose', 'OIDC: Saved Configuration: ' + JSON.stringify(strategy));
  7282. if (preset) { parent.authLog('setupDomainAuthStrategy', 'OIDC: ' + preset.toUpperCase() + ': Setup Complete'); }
  7283. else { parent.authLog('setupDomainAuthStrategy', 'OIDC: Setup Complete'); }
  7284. authStrategyFlags |= domainAuthStrategyConsts.oidc
  7285. function migrateOldConfigs(strategy) {
  7286. let oldConfigs = {
  7287. 'client': {
  7288. 'clientid': 'client_id',
  7289. 'clientsecret': 'client_secret',
  7290. 'callbackurl': 'redirect_uri'
  7291. },
  7292. 'issuer': {
  7293. 'authorizationurl': 'authorization_endpoint',
  7294. 'tokenurl': 'token_endpoint',
  7295. 'userinfourl': 'userinfo_endpoint'
  7296. },
  7297. 'custom': {
  7298. 'tenantid': 'tenant_id',
  7299. 'customerid': 'customer_id'
  7300. }
  7301. }
  7302. for (var type in oldConfigs) {
  7303. for (const [key, value] of Object.entries(oldConfigs[type])) {
  7304. if (Object.hasOwn(strategy, key)) {
  7305. if (strategy[type][value] && obj.common.validateString(strategy[type][value])) {
  7306. let error = new Error('OIDC: OLD CONFIG: Config conflict, new config overrides old config');
  7307. parent.authLog('migrateOldConfigs', `${JSON.stringify(error)} OLD CONFIG: ${key}: ${strategy[key]} NEW CONFIG: ${value}:${strategy[type][value]}`);
  7308. } else {
  7309. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
  7310. strategy[type][value] = strategy[key];
  7311. }
  7312. delete strategy[key]
  7313. }
  7314. }
  7315. }
  7316. if (typeof strategy.scope == 'string') {
  7317. if (!strategy.custom.scope) {
  7318. strategy.custom.scope = strategy.scope;
  7319. strategy.options.params = { 'scope': strategy.scope };
  7320. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.scope => strategy.custom.scope`);
  7321. } else {
  7322. let error = new Error('OIDC: OLD CONFIG: Config conflict, using new config values.');
  7323. parent.authLog('migrateOldConfigs', `${error.message} OLD CONFIG: strategy.scope: ${strategy.scope} NEW CONFIG: strategy.custom.scope:${strategy.custom.scope}`);
  7324. parent.debug('warning', error.message)
  7325. }
  7326. delete strategy.scope
  7327. }
  7328. if (strategy.groups && strategy.groups.sync && strategy.groups.sync.enabled && strategy.groups.sync.enabled === true) {
  7329. if (strategy.groups.sync.filter) {
  7330. delete strategy.groups.sync.enabled;
  7331. } else {
  7332. strategy.groups.sync = true;
  7333. }
  7334. parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.groups.sync.enabled => strategy.groups.sync`);
  7335. }
  7336. return strategy
  7337. }
  7338. // Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
  7339. function oidcCallback(tokenset, profile, done) {
  7340. // Handle case where done might not be the third parameter
  7341. if (typeof done !== 'function') {
  7342. // OpenID Connect strategy calls with (tokenset, done) instead of (tokenset, profile, done)
  7343. if (typeof profile === 'function') {
  7344. done = profile;
  7345. profile = null;
  7346. } else {
  7347. parent.debug('error', 'OIDC: Unable to find callback function in parameters');
  7348. return;
  7349. }
  7350. }
  7351. // If profile is null/undefined, extract user info from the tokenset
  7352. if (!profile && tokenset && tokenset.id_token) {
  7353. try {
  7354. // Simple JWT decoder to extract user claims from id_token
  7355. const parts = tokenset.id_token.split('.');
  7356. if (parts.length === 3) {
  7357. const payload = parts[1];
  7358. const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
  7359. const decoded = JSON.parse(Buffer.from(paddedPayload, 'base64').toString());
  7360. if (decoded) {
  7361. profile = decoded;
  7362. }
  7363. }
  7364. } catch (err) {
  7365. parent.debug('error', `OIDC: Failed to decode id_token: ${err.message}`);
  7366. }
  7367. }
  7368. // Initialize user object
  7369. let user = { 'strategy': 'oidc' }
  7370. let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null;
  7371. user.sid = null;
  7372. if (profile && obj.common.validateString(profile.sub)) {
  7373. user.sid = '~oidc:' + profile.sub;
  7374. } else if (profile && obj.common.validateString(profile.oid)) {
  7375. user.sid = '~oidc:' + profile.oid;
  7376. } else if (profile && obj.common.validateString(profile.email)) {
  7377. user.sid = '~oidc:' + profile.email;
  7378. } else if (profile && obj.common.validateString(profile.upn)) {
  7379. user.sid = '~oidc:' + profile.upn;
  7380. }
  7381. user.name = profile && obj.common.validateString(profile.name) ? profile.name : null;
  7382. user.email = profile && obj.common.validateString(profile.email) ? profile.email : null;
  7383. if (claims != null) {
  7384. user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid;
  7385. user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name;
  7386. user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email;
  7387. }
  7388. // Ensure we have a valid sid before proceeding
  7389. if (!user.sid) {
  7390. parent.debug('error', `OIDC: No valid user identifier found in profile`);
  7391. return done(new Error('OIDC: No valid user identifier found in profile'));
  7392. }
  7393. user.emailVerified = profile && profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
  7394. user.groups = profile && obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
  7395. user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null;
  7396. if (strategy.groups && obj.common.validateString(strategy.groups.claim)) {
  7397. user.groups = profile && obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
  7398. }
  7399. // Setup end session enpoint if not already configured this requires an auth token
  7400. try {
  7401. if (!strategy.issuer.end_session_endpoint) {
  7402. strategy.issuer.end_session_endpoint = strategy.obj.client.endSessionUrl({ 'id_token_hint': tokenset })
  7403. parent.authLog('oidcCallback', `OIDC: Discovered end_session_endpoint: ${strategy.issuer.end_session_endpoint}`);
  7404. }
  7405. } catch (err) {
  7406. let error = new Error('OIDC: Discovering end_session_endpoint failed. Using Default.', { cause: err });
  7407. strategy.issuer.end_session_endpoint = strategy.issuer.issuer + '/logout';
  7408. 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)}`);
  7409. parent.authLog('oidcCallback', error.message);
  7410. }
  7411. // Setup presets and groups, get groups from API if needed then return
  7412. if (strategy.groups && typeof user.preset == 'string') {
  7413. getGroups(user.preset, tokenset).then((groups) => {
  7414. user = Object.assign(user, { 'groups': groups });
  7415. done(null, user);
  7416. }).catch((err) => {
  7417. let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
  7418. parent.debug('error', `${JSON.stringify(error)}`);
  7419. parent.authLog('oidcCallback', error.message);
  7420. user.groups = [];
  7421. done(null, user);
  7422. });
  7423. } else {
  7424. done(null, user);
  7425. }
  7426. async function getGroups(preset, tokenset) {
  7427. let url = '';
  7428. if (preset == 'azure') { url = strategy.groups.recursive == true ? 'https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$top=999' : 'https://graph.microsoft.com/v1.0/me/memberOf?$top=999'; }
  7429. 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; }
  7430. return new Promise((resolve, reject) => {
  7431. const options = {
  7432. 'headers': { authorization: 'Bearer ' + tokenset.access_token }
  7433. }
  7434. if (obj.httpsProxyAgent) { options.agent = obj.httpsProxyAgent; }
  7435. const req = require('https').get(url, options, (res) => {
  7436. let data = []
  7437. res.on('data', (chunk) => {
  7438. data.push(chunk);
  7439. });
  7440. res.on('end', () => {
  7441. if (res.statusCode < 200 || res.statusCode >= 300) {
  7442. let error = new Error('OIDC: GROUPS: Bad response code from API, statusCode: ' + res.statusCode);
  7443. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  7444. console.error(error);
  7445. reject(error);
  7446. }
  7447. if (data.length == 0) {
  7448. let error = new Error('OIDC: GROUPS: Getting groups from API failed, request returned no data in response.');
  7449. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  7450. console.error(error);
  7451. reject(error);
  7452. }
  7453. try {
  7454. if (Buffer.isBuffer(data[0])) {
  7455. data = Buffer.concat(data);
  7456. data = data.toString();
  7457. } else { // else if (typeof data[0] == 'string')
  7458. data = data.join();
  7459. }
  7460. } catch (err) {
  7461. let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: err });
  7462. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  7463. console.error(error);
  7464. reject(error);
  7465. }
  7466. if (preset == 'azure') {
  7467. data = JSON.parse(data);
  7468. if (data.error) {
  7469. let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: data.error });
  7470. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  7471. console.error(error);
  7472. reject(error);
  7473. }
  7474. data = data.value;
  7475. }
  7476. if (preset == 'google') {
  7477. data = data.split('\n');
  7478. data = data.join('');
  7479. data = JSON.parse(data);
  7480. data = data.groups;
  7481. }
  7482. let groups = []
  7483. for (var i in data) {
  7484. if (typeof data[i].displayName == 'string') {
  7485. groups.push(data[i].displayName);
  7486. }
  7487. }
  7488. if (groups.length == 0) {
  7489. let warn = new Error('OIDC: GROUPS: No groups returned from API.');
  7490. parent.authLog('getGroups', `WARN: ${warn.message} DATA: ${data}`);
  7491. console.warn(warn);
  7492. resolve(groups);
  7493. } else {
  7494. resolve(groups);
  7495. }
  7496. });
  7497. });
  7498. req.on('error', (err) => {
  7499. let error = new Error('OIDC: GROUPS: Request error.', { cause: err });
  7500. parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
  7501. console.error(error);
  7502. reject(error);
  7503. });
  7504. req.end();
  7505. });
  7506. }
  7507. }
  7508. }
  7509. return authStrategyFlags;
  7510. }
  7511. // Handle an incoming request as a web relay
  7512. function handleWebRelayRequest(req, res) {
  7513. var webRelaySessionId = null;
  7514. if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
  7515. else if (req.session.z != null) { webRelaySessionId = req.session.z; }
  7516. if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
  7517. var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
  7518. if (relaySession != null) {
  7519. // The web relay session is valid, use it
  7520. relaySession.handleRequest(req, res);
  7521. } else {
  7522. // No web relay session with this relay identifier, close the HTTP request.
  7523. res.sendStatus(404);
  7524. }
  7525. } else {
  7526. // The user is not logged in or does not have a relay identifier, close the HTTP request.
  7527. res.sendStatus(404);
  7528. }
  7529. }
  7530. // Handle an incoming websocket connection as a web relay
  7531. function handleWebRelayWebSocket(ws, req) {
  7532. var webRelaySessionId = null;
  7533. if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
  7534. else if (req.session.z != null) { webRelaySessionId = req.session.z; }
  7535. if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
  7536. var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
  7537. if (relaySession != null) {
  7538. // The multi-tunnel session is valid, use it
  7539. relaySession.handleWebSocket(ws, req);
  7540. } else {
  7541. // No multi-tunnel session with this relay identifier, close the websocket.
  7542. ws.close();
  7543. }
  7544. } else {
  7545. // The user is not logged in or does not have a relay identifier, close the websocket.
  7546. ws.close();
  7547. }
  7548. }
  7549. // Perform server inner authentication
  7550. // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
  7551. // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username
  7552. // and password or authentication cookie.
  7553. function PerformWSSessionInnerAuth(ws, req, domain, func) {
  7554. // When data is received from the web socket
  7555. ws.on('message', function (data) {
  7556. var command;
  7557. try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; }
  7558. if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
  7559. switch (command.action) {
  7560. case 'serverAuth': { // This command is used to perform server "inner" authentication.
  7561. // Check the client nonce and TLS hash
  7562. if ((obj.common.validateString(command.cnonce, 1, 256) == false) || (obj.common.validateString(command.tlshash, 1, 512) == false)) {
  7563. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badargs' })); } catch (ex) { }
  7564. try { ws.close(); } catch (ex) { }
  7565. break;
  7566. }
  7567. // Check that the TLS hash is an acceptable one.
  7568. var h = Buffer.from(command.tlshash, 'hex').toString('binary');
  7569. if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) {
  7570. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badtlscert' })); } catch (ex) { }
  7571. try { ws.close(); } catch (ex) { }
  7572. return;
  7573. }
  7574. // TLS hash check is a success, sign the request.
  7575. // Perform the hash signature using the server agent certificate
  7576. var nonce = obj.crypto.randomBytes(48);
  7577. var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
  7578. parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
  7579. // Send back our certificate + nonce + signature
  7580. 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') }));
  7581. });
  7582. break;
  7583. }
  7584. case 'userAuth': { // This command is used to perform user authentication.
  7585. // Check username and password authentication
  7586. if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
  7587. obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) {
  7588. if ((err != null) || (userid == null)) {
  7589. // Invalid authentication
  7590. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
  7591. try { ws.close(); } catch (ex) { }
  7592. } else {
  7593. var user = obj.users[userid];
  7594. if ((err == null) && (user)) {
  7595. // Check if a 2nd factor is needed
  7596. 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'))
  7597. // See if we support two-factor trusted cookies
  7598. var twoFactorCookieDays = 30;
  7599. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7600. // Check if two factor can be skipped
  7601. const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
  7602. if ((twoFactorSkip == null) && (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true)) {
  7603. // Figure out if email 2FA is allowed
  7604. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7605. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7606. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7607. //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  7608. if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) {
  7609. if ((command.token == '**email**') && (email2fa == true)) {
  7610. // Cause a token to be sent to the user's registered email
  7611. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7612. obj.db.SetUser(user);
  7613. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7614. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7615. // Ask for a login token & confirm email was sent
  7616. 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) { }
  7617. } else if ((command.token == '**sms**') && (sms2fa == true)) {
  7618. // Cause a token to be sent to the user's phone number
  7619. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7620. obj.db.SetUser(user);
  7621. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7622. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7623. // Ask for a login token & confirm sms was sent
  7624. 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) { }
  7625. } else if ((command.token == '**msg**') && (msg2fa == true)) {
  7626. // Cause a token to be sent to the user's messenger account
  7627. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7628. obj.db.SetUser(user);
  7629. parent.debug('web', 'Sending 2FA message to: ' + user.phone);
  7630. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7631. // Ask for a login token & confirm sms was sent
  7632. 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) { }
  7633. /*
  7634. } else if ((command.token == '**push**') && (push2fa == true)) {
  7635. // Cause push notification to device
  7636. const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  7637. const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
  7638. var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
  7639. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  7640. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  7641. 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); }
  7642. });
  7643. */
  7644. } else {
  7645. // Ask for a login token
  7646. parent.debug('web', 'Asking for login token');
  7647. 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); }
  7648. }
  7649. } else {
  7650. checkUserOneTimePassword(req, domain, user, command.token, null, function (result, authData) {
  7651. if (result == false) {
  7652. // Failed, ask for a login token again
  7653. parent.debug('web', 'Invalid login token, asking again');
  7654. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7655. } else {
  7656. // We are authenticated with 2nd factor.
  7657. // Check email verification
  7658. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7659. parent.debug('web', 'Invalid login, asking for email validation');
  7660. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7661. } else {
  7662. // We are authenticated
  7663. ws._socket.pause();
  7664. ws.removeAllListeners(['message', 'close', 'error']);
  7665. func(ws, req, domain, user, authData);
  7666. }
  7667. }
  7668. });
  7669. }
  7670. } else {
  7671. // Check email verification
  7672. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7673. parent.debug('web', 'Invalid login, asking for email validation');
  7674. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7675. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7676. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7677. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7678. } else {
  7679. // We are authenticated
  7680. ws._socket.pause();
  7681. ws.removeAllListeners(['message', 'close', 'error']);
  7682. func(ws, req, domain, user, twoFactorSkip);
  7683. }
  7684. }
  7685. }
  7686. }
  7687. });
  7688. } else {
  7689. // Invalid authentication
  7690. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
  7691. try { ws.close(); } catch (ex) { }
  7692. }
  7693. break;
  7694. }
  7695. }
  7696. });
  7697. // If error, do nothing
  7698. ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
  7699. // If the web socket is closed
  7700. ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
  7701. // Resume the socket to perform inner authentication
  7702. try { ws._socket.resume(); } catch (ex) { }
  7703. }
  7704. // Authenticates a session and forwards
  7705. function PerformWSSessionAuth(ws, req, noAuthOk, func) {
  7706. // Check if the session expired
  7707. if ((req.session != null) && (typeof req.session.expire == 'number') && (req.session.expire <= Date.now())) {
  7708. parent.debug('web', 'WSERROR: Session expired.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'expired', msg: 'expired-1' })); ws.close(); } catch (e) { } return;
  7709. }
  7710. // Check if this is a banned ip address
  7711. 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; }
  7712. try {
  7713. // Hold this websocket until we are ready.
  7714. ws._socket.pause();
  7715. // Check IP filtering and domain
  7716. var domain = null;
  7717. if (noAuthOk == true) {
  7718. domain = getDomain(req);
  7719. 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; }
  7720. } else {
  7721. // If authentication is required, enforce IP address filtering.
  7722. domain = checkUserIpAddress(ws, req);
  7723. if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, user auth required.'); return; }
  7724. }
  7725. // Check if inner authentication is requested
  7726. if (req.headers['x-meshauth'] === '*') { func(ws, req, domain, null); return; }
  7727. 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'))
  7728. // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
  7729. if ((req.query.user != null) && (req.query.pass != null)) {
  7730. // A user/pass is provided in URL arguments
  7731. obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
  7732. var user = obj.users[userid];
  7733. // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
  7734. if ((user != null) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
  7735. // No tools allowed, close the websocket connection
  7736. parent.debug('web', 'ERR: Websocket no tools allowed');
  7737. try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
  7738. return;
  7739. }
  7740. // See if we support two-factor trusted cookies
  7741. var twoFactorCookieDays = 30;
  7742. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7743. if ((err == null) && (user)) {
  7744. // Check if a 2nd factor is needed
  7745. if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  7746. // Figure out if email 2FA is allowed
  7747. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7748. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7749. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7750. //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
  7751. if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) {
  7752. if ((req.query.token == '**email**') && (email2fa == true)) {
  7753. // Cause a token to be sent to the user's registered email
  7754. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7755. obj.db.SetUser(user);
  7756. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7757. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7758. // Ask for a login token & confirm email was sent
  7759. 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) { }
  7760. } else if ((req.query.token == '**sms**') && (sms2fa == true)) {
  7761. // Cause a token to be sent to the user's phone number
  7762. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7763. obj.db.SetUser(user);
  7764. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7765. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7766. // Ask for a login token & confirm sms was sent
  7767. 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) { }
  7768. } else if ((req.query.token == '**msg**') && (msg2fa == true)) {
  7769. // Cause a token to be sent to the user's messenger account
  7770. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7771. obj.db.SetUser(user);
  7772. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  7773. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7774. // Ask for a login token & confirm message was sent
  7775. 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) { }
  7776. /*
  7777. } else if ((command.token == '**push**') && (push2fa == true)) {
  7778. // Cause push notification to device
  7779. const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
  7780. const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
  7781. var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
  7782. var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
  7783. parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
  7784. 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); }
  7785. });
  7786. */
  7787. } else {
  7788. // Ask for a login token
  7789. parent.debug('web', 'Asking for login token');
  7790. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7791. }
  7792. } else {
  7793. checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result, authData) {
  7794. if (result == false) {
  7795. // Failed, ask for a login token again
  7796. parent.debug('web', 'Invalid login token, asking again');
  7797. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7798. } else {
  7799. // We are authenticated with 2nd factor.
  7800. // Check email verification
  7801. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7802. parent.debug('web', 'Invalid login, asking for email validation');
  7803. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7804. } else {
  7805. req.session.userid = user._id;
  7806. req.session.ip = req.clientIp;
  7807. setSessionRandom(req);
  7808. func(ws, req, domain, user, null, authData);
  7809. }
  7810. }
  7811. });
  7812. }
  7813. } else {
  7814. // Check email verification
  7815. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7816. parent.debug('web', 'Invalid login, asking for email validation');
  7817. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7818. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7819. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7820. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7821. } else {
  7822. // We are authenticated
  7823. req.session.userid = user._id;
  7824. req.session.ip = req.clientIp;
  7825. setSessionRandom(req);
  7826. func(ws, req, domain, user);
  7827. }
  7828. }
  7829. } else {
  7830. // Failed to authenticate, see if a default user is active
  7831. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7832. // A default user is active
  7833. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7834. } else {
  7835. // If not authenticated, close the websocket connection
  7836. parent.debug('web', 'ERR: Websocket bad user/pass auth');
  7837. //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 });
  7838. //obj.setbadLogin(req);
  7839. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2a' })); ws.close(); } catch (e) { }
  7840. }
  7841. }
  7842. });
  7843. return;
  7844. }
  7845. if ((req.query.auth != null) && (req.query.auth != '')) {
  7846. // This is a encrypted cookie authentication
  7847. var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
  7848. if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
  7849. if ((cookie != null) && (cookie.ip != null) && !checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
  7850. parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
  7851. cookie = null;
  7852. }
  7853. if ((cookie != null) && (cookie.userid != null) && (obj.users[cookie.userid]) && (cookie.domainid == domain.id) && (cookie.userid.split('/')[1] == domain.id)) {
  7854. // Valid cookie, we are authenticated. Cookie of format { userid: 'user//name', domain: '' }
  7855. func(ws, req, domain, obj.users[cookie.userid], cookie);
  7856. return;
  7857. } else if ((cookie != null) && (cookie.a === 3) && (typeof cookie.u == 'string') && (obj.users[cookie.u]) && (cookie.u.split('/')[1] == domain.id)) {
  7858. // Valid cookie, we are authenticated. Cookie of format { u: 'user//name', a: 3 }
  7859. func(ws, req, domain, obj.users[cookie.u], cookie);
  7860. return;
  7861. } else if ((cookie != null) && (cookie.nouser === 1)) {
  7862. // This is a valid cookie, but no user. This is used for agent self-sharing.
  7863. func(ws, req, domain, null, cookie);
  7864. return;
  7865. } /*else {
  7866. // This is a bad cookie, keep going anyway, maybe we have a active session that will save us.
  7867. if ((cookie != null) && (cookie.domainid != domain.id)) { parent.debug('web', 'ERR: Invalid domain, got \"' + cookie.domainid + '\", expected \"' + domain.id + '\".'); }
  7868. parent.debug('web', 'ERR: Websocket bad cookie auth (Cookie:' + (cookie != null) + '): ' + req.query.auth);
  7869. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2b' })); ws.close(); } catch (e) { }
  7870. return;
  7871. }
  7872. */
  7873. }
  7874. if (req.headers['x-meshauth'] != null) {
  7875. // This is authentication using a custom HTTP header
  7876. var s = req.headers['x-meshauth'].split(',');
  7877. for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); }
  7878. if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; }
  7879. obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) {
  7880. var user = obj.users[userid];
  7881. if ((err == null) && (user)) {
  7882. // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
  7883. if ((user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
  7884. // No tools allowed, close the websocket connection
  7885. parent.debug('web', 'ERR: Websocket no tools allowed');
  7886. try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
  7887. return;
  7888. }
  7889. // Check if a 2nd factor is needed
  7890. if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
  7891. // See if we support two-factor trusted cookies
  7892. var twoFactorCookieDays = 30;
  7893. if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
  7894. // Figure out if email 2FA is allowed
  7895. var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
  7896. var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
  7897. var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
  7898. if (s.length != 3) {
  7899. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7900. } else {
  7901. checkUserOneTimePassword(req, domain, user, s[2], null, function (result, authData) {
  7902. if (result == false) {
  7903. if ((s[2] == '**email**') && (email2fa == true)) {
  7904. // Cause a token to be sent to the user's registered email
  7905. user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
  7906. obj.db.SetUser(user);
  7907. parent.debug('web', 'Sending 2FA email to: ' + user.email);
  7908. domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
  7909. // Ask for a login token & confirm email was sent
  7910. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7911. } else if ((s[2] == '**sms**') && (sms2fa == true)) {
  7912. // Cause a token to be sent to the user's phone number
  7913. user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7914. obj.db.SetUser(user);
  7915. parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
  7916. parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
  7917. // Ask for a login token & confirm sms was sent
  7918. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7919. } else if ((s[2] == '**msg**') && (msg2fa == true)) {
  7920. // Cause a token to be sent to the user's phone number
  7921. user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
  7922. obj.db.SetUser(user);
  7923. parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
  7924. parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
  7925. // Ask for a login token & confirm sms was sent
  7926. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7927. } else {
  7928. // Ask for a login token
  7929. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7930. }
  7931. } else {
  7932. // We are authenticated with 2nd factor.
  7933. // Check email verification
  7934. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7935. parent.debug('web', 'Invalid login, asking for email validation');
  7936. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
  7937. } else {
  7938. func(ws, req, domain, user, null, authData);
  7939. }
  7940. }
  7941. });
  7942. }
  7943. } else {
  7944. // We are authenticated
  7945. // Check email verification
  7946. if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
  7947. parent.debug('web', 'Invalid login, asking for email validation');
  7948. try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
  7949. } else {
  7950. req.session.userid = user._id;
  7951. req.session.ip = req.clientIp;
  7952. setSessionRandom(req);
  7953. func(ws, req, domain, user);
  7954. }
  7955. }
  7956. } else {
  7957. // Failed to authenticate, see if a default user is active
  7958. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7959. // A default user is active
  7960. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7961. } else {
  7962. // If not authenticated, close the websocket connection
  7963. parent.debug('web', 'ERR: Websocket bad user/pass auth');
  7964. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2d' })); ws.close(); } catch (e) { }
  7965. }
  7966. }
  7967. });
  7968. return;
  7969. }
  7970. if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
  7971. // A default user is active
  7972. func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
  7973. return;
  7974. }
  7975. if (req.session && (req.session.userid != null) && (req.session.userid.split('/')[1] == domain.id) && (obj.users[req.session.userid])) {
  7976. // This user is logged in using the ExpressJS session
  7977. func(ws, req, domain, obj.users[req.session.userid]);
  7978. return;
  7979. }
  7980. if (noAuthOk != true) {
  7981. // If not authenticated, close the websocket connection
  7982. parent.debug('web', 'ERR: Websocket no auth');
  7983. try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-4' })); ws.close(); } catch (e) { }
  7984. } else {
  7985. // Continue this session without user authentication,
  7986. // this is expected if the agent is connecting for a tunnel.
  7987. func(ws, req, domain, null);
  7988. }
  7989. } catch (e) { console.log(e); }
  7990. }
  7991. // Find a free port starting with the specified one and going up.
  7992. function CheckListenPort(port, addr, func) {
  7993. var s = obj.net.createServer(function (socket) { });
  7994. obj.tcpServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on('error', function (err) {
  7995. if (args.exactports) { console.error('ERROR: MeshCentral HTTPS server port ' + port + ' not available.'); process.exit(); }
  7996. else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } }
  7997. });
  7998. }
  7999. // Start the ExpressJS web server
  8000. function StartWebServer(port, addr) {
  8001. if ((port < 1) || (port > 65535)) return;
  8002. obj.args.port = port;
  8003. if (obj.tlsServer != null) {
  8004. if (obj.args.lanonly == true) {
  8005. 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) : '') + '.'); });
  8006. } else {
  8007. obj.tcpServer = obj.tlsServer.listen(port, addr, function () {
  8008. console.log('MeshCentral HTTPS server running on ' + certificates.CommonName + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
  8009. if (args.relaydns != null) { console.log('MeshCentral HTTPS relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
  8010. });
  8011. obj.parent.updateServerState('servername', certificates.CommonName);
  8012. }
  8013. obj.parent.debug('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
  8014. obj.parent.updateServerState('https-port', port);
  8015. if (args.aliasport != null) { obj.parent.updateServerState('https-aliasport', args.aliasport); }
  8016. } else {
  8017. obj.tcpServer = obj.app.listen(port, addr, function () {
  8018. console.log('MeshCentral HTTP server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
  8019. if (args.relaydns != null) { console.log('MeshCentral HTTP relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
  8020. });
  8021. obj.parent.updateServerState('http-port', port);
  8022. if (args.aliasport != null) { obj.parent.updateServerState('http-aliasport', args.aliasport); }
  8023. }
  8024. // Check if there is a permissions problem with the ports.
  8025. if (require('os').platform() != 'win32') {
  8026. var expectedPort = obj.parent.config.settings.port ? obj.parent.config.settings.port : 443;
  8027. if ((expectedPort != port) && (port >= 1024) && (port < 1034)) {
  8028. console.log('');
  8029. console.log('WARNING: MeshCentral is running without permissions to use ports below 1025.');
  8030. console.log(' Use setcap to grant access to lower ports, or read installation guide.');
  8031. console.log('');
  8032. console.log(' sudo setcap \'cap_net_bind_service=+ep\' `which node` \r\n');
  8033. obj.parent.addServerWarning('Server running without permissions to use ports below 1025.', false);
  8034. }
  8035. }
  8036. }
  8037. // Start the ExpressJS web server on agent-only alternative port
  8038. function StartAltWebServer(port, addr) {
  8039. if ((port < 1) || (port > 65535)) return;
  8040. var agentAliasPort = null;
  8041. var agentAliasDns = null;
  8042. if (args.agentaliasport != null) { agentAliasPort = args.agentaliasport; }
  8043. if (args.agentaliasdns != null) { agentAliasDns = args.agentaliasdns; }
  8044. if (obj.tlsAltServer != null) {
  8045. if (obj.args.lanonly == true) {
  8046. obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
  8047. } else {
  8048. 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) : '') + '.'); });
  8049. }
  8050. obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
  8051. obj.parent.updateServerState('https-agent-port', port);
  8052. } else {
  8053. obj.tcpAltServer = obj.agentapp.listen(port, addr, function () { console.log('MeshCentral HTTP agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
  8054. obj.parent.updateServerState('http-agent-port', port);
  8055. }
  8056. }
  8057. // Force mesh agent disconnection
  8058. obj.forceMeshAgentDisconnect = function (user, domain, nodeid, disconnectMode) {
  8059. if (nodeid == null) return;
  8060. var splitnode = nodeid.split('/');
  8061. if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
  8062. var agent = obj.wsagents[nodeid];
  8063. if (agent == null) return;
  8064. // Check we have agent rights
  8065. if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
  8066. };
  8067. // Send the core module to the mesh agent
  8068. obj.sendMeshAgentCore = function (user, domain, nodeid, coretype, coredata) {
  8069. if (nodeid == null) return;
  8070. const splitnode = nodeid.split('/');
  8071. if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
  8072. // TODO: This command only works if the agent is connected on the same server. Will not work with multi server peering.
  8073. const agent = obj.wsagents[nodeid];
  8074. if (agent == null) return;
  8075. // Check we have agent rights
  8076. if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) {
  8077. if (coretype == 'clear') {
  8078. // Clear the mesh agent core
  8079. agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
  8080. agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0));
  8081. } else if (coretype == 'default') {
  8082. // Reset to default code
  8083. agent.agentCoreCheck = 0; // Tell the agent object we are using a default code
  8084. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  8085. } else if (coretype == 'recovery') {
  8086. // Reset to recovery core
  8087. agent.agentCoreCheck = 1001; // Tell the agent object we are using the recovery core.
  8088. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  8089. } else if (coretype == 'tiny') {
  8090. // Reset to tiny core
  8091. agent.agentCoreCheck = 1011; // Tell the agent object we are using the tiny core.
  8092. agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
  8093. } else if (coretype == 'custom') {
  8094. agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
  8095. var buf = Buffer.from(coredata, 'utf8');
  8096. const hash = obj.crypto.createHash('sha384').update(buf).digest().toString('binary'); // Perform a SHA384 hash on the core module
  8097. agent.sendBinary(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + buf.toString('binary')); // Send the code module to the agent
  8098. }
  8099. }
  8100. };
  8101. // Get the server path of a user or mesh object
  8102. function getServerRootFilePath(obj) {
  8103. if ((typeof obj != 'object') || (obj.domain == null) || (obj._id == null)) return null;
  8104. var domainname = 'domain', splitname = obj._id.split('/');
  8105. if (splitname.length != 3) return null;
  8106. if (obj.domain !== '') domainname = 'domain-' + obj.domain;
  8107. return obj.path.join(obj.filespath, domainname + "/" + splitname[0] + "-" + splitname[2]);
  8108. }
  8109. // Return true is the input string looks like an email address
  8110. function checkEmail(str) {
  8111. var x = str.split('@');
  8112. var ok = ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2));
  8113. if (ok == true) { var y = x[1].split('.'); for (var i in y) { if (y[i].length == 0) { ok = false; } } }
  8114. return ok;
  8115. }
  8116. /*
  8117. obj.wssessions = {}; // UserId --> Array Of Sessions
  8118. obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
  8119. obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
  8120. obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
  8121. obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
  8122. */
  8123. // Count sessions and event any changes
  8124. obj.recountSessions = function (changedSessionId) {
  8125. var userid, oldcount, newcount, x, serverid;
  8126. if (changedSessionId == null) {
  8127. // Recount all sessions
  8128. // Calculate the session count for all userid's
  8129. var newSessionsCount = {};
  8130. for (userid in obj.wssessions) { newSessionsCount[userid] = obj.wssessions[userid].length; }
  8131. for (serverid in obj.wsPeerSessions3) {
  8132. for (userid in obj.wsPeerSessions3[serverid]) {
  8133. x = obj.wsPeerSessions3[serverid][userid].length;
  8134. if (newSessionsCount[userid] == null) { newSessionsCount[userid] = x; } else { newSessionsCount[userid] += x; }
  8135. }
  8136. }
  8137. // See what session counts have changed, event any changes
  8138. for (userid in newSessionsCount) {
  8139. newcount = newSessionsCount[userid];
  8140. oldcount = obj.sessionsCount[userid];
  8141. if (oldcount == null) { oldcount = 0; } else { delete obj.sessionsCount[userid]; }
  8142. if (newcount != oldcount) {
  8143. x = userid.split('/');
  8144. var u = obj.users[userid];
  8145. if (u) {
  8146. var targets = ['*', 'server-users'];
  8147. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  8148. obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
  8149. }
  8150. }
  8151. }
  8152. // If there are any counts left in the old counts, event to zero
  8153. for (userid in obj.sessionsCount) {
  8154. oldcount = obj.sessionsCount[userid];
  8155. if ((oldcount != null) && (oldcount != 0)) {
  8156. x = userid.split('/');
  8157. var u = obj.users[userid];
  8158. if (u) {
  8159. var targets = ['*', 'server-users'];
  8160. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  8161. obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', userid: userid, username: x[2], count: 0, domain: x[1], nolog: 1, nopeers: 1 })
  8162. }
  8163. }
  8164. }
  8165. // Set the new session counts
  8166. obj.sessionsCount = newSessionsCount;
  8167. } else {
  8168. // Figure out the userid
  8169. userid = changedSessionId.split('/').slice(0, 3).join('/');
  8170. // Recount only changedSessionId
  8171. newcount = 0;
  8172. if (obj.wssessions[userid] != null) { newcount = obj.wssessions[userid].length; }
  8173. for (serverid in obj.wsPeerSessions3) { if (obj.wsPeerSessions3[serverid][userid] != null) { newcount += obj.wsPeerSessions3[serverid][userid].length; } }
  8174. oldcount = obj.sessionsCount[userid];
  8175. if (oldcount == null) { oldcount = 0; }
  8176. // If the count changed, update and event
  8177. if (newcount != oldcount) {
  8178. x = userid.split('/');
  8179. var u = obj.users[userid];
  8180. if (u) {
  8181. var targets = ['*', 'server-users'];
  8182. if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
  8183. obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
  8184. obj.sessionsCount[userid] = newcount;
  8185. }
  8186. }
  8187. }
  8188. };
  8189. /* Access Control Functions */
  8190. // Remove user rights
  8191. function removeUserRights(rights, user) {
  8192. if (user.removeRights == null) return rights;
  8193. var add = 0, substract = 0;
  8194. if ((user.removeRights & 0x00000008) != 0) { substract += 0x00000008; } // No Remote Control
  8195. if ((user.removeRights & 0x00010000) != 0) { add += 0x00010000; } // No Desktop
  8196. if ((user.removeRights & 0x00000100) != 0) { add += 0x00000100; } // Desktop View Only
  8197. if ((user.removeRights & 0x00000200) != 0) { add += 0x00000200; } // No Terminal
  8198. if ((user.removeRights & 0x00000400) != 0) { add += 0x00000400; } // No Files
  8199. if ((user.removeRights & 0x00000010) != 0) { substract += 0x00000010; } // No Console
  8200. if ((user.removeRights & 0x00008000) != 0) { substract += 0x00008000; } // No Uninstall
  8201. if ((user.removeRights & 0x00020000) != 0) { substract += 0x00020000; } // No Remote Command
  8202. if ((user.removeRights & 0x00000040) != 0) { substract += 0x00000040; } // No Wake
  8203. if ((user.removeRights & 0x00040000) != 0) { substract += 0x00040000; } // No Reset/Off
  8204. if (rights != 0xFFFFFFFF) {
  8205. // If not administrator, add and subsctract restrictions
  8206. rights |= add;
  8207. rights &= (0xFFFFFFFF - substract);
  8208. } else {
  8209. // If administrator for a device group, start with permissions and add and subsctract restrictions
  8210. rights = 1 + 2 + 4 + 8 + 32 + 64 + 128 + 16384 + 32768 + 131072 + 262144 + 524288 + 1048576;
  8211. rights |= add;
  8212. rights &= (0xFFFFFFFF - substract);
  8213. }
  8214. return rights;
  8215. }
  8216. // Return the node and rights for a array of nodeids
  8217. obj.GetNodesWithRights = function (domain, user, nodeids, func) {
  8218. var rc = nodeids.length, r = {};
  8219. for (var i in nodeids) {
  8220. obj.GetNodeWithRights(domain, user, nodeids[i], function (node, rights, visible) {
  8221. if ((node != null) && (visible == true)) { r[node._id] = { node: node, rights: rights }; if (--rc == 0) { func(r); } }
  8222. });
  8223. }
  8224. }
  8225. // Return the node and rights for a given nodeid
  8226. obj.GetNodeWithRights = function (domain, user, nodeid, func) {
  8227. // Perform user pre-validation
  8228. if ((user == null) || (nodeid == null)) { func(null, 0, false); return; } // Invalid user
  8229. if (typeof user == 'string') { user = obj.users[user]; }
  8230. if (user == null) { func(null, 0, false); return; } // No rights
  8231. // Perform node pre-validation
  8232. if (obj.common.validateString(nodeid, 0, 128) == false) { func(null, 0, false); return; } // Invalid nodeid
  8233. const snode = nodeid.split('/');
  8234. if ((snode.length != 3) || (snode[0] != 'node')) { func(null, 0, false); return; } // Invalid nodeid
  8235. if ((domain != null) && (snode[1] != domain.id)) { func(null, 0, false); return; } // Invalid domain
  8236. // Check that we have permissions for this node.
  8237. db.Get(nodeid, function (err, nodes) {
  8238. if ((nodes == null) || (nodes.length != 1)) { func(null, 0, false); return; } // No such nodeid
  8239. // This is a super user that can see all device groups for a given domain
  8240. if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (nodes[0].domain == user.domain)) {
  8241. func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return;
  8242. }
  8243. // If no links, stop here.
  8244. if (user.links == null) { func(null, 0, false); return; }
  8245. // Check device link
  8246. var rights = 0, visible = false, r = user.links[nodeid];
  8247. if (r != null) {
  8248. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device link, stop here.
  8249. rights |= r.rights;
  8250. visible = true;
  8251. }
  8252. // Check device group link
  8253. r = user.links[nodes[0].meshid];
  8254. if (r != null) {
  8255. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device group link, stop here.
  8256. rights |= r.rights;
  8257. visible = true;
  8258. }
  8259. // Check user group links
  8260. for (var i in user.links) {
  8261. if (i.startsWith('ugrp/')) {
  8262. const g = obj.userGroups[i];
  8263. if (g && (g.links != null)) {
  8264. r = g.links[nodes[0].meshid];
  8265. if (r != null) {
  8266. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group link, stop here.
  8267. rights |= r.rights; // TODO: Deal with reverse rights
  8268. visible = true;
  8269. }
  8270. r = g.links[nodeid];
  8271. if (r != null) {
  8272. if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group direct link, stop here.
  8273. rights |= r.rights; // TODO: Deal with reverse rights
  8274. visible = true;
  8275. }
  8276. }
  8277. }
  8278. }
  8279. // Remove any user rights
  8280. rights = removeUserRights(rights, user);
  8281. // Return the rights we found
  8282. func(nodes[0], rights, visible);
  8283. });
  8284. }
  8285. // Returns a list of all meshes that this user has some rights too
  8286. obj.GetAllMeshWithRights = function (user, rights) {
  8287. if (typeof user == 'string') { user = obj.users[user]; }
  8288. if (user == null) { return []; }
  8289. var r = [];
  8290. if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) ) {
  8291. // This is a super user that can see all device groups for a given domain
  8292. var meshStartStr = 'mesh/' + user.domain + '/';
  8293. for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]); } }
  8294. return r;
  8295. }
  8296. if (user.links == null) { return []; }
  8297. for (var i in user.links) {
  8298. if (i.startsWith('mesh/')) {
  8299. // Grant access to a device group thru a direct link
  8300. const m = obj.meshes[i];
  8301. if ((m) && (r.indexOf(m) == -1) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) { r.push(m); }
  8302. } else if (i.startsWith('ugrp/')) {
  8303. // Grant access to a device group thru a user group
  8304. const g = obj.userGroups[i];
  8305. for (var j in g.links) {
  8306. if (j.startsWith('mesh/') && ((rights == null) || ((g.links[j].rights != null) && (g.links[j].rights & rights) != 0))) {
  8307. const m = obj.meshes[j];
  8308. if ((m) && (m.deleted == null) && (r.indexOf(m) == -1)) { r.push(m); }
  8309. }
  8310. }
  8311. }
  8312. }
  8313. return r;
  8314. }
  8315. // Returns a list of all mesh id's that this user has some rights too
  8316. obj.GetAllMeshIdWithRights = function (user, rights) {
  8317. if (typeof user == 'string') { user = obj.users[user]; }
  8318. if (user == null) { return []; }
  8319. var r = [];
  8320. if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0)))) {
  8321. // This is a super user that can see all device groups for a given domain
  8322. var meshStartStr = 'mesh/' + user.domain + '/';
  8323. for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]._id); } }
  8324. return r;
  8325. }
  8326. if (user.links == null) { return []; }
  8327. for (var i in user.links) {
  8328. if (i.startsWith('mesh/')) {
  8329. // Grant access to a device group thru a direct link
  8330. const m = obj.meshes[i];
  8331. if ((m) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
  8332. if (r.indexOf(m._id) == -1) { r.push(m._id); }
  8333. }
  8334. } else if (i.startsWith('ugrp/')) {
  8335. // Grant access to a device group thru a user group
  8336. const g = obj.userGroups[i];
  8337. if (g && (g.links != null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
  8338. for (var j in g.links) {
  8339. if (j.startsWith('mesh/')) {
  8340. const m = obj.meshes[j];
  8341. if ((m) && (m.deleted == null)) {
  8342. if (r.indexOf(m._id) == -1) { r.push(m._id); }
  8343. }
  8344. }
  8345. }
  8346. }
  8347. }
  8348. }
  8349. return r;
  8350. }
  8351. // Get the rights of a user on a given device group
  8352. obj.GetMeshRights = function (user, mesh) {
  8353. if ((user == null) || (mesh == null)) { return 0; }
  8354. if (typeof user == 'string') { user = obj.users[user]; }
  8355. if (user == null) { return 0; }
  8356. var r, meshid;
  8357. if (typeof mesh == 'string') {
  8358. meshid = mesh;
  8359. } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
  8360. meshid = mesh._id;
  8361. } else return 0;
  8362. // Check if this is a super user that can see all device groups for a given domain
  8363. if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return removeUserRights(0xFFFFFFFF, user); }
  8364. // Check direct user to device group permissions
  8365. if (user.links == null) return 0;
  8366. var rights = 0;
  8367. r = user.links[meshid];
  8368. if (r != null) {
  8369. var rights = r.rights;
  8370. if (rights == 0xFFFFFFFF) { return removeUserRights(rights, user); } // If the user has full access thru direct link, stop here.
  8371. }
  8372. // Check if we are part of any user groups that would give this user more access.
  8373. for (var i in user.links) {
  8374. if (i.startsWith('ugrp')) {
  8375. const g = obj.userGroups[i];
  8376. if (g) {
  8377. r = g.links[meshid];
  8378. if (r != null) {
  8379. if (r.rights == 0xFFFFFFFF) {
  8380. return removeUserRights(r.rights, user); // If the user hash full access thru a user group link, stop here.
  8381. } else {
  8382. rights |= r.rights; // Add to existing rights (TODO: Deal with reverse rights)
  8383. }
  8384. }
  8385. }
  8386. }
  8387. }
  8388. return removeUserRights(rights, user);
  8389. }
  8390. // Returns true if the user can view the given device group
  8391. obj.IsMeshViewable = function (user, mesh) {
  8392. if ((user == null) || (mesh == null)) { return false; }
  8393. if (typeof user == 'string') { user = obj.users[user]; }
  8394. if (user == null) { return false; }
  8395. var meshid;
  8396. if (typeof mesh == 'string') {
  8397. meshid = mesh;
  8398. } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
  8399. meshid = mesh._id;
  8400. } else return false;
  8401. // Check if this is a super user that can see all device groups for a given domain
  8402. if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return true; }
  8403. // Check direct user to device group permissions
  8404. if (user.links == null) { return false; }
  8405. if (user.links[meshid] != null) { return true; } // If the user has a direct link, stop here.
  8406. // Check if we are part of any user groups that would give this user visibility to this device group.
  8407. for (var i in user.links) {
  8408. if (i.startsWith('ugrp')) {
  8409. const g = obj.userGroups[i];
  8410. if (g && (g.links[meshid] != null)) { return true; } // If the user has a user group link, stop here.
  8411. }
  8412. }
  8413. return false;
  8414. }
  8415. var GetNodeRightsCache = {};
  8416. var GetNodeRightsCacheCount = 0;
  8417. // Return the user rights for a given node
  8418. obj.GetNodeRights = function (user, mesh, nodeid) {
  8419. if ((user == null) || (mesh == null) || (nodeid == null)) { return 0; }
  8420. if (typeof user == 'string') { user = obj.users[user]; }
  8421. if (user == null) { return 0; }
  8422. var meshid;
  8423. if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; } else return 0;
  8424. // Check if we have this in the cache
  8425. const cacheid = user._id + '/' + meshid + '/' + nodeid;
  8426. const cache = GetNodeRightsCache[cacheid];
  8427. if (cache != null) { if (cache.t > Date.now()) { return cache.o; } else { GetNodeRightsCacheCount--; } } // Cache hit, or we need to update the cache
  8428. if (GetNodeRightsCacheCount > 2000) { GetNodeRightsCache = {}; GetNodeRightsCacheCount = 0; } // From time to time, flush the cache
  8429. var r = obj.GetMeshRights(user, mesh);
  8430. if (r == 0xFFFFFFFF) {
  8431. const out = removeUserRights(r, user);
  8432. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  8433. GetNodeRightsCacheCount++;
  8434. return out;
  8435. }
  8436. // Check direct device rights using device data
  8437. if ((user.links != null) && (user.links[nodeid] != null)) { r |= user.links[nodeid].rights; } // TODO: Deal with reverse permissions
  8438. if (r == 0xFFFFFFFF) {
  8439. const out = removeUserRights(r, user);
  8440. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  8441. GetNodeRightsCacheCount++;
  8442. return out;
  8443. }
  8444. // Check direct device rights thru a user group
  8445. for (var i in user.links) {
  8446. if (i.startsWith('ugrp')) {
  8447. const g = obj.userGroups[i];
  8448. if (g && (g.links[nodeid] != null)) { r |= g.links[nodeid].rights; }
  8449. }
  8450. }
  8451. const out = removeUserRights(r, user);
  8452. GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
  8453. GetNodeRightsCacheCount++;
  8454. return out;
  8455. }
  8456. // Returns a list of displatch targets for a given mesh
  8457. // We have to target the meshid and all user groups for this mesh, plus any added targets
  8458. obj.CreateMeshDispatchTargets = function (mesh, addedTargets) {
  8459. var targets = (addedTargets != null) ? addedTargets : [];
  8460. if (targets.indexOf('*') == -1) { targets.push('*'); }
  8461. if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
  8462. if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
  8463. return targets;
  8464. }
  8465. // Returns a list of displatch targets for a given mesh
  8466. // We have to target the meshid and all user groups for this mesh, plus any added targets
  8467. obj.CreateNodeDispatchTargets = function (mesh, nodeid, addedTargets) {
  8468. var targets = (addedTargets != null) ? addedTargets : [];
  8469. targets.push(nodeid);
  8470. if (targets.indexOf('*') == -1) { targets.push('*'); }
  8471. if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
  8472. if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
  8473. for (var i in obj.userGroups) { const g = obj.userGroups[i]; if ((g != null) && (g.links != null) && (g.links[nodeid] != null)) { targets.push(i); } }
  8474. return targets;
  8475. }
  8476. // Clone a safe version of a user object, remove everything that is secret.
  8477. obj.CloneSafeUser = function (user) {
  8478. if (typeof user != 'object') { return user; }
  8479. var user2 = Object.assign({}, user); // Shallow clone
  8480. delete user2.hash;
  8481. delete user2.passhint;
  8482. delete user2.salt;
  8483. delete user2.type;
  8484. delete user2.domain;
  8485. delete user2.subscriptions;
  8486. delete user2.passtype;
  8487. delete user2.otpsms;
  8488. delete user2.otpmsg;
  8489. if ((typeof user2.otpekey == 'object') && (user2.otpekey != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
  8490. if ((typeof user2.otpduo == 'object') && (user2.otpduo != null)) { user2.otpduo = 1; } // Indicates that duo 2FA is enabled.
  8491. if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
  8492. 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.
  8493. if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
  8494. if ((typeof user2.otpdev == 'string') && (user2.otpdev != null)) { user2.otpdev = 1; } // Indicates device for 2FA push notification
  8495. if ((typeof user2.webpush == 'object') && (user2.webpush != null)) { user2.webpush = user2.webpush.length; } // Indicates the number of web push sessions we have
  8496. return user2;
  8497. }
  8498. // Clone a safe version of a node object, remove everything that is secret.
  8499. obj.CloneSafeNode = function (node) {
  8500. if (typeof node != 'object') { return node; }
  8501. var r = node;
  8502. if ((r.pmt != null) || (r.ssh != null) || (r.rdp != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
  8503. r = Object.assign({}, r); // Shallow clone
  8504. if (r.pmt != null) { r.pmt = 1; }
  8505. if (r.ssh != null) {
  8506. var n = {};
  8507. for (var i in r.ssh) {
  8508. if (i.startsWith('user/')) {
  8509. if (r.ssh[i].p) { n[i] = 1; } // Username and password
  8510. else if (r.ssh[i].k && r.ssh[i].kp) { n[i] = 2; } // Username, key and password
  8511. else if (r.ssh[i].k) { n[i] = 3; } // Username and key. No password.
  8512. }
  8513. }
  8514. r.ssh = n;
  8515. }
  8516. if (r.rdp != null) { var n = {}; for (var i in r.rdp) { if (i.startsWith('user/')) { n[i] = 1; } } r.rdp = n; }
  8517. if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
  8518. r.intelamt = Object.assign({}, r.intelamt); // Shallow clone
  8519. if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node
  8520. if (r.intelamt.mpspass != null) { r.intelamt.mpspass = 1; }; // Remove the Intel AMT MPS password from the node
  8521. }
  8522. }
  8523. return r;
  8524. }
  8525. // Clone a safe version of a mesh object, remove everything that is secret.
  8526. obj.CloneSafeMesh = function (mesh) {
  8527. if (typeof mesh != 'object') { return mesh; }
  8528. var r = mesh;
  8529. if (((r.amt != null) && (r.amt.password != null)) || ((r.kvm != null) && (r.kvm.pass != null))) {
  8530. r = Object.assign({}, r); // Shallow clone
  8531. if ((r.amt != null) && (r.amt.password != null)) {
  8532. r.amt = Object.assign({}, r.amt); // Shallow clone
  8533. if ((r.amt.password != null) && (r.amt.password != '')) { r.amt.password = 1; } // Remove the Intel AMT password from the policy
  8534. }
  8535. if ((r.kvm != null) && (r.kvm.pass != null)) {
  8536. r.kvm = Object.assign({}, r.kvm); // Shallow clone
  8537. if ((r.kvm.pass != null) && (r.kvm.pass != '')) { r.kvm.pass = 1; } // Remove the IP KVM device password
  8538. }
  8539. }
  8540. return r;
  8541. }
  8542. // Filter the user web site and only output state that we need to keep
  8543. const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes','uiViewMode'];
  8544. const acceptableUserWebStateDesktopStrings = [
  8545. 'encoding', 'showfocus', 'showmouse', 'quality', 'scaling', 'framerate', 'agentencoding', 'swapmouse',
  8546. 'rmw', 'remotekeymap', 'autoclipboard', 'autolock', 'localkeymap', 'kvmrmw', 'rdpsize', 'rdpsmb',
  8547. 'rdprmw', 'rdpautoclipboard', 'rdpflags'
  8548. ];
  8549. obj.filterUserWebState = function (state) {
  8550. if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }
  8551. if ((state == null) || (typeof state != 'object')) { return null; }
  8552. var out = {};
  8553. for (var i in acceptableUserWebStateStrings) {
  8554. var n = acceptableUserWebStateStrings[i];
  8555. if ((state[n] != null) && ((typeof state[n] == 'number') || (typeof state[n] == 'boolean') || ((typeof state[n] == 'string') && (state[n].length < 64)))) { out[n] = state[n]; }
  8556. }
  8557. if ((typeof state.stars == 'string') && (state.stars.length < 2048)) { out.stars = state.stars; }
  8558. if (typeof state.desktopsettings == 'string') { try { state.desktopsettings = JSON.parse(state.desktopsettings); } catch (ex) { delete state.desktopsettings; } }
  8559. if (state.desktopsettings != null) {
  8560. out.desktopsettings = {};
  8561. for (var i in acceptableUserWebStateDesktopStrings) {
  8562. var n = acceptableUserWebStateDesktopStrings[i];
  8563. 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]; }
  8564. }
  8565. out.desktopsettings = JSON.stringify(out.desktopsettings);
  8566. }
  8567. if ((typeof state.deskKeyShortcuts == 'string') && (state.deskKeyShortcuts.length < 2048)) { out.deskKeyShortcuts = state.deskKeyShortcuts; }
  8568. if ((typeof state.deskStrings == 'string') && (state.deskStrings.length < 10000)) { out.deskStrings = state.deskStrings; }
  8569. if ((typeof state.runopt == 'string') && (state.runopt.length < 30000)) { out.runopt = state.runopt; }
  8570. return JSON.stringify(out);
  8571. }
  8572. // Return the correct render page given mobile, minify and override path.
  8573. function getRenderPage(pagename, req, domain) {
  8574. var mobile = isMobileBrowser(req), minify = (domain.minify == true), p;
  8575. if (req.query.mobile == '1') { mobile = true; } else if (req.query.mobile == '0') { mobile = false; }
  8576. if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
  8577. if ((domain != null) && (domain.mobilesite === false)) { mobile = false; }
  8578. if (mobile) {
  8579. if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
  8580. if (minify) {
  8581. p = obj.path.join(domain.webviewspath, pagename + '-mobile-min');
  8582. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
  8583. }
  8584. p = obj.path.join(domain.webviewspath, pagename + '-mobile');
  8585. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
  8586. }
  8587. if (obj.parent.webViewsOverridePath != null) {
  8588. if (minify) {
  8589. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile-min');
  8590. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
  8591. }
  8592. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile');
  8593. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
  8594. }
  8595. if (minify) {
  8596. p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile-min');
  8597. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify document
  8598. }
  8599. p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile');
  8600. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile document
  8601. }
  8602. if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
  8603. if (minify) {
  8604. p = obj.path.join(domain.webviewspath, pagename + '-min');
  8605. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
  8606. }
  8607. p = obj.path.join(domain.webviewspath, pagename);
  8608. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
  8609. }
  8610. if (obj.parent.webViewsOverridePath != null) {
  8611. if (minify) {
  8612. p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-min');
  8613. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
  8614. }
  8615. p = obj.path.join(obj.parent.webViewsOverridePath, pagename);
  8616. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
  8617. }
  8618. if (minify) {
  8619. p = obj.path.join(obj.parent.webViewsPath, pagename + '-min');
  8620. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify document
  8621. }
  8622. p = obj.path.join(obj.parent.webViewsPath, pagename);
  8623. if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Default document
  8624. return null;
  8625. }
  8626. function generateCustomCSSTags(customFilesObject, currentTemplate) {
  8627. var cssTags = '';
  8628. cssTags += '<link keeplink=1 type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />\n ';
  8629. if (customFilesObject) {
  8630. if (Array.isArray(customFilesObject)) {
  8631. // Legacy array support - convert to object format
  8632. for (var i = 0; i < customFilesObject.length; i++) {
  8633. var customFileConfig = customFilesObject[i];
  8634. if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
  8635. if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
  8636. (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
  8637. for (var j = 0; j < customFileConfig.css.length; j++) {
  8638. cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFileConfig.css[j] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
  8639. }
  8640. }
  8641. }
  8642. }
  8643. } else if (customFilesObject.css && Array.isArray(customFilesObject.css)) {
  8644. // Legacy single object support
  8645. for (var i = 0; i < customFilesObject.css.length; i++) {
  8646. cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFilesObject.css[i] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
  8647. }
  8648. } else if (typeof customFilesObject === 'object') {
  8649. // New object format - iterate through each custom file configuration
  8650. for (var configName in customFilesObject) {
  8651. var customFileConfig = customFilesObject[configName];
  8652. if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
  8653. if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
  8654. (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
  8655. for (var j = 0; j < customFileConfig.css.length; j++) {
  8656. cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFileConfig.css[j] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
  8657. }
  8658. }
  8659. }
  8660. }
  8661. }
  8662. }
  8663. return cssTags.trim();
  8664. }
  8665. function generateCustomJSTags(customFilesObject, currentTemplate) {
  8666. var jsTags = '';
  8667. jsTags += '<script keeplink=1 type="text/javascript" src="scripts/custom.js"></script>\n ';
  8668. if (customFilesObject) {
  8669. if (Array.isArray(customFilesObject)) {
  8670. // Legacy array support - convert to object format
  8671. for (var i = 0; i < customFilesObject.length; i++) {
  8672. var customFileConfig = customFilesObject[i];
  8673. if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
  8674. if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
  8675. (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
  8676. for (var j = 0; j < customFileConfig.js.length; j++) {
  8677. jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFileConfig.js[j] + '"></script>\n ';
  8678. }
  8679. }
  8680. }
  8681. }
  8682. } else if (customFilesObject.js && Array.isArray(customFilesObject.js)) {
  8683. // Legacy single object support
  8684. for (var i = 0; i < customFilesObject.js.length; i++) {
  8685. jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFilesObject.js[i] + '"></script>\n ';
  8686. }
  8687. } else if (typeof customFilesObject === 'object') {
  8688. // New object format - iterate through each custom file configuration
  8689. for (var configName in customFilesObject) {
  8690. var customFileConfig = customFilesObject[configName];
  8691. if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
  8692. if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
  8693. (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
  8694. for (var j = 0; j < customFileConfig.js.length; j++) {
  8695. jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFileConfig.js[j] + '"></script>\n ';
  8696. }
  8697. }
  8698. }
  8699. }
  8700. }
  8701. }
  8702. return jsTags.trim();
  8703. }
  8704. // Return the correct render page arguments.
  8705. function getRenderArgs(xargs, req, domain, page) {
  8706. var minify = (domain.minify == true);
  8707. if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
  8708. xargs.min = minify ? '-min' : '';
  8709. xargs.titlehtml = domain.titlehtml;
  8710. xargs.title = (domain.title != null) ? domain.title : 'MeshCentral';
  8711. if (
  8712. ((page == 'login2') && (domain.loginpicture == null) && (domain.titlehtml == null)) ||
  8713. ((page != 'login2') && (domain.titlepicture == null) && (domain.titlehtml == null))
  8714. ) {
  8715. if (domain.title == null) {
  8716. xargs.title1 = 'MeshCentral';
  8717. xargs.title2 = '';
  8718. } else {
  8719. xargs.title1 = domain.title;
  8720. xargs.title2 = domain.title2 ? domain.title2 : '';
  8721. }
  8722. } else {
  8723. xargs.title1 = domain.title1 ? domain.title1 : '';
  8724. xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
  8725. }
  8726. xargs.title2 = obj.common.replacePlaceholders(xargs.title2, {
  8727. 'serverversion': obj.parent.currentVer,
  8728. 'servername': obj.getWebServerName(domain, req),
  8729. 'agentsessions': Object.keys(parent.webserver.wsagents).length,
  8730. 'connectedusers': Object.keys(parent.webserver.wssessions).length,
  8731. 'userssessions': Object.keys(parent.webserver.wssessions2).length,
  8732. 'relaysessions': parent.webserver.relaySessionCount,
  8733. 'relaycount': Object.keys(parent.webserver.wsrelays).length
  8734. });
  8735. xargs.extitle = encodeURIComponent(xargs.title).split('\'').join('\\\'');
  8736. xargs.domainurl = domain.url;
  8737. xargs.autocomplete = (domain.autocomplete === false) ? 'autocomplete=off x' : 'autocomplete'; // This option allows autocomplete to be turned off on the login page.
  8738. if (typeof domain.hide == 'number') { xargs.hide = domain.hide; }
  8739. // To mitigate any possible BREACH attack, we generate a random 0 to 255 bytes length string here.
  8740. xargs.randomlength = (args.webpagelengthrandomization !== false) ? parent.crypto.randomBytes(parent.crypto.randomBytes(1)[0]).toString('base64') : '';
  8741. // Generate custom CSS and JS tags
  8742. if (xargs.customFiles) {
  8743. try {
  8744. var customFiles = JSON.parse(decodeURIComponent(xargs.customFiles));
  8745. xargs.customCSSTags = generateCustomCSSTags(customFiles, page);
  8746. xargs.customJSTags = generateCustomJSTags(customFiles, page);
  8747. } catch (ex) {
  8748. xargs.customCSSTags = generateCustomCSSTags(null, page);
  8749. xargs.customJSTags = generateCustomJSTags(null, page);
  8750. }
  8751. } else {
  8752. xargs.customCSSTags = generateCustomCSSTags(null, page);
  8753. xargs.customJSTags = generateCustomJSTags(null, page);
  8754. }
  8755. return xargs;
  8756. }
  8757. // Route a command from a agent. domainid, nodeid and meshid are the values of the source agent.
  8758. obj.routeAgentCommand = function (command, domainid, nodeid, meshid) {
  8759. // Route a message.
  8760. // If this command has a sessionid, that is the target.
  8761. if (command.sessionid != null) {
  8762. if (typeof command.sessionid != 'string') return;
  8763. var splitsessionid = command.sessionid.split('/');
  8764. // Check that we are in the same domain and the user has rights over this node.
  8765. if ((splitsessionid.length == 4) && (splitsessionid[0] == 'user') && (splitsessionid[1] == domainid)) {
  8766. // Check if this user has rights to get this message
  8767. if (obj.GetNodeRights(splitsessionid[0] + '/' + splitsessionid[1] + '/' + splitsessionid[2], meshid, nodeid) == 0) return; // TODO: Check if this is ok
  8768. // See if the session is connected. If so, go ahead and send this message to the target node
  8769. var ws = obj.wssessions2[command.sessionid];
  8770. if (ws != null) {
  8771. command.nodeid = nodeid; // Set the nodeid, required for responses.
  8772. delete command.sessionid; // Remove the sessionid, since we are sending to that sessionid, so it's implyed.
  8773. try { ws.send(JSON.stringify(command)); } catch (ex) { }
  8774. } else if (parent.multiServer != null) {
  8775. // See if we can send this to a peer server
  8776. var serverid = obj.wsPeerSessions2[command.sessionid];
  8777. if (serverid != null) {
  8778. command.fromNodeid = nodeid;
  8779. parent.multiServer.DispatchMessageSingleServer(command, serverid);
  8780. }
  8781. }
  8782. }
  8783. } else if (command.userid != null) { // If this command has a userid, that is the target.
  8784. if (typeof command.userid != 'string') return;
  8785. var splituserid = command.userid.split('/');
  8786. // Check that we are in the same domain and the user has rights over this node.
  8787. if ((splituserid[0] == 'user') && (splituserid[1] == domainid)) {
  8788. // Check if this user has rights to get this message
  8789. if (obj.GetNodeRights(command.userid, meshid, nodeid) == 0) return; // TODO: Check if this is ok
  8790. // See if the session is connected
  8791. var sessions = obj.wssessions[command.userid];
  8792. // Go ahead and send this message to the target node
  8793. if (sessions != null) {
  8794. command.nodeid = nodeid; // Set the nodeid, required for responses.
  8795. delete command.userid; // Remove the userid, since we are sending to that userid, so it's implyed.
  8796. for (i in sessions) { sessions[i].send(JSON.stringify(command)); }
  8797. }
  8798. if (parent.multiServer != null) {
  8799. // TODO: Add multi-server support
  8800. }
  8801. }
  8802. } else { // Route this command to all users with MESHRIGHT_AGENTCONSOLE rights to this device group
  8803. command.nodeid = nodeid;
  8804. var cmdstr = JSON.stringify(command);
  8805. // Find all connected user sessions with access to this device
  8806. for (var userid in obj.wssessions) {
  8807. var xsessions = obj.wssessions[userid];
  8808. if (obj.GetNodeRights(userid, meshid, nodeid) != 0) {
  8809. // Send the message to all sessions for this user on this server
  8810. for (i in xsessions) { try { xsessions[i].send(cmdstr); } catch (e) { } }
  8811. }
  8812. }
  8813. // Send the message to all users of other servers
  8814. if (parent.multiServer != null) {
  8815. delete command.nodeid;
  8816. command.fromNodeid = nodeid;
  8817. command.meshid = meshid;
  8818. parent.multiServer.DispatchMessage(command);
  8819. }
  8820. }
  8821. }
  8822. // Returns a list of acceptable languages in order
  8823. obj.getLanguageCodes = function (req) {
  8824. // If a user set a localization, use that
  8825. if ((req.query.lang == null) && (req.session != null) && (req.session.userid)) {
  8826. var user = obj.users[req.session.userid];
  8827. if ((user != null) && (user.lang != null)) { req.query.lang = user.lang; }
  8828. };
  8829. // Get a list of acceptable languages in order
  8830. var acceptLanguages = [];
  8831. if (req.query.lang != null) {
  8832. acceptLanguages.push(req.query.lang.toLowerCase());
  8833. } else {
  8834. if (req.headers['accept-language'] != null) {
  8835. var acceptLanguageSplit = req.headers['accept-language'].split(';');
  8836. for (var i in acceptLanguageSplit) {
  8837. var acceptLanguageSplitEx = acceptLanguageSplit[i].split(',');
  8838. for (var j in acceptLanguageSplitEx) { if (acceptLanguageSplitEx[j].startsWith('q=') == false) { acceptLanguages.push(acceptLanguageSplitEx[j].toLowerCase()); } }
  8839. }
  8840. }
  8841. }
  8842. return acceptLanguages;
  8843. }
  8844. // Render a page using the proper language
  8845. function render(req, res, filename, args, user) {
  8846. if (obj.renderPages != null) {
  8847. // Get the list of acceptable languages in order
  8848. var acceptLanguages = obj.getLanguageCodes(req);
  8849. var domain = getDomain(req);
  8850. // Take a look at the options we have for this file
  8851. var fileOptions = obj.renderPages[domain.id][obj.path.basename(filename)];
  8852. if (fileOptions != null) {
  8853. for (var i in acceptLanguages) {
  8854. if (acceptLanguages[i] == 'zh-tw') { acceptLanguages[i] = 'zh-cht'; } // Change newer "zh-tw" to legacy "zh-cht" Chinese (Traditional) for now
  8855. if (acceptLanguages[i] == 'zh-cn') { acceptLanguages[i] = 'zh-chs'; } // Change newer "zh-ch" to legacy "zh-chs" Chinese (Simplified) for now
  8856. if ((acceptLanguages[i] == 'en') || (acceptLanguages[i].startsWith('en-'))) {
  8857. // English requested
  8858. args.lang = 'en';
  8859. 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".
  8860. break;
  8861. }
  8862. // See if a language (like "fr-ca") or short-language (like "fr") matches an available translation file.
  8863. var foundLanguage = null;
  8864. if (fileOptions[acceptLanguages[i]] != null) { foundLanguage = acceptLanguages[i]; } else {
  8865. const ptr = acceptLanguages[i].indexOf('-');
  8866. if (ptr >= 0) {
  8867. const shortAcceptedLanguage = acceptLanguages[i].substring(0, ptr);
  8868. if (fileOptions[shortAcceptedLanguage] != null) { foundLanguage = shortAcceptedLanguage; }
  8869. }
  8870. }
  8871. // If a language is found, render it.
  8872. if (foundLanguage != null) {
  8873. // Found a match. If the file no longer exists, default to English.
  8874. obj.fs.exists(fileOptions[foundLanguage] + '.handlebars', function (exists) {
  8875. if (exists) { args.lang = foundLanguage; res.render(fileOptions[foundLanguage], args); } else { args.lang = 'en'; res.render(filename, args); }
  8876. });
  8877. if (user && (user.llang != foundLanguage)) { user.llang = foundLanguage; obj.db.SetUser(user); } // Set user 'last language' used if needed.
  8878. return;
  8879. }
  8880. }
  8881. }
  8882. }
  8883. // No matches found, render the default English page.
  8884. res.render(filename, args);
  8885. }
  8886. // Get the list of pages with different languages that can be rendered
  8887. function getRenderList() {
  8888. // Fetch default rendeing pages
  8889. var translateFolder = null;
  8890. if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
  8891. if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
  8892. if (translateFolder != null) {
  8893. obj.renderPages = {};
  8894. obj.renderLanguages = ['en'];
  8895. for (var i in parent.config.domains) {
  8896. if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
  8897. if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
  8898. var files = obj.fs.readdirSync(translateFolder);
  8899. var domain = parent.config.domains[i].id;
  8900. obj.renderPages[domain] = {};
  8901. for (var i in files) {
  8902. var name = files[i];
  8903. if (name.endsWith('.handlebars')) {
  8904. name = name.substring(0, name.length - 11);
  8905. var xname = name.split('_');
  8906. if (xname.length == 2) {
  8907. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8908. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8909. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8910. }
  8911. }
  8912. }
  8913. // See if there are any custom rending pages that will override the default ones
  8914. if ((obj.parent.webViewsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webViewsOverridePath, 'translations')))) {
  8915. translateFolder = obj.path.join(obj.parent.webViewsOverridePath, 'translations');
  8916. var files = obj.fs.readdirSync(translateFolder);
  8917. for (var i in files) {
  8918. var name = files[i];
  8919. if (name.endsWith('.handlebars')) {
  8920. name = name.substring(0, name.length - 11);
  8921. var xname = name.split('_');
  8922. if (xname.length == 2) {
  8923. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8924. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8925. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8926. }
  8927. }
  8928. }
  8929. }
  8930. // See if there is a custom meshcentral-web-domain folder as that will override the default ones
  8931. if (obj.fs.existsSync(obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations'))) {
  8932. translateFolder = obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations');
  8933. var files = obj.fs.readdirSync(translateFolder);
  8934. for (var i in files) {
  8935. var name = files[i];
  8936. if (name.endsWith('.handlebars')) {
  8937. name = name.substring(0, name.length - 11);
  8938. var xname = name.split('_');
  8939. if (xname.length == 2) {
  8940. if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
  8941. obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
  8942. if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
  8943. }
  8944. }
  8945. }
  8946. }
  8947. }
  8948. }
  8949. }
  8950. // Get the list of pages with different languages that can be rendered
  8951. function getEmailLanguageList() {
  8952. // Fetch default rendeing pages
  8953. var translateFolder = null;
  8954. if (obj.fs.existsSync('emails/translations')) { translateFolder = 'emails/translations'; }
  8955. if (obj.fs.existsSync(obj.path.join(__dirname, 'emails', 'translations'))) { translateFolder = obj.path.join(__dirname, 'emails', 'translations'); }
  8956. if (translateFolder != null) {
  8957. obj.emailLanguages = ['en'];
  8958. var files = obj.fs.readdirSync(translateFolder);
  8959. for (var i in files) {
  8960. var name = files[i];
  8961. if (name.endsWith('.html')) {
  8962. name = name.substring(0, name.length - 5);
  8963. var xname = name.split('_');
  8964. if (xname.length == 2) {
  8965. if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
  8966. }
  8967. }
  8968. }
  8969. // See if there are any custom rending pages that will override the default ones
  8970. if ((obj.parent.webEmailsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webEmailsOverridePath, 'translations')))) {
  8971. translateFolder = obj.path.join(obj.parent.webEmailsOverridePath, 'translations');
  8972. var files = obj.fs.readdirSync(translateFolder);
  8973. for (var i in files) {
  8974. var name = files[i];
  8975. if (name.endsWith('.html')) {
  8976. name = name.substring(0, name.length - 5);
  8977. var xname = name.split('_');
  8978. if (xname.length == 2) {
  8979. if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
  8980. }
  8981. }
  8982. }
  8983. }
  8984. }
  8985. }
  8986. // Perform a web push to a user
  8987. // If any of the push fail, remove the subscription from the user's webpush subscription list.
  8988. obj.performWebPush = function (domain, user, payload, options) {
  8989. if ((parent.webpush == null) || (Array.isArray(user.webpush) == false) || (user.webpush.length == 0)) return;
  8990. var completionFunc = function pushCompletionFunc(sub, fail) {
  8991. pushCompletionFunc.failCount += fail;
  8992. if (--pushCompletionFunc.pushCount == 0) {
  8993. if (pushCompletionFunc.failCount > 0) {
  8994. var user = pushCompletionFunc.user, newwebpush = [];
  8995. for (var i in user.webpush) { if (user.webpush[i].fail == null) { newwebpush.push(user.webpush[i]); } }
  8996. user.webpush = newwebpush;
  8997. // Update the database
  8998. obj.db.SetUser(user);
  8999. // Event the change
  9000. var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
  9001. 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.
  9002. var targets = ['*', 'server-users', user._id];
  9003. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  9004. parent.DispatchEvent(targets, obj, message);
  9005. }
  9006. }
  9007. }
  9008. completionFunc.pushCount = user.webpush.length;
  9009. completionFunc.user = user;
  9010. completionFunc.domain = domain;
  9011. completionFunc.failCount = 0;
  9012. for (var i in user.webpush) {
  9013. var errorFunc = function pushErrorFunc(error) { pushErrorFunc.sub.fail = 1; pushErrorFunc.call(pushErrorFunc.sub, 1); }
  9014. errorFunc.sub = user.webpush[i];
  9015. errorFunc.call = completionFunc;
  9016. var successFunc = function pushSuccessFunc(value) { pushSuccessFunc.call(pushSuccessFunc.sub, 0); }
  9017. successFunc.sub = user.webpush[i];
  9018. successFunc.call = completionFunc;
  9019. parent.webpush.sendNotification(user.webpush[i], JSON.stringify(payload), options).then(successFunc, errorFunc);
  9020. }
  9021. }
  9022. // Ensure exclusivity of a push messaging token for Android device
  9023. obj.removePmtFromAllOtherNodes = function (node) {
  9024. if (typeof node.pmt != 'string') return;
  9025. db.Get('pmt_' + node.pmt, function (err, docs) {
  9026. if ((err == null) && (docs.length == 1)) {
  9027. var oldNodeId = docs[0].nodeid;
  9028. db.Get(oldNodeId, function (nerr, ndocs) {
  9029. if ((nerr == null) && (ndocs.length == 1)) {
  9030. var oldNode = ndocs[0];
  9031. if (oldNode.pmt == node.pmt) {
  9032. // Remove the push messaging token and save the node.
  9033. delete oldNode.pmt;
  9034. db.Set(oldNode);
  9035. // Event the node change
  9036. var event = { etype: 'node', action: 'changenode', nodeid: oldNode._id, domain: oldNode.domain, node: obj.CloneSafeNode(oldNode) }
  9037. 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.
  9038. parent.DispatchEvent(['*', oldNode.meshid, oldNode._id], obj, event);
  9039. }
  9040. }
  9041. });
  9042. }
  9043. db.Set({ _id: 'pmt_' + node.pmt, type: 'pmt', domain: node.domain, time: Date.now(), nodeid: node._id })
  9044. });
  9045. }
  9046. // Return true if a mobile browser is detected.
  9047. // 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/
  9048. function isMobileBrowser(req) {
  9049. //var ua = req.headers['user-agent'].toLowerCase();
  9050. //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)));
  9051. if (typeof req.headers['user-agent'] != 'string') return false;
  9052. return (req.headers['user-agent'].toLowerCase().indexOf('mobile') >= 0);
  9053. }
  9054. // Return decoded user agent information
  9055. obj.getUserAgentInfo = function (req) {
  9056. var browser = 'Unknown', os = 'Unknown';
  9057. try {
  9058. const ua = obj.uaparser((typeof req == 'string') ? req : req.headers['user-agent']);
  9059. // Add client hints if available
  9060. if((typeof req != 'string')){
  9061. const ch = new obj.uaclienthints.UAClientHints().setValuesFromHeaders(req.headers);
  9062. Object.assign(ua, ch);
  9063. }
  9064. if (ua.browser && ua.browser.name) { ua.browserStr = ua.browser.name; if (ua.browser.version) { ua.browserStr += '/' + ua.browser.version } }
  9065. if (ua.os && ua.os.name) { ua.osStr = ua.os.name; if (ua.os.version) { ua.osStr += '/' + ua.os.version } }
  9066. // If the platform is set, use that instead of the OS
  9067. if (ua.platform) {
  9068. ua.osStr = ua.platform;
  9069. // Special case for Windows 11
  9070. if (ua.platformVersion) {
  9071. if (ua.platform == 'Windows' && parseInt(ua.platformVersion) >= 13) {
  9072. ua.platformVersion = '11';
  9073. }
  9074. ua.osStr += '/' + ua.platformVersion
  9075. }
  9076. }
  9077. return ua;
  9078. } catch (ex) { return { browserStr: browser, osStr: os } }
  9079. }
  9080. // Return the query string portion of the URL, the ? and anything after BUT remove secret keys from authentication providers
  9081. function getQueryPortion(req) {
  9082. var removeKeys = ['duo_code', 'state']; // Keys to remove
  9083. var s = req.url.indexOf('?');
  9084. if (s == -1) {
  9085. if (req.body && req.body.urlargs) {
  9086. return req.body.urlargs;
  9087. }
  9088. return '';
  9089. }
  9090. var queryString = req.url.substring(s + 1);
  9091. var params = queryString.split('&');
  9092. var filteredParams = [];
  9093. for (var i = 0; i < params.length; i++) {
  9094. var key = params[i].split('=')[0];
  9095. if (removeKeys.indexOf(key) === -1) {
  9096. filteredParams.push(params[i]);
  9097. }
  9098. }
  9099. return (filteredParams.length > 0 ? ('?' + filteredParams.join('&')) : '');
  9100. }
  9101. // Generate a random Intel AMT password
  9102. function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
  9103. function getRandomAmtPassword() { var p; do { p = Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
  9104. function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); }
  9105. 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; }
  9106. // Generate a 8 digit integer with even random probability for each value.
  9107. function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
  9108. function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
  9109. // Clean a IPv6 address that encodes a IPv4 address
  9110. function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
  9111. // Set the content disposition header for a HTTP response.
  9112. // Because the filename can't have any special characters in it, we need to be extra careful.
  9113. function setContentDispositionHeader(res, type, name, size, altname) {
  9114. 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('');
  9115. try {
  9116. var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(name) + '"' };
  9117. if (typeof size == 'number') { x['Content-Length'] = size; }
  9118. res.set(x);
  9119. } catch (ex) {
  9120. var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
  9121. if (typeof size == 'number') { x['Content-Length'] = size; }
  9122. res.set(x);
  9123. }
  9124. }
  9125. // Perform a IP match against a list
  9126. function isIPMatch(ip, matchList) {
  9127. const ipcheck = require('ipcheck');
  9128. for (var i in matchList) { if (ipcheck.match(ip, matchList[i]) == true) return true; }
  9129. return false;
  9130. }
  9131. // This is the invalid login throttling code
  9132. obj.badLoginTable = {};
  9133. obj.badLoginTableLastClean = 0;
  9134. if (parent.config.settings == null) { parent.config.settings = {}; }
  9135. if (parent.config.settings.maxinvalidlogin !== false) {
  9136. if (typeof parent.config.settings.maxinvalidlogin != 'object') { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
  9137. if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
  9138. if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
  9139. if ((typeof parent.config.settings.maxinvalidlogin.coolofftime != 'number') || (parent.config.settings.maxinvalidlogin.coolofftime < 1)) { parent.config.settings.maxinvalidlogin.coolofftime = null; }
  9140. }
  9141. obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
  9142. if (parent.config.settings.maxinvalidlogin === false) return;
  9143. if (typeof ip == 'object') { ip = ip.clientIp; }
  9144. if (parent.config.settings.maxinvalidlogin != null) {
  9145. if (typeof parent.config.settings.maxinvalidlogin.exclude == 'string') {
  9146. const excludeSplit = parent.config.settings.maxinvalidlogin.exclude.split(',');
  9147. for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
  9148. } else if (Array.isArray(parent.config.settings.maxinvalidlogin.exclude)) {
  9149. for (var i in parent.config.settings.maxinvalidlogin.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalidlogin.exclude[i])) return; }
  9150. }
  9151. }
  9152. var splitip = ip.split('.');
  9153. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
  9154. if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
  9155. if (typeof obj.badLoginTable[ip] == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return; } } // Check cooloff period
  9156. if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
  9157. if ((obj.badLoginTable[ip].length >= parent.config.settings.maxinvalidlogin.count) && (parent.config.settings.maxinvalidlogin.coolofftime != null)) {
  9158. obj.badLoginTable[ip] = Date.now() + (parent.config.settings.maxinvalidlogin.coolofftime * 60000); // Move to cooloff period
  9159. }
  9160. }
  9161. obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
  9162. if (parent.config.settings.maxinvalidlogin === false) return true;
  9163. if (typeof ip == 'object') { ip = ip.clientIp; }
  9164. var splitip = ip.split('.');
  9165. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
  9166. var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
  9167. var ipTable = obj.badLoginTable[ip];
  9168. if (ipTable == null) return true;
  9169. if (typeof ipTable == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return false; } } // Check cooloff period
  9170. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  9171. if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
  9172. return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
  9173. }
  9174. obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
  9175. if (parent.config.settings.maxinvalidlogin === false) return;
  9176. var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
  9177. for (var ip in obj.badLoginTable) {
  9178. var ipTable = obj.badLoginTable[ip];
  9179. if (typeof ipTable == 'number') {
  9180. if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } // Check cooloff period
  9181. } else {
  9182. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  9183. if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
  9184. }
  9185. }
  9186. obj.badLoginTableLastClean = 0;
  9187. }
  9188. // This is the invalid 2FA throttling code
  9189. obj.bad2faTable = {};
  9190. obj.bad2faTableLastClean = 0;
  9191. if (parent.config.settings == null) { parent.config.settings = {}; }
  9192. if (parent.config.settings.maxinvalid2fa !== false) {
  9193. if (typeof parent.config.settings.maxinvalid2fa != 'object') { parent.config.settings.maxinvalid2fa = { time: 10, count: 10 }; }
  9194. if (typeof parent.config.settings.maxinvalid2fa.time != 'number') { parent.config.settings.maxinvalid2fa.time = 10; }
  9195. if (typeof parent.config.settings.maxinvalid2fa.count != 'number') { parent.config.settings.maxinvalid2fa.count = 10; }
  9196. if ((typeof parent.config.settings.maxinvalid2fa.coolofftime != 'number') || (parent.config.settings.maxinvalid2fa.coolofftime < 1)) { parent.config.settings.maxinvalid2fa.coolofftime = null; }
  9197. }
  9198. obj.setbad2Fa = function (ip) { // Set an IP address that just did a bad 2FA request
  9199. if (parent.config.settings.maxinvalid2fa === false) return;
  9200. if (typeof ip == 'object') { ip = ip.clientIp; }
  9201. if (parent.config.settings.maxinvalid2fa != null) {
  9202. if (typeof parent.config.settings.maxinvalid2fa.exclude == 'string') {
  9203. const excludeSplit = parent.config.settings.maxinvalid2fa.exclude.split(',');
  9204. for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
  9205. } else if (Array.isArray(parent.config.settings.maxinvalid2fa.exclude)) {
  9206. for (var i in parent.config.settings.maxinvalid2fa.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalid2fa.exclude[i])) return; }
  9207. }
  9208. }
  9209. var splitip = ip.split('.');
  9210. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
  9211. if (++obj.bad2faTableLastClean > 100) { obj.cleanBad2faTable(); }
  9212. if (typeof obj.bad2faTable[ip] == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return; } } // Check cooloff period
  9213. if (obj.bad2faTable[ip] == null) { obj.bad2faTable[ip] = [Date.now()]; } else { obj.bad2faTable[ip].push(Date.now()); }
  9214. if ((obj.bad2faTable[ip].length >= parent.config.settings.maxinvalid2fa.count) && (parent.config.settings.maxinvalid2fa.coolofftime != null)) {
  9215. obj.bad2faTable[ip] = Date.now() + (parent.config.settings.maxinvalid2fa.coolofftime * 60000); // Move to cooloff period
  9216. }
  9217. }
  9218. obj.checkAllow2Fa = function (ip) { // Check if an IP address is allowed to perform 2FA
  9219. if (parent.config.settings.maxinvalid2fa === false) return true;
  9220. if (typeof ip == 'object') { ip = ip.clientIp; }
  9221. var splitip = ip.split('.');
  9222. if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
  9223. var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
  9224. var ipTable = obj.bad2faTable[ip];
  9225. if (ipTable == null) return true;
  9226. if (typeof ipTable == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return false; } } // Check cooloff period
  9227. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  9228. if (ipTable.length == 0) { delete obj.bad2faTable[ip]; return true; }
  9229. return (ipTable.length < parent.config.settings.maxinvalid2fa.count); // No more than x bad 2FAs in x minutes
  9230. }
  9231. obj.cleanBad2faTable = function () { // Clean up the IP address 2FA blockage table, we do this occasionaly.
  9232. if (parent.config.settings.maxinvalid2fa === false) return;
  9233. var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
  9234. for (var ip in obj.bad2faTable) {
  9235. var ipTable = obj.bad2faTable[ip];
  9236. if (typeof ipTable == 'number') {
  9237. if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } // Check cooloff period
  9238. } else {
  9239. while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
  9240. if (ipTable.length == 0) { delete obj.bad2faTable[ip]; }
  9241. }
  9242. }
  9243. obj.bad2faTableLastClean = 0;
  9244. }
  9245. // Hold a websocket until additional arguments are provided within the socket.
  9246. // This is a generic function that can be used for any websocket to avoid passing arguments in the URL.
  9247. function getWebsocketArgs(ws, req, func) {
  9248. if (req.query.moreargs != '1') {
  9249. // No more arguments needed, pass the websocket thru
  9250. func(ws, req);
  9251. } else {
  9252. // More arguments are needed
  9253. delete req.query.moreargs;
  9254. const xfunc = function getWebsocketArgsEx(msg) {
  9255. var command = null;
  9256. try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
  9257. if ((command != null) && (command.action === 'urlargs') && (typeof command.args == 'object')) {
  9258. for (var i in command.args) { getWebsocketArgsEx.req.query[i] = command.args[i]; }
  9259. ws.removeEventListener('message', getWebsocketArgsEx);
  9260. getWebsocketArgsEx.func(getWebsocketArgsEx.ws, getWebsocketArgsEx.req);
  9261. }
  9262. }
  9263. xfunc.ws = ws;
  9264. xfunc.req = req;
  9265. xfunc.func = func;
  9266. ws.on('message', xfunc);
  9267. }
  9268. }
  9269. // Set a random value to this session. Only works if the session has a userid.
  9270. // This random value along with the userid is used to destroy the session when logging out.
  9271. function setSessionRandom(req) {
  9272. if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return;
  9273. var x = obj.crypto.randomBytes(6).toString('base64');
  9274. while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); }
  9275. req.session.x = x;
  9276. }
  9277. // Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway.
  9278. function clearDestroyedSessions() {
  9279. var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000);
  9280. for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } }
  9281. for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; }
  9282. }
  9283. // Check and/or convert the agent color value into a correct string or return empty string.
  9284. function checkAgentColorString(header, value) {
  9285. if ((typeof header !== 'string') || (typeof value !== 'string')) return '';
  9286. if (value.startsWith('#') && (value.length == 7)) {
  9287. // Convert color in hex format
  9288. value = parseInt(value.substring(1, 3), 16) + ',' + parseInt(value.substring(3, 5), 16) + ',' + parseInt(value.substring(5, 7), 16);
  9289. } else {
  9290. // Check color in decimal format
  9291. const valueSplit = value.split(',');
  9292. if (valueSplit.length != 3) return '';
  9293. const r = parseInt(valueSplit[0]), g = parseInt(valueSplit[1]), b = parseInt(valueSplit[2]);
  9294. if (isNaN(r) || (r < 0) || (r > 255) || isNaN(g) || (g < 0) || (g > 255) || isNaN(b) || (b < 0) || (b > 255)) return '';
  9295. value = r + ',' + g + ',' + b;
  9296. }
  9297. return header + value + '\r\n';
  9298. }
  9299. // Check that everything is cleaned up
  9300. function checkWebRelaySessionsTimeout() {
  9301. for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
  9302. }
  9303. // Return true if this is a private IP address
  9304. function isPrivateAddress(ip_addr) {
  9305. // If this is a loopback address, return true
  9306. if ((ip_addr == '127.0.0.1') || (ip_addr == '::1')) return true;
  9307. // Check IPv4 private addresses
  9308. const ipcheck = require('ipcheck');
  9309. 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']
  9310. for (var i in IPv4PrivateRanges) { if (ipcheck.match(ip_addr, IPv4PrivateRanges[i])) return true; }
  9311. // Check IPv6 private addresses
  9312. return /^::$/.test(ip_addr) ||
  9313. /^::1$/.test(ip_addr) ||
  9314. /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  9315. /^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  9316. /^64:ff9b::([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
  9317. /^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) ||
  9318. /^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) ||
  9319. /^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) ||
  9320. /^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) ||
  9321. /^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) ||
  9322. /^f[c-d]([0-9a-fA-F]{2,2}):/i.test(ip_addr) ||
  9323. /^fe[8-9a-bA-B][0-9a-fA-F]:/i.test(ip_addr) ||
  9324. /^ff([0-9a-fA-F]{2,2}):/i.test(ip_addr)
  9325. }
  9326. // Check that a cookie IP is within the correct range depending on the active policy
  9327. function checkCookieIp(cookieip, ip) {
  9328. if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking
  9329. if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers.
  9330. if (require('ipcheck').match(cookieip, ip + '/24')) return true; // 'lax' - IP address need to be in the some range
  9331. 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.
  9332. }
  9333. // Takes a formating string like "this {{{a}}} is an {{{b}}} example" and fills the a and b with input o.a and o.b
  9334. function assembleStringFromObject(format, o) {
  9335. var r = '', i = format.indexOf('{{{');
  9336. if (i > 0) { r = format.substring(0, i); format = format.substring(i); }
  9337. const cmd = format.split('{{{');
  9338. 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); }
  9339. return r;
  9340. }
  9341. // Sync an account with an external user group.
  9342. // Return true if the user was changed
  9343. function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) {
  9344. var userChanged = false;
  9345. if (user.links == null) { user.links = {}; }
  9346. // Create a user of memberships for this user that type
  9347. var existingUserMemberships = {};
  9348. for (var i in user.links) {
  9349. if (i.startsWith('ugrp/') && (obj.userGroups[i] != null) && (obj.userGroups[i].membershipType == userMembershipType)) { existingUserMemberships[i] = obj.userGroups[i]; }
  9350. }
  9351. // Go thru the list user memberships and create and add to any user groups as needed
  9352. for (var i in userMemberships) {
  9353. const membership = userMemberships[i];
  9354. var ugrpid = 'ugrp/' + domain.id + '/' + obj.crypto.createHash('sha384').update(membership).digest('base64').replace(/\+/g, '@').replace(/\//g, '$');
  9355. var ugrp = obj.userGroups[ugrpid];
  9356. if (ugrp == null) {
  9357. // This user group does not exist, create it
  9358. ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} };
  9359. // Save the new group
  9360. db.Set(ugrp);
  9361. if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; }
  9362. // Event the user group creation
  9363. 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 };
  9364. parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
  9365. // Log in the auth log
  9366. parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
  9367. }
  9368. if (existingUserMemberships[ugrpid] == null) {
  9369. // This user is not part of the user group, add it.
  9370. if (user.links == null) { user.links = {}; }
  9371. user.links[ugrp._id] = { rights: 1 };
  9372. userChanged = true;
  9373. db.SetUser(user);
  9374. parent.DispatchEvent([user._id], obj, 'resubscribe');
  9375. // Notify user change
  9376. var targets = ['*', 'server-users', user._id];
  9377. 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 };
  9378. 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.
  9379. parent.DispatchEvent(targets, obj, event);
  9380. // Add a user to the user group
  9381. ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
  9382. db.Set(ugrp);
  9383. // Notify user group change
  9384. 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 };
  9385. 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.
  9386. parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
  9387. // Log in the auth log
  9388. parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
  9389. } else {
  9390. // User is already part of this user group
  9391. delete existingUserMemberships[ugrpid];
  9392. }
  9393. }
  9394. // Remove the user from any memberships they don't belong to anymore
  9395. for (var ugrpid in existingUserMemberships) {
  9396. var ugrp = obj.userGroups[ugrpid];
  9397. parent.authLog('https', userMembershipType.toUpperCase() + ': Removing ' + user.name + ' from user group ' + ugrp.name + '.');
  9398. if ((user.links != null) && (user.links[ugrpid] != null)) {
  9399. delete user.links[ugrpid];
  9400. // Notify user change
  9401. var targets = ['*', 'server-users', user._id, user._id];
  9402. 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 };
  9403. 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.
  9404. parent.DispatchEvent(targets, obj, event);
  9405. db.SetUser(user);
  9406. parent.DispatchEvent([user._id], obj, 'resubscribe');
  9407. }
  9408. if (ugrp != null) {
  9409. // Remove the user from the group
  9410. if ((ugrp.links != null) && (ugrp.links[user._id] != null)) {
  9411. delete ugrp.links[user._id];
  9412. db.Set(ugrp);
  9413. // Notify user group change
  9414. 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 };
  9415. 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.
  9416. parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
  9417. }
  9418. }
  9419. }
  9420. return userChanged;
  9421. }
  9422. return obj;
  9423. };