Skip to content

Commit

Permalink
Merge pull request #4344 from NginxProxyManager/stream-ssl
Browse files Browse the repository at this point in the history
SSL for Streams - 2025
  • Loading branch information
jc21 authored Feb 5, 2025
2 parents 498109a + 6a60627 commit c56c95a
Show file tree
Hide file tree
Showing 26 changed files with 818 additions and 182 deletions.
119 changes: 98 additions & 21 deletions backend/internal/stream.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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 {castJsonIfNeed} = require('../lib/helpers');
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');
const {castJsonIfNeed} = require('../lib/helpers');

function omissions () {
return ['is_deleted'];
return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
}

const internalStream = {
Expand All @@ -18,6 +20,12 @@ const internalStream = {
* @returns {Promise}
*/
create: (access, data) => {
const 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
Expand All @@ -27,16 +35,44 @@ const internalStream = {
data.meta = {};
}

// streams aren't routed by domain name so don't store domain names in the DB
let data_no_domains = structuredClone(data);
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)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
return row;
});
})
.then((row) => {
Expand All @@ -60,6 +96,12 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
const 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
Expand All @@ -71,16 +113,32 @@ 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)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
Expand All @@ -93,6 +151,17 @@ const internalStream = {
return saved_row;
});
});
})
.then(() => {
return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
.then((row) => {
return internalNginx.configure(streamModel, 'stream', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
});
},

Expand All @@ -115,7 +184,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.first();

if (access_data.permission_visibility !== 'all') {
Expand All @@ -132,6 +201,7 @@ const internalStream = {
if (!row || !row.id) {
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);
Expand Down Expand Up @@ -197,14 +267,14 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ['owner']
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
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;
Expand Down Expand Up @@ -250,7 +320,7 @@ const internalStream = {
if (!row || !row.id) {
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;
Expand Down Expand Up @@ -298,7 +368,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');

if (access_data.permission_visibility !== 'all') {
Expand All @@ -317,6 +387,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;
});
},

Expand Down
38 changes: 38 additions & 0 deletions backend/migrations/20240427161436_stream_ssl.js
Original file line number Diff line number Diff line change
@@ -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');
});
};
26 changes: 18 additions & 8 deletions backend/models/stream.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// Objection Docs:
// http://vincit.github.io/objection.js/

const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
const Model = require('objection').Model;
const db = require('../db');
const helpers = require('../lib/helpers');
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');

Model.knex(db);

const boolFields = [
'enabled',
'is_deleted',
'tcp_forwarding',
'udp_forwarding',
Expand Down Expand Up @@ -64,6 +63,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);
}
}
};
}
Expand Down
20 changes: 17 additions & 3 deletions backend/schema/components/stream-object.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
"incoming_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"if": {"properties": {"tcp_forwarding": {"const": true}}},
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
"maximum": 65535
},
"forwarding_host": {
"anyOf": [
Expand Down Expand Up @@ -55,8 +53,24 @@
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"meta": {
"type": "object"
},
"owner": {
"$ref": "./user-object.json"
},
"certificate": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./certificate-object.json"
}
]
}
}
}
5 changes: 3 additions & 2 deletions backend/schema/paths/nginx/streams/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["access_list", "owner", "certificate"]
"enum": ["owner", "certificate"]
}
}
],
Expand All @@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
"enabled": true
"enabled": true,
"certificate_id": 0
}
]
}
Expand Down
6 changes: 5 additions & 1 deletion backend/schema/paths/nginx/streams/post.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
}
Expand Down Expand Up @@ -73,7 +76,8 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
}
},
"certificate_id": 0
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion backend/schema/paths/nginx/streams/streamID/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
"enabled": true
"enabled": true,
"certificate_id": 0
}
}
},
Expand Down
Loading

0 comments on commit c56c95a

Please sign in to comment.