proxy-host.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import _ from "lodash";
  2. import errs from "../lib/error.js";
  3. import { castJsonIfNeed } from "../lib/helpers.js";
  4. import utils from "../lib/utils.js";
  5. import proxyHostModel from "../models/proxy_host.js";
  6. import internalAuditLog from "./audit-log.js";
  7. import internalCertificate from "./certificate.js";
  8. import internalHost from "./host.js";
  9. import internalNginx from "./nginx.js";
  10. const omissions = () => {
  11. return ["is_deleted", "owner.is_deleted"];
  12. };
  13. const internalProxyHost = {
  14. /**
  15. * @param {Access} access
  16. * @param {Object} data
  17. * @returns {Promise}
  18. */
  19. create: (access, data) => {
  20. let thisData = data;
  21. const createCertificate = thisData.certificate_id === "new";
  22. if (createCertificate) {
  23. delete thisData.certificate_id;
  24. }
  25. return access
  26. .can("proxy_hosts:create", thisData)
  27. .then(() => {
  28. // Get a list of the domain names and check each of them against existing records
  29. const domain_name_check_promises = [];
  30. thisData.domain_names.map((domain_name) => {
  31. domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
  32. return true;
  33. });
  34. return Promise.all(domain_name_check_promises).then((check_results) => {
  35. check_results.map((result) => {
  36. if (result.is_taken) {
  37. throw new errs.ValidationError(`${result.hostname} is already in use`);
  38. }
  39. return true;
  40. });
  41. });
  42. })
  43. .then(() => {
  44. // At this point the domains should have been checked
  45. thisData.owner_user_id = access.token.getUserId(1);
  46. thisData = internalHost.cleanSslHstsData(thisData);
  47. // Fix for db field not having a default value
  48. // for this optional field.
  49. if (typeof thisData.advanced_config === "undefined") {
  50. thisData.advanced_config = "";
  51. }
  52. return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
  53. })
  54. .then((row) => {
  55. if (createCertificate) {
  56. return internalCertificate
  57. .createQuickCertificate(access, thisData)
  58. .then((cert) => {
  59. // update host with cert id
  60. return internalProxyHost.update(access, {
  61. id: row.id,
  62. certificate_id: cert.id,
  63. });
  64. })
  65. .then(() => {
  66. return row;
  67. });
  68. }
  69. return row;
  70. })
  71. .then((row) => {
  72. // re-fetch with cert
  73. return internalProxyHost.get(access, {
  74. id: row.id,
  75. expand: ["certificate", "owner", "access_list.[clients,items]"],
  76. });
  77. })
  78. .then((row) => {
  79. // Configure nginx
  80. return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
  81. return row;
  82. });
  83. })
  84. .then((row) => {
  85. // Audit log
  86. thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
  87. // Add to audit log
  88. return internalAuditLog
  89. .add(access, {
  90. action: "created",
  91. object_type: "proxy-host",
  92. object_id: row.id,
  93. meta: thisData,
  94. })
  95. .then(() => {
  96. return row;
  97. });
  98. });
  99. },
  100. /**
  101. * @param {Access} access
  102. * @param {Object} data
  103. * @param {Number} data.id
  104. * @return {Promise}
  105. */
  106. update: (access, data) => {
  107. let thisData = data;
  108. const create_certificate = thisData.certificate_id === "new";
  109. if (create_certificate) {
  110. delete thisData.certificate_id;
  111. }
  112. return access
  113. .can("proxy_hosts:update", thisData.id)
  114. .then((/*access_data*/) => {
  115. // Get a list of the domain names and check each of them against existing records
  116. const domain_name_check_promises = [];
  117. if (typeof thisData.domain_names !== "undefined") {
  118. thisData.domain_names.map((domain_name) => {
  119. return domain_name_check_promises.push(
  120. internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
  121. );
  122. });
  123. return Promise.all(domain_name_check_promises).then((check_results) => {
  124. check_results.map((result) => {
  125. if (result.is_taken) {
  126. throw new errs.ValidationError(`${result.hostname} is already in use`);
  127. }
  128. return true;
  129. });
  130. });
  131. }
  132. })
  133. .then(() => {
  134. return internalProxyHost.get(access, { id: thisData.id });
  135. })
  136. .then((row) => {
  137. if (row.id !== thisData.id) {
  138. // Sanity check that something crazy hasn't happened
  139. throw new errs.InternalValidationError(
  140. `Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
  141. );
  142. }
  143. if (create_certificate) {
  144. return internalCertificate
  145. .createQuickCertificate(access, {
  146. domain_names: thisData.domain_names || row.domain_names,
  147. meta: _.assign({}, row.meta, thisData.meta),
  148. })
  149. .then((cert) => {
  150. // update host with cert id
  151. thisData.certificate_id = cert.id;
  152. })
  153. .then(() => {
  154. return row;
  155. });
  156. }
  157. return row;
  158. })
  159. .then((row) => {
  160. // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
  161. thisData = _.assign(
  162. {},
  163. {
  164. domain_names: row.domain_names,
  165. },
  166. data,
  167. );
  168. thisData = internalHost.cleanSslHstsData(thisData, row);
  169. return proxyHostModel
  170. .query()
  171. .where({ id: thisData.id })
  172. .patch(thisData)
  173. .then(utils.omitRow(omissions()))
  174. .then((saved_row) => {
  175. // Add to audit log
  176. return internalAuditLog
  177. .add(access, {
  178. action: "updated",
  179. object_type: "proxy-host",
  180. object_id: row.id,
  181. meta: thisData,
  182. })
  183. .then(() => {
  184. return saved_row;
  185. });
  186. });
  187. })
  188. .then(() => {
  189. return internalProxyHost
  190. .get(access, {
  191. id: thisData.id,
  192. expand: ["owner", "certificate", "access_list.[clients,items]"],
  193. })
  194. .then((row) => {
  195. if (!row.enabled) {
  196. // No need to add nginx config if host is disabled
  197. return row;
  198. }
  199. // Configure nginx
  200. return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
  201. row.meta = new_meta;
  202. return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
  203. });
  204. });
  205. });
  206. },
  207. /**
  208. * @param {Access} access
  209. * @param {Object} data
  210. * @param {Number} data.id
  211. * @param {Array} [data.expand]
  212. * @param {Array} [data.omit]
  213. * @return {Promise}
  214. */
  215. get: (access, data) => {
  216. const thisData = data || {};
  217. return access
  218. .can("proxy_hosts:get", thisData.id)
  219. .then((access_data) => {
  220. const query = proxyHostModel
  221. .query()
  222. .where("is_deleted", 0)
  223. .andWhere("id", thisData.id)
  224. .allowGraph("[owner,access_list.[clients,items],certificate]")
  225. .first();
  226. if (access_data.permission_visibility !== "all") {
  227. query.andWhere("owner_user_id", access.token.getUserId(1));
  228. }
  229. if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
  230. query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
  231. }
  232. return query.then(utils.omitRow(omissions()));
  233. })
  234. .then((row) => {
  235. if (!row || !row.id) {
  236. throw new errs.ItemNotFoundError(thisData.id);
  237. }
  238. const thisRow = internalHost.cleanRowCertificateMeta(row);
  239. // Custom omissions
  240. if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
  241. return _.omit(row, thisData.omit);
  242. }
  243. return thisRow;
  244. });
  245. },
  246. /**
  247. * @param {Access} access
  248. * @param {Object} data
  249. * @param {Number} data.id
  250. * @param {String} [data.reason]
  251. * @returns {Promise}
  252. */
  253. delete: (access, data) => {
  254. return access
  255. .can("proxy_hosts:delete", data.id)
  256. .then(() => {
  257. return internalProxyHost.get(access, { id: data.id });
  258. })
  259. .then((row) => {
  260. if (!row || !row.id) {
  261. throw new errs.ItemNotFoundError(data.id);
  262. }
  263. return proxyHostModel
  264. .query()
  265. .where("id", row.id)
  266. .patch({
  267. is_deleted: 1,
  268. })
  269. .then(() => {
  270. // Delete Nginx Config
  271. return internalNginx.deleteConfig("proxy_host", row).then(() => {
  272. return internalNginx.reload();
  273. });
  274. })
  275. .then(() => {
  276. // Add to audit log
  277. return internalAuditLog.add(access, {
  278. action: "deleted",
  279. object_type: "proxy-host",
  280. object_id: row.id,
  281. meta: _.omit(row, omissions()),
  282. });
  283. });
  284. })
  285. .then(() => {
  286. return true;
  287. });
  288. },
  289. /**
  290. * @param {Access} access
  291. * @param {Object} data
  292. * @param {Number} data.id
  293. * @param {String} [data.reason]
  294. * @returns {Promise}
  295. */
  296. enable: (access, data) => {
  297. return access
  298. .can("proxy_hosts:update", data.id)
  299. .then(() => {
  300. return internalProxyHost.get(access, {
  301. id: data.id,
  302. expand: ["certificate", "owner", "access_list"],
  303. });
  304. })
  305. .then((row) => {
  306. if (!row || !row.id) {
  307. throw new errs.ItemNotFoundError(data.id);
  308. }
  309. if (row.enabled) {
  310. throw new errs.ValidationError("Host is already enabled");
  311. }
  312. row.enabled = 1;
  313. return proxyHostModel
  314. .query()
  315. .where("id", row.id)
  316. .patch({
  317. enabled: 1,
  318. })
  319. .then(() => {
  320. // Configure nginx
  321. return internalNginx.configure(proxyHostModel, "proxy_host", row);
  322. })
  323. .then(() => {
  324. // Add to audit log
  325. return internalAuditLog.add(access, {
  326. action: "enabled",
  327. object_type: "proxy-host",
  328. object_id: row.id,
  329. meta: _.omit(row, omissions()),
  330. });
  331. });
  332. })
  333. .then(() => {
  334. return true;
  335. });
  336. },
  337. /**
  338. * @param {Access} access
  339. * @param {Object} data
  340. * @param {Number} data.id
  341. * @param {String} [data.reason]
  342. * @returns {Promise}
  343. */
  344. disable: (access, data) => {
  345. return access
  346. .can("proxy_hosts:update", data.id)
  347. .then(() => {
  348. return internalProxyHost.get(access, { id: data.id });
  349. })
  350. .then((row) => {
  351. if (!row || !row.id) {
  352. throw new errs.ItemNotFoundError(data.id);
  353. }
  354. if (!row.enabled) {
  355. throw new errs.ValidationError("Host is already disabled");
  356. }
  357. row.enabled = 0;
  358. return proxyHostModel
  359. .query()
  360. .where("id", row.id)
  361. .patch({
  362. enabled: 0,
  363. })
  364. .then(() => {
  365. // Delete Nginx Config
  366. return internalNginx.deleteConfig("proxy_host", row).then(() => {
  367. return internalNginx.reload();
  368. });
  369. })
  370. .then(() => {
  371. // Add to audit log
  372. return internalAuditLog.add(access, {
  373. action: "disabled",
  374. object_type: "proxy-host",
  375. object_id: row.id,
  376. meta: _.omit(row, omissions()),
  377. });
  378. });
  379. })
  380. .then(() => {
  381. return true;
  382. });
  383. },
  384. /**
  385. * All Hosts
  386. *
  387. * @param {Access} access
  388. * @param {Array} [expand]
  389. * @param {String} [search_query]
  390. * @returns {Promise}
  391. */
  392. getAll: async (access, expand, searchQuery) => {
  393. const accessData = await access.can("proxy_hosts:list");
  394. const query = proxyHostModel
  395. .query()
  396. .where("is_deleted", 0)
  397. .groupBy("id")
  398. .allowGraph("[owner,access_list,certificate]")
  399. .orderBy(castJsonIfNeed("domain_names"), "ASC");
  400. if (accessData.permission_visibility !== "all") {
  401. query.andWhere("owner_user_id", access.token.getUserId(1));
  402. }
  403. // Query is used for searching
  404. if (typeof searchQuery === "string" && searchQuery.length > 0) {
  405. query.where(function () {
  406. this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
  407. });
  408. }
  409. if (typeof expand !== "undefined" && expand !== null) {
  410. query.withGraphFetched(`[${expand.join(", ")}]`);
  411. }
  412. const rows = await query.then(utils.omitRows(omissions()));
  413. if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
  414. return internalHost.cleanAllRowsCertificateMeta(rows);
  415. }
  416. return rows;
  417. },
  418. /**
  419. * Report use
  420. *
  421. * @param {Number} user_id
  422. * @param {String} visibility
  423. * @returns {Promise}
  424. */
  425. getCount: (user_id, visibility) => {
  426. const query = proxyHostModel.query().count("id as count").where("is_deleted", 0);
  427. if (visibility !== "all") {
  428. query.andWhere("owner_user_id", user_id);
  429. }
  430. return query.first().then((row) => {
  431. return Number.parseInt(row.count, 10);
  432. });
  433. },
  434. };
  435. export default internalProxyHost;