mirror of
https://github.com/xiaoxinpro/nginx-proxy-manager-zh.git
synced 2025-02-10 21:48:14 -05:00
Merge pull request #4344 from NginxProxyManager/stream-ssl
SSL for Streams - 2025
This commit is contained in:
commit
c56c95a59a
@ -4,10 +4,12 @@ const utils = require('../lib/utils');
|
|||||||
const streamModel = require('../models/stream');
|
const streamModel = require('../models/stream');
|
||||||
const internalNginx = require('./nginx');
|
const internalNginx = require('./nginx');
|
||||||
const internalAuditLog = require('./audit-log');
|
const internalAuditLog = require('./audit-log');
|
||||||
|
const internalCertificate = require('./certificate');
|
||||||
|
const internalHost = require('./host');
|
||||||
const {castJsonIfNeed} = require('../lib/helpers');
|
const {castJsonIfNeed} = require('../lib/helpers');
|
||||||
|
|
||||||
function omissions () {
|
function omissions () {
|
||||||
return ['is_deleted'];
|
return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalStream = {
|
const internalStream = {
|
||||||
@ -18,6 +20,12 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: (access, data) => {
|
create: (access, data) => {
|
||||||
|
const create_certificate = data.certificate_id === 'new';
|
||||||
|
|
||||||
|
if (create_certificate) {
|
||||||
|
delete data.certificate_id;
|
||||||
|
}
|
||||||
|
|
||||||
return access.can('streams:create', data)
|
return access.can('streams:create', data)
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// TODO: At this point the existing ports should have been checked
|
// TODO: At this point the existing ports should have been checked
|
||||||
@ -27,16 +35,44 @@ const internalStream = {
|
|||||||
data.meta = {};
|
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
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.insertAndFetch(data)
|
.insertAndFetch(data_no_domains)
|
||||||
.then(utils.omitRow(omissions()));
|
.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) => {
|
.then((row) => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(streamModel, 'stream', row)
|
return internalNginx.configure(streamModel, 'stream', row)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalStream.get(access, {id: row.id, expand: ['owner']});
|
return row;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
@ -60,6 +96,12 @@ const internalStream = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
|
const create_certificate = data.certificate_id === 'new';
|
||||||
|
|
||||||
|
if (create_certificate) {
|
||||||
|
delete data.certificate_id;
|
||||||
|
}
|
||||||
|
|
||||||
return access.can('streams:update', data.id)
|
return access.can('streams:update', data.id)
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// TODO: at this point the existing streams should have been checked
|
// TODO: at this point the existing streams should have been checked
|
||||||
@ -71,16 +113,32 @@ const internalStream = {
|
|||||||
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
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
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.patchAndFetchById(row.id, data)
|
.patchAndFetchById(row.id, data)
|
||||||
.then(utils.omitRow(omissions()))
|
.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) => {
|
.then((saved_row) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
@ -93,6 +151,17 @@ const internalStream = {
|
|||||||
return saved_row;
|
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());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -115,7 +184,7 @@ const internalStream = {
|
|||||||
.query()
|
.query()
|
||||||
.where('is_deleted', 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere('id', data.id)
|
.andWhere('id', data.id)
|
||||||
.allowGraph('[owner]')
|
.allowGraph('[owner,certificate]')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (access_data.permission_visibility !== 'all') {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
@ -132,6 +201,7 @@ const internalStream = {
|
|||||||
if (!row || !row.id) {
|
if (!row || !row.id) {
|
||||||
throw new error.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
// Custom omissions
|
// Custom omissions
|
||||||
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
row = _.omit(row, data.omit);
|
row = _.omit(row, data.omit);
|
||||||
@ -197,14 +267,14 @@ const internalStream = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return internalStream.get(access, {
|
return internalStream.get(access, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
expand: ['owner']
|
expand: ['certificate', 'owner']
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row || !row.id) {
|
if (!row || !row.id) {
|
||||||
throw new error.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
} else if (row.enabled) {
|
} else if (row.enabled) {
|
||||||
throw new error.ValidationError('Host is already enabled');
|
throw new error.ValidationError('Stream is already enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 1;
|
row.enabled = 1;
|
||||||
@ -250,7 +320,7 @@ const internalStream = {
|
|||||||
if (!row || !row.id) {
|
if (!row || !row.id) {
|
||||||
throw new error.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
} else if (!row.enabled) {
|
} else if (!row.enabled) {
|
||||||
throw new error.ValidationError('Host is already disabled');
|
throw new error.ValidationError('Stream is already disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 0;
|
row.enabled = 0;
|
||||||
@ -298,7 +368,7 @@ const internalStream = {
|
|||||||
.query()
|
.query()
|
||||||
.where('is_deleted', 0)
|
.where('is_deleted', 0)
|
||||||
.groupBy('id')
|
.groupBy('id')
|
||||||
.allowGraph('[owner]')
|
.allowGraph('[owner,certificate]')
|
||||||
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
|
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
|
||||||
|
|
||||||
if (access_data.permission_visibility !== 'all') {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
@ -317,6 +387,13 @@ const internalStream = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRows(omissions()));
|
return query.then(utils.omitRows(omissions()));
|
||||||
|
})
|
||||||
|
.then((rows) => {
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
|
||||||
|
return internalHost.cleanAllRowsCertificateMeta(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
38
backend/migrations/20240427161436_stream_ssl.js
Normal file
38
backend/migrations/20240427161436_stream_ssl.js
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
};
|
@ -1,15 +1,14 @@
|
|||||||
// Objection Docs:
|
const Model = require('objection').Model;
|
||||||
// http://vincit.github.io/objection.js/
|
|
||||||
|
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const helpers = require('../lib/helpers');
|
const helpers = require('../lib/helpers');
|
||||||
const Model = require('objection').Model;
|
|
||||||
const User = require('./user');
|
const User = require('./user');
|
||||||
|
const Certificate = require('./certificate');
|
||||||
const now = require('./now_helper');
|
const now = require('./now_helper');
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = [
|
const boolFields = [
|
||||||
|
'enabled',
|
||||||
'is_deleted',
|
'is_deleted',
|
||||||
'tcp_forwarding',
|
'tcp_forwarding',
|
||||||
'udp_forwarding',
|
'udp_forwarding',
|
||||||
@ -64,6 +63,17 @@ class Stream extends Model {
|
|||||||
modify: function (qb) {
|
modify: function (qb) {
|
||||||
qb.where('user.is_deleted', 0);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,7 @@
|
|||||||
"incoming_port": {
|
"incoming_port": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 65535,
|
"maximum": 65535
|
||||||
"if": {"properties": {"tcp_forwarding": {"const": true}}},
|
|
||||||
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
|
|
||||||
},
|
},
|
||||||
"forwarding_host": {
|
"forwarding_host": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@ -55,8 +53,24 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"$ref": "../common.json#/properties/enabled"
|
"$ref": "../common.json#/properties/enabled"
|
||||||
},
|
},
|
||||||
|
"certificate_id": {
|
||||||
|
"$ref": "../common.json#/properties/certificate_id"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"$ref": "./user-object.json"
|
||||||
|
},
|
||||||
|
"certificate": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "./certificate-object.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"description": "Expansions",
|
"description": "Expansions",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["access_list", "owner", "certificate"]
|
"enum": ["owner", "certificate"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -40,7 +40,8 @@
|
|||||||
"nginx_online": true,
|
"nginx_online": true,
|
||||||
"nginx_err": null
|
"nginx_err": null
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"certificate_id": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
"udp_forwarding": {
|
"udp_forwarding": {
|
||||||
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
|
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
|
||||||
},
|
},
|
||||||
|
"certificate_id": {
|
||||||
|
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "../../../components/stream-object.json#/properties/meta"
|
"$ref": "../../../components/stream-object.json#/properties/meta"
|
||||||
}
|
}
|
||||||
@ -73,7 +76,8 @@
|
|||||||
"nickname": "Admin",
|
"nickname": "Admin",
|
||||||
"avatar": "",
|
"avatar": "",
|
||||||
"roles": ["admin"]
|
"roles": ["admin"]
|
||||||
}
|
},
|
||||||
|
"certificate_id": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
"nginx_online": true,
|
"nginx_online": true,
|
||||||
"nginx_err": null
|
"nginx_err": null
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"certificate_id": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -29,56 +29,26 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
"properties": {
|
"properties": {
|
||||||
"domain_names": {
|
"incoming_port": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
|
"$ref": "../../../../components/stream-object.json#/properties/incoming_port"
|
||||||
},
|
},
|
||||||
"forward_scheme": {
|
"forwarding_host": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
|
"$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
|
||||||
},
|
},
|
||||||
"forward_host": {
|
"forwarding_port": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
|
"$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
|
||||||
},
|
},
|
||||||
"forward_port": {
|
"tcp_forwarding": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
|
"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
|
||||||
|
},
|
||||||
|
"udp_forwarding": {
|
||||||
|
"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
|
||||||
},
|
},
|
||||||
"certificate_id": {
|
"certificate_id": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id"
|
"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
|
||||||
},
|
|
||||||
"ssl_forced": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced"
|
|
||||||
},
|
|
||||||
"hsts_enabled": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled"
|
|
||||||
},
|
|
||||||
"hsts_subdomains": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
|
|
||||||
},
|
|
||||||
"http2_support": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
|
|
||||||
},
|
|
||||||
"block_exploits": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits"
|
|
||||||
},
|
|
||||||
"caching_enabled": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
|
|
||||||
},
|
|
||||||
"allow_websocket_upgrade": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
|
|
||||||
},
|
|
||||||
"access_list_id": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
|
|
||||||
},
|
|
||||||
"advanced_config": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config"
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/enabled"
|
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/meta"
|
"$ref": "../../../../components/stream-object.json#/properties/meta"
|
||||||
},
|
|
||||||
"locations": {
|
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,42 +64,32 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"value": {
|
"value": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created_on": "2024-10-08T23:23:03.000Z",
|
"created_on": "2024-10-09T02:33:45.000Z",
|
||||||
"modified_on": "2024-10-08T23:26:37.000Z",
|
"modified_on": "2024-10-09T02:33:45.000Z",
|
||||||
"owner_user_id": 1,
|
"owner_user_id": 1,
|
||||||
"domain_names": ["test.example.com"],
|
"incoming_port": 9090,
|
||||||
"forward_host": "192.168.0.10",
|
"forwarding_host": "router.internal",
|
||||||
"forward_port": 8989,
|
"forwarding_port": 80,
|
||||||
"access_list_id": 0,
|
"tcp_forwarding": true,
|
||||||
"certificate_id": 0,
|
"udp_forwarding": false,
|
||||||
"ssl_forced": false,
|
|
||||||
"caching_enabled": false,
|
|
||||||
"block_exploits": false,
|
|
||||||
"advanced_config": "",
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"nginx_online": true,
|
"nginx_online": true,
|
||||||
"nginx_err": null
|
"nginx_err": null
|
||||||
},
|
},
|
||||||
"allow_websocket_upgrade": false,
|
|
||||||
"http2_support": false,
|
|
||||||
"forward_scheme": "http",
|
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"hsts_enabled": false,
|
|
||||||
"hsts_subdomains": false,
|
|
||||||
"owner": {
|
"owner": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created_on": "2024-10-07T22:43:55.000Z",
|
"created_on": "2024-10-09T02:33:16.000Z",
|
||||||
"modified_on": "2024-10-08T12:52:54.000Z",
|
"modified_on": "2024-10-09T02:33:16.000Z",
|
||||||
"is_deleted": false,
|
"is_deleted": false,
|
||||||
"is_disabled": false,
|
"is_disabled": false,
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"nickname": "some guy",
|
"nickname": "Admin",
|
||||||
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
|
"avatar": "",
|
||||||
"roles": ["admin"]
|
"roles": ["admin"]
|
||||||
},
|
},
|
||||||
"certificate": null,
|
"certificate_id": 0
|
||||||
"access_list": null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% if certificate.provider == "letsencrypt" %}
|
{% if certificate.provider == "letsencrypt" %}
|
||||||
# Let's Encrypt SSL
|
# Let's Encrypt SSL
|
||||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||||
|
include conf.d/include/ssl-cache.conf;
|
||||||
include conf.d/include/ssl-ciphers.conf;
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
|
||||||
|
13
backend/templates/_certificates_stream.conf
Normal file
13
backend/templates/_certificates_stream.conf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% if certificate and certificate_id > 0 %}
|
||||||
|
{% if certificate.provider == "letsencrypt" %}
|
||||||
|
# Let's Encrypt SSL
|
||||||
|
include conf.d/include/ssl-cache-stream.conf;
|
||||||
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
|
||||||
|
{%- else %}
|
||||||
|
# Custom SSL
|
||||||
|
ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
|
||||||
|
ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
@ -5,12 +5,10 @@
|
|||||||
{% if enabled %}
|
{% if enabled %}
|
||||||
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
|
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
|
||||||
server {
|
server {
|
||||||
listen {{ incoming_port }};
|
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
|
||||||
{% if ipv6 -%}
|
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
|
||||||
listen [::]:{{ incoming_port }};
|
|
||||||
{% else -%}
|
{%- include "_certificates_stream.conf" %}
|
||||||
#listen [::]:{{ incoming_port }};
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
||||||
|
|
||||||
@ -19,14 +17,12 @@ server {
|
|||||||
include /data/nginx/custom/server_stream_tcp[.]conf;
|
include /data/nginx/custom/server_stream_tcp[.]conf;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if udp_forwarding == 1 or udp_forwarding == true %}
|
|
||||||
|
{% if udp_forwarding == 1 or udp_forwarding == true -%}
|
||||||
server {
|
server {
|
||||||
listen {{ incoming_port }} udp;
|
listen {{ incoming_port }} udp;
|
||||||
{% if ipv6 -%}
|
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
|
||||||
listen [::]:{{ incoming_port }} udp;
|
|
||||||
{% else -%}
|
|
||||||
#listen [::]:{{ incoming_port }} udp;
|
|
||||||
{% endif %}
|
|
||||||
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
|
@ -22,6 +22,10 @@ services:
|
|||||||
test: ["CMD", "/usr/bin/check-health"]
|
test: ["CMD", "/usr/bin/check-health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
|
expose:
|
||||||
|
- '80-81/tcp'
|
||||||
|
- '443/tcp'
|
||||||
|
- '1500-1503/tcp'
|
||||||
networks:
|
networks:
|
||||||
fulltest:
|
fulltest:
|
||||||
aliases:
|
aliases:
|
||||||
@ -97,7 +101,7 @@ services:
|
|||||||
HTTP_PROXY: 'squid:3128'
|
HTTP_PROXY: 'squid:3128'
|
||||||
HTTPS_PROXY: 'squid:3128'
|
HTTPS_PROXY: 'squid:3128'
|
||||||
volumes:
|
volumes:
|
||||||
- 'cypress_logs:/results'
|
- 'cypress_logs:/test/results'
|
||||||
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- '/etc/localtime:/etc/localtime:ro'
|
||||||
command: cypress run --browser chrome --config-file=cypress/config/ci.js
|
command: cypress run --browser chrome --config-file=cypress/config/ci.js
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
ssl_session_timeout 5m;
|
||||||
|
ssl_session_cache shared:SSL_stream:50m;
|
2
docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
Normal file
2
docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ssl_session_timeout 5m;
|
||||||
|
ssl_session_cache shared:SSL:50m;
|
@ -1,6 +1,3 @@
|
|||||||
ssl_session_timeout 5m;
|
|
||||||
ssl_session_cache shared:SSL:50m;
|
|
||||||
|
|
||||||
# intermediate configuration. tweak to your needs.
|
# intermediate configuration. tweak to your needs.
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||||
|
@ -8,7 +8,7 @@ BLUE='\E[1;34m'
|
|||||||
GREEN='\E[1;32m'
|
GREEN='\E[1;32m'
|
||||||
RESET='\E[0m'
|
RESET='\E[0m'
|
||||||
|
|
||||||
S6_OVERLAY_VERSION=3.1.5.0
|
S6_OVERLAY_VERSION=3.2.0.2
|
||||||
TARGETPLATFORM=${1:-linux/amd64}
|
TARGETPLATFORM=${1:-linux/amd64}
|
||||||
|
|
||||||
# Determine the correct binary file for the architecture given
|
# Determine the correct binary file for the architecture given
|
||||||
|
@ -3,8 +3,16 @@
|
|||||||
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
|
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
|
||||||
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button>
|
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body has-tabs">
|
||||||
|
<div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
|
||||||
<form>
|
<form>
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
|
||||||
|
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Details -->
|
||||||
|
<div role="tabpanel" class="tab-pane active" id="details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-12">
|
<div class="col-sm-12 col-md-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -46,6 +54,137 @@
|
|||||||
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
|
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SSL -->
|
||||||
|
<div role="tabpanel" class="tab-pane" id="ssl-options">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label>
|
||||||
|
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
|
||||||
|
<option selected value="0" data-data="{"id":0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
|
||||||
|
<option selected value="new" data-data="{"id":"new"}"><%- i18n('all-hosts', 'new-cert') %></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS challenge -->
|
||||||
|
<div class="col-sm-12 col-md-12 letsencrypt">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label>
|
||||||
|
<input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="custom-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="custom-switch-input"
|
||||||
|
name="meta[dns_challenge]"
|
||||||
|
value="1"
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<span class="custom-switch-indicator"></span>
|
||||||
|
<span class="custom-switch-description"><%= i18n('ssl', 'dns-challenge') %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-12 letsencrypt">
|
||||||
|
<fieldset class="form-fieldset dns-challenge">
|
||||||
|
<div class="text-red mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'certbot-warning') %></div>
|
||||||
|
|
||||||
|
<!-- Certbot DNS plugin selection -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label>
|
||||||
|
<select
|
||||||
|
name="meta[dns_provider]"
|
||||||
|
id="dns_provider"
|
||||||
|
class="form-control custom-select"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
hidden
|
||||||
|
<%- getDnsProvider() === null ? 'selected' : '' %>
|
||||||
|
>Please Choose...</option>
|
||||||
|
<% _.each(dns_plugins, function(plugin_info, plugin_name){ %>
|
||||||
|
<option
|
||||||
|
value="<%- plugin_name %>"
|
||||||
|
<%- getDnsProvider() === plugin_name ? 'selected' : '' %>
|
||||||
|
><%- plugin_info.name %></option>
|
||||||
|
<% }); %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Certbot credentials file content -->
|
||||||
|
<div class="row credentials-file-content">
|
||||||
|
<div class="col-sm-12 col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label>
|
||||||
|
<textarea
|
||||||
|
name="meta[dns_provider_credentials]"
|
||||||
|
class="form-control text-monospace"
|
||||||
|
id="dns_provider_credentials"
|
||||||
|
><%- getDnsProviderCredentials() %></textarea>
|
||||||
|
<div class="text-secondary small">
|
||||||
|
<i class="fe fe-info"></i>
|
||||||
|
<%= i18n('ssl', 'credentials-file-content-info') %>
|
||||||
|
</div>
|
||||||
|
<div class="text-red small">
|
||||||
|
<i class="fe fe-alert-triangle"></i>
|
||||||
|
<%= i18n('ssl', 'stored-as-plaintext-info') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS propagation delay -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-12">
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
name="meta[propagation_seconds]"
|
||||||
|
class="form-control"
|
||||||
|
id="propagation_seconds"
|
||||||
|
value="<%- getPropagationSeconds() %>"
|
||||||
|
>
|
||||||
|
<div class="text-secondary small">
|
||||||
|
<i class="fe fe-info"></i>
|
||||||
|
<%= i18n('ssl', 'propagation-seconds-info') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lets encrypt -->
|
||||||
|
<div class="col-sm-12 col-md-12 letsencrypt">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
|
||||||
|
<input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-12 letsencrypt">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="custom-switch">
|
||||||
|
<input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
|
||||||
|
<span class="custom-switch-indicator"></span>
|
||||||
|
<span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -2,10 +2,14 @@ const Mn = require('backbone.marionette');
|
|||||||
const App = require('../../main');
|
const App = require('../../main');
|
||||||
const StreamModel = require('../../../models/stream');
|
const StreamModel = require('../../../models/stream');
|
||||||
const template = require('./form.ejs');
|
const template = require('./form.ejs');
|
||||||
|
const dns_providers = require('../../../../../global/certbot-dns-plugins');
|
||||||
|
|
||||||
require('jquery-serializejson');
|
require('jquery-serializejson');
|
||||||
require('jquery-mask-plugin');
|
require('jquery-mask-plugin');
|
||||||
require('selectize');
|
require('selectize');
|
||||||
|
const Helpers = require("../../../lib/helpers");
|
||||||
|
const certListItemTemplate = require("../certificates-list-item.ejs");
|
||||||
|
const i18n = require("../../i18n");
|
||||||
|
|
||||||
module.exports = Mn.View.extend({
|
module.exports = Mn.View.extend({
|
||||||
template: template,
|
template: template,
|
||||||
@ -18,7 +22,17 @@ module.exports = Mn.View.extend({
|
|||||||
buttons: '.modal-footer button',
|
buttons: '.modal-footer button',
|
||||||
switches: '.custom-switch-input',
|
switches: '.custom-switch-input',
|
||||||
cancel: 'button.cancel',
|
cancel: 'button.cancel',
|
||||||
save: 'button.save'
|
save: 'button.save',
|
||||||
|
le_error_info: '#le-error-info',
|
||||||
|
certificate_select: 'select[name="certificate_id"]',
|
||||||
|
domain_names: 'input[name="domain_names"]',
|
||||||
|
dns_challenge_switch: 'input[name="meta[dns_challenge]"]',
|
||||||
|
dns_challenge_content: '.dns-challenge',
|
||||||
|
dns_provider: 'select[name="meta[dns_provider]"]',
|
||||||
|
credentials_file_content: '.credentials-file-content',
|
||||||
|
dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
|
||||||
|
propagation_seconds: 'input[name="meta[propagation_seconds]"]',
|
||||||
|
letsencrypt: '.letsencrypt'
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@ -48,6 +62,35 @@ module.exports = Mn.View.extend({
|
|||||||
data.tcp_forwarding = !!data.tcp_forwarding;
|
data.tcp_forwarding = !!data.tcp_forwarding;
|
||||||
data.udp_forwarding = !!data.udp_forwarding;
|
data.udp_forwarding = !!data.udp_forwarding;
|
||||||
|
|
||||||
|
if (typeof data.meta === 'undefined') data.meta = {};
|
||||||
|
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
|
||||||
|
data.meta.dns_challenge = true;
|
||||||
|
|
||||||
|
if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
|
||||||
|
|
||||||
|
if (typeof data.domain_names === 'string' && data.domain_names) {
|
||||||
|
data.domain_names = data.domain_names.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any domain names containing wildcards, which are not allowed with letsencrypt
|
||||||
|
if (data.certificate_id === 'new') {
|
||||||
|
let domain_err = false;
|
||||||
|
if (!data.meta.dns_challenge) {
|
||||||
|
data.domain_names.map(function (name) {
|
||||||
|
if (name.match(/\*/im)) {
|
||||||
|
domain_err = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain_err) {
|
||||||
|
alert(i18n('ssl', 'no-wildcard-without-dns'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.certificate_id = parseInt(data.certificate_id, 10);
|
||||||
|
}
|
||||||
|
|
||||||
let method = App.Api.Nginx.Streams.create;
|
let method = App.Api.Nginx.Streams.create;
|
||||||
let is_new = true;
|
let is_new = true;
|
||||||
|
|
||||||
@ -70,10 +113,108 @@ module.exports = Mn.View.extend({
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert(err.message);
|
let more_info = '';
|
||||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
if (err.code === 500 && err.debug) {
|
||||||
});
|
try {
|
||||||
|
more_info = JSON.parse(err.debug).debug.stack.join("\n");
|
||||||
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`;
|
||||||
|
this.ui.le_error_info.show();
|
||||||
|
this.ui.le_error_info[0].scrollIntoView();
|
||||||
|
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||||
|
this.ui.save.removeClass('btn-loading');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'change @ui.certificate_select': function () {
|
||||||
|
let id = this.ui.certificate_select.val();
|
||||||
|
if (id === 'new') {
|
||||||
|
this.ui.letsencrypt.show().find('input').prop('disabled', false);
|
||||||
|
this.ui.domain_names.prop('required', 'required');
|
||||||
|
|
||||||
|
this.ui.dns_challenge_switch
|
||||||
|
.prop('disabled', true)
|
||||||
|
.parents('.form-group')
|
||||||
|
.css('opacity', 0.5);
|
||||||
|
|
||||||
|
this.ui.dns_provider.prop('required', 'required');
|
||||||
|
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
|
||||||
|
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
|
||||||
|
this.ui.dns_provider_credentials.prop('required', 'required');
|
||||||
|
}
|
||||||
|
this.ui.dns_challenge_content.show();
|
||||||
|
} else {
|
||||||
|
this.ui.letsencrypt.hide().find('input').prop('disabled', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'change @ui.dns_provider': function () {
|
||||||
|
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
|
||||||
|
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
|
||||||
|
this.ui.dns_provider_credentials.prop('required', 'required');
|
||||||
|
this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
|
||||||
|
this.ui.credentials_file_content.show();
|
||||||
|
} else {
|
||||||
|
this.ui.dns_provider_credentials.prop('required', false);
|
||||||
|
this.ui.credentials_file_content.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
templateContext: {
|
||||||
|
getLetsencryptEmail: function () {
|
||||||
|
return App.Cache.User.get('email');
|
||||||
|
},
|
||||||
|
getDnsProvider: function () {
|
||||||
|
return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
|
||||||
|
},
|
||||||
|
getDnsProviderCredentials: function () {
|
||||||
|
return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
|
||||||
|
},
|
||||||
|
getPropagationSeconds: function () {
|
||||||
|
return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
|
||||||
|
},
|
||||||
|
dns_plugins: dns_providers,
|
||||||
|
},
|
||||||
|
|
||||||
|
onRender: function () {
|
||||||
|
let view = this;
|
||||||
|
|
||||||
|
// Certificates
|
||||||
|
this.ui.le_error_info.hide();
|
||||||
|
this.ui.dns_challenge_content.hide();
|
||||||
|
this.ui.credentials_file_content.hide();
|
||||||
|
this.ui.letsencrypt.hide();
|
||||||
|
this.ui.certificate_select.selectize({
|
||||||
|
valueField: 'id',
|
||||||
|
labelField: 'nice_name',
|
||||||
|
searchField: ['nice_name', 'domain_names'],
|
||||||
|
create: false,
|
||||||
|
preload: true,
|
||||||
|
allowEmptyOption: true,
|
||||||
|
render: {
|
||||||
|
option: function (item) {
|
||||||
|
item.i18n = App.i18n;
|
||||||
|
item.formatDbDate = Helpers.formatDbDate;
|
||||||
|
return certListItemTemplate(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
load: function (query, callback) {
|
||||||
|
App.Api.Nginx.Certificates.getAll()
|
||||||
|
.then(rows => {
|
||||||
|
callback(rows);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLoad: function () {
|
||||||
|
view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (options) {
|
initialize: function (options) {
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<% if (tcp_forwarding) { %>
|
<% if (certificate) { %>
|
||||||
|
<span class="tag"><%- i18n('streams', 'tcp+ssl') %></span>
|
||||||
|
<% }
|
||||||
|
else if (tcp_forwarding) { %>
|
||||||
<span class="tag"><%- i18n('streams', 'tcp') %></span>
|
<span class="tag"><%- i18n('streams', 'tcp') %></span>
|
||||||
<% }
|
<% }
|
||||||
if (udp_forwarding) { %>
|
if (udp_forwarding) { %>
|
||||||
@ -24,6 +27,9 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<%
|
<%
|
||||||
var o = isOnline();
|
var o = isOnline();
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<th><%- i18n('streams', 'incoming-port') %></th>
|
<th><%- i18n('streams', 'incoming-port') %></th>
|
||||||
<th><%- i18n('str', 'destination') %></th>
|
<th><%- i18n('str', 'destination') %></th>
|
||||||
<th><%- i18n('streams', 'protocol') %></th>
|
<th><%- i18n('streams', 'protocol') %></th>
|
||||||
|
<th><%- i18n('str', 'ssl') %></th>
|
||||||
<th><%- i18n('str', 'status') %></th>
|
<th><%- i18n('str', 'status') %></th>
|
||||||
<% if (canManage) { %>
|
<% if (canManage) { %>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
|
@ -88,7 +88,7 @@ module.exports = Mn.View.extend({
|
|||||||
onRender: function () {
|
onRender: function () {
|
||||||
let view = this;
|
let view = this;
|
||||||
|
|
||||||
view.fetch(['owner'])
|
view.fetch(['owner', 'certificate'])
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!view.isDestroyed()) {
|
if (!view.isDestroyed()) {
|
||||||
if (response && response.length) {
|
if (response && response.length) {
|
||||||
|
@ -179,7 +179,9 @@
|
|||||||
"delete-confirm": "Are you sure you want to delete this Stream?",
|
"delete-confirm": "Are you sure you want to delete this Stream?",
|
||||||
"help-title": "What is a Stream?",
|
"help-title": "What is a Stream?",
|
||||||
"help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
|
"help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
|
||||||
"search": "Search Incoming Port…"
|
"search": "Search Incoming Port…",
|
||||||
|
"ssl-certificate": "SSL Certificate for TCP Forwarding",
|
||||||
|
"tcp+ssl": "TCP+SSL"
|
||||||
},
|
},
|
||||||
"certificates": {
|
"certificates": {
|
||||||
"title": "SSL Certificates",
|
"title": "SSL Certificates",
|
||||||
|
@ -15,8 +15,11 @@ const model = Backbone.Model.extend({
|
|||||||
udp_forwarding: false,
|
udp_forwarding: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
meta: {},
|
meta: {},
|
||||||
|
certificate_id: 0,
|
||||||
|
domain_names: [],
|
||||||
// The following are expansions:
|
// The following are expansions:
|
||||||
owner: null
|
owner: null,
|
||||||
|
certificate: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
FROM cypress/included:13.9.0
|
FROM cypress/included:14.0.1
|
||||||
|
|
||||||
COPY --chown=1000 ./test /test
|
|
||||||
|
|
||||||
# Disable Cypress CLI colors
|
# Disable Cypress CLI colors
|
||||||
ENV FORCE_COLOR=0
|
ENV FORCE_COLOR=0
|
||||||
ENV NO_COLOR=1
|
ENV NO_COLOR=1
|
||||||
|
|
||||||
|
# testssl.sh and mkcert
|
||||||
|
RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \
|
||||||
|
&& tar -xzf /tmp/testssl.tgz -C /tmp \
|
||||||
|
&& mv /tmp/testssl.sh-3.2rc4 /testssl \
|
||||||
|
&& rm /tmp/testssl.tgz \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y bsdmainutils curl dnsutils \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \
|
||||||
|
&& chmod +x /bin/mkcert
|
||||||
|
|
||||||
|
COPY --chown=1000 ./test /test
|
||||||
WORKDIR /test
|
WORKDIR /test
|
||||||
RUN yarn install && yarn cache clean
|
RUN yarn install && yarn cache clean
|
||||||
ENTRYPOINT []
|
ENTRYPOINT []
|
||||||
|
213
test/cypress/e2e/api/Streams.cy.js
Normal file
213
test/cypress/e2e/api/Streams.cy.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
describe('Streams', () => {
|
||||||
|
let token;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.getToken().then((tok) => {
|
||||||
|
token = tok;
|
||||||
|
// Set default site content
|
||||||
|
cy.task('backendApiPut', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/settings/default-site',
|
||||||
|
data: {
|
||||||
|
value: 'html',
|
||||||
|
meta: {
|
||||||
|
html: '<p>yay it works</p>'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a custom cert pair
|
||||||
|
cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => {
|
||||||
|
expect(result.code).to.eq(0);
|
||||||
|
// Install CA
|
||||||
|
cy.exec('mkcert -install').then((result) => {
|
||||||
|
expect(result.code).to.eq(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.exec('rm -f /test/results/testssl.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be able to create TCP Stream', function() {
|
||||||
|
cy.task('backendApiPost', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/nginx/streams',
|
||||||
|
data: {
|
||||||
|
incoming_port: 1500,
|
||||||
|
forwarding_host: '127.0.0.1',
|
||||||
|
forwarding_port: 80,
|
||||||
|
certificate_id: 0,
|
||||||
|
meta: {
|
||||||
|
dns_provider_credentials: "",
|
||||||
|
letsencrypt_agree: false,
|
||||||
|
dns_challenge: true
|
||||||
|
},
|
||||||
|
tcp_forwarding: true,
|
||||||
|
udp_forwarding: false
|
||||||
|
}
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
|
||||||
|
expect(data).to.have.property('id');
|
||||||
|
expect(data.id).to.be.greaterThan(0);
|
||||||
|
expect(data).to.have.property('enabled', true);
|
||||||
|
expect(data).to.have.property('tcp_forwarding', true);
|
||||||
|
expect(data).to.have.property('udp_forwarding', false);
|
||||||
|
|
||||||
|
cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => {
|
||||||
|
expect(result.code).to.eq(0);
|
||||||
|
expect(result.stdout).to.contain('yay it works');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be able to create UDP Stream', function() {
|
||||||
|
cy.task('backendApiPost', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/nginx/streams',
|
||||||
|
data: {
|
||||||
|
incoming_port: 1501,
|
||||||
|
forwarding_host: '127.0.0.1',
|
||||||
|
forwarding_port: 80,
|
||||||
|
certificate_id: 0,
|
||||||
|
meta: {
|
||||||
|
dns_provider_credentials: "",
|
||||||
|
letsencrypt_agree: false,
|
||||||
|
dns_challenge: true
|
||||||
|
},
|
||||||
|
tcp_forwarding: false,
|
||||||
|
udp_forwarding: true
|
||||||
|
}
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
|
||||||
|
expect(data).to.have.property('id');
|
||||||
|
expect(data.id).to.be.greaterThan(0);
|
||||||
|
expect(data).to.have.property('enabled', true);
|
||||||
|
expect(data).to.have.property('tcp_forwarding', false);
|
||||||
|
expect(data).to.have.property('udp_forwarding', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be able to create TCP/UDP Stream', function() {
|
||||||
|
cy.task('backendApiPost', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/nginx/streams',
|
||||||
|
data: {
|
||||||
|
incoming_port: 1502,
|
||||||
|
forwarding_host: '127.0.0.1',
|
||||||
|
forwarding_port: 80,
|
||||||
|
certificate_id: 0,
|
||||||
|
meta: {
|
||||||
|
dns_provider_credentials: "",
|
||||||
|
letsencrypt_agree: false,
|
||||||
|
dns_challenge: true
|
||||||
|
},
|
||||||
|
tcp_forwarding: true,
|
||||||
|
udp_forwarding: true
|
||||||
|
}
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
|
||||||
|
expect(data).to.have.property('id');
|
||||||
|
expect(data.id).to.be.greaterThan(0);
|
||||||
|
expect(data).to.have.property('enabled', true);
|
||||||
|
expect(data).to.have.property('tcp_forwarding', true);
|
||||||
|
expect(data).to.have.property('udp_forwarding', true);
|
||||||
|
|
||||||
|
cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => {
|
||||||
|
expect(result.code).to.eq(0);
|
||||||
|
expect(result.stdout).to.contain('yay it works');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be able to create SSL TCP Stream', function() {
|
||||||
|
let certID = 0;
|
||||||
|
|
||||||
|
// Create custom cert
|
||||||
|
cy.task('backendApiPost', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/nginx/certificates',
|
||||||
|
data: {
|
||||||
|
provider: "other",
|
||||||
|
nice_name: "Custom Certificate for SSL Stream",
|
||||||
|
},
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
|
||||||
|
expect(data).to.have.property('id');
|
||||||
|
certID = data.id;
|
||||||
|
|
||||||
|
// Upload files
|
||||||
|
cy.task('backendApiPostFiles', {
|
||||||
|
token: token,
|
||||||
|
path: `/api/nginx/certificates/${certID}/upload`,
|
||||||
|
files: {
|
||||||
|
certificate: 'website1.pem',
|
||||||
|
certificate_key: 'website1.key.pem',
|
||||||
|
},
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
|
||||||
|
expect(data).to.have.property('certificate');
|
||||||
|
expect(data).to.have.property('certificate_key');
|
||||||
|
|
||||||
|
// Create the stream
|
||||||
|
cy.task('backendApiPost', {
|
||||||
|
token: token,
|
||||||
|
path: '/api/nginx/streams',
|
||||||
|
data: {
|
||||||
|
incoming_port: 1503,
|
||||||
|
forwarding_host: '127.0.0.1',
|
||||||
|
forwarding_port: 80,
|
||||||
|
certificate_id: certID,
|
||||||
|
meta: {
|
||||||
|
dns_provider_credentials: "",
|
||||||
|
letsencrypt_agree: false,
|
||||||
|
dns_challenge: true
|
||||||
|
},
|
||||||
|
tcp_forwarding: true,
|
||||||
|
udp_forwarding: false
|
||||||
|
}
|
||||||
|
}).then((data) => {
|
||||||
|
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
|
||||||
|
expect(data).to.have.property('id');
|
||||||
|
expect(data.id).to.be.greaterThan(0);
|
||||||
|
expect(data).to.have.property("enabled", true);
|
||||||
|
expect(data).to.have.property('tcp_forwarding', true);
|
||||||
|
expect(data).to.have.property('udp_forwarding', false);
|
||||||
|
expect(data).to.have.property('certificate_id', certID);
|
||||||
|
|
||||||
|
// Check the ssl termination
|
||||||
|
cy.task('log', '[testssl.sh] Running ...');
|
||||||
|
cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', {
|
||||||
|
timeout: 120000, // 2 minutes
|
||||||
|
}).then((result) => {
|
||||||
|
cy.task('log', '[testssl.sh] ' + result.stdout);
|
||||||
|
|
||||||
|
const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"];
|
||||||
|
const ignoredIDs = [
|
||||||
|
'cert_chain_of_trust',
|
||||||
|
'cert_extlifeSpan',
|
||||||
|
'cert_revocation',
|
||||||
|
'overall_grade',
|
||||||
|
];
|
||||||
|
|
||||||
|
cy.readFile('/test/results/testssl.json').then((data) => {
|
||||||
|
// Parse each array item
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const item = data[i];
|
||||||
|
if (ignoredIDs.includes(item.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(item.severity).to.be.oneOf(allowedSeverities);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -4,18 +4,18 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jc21/cypress-swagger-validation": "^0.3.1",
|
"@jc21/cypress-swagger-validation": "^0.3.2",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"cypress": "^13.15.0",
|
"cypress": "^14.0.1",
|
||||||
"cypress-multi-reporters": "^1.6.4",
|
"cypress-multi-reporters": "^2.0.5",
|
||||||
"cypress-wait-until": "^3.0.2",
|
"cypress-wait-until": "^3.0.2",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-align-assignments": "^1.1.2",
|
"eslint-plugin-align-assignments": "^1.1.2",
|
||||||
"eslint-plugin-chai-friendly": "^1.0.1",
|
"eslint-plugin-chai-friendly": "^1.0.1",
|
||||||
"eslint-plugin-cypress": "^3.5.0",
|
"eslint-plugin-cypress": "^4.1.0",
|
||||||
"form-data": "^4.0.1",
|
"form-data": "^4.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mocha": "^10.7.3",
|
"mocha": "^11.1.0",
|
||||||
"mocha-junit-reporter": "^2.2.1"
|
"mocha-junit-reporter": "^2.2.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Loading…
Reference in New Issue
Block a user