db.js 272 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947
  1. /**
  2. * @description MeshCentral database module
  3. * @author Ylian Saint-Hilaire
  4. * @copyright Intel Corporation 2018-2022
  5. * @license Apache-2.0
  6. * @version v0.0.2
  7. */
  8. /*xjslint node: true */
  9. /*xjslint plusplus: true */
  10. /*xjslint maxlen: 256 */
  11. /*jshint node: true */
  12. /*jshint strict: false */
  13. /*jshint esversion: 6 */
  14. "use strict";
  15. //
  16. // Construct Meshcentral database object
  17. //
  18. // The default database is NeDB
  19. // https://github.com/louischatriot/nedb
  20. //
  21. // Alternativety, MongoDB can be used
  22. // https://www.mongodb.com/
  23. // Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
  24. // The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
  25. //
  26. module.exports.CreateDB = function (parent, func) {
  27. var obj = {};
  28. var Datastore = null;
  29. var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
  30. var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
  31. var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire power events after 30 days (2592000). (Seconds * Minutes * Hours * Days)
  32. const common = require('./common.js');
  33. obj.identifier = null;
  34. obj.dbKey = null;
  35. obj.dbRecordsEncryptKey = null;
  36. obj.dbRecordsDecryptKey = null;
  37. obj.changeStream = false;
  38. obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
  39. obj.dbCounters = {
  40. fileSet: 0,
  41. fileRemove: 0,
  42. powerSet: 0,
  43. eventsSet: 0
  44. }
  45. // MongoDB bulk operations state
  46. if (parent.config.settings.mongodbbulkoperations) {
  47. // Added counters
  48. obj.dbCounters.fileSetPending = 0;
  49. obj.dbCounters.fileSetBulk = 0;
  50. obj.dbCounters.fileRemovePending = 0;
  51. obj.dbCounters.fileRemoveBulk = 0;
  52. obj.dbCounters.powerSetPending = 0;
  53. obj.dbCounters.powerSetBulk = 0;
  54. obj.dbCounters.eventsSetPending = 0;
  55. obj.dbCounters.eventsSetBulk = 0;
  56. /// Added bulk accumulators
  57. obj.filePendingGet = null;
  58. obj.filePendingGets = null;
  59. obj.filePendingRemove = null;
  60. obj.filePendingRemoves = null;
  61. obj.filePendingSet = false;
  62. obj.filePendingSets = null;
  63. obj.filePendingCb = null;
  64. obj.filePendingCbs = null;
  65. obj.powerFilePendingSet = false;
  66. obj.powerFilePendingSets = null;
  67. obj.powerFilePendingCb = null;
  68. obj.powerFilePendingCbs = null;
  69. obj.eventsFilePendingSet = false;
  70. obj.eventsFilePendingSets = null;
  71. obj.eventsFilePendingCb = null;
  72. obj.eventsFilePendingCbs = null;
  73. }
  74. obj.SetupDatabase = function (func) {
  75. // Check if the database unique identifier is present
  76. // This is used to check that in server peering mode, everyone is using the same database.
  77. obj.Get('DatabaseIdentifier', function (err, docs) {
  78. if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
  79. if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
  80. obj.identifier = docs[0].value;
  81. } else {
  82. obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
  83. obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
  84. }
  85. });
  86. // Load database schema version and check if we need to update
  87. obj.Get('SchemaVersion', function (err, docs) {
  88. if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
  89. var ver = 0;
  90. if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
  91. if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
  92. // TODO: Any schema upgrades here...
  93. obj.Set({ _id: 'SchemaVersion', value: 2 });
  94. func(ver);
  95. });
  96. };
  97. // Perform database maintenance
  98. obj.maintenance = function () {
  99. if (obj.databaseType == 1) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
  100. obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  101. obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  102. obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  103. } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { // MariaDB or MySQL
  104. sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
  105. sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
  106. sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
  107. sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
  108. } else if (obj.databaseType == 7) { // AceBase
  109. //console.log('Performing AceBase maintenance');
  110. obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
  111. obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
  112. obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
  113. //console.log('AceBase maintenance done');
  114. });
  115. });
  116. });
  117. } else if (obj.databaseType == 8) { // SQLite3
  118. // TODO
  119. }
  120. obj.removeInactiveDevices();
  121. }
  122. // Remove inactive devices
  123. obj.removeInactiveDevices = function (showall, cb) {
  124. // Get a list of domains and what their inactive device removal setting is
  125. var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
  126. for (var i in parent.config.domains) {
  127. if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
  128. var v = parent.config.domains[i].autoremoveinactivedevices;
  129. if ((v >= 1) && (v <= 2000)) {
  130. if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
  131. removeInactiveDevicesPerDomain[i] = v;
  132. minRemoveInactiveDevicesPerDomain[i] = v;
  133. }
  134. }
  135. }
  136. // Check if any device groups have a inactive device removal setting
  137. for (var i in parent.webserver.meshes) {
  138. if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
  139. var v = parent.webserver.meshes[i].expireDevs;
  140. if ((v >= 1) && (v <= 2000)) {
  141. if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
  142. if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
  143. minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
  144. }
  145. } else {
  146. delete parent.webserver.meshes[i].expireDevs;
  147. }
  148. }
  149. }
  150. // If there are no such settings for any domain, we can exit now.
  151. if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
  152. const now = Date.now();
  153. // For each domain with a inactive device removal setting, get a list of last device connections
  154. for (var domainid in minRemoveInactiveDevicesPerDomain) {
  155. obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
  156. if ((err != null) || (docs == null)) return;
  157. for (var j in docs) {
  158. const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
  159. var expireDays = -1;
  160. if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
  161. const mesh = parent.webserver.meshes[docs[j].meshid];
  162. if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
  163. var remove = false;
  164. if (expireDays > 0) {
  165. if (expireDays < days) { remove = true; }
  166. if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
  167. if (remove) {
  168. // Check if this device is connected right now
  169. const nodeid = docs[j]._id.substring(2);
  170. const conn = parent.GetConnectivityState(nodeid);
  171. if (conn == null) {
  172. // Remove the device
  173. obj.Get(nodeid, function (err, docs) {
  174. if (err != null) return;
  175. if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
  176. const node = docs[0];
  177. // Delete this node including network interface information, events and timeline
  178. obj.Remove(node._id); // Remove node with that id
  179. obj.Remove('if' + node._id); // Remove interface information
  180. obj.Remove('nt' + node._id); // Remove notes
  181. obj.Remove('lc' + node._id); // Remove last connect time
  182. obj.Remove('si' + node._id); // Remove system information
  183. obj.Remove('al' + node._id); // Remove error log last time
  184. if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
  185. obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
  186. obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
  187. if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
  188. obj.Get('ra' + node._id, function (err, nodes) {
  189. if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
  190. obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
  191. });
  192. // Remove any user node links
  193. if (node.links != null) {
  194. for (var i in node.links) {
  195. if (i.startsWith('user/')) {
  196. var cuser = parent.webserver.users[i];
  197. if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
  198. // Remove the user link & save the user
  199. delete cuser.links[node._id];
  200. if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
  201. obj.SetUser(cuser);
  202. // Notify user change
  203. var targets = ['*', 'server-users', cuser._id];
  204. var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) };
  205. if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
  206. parent.DispatchEvent(targets, obj, event);
  207. }
  208. }
  209. }
  210. }
  211. // Event node deletion
  212. var meshname = '(unknown)';
  213. if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
  214. var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain };
  215. // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
  216. //if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
  217. parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
  218. });
  219. }
  220. }
  221. }
  222. }
  223. });
  224. }
  225. }
  226. // Remove all reference to a domain from the database
  227. obj.removeDomain = function (domainName, func) {
  228. var pendingCalls;
  229. // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
  230. if (obj.databaseType == 7) {
  231. // AceBase
  232. pendingCalls = 3;
  233. obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
  234. obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
  235. obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
  236. } else if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) {
  237. // MariaDB, MySQL or PostgreSQL
  238. pendingCalls = 2;
  239. sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
  240. sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
  241. } else if (obj.databaseType == 3) {
  242. // MongoDB
  243. pendingCalls = 3;
  244. obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  245. obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  246. obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  247. } else {
  248. // NeDB or MongoJS
  249. pendingCalls = 3;
  250. obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  251. obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  252. obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
  253. }
  254. }
  255. obj.cleanup = function (func) {
  256. // TODO: Remove all mesh links to invalid users
  257. // TODO: Remove all meshes that dont have any links
  258. // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
  259. if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) {
  260. // MariaDB, MySQL or PostgreSQL
  261. obj.RemoveAllOfType('event', function () { });
  262. obj.RemoveAllOfType('power', function () { });
  263. obj.RemoveAllOfType('smbios', function () { });
  264. } else if (obj.databaseType == 3) {
  265. // MongoDB
  266. obj.file.deleteMany({ type: 'event' }, { multi: true });
  267. obj.file.deleteMany({ type: 'power' }, { multi: true });
  268. obj.file.deleteMany({ type: 'smbios' }, { multi: true });
  269. } else if ((obj.databaseType == 1) || (obj.databaseType == 2)) {
  270. // NeDB or MongoJS
  271. obj.file.remove({ type: 'event' }, { multi: true });
  272. obj.file.remove({ type: 'power' }, { multi: true });
  273. obj.file.remove({ type: 'smbios' }, { multi: true });
  274. }
  275. // List of valid identifiers
  276. var validIdentifiers = {}
  277. // Load all user groups
  278. obj.GetAllType('ugrp', function (err, docs) {
  279. if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
  280. if ((err == null) && (docs.length > 0)) {
  281. for (var i in docs) {
  282. // Add this as a valid user identifier
  283. validIdentifiers[docs[i]._id] = 1;
  284. }
  285. }
  286. // Fix all of the creating & login to ticks by seconds, not milliseconds.
  287. obj.GetAllType('user', function (err, docs) {
  288. if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
  289. if ((err == null) && (docs.length > 0)) {
  290. for (var i in docs) {
  291. var fixed = false;
  292. // Add this as a valid user identifier
  293. validIdentifiers[docs[i]._id] = 1;
  294. // Fix email address capitalization
  295. if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
  296. docs[i].email = docs[i].email.toLowerCase(); fixed = true;
  297. }
  298. // Fix account creation
  299. if (docs[i].creation) {
  300. if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
  301. if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
  302. }
  303. // Fix last account login
  304. if (docs[i].login) {
  305. if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
  306. if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
  307. }
  308. // Fix last password change
  309. if (docs[i].passchange) {
  310. if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
  311. if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
  312. }
  313. // Fix subscriptions
  314. if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
  315. // Save the user if needed
  316. if (fixed) { obj.Set(docs[i]); }
  317. }
  318. // Remove all objects that have a "meshid" that no longer points to a valid mesh.
  319. // Fix any incorrectly escaped user identifiers
  320. obj.GetAllType('mesh', function (err, docs) {
  321. if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
  322. var meshlist = [];
  323. if ((err == null) && (docs.length > 0)) {
  324. for (var i in docs) {
  325. var meshChange = false;
  326. docs[i] = common.unEscapeLinksFieldName(docs[i]);
  327. meshlist.push(docs[i]._id);
  328. // Make sure all mesh types are number type, if not, fix it.
  329. if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
  330. // If the device group is deleted, remove any invite codes
  331. if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
  332. // Take a look at the links
  333. if (docs[i].links != null) {
  334. for (var j in docs[i].links) {
  335. if (validIdentifiers[j] == null) {
  336. // This identifier is not known, let see if we can fix it.
  337. var xid = j, xid2 = common.unEscapeFieldName(xid);
  338. while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
  339. if (validIdentifiers[xid2] == 1) {
  340. //console.log('Fixing id: ' + j + ' to ' + xid2);
  341. docs[i].links[xid2] = docs[i].links[j];
  342. delete docs[i].links[j];
  343. meshChange = true;
  344. } else {
  345. // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
  346. //console.log('Unknown id: ' + j);
  347. }
  348. }
  349. }
  350. }
  351. // Save the updated device group if needed
  352. if (meshChange) { obj.Set(docs[i]); }
  353. }
  354. }
  355. if (obj.databaseType == 8) {
  356. // SQLite
  357. } else if (obj.databaseType == 7) {
  358. // AceBase
  359. } else if (obj.databaseType == 6) {
  360. // Postgres
  361. sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
  362. } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
  363. // MariaDB
  364. sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
  365. } else if (obj.databaseType == 3) {
  366. // MongoDB
  367. obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
  368. } else {
  369. // NeDB or MongoJS
  370. obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
  371. }
  372. // We are done
  373. validIdentifiers = null;
  374. if (func) { func(); }
  375. });
  376. }
  377. });
  378. });
  379. };
  380. // Get encryption key
  381. obj.getEncryptDataKey = function (password, salt, iterations) {
  382. if (typeof password != 'string') return null;
  383. let key;
  384. try {
  385. key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
  386. } catch (ex) {
  387. // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
  388. key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
  389. }
  390. return key
  391. }
  392. // Encrypt data
  393. obj.encryptData = function (password, plaintext) {
  394. let encryptionVersion = 0x01;
  395. let iterations = 100000
  396. const iv = parent.crypto.randomBytes(16);
  397. var key = obj.getEncryptDataKey(password, iv, iterations);
  398. if (key == null) return null;
  399. const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
  400. var ciphertext = aes.update(plaintext);
  401. let versionbuf = Buffer.allocUnsafe(2);
  402. versionbuf.writeUInt16BE(encryptionVersion);
  403. let iterbuf = Buffer.allocUnsafe(4);
  404. iterbuf.writeUInt32BE(iterations);
  405. let encryptedBuf = aes.final();
  406. ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
  407. return ciphertext.toString('base64');
  408. }
  409. // Decrypt data
  410. obj.decryptData = function (password, ciphertext) {
  411. // Adding an encryption version lets us avoid try catching in the future
  412. let ciphertextBytes = Buffer.from(ciphertext, 'base64');
  413. let encryptionVersion = ciphertextBytes.readUInt16BE(0);
  414. try {
  415. switch (encryptionVersion) {
  416. case 0x01:
  417. let iterations = ciphertextBytes.readUInt32BE(2);
  418. let authTag = ciphertextBytes.slice(6, 22);
  419. const iv = ciphertextBytes.slice(22, 38);
  420. const data = ciphertextBytes.slice(38);
  421. let key = obj.getEncryptDataKey(password, iv, iterations);
  422. if (key == null) return null;
  423. const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
  424. aes.setAuthTag(authTag);
  425. let plaintextBytes = Buffer.from(aes.update(data));
  426. plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
  427. return plaintextBytes;
  428. default:
  429. return obj.oldDecryptData(password, ciphertextBytes);
  430. }
  431. } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
  432. }
  433. // Encrypt data
  434. // The older encryption system uses CBC without integraty checking.
  435. // This method is kept only for testing
  436. obj.oldEncryptData = function (password, plaintext) {
  437. let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
  438. if (key == null) return null;
  439. const iv = parent.crypto.randomBytes(16);
  440. const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
  441. var ciphertext = aes.update(plaintext);
  442. ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
  443. return ciphertext.toString('base64');
  444. }
  445. // Decrypt data
  446. // The older encryption system uses CBC without integraty checking.
  447. // This method is kept only to convert the old encryption to the new one.
  448. obj.oldDecryptData = function (password, ciphertextBytes) {
  449. if (typeof password != 'string') return null;
  450. try {
  451. const iv = ciphertextBytes.slice(0, 16);
  452. const data = ciphertextBytes.slice(16);
  453. let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
  454. const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
  455. let plaintextBytes = Buffer.from(aes.update(data));
  456. plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
  457. return plaintextBytes;
  458. } catch (ex) { return null; }
  459. }
  460. // Get the number of records in the database for various types, this is the slow NeDB way.
  461. // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
  462. obj.getStats = function (func) {
  463. if (obj.databaseType == 7) {
  464. // AceBase
  465. // TODO
  466. } else if (obj.databaseType == 6) {
  467. // PostgreSQL
  468. // TODO
  469. } else if (obj.databaseType == 5) {
  470. // MySQL
  471. // TODO
  472. } else if (obj.databaseType == 4) {
  473. // MariaDB
  474. // TODO
  475. } else if (obj.databaseType == 3) {
  476. // MongoDB
  477. obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
  478. var counters = {}, totalCount = 0;
  479. if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
  480. func(counters);
  481. });
  482. } else if (obj.databaseType == 2) {
  483. // MongoJS
  484. obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
  485. var counters = {}, totalCount = 0;
  486. if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
  487. func(counters);
  488. });
  489. } else if (obj.databaseType == 1) {
  490. // NeDB version
  491. obj.file.count({ type: 'node' }, function (err, nodeCount) {
  492. obj.file.count({ type: 'mesh' }, function (err, meshCount) {
  493. obj.file.count({ type: 'user' }, function (err, userCount) {
  494. obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
  495. obj.file.count({ type: 'note' }, function (err, noteCount) {
  496. obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
  497. obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
  498. obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
  499. obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
  500. obj.file.count({}, function (err, totalCount) {
  501. func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
  502. });
  503. });
  504. });
  505. });
  506. });
  507. });
  508. });
  509. });
  510. });
  511. });
  512. }
  513. }
  514. // This is used to rate limit a number of operation per day. Returns a startValue each new days, but you can substract it and save the value in the db.
  515. obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
  516. obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
  517. // Encrypt an database object
  518. obj.performRecordEncryptionRecode = function (func) {
  519. var count = 0;
  520. obj.GetAllType('user', function (err, docs) {
  521. if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
  522. if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
  523. obj.GetAllType('node', function (err, docs) {
  524. if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
  525. obj.GetAllType('mesh', function (err, docs) {
  526. if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
  527. if (obj.databaseType == 1) { // If we are using NeDB, compact the database.
  528. obj.file.persistence.compactDatafile();
  529. obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
  530. } else {
  531. func(count); // For all other databases, normal exit.
  532. }
  533. });
  534. });
  535. });
  536. }
  537. // Encrypt an database object
  538. function performTypedRecordDecrypt(data) {
  539. if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
  540. for (var i in data) {
  541. if ((data[i] == null) || (typeof data[i] != 'object')) continue;
  542. data[i] = performPartialRecordDecrypt(data[i]);
  543. if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
  544. if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
  545. if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
  546. }
  547. return data;
  548. }
  549. // Encrypt an database object
  550. function performTypedRecordEncrypt(data) {
  551. if (obj.dbRecordsEncryptKey == null) return data;
  552. if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
  553. else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
  554. var xdata = Clone(data);
  555. if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
  556. if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
  557. return xdata;
  558. }
  559. else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
  560. var xdata = Clone(data);
  561. if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
  562. if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
  563. return xdata;
  564. }
  565. return data;
  566. }
  567. // Encrypt an object and return a buffer.
  568. function performPartialRecordEncrypt(plainobj, encryptNames) {
  569. if (typeof plainobj != 'object') return plainobj;
  570. var enc = {}, enclen = 0;
  571. for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
  572. if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
  573. return plainobj;
  574. }
  575. // Encrypt an object and return a buffer.
  576. function performPartialRecordDecrypt(plainobj) {
  577. if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
  578. var enc = performRecordDecrypt(plainobj._CRYPT);
  579. if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
  580. delete plainobj._CRYPT;
  581. return plainobj;
  582. }
  583. // Encrypt an object and return a base64.
  584. function performRecordEncrypt(plainobj) {
  585. if (obj.dbRecordsEncryptKey == null) return null;
  586. const iv = parent.crypto.randomBytes(12);
  587. const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
  588. var ciphertext = aes.update(JSON.stringify(plainobj));
  589. var cipherfinal = aes.final();
  590. ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
  591. return ciphertext.toString('base64');
  592. }
  593. // Takes a base64 and return an object.
  594. function performRecordDecrypt(ciphertext) {
  595. if (obj.dbRecordsDecryptKey == null) return null;
  596. const ciphertextBytes = Buffer.from(ciphertext, 'base64');
  597. const iv = ciphertextBytes.slice(0, 12);
  598. const data = ciphertextBytes.slice(28);
  599. const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
  600. aes.setAuthTag(ciphertextBytes.slice(12, 28));
  601. var plaintextBytes, r;
  602. try {
  603. plaintextBytes = Buffer.from(aes.update(data));
  604. plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
  605. r = JSON.parse(plaintextBytes.toString());
  606. } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
  607. return r;
  608. }
  609. // Clone an object (TODO: Make this more efficient)
  610. function Clone(v) { return JSON.parse(JSON.stringify(v)); }
  611. // Read expiration time from configuration file
  612. if (typeof parent.args.dbexpire == 'object') {
  613. if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
  614. if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
  615. if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
  616. }
  617. // If a DB record encryption key is provided, perform database record encryption
  618. if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
  619. // Hash the database password into a AES256 key and setup encryption and decryption.
  620. obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
  621. }
  622. // If a DB record decryption key is provided, perform database record decryption
  623. if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
  624. // Hash the database password into a AES256 key and setup encryption and decryption.
  625. obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
  626. }
  627. function createTablesIfNotExist(dbname) {
  628. var useDatabase = 'USE ' + dbname;
  629. sqlDbQuery(useDatabase, null, function (err, docs) {
  630. if (err != null) {
  631. console.log("Unable to connect to database: " + err);
  632. process.exit();
  633. }
  634. if (err == null) {
  635. parent.debug('db', 'Checking tables...');
  636. sqlDbBatchExec([
  637. 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
  638. 'CREATE TABLE IF NOT EXISTS events (id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
  639. 'CREATE TABLE IF NOT EXISTS eventids (fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
  640. 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
  641. 'CREATE TABLE IF NOT EXISTS power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
  642. 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
  643. 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
  644. ], function (err) {
  645. parent.debug('db', 'Checking indexes...');
  646. sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
  647. sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
  648. sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
  649. sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
  650. sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
  651. sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
  652. sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
  653. sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
  654. sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
  655. sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
  656. sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
  657. sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
  658. setupFunctions(func);
  659. });
  660. }
  661. });
  662. }
  663. if (parent.args.sqlite3) {
  664. // SQLite3 database setup
  665. obj.databaseType = 8;
  666. const sqlite3 = require('sqlite3');
  667. obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
  668. if (err && (err.code == 'SQLITE_CANTOPEN')) {
  669. // Database needs to be created
  670. obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), function (err) {
  671. if (err) { console.log("SQLite Error: " + err); exit(1); return; }
  672. obj.file.exec(`
  673. CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
  674. CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
  675. CREATE TABLE eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT);
  676. CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
  677. CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
  678. CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
  679. CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
  680. CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
  681. CREATE INDEX ndxextra ON main (extra);
  682. CREATE INDEX ndxextraex ON main (extraex);
  683. CREATE INDEX ndxeventstime ON events(time);
  684. CREATE INDEX ndxeventsusername ON events(domain, userid, time);
  685. CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
  686. CREATE INDEX ndxeventids ON eventids(target);
  687. CREATE INDEX ndxserverstattime ON serverstats (time);
  688. CREATE INDEX ndxserverstatexpire ON serverstats (expire);
  689. CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
  690. CREATE INDEX ndxsmbiostime ON smbios (time);
  691. CREATE INDEX ndxsmbiosexpire ON smbios (expire);
  692. `, function (err) {
  693. // Completed setup of SQLite3
  694. setupFunctions(func);
  695. }
  696. );
  697. });
  698. return;
  699. } else if (err) { console.log("SQLite Error: " + err); exit(1); return; }
  700. // Completed setup of SQLite3
  701. setupFunctions(func);
  702. });
  703. } else if (parent.args.acebase) {
  704. // AceBase database setup
  705. obj.databaseType = 7;
  706. const { AceBase } = require('acebase');
  707. // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
  708. obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
  709. // Get all the databases ready
  710. obj.file.ready(function () {
  711. // Create AceBase indexes
  712. obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
  713. obj.file.indexes.create('meshcentral', 'email');
  714. obj.file.indexes.create('meshcentral', 'meshid');
  715. obj.file.indexes.create('meshcentral', 'intelamt.uuid');
  716. obj.file.indexes.create('events', 'userid', { include: ['action'] });
  717. obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
  718. obj.file.indexes.create('events', 'ids', { include: ['time'] });
  719. obj.file.indexes.create('events', 'time');
  720. obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
  721. obj.file.indexes.create('power', 'time');
  722. obj.file.indexes.create('stats', 'time');
  723. obj.file.indexes.create('stats', 'expire');
  724. // Completed setup of AceBase
  725. setupFunctions(func);
  726. });
  727. } else if (parent.args.mariadb || parent.args.mysql) {
  728. var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
  729. if (typeof connectinArgs == 'string') {
  730. const parts = connectinArgs.split(/[:@/]+/);
  731. var connectionObject = {
  732. "user": parts[1],
  733. "password": parts[2],
  734. "host": parts[3],
  735. "port": parts[4],
  736. "database": parts[5]
  737. };
  738. var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
  739. } else {
  740. var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
  741. // Including the db name in the connection obj will cause a connection faliure if it does not exist
  742. var connectionObject = Clone(connectinArgs);
  743. delete connectionObject.database;
  744. try {
  745. if (connectinArgs.ssl) {
  746. if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
  747. if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
  748. if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
  749. if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
  750. }
  751. } catch (ex) {
  752. console.log('Error loading SQL Connector certificate: ' + ex);
  753. process.exit();
  754. }
  755. }
  756. if (parent.args.mariadb) {
  757. // Use MariaDB
  758. obj.databaseType = 4;
  759. var tempDatastore = require('mariadb').createPool(connectionObject);
  760. tempDatastore.getConnection().then(function (conn) {
  761. conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
  762. conn.release();
  763. }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
  764. }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
  765. setTimeout(function () { tempDatastore.end(); }, 2000);
  766. connectionObject.database = dbname;
  767. Datastore = require('mariadb').createPool(connectionObject);
  768. createTablesIfNotExist(dbname);
  769. } else if (parent.args.mysql) {
  770. // Use MySQL
  771. obj.databaseType = 5;
  772. var tempDatastore = require('mysql2').createPool(connectionObject);
  773. tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
  774. if (error != null) {
  775. console.log('Auto-create database failed: ' + error);
  776. }
  777. connectionObject.database = dbname;
  778. Datastore = require('mysql2').createPool(connectionObject);
  779. createTablesIfNotExist(dbname);
  780. });
  781. setTimeout(function () { tempDatastore.end(); }, 2000);
  782. }
  783. } else if (parent.args.postgres) {
  784. // Postgres SQL
  785. var connectinArgs = parent.args.postgres;
  786. var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
  787. delete connectinArgs.database;
  788. obj.databaseType = 6;
  789. const { Pool, Client } = require('pg');
  790. connectinArgs.database = dbname;
  791. Datastore = new Client(connectinArgs);
  792. Datastore.connect();
  793. sqlDbQuery('SELECT 1 FROM pg_database WHERE datname = $1', [dbname], function (dberr, dbdocs) { // check database exists first before creating
  794. if (dberr == null) { // database exists now check tables exists
  795. sqlDbQuery('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, docs) {
  796. if (err == null) { setupFunctions(func); } else { postgreSqlCreateTables(func); } // If not present, create the tables and indexes
  797. });
  798. } else { // If not present, create the tables and indexes
  799. const pgtools = require('pgtools');
  800. pgtools.createdb(connectinArgs, dbname, function (err, res) {
  801. if (err == null) {
  802. // Create the tables and indexes
  803. postgreSqlCreateTables(func);
  804. } else {
  805. // Database already existed, perform a test query to see if the main table is present
  806. sqlDbQuery('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, docs) {
  807. if (err == null) { setupFunctions(func); } else { postgreSqlCreateTables(func); } // If not present, create the tables and indexes
  808. });
  809. }
  810. });
  811. }
  812. });
  813. } else if (parent.args.mongodb) {
  814. // Use MongoDB
  815. obj.databaseType = 3;
  816. // If running an older NodeJS version, TextEncoder/TextDecoder is required
  817. if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
  818. if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
  819. require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
  820. if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
  821. Datastore = client;
  822. parent.debug('db', 'Connected to MongoDB database...');
  823. // Get the database name and setup the database client
  824. var dbname = 'meshcentral';
  825. if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
  826. const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
  827. const db = client.db(dbname);
  828. // Check the database version
  829. db.admin().serverInfo(function (err, info) {
  830. if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
  831. console.log('WARNING: Unable to check MongoDB version.');
  832. } else {
  833. if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
  834. // We are running with mongoDB older than 3.6, this is not good.
  835. parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.");
  836. }
  837. }
  838. });
  839. // Setup MongoDB main collection and indexes
  840. obj.file = db.collection(dbcollectionname);
  841. obj.file.indexes(function (err, indexes) {
  842. // Check if we need to reset indexes
  843. var indexesByName = {}, indexCount = 0;
  844. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  845. if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
  846. console.log('Resetting main indexes...');
  847. obj.file.dropIndexes(function (err) {
  848. obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
  849. obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
  850. obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
  851. obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
  852. });
  853. }
  854. });
  855. // Setup the changeStream on the MongoDB main collection if possible
  856. if (parent.args.mongodbchangestream == true) {
  857. obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
  858. if (typeof obj.file.watch != 'function') {
  859. console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
  860. } else {
  861. obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
  862. obj.fileChangeStream.on('change', function (change) {
  863. obj.dbCounters.changeStream.change++;
  864. if ((change.operationType == 'update') || (change.operationType == 'replace')) {
  865. obj.dbCounters.changeStream.update++;
  866. switch (change.fullDocument.type) {
  867. case 'node': { dbNodeChange(change, false); break; } // A node has changed
  868. case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
  869. case 'user': { dbUserChange(change, false); break; } // A user account has changed
  870. case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
  871. }
  872. } else if (change.operationType == 'insert') {
  873. obj.dbCounters.changeStream.insert++;
  874. switch (change.fullDocument.type) {
  875. case 'node': { dbNodeChange(change, true); break; } // A node has added
  876. case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
  877. case 'user': { dbUserChange(change, true); break; } // A user account has created
  878. case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
  879. }
  880. } else if (change.operationType == 'delete') {
  881. obj.dbCounters.changeStream.delete++;
  882. if ((change.documentKey == null) || (change.documentKey._id == null)) return;
  883. var splitId = change.documentKey._id.split('/');
  884. switch (splitId[0]) {
  885. case 'node': {
  886. //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
  887. //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
  888. break;
  889. }
  890. case 'mesh': {
  891. parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
  892. break;
  893. }
  894. case 'user': {
  895. //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
  896. //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
  897. break;
  898. }
  899. case 'ugrp': {
  900. parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
  901. break;
  902. }
  903. }
  904. }
  905. });
  906. obj.changeStream = true;
  907. }
  908. }
  909. // Setup MongoDB events collection and indexes
  910. obj.eventsfile = db.collection('events'); // Collection containing all events
  911. obj.eventsfile.indexes(function (err, indexes) {
  912. // Check if we need to reset indexes
  913. var indexesByName = {}, indexCount = 0;
  914. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  915. if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
  916. // Reset all indexes
  917. console.log("Resetting events indexes...");
  918. obj.eventsfile.dropIndexes(function (err) {
  919. obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
  920. obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
  921. obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
  922. obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
  923. });
  924. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
  925. // Reset the timeout index
  926. console.log("Resetting events expire index...");
  927. obj.eventsfile.dropIndex('ExpireTime1', function (err) {
  928. obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
  929. });
  930. }
  931. });
  932. // Setup MongoDB power events collection and indexes
  933. obj.powerfile = db.collection('power'); // Collection containing all power events
  934. obj.powerfile.indexes(function (err, indexes) {
  935. // Check if we need to reset indexes
  936. var indexesByName = {}, indexCount = 0;
  937. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  938. if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
  939. // Reset all indexes
  940. console.log("Resetting power events indexes...");
  941. obj.powerfile.dropIndexes(function (err) {
  942. // Create all indexes
  943. obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
  944. obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
  945. });
  946. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
  947. // Reset the timeout index
  948. console.log("Resetting power events expire index...");
  949. obj.powerfile.dropIndex('ExpireTime1', function (err) {
  950. // Reset the expire power events index
  951. obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
  952. });
  953. }
  954. });
  955. // Setup MongoDB smbios collection, no indexes needed
  956. obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
  957. // Setup MongoDB server stats collection
  958. obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
  959. obj.serverstatsfile.indexes(function (err, indexes) {
  960. // Check if we need to reset indexes
  961. var indexesByName = {}, indexCount = 0;
  962. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  963. if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
  964. // Reset all indexes
  965. console.log("Resetting server stats indexes...");
  966. obj.serverstatsfile.dropIndexes(function (err) {
  967. // Create all indexes
  968. obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
  969. obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
  970. });
  971. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
  972. // Reset the timeout index
  973. console.log("Resetting server stats expire index...");
  974. obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
  975. // Reset the expire server stats index
  976. obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
  977. });
  978. }
  979. });
  980. // Setup plugin info collection
  981. if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
  982. setupFunctions(func); // Completed setup of MongoDB
  983. });
  984. } else if (parent.args.xmongodb) {
  985. // Use MongoJS, this is the old system.
  986. obj.databaseType = 2;
  987. Datastore = require('mongojs');
  988. var db = Datastore(parent.args.xmongodb);
  989. var dbcollection = 'meshcentral';
  990. if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
  991. // Setup MongoDB main collection and indexes
  992. obj.file = db.collection(dbcollection);
  993. obj.file.getIndexes(function (err, indexes) {
  994. // Check if we need to reset indexes
  995. var indexesByName = {}, indexCount = 0;
  996. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  997. if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
  998. console.log("Resetting main indexes...");
  999. obj.file.dropIndexes(function (err) {
  1000. obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
  1001. obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
  1002. obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
  1003. obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
  1004. });
  1005. }
  1006. });
  1007. // Setup MongoDB events collection and indexes
  1008. obj.eventsfile = db.collection('events'); // Collection containing all events
  1009. obj.eventsfile.getIndexes(function (err, indexes) {
  1010. // Check if we need to reset indexes
  1011. var indexesByName = {}, indexCount = 0;
  1012. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  1013. if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
  1014. // Reset all indexes
  1015. console.log("Resetting events indexes...");
  1016. obj.eventsfile.dropIndexes(function (err) {
  1017. obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
  1018. obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
  1019. obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
  1020. obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
  1021. });
  1022. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
  1023. // Reset the timeout index
  1024. console.log("Resetting events expire index...");
  1025. obj.eventsfile.dropIndex('ExpireTime1', function (err) {
  1026. obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
  1027. });
  1028. }
  1029. });
  1030. // Setup MongoDB power events collection and indexes
  1031. obj.powerfile = db.collection('power'); // Collection containing all power events
  1032. obj.powerfile.getIndexes(function (err, indexes) {
  1033. // Check if we need to reset indexes
  1034. var indexesByName = {}, indexCount = 0;
  1035. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  1036. if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
  1037. // Reset all indexes
  1038. console.log("Resetting power events indexes...");
  1039. obj.powerfile.dropIndexes(function (err) {
  1040. // Create all indexes
  1041. obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
  1042. obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
  1043. });
  1044. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
  1045. // Reset the timeout index
  1046. console.log("Resetting power events expire index...");
  1047. obj.powerfile.dropIndex('ExpireTime1', function (err) {
  1048. // Reset the expire power events index
  1049. obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
  1050. });
  1051. }
  1052. });
  1053. // Setup MongoDB smbios collection, no indexes needed
  1054. obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
  1055. // Setup MongoDB server stats collection
  1056. obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
  1057. obj.serverstatsfile.getIndexes(function (err, indexes) {
  1058. // Check if we need to reset indexes
  1059. var indexesByName = {}, indexCount = 0;
  1060. for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
  1061. if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
  1062. // Reset all indexes
  1063. console.log("Resetting server stats indexes...");
  1064. obj.serverstatsfile.dropIndexes(function (err) {
  1065. // Create all indexes
  1066. obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
  1067. obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
  1068. });
  1069. } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
  1070. // Reset the timeout index
  1071. console.log("Resetting server stats expire index...");
  1072. obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
  1073. // Reset the expire server stats index
  1074. obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
  1075. });
  1076. }
  1077. });
  1078. // Setup plugin info collection
  1079. if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
  1080. setupFunctions(func); // Completed setup of MongoJS
  1081. } else {
  1082. // Use NeDB (The default)
  1083. obj.databaseType = 1;
  1084. try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
  1085. if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
  1086. var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
  1087. // If a DB encryption key is provided, perform database encryption
  1088. if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
  1089. // Hash the database password into a AES256 key and setup encryption and decryption.
  1090. obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
  1091. datastoreOptions.afterSerialization = function (plaintext) {
  1092. const iv = parent.crypto.randomBytes(16);
  1093. const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
  1094. var ciphertext = aes.update(plaintext);
  1095. ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
  1096. return ciphertext.toString('base64');
  1097. }
  1098. datastoreOptions.beforeDeserialization = function (ciphertext) {
  1099. const ciphertextBytes = Buffer.from(ciphertext, 'base64');
  1100. const iv = ciphertextBytes.slice(0, 16);
  1101. const data = ciphertextBytes.slice(16);
  1102. const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
  1103. var plaintextBytes = Buffer.from(aes.update(data));
  1104. plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
  1105. return plaintextBytes.toString();
  1106. }
  1107. }
  1108. // Start NeDB main collection and setup indexes
  1109. obj.file = new Datastore(datastoreOptions);
  1110. obj.file.persistence.setAutocompactionInterval(86400000); // Compact once a day
  1111. obj.file.ensureIndex({ fieldName: 'type' });
  1112. obj.file.ensureIndex({ fieldName: 'domain' });
  1113. obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
  1114. obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
  1115. obj.file.ensureIndex({ fieldName: 'email', sparse: true });
  1116. // Setup the events collection and setup indexes
  1117. obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
  1118. obj.eventsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
  1119. obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
  1120. obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
  1121. obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
  1122. obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  1123. // Setup the power collection and setup indexes
  1124. obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
  1125. obj.powerfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
  1126. obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
  1127. obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
  1128. obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  1129. // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
  1130. //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
  1131. parent.fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
  1132. // Setup the server stats collection and setup indexes
  1133. obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
  1134. obj.serverstatsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
  1135. obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
  1136. obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
  1137. obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
  1138. // Setup plugin info collection
  1139. if (obj.pluginsActive) {
  1140. obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
  1141. obj.pluginsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
  1142. }
  1143. setupFunctions(func); // Completed setup of NeDB
  1144. }
  1145. // Create the PostgreSQL tables
  1146. function postgreSqlCreateTables(func) {
  1147. // Database was created, create the tables
  1148. parent.debug('db', 'Creating tables...');
  1149. sqlDbBatchExec([
  1150. 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON)',
  1151. 'CREATE TABLE IF NOT EXISTS events(id SERIAL PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON)',
  1152. 'CREATE TABLE IF NOT EXISTS eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
  1153. 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
  1154. 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
  1155. 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
  1156. 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)'
  1157. ], function (results) {
  1158. parent.debug('db', 'Creating indexes...');
  1159. sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
  1160. sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
  1161. sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
  1162. sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
  1163. sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
  1164. sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
  1165. sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
  1166. sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
  1167. sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
  1168. sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
  1169. sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
  1170. sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
  1171. setupFunctions(func);
  1172. });
  1173. }
  1174. // Check the object names for a "."
  1175. function checkObjectNames(r, tag) {
  1176. if (typeof r != 'object') return;
  1177. for (var i in r) {
  1178. if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
  1179. checkObjectNames(r[i], tag);
  1180. }
  1181. }
  1182. // Query the database
  1183. function sqlDbQuery(query, args, func, debug) {
  1184. if (obj.databaseType == 8) { // SQLite
  1185. if (args == null) { args = []; }
  1186. obj.file.all(query, args, function (err, docs) {
  1187. if (err != null) { console.log(query, args, err, docs); }
  1188. if (docs != null) {
  1189. for (var i in docs) {
  1190. if (typeof docs[i].doc == 'string') {
  1191. try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
  1192. console.log(query, args, docs[i]);
  1193. }
  1194. }
  1195. }
  1196. }
  1197. if (func) { func(err, docs); }
  1198. });
  1199. } else if (obj.databaseType == 4) { // MariaDB
  1200. Datastore.getConnection()
  1201. .then(function (conn) {
  1202. conn.query(query, args)
  1203. .then(function (rows) {
  1204. conn.release();
  1205. var docs = [];
  1206. for (var i in rows) {
  1207. if (rows[i].doc) {
  1208. docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
  1209. } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
  1210. // This is a SELECT COUNT() operation
  1211. docs = parseInt(rows[i]['COUNT(doc)']);
  1212. }
  1213. }
  1214. if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
  1215. })
  1216. .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
  1217. }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
  1218. } else if (obj.databaseType == 5) { // MySQL
  1219. Datastore.query(query, args, function (error, results, fields) {
  1220. if (error != null) {
  1221. if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
  1222. } else {
  1223. var docs = [];
  1224. for (var i in results) {
  1225. if (results[i].doc) {
  1226. if (typeof results[i].doc == 'string') {
  1227. docs.push(JSON.parse(results[i].doc));
  1228. } else {
  1229. docs.push(results[i].doc);
  1230. }
  1231. } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
  1232. // This is a SELECT COUNT() operation
  1233. docs = results[i]['COUNT(doc)'];
  1234. }
  1235. }
  1236. if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
  1237. }
  1238. });
  1239. } else if (obj.databaseType == 6) { // Postgres SQL
  1240. Datastore.query(query, args, function (error, results) {
  1241. if (error != null) {
  1242. if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
  1243. } else {
  1244. var docs = [];
  1245. if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
  1246. else if (results.command == 'SELECT') {
  1247. for (var i in results.rows) {
  1248. if (results.rows[i].doc) {
  1249. if (typeof results.rows[i].doc == 'string') {
  1250. docs.push(JSON.parse(results.rows[i].doc));
  1251. } else {
  1252. docs.push(results.rows[i].doc);
  1253. }
  1254. } else if (results.rows[i].count && (results.rows.length == 1)) {
  1255. // This is a SELECT COUNT() operation
  1256. docs = parseInt(results.rows[i].count);
  1257. }
  1258. }
  1259. }
  1260. if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
  1261. }
  1262. });
  1263. }
  1264. }
  1265. // Exec on the database
  1266. function sqlDbExec(query, args, func) {
  1267. if (obj.databaseType == 4) { // MariaDB
  1268. Datastore.getConnection()
  1269. .then(function (conn) {
  1270. conn.query(query, args)
  1271. .then(function (rows) {
  1272. conn.release();
  1273. if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
  1274. })
  1275. .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
  1276. }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
  1277. } else if ((obj.databaseType == 5) || (obj.databaseType == 6)) { // MySQL or Postgres SQL
  1278. Datastore.query(query, args, function (error, results, fields) {
  1279. if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
  1280. });
  1281. }
  1282. }
  1283. // Execute a batch of commands on the database
  1284. function sqlDbBatchExec(queries, func) {
  1285. if (obj.databaseType == 4) { // MariaDB
  1286. Datastore.getConnection()
  1287. .then(function (conn) {
  1288. var Promises = [];
  1289. for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
  1290. Promise.all(Promises)
  1291. .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
  1292. .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
  1293. })
  1294. .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
  1295. } else if (obj.databaseType == 5) { // MySQL
  1296. Datastore.getConnection(function(err, connection) {
  1297. if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
  1298. var Promises = [];
  1299. for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(connection.promise().query(queries[i])); } else { Promises.push(connection.promise().query(queries[i][0], queries[i][1])); } }
  1300. Promise.all(Promises)
  1301. .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
  1302. .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
  1303. });
  1304. } else if (obj.databaseType == 6) { // Postgres
  1305. var Promises = [];
  1306. for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
  1307. Promise.all(Promises)
  1308. .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
  1309. .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
  1310. }
  1311. }
  1312. function setupFunctions(func) {
  1313. if (obj.databaseType == 8) {
  1314. // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
  1315. obj.Set = function (value, func) {
  1316. obj.dbCounters.fileSet++;
  1317. var extra = null, extraex = null;
  1318. value = common.escapeLinksFieldNameEx(value);
  1319. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  1320. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  1321. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  1322. sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
  1323. }
  1324. obj.SetRaw = function (value, func) {
  1325. obj.dbCounters.fileSet++;
  1326. var extra = null, extraex = null;
  1327. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  1328. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  1329. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  1330. sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
  1331. }
  1332. obj.Get = function (_id, func) {
  1333. sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
  1334. if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1335. func(err, performTypedRecordDecrypt(docs));
  1336. });
  1337. }
  1338. obj.GetAll = function (func) {
  1339. sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
  1340. if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1341. func(err, performTypedRecordDecrypt(docs));
  1342. });
  1343. }
  1344. obj.GetHash = function (id, func) {
  1345. sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
  1346. if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1347. func(err, performTypedRecordDecrypt(docs));
  1348. });
  1349. }
  1350. obj.GetAllTypeNoTypeField = function (type, domain, func) {
  1351. sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
  1352. if ((docs != null) && (docs.length > 0)) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1353. func(err, performTypedRecordDecrypt(docs));
  1354. });
  1355. };
  1356. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  1357. if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
  1358. if (id && (id != '')) {
  1359. sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
  1360. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1361. func(err, performTypedRecordDecrypt(docs));
  1362. });
  1363. } else {
  1364. if (extrasids == null) {
  1365. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
  1366. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1367. func(err, performTypedRecordDecrypt(docs));
  1368. });
  1369. } else {
  1370. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
  1371. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1372. func(err, performTypedRecordDecrypt(docs));
  1373. });
  1374. }
  1375. }
  1376. };
  1377. obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
  1378. if (id && (id != '')) {
  1379. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) {
  1380. func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
  1381. });
  1382. } else {
  1383. if (extrasids == null) {
  1384. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
  1385. func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
  1386. });
  1387. } else {
  1388. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) {
  1389. func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
  1390. });
  1391. }
  1392. }
  1393. };
  1394. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  1395. if (id && (id != '')) {
  1396. sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [id, type, domain], function (err, docs) {
  1397. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1398. func(err, performTypedRecordDecrypt(docs));
  1399. });
  1400. } else {
  1401. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
  1402. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1403. func(err, performTypedRecordDecrypt(docs));
  1404. });
  1405. }
  1406. };
  1407. obj.GetAllType = function (type, func) {
  1408. sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
  1409. if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1410. func(err, performTypedRecordDecrypt(docs));
  1411. });
  1412. }
  1413. obj.GetAllIdsOfType = function (ids, domain, type, func) {
  1414. sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
  1415. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1416. func(err, performTypedRecordDecrypt(docs));
  1417. });
  1418. }
  1419. obj.GetUserWithEmail = function (domain, email, func) {
  1420. sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
  1421. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1422. func(err, performTypedRecordDecrypt(docs));
  1423. });
  1424. }
  1425. obj.GetUserWithVerifiedEmail = function (domain, email, func) {
  1426. sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
  1427. if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1428. func(err, performTypedRecordDecrypt(docs));
  1429. });
  1430. }
  1431. obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
  1432. obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
  1433. obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
  1434. obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
  1435. obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
  1436. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  1437. obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
  1438. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  1439. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  1440. obj.getLocalAmtNodes = function (func) {
  1441. sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
  1442. if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1443. var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r);
  1444. });
  1445. };
  1446. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
  1447. sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
  1448. if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
  1449. func(err, docs);
  1450. });
  1451. };
  1452. obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
  1453. // Database actions on the events collection
  1454. obj.GetAllEvents = function (func) {
  1455. sqlDbQuery('SELECT doc FROM events', null, func);
  1456. };
  1457. obj.StoreEvent = function (event, func) {
  1458. obj.dbCounters.eventsSet++;
  1459. sqlDbQuery('INSERT INTO events VALUES (NULL, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)], function (err, docs) {
  1460. if(func){ func(); }
  1461. if ((err == null) && (docs[0].id)) {
  1462. for (var i in event.ids) {
  1463. if (event.ids[i] != '*') {
  1464. obj.pendingTransfer++;
  1465. sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
  1466. }
  1467. }
  1468. }
  1469. });
  1470. };
  1471. obj.GetEvents = function (ids, domain, filter, func) {
  1472. var query = "SELECT doc FROM events ";
  1473. var dataarray = [domain];
  1474. if (ids.indexOf('*') >= 0) {
  1475. query = query + "WHERE (domain = $1";
  1476. if (filter != null) {
  1477. query = query + " AND action = $2";
  1478. dataarray.push(filter);
  1479. }
  1480. query = query + ") ORDER BY time DESC";
  1481. } else {
  1482. query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
  1483. if (filter != null) {
  1484. query = query + " AND action = $2";
  1485. dataarray.push(filter);
  1486. }
  1487. query = query + ") GROUP BY id ORDER BY time DESC ";
  1488. }
  1489. sqlDbQuery(query, dataarray, func);
  1490. };
  1491. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  1492. var query = "SELECT doc FROM events ";
  1493. var dataarray = [domain];
  1494. if (ids.indexOf('*') >= 0) {
  1495. query = query + "WHERE (domain = $1";
  1496. if (filter != null) {
  1497. query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
  1498. dataarray.push(filter);
  1499. } else {
  1500. query = query + ") ORDER BY time DESC LIMIT $2";
  1501. }
  1502. } else {
  1503. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
  1504. if (filter != null) {
  1505. query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
  1506. dataarray.push(filter);
  1507. } else {
  1508. query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
  1509. }
  1510. }
  1511. dataarray.push(limit);
  1512. sqlDbQuery(query, dataarray, func);
  1513. };
  1514. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  1515. var query = "SELECT doc FROM events ";
  1516. var dataarray = [domain, userid];
  1517. if (ids.indexOf('*') >= 0) {
  1518. query = query + "WHERE (domain = $1 AND userid = $2";
  1519. if (filter != null) {
  1520. query = query + " AND action = $3";
  1521. dataarray.push(filter);
  1522. }
  1523. query = query + ") ORDER BY time DESC";
  1524. } else {
  1525. query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
  1526. if (filter != null) {
  1527. query = query + " AND action = $3";
  1528. dataarray.push(filter);
  1529. }
  1530. query = query + ") GROUP BY id ORDER BY time DESC";
  1531. }
  1532. sqlDbQuery(query, dataarray, func);
  1533. };
  1534. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  1535. var query = "SELECT doc FROM events ";
  1536. var dataarray = [domain, userid];
  1537. if (ids.indexOf('*') >= 0) {
  1538. query = query + "WHERE (domain = $1 AND userid = $2";
  1539. if (filter != null) {
  1540. query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
  1541. dataarray.push(filter);
  1542. } else {
  1543. query = query + ") ORDER BY time DESC LIMIT $3";
  1544. }
  1545. } else {
  1546. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
  1547. if (filter != null) {
  1548. query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
  1549. dataarray.push(filter);
  1550. } else {
  1551. query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
  1552. }
  1553. }
  1554. dataarray.push(limit);
  1555. sqlDbQuery(query, dataarray, func);
  1556. };
  1557. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
  1558. if (ids.indexOf('*') >= 0) {
  1559. sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
  1560. } else {
  1561. sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target IN (' + dbMergeSqlArray(ids) + ')) AND (time BETWEEN $2 AND $3)) GROUP BY id ORDER BY time', [domain, start, end], func);
  1562. }
  1563. };
  1564. //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
  1565. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  1566. var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
  1567. var dataarray = [nodeid, domain];
  1568. if (filter != null) {
  1569. query = query + "AND action = $3) ORDER BY time DESC LIMIT $4";
  1570. dataarray.push(filter);
  1571. } else {
  1572. query = query + ") ORDER BY time DESC LIMIT $3";
  1573. }
  1574. dataarray.push(limit);
  1575. sqlDbQuery(query, dataarray, func);
  1576. };
  1577. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  1578. var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
  1579. var dataarray = [nodeid, domain, userid];
  1580. if (filter != null) {
  1581. query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
  1582. dataarray.push(filter);
  1583. } else {
  1584. query = query + "ORDER BY time DESC LIMIT $4";
  1585. }
  1586. dataarray.push(limit);
  1587. sqlDbQuery(query, dataarray, func);
  1588. };
  1589. obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
  1590. obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
  1591. obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
  1592. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response[0]['COUNT(*)'] : 0); }); }
  1593. // Database actions on the power collection
  1594. obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
  1595. obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (NULL, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
  1596. obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
  1597. obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
  1598. obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
  1599. // Database actions on the SMBIOS collection
  1600. obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
  1601. obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
  1602. obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
  1603. obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
  1604. // Database actions on the Server Stats collection
  1605. obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, JSON.stringify(data)], func); };
  1606. obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
  1607. // Read a configuration file from the database
  1608. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  1609. // Write a configuration file to the database
  1610. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  1611. // List all configuration files
  1612. obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
  1613. // Get database information (TODO: Complete this)
  1614. obj.getDbStats = function (func) {
  1615. obj.stats = { c: 4 };
  1616. sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response) { obj.stats.meshcentral = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1617. sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response) { obj.stats.serverstats = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1618. sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response) { obj.stats.power = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1619. sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response) { obj.stats.smbios = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1620. }
  1621. // Plugin operations
  1622. if (obj.pluginsActive) {
  1623. obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
  1624. obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
  1625. obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
  1626. obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
  1627. obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
  1628. obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
  1629. }
  1630. } else if (obj.databaseType == 7) {
  1631. // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
  1632. obj.Set = function (data, func) {
  1633. data = common.escapeLinksFieldNameEx(data);
  1634. var xdata = performTypedRecordEncrypt(data);
  1635. obj.dbCounters.fileSet++;
  1636. obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
  1637. };
  1638. obj.Get = function (id, func) {
  1639. obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
  1640. if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
  1641. });
  1642. };
  1643. obj.GetAll = function (func) {
  1644. obj.file.ref('meshcentral').get(function(snapshot) {
  1645. const val = snapshot.val();
  1646. const docs = Object.keys(val).map(function(key) { return val[key]; });
  1647. func(null, common.aceUnEscapeAllFieldNames(docs));
  1648. });
  1649. };
  1650. obj.GetHash = function (id, func) {
  1651. obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
  1652. if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
  1653. });
  1654. };
  1655. obj.GetAllTypeNoTypeField = function (type, domain, func) {
  1656. obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
  1657. const docs = [];
  1658. for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
  1659. func(null, common.aceUnEscapeAllFieldNames(docs));
  1660. });
  1661. }
  1662. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  1663. if (meshes.length == 0) { func(null, []); return; }
  1664. var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
  1665. if (id) { query = query.filter('_id', '==', id); }
  1666. if (extrasids == null) {
  1667. query = query.filter('meshid', 'in', meshes);
  1668. query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
  1669. } else {
  1670. // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
  1671. query.get(function (snapshots) {
  1672. const docs = [];
  1673. for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
  1674. func(null, performTypedRecordDecrypt(docs));
  1675. });
  1676. }
  1677. };
  1678. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  1679. var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
  1680. if (id) { query = query.filter('_id', '==', id); }
  1681. query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
  1682. };
  1683. obj.GetAllType = function (type, func) {
  1684. obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
  1685. const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
  1686. func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
  1687. });
  1688. };
  1689. obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.query('meshcentral').filter('_id', 'in', ids).filter('domain', '==', domain).filter('type', '==', type).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
  1690. obj.GetUserWithEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
  1691. obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).filter('emailVerified', '==', true).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
  1692. obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
  1693. obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
  1694. obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
  1695. obj.InsertMany = function (data, func) { var r = {}; for (var i in data) { const ref = obj.file.ref('meshcentral').child(encodeURIComponent(data[i]._id)); r[ref.key] = common.aceEscapeFieldNames(data[i]); } obj.file.ref('meshcentral').set(r).then(function (ref) { func(); }); }; // Insert records directly, no link escaping
  1696. obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
  1697. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  1698. obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
  1699. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  1700. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  1701. obj.getLocalAmtNodes = function (func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('host', 'exists').filter('host', '!=', null).filter('intelamt', 'exists').get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
  1702. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domainid).filter('mtype', '!=', mtype).filter('intelamt.uuid', '==', uuid).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
  1703. obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domainid).get({ snapshots: false }, function (snapshots) { func((snapshots.length > max), snapshots.length); }); } }
  1704. // Database actions on the events collection
  1705. obj.GetAllEvents = function (func) {
  1706. obj.file.ref('events').get(function (snapshot) {
  1707. const val = snapshot.val();
  1708. const docs = Object.keys(val).map(function(key) { return val[key]; });
  1709. func(null, docs);
  1710. })
  1711. };
  1712. obj.StoreEvent = function (event, func) {
  1713. if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
  1714. obj.dbCounters.eventsSet++;
  1715. obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
  1716. };
  1717. obj.GetEvents = function (ids, domain, filter, func) {
  1718. // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
  1719. if (filter != null) {
  1720. obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
  1721. const docs = [];
  1722. for (var i in snapshots) {
  1723. const doc = snapshots[i].val();
  1724. if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
  1725. var found = false;
  1726. for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
  1727. if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
  1728. }
  1729. func(null, docs);
  1730. });
  1731. } else {
  1732. obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
  1733. const docs = [];
  1734. for (var i in snapshots) {
  1735. const doc = snapshots[i].val();
  1736. if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
  1737. var found = false;
  1738. for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
  1739. if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
  1740. }
  1741. func(null, docs);
  1742. });
  1743. }
  1744. };
  1745. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  1746. // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
  1747. // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
  1748. // obj.file.indexes.create('events', 'ids', { type: 'array' });
  1749. // db.query('events').filter('ids', 'array:contains-one-of', ids)
  1750. if (filter != null) {
  1751. obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
  1752. const docs = [];
  1753. for (var i in snapshots) {
  1754. const doc = snapshots[i].val();
  1755. if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
  1756. var found = false;
  1757. for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
  1758. if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
  1759. }
  1760. func(null, docs);
  1761. });
  1762. } else {
  1763. obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
  1764. const docs = [];
  1765. for (var i in snapshots) {
  1766. const doc = snapshots[i].val();
  1767. if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
  1768. var found = false;
  1769. for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
  1770. if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
  1771. }
  1772. func(null, docs);
  1773. });
  1774. }
  1775. };
  1776. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  1777. if (filter != null) {
  1778. obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1779. } else {
  1780. obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1781. }
  1782. };
  1783. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  1784. if (filter != null) {
  1785. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1786. } else {
  1787. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1788. }
  1789. };
  1790. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
  1791. obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1792. };
  1793. obj.GetUserLoginEvents = function (domain, userid, func) {
  1794. obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1795. };
  1796. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  1797. if (filter != null) {
  1798. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1799. } else {
  1800. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1801. }
  1802. };
  1803. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  1804. if (filter != null) {
  1805. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1806. } else {
  1807. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1808. }
  1809. obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1810. };
  1811. obj.RemoveAllEvents = function (domain) {
  1812. obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
  1813. };
  1814. obj.RemoveAllNodeEvents = function (domain, nodeid) {
  1815. if ((domain == null) || (nodeid == null)) return;
  1816. obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
  1817. };
  1818. obj.RemoveAllUserEvents = function (domain, userid) {
  1819. if ((domain == null) || (userid == null)) return;
  1820. obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
  1821. };
  1822. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
  1823. obj.file.query('events').filter('domain', '==', domainid).filter('userid', '==', userid).filter('time', '>', lastlogin).sort('time', false).get({ snapshots: false }, function (snapshots) { func(null, snapshots.length); });
  1824. }
  1825. // Database actions on the power collection
  1826. obj.getAllPower = function (func) {
  1827. obj.file.ref('power').get(function (snapshot) {
  1828. const val = snapshot.val();
  1829. const docs = Object.keys(val).map(function(key) { return val[key]; });
  1830. func(null, docs);
  1831. });
  1832. };
  1833. obj.storePowerEvent = function (event, multiServer, func) {
  1834. if (multiServer != null) { event.server = multiServer.serverid; }
  1835. obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
  1836. };
  1837. obj.getPowerTimeline = function (nodeid, func) {
  1838. obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
  1839. const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
  1840. });
  1841. };
  1842. obj.removeAllPowerEvents = function () {
  1843. obj.file.ref('power').remove().then(function () { if (func) { func(); } });
  1844. };
  1845. obj.removeAllPowerEventsForNode = function (nodeid) {
  1846. if (nodeid == null) return;
  1847. obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
  1848. };
  1849. // Database actions on the SMBIOS collection
  1850. if (obj.smbiosfile != null) {
  1851. obj.GetAllSMBIOS = function (func) {
  1852. obj.file.ref('smbios').get(function (snapshot) {
  1853. const val = snapshot.val();
  1854. const docs = Object.keys(val).map(function(key) { return val[key]; });
  1855. func(null, docs);
  1856. });
  1857. };
  1858. obj.SetSMBIOS = function (smbios, func) {
  1859. obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
  1860. };
  1861. obj.RemoveSMBIOS = function (id) {
  1862. obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
  1863. };
  1864. obj.GetSMBIOS = function (id, func) {
  1865. obj.file.query('smbios').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
  1866. };
  1867. }
  1868. // Database actions on the Server Stats collection
  1869. obj.SetServerStats = function (data, func) {
  1870. obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
  1871. };
  1872. obj.GetServerStats = function (hours, func) {
  1873. var t = new Date();
  1874. t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
  1875. obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
  1876. const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
  1877. });
  1878. };
  1879. // Read a configuration file from the database
  1880. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  1881. // Write a configuration file to the database
  1882. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  1883. // List all configuration files
  1884. obj.listConfigFiles = function (func) {
  1885. obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
  1886. const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
  1887. });
  1888. }
  1889. // Get database information
  1890. obj.getDbStats = function (func) {
  1891. obj.stats = { c: 5 };
  1892. obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1893. obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1894. obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1895. obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1896. obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  1897. }
  1898. // Plugin operations
  1899. if (obj.pluginsActive) {
  1900. obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.file.ref('plugin').child(encodeURIComponent(plugin._id)).set(plugin).then(function (ref) { if (func) { func(); } }) }; // Add a plugin
  1901. obj.getPlugins = function (func) {
  1902. obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
  1903. const val = snapshot.val();
  1904. const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
  1905. func(null, docs);
  1906. });
  1907. }; // Get all plugins
  1908. obj.getPlugin = function (id, func) { obj.file.query('plugin').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; // Get plugin
  1909. obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
  1910. obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
  1911. obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
  1912. }
  1913. } else if (obj.databaseType == 6) {
  1914. // Database actions on the main collection (Postgres)
  1915. obj.Set = function (value, func) {
  1916. obj.dbCounters.fileSet++;
  1917. var extra = null, extraex = null;
  1918. value = common.escapeLinksFieldNameEx(value);
  1919. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  1920. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  1921. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  1922. sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
  1923. }
  1924. obj.SetRaw = function (value, func) {
  1925. obj.dbCounters.fileSet++;
  1926. var extra = null, extraex = null;
  1927. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  1928. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  1929. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  1930. sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
  1931. }
  1932. obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
  1933. obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1934. obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1935. obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
  1936. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  1937. if (limit == 0) { limit = 0xFFFFFFFF; }
  1938. if (id && (id != '')) {
  1939. sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  1940. } else {
  1941. if (extrasids == null) {
  1942. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
  1943. } else {
  1944. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  1945. }
  1946. }
  1947. };
  1948. obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
  1949. if (id && (id != '')) {
  1950. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
  1951. } else {
  1952. if (extrasids == null) {
  1953. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { func(err, docs); }, true);
  1954. } else {
  1955. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
  1956. }
  1957. }
  1958. };
  1959. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  1960. if (id && (id != '')) {
  1961. sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  1962. } else {
  1963. sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  1964. }
  1965. };
  1966. obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1967. obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1968. obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1969. obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  1970. obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
  1971. obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
  1972. obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
  1973. obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
  1974. obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
  1975. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  1976. obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
  1977. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  1978. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  1979. obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
  1980. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
  1981. obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
  1982. // Database actions on the events collection
  1983. obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
  1984. obj.StoreEvent = function (event, func) {
  1985. obj.dbCounters.eventsSet++;
  1986. sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) {
  1987. if(func){ func(); }
  1988. if (docs.id) {
  1989. for (var i in event.ids) {
  1990. if (event.ids[i] != '*') {
  1991. obj.pendingTransfer++;
  1992. sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
  1993. }
  1994. }
  1995. }
  1996. });
  1997. };
  1998. obj.GetEvents = function (ids, domain, filter, func) {
  1999. var query = "SELECT doc FROM events ";
  2000. var dataarray = [domain];
  2001. if (ids.indexOf('*') >= 0) {
  2002. query = query + "WHERE (domain = $1";
  2003. if (filter != null) {
  2004. query = query + " AND action = $2";
  2005. dataarray.push(filter);
  2006. }
  2007. query = query + ") ORDER BY time DESC";
  2008. } else {
  2009. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
  2010. dataarray.push(ids);
  2011. if (filter != null) {
  2012. query = query + " AND action = $3";
  2013. dataarray.push(filter);
  2014. }
  2015. query = query + ") GROUP BY id ORDER BY time DESC";
  2016. }
  2017. sqlDbQuery(query, dataarray, func);
  2018. };
  2019. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  2020. var query = "SELECT doc FROM events ";
  2021. var dataarray = [domain];
  2022. if (ids.indexOf('*') >= 0) {
  2023. query = query + "WHERE (domain = $1";
  2024. if (filter != null) {
  2025. query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
  2026. dataarray.push(filter);
  2027. } else {
  2028. query = query + ") ORDER BY time DESC LIMIT $2";
  2029. }
  2030. } else {
  2031. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2032. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
  2033. dataarray.push(ids);
  2034. if (filter != null) {
  2035. query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
  2036. dataarray.push(filter);
  2037. } else {
  2038. query = query + ") ORDER BY time DESC LIMIT $3";
  2039. }
  2040. }
  2041. dataarray.push(limit);
  2042. sqlDbQuery(query, dataarray, func);
  2043. };
  2044. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  2045. var query = "SELECT doc FROM events ";
  2046. var dataarray = [domain, userid];
  2047. if (ids.indexOf('*') >= 0) {
  2048. query = query + "WHERE (domain = $1 AND userid = $2";
  2049. if (filter != null) {
  2050. query = query + " AND action = $3";
  2051. dataarray.push(filter);
  2052. }
  2053. query = query + ") ORDER BY time DESC";
  2054. } else {
  2055. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2056. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
  2057. dataarray.push(ids);
  2058. if (filter != null) {
  2059. query = query + " AND action = $4";
  2060. dataarray.push(filter);
  2061. }
  2062. query = query + ") GROUP BY id ORDER BY time DESC";
  2063. }
  2064. sqlDbQuery(query, dataarray, func);
  2065. };
  2066. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  2067. var query = "SELECT doc FROM events ";
  2068. var dataarray = [domain, userid];
  2069. if (ids.indexOf('*') >= 0) {
  2070. query = query + "WHERE (domain = $1 AND userid = $2";
  2071. if (filter != null) {
  2072. query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
  2073. dataarray.push(filter);
  2074. } else {
  2075. query = query + ") ORDER BY time DESC LIMIT $3";
  2076. }
  2077. } else {
  2078. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2079. query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
  2080. dataarray.push(ids);
  2081. if (filter != null) {
  2082. query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
  2083. dataarray.push(filter);
  2084. } else {
  2085. query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
  2086. }
  2087. }
  2088. dataarray.push(limit);
  2089. sqlDbQuery(query, dataarray, func);
  2090. };
  2091. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
  2092. if (ids.indexOf('*') >= 0) {
  2093. sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
  2094. } else {
  2095. sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target = ANY ($2)) AND (time BETWEEN $3 AND $4)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
  2096. }
  2097. };
  2098. //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
  2099. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  2100. var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
  2101. var dataarray = [nodeid, domain];
  2102. if (filter != null) {
  2103. query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
  2104. dataarray.push(filter);
  2105. } else {
  2106. query = query + ") ORDER BY time DESC LIMIT $3";
  2107. }
  2108. dataarray.push(limit);
  2109. sqlDbQuery(query, dataarray, func);
  2110. };
  2111. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  2112. var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
  2113. var dataarray = [nodeid, domain, userid];
  2114. if (filter != null) {
  2115. query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
  2116. dataarray.push(filter);
  2117. } else {
  2118. query = query + ") ORDER BY time DESC LIMIT $4";
  2119. }
  2120. dataarray.push(limit);
  2121. sqlDbQuery(query, dataarray, func);
  2122. };
  2123. obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
  2124. obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
  2125. obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
  2126. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response, raw) { func(err == null ? parseInt(raw.rows[0].count) : 0); }); }
  2127. // Database actions on the power collection
  2128. obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
  2129. obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (DEFAULT, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, event], func); };
  2130. obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
  2131. obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
  2132. obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
  2133. // Database actions on the SMBIOS collection
  2134. obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
  2135. obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, smbios], func); };
  2136. obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
  2137. obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
  2138. // Database actions on the Server Stats collection
  2139. obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, data], func); };
  2140. obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
  2141. // Read a configuration file from the database
  2142. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  2143. // Write a configuration file to the database
  2144. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  2145. // List all configuration files
  2146. obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
  2147. // Get database information (TODO: Complete this)
  2148. obj.getDbStats = function (func) {
  2149. obj.stats = { c: 4 };
  2150. sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response, raw) { obj.stats.meshcentral = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2151. sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response, raw) { obj.stats.serverstats = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2152. sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response, raw) { obj.stats.power = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2153. sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response, raw) { obj.stats.smbios = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2154. }
  2155. // Plugin operations
  2156. if (obj.pluginsActive) {
  2157. obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
  2158. obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
  2159. obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
  2160. obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
  2161. obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
  2162. obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
  2163. }
  2164. } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
  2165. // Database actions on the main collection (MariaDB or MySQL)
  2166. obj.Set = function (value, func) {
  2167. obj.dbCounters.fileSet++;
  2168. var extra = null, extraex = null;
  2169. value = common.escapeLinksFieldNameEx(value);
  2170. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  2171. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  2172. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  2173. sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
  2174. }
  2175. obj.SetRaw = function (value, func) {
  2176. obj.dbCounters.fileSet++;
  2177. var extra = null, extraex = null;
  2178. if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
  2179. if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
  2180. if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
  2181. sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
  2182. }
  2183. obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
  2184. obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  2185. obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  2186. obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
  2187. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  2188. if (limit == 0) { limit = 0xFFFFFFFF; }
  2189. if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2190. if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2191. if (id && (id != '')) {
  2192. sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  2193. } else {
  2194. sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  2195. }
  2196. };
  2197. obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
  2198. if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2199. if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2200. if (id && (id != '')) {
  2201. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
  2202. } else {
  2203. sqlDbQuery('SELECT COUNT(doc) FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
  2204. }
  2205. };
  2206. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  2207. if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2208. if (id && (id != '')) {
  2209. sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  2210. } else {
  2211. sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
  2212. }
  2213. };
  2214. obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  2215. obj.GetAllIdsOfType = function (ids, domain, type, func) {
  2216. if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2217. sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2218. }
  2219. obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  2220. obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
  2221. obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
  2222. obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
  2223. obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
  2224. obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
  2225. obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
  2226. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  2227. obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
  2228. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  2229. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  2230. obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
  2231. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
  2232. obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
  2233. // Database actions on the events collection
  2234. obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
  2235. obj.StoreEvent = function (event, func) {
  2236. obj.dbCounters.eventsSet++;
  2237. var batchQuery = [['INSERT INTO events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
  2238. for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
  2239. sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
  2240. };
  2241. obj.GetEvents = function (ids, domain, filter, func) {
  2242. var query = "SELECT doc FROM events ";
  2243. var dataarray = [domain];
  2244. if (ids.indexOf('*') >= 0) {
  2245. query = query + "WHERE (domain = ?";
  2246. if (filter != null) {
  2247. query = query + " AND action = ?";
  2248. dataarray.push(filter);
  2249. }
  2250. query = query + ") ORDER BY time DESC";
  2251. } else {
  2252. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2253. query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
  2254. dataarray.push(ids);
  2255. if (filter != null) {
  2256. query = query + " AND action = ?";
  2257. dataarray.push(filter);
  2258. }
  2259. query = query + ") GROUP BY id ORDER BY time DESC";
  2260. }
  2261. sqlDbQuery(query, dataarray, func);
  2262. };
  2263. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  2264. var query = "SELECT doc FROM events ";
  2265. var dataarray = [domain];
  2266. if (ids.indexOf('*') >= 0) {
  2267. query = query + "WHERE (domain = ?";
  2268. if (filter != null) {
  2269. query = query + " AND action = ? ";
  2270. dataarray.push(filter);
  2271. }
  2272. query = query + ") ORDER BY time DESC LIMIT ?";
  2273. } else {
  2274. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2275. query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
  2276. dataarray.push(ids);
  2277. if (filter != null) {
  2278. query = query + " AND action = ?";
  2279. dataarray.push(filter);
  2280. }
  2281. query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
  2282. }
  2283. dataarray.push(limit);
  2284. sqlDbQuery(query, dataarray, func);
  2285. };
  2286. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  2287. var query = "SELECT doc FROM events ";
  2288. var dataarray = [domain, userid];
  2289. if (ids.indexOf('*') >= 0) {
  2290. query = query + "WHERE (domain = ? AND userid = ?";
  2291. if (filter != null) {
  2292. query = query + " AND action = ?";
  2293. dataarray.push(filter);
  2294. }
  2295. query = query + ") ORDER BY time DESC";
  2296. } else {
  2297. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2298. query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
  2299. dataarray.push(ids);
  2300. if (filter != null) {
  2301. query = query + " AND action = ?";
  2302. dataarray.push(filter);
  2303. }
  2304. query = query + ") GROUP BY id ORDER BY time DESC";
  2305. }
  2306. sqlDbQuery(query, dataarray, func);
  2307. };
  2308. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  2309. var query = "SELECT doc FROM events ";
  2310. var dataarray = [domain, userid];
  2311. if (ids.indexOf('*') >= 0) {
  2312. query = query + "WHERE (domain = ? AND userid = ?";
  2313. if (filter != null) {
  2314. query = query + " AND action = ?";
  2315. dataarray.push(filter);
  2316. }
  2317. query = query + ") ORDER BY time DESC LIMIT ?";
  2318. } else {
  2319. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2320. query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
  2321. dataarray.push(ids);
  2322. if (filter != null) {
  2323. query = query + " AND action = ?";
  2324. dataarray.push(filter);
  2325. }
  2326. query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
  2327. }
  2328. dataarray.push(limit);
  2329. sqlDbQuery(query, dataarray, func);
  2330. };
  2331. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
  2332. if (ids.indexOf('*') >= 0) {
  2333. sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
  2334. } else {
  2335. if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
  2336. sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
  2337. }
  2338. };
  2339. //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
  2340. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  2341. var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
  2342. var dataarray = [nodeid, domain];
  2343. if (filter != null) {
  2344. query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
  2345. dataarray.push(filter);
  2346. } else {
  2347. query = query + ") ORDER BY time DESC LIMIT ?";
  2348. }
  2349. dataarray.push(limit);
  2350. sqlDbQuery(query, dataarray, func);
  2351. };
  2352. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  2353. var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
  2354. var dataarray = [nodeid, domain, userid];
  2355. if (filter != null) {
  2356. query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
  2357. dataarray.push(filter);
  2358. } else {
  2359. query = query + ") ORDER BY time DESC LIMIT ?";
  2360. }
  2361. dataarray.push(limit);
  2362. sqlDbQuery(query, dataarray, func);
  2363. };
  2364. obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
  2365. obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
  2366. obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
  2367. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
  2368. // Database actions on the power collection
  2369. obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
  2370. obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
  2371. obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
  2372. obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
  2373. obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
  2374. // Database actions on the SMBIOS collection
  2375. obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
  2376. obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
  2377. obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
  2378. obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
  2379. // Database actions on the Server Stats collection
  2380. obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
  2381. obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
  2382. // Read a configuration file from the database
  2383. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  2384. // Write a configuration file to the database
  2385. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  2386. // List all configuration files
  2387. obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
  2388. // Get database information (TODO: Complete this)
  2389. obj.getDbStats = function (func) {
  2390. obj.stats = { c: 4 };
  2391. sqlDbExec('SELECT COUNT(id) FROM main', null, function (err, response) { obj.stats.meshcentral = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2392. sqlDbExec('SELECT COUNT(time) FROM serverstats', null, function (err, response) { obj.stats.serverstats = Number(response['COUNT(time)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2393. sqlDbExec('SELECT COUNT(id) FROM power', null, function (err, response) { obj.stats.power = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2394. sqlDbExec('SELECT COUNT(id) FROM smbios', null, function (err, response) { obj.stats.smbios = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
  2395. }
  2396. // Plugin operations
  2397. if (obj.pluginsActive) {
  2398. obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
  2399. obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
  2400. obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
  2401. obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
  2402. obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
  2403. obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
  2404. }
  2405. } else if (obj.databaseType == 3) {
  2406. // Database actions on the main collection (MongoDB)
  2407. // Bulk operations
  2408. if (parent.config.settings.mongodbbulkoperations) {
  2409. obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
  2410. if (obj.filePendingSet == false) {
  2411. // Perform the operation now
  2412. obj.dbCounters.fileSet++;
  2413. obj.filePendingSet = true; obj.filePendingSets = null;
  2414. if (func != null) { obj.filePendingCbs = [func]; }
  2415. obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
  2416. } else {
  2417. // Add this operation to the pending list
  2418. obj.dbCounters.fileSetPending++;
  2419. if (obj.filePendingSets == null) { obj.filePendingSets = {} }
  2420. obj.filePendingSets[data._id] = data;
  2421. if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
  2422. }
  2423. };
  2424. obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
  2425. // Encode arguments into return function if any are present.
  2426. var func2 = func;
  2427. if (arguments.length > 2) {
  2428. var parms = [func];
  2429. for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
  2430. var func2 = function _func2(arg1, arg2) {
  2431. var userCallback = _func2.userArgs.shift();
  2432. _func2.userArgs.unshift(arg2);
  2433. _func2.userArgs.unshift(arg1);
  2434. userCallback.apply(obj, _func2.userArgs);
  2435. };
  2436. func2.userArgs = parms;
  2437. }
  2438. if (obj.filePendingGets == null) {
  2439. // No pending gets, perform the operation now.
  2440. obj.filePendingGets = {};
  2441. obj.filePendingGets[id] = [func2];
  2442. obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
  2443. } else {
  2444. // Add get to pending list.
  2445. if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
  2446. if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
  2447. }
  2448. };
  2449. } else {
  2450. obj.Set = function (data, func) {
  2451. obj.dbCounters.fileSet++;
  2452. data = common.escapeLinksFieldNameEx(data);
  2453. obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
  2454. };
  2455. obj.Get = function (id, func) {
  2456. if (arguments.length > 2) {
  2457. var parms = [func];
  2458. for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
  2459. var func2 = function _func2(arg1, arg2) {
  2460. var userCallback = _func2.userArgs.shift();
  2461. _func2.userArgs.unshift(arg2);
  2462. _func2.userArgs.unshift(arg1);
  2463. userCallback.apply(obj, _func2.userArgs);
  2464. };
  2465. func2.userArgs = parms;
  2466. obj.file.find({ _id: id }).toArray(function (err, docs) {
  2467. if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
  2468. func2(err, performTypedRecordDecrypt(docs));
  2469. });
  2470. } else {
  2471. obj.file.find({ _id: id }).toArray(function (err, docs) {
  2472. if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
  2473. func(err, performTypedRecordDecrypt(docs));
  2474. });
  2475. }
  2476. };
  2477. }
  2478. obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2479. obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2480. obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2481. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  2482. if (extrasids == null) {
  2483. const x = { type: type, domain: domain, meshid: { $in: meshes } };
  2484. if (id) { x._id = id; }
  2485. var f = obj.file.find(x, { type: 0 });
  2486. if (skip > 0) f = f.skip(skip); // Skip records
  2487. if (limit > 0) f = f.limit(limit); // Limit records
  2488. f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2489. } else {
  2490. const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
  2491. if (id) { x._id = id; }
  2492. var f = obj.file.find(x, { type: 0 });
  2493. if (skip > 0) f = f.skip(skip); // Skip records
  2494. if (limit > 0) f = f.limit(limit); // Limit records
  2495. f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2496. }
  2497. };
  2498. obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
  2499. if (extrasids == null) {
  2500. const x = { type: type, domain: domain, meshid: { $in: meshes } };
  2501. if (id) { x._id = id; }
  2502. var f = obj.file.find(x, { type: 0 });
  2503. f.count(function (err, count) { func(err, count); });
  2504. } else {
  2505. const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
  2506. if (id) { x._id = id; }
  2507. var f = obj.file.find(x, { type: 0 });
  2508. f.count(function (err, count) { func(err, count); });
  2509. }
  2510. };
  2511. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  2512. var x = { type: type, domain: domain, nodeid: { $in: nodes } };
  2513. if (id) { x._id = id; }
  2514. obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2515. };
  2516. obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2517. obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2518. obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2519. obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2520. // Bulk operations
  2521. if (parent.config.settings.mongodbbulkoperations) {
  2522. obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
  2523. if (obj.filePendingRemoves == null) {
  2524. // No pending removes, perform the operation now.
  2525. obj.dbCounters.fileRemove++;
  2526. obj.filePendingRemoves = {};
  2527. obj.filePendingRemoves[id] = [func];
  2528. obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
  2529. } else {
  2530. // Add remove to pending list.
  2531. obj.dbCounters.fileRemovePending++;
  2532. if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
  2533. if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
  2534. }
  2535. };
  2536. } else {
  2537. obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
  2538. }
  2539. obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
  2540. obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
  2541. obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
  2542. obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
  2543. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  2544. obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
  2545. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  2546. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  2547. obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
  2548. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
  2549. // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
  2550. // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
  2551. //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
  2552. obj.isMaxType = function (max, type, domainid, func) {
  2553. if (obj.file.countDocuments) {
  2554. if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
  2555. } else {
  2556. if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
  2557. }
  2558. }
  2559. // Database actions on the events collection
  2560. obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
  2561. // Bulk operations
  2562. if (parent.config.settings.mongodbbulkoperations) {
  2563. obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
  2564. if (obj.eventsFilePendingSet == false) {
  2565. // Perform the operation now
  2566. obj.dbCounters.eventsSet++;
  2567. obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
  2568. if (func != null) { obj.eventsFilePendingCbs = [func]; }
  2569. obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
  2570. } else {
  2571. // Add this operation to the pending list
  2572. obj.dbCounters.eventsSetPending++;
  2573. if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
  2574. obj.eventsFilePendingSets.push(event);
  2575. if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
  2576. }
  2577. };
  2578. } else {
  2579. obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
  2580. }
  2581. obj.GetEvents = function (ids, domain, filter, func) {
  2582. var finddata = { domain: domain, ids: { $in: ids } };
  2583. if (filter != null) finddata.action = filter;
  2584. obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
  2585. };
  2586. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  2587. var finddata = { domain: domain, ids: { $in: ids } };
  2588. if (filter != null) finddata.action = filter;
  2589. obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
  2590. };
  2591. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  2592. var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
  2593. if (filter != null) finddata.action = filter;
  2594. obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
  2595. };
  2596. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  2597. var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
  2598. if (filter != null) finddata.action = filter;
  2599. obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
  2600. };
  2601. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
  2602. obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
  2603. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  2604. var finddata = { domain: domain, nodeid: nodeid };
  2605. if (filter != null) finddata.action = filter;
  2606. obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
  2607. };
  2608. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  2609. var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
  2610. if (filter != null) finddata.action = filter;
  2611. obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
  2612. };
  2613. obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
  2614. obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
  2615. obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
  2616. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
  2617. if (obj.eventsfile.countDocuments) {
  2618. obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
  2619. } else {
  2620. obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
  2621. }
  2622. }
  2623. // Database actions on the power collection
  2624. obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
  2625. // Bulk operations
  2626. if (parent.config.settings.mongodbbulkoperations) {
  2627. obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
  2628. if (multiServer != null) { event.server = multiServer.serverid; }
  2629. if (obj.powerFilePendingSet == false) {
  2630. // Perform the operation now
  2631. obj.dbCounters.powerSet++;
  2632. obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
  2633. if (func != null) { obj.powerFilePendingCbs = [func]; }
  2634. obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
  2635. } else {
  2636. // Add this operation to the pending list
  2637. obj.dbCounters.powerSetPending++;
  2638. if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
  2639. obj.powerFilePendingSets.push(event);
  2640. if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
  2641. }
  2642. };
  2643. } else {
  2644. obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
  2645. }
  2646. obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
  2647. obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
  2648. obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
  2649. // Database actions on the SMBIOS collection
  2650. obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
  2651. obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
  2652. obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
  2653. obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
  2654. // Database actions on the Server Stats collection
  2655. obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
  2656. obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
  2657. // Read a configuration file from the database
  2658. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  2659. // Write a configuration file to the database
  2660. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  2661. // List all configuration files
  2662. obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
  2663. // Get database information
  2664. obj.getDbStats = function (func) {
  2665. obj.stats = { c: 6 };
  2666. obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
  2667. obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2668. obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2669. obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2670. obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2671. obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2672. }
  2673. // Correct database information of obj.getDbStats before returning it
  2674. function getDbStatsEx(data) {
  2675. var r = {};
  2676. if (data.recordTypes != null) { r = data.recordTypes; }
  2677. try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
  2678. try { r.power = data['meshcentral.power'].count; } catch (ex) { }
  2679. try { r.events = data['meshcentral.events'].count; } catch (ex) { }
  2680. try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
  2681. return r;
  2682. }
  2683. // Plugin operations
  2684. if (obj.pluginsActive) {
  2685. obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
  2686. obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
  2687. obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
  2688. obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
  2689. obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
  2690. obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
  2691. }
  2692. } else {
  2693. // Database actions on the main collection (NeDB and MongoJS)
  2694. obj.Set = function (data, func) {
  2695. obj.dbCounters.fileSet++;
  2696. data = common.escapeLinksFieldNameEx(data);
  2697. var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
  2698. };
  2699. obj.Get = function (id, func) {
  2700. if (arguments.length > 2) {
  2701. var parms = [func];
  2702. for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
  2703. var func2 = function _func2(arg1, arg2) {
  2704. var userCallback = _func2.userArgs.shift();
  2705. _func2.userArgs.unshift(arg2);
  2706. _func2.userArgs.unshift(arg1);
  2707. userCallback.apply(obj, _func2.userArgs);
  2708. };
  2709. func2.userArgs = parms;
  2710. obj.file.find({ _id: id }, function (err, docs) {
  2711. if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
  2712. func2(err, performTypedRecordDecrypt(docs));
  2713. });
  2714. } else {
  2715. obj.file.find({ _id: id }, function (err, docs) {
  2716. if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
  2717. func(err, performTypedRecordDecrypt(docs));
  2718. });
  2719. }
  2720. };
  2721. obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2722. obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
  2723. obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2724. //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
  2725. //var x = { type: type, domain: domain, meshid: { $in: meshes } };
  2726. //if (id) { x._id = id; }
  2727. //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2728. //};
  2729. obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
  2730. if (extrasids == null) {
  2731. const x = { type: type, domain: domain, meshid: { $in: meshes } };
  2732. if (id) { x._id = id; }
  2733. obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2734. } else {
  2735. const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
  2736. if (id) { x._id = id; }
  2737. obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2738. }
  2739. };
  2740. obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
  2741. var x = { type: type, domain: domain, nodeid: { $in: nodes } };
  2742. if (id) { x._id = id; }
  2743. obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
  2744. };
  2745. obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2746. obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2747. obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2748. obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
  2749. obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
  2750. obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
  2751. obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
  2752. obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
  2753. obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
  2754. obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
  2755. obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
  2756. obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
  2757. obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
  2758. obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
  2759. obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
  2760. obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
  2761. // Database actions on the events collection
  2762. obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
  2763. obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
  2764. obj.GetEvents = function (ids, domain, filter, func) {
  2765. var finddata = { domain: domain, ids: { $in: ids } };
  2766. if (filter != null) finddata.action = filter;
  2767. if (obj.databaseType == 1) {
  2768. obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
  2769. } else {
  2770. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
  2771. }
  2772. };
  2773. obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
  2774. var finddata = { domain: domain, ids: { $in: ids } };
  2775. if (filter != null) finddata.action = filter;
  2776. if (obj.databaseType == 1) {
  2777. obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
  2778. } else {
  2779. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
  2780. }
  2781. };
  2782. obj.GetUserEvents = function (ids, domain, userid, filter, func) {
  2783. var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
  2784. if (filter != null) finddata.action = filter;
  2785. if (obj.databaseType == 1) {
  2786. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
  2787. } else {
  2788. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
  2789. }
  2790. };
  2791. obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
  2792. var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
  2793. if (filter != null) finddata.action = filter;
  2794. if (obj.databaseType == 1) {
  2795. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
  2796. } else {
  2797. obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
  2798. }
  2799. };
  2800. obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
  2801. if (obj.databaseType == 1) {
  2802. obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
  2803. } else {
  2804. obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
  2805. }
  2806. };
  2807. obj.GetUserLoginEvents = function (domain, userid, func) {
  2808. if (obj.databaseType == 1) {
  2809. obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
  2810. } else {
  2811. obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
  2812. }
  2813. };
  2814. obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
  2815. var finddata = { domain: domain, nodeid: nodeid };
  2816. if (filter != null) finddata.action = filter;
  2817. if (obj.databaseType == 1) {
  2818. obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
  2819. } else {
  2820. obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
  2821. }
  2822. };
  2823. obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
  2824. var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
  2825. if (filter != null) finddata.action = filter;
  2826. if (obj.databaseType == 1) {
  2827. obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
  2828. } else {
  2829. obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
  2830. }
  2831. };
  2832. obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
  2833. obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
  2834. obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
  2835. obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
  2836. // Database actions on the power collection
  2837. obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
  2838. obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
  2839. obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
  2840. obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
  2841. obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
  2842. // Database actions on the SMBIOS collection
  2843. if (obj.smbiosfile != null) {
  2844. obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
  2845. obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
  2846. obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
  2847. obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
  2848. }
  2849. // Database actions on the Server Stats collection
  2850. obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
  2851. obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
  2852. // Read a configuration file from the database
  2853. obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
  2854. // Write a configuration file to the database
  2855. obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
  2856. // List all configuration files
  2857. obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
  2858. // Get database information
  2859. obj.getDbStats = function (func) {
  2860. obj.stats = { c: 5 };
  2861. obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
  2862. obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2863. obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2864. obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2865. obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
  2866. }
  2867. // Correct database information of obj.getDbStats before returning it
  2868. function getDbStatsEx(data) {
  2869. var r = {};
  2870. if (data.recordTypes != null) { r = data.recordTypes; }
  2871. try { r.smbios = data['smbios'].count; } catch (ex) { }
  2872. try { r.power = data['power'].count; } catch (ex) { }
  2873. try { r.events = data['events'].count; } catch (ex) { }
  2874. try { r.serverstats = data['serverstats'].count; } catch (ex) { }
  2875. return r;
  2876. }
  2877. // Plugin operations
  2878. if (obj.pluginsActive) {
  2879. obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
  2880. obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
  2881. obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
  2882. obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
  2883. obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
  2884. obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
  2885. }
  2886. }
  2887. // Get all configuration files
  2888. obj.getAllConfigFiles = function (password, func) {
  2889. obj.GetAllType('cfile', function (err, docs) {
  2890. if (err != null) { func(null); return; }
  2891. var r = null;
  2892. for (var i = 0; i < docs.length; i++) {
  2893. var name = docs[i]._id.split('/')[1];
  2894. var data = obj.decryptData(password, docs[i].data);
  2895. if (data != null) { if (r == null) { r = {}; } r[name] = data; }
  2896. }
  2897. func(r);
  2898. });
  2899. }
  2900. func(obj); // Completed function setup
  2901. }
  2902. // Return a human readable string with current backup configuration
  2903. obj.getBackupConfig = function () {
  2904. var r = '', backupPath = parent.backuppath;
  2905. if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
  2906. var dbname = 'meshcentral';
  2907. if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
  2908. else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
  2909. else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
  2910. const currentDate = new Date();
  2911. const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
  2912. const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
  2913. const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
  2914. r += 'DB Name: ' + dbname + '\r\n';
  2915. r += 'DB Type: ' + ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'AceBase'][obj.databaseType] + '\r\n';
  2916. r += 'BackupPath: ' + backupPath + '\r\n';
  2917. r += 'newAutoBackupFile: ' + newAutoBackupFile + '\r\n';
  2918. r += 'newAutoBackupPath: ' + newAutoBackupPath + '\r\n';
  2919. if (parent.config.settings.autobackup == null) {
  2920. r += 'No Settings/AutoBackup\r\n';
  2921. } else {
  2922. if (parent.config.settings.autobackup.backupintervalhours != null) {
  2923. r += 'Backup Interval (Hours): ';
  2924. if (typeof parent.config.settings.autobackup.backupintervalhours != 'number') { r += 'Bad backupintervalhours type\r\n'; }
  2925. else { r += parent.config.settings.autobackup.backupintervalhours + '\r\n'; }
  2926. }
  2927. if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
  2928. r += 'Keep Last Backups (Days): ';
  2929. if (typeof parent.config.settings.autobackup.keeplastdaysbackup != 'number') { r += 'Bad keeplastdaysbackup type\r\n'; }
  2930. else { r += parent.config.settings.autobackup.keeplastdaysbackup + '\r\n'; }
  2931. }
  2932. if (parent.config.settings.autobackup.zippassword != null) {
  2933. r += 'ZIP Password: ';
  2934. if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
  2935. else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank, Backups will not be encrypted\r\n'; }
  2936. else { r += 'Set\r\n'; }
  2937. }
  2938. if (parent.config.settings.autobackup.mongodumppath != null) {
  2939. r += 'MongoDump Path: ';
  2940. if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
  2941. else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
  2942. }
  2943. if (parent.config.settings.autobackup.mysqldumppath != null) {
  2944. r += 'MySqlDump Path: ';
  2945. if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
  2946. else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
  2947. }
  2948. if (typeof parent.config.settings.autobackup.s3 == 'object') {
  2949. r += 'S3 Backups: Enabled\r\n';
  2950. }
  2951. if (typeof parent.config.settings.autobackup.webdav == 'object') {
  2952. r += 'WebDAV Backups: Enabled\r\n';
  2953. }
  2954. if (typeof parent.config.settings.autobackup.googledrive == 'object') {
  2955. r += 'Google Drive Backups: Enabled\r\n';
  2956. }
  2957. }
  2958. return r;
  2959. }
  2960. function buildSqlDumpCommand() {
  2961. var props = (obj.databaseType == 4) ? parent.args.mariadb : parent.args.mysql;
  2962. var mysqldumpPath = 'mysqldump';
  2963. if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
  2964. mysqldumpPath = parent.config.settings.autobackup.mysqldumppath;
  2965. }
  2966. var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
  2967. // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
  2968. cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
  2969. if (props.host) { cmd += ' -h ' + props.host; }
  2970. if (props.port) { cmd += ' -P ' + props.port; }
  2971. if (props.awsrds) { cmd += ' --single-transaction'; }
  2972. // SSL options different on mariadb/mysql
  2973. var sslOptions = '';
  2974. if (obj.databaseType == 4) {
  2975. if (props.ssl) {
  2976. sslOptions = ' --ssl';
  2977. if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
  2978. if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-verify-server-cert';
  2979. if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
  2980. if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
  2981. }
  2982. } else {
  2983. if (props.ssl) {
  2984. sslOptions = ' --ssl-mode=required';
  2985. if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
  2986. if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
  2987. else sslOptions += ' --ssl-mode=required';
  2988. if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
  2989. if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
  2990. }
  2991. }
  2992. cmd += sslOptions;
  2993. var dbname = (props.database) ? props.database : 'meshcentral';
  2994. cmd += ' ' + dbname
  2995. return cmd;
  2996. }
  2997. function buildMongoDumpCommand() {
  2998. const dburl = parent.args.mongodb;
  2999. var mongoDumpPath = 'mongodump';
  3000. if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
  3001. mongoDumpPath = parent.config.settings.autobackup.mongodumppath;
  3002. }
  3003. var cmd = '"' + mongoDumpPath + '"';
  3004. if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
  3005. return cmd;
  3006. }
  3007. // Check that the server is capable of performing a backup
  3008. obj.checkBackupCapability = function (func) {
  3009. if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); }
  3010. if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
  3011. // Check that we have access to MongoDump
  3012. var backupPath = parent.backuppath;
  3013. if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
  3014. try { parent.fs.mkdirSync(backupPath); } catch (ex) { }
  3015. if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; }
  3016. var cmd = buildMongoDumpCommand();
  3017. cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
  3018. const child_process = require('child_process');
  3019. child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
  3020. try {
  3021. if ((error != null) && (error != '')) {
  3022. if (parent.platform == 'win32') {
  3023. func(1, "Unable to find mongodump.exe, MongoDB database auto-backup will not be performed.");
  3024. } else {
  3025. func(1, "Unable to find mongodump, MongoDB database auto-backup will not be performed.");
  3026. }
  3027. } else {
  3028. func();
  3029. }
  3030. } catch (ex) { console.log(ex); }
  3031. });
  3032. } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
  3033. // Check that we have access to mysqldump
  3034. var backupPath = parent.backuppath;
  3035. if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
  3036. try { parent.fs.mkdirSync(backupPath); } catch (e) { }
  3037. if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; }
  3038. var cmd = buildSqlDumpCommand();
  3039. cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
  3040. const child_process = require('child_process');
  3041. child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
  3042. try {
  3043. if ((error != null) && (error != '')) {
  3044. if (parent.platform == 'win32') {
  3045. func(1, "Unable to find mysqldump.exe, MySQL/MariaDB database auto-backup will not be performed.");
  3046. } else {
  3047. func(1, "Unable to find mysqldump, MySQL/MariaDB database auto-backup will not be performed.");
  3048. }
  3049. } else {
  3050. func();
  3051. }
  3052. } catch (ex) { console.log(ex); }
  3053. });
  3054. } else {
  3055. func();
  3056. }
  3057. }
  3058. // MongoDB pending bulk read operation, perform fast bulk document reads.
  3059. function fileBulkReadCompleted(err, docs) {
  3060. // Send out callbacks with results
  3061. if (docs != null) {
  3062. for (var i in docs) {
  3063. if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
  3064. const id = docs[i]._id;
  3065. if (obj.filePendingGets[id] != null) {
  3066. for (var j in obj.filePendingGets[id]) {
  3067. if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
  3068. }
  3069. delete obj.filePendingGets[id];
  3070. }
  3071. }
  3072. }
  3073. // If there are not results, send out a null callback
  3074. for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
  3075. // Move on to process any more pending get operations
  3076. obj.filePendingGets = obj.filePendingGet;
  3077. obj.filePendingGet = null;
  3078. if (obj.filePendingGets != null) {
  3079. var findlist = [];
  3080. for (var i in obj.filePendingGets) { findlist.push(i); }
  3081. obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
  3082. }
  3083. }
  3084. // MongoDB pending bulk remove operation, perform fast bulk document removes.
  3085. function fileBulkRemoveCompleted(err) {
  3086. // Send out callbacks
  3087. for (var i in obj.filePendingRemoves) {
  3088. for (var j in obj.filePendingRemoves[i]) {
  3089. if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
  3090. }
  3091. }
  3092. // Move on to process any more pending get operations
  3093. obj.filePendingRemoves = obj.filePendingRemove;
  3094. obj.filePendingRemove = null;
  3095. if (obj.filePendingRemoves != null) {
  3096. obj.dbCounters.fileRemoveBulk++;
  3097. var findlist = [], count = 0;
  3098. for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
  3099. obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
  3100. }
  3101. }
  3102. // MongoDB pending bulk write operation, perform fast bulk document replacement.
  3103. function fileBulkWriteCompleted() {
  3104. // Callbacks
  3105. if (obj.filePendingCbs != null) {
  3106. for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
  3107. obj.filePendingCbs = null;
  3108. }
  3109. if (obj.filePendingSets != null) {
  3110. // Perform pending operations
  3111. obj.dbCounters.fileSetBulk++;
  3112. var ops = [];
  3113. obj.filePendingCbs = obj.filePendingCb;
  3114. obj.filePendingCb = null;
  3115. for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
  3116. obj.file.bulkWrite(ops, fileBulkWriteCompleted);
  3117. obj.filePendingSets = null;
  3118. } else {
  3119. // All done, no pending operations.
  3120. obj.filePendingSet = false;
  3121. }
  3122. }
  3123. // MongoDB pending bulk write operation, perform fast bulk document replacement.
  3124. function eventsFileBulkWriteCompleted() {
  3125. // Callbacks
  3126. if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
  3127. if (obj.eventsFilePendingSets != null) {
  3128. // Perform pending operations
  3129. obj.dbCounters.eventsSetBulk++;
  3130. var ops = [];
  3131. for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
  3132. obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
  3133. obj.eventsFilePendingCb = null;
  3134. obj.eventsFilePendingSets = null;
  3135. obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
  3136. } else {
  3137. // All done, no pending operations.
  3138. obj.eventsFilePendingSet = false;
  3139. }
  3140. }
  3141. // MongoDB pending bulk write operation, perform fast bulk document replacement.
  3142. function powerFileBulkWriteCompleted() {
  3143. // Callbacks
  3144. if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
  3145. if (obj.powerFilePendingSets != null) {
  3146. // Perform pending operations
  3147. obj.dbCounters.powerSetBulk++;
  3148. var ops = [];
  3149. for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
  3150. obj.powerFilePendingCbs = obj.powerFilePendingCb;
  3151. obj.powerFilePendingCb = null;
  3152. obj.powerFilePendingSets = null;
  3153. obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
  3154. } else {
  3155. // All done, no pending operations.
  3156. obj.powerFilePendingSet = false;
  3157. }
  3158. }
  3159. // Perform a server backup
  3160. obj.performingBackup = false;
  3161. obj.performBackup = function (func) {
  3162. try {
  3163. if (obj.performingBackup) return 1;
  3164. obj.performingBackup = true;
  3165. //console.log('Performing backup...');
  3166. var backupPath = parent.backuppath;
  3167. if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
  3168. try { parent.fs.mkdirSync(backupPath); } catch (e) { }
  3169. const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
  3170. const dburl = parent.args.mongodb;
  3171. const currentDate = new Date();
  3172. const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
  3173. const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
  3174. const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
  3175. if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
  3176. // Perform a MongoDump backup
  3177. const newBackupFile = 'mongodump-' + fileSuffix;
  3178. var newBackupPath = parent.path.join(backupPath, newBackupFile);
  3179. var cmd = buildMongoDumpCommand();
  3180. cmd += (dburl) ? ' --archive=\"' + newBackupPath + '.archive\"' :
  3181. ' --db=\"' + dbname + '\" --archive=\"' + newBackupPath + '.archive\"';
  3182. const child_process = require('child_process');
  3183. var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
  3184. try {
  3185. var mongoDumpSuccess = true;
  3186. backupProcess = null;
  3187. if ((error != null) && (error != '')) { mongoDumpSuccess = false; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); }
  3188. // Perform archive compression
  3189. var archiver = require('archiver');
  3190. var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
  3191. var archive = null;
  3192. if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
  3193. try {
  3194. archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
  3195. archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
  3196. if (func) { func('Creating encrypted ZIP'); }
  3197. } catch (ex) { // registering encryption failed, so create without encryption
  3198. archive = archiver('zip', { zlib: { level: 9 } });
  3199. if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
  3200. }
  3201. } else {
  3202. archive = archiver('zip', { zlib: { level: 9 } });
  3203. }
  3204. output.on('close', function () {
  3205. obj.performingBackup = false;
  3206. if (func) { if (mongoDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without mongodb database: ' + error); } }
  3207. obj.performCloudBackup(newAutoBackupPath + '.zip', func);
  3208. setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.archive', function () { }); } catch (ex) { console.log(ex); } }, 5000);
  3209. });
  3210. output.on('end', function () { });
  3211. output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3212. archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
  3213. archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3214. archive.pipe(output);
  3215. if (mongoDumpSuccess == true) { archive.file(newBackupPath + '.archive', { name: newBackupFile + '.archive' }); }
  3216. archive.directory(parent.datapath, 'meshcentral-data');
  3217. archive.finalize();
  3218. } catch (ex) { console.log(ex); }
  3219. });
  3220. } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
  3221. // Perform a MySqlDump backup
  3222. const newBackupFile = 'mysqldump-' + fileSuffix;
  3223. var newBackupPath = parent.path.join(backupPath, newBackupFile);
  3224. var cmd = buildSqlDumpCommand();
  3225. cmd += ' --result-file=\"' + newBackupPath + '.sql\"';
  3226. const child_process = require('child_process');
  3227. var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
  3228. try {
  3229. var sqlDumpSuccess = true;
  3230. backupProcess = null;
  3231. if ((error != null) && (error != '')) { sqlDumpSuccess = false; console.log('ERROR: Unable to perform MySQL/MariaDB backup: ' + error + '\r\n'); }
  3232. var archiver = require('archiver');
  3233. var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
  3234. var archive = null;
  3235. if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
  3236. try {
  3237. archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
  3238. archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
  3239. if (func) { func('Creating encrypted ZIP'); }
  3240. } catch (ex) { // registering encryption failed, so create without encryption
  3241. archive = archiver('zip', { zlib: { level: 9 } });
  3242. if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
  3243. }
  3244. } else {
  3245. archive = archiver('zip', { zlib: { level: 9 } });
  3246. }
  3247. output.on('close', function () {
  3248. obj.performingBackup = false;
  3249. if (func) { if (sqlDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without MySQL/MariaDB database: ' + error); } }
  3250. obj.performCloudBackup(newAutoBackupPath + '.zip', func);
  3251. setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.sql', function () { }); } catch (ex) { console.log(ex); } }, 5000);
  3252. });
  3253. output.on('end', function () { });
  3254. output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3255. archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
  3256. archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3257. archive.pipe(output);
  3258. if (sqlDumpSuccess == true) { archive.file(newBackupPath + '.sql', { name: newBackupFile + '.sql' }); }
  3259. archive.directory(parent.datapath, 'meshcentral-data');
  3260. archive.finalize();
  3261. } catch (ex) { console.log(ex); }
  3262. });
  3263. } else {
  3264. // Perform a NeDB backup
  3265. var archiver = require('archiver');
  3266. var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
  3267. var archive = null;
  3268. if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
  3269. try {
  3270. archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
  3271. archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
  3272. if (func) { func('Creating encrypted ZIP'); }
  3273. } catch (ex) { // registering encryption failed, so create without encryption
  3274. archive = archiver('zip', { zlib: { level: 9 } });
  3275. if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
  3276. }
  3277. } else {
  3278. archive = archiver('zip', { zlib: { level: 9 } });
  3279. }
  3280. output.on('close', function () { obj.performingBackup = false; if (func) { func('Auto-backup completed.'); } obj.performCloudBackup(newAutoBackupPath + '.zip', func); });
  3281. output.on('end', function () { });
  3282. output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3283. archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
  3284. archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
  3285. archive.pipe(output);
  3286. archive.directory(parent.datapath, 'meshcentral-data');
  3287. archive.finalize();
  3288. }
  3289. // Remove old backups
  3290. if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
  3291. var cutoffDate = new Date();
  3292. cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
  3293. parent.fs.readdir(parent.backuppath, function (err, dir) {
  3294. try {
  3295. if ((err == null) && (dir.length > 0)) {
  3296. for (var i in dir) {
  3297. var name = dir[i];
  3298. if (name.startsWith('meshcentral-autobackup-') && name.endsWith('.zip')) {
  3299. var timex = name.substring(23, name.length - 4).split('-');
  3300. if (timex.length == 5) {
  3301. var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
  3302. if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } }
  3303. }
  3304. }
  3305. }
  3306. }
  3307. } catch (ex) { console.log(ex); }
  3308. });
  3309. }
  3310. } catch (ex) { console.log(ex); }
  3311. return 0;
  3312. }
  3313. // Perform cloud backup
  3314. obj.performCloudBackup = function (filename, func) {
  3315. // WebDAV Backup
  3316. if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
  3317. const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
  3318. // Fetch the folder name
  3319. var webdavfolderName = 'MeshCentral-Backups';
  3320. if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; }
  3321. // Clean up our WebDAV folder
  3322. function performWebDavCleanup(client) {
  3323. if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
  3324. var directoryItems = client.getDirectoryContents(webdavfolderName);
  3325. directoryItems.then(
  3326. function (files) {
  3327. for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
  3328. files.sort(xdateTimeSort);
  3329. while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) {
  3330. client.deleteFile(files.shift().filename).then(function (state) {
  3331. if (func) { func('WebDAV file deleted.'); }
  3332. }).catch(function (err) {
  3333. if (func) { func('WebDAV (deleteFile) error: ' + err); }
  3334. });
  3335. }
  3336. }
  3337. ).catch(function (err) {
  3338. if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
  3339. });
  3340. }
  3341. }
  3342. // Upload to the WebDAV folder
  3343. function performWebDavUpload(client, filepath) {
  3344. require('fs').stat(filepath, function(err,stat){
  3345. var fileStream = require('fs').createReadStream(filepath);
  3346. fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } })
  3347. fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } })
  3348. fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath), { headers: { "Content-Length": stat.size } }));
  3349. if (func) { func('Uploading using WebDAV...'); }
  3350. });
  3351. }
  3352. if (func) { func('Attempting WebDAV upload...'); }
  3353. const { createClient } = require('webdav');
  3354. const client = createClient(parent.config.settings.autobackup.webdav.url, {
  3355. username: parent.config.settings.autobackup.webdav.username,
  3356. password: parent.config.settings.autobackup.webdav.password,
  3357. maxContentLength: Infinity,
  3358. maxBodyLength: Infinity
  3359. });
  3360. client.exists(webdavfolderName).then(function(a){
  3361. if(a){
  3362. performWebDavCleanup(client);
  3363. performWebDavUpload(client, filename);
  3364. }else{
  3365. client.createDirectory(webdavfolderName, {recursive: true}).then(function (a) {
  3366. if (func) { func('WebDAV folder created'); }
  3367. performWebDavUpload(client, filename);
  3368. }).catch(function (err) {
  3369. if (func) { func('WebDAV (createDirectory) error: ' + err); }
  3370. });
  3371. }
  3372. }).catch(function (err) {
  3373. if (func) { func('WebDAV (exists) error: ' + err); }
  3374. });
  3375. }
  3376. // Google Drive Backup
  3377. if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
  3378. obj.Get('GoogleDriveBackup', function (err, docs) {
  3379. if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
  3380. if (func) { func('Attempting Google Drive upload...'); }
  3381. const {google} = require('googleapis');
  3382. const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
  3383. oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
  3384. oAuth2Client.setCredentials(docs[0].token);
  3385. const drive = google.drive({ version: 'v3', auth: oAuth2Client });
  3386. const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
  3387. // Called once we know our folder id, clean up and upload a backup.
  3388. var useGoogleDrive = function (folderid) {
  3389. // List files to see if we need to delete older ones
  3390. if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
  3391. drive.files.list({
  3392. q: 'trashed = false and \'' + folderid + '\' in parents',
  3393. fields: 'nextPageToken, files(id, name, size, createdTime)',
  3394. }, function (err, res) {
  3395. if (err) {
  3396. console.log('GoogleDrive (files.list) error: ' + err);
  3397. if (func) { func('GoogleDrive (files.list) error: ' + err); }
  3398. return;
  3399. }
  3400. // Delete any old files if more than 10 files are present in the backup folder.
  3401. res.data.files.sort(createdTimeSort);
  3402. while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
  3403. });
  3404. }
  3405. //console.log('Uploading...');
  3406. if (func) { func('Uploading to Google Drive...'); }
  3407. // Upload the backup
  3408. drive.files.create({
  3409. requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
  3410. media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
  3411. }, function (err, res) {
  3412. if (err) {
  3413. console.log('GoogleDrive (files.create) error: ' + err);
  3414. if (func) { func('GoogleDrive (files.create) error: ' + err); }
  3415. return;
  3416. }
  3417. //console.log('Upload done.');
  3418. if (func) { func('Google Drive upload completed.'); }
  3419. });
  3420. }
  3421. // Fetch the folder name
  3422. var folderName = 'MeshCentral-Backups';
  3423. if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
  3424. // Find our backup folder, create one if needed.
  3425. drive.files.list({
  3426. q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
  3427. fields: 'nextPageToken, files(id, name)',
  3428. }, function (err, res) {
  3429. if (err) {
  3430. console.log('GoogleDrive error: ' + err);
  3431. if (func) { func('GoogleDrive error: ' + err); }
  3432. return;
  3433. }
  3434. if (res.data.files.length == 0) {
  3435. // Create a folder
  3436. drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
  3437. if (err) {
  3438. console.log('GoogleDrive (folder.create) error: ' + err);
  3439. if (func) { func('GoogleDrive (folder.create) error: ' + err); }
  3440. return;
  3441. }
  3442. useGoogleDrive(file.data.id);
  3443. });
  3444. } else { useGoogleDrive(res.data.files[0].id); }
  3445. });
  3446. });
  3447. }
  3448. // S3 Backup
  3449. if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
  3450. var s3folderName = 'MeshCentral-Backups';
  3451. if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
  3452. // Construct the config object
  3453. var accessKey = parent.config.settings.autobackup.s3.accesskey,
  3454. secretKey = parent.config.settings.autobackup.s3.secretkey,
  3455. endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
  3456. port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
  3457. useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
  3458. bucketName = parent.config.settings.autobackup.s3.bucketname,
  3459. pathPrefix = s3folderName,
  3460. threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
  3461. fileToUpload = filename;
  3462. // Create a MinIO client
  3463. const Minio = require('minio');
  3464. var minioClient = new Minio.Client({
  3465. endPoint: endpoint,
  3466. port: port,
  3467. useSSL: useSsl,
  3468. accessKey: accessKey,
  3469. secretKey: secretKey
  3470. });
  3471. // List objects in the specified bucket and path prefix
  3472. var listObjectsPromise = new Promise(function(resolve, reject) {
  3473. var items = [];
  3474. var stream = minioClient.listObjects(bucketName, pathPrefix, true);
  3475. stream.on('data', function(item) {
  3476. if (!item.name.endsWith('/')) { // Exclude directories
  3477. items.push(item);
  3478. }
  3479. });
  3480. stream.on('end', function() {
  3481. resolve(items);
  3482. });
  3483. stream.on('error', function(err) {
  3484. reject(err);
  3485. });
  3486. });
  3487. listObjectsPromise.then(function(objects) {
  3488. // Count the number of files
  3489. var fileCount = objects.length;
  3490. // Return if no files to carry on uploading
  3491. if (fileCount === 0) { return Promise.resolve(); }
  3492. // Sort the files by LastModified date (oldest first)
  3493. objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
  3494. // Check if the threshold is zero and return if
  3495. if (threshold === 0) { return Promise.resolve(); }
  3496. // Check if the number of files exceeds the threshold (maxfiles) is 0
  3497. if (fileCount >= threshold) {
  3498. // Calculate how many files need to be deleted to make space for the new file
  3499. var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
  3500. if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
  3501. // Create an array of promises for deleting files
  3502. var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
  3503. return new Promise(function(resolve, reject) {
  3504. minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
  3505. if (err) {
  3506. reject(err);
  3507. } else {
  3508. if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
  3509. resolve();
  3510. }
  3511. });
  3512. });
  3513. });
  3514. // Wait for all deletions to complete
  3515. return Promise.all(deletePromises);
  3516. } else {
  3517. return Promise.resolve(); // No deletion needed
  3518. }
  3519. }).then(function() {
  3520. // Determine the upload path by combining the pathPrefix with the filename
  3521. var fileName = require('path').basename(fileToUpload);
  3522. var uploadPath = require('path').join(pathPrefix, fileName);
  3523. // Upload a new file
  3524. var uploadPromise = new Promise(function(resolve, reject) {
  3525. if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
  3526. minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
  3527. if (err) {
  3528. reject(err);
  3529. } else {
  3530. if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
  3531. resolve(etag);
  3532. }
  3533. });
  3534. });
  3535. return uploadPromise;
  3536. }).catch(function(error) {
  3537. if (func) { func('Error managing files in S3: ' + error); }
  3538. });
  3539. }
  3540. }
  3541. // Transfer NeDB data into the current database
  3542. obj.nedbtodb = function (func) {
  3543. var nedbDatastore = null;
  3544. try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
  3545. if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
  3546. var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
  3547. // If a DB encryption key is provided, perform database encryption
  3548. if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
  3549. // Hash the database password into a AES256 key and setup encryption and decryption.
  3550. var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
  3551. datastoreOptions.afterSerialization = function (plaintext) {
  3552. const iv = parent.crypto.randomBytes(16);
  3553. const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
  3554. var ciphertext = aes.update(plaintext);
  3555. ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
  3556. return ciphertext.toString('base64');
  3557. }
  3558. datastoreOptions.beforeDeserialization = function (ciphertext) {
  3559. const ciphertextBytes = Buffer.from(ciphertext, 'base64');
  3560. const iv = ciphertextBytes.slice(0, 16);
  3561. const data = ciphertextBytes.slice(16);
  3562. const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
  3563. var plaintextBytes = Buffer.from(aes.update(data));
  3564. plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
  3565. return plaintextBytes.toString();
  3566. }
  3567. }
  3568. // Setup all NeDB collections
  3569. var nedbfile = new nedbDatastore(datastoreOptions);
  3570. var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
  3571. var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
  3572. var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
  3573. // Transfered record counts
  3574. var normalRecordsTransferCount = 0;
  3575. var eventRecordsTransferCount = 0;
  3576. var powerRecordsTransferCount = 0;
  3577. var statsRecordsTransferCount = 0;
  3578. obj.pendingTransfer = 0;
  3579. // Transfer the data from main database
  3580. nedbfile.find({}, function (err, docs) {
  3581. if ((err == null) && (docs.length > 0)) {
  3582. performTypedRecordDecrypt(docs)
  3583. for (var i in docs) {
  3584. obj.pendingTransfer++;
  3585. normalRecordsTransferCount++;
  3586. obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
  3587. }
  3588. }
  3589. // Transfer events
  3590. nedbeventsfile.find({}, function (err, docs) {
  3591. if ((err == null) && (docs.length > 0)) {
  3592. for (var i in docs) {
  3593. obj.pendingTransfer++;
  3594. eventRecordsTransferCount++;
  3595. obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
  3596. }
  3597. }
  3598. // Transfer power events
  3599. nedbpowerfile.find({}, function (err, docs) {
  3600. if ((err == null) && (docs.length > 0)) {
  3601. for (var i in docs) {
  3602. obj.pendingTransfer++;
  3603. powerRecordsTransferCount++;
  3604. obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
  3605. }
  3606. }
  3607. // Transfer server stats
  3608. nedbserverstatsfile.find({}, function (err, docs) {
  3609. if ((err == null) && (docs.length > 0)) {
  3610. for (var i in docs) {
  3611. obj.pendingTransfer++;
  3612. statsRecordsTransferCount++;
  3613. obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
  3614. }
  3615. }
  3616. // Only exit when all the records are stored.
  3617. setInterval(function () {
  3618. if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
  3619. }, 200)
  3620. });
  3621. });
  3622. });
  3623. });
  3624. }
  3625. function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
  3626. // Called when a node has changed
  3627. function dbNodeChange(nodeChange, added) {
  3628. if (parent.webserver == null) return;
  3629. common.unEscapeLinksFieldName(nodeChange.fullDocument);
  3630. const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
  3631. parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
  3632. }
  3633. // Called when a device group has changed
  3634. function dbMeshChange(meshChange, added) {
  3635. if (parent.webserver == null) return;
  3636. common.unEscapeLinksFieldName(meshChange.fullDocument);
  3637. const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
  3638. // Update the mesh object in memory
  3639. const mmesh = parent.webserver.meshes[mesh._id];
  3640. if (mmesh != null) {
  3641. // Update an existing device group
  3642. for (var i in mesh) { mmesh[i] = mesh[i]; }
  3643. for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
  3644. } else {
  3645. // Device group not present, create it.
  3646. parent.webserver.meshes[mesh._id] = mesh;
  3647. }
  3648. // Send the mesh update
  3649. if (mesh.deleted) { mesh.action = 'deletemesh'; } else { mesh.action = (added ? 'createmesh' : 'meshchange'); }
  3650. mesh.meshid = mesh._id;
  3651. mesh.nolog = 1;
  3652. delete mesh.type;
  3653. delete mesh._id;
  3654. parent.DispatchEvent(['*', mesh.meshid], obj, parent.webserver.CloneSafeMesh(mesh));
  3655. }
  3656. // Called when a user account has changed
  3657. function dbUserChange(userChange, added) {
  3658. if (parent.webserver == null) return;
  3659. common.unEscapeLinksFieldName(userChange.fullDocument);
  3660. const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
  3661. // Update the user object in memory
  3662. const muser = parent.webserver.users[user._id];
  3663. if (muser != null) {
  3664. // Update an existing user
  3665. for (var i in user) { muser[i] = user[i]; }
  3666. for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
  3667. } else {
  3668. // User not present, create it.
  3669. parent.webserver.users[user._id] = user;
  3670. }
  3671. // Send the user update
  3672. var targets = ['*', 'server-users', user._id];
  3673. if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
  3674. parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
  3675. }
  3676. // Called when a user group has changed
  3677. function dbUGrpChange(ugrpChange, added) {
  3678. if (parent.webserver == null) return;
  3679. common.unEscapeLinksFieldName(ugrpChange.fullDocument);
  3680. const usergroup = ugrpChange.fullDocument;
  3681. // Update the user group object in memory
  3682. const uusergroup = parent.webserver.userGroups[usergroup._id];
  3683. if (uusergroup != null) {
  3684. // Update an existing user group
  3685. for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
  3686. for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
  3687. } else {
  3688. // Usergroup not present, create it.
  3689. parent.webserver.userGroups[usergroup._id] = usergroup;
  3690. }
  3691. // Send the user group update
  3692. usergroup.action = (added ? 'createusergroup' : 'usergroupchange');
  3693. usergroup.ugrpid = usergroup._id;
  3694. usergroup.nolog = 1;
  3695. delete usergroup.type;
  3696. delete usergroup._id;
  3697. parent.DispatchEvent(['*', usergroup.ugrpid], obj, usergroup);
  3698. }
  3699. function dbMergeSqlArray(arr) {
  3700. var x = '';
  3701. for (var i in arr) { if (x != '') { x += ','; } x += '"' + arr[i] + '"'; }
  3702. return x;
  3703. }
  3704. return obj;
  3705. };