Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

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