Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

714 righe
25 KiB

  1. const fs = require('fs-extra');
  2. const Scope = require('./scope');
  3. const Parser = require('./parser');
  4. const db = require('./db');
  5. const validator = require('../validator');
  6. const config = require('../setup/config');
  7. const debug = require('debug')('server-connect:app');
  8. const { clone, formatDate, parseDate } = require('../core/util');
  9. const { toSystemPath } = require('../core/path');
  10. const os = require('os');
  11. if (!global.db) {
  12. global.db = {};
  13. }
  14. function App(req = {}, res = {}) {
  15. this.error = false;
  16. this.data = {};
  17. this.meta = {};
  18. this.settings = {};
  19. this.modules = {};
  20. this.io = global.io;
  21. this.req = req;
  22. this.res = res;
  23. this.global = new Scope();
  24. this.scope = this.global;
  25. this.mail = {};
  26. this.auth = {};
  27. this.oauth = {};
  28. this.db = {};
  29. this.s3 = {};
  30. this.jwt = {};
  31. this.trx = {};
  32. if (config.globals.data) {
  33. this.set(config.globals.data);
  34. }
  35. this.set({
  36. $_ERROR: null,
  37. //$_SERVER: process.env,
  38. $_ENV: process.env,
  39. $_GET: req.query,
  40. $_POST: req.body,
  41. $_PARAM: req.params,
  42. $_HEADER: req.headers,
  43. $_COOKIE: req.cookies,
  44. $_SESSION: req.session
  45. });
  46. let urlParts = req.originalUrl ? req.originalUrl.split('?') : ['', ''];
  47. let $server = {
  48. CONTENT_TYPE: req.headers && req.headers['content-type'],
  49. HTTPS: req.protocol == 'https',
  50. PATH_INFO: req.path,
  51. QUERY_STRING: urlParts[1],
  52. REMOTE_ADDR: req.ip,
  53. REQUEST_PROTOCOL: req.protocol,
  54. REQUEST_METHOD: req.method,
  55. SERVER_NAME: req.hostname,
  56. BASE_URL: req.headers && req.protocol + '://' + req.headers['host'],
  57. URL: urlParts[0],
  58. HOSTNAME: os.hostname()
  59. };
  60. if (req.headers) {
  61. for (let header in req.headers) {
  62. $server['HTTP_' + header.toUpperCase().replace(/-/, '_')] = req.headers[header];
  63. }
  64. }
  65. // Try to keep same as ASP/PHP
  66. this.set('$_SERVER', $server);
  67. }
  68. App.prototype = {
  69. set: function(key, value) {
  70. this.global.set(key, value);
  71. },
  72. get: function(key, def) {
  73. let value = this.global.get(key);
  74. return value !== undefined ? value : def;
  75. },
  76. remove: function(key) {
  77. this.global.remove(key);
  78. },
  79. setSession: function(key, value) {
  80. this.req.session[key] = value;
  81. },
  82. getSession: function(key) {
  83. return this.req.session[key];
  84. },
  85. removeSession: function(key) {
  86. delete this.req.session[key];
  87. },
  88. setCookie: function(name, value, opts) {
  89. if (!this.res.headersSent) {
  90. if (this.res.cookie) {
  91. this.res.cookie(name, value, opts);
  92. }
  93. } else {
  94. debug(`Trying to set ${name} cookie while headers were already sent.`);
  95. }
  96. },
  97. getCookie: function(name, signed) {
  98. return signed ? this.req.signedCookies[name] : this.req.cookies[name];
  99. },
  100. removeCookie: function(name, opts) {
  101. if (this.res.clearCookie) {
  102. // copy all options except expires and maxAge
  103. const clearOpts = Object.assign({}, opts);
  104. delete clearOpts.expires;
  105. delete clearOpts.maxAge;
  106. this.res.clearCookie(name, clearOpts);
  107. }
  108. },
  109. setMailer: function(name, options) {
  110. let setup = {};
  111. options = this.parse(options);
  112. switch (options.server) {
  113. case 'mail':
  114. setup.sendmail = true;
  115. break;
  116. case 'ses':
  117. const aws = require('@aws-sdk/client-ses');
  118. const ses = new AWS.SES({
  119. endpoint: options.endpoint,
  120. credentials: {
  121. accessKeyId: options.accessKeyId,
  122. secretAccessKey: options.secretAccessKey
  123. }
  124. });
  125. setup.SES = { ses, aws };
  126. break;
  127. default:
  128. // https://nodemailer.com/smtp/
  129. setup.host = options.host || 'localhost';
  130. setup.port = options.port || 25;
  131. setup.secure = options.useSSL || false;
  132. setup.auth = {
  133. user: options.username,
  134. pass: options.password
  135. };
  136. setup.tls = {
  137. rejectUnauthorized: false
  138. };
  139. }
  140. return this.mail[name] = setup;
  141. },
  142. getMailer: function(name) {
  143. if (this.mail[name]) {
  144. return this.mail[name];
  145. }
  146. if (config.mail[name]) {
  147. return this.setMailer(name, config.mail[name]);
  148. }
  149. if (fs.existsSync(`app/modules/Mailer/${name}.json`)) {
  150. let action = fs.readJSONSync(`app/modules/Mailer/${name}.json`);
  151. return this.setMailer(name, action.options);
  152. }
  153. throw new Error(`Couldn't find mailer "${name}".`);
  154. },
  155. setAuthProvider: async function(name, options) {
  156. const Provider = require('../auth/' + options.provider.toLowerCase());
  157. const provider = new Provider(this, options, name);
  158. if (!provider.identity) await provider.autoLogin();
  159. this.set('identity', provider.identity);
  160. return this.auth[name] = provider;
  161. },
  162. getAuthProvider: async function(name) {
  163. if (this.auth[name]) {
  164. return this.auth[name];
  165. }
  166. if (config.auth[name]) {
  167. return await this.setAuthProvider(name, config.auth[name]);
  168. }
  169. if (fs.existsSync(`app/modules/SecurityProviders/${name}.json`)) {
  170. let action = fs.readJSONSync(`app/modules/SecurityProviders/${name}.json`);
  171. return await this.setAuthProvider(name, action.options);
  172. }
  173. throw new Error(`Couldn't find security provider "${name}".`);
  174. },
  175. setOAuthProvider: async function(name, options) {
  176. const OAuth2 = require('../oauth');
  177. const services = require('../oauth/services');
  178. options = this.parse(options);
  179. let service = this.parseOptional(options.service, 'string', null);
  180. let opts = service ? services[service] : {};
  181. opts.client_id = this.parseOptional(options.client_id, 'string', null);
  182. opts.client_secret = this.parseOptional(options.client_secret, 'string', null);
  183. opts.token_endpoint = opts.token_endpoint || this.parseRequired(options.token_endpoint, 'string', 'oauth.provider: token_endpoint is required.');
  184. opts.auth_endpoint = opts.auth_endpoint || this.parseOptional(options.auth_endpoint, 'string', '');
  185. opts.scope_separator = opts.scope_separator || this.parseOptional(options.scope_separator, 'string', ' ');
  186. opts.access_token = this.parseOptional(options.access_token, 'string', null);
  187. opts.refresh_token = this.parseOptional(options.refresh_token, 'string', null);
  188. opts.jwt_bearer = this.parseOptional(options.jwt_bearer, 'string', false);
  189. opts.client_credentials = this.parseOptional(options.client_credentials, 'boolean', false);
  190. opts.params = Object.assign({}, opts.params, this.parseOptional(options.params, 'object', {}));
  191. this.oauth[name] = new OAuth2(this, this.parse(options), name);
  192. await this.oauth[name].init();
  193. return this.oauth[name];
  194. },
  195. getOAuthProvider: async function(name) {
  196. if (this.oauth[name]) {
  197. return this.oauth[name];
  198. }
  199. if (config.oauth[name]) {
  200. return await this.setOAuthProvider(name, config.oauth[name]);
  201. }
  202. if (fs.existsSync(`app/modules/oauth/${name}.json`)) {
  203. let action = fs.readJSONSync(`app/modules/oauth/${name}.json`);
  204. return await this.setOAuthProvider(name, action.options);
  205. }
  206. throw new Error(`Couldn't find oauth provider "${name}".`);
  207. },
  208. setDbConnection: function(name, options) {
  209. if (global.db[name]) {
  210. return global.db[name];
  211. }
  212. options = this.parse(options);
  213. switch (options.client) {
  214. case 'couchdb':
  215. const { host, port, user, password, database } = options.connection;
  216. const nano = require('nano');
  217. global.db[name] = nano(`http://${user ? user + (password ? ':' + password : '') + '@' : ''}${host}${port ? ':' + port : ''}/${database}`);
  218. global.db[name].client = options.client;
  219. return global.db[name];
  220. case 'sqlite3':
  221. if (options.connection.filename) {
  222. options.connection.filename = toSystemPath(options.connection.filename);
  223. }
  224. break;
  225. case 'mysql':
  226. case 'mysql2':
  227. options.connection = {
  228. supportBigNumbers: true,
  229. dateStrings: !!options.tz,
  230. ...options.connection
  231. };
  232. break;
  233. case 'mssql':
  234. if (options.tz) {
  235. // use local timezone to have same behavior as the other drivers
  236. // to prevent problems node should have same timezone as database
  237. options.connection.options = { useUTC: false, ...options.connection.options };
  238. }
  239. break;
  240. case 'postgres':
  241. if (options.tz) {
  242. const types = require('pg').types;
  243. const parseFn = val => val;
  244. types.setTypeParser(types.builtins.TIME, parseFn);
  245. types.setTypeParser(types.builtins.TIMETZ, parseFn);
  246. types.setTypeParser(types.builtins.TIMESTAMP, parseFn);
  247. types.setTypeParser(types.builtins.TIMESTAMPTZ, parseFn);
  248. }
  249. break;
  250. }
  251. if (options.connection && options.connection.ssl) {
  252. if (options.connection.ssl.key) {
  253. options.connection.ssl.key = fs.readFileSync(toSystemPath(options.connection.ssl.key));
  254. }
  255. if (options.connection.ssl.ca) {
  256. options.connection.ssl.ca = fs.readFileSync(toSystemPath(options.connection.ssl.ca));
  257. }
  258. if (options.connection.ssl.cert) {
  259. options.connection.ssl.cert = fs.readFileSync(toSystemPath(options.connection.ssl.cert));
  260. }
  261. }
  262. options.useNullAsDefault = true;
  263. const formatRecord = (record, meta) => {
  264. for (column in record) {
  265. if (record[column] != null) {
  266. if (meta.has(column)) {
  267. const info = meta.get(column);
  268. if (['json', 'object', 'array'].includes(info.type)) {
  269. if (typeof record[column] == 'string') {
  270. try {
  271. // column of type json returned as string, need parse
  272. record[column] = JSON.parse(record[column]);
  273. } catch (err) {
  274. console.warn(err);
  275. }
  276. }
  277. }
  278. if (info.type == 'date') {
  279. if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) {
  280. record[column] = undefined;
  281. } else {
  282. record[column] = formatDate(record[column], 'yyyy-MM-dd');
  283. }
  284. }
  285. if (info.type == 'time') {
  286. if (options.tz == 'local') {
  287. record[column] = formatDate(record[column], 'HH:mm:ss.v');
  288. } else if (options.tz == 'utc') {
  289. record[column] = parseDate(parseDate(formatDate('now', 'yyyy-MM-dd ') + formatDate(record[column], 'HH:mm:ss.v'))).toISOString().slice(11);
  290. }
  291. }
  292. if (['datetime', 'timestamp'].includes(info.type) || Object.prototype.toString.call(record[column]) == '[object Date]') {
  293. if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) {
  294. record[column] = undefined;
  295. } else if (options.tz == 'local') {
  296. record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
  297. } else if (options.tz == 'utc') {
  298. record[column] = parseDate(record[column]).toISOString();
  299. }
  300. }
  301. } else {
  302. // try to detect datetime
  303. if (Object.prototype.toString.call(record[column]) == '[object Date]') {
  304. if (options.tz == 'local') {
  305. record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
  306. } else if (options.tz == 'utc') {
  307. record[column] = parseDate(record[column]).toISOString();
  308. }
  309. } else if (typeof record[column] == 'string' && /^\d{4}-\d{2}-\d{2}([T\s]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}(:\d{2})?)?)?$/.test(record[column])) {
  310. if (record[column].startsWith('0000-00-00')) {
  311. record[column] = undefined;
  312. } else if (options.tz == 'local') {
  313. record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
  314. } else if (options.tz == 'utc') {
  315. record[column] = parseDate(record[column]).toISOString();
  316. }
  317. }
  318. }
  319. if (record[column] == undefined) {
  320. record[column] = null;
  321. } else if (record[column].toJSON) {
  322. record[column] = record[column].toJSON();
  323. }
  324. }
  325. }
  326. return record;
  327. };
  328. options.postProcessResponse = function(result, queryContext) {
  329. const meta = new Map();
  330. if (Array.isArray(queryContext)) {
  331. for (let item of queryContext) {
  332. if (item.name && item.type) {
  333. meta.set(item.name, item);
  334. }
  335. }
  336. }
  337. if (Array.isArray(result)) {
  338. return result.map(record => formatRecord(record, meta));
  339. } else {
  340. return formatRecord(result, meta);
  341. }
  342. };
  343. global.db[name] = db(options);
  344. //this.res.on('finish', () => global.db[name].destroy());
  345. return global.db[name];
  346. },
  347. getDbConnection: function(name) {
  348. if (this.trx[name]) return this.trx[name];
  349. if (global.db[name]) {
  350. return global.db[name];
  351. }
  352. if (config.db[name]) {
  353. return this.setDbConnection(name, config.db[name]);
  354. }
  355. if (fs.existsSync(`app/modules/connections/${name}.json`)) {
  356. let action = fs.readJSONSync(`app/modules/connections/${name}.json`);
  357. return this.setDbConnection(name, action.options);
  358. }
  359. throw new Error(`Couldn't find database connection "${name}".`);
  360. },
  361. setS3Provider: function(name, options) {
  362. options = this.parse(options);
  363. const { S3 } = require('@aws-sdk/client-s3');
  364. const endpoint = this.parseRequired(options.endpoint, 'string', 's3.provider: endpoint is required.');
  365. const accessKeyId = this.parseRequired(options.accessKeyId, 'string', 's3.provider: accessKeyId is required.');
  366. const secretAccessKey = this.parseRequired(options.secretAccessKey, 'string', 's3.provider: secretAccessKey is required.');
  367. let region = options.region || 'us-east-1';
  368. let pos = endpoint.indexOf('.amazonaws');
  369. if (pos > 3) region = endpoint.substr(3, pos - 3);
  370. let forcePathStyle = options.forcePathStyle || false;
  371. this.s3[name] = new S3({ endpoint: 'https://' + endpoint, credentials: { accessKeyId, secretAccessKey }, region, signatureVersion: 'v4', forcePathStyle });
  372. return this.s3[name];
  373. },
  374. getS3Provider: function(name) {
  375. if (this.s3[name]) {
  376. return this.s3[name];
  377. }
  378. if (config.s3[name]) {
  379. return this.setS3Provider(name, config.s3[name]);
  380. }
  381. if (fs.existsSync(`app/modules/s3/${name}.json`)) {
  382. let action = fs.readJSONSync(`app/modules/s3/${name}.json`);
  383. return this.setS3Provider(name, action.options);
  384. }
  385. throw new Error(`Couldn't find S3 provider "${name}".`);
  386. },
  387. setJSONWebToken: function(name, options) {
  388. const jwt = require('jsonwebtoken');
  389. options = this.parse(options);
  390. let opts = {};
  391. if (options.alg) opts.algorithm = options.alg;
  392. if (options.iss) opts.issuer = options.iss;
  393. if (options.sub) opts.subject = options.sub;
  394. if (options.aud) opts.audience = options.aud;
  395. if (options.jti) opts.jwtid = options.jti;
  396. if (options.expiresIn) opts.expiresIn = options.expiresIn
  397. return this.jwt[name] = jwt.sign({
  398. ...options.claims
  399. }, options.key, {
  400. expiresIn: 3600, // use as default (required by google)
  401. ...opts
  402. });
  403. },
  404. getJSONWebToken: function(name) {
  405. if (config.jwt[name]) {
  406. return this.setJSONWebToken(name, config.jwt[name]);
  407. }
  408. if (fs.existsSync(`app/modules/jwt/${name}.json`)) {
  409. let action = fs.readJSONSync(`app/modules/jwt/${name}.json`);
  410. return this.setJSONWebToken(name, action.options);
  411. }
  412. throw new Error(`Couldn't find JSON Web Token "${name}".`);
  413. },
  414. define: async function(cfg, internal) {
  415. if (cfg.settings) {
  416. this.settings = clone(cfg.settings);
  417. }
  418. if (cfg.vars) {
  419. this.set(clone(cfg.vars));
  420. }
  421. if (cfg.meta) {
  422. this.meta = clone(cfg.meta);
  423. await validator.init(this, this.meta);
  424. }
  425. if (fs.existsSync('app/modules/global.json')) {
  426. await this.exec(await fs.readJSON('app/modules/global.json'), true);
  427. }
  428. /*
  429. debug('body: %o', this.req.body);
  430. debug('query: %o', this.req.query);
  431. debug('params: %o', this.req.params);
  432. debug('headers: %o', this.req.headers);
  433. debug('cookies: %o', this.req.cookies);
  434. debug('session: %o', this.req.session);
  435. */
  436. await this.exec(cfg.exec || cfg, internal);
  437. },
  438. sub: async function(actions, scope) {
  439. const subApp = new App(this.req, this.res);
  440. subApp.global = this.global;
  441. subApp.scope = this.scope.create(scope);
  442. await subApp.exec(actions, true);
  443. return subApp.data;
  444. },
  445. exec: async function(actions, internal) {
  446. if (actions.exec) {
  447. return this.exec(actions.exec, internal);
  448. }
  449. actions = clone(actions);
  450. await this._exec(actions.steps || actions);
  451. if (this.error !== false) {
  452. if (actions.catch) {
  453. this.scope.set('$_ERROR', this.error.message);
  454. this.error = false;
  455. await this._exec(actions.catch, true);
  456. } else {
  457. throw this.error;
  458. }
  459. }
  460. if (!internal && !this.res.headersSent && !this.noOutput) {
  461. this.res.json(this.data);
  462. }
  463. },
  464. _exec: async function(steps, ignoreAbort) {
  465. if (this.res.headersSent) return;
  466. if (typeof steps == 'string') {
  467. return this.exec(await fs.readJSON(`app/modules/${steps}.json`), true);
  468. }
  469. if (this.res.headersSent) {
  470. // do not execute other steps after headers has been sent
  471. return;
  472. }
  473. if (Array.isArray(steps)) {
  474. for (let step of steps) {
  475. await this._exec(step);
  476. if (this.error) return;
  477. }
  478. return;
  479. }
  480. if (steps.disabled) {
  481. return;
  482. }
  483. if (steps.action) {
  484. try {
  485. let module;
  486. if (fs.existsSync(`extensions/server_connect/modules/${steps.module}.js`)) {
  487. module = require(`../../extensions/server_connect/modules/${steps.module}`);
  488. } else if (fs.existsSync(`lib/modules/${steps.module}.js`)) {
  489. module = require(`../modules/${steps.module}`);
  490. } else {
  491. throw new Error(`Module ${steps.module} doesn't exist`);
  492. }
  493. if (typeof module[steps.action] != 'function') {
  494. throw new Error(`Action ${steps.action} doesn't exist in ${steps.module || 'core'}`);
  495. }
  496. debug(`Executing action step ${steps.action}`);
  497. debug(`options: %O`, steps.options);
  498. if (!ignoreAbort && config.abortOnDisconnect && this.req.isDisconnected) {
  499. throw new Error('Aborted');
  500. }
  501. const data = await module[steps.action].call(this, clone(steps.options), steps.name, steps.meta);
  502. if (data instanceof Error) {
  503. throw data;
  504. }
  505. if (steps.name) {
  506. this.scope.set(steps.name, data);
  507. if (steps.output) {
  508. this.data[steps.name] = data;
  509. }
  510. }
  511. } catch (e) {
  512. this.error = e;
  513. return;
  514. }
  515. }
  516. },
  517. parse: function(value, scope) {
  518. return Parser.parseValue(value, scope || this.scope);
  519. },
  520. parseRequired: function(value, type, err) {
  521. if (value === undefined) {
  522. throw new Error(err);
  523. }
  524. let val = Parser.parseValue(value, this.scope);
  525. if (type == '*') {
  526. if (val === undefined) {
  527. throw new Error(err);
  528. }
  529. } else if (type == 'boolean') {
  530. val = !!val;
  531. } else if (typeof val != type) {
  532. throw new Error(err);
  533. }
  534. return val;
  535. },
  536. parseOptional: function(value, type, def) {
  537. if (value === undefined) return def;
  538. let val = Parser.parseValue(value, this.scope);
  539. if (type == '*') {
  540. if (val === undefined) val = def;
  541. } else if (type == 'boolean') {
  542. if (val === undefined) {
  543. val = def;
  544. } else {
  545. val = !!val;
  546. }
  547. } else if (typeof val != type) {
  548. val = def;
  549. }
  550. return val;
  551. },
  552. parseSQL: function(sql) {
  553. if (!sql) return null;
  554. ['values', 'orders'].forEach((prop) => {
  555. if (Array.isArray(sql[prop])) {
  556. sql[prop] = sql[prop].filter((value) => {
  557. if (!value.condition) return true;
  558. return !!Parser.parseValue(value.condition, this.scope);
  559. });
  560. }
  561. });
  562. if (sql.wheres && sql.wheres.rules) {
  563. if (sql.wheres.conditional && !Parser.parseValue(sql.wheres.conditional, this.scope)) {
  564. delete sql.wheres;
  565. } else {
  566. sql.wheres.rules = sql.wheres.rules.filter(function filterConditional(rule) {
  567. if (!rule.rules) return true;
  568. if (rule.conditional && !Parser.parseValue(rule.conditional, this.scope)) return false;
  569. rule.rules = rule.rules.filter(filterConditional, this);
  570. return rule.rules.length;
  571. }, this);
  572. if (!sql.wheres.rules.length) {
  573. delete sql.wheres;
  574. }
  575. }
  576. }
  577. if (sql.sub) {
  578. for (const name in sql.sub) {
  579. sql.sub[name] = this.parseSQL(sql.sub[name]);
  580. }
  581. }
  582. return Parser.parseValue(sql, this.scope);
  583. },
  584. };
  585. module.exports = App;