diff --git a/backend/internal/stream.js b/backend/internal/stream.js index a159cfdd..b02ec967 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -1,9 +1,11 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -const streamModel = require('../models/stream'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const streamModel = require('../models/stream'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const internalHost = require('./host'); function omissions () { return ['is_deleted']; @@ -17,6 +19,12 @@ const internalStream = { * @returns {Promise} */ create: (access, data) => { + let create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + return access.can('streams:create', data) .then((/*access_data*/) => { // TODO: At this point the existing ports should have been checked @@ -26,11 +34,40 @@ const internalStream = { data.meta = {}; } + let data_no_domains = structuredClone(data); + + // streams aren't routed by domain name so don't store domain names in the DB + delete data_no_domains.domain_names; + return streamModel .query() - .insertAndFetch(data) + .insertAndFetch(data_no_domains) .then(utils.omitRow(omissions())); }) + .then((row) => { + if (create_certificate) { + return internalCertificate.createQuickCertificate(access, data) + .then((cert) => { + // update host with cert id + return internalStream.update(access, { + id: row.id, + certificate_id: cert.id + }); + }) + .then(() => { + return row; + }); + } else { + return row; + } + }) + .then((row) => { + // re-fetch with cert + return internalStream.get(access, { + id: row.id, + expand: ['certificate', 'owner'] + }); + }) .then((row) => { // Configure nginx return internalNginx.configure(streamModel, 'stream', row) @@ -59,6 +96,12 @@ const internalStream = { * @return {Promise} */ update: (access, data) => { + let create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + return access.can('streams:update', data.id) .then((/*access_data*/) => { // TODO: at this point the existing streams should have been checked @@ -70,6 +113,28 @@ const internalStream = { throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); } + if (create_certificate) { + return internalCertificate.createQuickCertificate(access, { + domain_names: data.domain_names || row.domain_names, + meta: _.assign({}, row.meta, data.meta) + }) + .then((cert) => { + // update host with cert id + data.certificate_id = cert.id; + }) + .then(() => { + return row; + }); + } else { + return row; + } + }) + .then((row) => { + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + data = _.assign({}, { + domain_names: row.domain_names + }, data); + return streamModel .query() .patchAndFetchById(row.id, data) @@ -114,7 +179,7 @@ const internalStream = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowGraph('[owner]') + .allowGraph('[owner,certificate]') .first(); if (access_data.permission_visibility !== 'all') { @@ -131,6 +196,7 @@ const internalStream = { if (!row) { throw new error.ItemNotFoundError(data.id); } + row = internalHost.cleanRowCertificateMeta(row); // Custom omissions if (typeof data.omit !== 'undefined' && data.omit !== null) { row = _.omit(row, data.omit); @@ -196,14 +262,14 @@ const internalStream = { .then(() => { return internalStream.get(access, { id: data.id, - expand: ['owner'] + expand: ['certificate', 'owner'] }); }) .then((row) => { if (!row) { throw new error.ItemNotFoundError(data.id); } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); + throw new error.ValidationError('Stream is already enabled'); } row.enabled = 1; @@ -249,7 +315,7 @@ const internalStream = { if (!row) { throw new error.ItemNotFoundError(data.id); } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); + throw new error.ValidationError('Stream is already disabled'); } row.enabled = 0; @@ -297,7 +363,7 @@ const internalStream = { .query() .where('is_deleted', 0) .groupBy('id') - .allowGraph('[owner]') + .allowGraph('[owner,certificate]') .orderBy('incoming_port', 'ASC'); if (access_data.permission_visibility !== 'all') { @@ -316,6 +382,13 @@ const internalStream = { } return query.then(utils.omitRows(omissions())); + }) + .then((rows) => { + if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { + return internalHost.cleanAllRowsCertificateMeta(rows); + } + + return rows; }); }, diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js new file mode 100644 index 00000000..5f47b18e --- /dev/null +++ b/backend/migrations/20240427161436_stream_ssl.js @@ -0,0 +1,38 @@ +const migrate_name = 'stream_ssl'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('stream', (table) => { + table.integer('certificate_id').notNull().unsigned().defaultTo(0); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.table('stream', (table) => { + table.dropColumn('certificate_id'); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; diff --git a/backend/models/stream.js b/backend/models/stream.js index 7d84d2c3..23dd7ac0 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -1,10 +1,11 @@ // Objection Docs: // http://vincit.github.io/objection.js/ -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const now = require('./now_helper'); +const Certificate = require('./certificate'); Model.knex(db); @@ -47,6 +48,17 @@ class Stream extends Model { modify: function (qb) { qb.where('user.is_deleted', 0); } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'stream.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } } }; } diff --git a/backend/schema/endpoints/streams.json b/backend/schema/endpoints/streams.json index 159c8036..7d97d2ab 100644 --- a/backend/schema/endpoints/streams.json +++ b/backend/schema/endpoints/streams.json @@ -46,6 +46,12 @@ "udp_forwarding": { "type": "boolean" }, + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "certificate_id": { + "$ref": "../definitions.json#/definitions/certificate_id" + }, "enabled": { "$ref": "../definitions.json#/definitions/enabled" }, @@ -78,6 +84,12 @@ "udp_forwarding": { "$ref": "#/definitions/udp_forwarding" }, + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "certificate_id": { + "$ref": "#/definitions/certificate_id" + }, "enabled": { "$ref": "#/definitions/enabled" }, @@ -137,6 +149,12 @@ "udp_forwarding": { "$ref": "#/definitions/udp_forwarding" }, + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "certificate_id": { + "$ref": "#/definitions/certificate_id" + }, "meta": { "$ref": "#/definitions/meta" } @@ -177,6 +195,12 @@ "udp_forwarding": { "$ref": "#/definitions/udp_forwarding" }, + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "certificate_id": { + "$ref": "#/definitions/certificate_id" + }, "meta": { "$ref": "#/definitions/meta" } diff --git a/frontend/js/app/nginx/stream/main.js b/frontend/js/app/nginx/stream/main.js index 8a86e583..83bdc15e 100644 --- a/frontend/js/app/nginx/stream/main.js +++ b/frontend/js/app/nginx/stream/main.js @@ -88,7 +88,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - view.fetch(['owner']) + view.fetch(['owner', 'certificate']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) {