UNPKG

total5

Version:
2,110 lines (1,696 loc) 72.9 kB
// Total.js framework // The MIT License // Copyright 2012-2025 (c) Peter Širka <petersirka@gmail.com> 'use strict'; const NODE_MODULES = { buffer: 1, child_process: 1, process: 1, fs: 1, events: 1, http: 1, https: 1, http2: 1, util: 1, net: 1, os: 1, path: 1, punycode: 1, readline: 1, repl: 1, stream: 1, string_decoder: 1, tls: 1, trace_events: 1, tty: 1, dgram: 1, url: 1, v8: 1, vm: 1, wasi: 1, worker_threads: 1, zlib: 1, crypto: 1, dns: 1 }; const EMPTYOBJECT = {}; const EMPTYARRAY = []; const REG_SKIPERRORS = /epipe|invalid\sdistance|err_ipc_channel_closed/i; const REG_HTTPHTTPS = /^(http|https):\/\//i; const SOCKETWINDOWS = '\\\\?\\pipe'; const CLUSTER_STATS = { TYPE: 'stats' }; const IGNORE_AUDIT = { password: 1, token: 1, accesstoken: 1, access_token: 1, pin: 1 }; var CONCAT = new Array(2); Object.freeze(EMPTYOBJECT); Object.freeze(EMPTYARRAY); // Globals global.F = global.Total = {}; global.DEBUG = true; global.CONF = {}; global.REPO = {}; global.MAIN = {}; global.TEMP = {}; global.FUNC = {}; global.EMPTYOBJECT = EMPTYOBJECT; global.EMPTYARRAY = EMPTYARRAY; global.NOW = new Date(); global.DEF = {}; (function(F) { F.id = ''; F.clusterid = ''; F.is5 = F.version = 5014; F.isBundle = false; F.isLoaded = false; F.version_header = '5'; F.version_node = process.version + ''; F.EMPTYOBJECT = EMPTYOBJECT; F.EMPTYARRAY = EMPTYARRAY; F.resources = {}; // Loaded resources F.connections = {}; // WebSocket connections F.schedules = {}; // Registered schedulers F.modules = {}; F.plugins = {}; F.actions = {}; F.apiservices = {}; F.processing = {}; F.transformations = {}; F.consumption = {}; F.flowstreams = {}; F.filestorages = {}; F.jsonschemas = {}; F.querybuilders = {}; F.openclients = {}; F.nodemodules = {}; F.workers = {}; F.config = CONF; F.def = DEF; F.repo = REPO; F.timeouts = []; F.errors = []; F.paused = []; F.crons = []; F.pypelines = []; F.internal = { ticks: 0, counter: 0, uid: 1, interval: null // setInterval identifier }; F.routes = { fallback: {}, virtual: {}, api: {}, routes: [], routescache: {}, websockets: [], websocketscache: {}, files: [], filescache: {}, timeout: null, middleware: {}, imagesmiddleware: {}, proxies: [] }; // Internal cache F.temporary = { path: {}, actions: {}, cache: {}, notfound: {}, processing: {}, views: {}, viewscache: [], directories: {}, versions: {}, dependencies: {}, // temporary for module dependencies other: {}, files: {}, cryptokeys: {}, // for crypto keys internal: {}, // controllers/modules names for the routing ready: {}, ddos: {}, service: { redirect: 0, request: 0, file: 0, usage: 0 }, pending: [], tmp: {}, merged: {}, minified: {}, tmsblocked: {}, dnscache: {}, blocked: {}, bans: {}, calls: {}, utils: {}, mail: {}, images: {}, querybuilders: {}, templates: {}, smtp: {}, datetime: {} // date time formatters }; // Internal stats F.stats = { compilation: 0, error: 0, performance: { publish: 0, subscribe: 0, calls: 0, download: 0, upload: 0, request: 0, message: 0, file: 0, open: 0, online: 0, usage: 0, mail: 0, dbrm: 0, dbwm: 0, external: 0 }, other: { websocketping: 0, websocketcleaner: 0, obsolete: 0, mail: 0 }, request: { request: 0, external: 0, pending: 0, web: 0, xhr: 0, file: 0, websocket: 0, get: 0, options: 0, head: 0, post: 0, put: 0, patch: 0, upload: 0, schema: 0, operation: 0, blocked: 0, 'delete': 0, mobile: 0, desktop: 0, size: 0 }, response: { ddos: 0, html: 0, xml: 0, json: 0, websocket: 0, timeout: 0, custom: 0, binary: 0, pipe: 0, file: 0, image: 0, destroy: 0, stream: 0, streaming: 0, text: 0, empty: 0, redirect: 0, forward: 0, proxy: 0, notmodified: 0, sse: 0, errorbuilder: 0, error400: 0, error401: 0, error403: 0, error404: 0, error409: 0, error431: 0, error500: 0, error501: 0, error503: 0, size: 0 } }; F.path = {}; F.path.root = path => path ? F.path.$join(F.directory, path) : F.directory; F.path.logs = path => path ? F.path.$join(F.temporary.directories.logs, path) : F.temporary.directories.logs; F.path.scripts = path => path ? F.path.$join(F.temporary.directories.scripts, path) : F.temporary.directories.scripts; F.path.public = path => path ? F.path.$join(F.temporary.directories.public, path) : F.temporary.directories.public; F.path.private = path => path ? F.path.$join(F.temporary.directories.private, path) : F.temporary.directories.private; F.path.databases = path => path ? F.path.$join(F.temporary.directories.databases, path) : F.temporary.directories.databases; F.path.plugins = path => path ? F.path.$join(F.temporary.directories.plugins, path) : F.temporary.directories.plugins; F.path.templates = path => path ? F.path.$join(F.temporary.directories.templates, path) : F.temporary.directories.templates; F.path.flowstreams = path => path ? F.path.$join(F.temporary.directories.flowstreams, path) : F.temporary.directories.flowstreams; F.path.modules = path => path ? F.path.$join(F.temporary.directories.modules, path) : F.temporary.directories.modules; F.path.directory = (type, path) => path ? F.path.$join(F.temporary.directories[type], path) : F.temporary.directories[type]; F.path.tmp = F.path.temp = path => path ? F.path.$join(F.temporary.directories.tmp, path) : F.temporary.directories.tmp; F.path.exists = (path, callback) => callback ? (F.Fs.lstat(path, (err, stats) => callback(err ? false : true, stats ? stats.size : 0, stats ? stats.isFile() : false))) : new Promise(resolve => F.path.exists(path, resolve)); F.path.$join = function(directory, path) { var key = '$' + directory; if (!F.temporary.path[key]) F.path.verify(directory); return F.Path.join(directory, path || ''); }; F.path.route = function(path, directory = 'root') { // Absolute if (path[0] === '~') return path.substring(1); // Plugins if (path[0] === '_') { let tmp = path.substring(1); let index = tmp.indexOf('/', 1); return F.path.plugins(tmp.substring(0, index) + (directory === 'root' ? '' : ('/' + directory)) + '/' + tmp.substring(index + 2)); } return F.path[directory](path); }; F.path.unlink = unlink; F.path.rmdir = rmdir; F.path.verify = function(path) { var key = '$verify' + path; if (F.temporary.path[key]) return; F.path.mkdir(path); F.temporary.path[key] = true; }; F.path.mkdir = function(path) { try { if (!pathexists(path)) F.Fs.mkdirSync(path, { recursive: true }); } catch {} }; F.virtualpath = function(path) { return F.config.$root ? (F.config.$root + (path[0] === '/' ? path.substring(1) : path)) : path; }; })(global.F); function mailsendforce(message) { message.send2(); } function pathexists(filename, isfile) { try { var val = F.Fs.statSync(filename); return val ? (isfile ? val.isFile() : true) : false; } catch { return false; } } function rmdir(arr, callback) { if (typeof(arr) === 'string') arr = [arr]; if (!arr.length) { callback && callback(); return; } var path = arr.shift(); if (path) { F.TUtils.ls(path, function(files, directories) { directories.reverse(); directories.push(path); files.wait((item, next) => F.Fs.unlink(item, next), function() { directories.wait((item, next) => F.Fs.rmdir(item, next), () => rmdir(arr, callback)); }); }); } else if (callback) callback(); } function unlink(arr, callback) { if (typeof(arr) === 'string') arr = [arr]; if (!arr.length) { callback && callback(); return; } var filename = arr.shift(); if (filename) F.Fs.unlink(filename, () => unlink(arr, callback)); else if (callback) callback(); } (function(CONF) { CONF.name = 'Total.js'; CONF.version = '1.0.0'; CONF.author = ''; CONF.secret = F.syshash; CONF.secret_encryption = ''; CONF.secret_totalapi = ''; CONF.secret_csrf = ''; CONF.secret_tapi = ''; CONF.secret_tms = ''; // New internal configuration CONF.$root = ''; CONF.$cors = ''; // hostnames separated by comma CONF.$api = '/api/'; // a default API endpoint CONF.$sourcemap = true; CONF.$httpreqlimit = 0; // request limit per ip CONF.$httpcompress = true; CONF.$httpetag = ''; CONF.$httpmaxsize = 256; // kB CONF.$httprangebuffer = 5120; // 5 MB CONF.$httptimeout = 5; // 5 seconds CONF.$httpfiles = { flac: true, jpg: true, jpeg: true, png: true, gif: true, ico: true, wasm: true, js: true, mjs: true, css: true, txt: true, xml: true, woff: true, woff2: true, otf: true, ttf: true, eot: true, svg: true, zip: true, rar: true, pdf: true, docx: true, xlsx: true, doc: true, xls: true, html: true, htm: true, appcache: true, manifest: true, map: true, ogv: true, ogg: true, mp4: true, mp3: true, webp: true, webm: true, swf: true, package: true, json: true, ui: true, md: true, m4v: true, jsx: true, heif: true, heic: true, ics: true, ts: true, m3u8: true, wav: true, xsd: true, xsl: true, xslt: true, ipynb: true, ijsnb: true, log: true }; CONF.$httpchecktypes = true; // for multipart data only CONF.$httpmaxage = 60; // in seconds CONF.$httpmaxkeys = 33; CONF.$httpmaxkey = 25; CONF.$blacklist = ''; CONF.$xpoweredby = 'Total.js'; CONF.$maxopenfiles = 100; CONF.$minifyjs = true; CONF.$minifycss = true; CONF.$minifyhtml = true; CONF.$localize = true; CONF.$port = 'auto'; CONF.$ip = '0.0.0.0'; CONF.$unixsocket = ''; CONF.$timezone = 'utc'; CONF.$insecure = false; CONF.$performance = false; CONF.$filtererrors = true; CONF.$cleartemp = true; CONF.$customtitles = false; CONF.$version = ''; CONF.$clearcache = 10; CONF.$imageconverter = 'gm'; CONF.$imagememory = 0; // disabled because e.g. GM v1.3.32 throws some error about the memory CONF.$stats = true; CONF.$npmcache = '/var/www/.npm'; CONF.$python = 'python3'; CONF.$wsmaxsize = 256; // kB CONF.$wscompress = true; CONF.$wsencodedecode = false; CONF.$wsmaxlatency = 2000; CONF.$proxytimeout = 5; // 5 seconds // CONF.$proxyrequest = ''; CONF.$cookiesamesite = 'Lax'; CONF.$cookiesecure = false; CONF.$csrfexpiration = '30 minutes'; CONF.$tapi = true; CONF.$tapiurl = 'eu'; CONF.$tapimail = false; CONF.$tapilogger = false; CONF.$imprint = true; CONF.$tms = false; CONF.$tmsmaxsize = 256; // kB CONF.$tmsurl = '/$tms/'; CONF.$tmsclearblocked = 60; // in minutes process.env.TZ = CONF.$timezone; })(global.CONF); (function(DEF) { DEF.onSuccess = function(value) { return { success: true, value: value }; }; DEF.onCSRFcreate = function(ctrl) { var data = [ctrl.ip, (ctrl.headers['user-agent'] || '').hash(true), NOW.add(F.config.$csrfexpiration).getTime()]; return F.config.secret_csrf ? JSON.stringify(data).encrypt(F.config.secret_csrf) : ''; }; DEF.onCSRFcheck = function(ctrl) { if (F.config.secret_csrf) { var token = ctrl.headers['x-csrf-token'] || ctrl.query.csrf; var is = false; if (token && token.length > 10) { var data = token.decrypt(F.config.secret_csrf); if (data) data = data.parseJSON(); is = data && data[0] === ctrl.ip && data[2] >= NOW.getTime() && data[1] === (ctrl.headers['user-agent'] || '').hash(true) ? true : false; } return is; } return true; }; DEF.onAudit = function(name, data) { F.stats.performance.open++; data.dtcreated = NOW = new Date(); F.Fs.appendFile(F.path.logs((name || 'audit') + '.log'), JSON.stringify(data) + '\n', NOOP); }; DEF.onMail = function(email, subject, body, callback, reply) { var tmp; if (typeof(callback) === 'string') { tmp = reply; reply = callback; callback = tmp; } var msg = new F.TMail.Message(subject, body); if (email.indexOf(',') !== -1) email = email.split(',').trim(); if (email instanceof Array) { for (let m of email) msg.to(m); } else msg.to(email); msg.from(F.config.mail_from || F.config.smtp.from || F.config.smtp.user, F.config.mail_from_name || F.config.smtp.name); callback && msg.callback(callback); if (reply) msg.reply(reply); else { tmp = F.config.mail_reply; if (tmp && tmp.length > 3) msg.reply(tmp); } tmp = F.config.mail_cc; if (tmp && tmp.length > 3) msg.cc(tmp); tmp = F.config.mail_bcc; if (tmp && tmp.length > 3) msg.bcc(tmp); msg.$sending = setImmediate(mailsendforce, msg); return msg; }; DEF.onViewCompile = function(name, html) { return html; }; DEF.onError = function(err, name, url) { NOW = new Date(); if (err instanceof F.TBuilders.ErrorBuilder) { if (!name) name = err[0].name; } else if (!name && err.name) name = err.name; err = err.toString(); console.log('ERROR ======= ' + (NOW.format('yyyy-MM-dd HH:mm:ss')) + ': ' + (name ? name + ' ---> ' : '') + err + (url ? (' (' + url + ')') : ''), err.stack ? err.stack : ''); var obj = { error: err, name: name, url: url, date: NOW }; if (F.errors.push(obj) > 10) F.errors.shift(); F.$events.error && F.emit('error', obj); F.stats.error++; }; DEF.helpers = {}; DEF.currencies = {}; DEF.parsers = {}; DEF.parsers.json = value => value.parseJSON(true); DEF.parsers.urlencoded = value => value.parseEncoded(); // Unused DEF.parsers.xml = value => value.parseXML(true); // Validators DEF.validators = { email: new RegExp('^[a-zA-Z0-9-_.+]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'), url: /^http(s)?:\/\/[^,{}\\]*$/i, phone: /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,8}$/im, zip: /^[0-9a-z\-\s]{3,20}$/i, uid: /^\d{14,}[a-z]{3}[01]{1}|^\d{9,14}[a-z]{2}[01]{1}a|^\d{4,18}[a-z]{2}\d{1}[01]{1}b|^[0-9a-f]{4,18}[a-z]{2}\d{1}[01]{1}c|^[0-9a-z]{4,18}[a-z]{2}\d{1}[01]{1}d|^[0-9a-zA-Z]{5,10}\d{1}[01]{1}f|^[0-9a-zA-Z]{10}[A-J]{1}r$/, xss: /<.*>/, sqlinjection: /'(''|[^'])*'|\b(ALTER|CREATE|DELETE|DROP|EXEC(UTE){0,1}|INSERT( +INTO){0,1}|MERGE|SELECT|UPDATE|UNION( +ALL){0,1})\b/ }; })(global.DEF); F.loadconfig = function(value) { let cfg = F.TUtils.parseConfig(value); let smtp = null; let isapi = false; for (let key in cfg) { let val = cfg[key]; let tmp; switch (key) { case 'totalapi': if (typeof(val) === 'string' && val.length > 5) key = 'secret_totalapi'; else key = '$tapi'; break; case '$tms': break; case 'smtp': case 'mail': if (typeof(val) === 'string') val = new Function('return ' + val)(); smtp = val || {}; if (!smtp.server) smtp.server = smtp.smtp || smtp.host || smtp.hostname || smtp.url; break; case 'mail_smtp': if (!smtp) smtp = {}; smtp.server = val; break; case 'mail_smtp_options': if (typeof(val) === 'string') val = new Function('return ' + val)(); if (!smtp) smtp = {}; for (let k in val) smtp[k] = val[k]; break; case '$cryptoiv': cfg[key] = val ? Buffer.from(val, (/[A-Z=\/+]/).test(val) ? 'base64' : 'hex') : null; break; case '$root': cfg[key] = val ? F.TUtils.normalize(val) : ''; break; case 'mail_from': break; case '$port': cfg[key] = +val; break; case '$timezone': process.env.TZ = val; break; case '$httpfiles': tmp = val.split(',').trim(); for (var m of tmp) F.config.$httpfiles[m] = true; continue; case '$api': F.config.$apibk = val || ''; break; } F.config[key] = cfg[key]; } if (cfg.$root) { if (!F.config.$apibk) F.config.$apibk = F.config.$api; F.config.$api = cfg.$root + F.config.$apibk.substring(1); } if (!F.config.secret_uid) F.config.secret_uid = (F.config.name).crc32(true) + ''; if (F.config.$performance) F.Http.globalAgent.maxSockets = 9999; if (!F.config.$httpetag) F.config.$httpetag = (F.config.version || '').toString().replace(/\.|\s/g, ''); if (smtp) F.config.smtp = smtp; process.env.NODE_TLS_REJECT_UNAUTHORIZED = F.config.$insecure ? '0' : '1'; F.logger(F.config.$logger == true); F.dir(); F.emit('$cors'); F.emit('$tms'); F.emit('$reconfigure'); }; F.loadresource = function(name, value) { if (value == null) { // download RESTBuilder.GET(name).callback(function(err, response) { if (err) throw new Error(err.toString()); if (response && (response instanceof Array || typeof(response) === 'object')) { if (response instanceof Array) { for (let item of response) F.loadresource(item.id || item.key || item.code || item.language, item.value || item.name || item.text || item.body); } else { for (let key in response) F.loadresource(key, response[key]); } } }); return; } var lines = value.split('\n'); var response = {}; for (let line of lines) { if (!line || line[0] === '#' || line.substring(0, 2) === '//') continue; let index = line.indexOf(':'); if (index === -1) { index = line.indexOf('='); if (index === -1) continue; } response[line.substring(0, index).trim()] = line.substring(index + 2).trim(); } F.resources[name] = response; }; F.loadenv = function(value) { var obj = value.parseEnv(); for (var key in obj) { if (!process.env.hasOwnProperty(key)) process.env[key] = obj[key]; } }; F.translate = function(language, value) { var index = -1; while (true) { index = value.indexOf('@(', index); if (index === -1) break; var counter = 0; for (let i = index + 2; i < value.length; i++) { var c = value[i]; if (c == '(') { counter++; } else if (c === ')') { if (counter) { counter--; continue; } var text = value.substring(index, i + 1); var translate = text.substring(2, text.length - 1); var translated = F.resource(language, 'T' + translate.hash(true).toString(36)); value = value.replaceAll(text, translated || translate); index += translated.length - 2; break; } } } return value; }; F.resource = function(language, key) { var dict = F.resources[language]; if (dict && dict[key]) return dict[key]; return language === 'default' ? '' : F.resource('default', key); }; F.localize = function(fn) { F.def.onLocalize = fn; }; F.auth = function(fn) { if (typeof(fn) === 'object') F.TBuilders.builtinauth(fn); else F.def.onAuthorize = fn; }; F.load = async function(types, callback, clear = true) { if (!types) types = ''; var beg = Date.now(); F.dir(); await F.TBundles.extract(); if (clear) await F.clear(true); process.send && process.send('total:ready'); if (typeof(types) === 'string') types = types.split(',').trim(); var list = async (path, extension = 'js') => new Promise(resolve => F.TUtils.ls(path, files => resolve(files), (path, isdir) => isdir ? true : (!path.includes('-bk') && !path.includes('_bk') && F.TUtils.getExtension(path) === extension))); var read = async (path) => new Promise(resolve => F.Fs.readFile(path, 'utf8', (err, response) => resolve(response ? response : ''))); var update = function(type, arr) { for (let i = 0; i < arr.length; i++) { let id = ''; if (type === 'modules') id = F.TUtils.getName(arr[i]).replace(/\.js$/, ''); arr[i] = { id: id, type: type, filename: arr[i] }; } return arr; }; if (types.length && !types.includes('stats')) { F.config.$stats = false; F.config.$sourcemap = false; } else F.config.$sourcemap = DEBUG; if (!types.length || types.includes('env')) { let env = await read(F.path.root('.env')); env && F.loadenv(env); } if (!types.length || types.includes('config')) { let config = await read(F.path.root('config')); config && F.loadconfig(config); } if (!types.length || types.includes('version')) { // It loads only the first line and converts it to a number. let version = await read(F.path.root('version')); if (version) { version = version.split('\n')[0]; if (version) F.config.version = MAIN.version = version.parseFloat(); } } if (!types.length || types.includes('resources')) { let resources = await list(F.path.root('resources'), 'resource'); for (let resource of resources) F.loadresource(F.TUtils.getName(resource).replace(/\.resource$/i, ''), await read(resource)); } if (!types.length || types.includes('jsonschemas')) { let jsonschemas = await list(F.path.root('jsonschemas'), 'json'); for (let jsonschema of jsonschemas) { let json = await read(jsonschema); json = json.parseJSON(); json && F.newjsonschema(F.TUtils.getName(jsonschema).replace(/\.json$/i, ''), json); } jsonschemas = await list(F.path.root('jsonschemas'), 'txt'); for (let jsonschema of jsonschemas) { let txt = await read(jsonschema); txt && F.newjsonschema(F.TUtils.getName(jsonschema).replace(/\.txt$/i, ''), txt); } } let loader = ['extensions', 'modules', 'actions', 'schemas', 'models', 'definitions', 'controllers', 'middleware', 'sources', 'scripts', 'transforms']; var files = []; var tmp; for (let type of loader) { if (!types.length || types.includes(type)) { tmp = await list(F.path.root(type), type === 'flowstreams' ? 'flow' : 'js'); if (tmp.length) files.push.apply(files, update(type, tmp)); } } if (!types.length || types.includes('plugins')) { var plugins = async () => new Promise(resolve => F.Fs.readdir(F.path.directory('plugins'), (err, response) => resolve(response || []))); tmp = await plugins(); for (let plugin of tmp) { if (plugin.includes('-bk') || plugin.includes('_bk') || plugin.toLowerCase().includes('ds_store')) continue; files.push({ id: F.TUtils.getName(plugin).replace(/\.js$/, ''), type: 'plugins', filename: F.path.directory('plugins', plugin + '/index.js') }); let loader = ['extensions', 'controllers', 'actions', 'schemas', 'models', 'definitions', 'sources', 'flowstreams', 'middleware', 'transforms']; for (let type of loader) { tmp = await list(F.path.root('plugins/' + plugin + '/' + type), type === 'flowstreams' ? 'flow' : 'js'); if (tmp.length) files.push.apply(files, update(type, tmp)); } } } files.sort(function(a) { if (a.type === 'module') return -1; if (a.type === 'middleware') return 1; return 0; }); for (let file of files) { let tmp = null; switch (file.type) { case 'modules': tmp = require(file.filename); if (!tmp.id) tmp.id = file.id; if (tmp.id) F.modules[tmp.id] = tmp; tmp.install && tmp.install(); break; case 'plugins': tmp = require(file.filename); F.plugins[file.id] = tmp; if (!tmp.id) tmp.id = file.id; tmp.install && tmp.install(); break; case 'controllers': case 'schemas': case 'actions': case 'extensions': case 'models': case 'definitions': case 'middleware': tmp = require(file.filename); tmp.install && tmp.install(); break; } } if (!types.length || types.includes('flowstreams')) F.TFlow.init(); if (!types.length || types.includes('stats')) F.loadstats(); F.loadservices(); F.stats.compilation = Date.now() - beg; F.stats.compiled = files.length; F.isLoaded = true; DEBUG && F.TSourceMap.refresh(); callback && callback(); F.emit('ready'); F.emit('load'); }; F.require = function(name) { if (name.startsWith('node:')) return require(name); if (NODE_MODULES[name]) return require('node:' + name); let mod = null; try { mod = require(name); } catch { mod = require(F.Path.join(F.config.$nodemodules, name)); } if (!mod) throw new Error('NPM module "' + name + '" not found'); return mod; }; F.runscript = function(filename) { F.Fs.readFile(filename || PATH.root('debug.js'), function(err, data) { if (data) { var scr = data.toString('utf8').trim(); var fn; if (data) { try { fn = new Function('require', scr); } catch (e) { console.error(e); } fn && fn(require); } } }); }; F.import = function(url, callback) { if (callback == null) return new Promise((resolve, reject) => F.import(url, (err, response) => err ? reject(err) : resolve(response))); var filename = F.path.tmp(F.clusterid + url.makeid() + '.js'); if (F.temporary.dependencies[url]) { callback && callback(null, require(filename)); return; } F.download(url, filename, function(err, response) { var m; if (!err) { m = require(filename); F.temporary.dependencies[url] = 1; } callback && callback(err, m, response); }); }; F.download = function(url, filename, callback, timeout) { if (!callback) return new Promise((resolve, reject) => F.download(url, filename, (err, response) => err ? reject(err) : resolve(response), timeout)); var opt = {}; if (typeof(url) === 'object') opt.unixsocket = url; else opt.url = url; opt.custom = true; opt.resolve = true; opt.timeout = timeout; opt.callback = function(err, response) { if (response) response.filename = filename; if (err) { callback && callback(err, response); callback = null; return; } var stream = F.Fs.createWriteStream(filename); var done = function(err) { if (callback) { callback(err, response); callback = null; } }; response.stream.pipe(stream); response.stream.on('error', done); stream.on('error', done); F.cleanup(stream, done); }; REQUEST(opt); }; F.cleanup = function(stream, callback) { if (!callback) return new Promise(resolve => F.cleanup(stream, resolve)); F.TUtils.onfinished(stream, function() { F.TUtils.destroystream(stream); if (callback) { callback(); callback = null; } }); }; F.python = function(filename, callback) { if (!callback) return new Promise((resolve, reject) => F.python(filename, (err, response) => err ? reject(err) : response)); F.Child.exec(F.config.$python + ' ' + filename, { cwd: F.Path.dirname(filename) }, callback); }; F.pipinstall = function(name, callback) { if (!callback) return new Promise((resolve, reject) => F.npminstall(name, err => err ? reject(err) : resolve())); var args = {}; args.cwd = F.directory; F.Child.exec(F.config.$python + ' -m pip install ' + name, args, function(err, response, output) { callback && callback(err ? (output || err) : null, null); }); }; F.npminstall = function(name, callback) { if (!callback) return new Promise((resolve, reject) => F.npminstall(name, err => err ? reject(err) : resolve())); var path = F.config.$nodemodules; F.path.mkdir(path, true); var index = name.lastIndexOf('@'); var folder = index === -1 ? name : name.substring(0, index); F.Fs.readFile(F.path.join(path, folder, 'package.json'), 'utf8', function(err, response) { var is = false; if (response) { response = response.parseJSON(); is = response && (index === -1 || response.version === name.substring(index + 1)); } if (is) { callback && callback(); } else { var args = {}; if (process.getuid && process.getuid() === 33) args.env = { NPM_CONFIG_CACHE: F.config.$npmcache }; args.cwd = path; F.Child.exec('npm install ' + name, args, function(err, response, output) { callback && callback(err ? (output || err) : null); }); } }); }; F.shell = function(cmd, callback, cwd) { var args = {}; if (typeof(callback) === 'string') { cwd = callback; callback = null; } args.cwd = cwd || F.directory; if (F.config.$shell) args.shell = F.config.$shell; if (callback) F.Child.exec(cmd, args, callback); else return new Promise((resolve, reject) => F.Child.exec(cmd, args, (err, response) => err ? reject(err) : resolve(response))); }; F.console = function() { if (!F.config.$imprint) return; var memory = process.memoryUsage(); var nodemodules = require.resolve('./index'); nodemodules = nodemodules.substring(0, nodemodules.length - (8 + 7)); print('===================================================='); print('PID : ' + process.pid); print('Node.js : ' + process.version); print('Total.js : v' + F.version); print('OS : ' + F.Os.platform() + ' ' + F.Os.release()); print('Memory : ' + memory.heapUsed.filesize(2) + ' / ' + memory.heapTotal.filesize(2)); print('User : ' + F.Os.userInfo().username); print('===================================================='); print('Name : ' + F.config.name); print('Version : ' + F.config.version); F.config.author && print('Author : ' + F.config.author); print('Date ({0}) : '.format(process.env.TZ) + NOW.format('yyyy-MM-dd HH:mm:ss')); print('Mode : ' + (DEBUG ? 'debug' : 'release')); print('Compiled : ' + F.stats.compiled + ' files (' + F.stats.compilation + 'ms)'); // F.threads && print('Threads : ' + Object.keys(F.threads).join(', ')); // global.THREAD && print('Thread : ' + global.THREAD); print('===================================================='); F.config.$root && print('Root : ' + F.config.$root); print('Directory : ' + process.cwd()); print('node_modules : ' + nodemodules); print('====================================================\n'); if (!F.isWorker && F.server) { var hostname = F.unixsocket ? ('Socket: ' + F.unixsocket) : '{2}://{0}:{1}/'.format(F.config.$ip, F.config.$port, F.isHTTPS ? 'https' : 'http'); if (!F.unixsocket && F.ip === '0.0.0.0') { var ni = F.Os.networkInterfaces(); if (ni.en0) { for (var i = 0; i < ni.en0.length; i++) { var nii = ni.en0[i]; // nii.family === 'IPv6' || if (nii.family === 'IPv4') { hostname += '\n{2}://{0}:{1}/'.format(nii.address, F.port, F.isHTTPS ? 'https' : 'http'); break; } } } } print(hostname); print(''); } }; F.loadservices = function() { F.internal.interval && clearInterval(F.internal.interval); // This timer solving timeouts F.internal.interval = setInterval(function() { F.internal.ticks++; global.NOW = new Date(); for (let key in F.flowstreams) { let flow = F.flowstreams[key]; if (!flow.$destroyed) flow.service(F.internal.ticks); } if (F.internal.ticks == 6 || F.internal.ticks == 12) F.TWebSocket.ping(); // 1 minute if (F.internal.ticks == 12) { F.internal.ticks = 0; F.internal.counter++; F.service(F.internal.counter); F.$events.service && F.emit('service', F.internal.counter); } F.TFlow.ping(); if (!F.temporary.pending.length) { F.stats.request.pending = 0; return; } let index = 0; while (true) { let ctrl = F.temporary.pending[index]; if (ctrl) { if (ctrl.destroyed || ctrl.res.headersSent || ctrl.res.writableEnded) { F.temporary.pending.splice(index, 1); } else if (ctrl.timeout <= 0) { F.stats.response.timeout++; if (F.timeouts.push((NOW = new Date()).toJSON() + ' ' + ctrl.url) > 5) F.timeouts.shift(); ctrl.fallback(408); F.temporary.pending.splice(index, 1); } else { F.stats.request.pending++; ctrl.timeout -= 5; // 5 seconds index++; } } else break; } F.stats.request.pending = F.temporary.pending.length; }, 5000); }; F.http = function(opt) { if (!opt) opt = {}; // opt.unixsocket {String} // opt.ip {String} // opt.port {Number} // opt.load {String} // config, env, modules, controllers, actions, schemas, models, definitions, sources, middleware, resources, plugins, stats // none - loads only web server if (F.isLoaded) { F.httpload(opt); return; } if (opt.config) { let cfg = []; for (let key in opt.config) cfg.push({ id: key, value: opt.config[key] }); F.loadconfig(cfg); } F.load(opt.load || opt.type || '', () => F.httpload(opt), opt.clear); }; F.httpload = function(opt) { if (F.server) { F.server.close(function() { F.server = null; F.httpload(opt); }); return; } F.server = F.Http.createServer(F.THttp.listen); F.server.on('upgrade', F.TWebSocket.listen); CONF.$performance && F.server.on('connection', function(socket) { socket.setNoDelay(true); socket.setKeepAlive(true, 10); }); var unixsocket = opt.unixsocket || F.config.$unixsocket; if (unixsocket) { try { F.Fs.unlinkSync(unixsocket); } catch {} if (F.isWindows && unixsocket.indexOf(SOCKETWINDOWS) === -1) unixsocket = F.Path.join(SOCKETWINDOWS, unixsocket); F.config.$unixsocket = F.unixsocket = unixsocket; var listen = function(count) { F.server.listen(unixsocket, function() { // Check if the socket exists if (F.isWindows) return; F.Fs.lstat(unixsocket, function(err) { if (count > 9) throw new Error('HTTP server can not listen the path "{0}"'.format(unixsocket)); if (err) setTimeout(listen, 500, count + 1); else if (opt.unixsocket777) F.Fs.chmodSync(unixsocket, 0o777); }); }); }; listen(1); } else { if (opt.port) F.config.$port = opt.port; if (F.config.$port === 'auto') { let port = process.env.PORT; if (!port) { for (let arg of process.argv) { if ((/^\d{3,5}$/).test(arg)) { port = arg; break; } } } if (port) port = +port; if (isNaN(port)) port = 8000; F.config.$port = port; } if (opt.ip) F.config.$ip = opt.ip; F.server.listen(F.config.$port, F.config.$ip); } F.config.$performance && F.server.on('connection', httptuningperformance); if (!process.connected && F.console) F.console(); }; F.logger = function(enable) { if (enable == null) enable = true; if (enable) { if (console.$backup) return; } else { if (!console.$backup) return; console.log = console.$backup.log; console.warn = console.$backup.warn; console.error = console.$backup.error; console.time = console.$backup.time; console.timeEnd = console.$backup.timeEnd; console.$backup = null; return; } var Console = require('node:console').Console; var path = F.path.root(); if (path.substring(path.length - 5, path.length - 1) === '.src') path = F.Path.join(path.substring(0, path.length - 5), 'logs/'); else path = F.Path.join(path, 'logs/'); F.path.mkdir(path); var output = F.Fs.createWriteStream(F.Path.join(path, 'debug.log'), { flags: 'a' }); var logger = new Console({ stdout: output, stderr: output }); console.$backup = {}; console.$backup.log = console.log; console.$backup.warn = console.warn; console.$backup.error = console.error; console.$backup.time = console.time; console.$backup.timeEnd = console.timeEnd; console.log = function() { logger.log.apply(logger, arguments); }; console.warn = function() { logger.warn.apply(logger, arguments); }; console.error = function() { logger.error.apply(logger, arguments); }; console.time = function() { logger.time.apply(logger, arguments); }; console.timeEnd = function() { logger.timeEnd.apply(logger, arguments); }; }; F.componentator = function(name, components, removeprev = true, attrs = '') { if (typeof(removeprev) === 'string') { attrs = removeprev; removeprev = true; } var meta = {}; meta.components = components; meta.name = name; F.$events.componentator && F.emit('componentator', meta); let url = 'https://componentator.com/download/'; let nameid = meta.name.slug(); let relative = 'ui-' + (removeprev ? (nameid + '-') : '') + meta.components.makeid() + '.min.js'; let filename = F.path.public(relative); F.repo[meta.name] = (F.config.$root || '/') + relative; if (removeprev) { F.Fs.readdir(F.path.public(), function(err, files) { let rem = []; for (let m of files) { if (m !== relative && m.indexOf('ui-' + nameid + '-') !== -1) rem.push(F.path.public(m)); } if (rem.length) F.path.unlink(rem); }); } F.Fs.lstat(filename, function(err) { if (err) { let opt = {}; opt.method = 'POST'; opt.body = 'id=' + meta.components + (attrs ? ('&' + attrs) : ''); opt.type = 'urlencoded'; opt.url = url; opt.callback = function(err, response) { if (err) Total.error(err, 'COMPONENTATOR()'); else Total.Fs.writeFile(filename, response.body, 'utf8', ERROR('COMPONENTATOR()')) }; REQUEST(opt); } }); }; F.syslog = function(msg) { NOW = new Date(); console.log('INFO ======= ' + (NOW.format('yyyy-MM-dd HH:mm:ss')) + ': ' + msg); }; F.error = function(err, name, url) { if (!arguments.length) return F.errorcallback; if (err) F.def.onError(err, name, url); }; F.errorcallback = function(err) { err && F.error(err); }; F.merge = function(url) { let arr = []; for (let i = 1; i < arguments.length; i++) { let links = arguments[i]; if (typeof(links) === 'string') { let tmp = links.split('+').trim(); for (let link of tmp) { if (REG_HTTPHTTPS.test(link)) { arr.push(link); continue; } if (link[0] !== '~' && link[0] !== '_') { if (link[0] === '/') { link = F.path.public(link); } else { let ext = F.TUtils.getExtension(link); if (ext === 'js') link = F.path.public('/js/' + link); else link = F.path.public('/css/' + link); } arr.push(link); } else arr.push(F.path.route(link, 'public')); } continue; } if (!(links instanceof Array)) links = [links]; for (let link of links) arr.push(link); } if (url[0] !== '/') url = '/' + url; url = F.virtualpath(url.toLowerCase()); var ext = F.TUtils.getExtension(url); var key = url.substring(1).replace(/\//g, '-').replace(/\.(js|html|css)$/, '') + '-min.' + ext; var filename = F.path.tmp(F.clusterid + 'merged_' + key); F.routes.virtual[url] = async function(ctrl) { if (DEBUG) { var buffer = await F.TMinificators.merge(true, arr); ctrl.binary(buffer, F.TUtils.contentTypes[ext] || F.TUtils.contentTypes.bin); } else { F.lock('merging_' + key, async function(next) { if (F.temporary.merged[key]) { if (F.temporary.notfound[url]) { ctrl.fallback(404); next(); return; } } else { if (F.temporary.notfound[url]) delete F.temporary.notfound[url]; F.temporary.merged[key] = true; await F.TMinificators.merge(filename, arr); } ctrl.response.minify = false; ctrl.file(filename); next(); }); } }; return url; }; F.lock = function(key, callback) { if (F.processing[key]) { F.processing[key].push(callback); } else { F.processing[key] = []; callback(function() { var pending = F.processing[key]; delete F.processing[key]; for (let fn of pending) fn(F.TUtils.noop); }); } }; F.touch = function(url) { if (url) { delete F.temporary.minified[url]; delete F.temporary.tmp[url]; delete F.temporary.notfound[url]; } else { F.temporary.minified = {}; F.temporary.tmp = {}; F.temporary.notfound = {}; } }; F.unauthorized = function($) { var user = $.user; if (user) { if (user.sa || user.su) return false; var compare = user.permissions || user.roles; var args = arguments; if (compare) { if (compare instanceof Array) { for (let i = 0; i < compare.length; i++) { for (let j = 1; j < args.length; j++) { if (args[j] === compare[i]) return false; } } } else { for (let j = 1; j < args.length; j++) { if (compare[args[j]]) return false; } } } } $.invalid(401); return true; }; F.middleware = function(name, fn, assign) { if (!fn) { let types = ['routes', 'files', 'websockets']; for (let type of types) { for (let route of F.routes[type]) { let index = route.middleware.indexOf(name); if (index !== -1) route.middleware.splice(index, 1); } } delete F.routes.middleware[name]; return; } F.routes.middleware[name] = fn; if (assign) { if (typeof(assign) === 'string') assign = assign.split(',').trim(); var routes = ['routes', 'files', 'websockets']; for (let a of assign) { if (a === '*') { for (let type of routes) { for (let route of F.routes[type]) { if (!route.middleware.includes(name)) route.middleware.push(name); } } } else { if (a === 'websocket' || a === 'file' || a === 'route') a += 's'; else if (a === 'dynamic') a = 'routes'; let routes = F.routes[a]; if (routes) { for (let route of routes) { if (!route.middleware.includes(name)) route.middleware.push(name); } } } } } }; F.pauseserver = function(name, enable) { var index; if (enable !== undefined) { if (enable) { if (!F.paused.includes(name)) F.paused.push(name); } else { index = F.paused.indexOf(name); if (index !== -1) F.paused.splice(index, 1); } return enable; } index = F.paused.indexOf(name); if (index != -1) { F.paused.splice(index, 1); enable = false; } else { F.paused.push(name); enable = true; } return enable === true; }; F.uid = function() { let ts = Date.now() / 100; let h = F.TUtils.convert62(ts); let index = F.internal.uid++; return h + F.TUtils.convert62(index + 99) + F.internal.uidc + h.length + (index % 2 ? 1 : 0) + 'f'; // "f" version }; F.cron = function(line, fn) { let obj = {}; obj.check = F.TCron.make(line); obj.exec = fn; obj.remove = function() { let index = F.crons.indexOf(this); if (index !== -1) F.crons.splice(index, 1); }; F.crons.push(obj); return obj; }; // Service F.service = function(count) { // Clears expired cache F.cache.refresh(); // Clears temporary memory for non-exist files F.temporary.notfound = {}; // UID state F.internal.uid = 1; F.internal.uidc = F.TUtils.random_text(1); if (F.config.$httpreqlimit) F.temporary.ddos = {}; if (count % F.config.$clearcache === 0) { F.temporary.actions = {}; F.temporary.path = {}; F.temporary.views = {}; F.temporary.utils = {}; F.temporary.calls = {}; F.temporary.images = {}; F.temporary.templates = {}; F.temporary.querybuilders = {}; for (let key in F.filestorages) F.filestorages[key].cache = {}; } if (count % 5 === 0) { global.TEMP = {}; F.TMail.refresh(); } // Bans for (let key in F.temporary.bans) { if (key !== 'is') { let tmp = F.temporary.bans[key]; if (tmp.expire < NOW) delete F.temporary.bans[key]; else F.temporary.bans.is = true; } } // Update expires date if (count % 60 === 0) F.config.$httpexpire = NOW.add('y', 1).toUTCString(); if (count % F.config.$tmsclearblocked === 0) F.temporary.tmsblocked = {}; if (count % 3 === 0) F.temporary.dnscache = {}; let blocked = F.temporary.blocked; if (blocked.is) { blocked.is = false; for (let key in blocked) { if (key !== 'is') { let tmp = blocked[key]; if (tmp.expire < NOW) delete blocked[key]; else blocked.is = true; } } } F.temporary.service.publish = F.stats.performance.publish; F.temporary.service.subscribe = F.stats.performance.subscribe; F.temporary.service.call = F.stats.performance.call; F.temporary.service.request = F.stats.performance.request; F.temporary.service.file = F.stats.performance.file; F.temporary.service.message = F.stats.performance.message; F.temporary.service.mail = F.stats.performance.mail; F.temporary.service.open = F.stats.performance.open; F.temporary.service.dbrm = F.stats.performance.dbrm; F.temporary.service.dbwm = F.stats.performance.dbwm; F.temporary.service.external = F.stats.performance.external; F.temporary.service.upload = F.stats.performance.upload; F.temporary.service.download = F.stats.performance.download; F.stats.request.size += F.stats.performance.download; F.stats.response.size += F.stats.performance.upload; F.stats.performance.publish = 0; F.stats.performance.subscribe = 0; F.stats.performance.call = 0; F.stats.performance.upload = 0; F.stats.performance.download = 0; F.stats.performance.external = 0; F.stats.performance.dbrm = 0; F.stats.performance.dbwm = 0; F.stats.performance.request = 0; F.stats.performance.file = 0; F.stats.performance.open = 0; F.stats.performance.message = 0; F.stats.performance.mail = 0; F.usage && F.usage(); F.temporary.service.usage = 0; if (count % 10 === 0 && global.gc) setTimeout(cleargc, 1000); // Exec crons for (let cron of F.crons) { if (cron.check(NOW)) cron.exec(NOW); } }; function cleargc() { global.gc(); } F.clear = function(init = true, callback) { if (callback == null) return new Promise(resolve => F.clear(init, () => resolve())); var dir = F.path.tmp(); var plus = F.clusterid; if (dir[dir.length - 1] !== '/') dir += '/'; if (F.is) dir = dir.replaceAll('/', '\\'); if (init) { if (F.config.$cleartemp) { // clears only JS and CSS files F.TUtils.ls(dir, function(files) { F.path.unlink(files, callback); }, function(filename, folder) { if (folder || (plus && !filename.substring(dir.length).startsWith(plus))) return false; var ext = F.TUtils.getExtension(filename); return ext === 'js' || ext === 'css' || ext === 'tmp' || ext === 'upload' || ext === 'html' || ext === 'htm'; }); return; } } if (!pathexists(dir)) { callback && callback(); return; } F.TUtils.ls(dir, function(files, directories) { if (init) { let arr = []; for (let file of files) { let filename = file.substring(dir.length); if (plus && !filename.startsWith(plus)) continue; if (filename.indexOf('/') === -1 && !filename.endsWith('.cache')) arr.push(file); } files = arr; directories = directories.remove(function(name) { name = F.TUtils.getName(name); return name[0] !== '~'; }); } F.path.unlink(files, () => F.path.rmdir(directories, callback)); }); if (!init) F.touch(); }; F.view = function(name, model, prepare) { var view = new F.TViewEngine.View(); prepare && prepare(view); return view.render(name, model); }; F.memorize = function(name, delay, skip) { if (!name) name = ''; if (delay && typeof(delay) !== 'number') { var tmp; tmp = skip; skip = delay; delay = tmp; } var data = {}; var filename = F.path.databases('memorize' + (name ? ('_' + name) : '') + '.json'); try { data = F.Fs.readFileSync(filename, 'utf8').parseJSON(true); } catch {} var replacer; var timeout; var ignore = {}; if (skip) { if (typeof(skip) === 'string') skip = skip.split(',').trim(); for (var m of skip) ignore[m] = 1; replacer = (key, value) => ignore[key] ? undefined : value; } var save = () => F.Fs.writeFile(filename, replacer ? JSON.stringify(data, replacer, '\t') : JSON.stringify(data, null, '\t'), ERROR('MEMORIZE(\'' + name + '\').save()')); data.save = function() { timeout && clearTimeout(timeout); timeout = setTimeout(save, delay || 10000); }; data.set = function(key, value) { data[key] = value; data.save(); }; return data; }; F.newjsonschema = function(name, obj) { var type = typeof(name); if (type === 'string' && typeof(obj) === 'string') { obj = obj.toJSONSchema(); obj.$id = name; } else if (type === 'string') { if (obj == null) { obj = name.toJSONSchema(); name = obj.$id; } } else if (type === 'object') { obj = name; name = obj.$id; } F.jsonschemas[name] = F.jsonschemas[obj.$id] = obj; obj.transform = F.TUtils.jsonschematransform; return obj; }; F.newtransform = function(name, action, id) { if (action == null) { let items = F.transformations[name]; let index = items.TfindIndex('id', id); if (index !== -1) { items.splice(index, 1); if (!items.length) delete F.transformations[name]; } } else { let obj = {}; obj.id = id; obj.action = action; obj.remove = function() { let arr = F.transformations[name]; if (arr) { let index = arr.indexOf(obj); if (index !== -1) { arr.splice(index, 1); if (!arr.length) delete F.transformations[name]; } } }; if (F.transformations[name]) F.transformations[name].push(obj); else F.transformations[name] = [obj]; return obj; } }; function transform(items, opt, index) { var t = items[index]; if (t) { opt.next = () => transform(items, opt, index + 1); t.action(opt, opt.value); } else opt.$callback(opt.error.items.length ? opt.error : null, opt.value); } F.transform = function(name, value, callback, controller) { if (typeof(callback) !== 'function') return new Promise((resolve, reject) => F.transform(name, value, (err, response) => err ? reject(err) : resolve(response), callback || controller)); var items = F.transformations[name]; if (items) { let opt = new F.TBuilders.Options(controller, new F.TBuilders.ErrorBuilder()); if (controller) opt.user = controller.user; opt.value = value; opt.$callback = callback; transform(items, opt, 0); } else callback(null, value); }; function auditjsonserialization(key, value) { if (!IGNORE_AUDIT[key] && value != null && value !== '') return value; } F.audit = function(name, $, message, type) { if (typeof(name) === 'object') { type = message; message = $; $ = name; name = null; } var data = {}; if ($.user) { data.userid = $.user.id; data.createdby = $.user.name || $.user.nick || $.user.alias; } if ($.controller) { if ($.controller.sessionid) data.sessionid = $.controller.sessionid; } data.ua = $.ua; data.ip = $.ip; data.url = $.url; if (type) data.type = type || 'info'; if ($.id) data.action = $.id; if ($.model) data.data = JSON.stringify({ params: $.params, query: $.query, model: $.model }, auditjsonserialization); if (F.clusterid) data.instance = F.clusterid; if (message) data.message = message; data.app = F.config.url || F.config.name; if (F.config.$tapilogger && F.config.$tapi && F.config.secret_totalapi) API('TAPI/logger', data).callback(ERROR('totalapi')); else F.def.onAudit(name, data, $); }; F.restore = function(filename, target, callback, filter) { if (!callback) return new Promise((resolve, reject) => F.restore(filename, target, (err, response) => err ? reject(err) : resolve(response), filter)); var buffer_key = Buffer.from(':'); var buffer_new = Buffer.from('\n'); var buffer_dir = Buffer.from('#'); var cache = {}; var data = null; var type = 0; var item =