Skip to content

Commit c56c95a

Browse files
authored
Merge pull request #4344 from NginxProxyManager/stream-ssl
SSL for Streams - 2025
2 parents 498109a + 6a60627 commit c56c95a

File tree

26 files changed

+818
-182
lines changed

26 files changed

+818
-182
lines changed

backend/internal/stream.js

+98-21
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
const _ = require('lodash');
2-
const error = require('../lib/error');
3-
const utils = require('../lib/utils');
4-
const streamModel = require('../models/stream');
5-
const internalNginx = require('./nginx');
6-
const internalAuditLog = require('./audit-log');
7-
const {castJsonIfNeed} = require('../lib/helpers');
1+
const _ = require('lodash');
2+
const error = require('../lib/error');
3+
const utils = require('../lib/utils');
4+
const streamModel = require('../models/stream');
5+
const internalNginx = require('./nginx');
6+
const internalAuditLog = require('./audit-log');
7+
const internalCertificate = require('./certificate');
8+
const internalHost = require('./host');
9+
const {castJsonIfNeed} = require('../lib/helpers');
810

911
function omissions () {
10-
return ['is_deleted'];
12+
return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
1113
}
1214

1315
const internalStream = {
@@ -18,6 +20,12 @@ const internalStream = {
1820
* @returns {Promise}
1921
*/
2022
create: (access, data) => {
23+
const create_certificate = data.certificate_id === 'new';
24+
25+
if (create_certificate) {
26+
delete data.certificate_id;
27+
}
28+
2129
return access.can('streams:create', data)
2230
.then((/*access_data*/) => {
2331
// TODO: At this point the existing ports should have been checked
@@ -27,16 +35,44 @@ const internalStream = {
2735
data.meta = {};
2836
}
2937

38+
// streams aren't routed by domain name so don't store domain names in the DB
39+
let data_no_domains = structuredClone(data);
40+
delete data_no_domains.domain_names;
41+
3042
return streamModel
3143
.query()
32-
.insertAndFetch(data)
44+
.insertAndFetch(data_no_domains)
3345
.then(utils.omitRow(omissions()));
3446
})
47+
.then((row) => {
48+
if (create_certificate) {
49+
return internalCertificate.createQuickCertificate(access, data)
50+
.then((cert) => {
51+
// update host with cert id
52+
return internalStream.update(access, {
53+
id: row.id,
54+
certificate_id: cert.id
55+
});
56+
})
57+
.then(() => {
58+
return row;
59+
});
60+
} else {
61+
return row;
62+
}
63+
})
64+
.then((row) => {
65+
// re-fetch with cert
66+
return internalStream.get(access, {
67+
id: row.id,
68+
expand: ['certificate', 'owner']
69+
});
70+
})
3571
.then((row) => {
3672
// Configure nginx
3773
return internalNginx.configure(streamModel, 'stream', row)
3874
.then(() => {
39-
return internalStream.get(access, {id: row.id, expand: ['owner']});
75+
return row;
4076
});
4177
})
4278
.then((row) => {
@@ -60,6 +96,12 @@ const internalStream = {
6096
* @return {Promise}
6197
*/
6298
update: (access, data) => {
99+
const create_certificate = data.certificate_id === 'new';
100+
101+
if (create_certificate) {
102+
delete data.certificate_id;
103+
}
104+
63105
return access.can('streams:update', data.id)
64106
.then((/*access_data*/) => {
65107
// TODO: at this point the existing streams should have been checked
@@ -71,16 +113,32 @@ const internalStream = {
71113
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
72114
}
73115

116+
if (create_certificate) {
117+
return internalCertificate.createQuickCertificate(access, {
118+
domain_names: data.domain_names || row.domain_names,
119+
meta: _.assign({}, row.meta, data.meta)
120+
})
121+
.then((cert) => {
122+
// update host with cert id
123+
data.certificate_id = cert.id;
124+
})
125+
.then(() => {
126+
return row;
127+
});
128+
} else {
129+
return row;
130+
}
131+
})
132+
.then((row) => {
133+
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
134+
data = _.assign({}, {
135+
domain_names: row.domain_names
136+
}, data);
137+
74138
return streamModel
75139
.query()
76140
.patchAndFetchById(row.id, data)
77141
.then(utils.omitRow(omissions()))
78-
.then((saved_row) => {
79-
return internalNginx.configure(streamModel, 'stream', saved_row)
80-
.then(() => {
81-
return internalStream.get(access, {id: row.id, expand: ['owner']});
82-
});
83-
})
84142
.then((saved_row) => {
85143
// Add to audit log
86144
return internalAuditLog.add(access, {
@@ -93,6 +151,17 @@ const internalStream = {
93151
return saved_row;
94152
});
95153
});
154+
})
155+
.then(() => {
156+
return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
157+
.then((row) => {
158+
return internalNginx.configure(streamModel, 'stream', row)
159+
.then((new_meta) => {
160+
row.meta = new_meta;
161+
row = internalHost.cleanRowCertificateMeta(row);
162+
return _.omit(row, omissions());
163+
});
164+
});
96165
});
97166
},
98167

@@ -115,7 +184,7 @@ const internalStream = {
115184
.query()
116185
.where('is_deleted', 0)
117186
.andWhere('id', data.id)
118-
.allowGraph('[owner]')
187+
.allowGraph('[owner,certificate]')
119188
.first();
120189

121190
if (access_data.permission_visibility !== 'all') {
@@ -132,6 +201,7 @@ const internalStream = {
132201
if (!row || !row.id) {
133202
throw new error.ItemNotFoundError(data.id);
134203
}
204+
row = internalHost.cleanRowCertificateMeta(row);
135205
// Custom omissions
136206
if (typeof data.omit !== 'undefined' && data.omit !== null) {
137207
row = _.omit(row, data.omit);
@@ -197,14 +267,14 @@ const internalStream = {
197267
.then(() => {
198268
return internalStream.get(access, {
199269
id: data.id,
200-
expand: ['owner']
270+
expand: ['certificate', 'owner']
201271
});
202272
})
203273
.then((row) => {
204274
if (!row || !row.id) {
205275
throw new error.ItemNotFoundError(data.id);
206276
} else if (row.enabled) {
207-
throw new error.ValidationError('Host is already enabled');
277+
throw new error.ValidationError('Stream is already enabled');
208278
}
209279

210280
row.enabled = 1;
@@ -250,7 +320,7 @@ const internalStream = {
250320
if (!row || !row.id) {
251321
throw new error.ItemNotFoundError(data.id);
252322
} else if (!row.enabled) {
253-
throw new error.ValidationError('Host is already disabled');
323+
throw new error.ValidationError('Stream is already disabled');
254324
}
255325

256326
row.enabled = 0;
@@ -298,7 +368,7 @@ const internalStream = {
298368
.query()
299369
.where('is_deleted', 0)
300370
.groupBy('id')
301-
.allowGraph('[owner]')
371+
.allowGraph('[owner,certificate]')
302372
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
303373

304374
if (access_data.permission_visibility !== 'all') {
@@ -317,6 +387,13 @@ const internalStream = {
317387
}
318388

319389
return query.then(utils.omitRows(omissions()));
390+
})
391+
.then((rows) => {
392+
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
393+
return internalHost.cleanAllRowsCertificateMeta(rows);
394+
}
395+
396+
return rows;
320397
});
321398
},
322399

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const migrate_name = 'stream_ssl';
2+
const logger = require('../logger').migrate;
3+
4+
/**
5+
* Migrate
6+
*
7+
* @see http://knexjs.org/#Schema
8+
*
9+
* @param {Object} knex
10+
* @returns {Promise}
11+
*/
12+
exports.up = function (knex) {
13+
logger.info('[' + migrate_name + '] Migrating Up...');
14+
15+
return knex.schema.table('stream', (table) => {
16+
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
17+
})
18+
.then(function () {
19+
logger.info('[' + migrate_name + '] stream Table altered');
20+
});
21+
};
22+
23+
/**
24+
* Undo Migrate
25+
*
26+
* @param {Object} knex
27+
* @returns {Promise}
28+
*/
29+
exports.down = function (knex) {
30+
logger.info('[' + migrate_name + '] Migrating Down...');
31+
32+
return knex.schema.table('stream', (table) => {
33+
table.dropColumn('certificate_id');
34+
})
35+
.then(function () {
36+
logger.info('[' + migrate_name + '] stream Table altered');
37+
});
38+
};

backend/models/stream.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
// Objection Docs:
2-
// http://vincit.github.io/objection.js/
3-
4-
const db = require('../db');
5-
const helpers = require('../lib/helpers');
6-
const Model = require('objection').Model;
7-
const User = require('./user');
8-
const now = require('./now_helper');
1+
const Model = require('objection').Model;
2+
const db = require('../db');
3+
const helpers = require('../lib/helpers');
4+
const User = require('./user');
5+
const Certificate = require('./certificate');
6+
const now = require('./now_helper');
97

108
Model.knex(db);
119

1210
const boolFields = [
11+
'enabled',
1312
'is_deleted',
1413
'tcp_forwarding',
1514
'udp_forwarding',
@@ -64,6 +63,17 @@ class Stream extends Model {
6463
modify: function (qb) {
6564
qb.where('user.is_deleted', 0);
6665
}
66+
},
67+
certificate: {
68+
relation: Model.HasOneRelation,
69+
modelClass: Certificate,
70+
join: {
71+
from: 'stream.certificate_id',
72+
to: 'certificate.id'
73+
},
74+
modify: function (qb) {
75+
qb.where('certificate.is_deleted', 0);
76+
}
6777
}
6878
};
6979
}

backend/schema/components/stream-object.json

+17-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
"incoming_port": {
2020
"type": "integer",
2121
"minimum": 1,
22-
"maximum": 65535,
23-
"if": {"properties": {"tcp_forwarding": {"const": true}}},
24-
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
22+
"maximum": 65535
2523
},
2624
"forwarding_host": {
2725
"anyOf": [
@@ -55,8 +53,24 @@
5553
"enabled": {
5654
"$ref": "../common.json#/properties/enabled"
5755
},
56+
"certificate_id": {
57+
"$ref": "../common.json#/properties/certificate_id"
58+
},
5859
"meta": {
5960
"type": "object"
61+
},
62+
"owner": {
63+
"$ref": "./user-object.json"
64+
},
65+
"certificate": {
66+
"oneOf": [
67+
{
68+
"type": "null"
69+
},
70+
{
71+
"$ref": "./certificate-object.json"
72+
}
73+
]
6074
}
6175
}
6276
}

backend/schema/paths/nginx/streams/get.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"description": "Expansions",
1515
"schema": {
1616
"type": "string",
17-
"enum": ["access_list", "owner", "certificate"]
17+
"enum": ["owner", "certificate"]
1818
}
1919
}
2020
],
@@ -40,7 +40,8 @@
4040
"nginx_online": true,
4141
"nginx_err": null
4242
},
43-
"enabled": true
43+
"enabled": true,
44+
"certificate_id": 0
4445
}
4546
]
4647
}

backend/schema/paths/nginx/streams/post.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"udp_forwarding": {
3333
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
3434
},
35+
"certificate_id": {
36+
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
37+
},
3538
"meta": {
3639
"$ref": "../../../components/stream-object.json#/properties/meta"
3740
}
@@ -73,7 +76,8 @@
7376
"nickname": "Admin",
7477
"avatar": "",
7578
"roles": ["admin"]
76-
}
79+
},
80+
"certificate_id": 0
7781
}
7882
}
7983
},

backend/schema/paths/nginx/streams/streamID/get.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"nginx_online": true,
4141
"nginx_err": null
4242
},
43-
"enabled": true
43+
"enabled": true,
44+
"certificate_id": 0
4445
}
4546
}
4647
},

0 commit comments

Comments
 (0)