Compare commits

..

2 Commits

Author SHA1 Message Date
Semjon Nordmann
0ae0c47d46 Merge 480c772b98 into 9687e9e450 2025-01-08 16:34:50 +01:00
Semjon Nordmann
480c772b98 Add support for proxy_protocol in proxy_hosts and streams
Closes #1114
Related To #1882
Related To #3537
Related To #3618

Co-authored-by: jwklijnsma <janwiebe@janwiebe.eu>
Co-authored-by: SBado <16034687+SBado@users.noreply.github.com>
2024-10-26 21:50:37 +02:00
63 changed files with 954 additions and 1413 deletions

View File

@@ -1 +1 @@
2.12.3
2.12.2

View File

@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.12.3-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.12.2-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>

View File

@@ -258,7 +258,6 @@ const internalAccessList = {
})
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.groupBy('access_list.id')
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();
@@ -508,13 +507,8 @@ const internalAccessList = {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
utils.execFile('openssl', ['passwd', '-apr1', item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'});
} catch (err) {
reject(err);
}
utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
.then((/*result*/) => {
next();
})
.catch((err) => {

View File

@@ -313,9 +313,6 @@ const internalCertificate = {
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -467,9 +464,6 @@ const internalCertificate = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') {

View File

@@ -156,7 +156,8 @@ const internalNginx = {
{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},
{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
{certificate: host.certificate}, host.locations[i]);
{certificate: host.certificate}, {proxy_protocol_enabled: host.proxy_protocol_enabled},
{loadbalancer_address: host.loadbalancer_address}, host.locations[i]);
if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split('/');

View File

@@ -4,12 +4,10 @@ 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', 'owner.is_deleted', 'certificate.is_deleted'];
return ['is_deleted'];
}
const internalStream = {
@@ -20,12 +18,6 @@ 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
@@ -35,44 +27,16 @@ 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_no_domains)
.insertAndFetch(data)
.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 row;
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((row) => {
@@ -96,12 +60,6 @@ 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
@@ -113,32 +71,16 @@ 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, {
@@ -151,17 +93,6 @@ 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());
});
});
});
},
@@ -184,7 +115,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner,certificate]')
.allowGraph('[owner]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -201,7 +132,6 @@ 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);
@@ -267,14 +197,14 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ['certificate', 'owner']
expand: ['owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Stream is already enabled');
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
@@ -320,7 +250,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Stream is already disabled');
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
@@ -368,7 +298,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
.allowGraph('[owner]')
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
if (access_data.permission_visibility !== 'all') {
@@ -387,13 +317,6 @@ 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;
});
},

View File

@@ -1,38 +0,0 @@
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');
});
};

View File

@@ -0,0 +1,56 @@
const migrate_name = 'proxy_protocol';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('proxy_protocol_enabled').notNull().defaultTo(0);
proxy_host.string('loadbalancer_address').notNull().defaultTo('');
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table('stream', function (stream) {
stream.integer('proxy_protocol_enabled').notNull().defaultTo(0);
stream.string('loadbalancer_address').notNull().defaultTo('');
})
.then(() => {
logger.info('[' + migrate_name + '] stream Table altered');
});
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.dropColumn('proxy_protocol_enabled');
proxy_host.dropColumn('loadbalancer_address');
})
.then(function () {
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table('stream', function (stream) {
stream.dropColumn('proxy_protocol_enabled');
stream.dropColumn('loadbalancer_address');
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
});
});
};

View File

@@ -4,6 +4,7 @@
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
@@ -67,11 +68,6 @@ class Certificate extends Model {
}
static get relationMappings () {
const ProxyHost = require('./proxy_host');
const DeadHost = require('./dead_host');
const User = require('./user');
const RedirectionHost = require('./redirection_host');
return {
owner: {
relation: Model.HasOneRelation,
@@ -83,39 +79,6 @@ class Certificate extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: ProxyHost,
join: {
from: 'certificate.id',
to: 'proxy_host.certificate_id'
},
modify: function (qb) {
qb.where('proxy_host.is_deleted', 0);
}
},
dead_hosts: {
relation: Model.HasManyRelation,
modelClass: DeadHost,
join: {
from: 'certificate.id',
to: 'dead_host.certificate_id'
},
modify: function (qb) {
qb.where('dead_host.is_deleted', 0);
}
},
redirection_hosts: {
relation: Model.HasManyRelation,
modelClass: RedirectionHost,
join: {
from: 'certificate.id',
to: 'redirection_host.certificate_id'
},
modify: function (qb) {
qb.where('redirection_host.is_deleted', 0);
}
}
};
}

View File

@@ -21,6 +21,7 @@ const boolFields = [
'enabled',
'hsts_enabled',
'hsts_subdomains',
'proxy_protocol_enabled',
];
class ProxyHost extends Model {

View File

@@ -1,17 +1,19 @@
const Model = require('objection').Model;
// 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 Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
const boolFields = [
'enabled',
'is_deleted',
'tcp_forwarding',
'udp_forwarding',
'proxy_protocol_enabled',
];
class Stream extends Model {
@@ -63,17 +65,6 @@ 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);
}
}
};
}

View File

@@ -110,6 +110,16 @@
"caching_enabled": {
"description": "Should we cache assets",
"type": "boolean"
},
"proxy_protocol_enabled": {
"description": "Should the proxy_procotol be enabled",
"type": "boolean"
},
"loadbalancer_address": {
"description": "Hostname, IP or CIDR range of the load balancer",
"type": "string",
"minLength": 0,
"maxLength": 255
}
}
}

View File

@@ -23,7 +23,9 @@
"locations",
"hsts_enabled",
"hsts_subdomains",
"certificate"
"certificate",
"proxy_protocol_enabled",
"loadbalancer_address"
],
"additionalProperties": false,
"properties": {
@@ -137,6 +139,12 @@
}
]
},
"proxy_protocol_enabled": {
"$ref": "../common.json#/properties/proxy_protocol_enabled"
},
"loadbalancer_address": {
"$ref": "../common.json#/properties/loadbalancer_address"
},
"owner": {
"$ref": "./user-object.json"
},

View File

@@ -1,7 +1,7 @@
{
"type": "object",
"description": "Stream object",
"required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta"],
"required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta", "proxy_protocol_enabled", "loadbalancer_address"],
"additionalProperties": false,
"properties": {
"id": {
@@ -19,7 +19,9 @@
"incoming_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
"maximum": 65535,
"if": {"properties": {"tcp_forwarding": {"const": true}}},
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
},
"forwarding_host": {
"anyOf": [
@@ -53,24 +55,14 @@
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"meta": {
"type": "object"
},
"owner": {
"$ref": "./user-object.json"
"proxy_protocol_enabled": {
"$ref": "../common.json#/properties/proxy_protocol_enabled"
},
"certificate": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./certificate-object.json"
}
]
"loadbalancer_address": {
"$ref": "../common.json#/properties/loadbalancer_address"
}
}
}

View File

@@ -5,9 +5,10 @@
"additionalProperties": false,
"properties": {
"expires": {
"description": "Token Expiry ISO Time String",
"example": "2025-02-04T20:40:46.340Z",
"type": "string"
"description": "Token Expiry Unix Time",
"example": 1566540249,
"minimum": 1,
"type": "number"
},
"token": {
"description": "JWT Token",

View File

@@ -50,7 +50,9 @@
"enabled": true,
"locations": null,
"hsts_enabled": false,
"hsts_subdomains": false
"hsts_subdomains": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": ""
}
]
}

View File

@@ -50,7 +50,9 @@
"enabled": true,
"locations": null,
"hsts_enabled": false,
"hsts_subdomains": false
"hsts_subdomains": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": ""
}
}
},

View File

@@ -79,6 +79,12 @@
},
"locations": {
"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
},
"proxy_protocol_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled"
},
"loadbalancer_address": {
"$ref": "../../../../components/proxy-host-object.json#/properties/loadbalancer_address"
}
}
}
@@ -116,6 +122,8 @@
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"owner": {
"id": 1,
"created_on": "2024-10-07T22:43:55.000Z",

View File

@@ -67,6 +67,12 @@
},
"locations": {
"$ref": "../../../components/proxy-host-object.json#/properties/locations"
},
"proxy_protocol_enabled": {
"$ref": "../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled"
},
"loadbalancer_address": {
"$ref": "../../../components/proxy-host-object.json#/properties/loadbalancer_address"
}
}
}
@@ -101,6 +107,8 @@
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"certificate": null,
"owner": {
"id": 1,

View File

@@ -14,7 +14,7 @@
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["owner", "certificate"]
"enum": ["access_list", "owner", "certificate"]
}
}
],
@@ -36,12 +36,13 @@
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
"enabled": true
}
]
}

View File

@@ -32,8 +32,11 @@
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
"proxy_protocol_enabled": {
"$ref": "../../../components/stream-object.json#/properties/proxy_protocol_enabled"
},
"loadbalancer_address": {
"$ref": "../../../components/stream-object.json#/properties/loadbalancer_address"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
@@ -60,6 +63,8 @@
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"meta": {
"nginx_online": true,
"nginx_err": null
@@ -76,8 +81,7 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"certificate_id": 0
}
}
}
},

View File

@@ -36,12 +36,13 @@
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
"enabled": true
}
}
},

View File

@@ -29,26 +29,62 @@
"additionalProperties": false,
"minProperties": 1,
"properties": {
"incoming_port": {
"$ref": "../../../../components/stream-object.json#/properties/incoming_port"
"domain_names": {
"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
},
"forwarding_host": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
"forward_scheme": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
},
"forwarding_port": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
"forward_host": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
},
"tcp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
},
"udp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
"forward_port": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
},
"certificate_id": {
"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
"$ref": "../../../../components/proxy-host-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": {
"$ref": "../../../../components/stream-object.json#/properties/meta"
"$ref": "../../../../components/proxy-host-object.json#/properties/meta"
},
"locations": {
"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
},
"proxy_protocol_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled"
},
"loadbalancer_address": {
"$ref": "../../../../components/proxy-host-object.json#/properties/loadbalancer_address"
}
}
}
@@ -64,32 +100,44 @@
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T02:33:45.000Z",
"modified_on": "2024-10-09T02:33:45.000Z",
"created_on": "2024-10-08T23:23:03.000Z",
"modified_on": "2024-10-08T23:26:37.000Z",
"owner_user_id": 1,
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"domain_names": ["test.example.com"],
"forward_host": "192.168.0.10",
"forward_port": 8989,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"proxy_protocol_enabled": false,
"loadbalancer_address": "",
"owner": {
"id": 1,
"created_on": "2024-10-09T02:33:16.000Z",
"modified_on": "2024-10-09T02:33:16.000Z",
"created_on": "2024-10-07T22:43:55.000Z",
"modified_on": "2024-10-08T12:52:54.000Z",
"is_deleted": false,
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"nickname": "some guy",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": ["admin"]
},
"certificate_id": 0
"certificate": null,
"access_list": null
}
}
},

View File

@@ -15,7 +15,7 @@
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}

View File

@@ -38,7 +38,7 @@
"default": {
"value": {
"result": {
"expires": "2025-02-04T20:40:46.340Z",
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}

View File

@@ -2,7 +2,6 @@
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.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;

View File

@@ -1,13 +0,0 @@
{% 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 -%}

View File

@@ -1,17 +1,31 @@
listen 80;
{% if ipv6 -%}
listen [::]:80;
{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true -%}
{% assign port_number_http = "88" -%}
{% assign port_number_https = "444" -%}
{% assign listen_extra_args = "proxy_protocol" -%}
{% else -%}
#listen [::]:80;
{% endif %}
{% assign port_number_http = "80" -%}
{% assign port_number_https = "443" -%}
{% assign listen_extra_args = "" -%}
{% endif -%}
listen {{ port_number_http }} {{ listen_extra_args }};
{% if ipv6 -%}
listen [::]:{{ port_number_http }} {{ listen_extra_args }};
{% else -%}
#listen [::]:{{ port_number_http }} {{ listen_extra_args }};
{% endif -%}
{% if certificate -%}
listen 443 ssl;
{% capture listen_extra_args_https %}ssl {{ listen_extra_args }}{% endcapture -%}
listen {{ port_number_https }} {{ listen_extra_args_https }};
{% if ipv6 -%}
listen [::]:443 ssl;
listen [::]:{{ port_number_https }} {{ listen_extra_args_https }};
{% else -%}
#listen [::]:443;
{% endif %}
{% endif %}
#listen [::]:{{ port_number_https }} {{ listen_extra_args_https }};
{% endif -%}
{% endif -%}
server_name {{ domain_names | join: " " }};
{% if http2_support == 1 or http2_support == true %}
http2 on;

View File

@@ -0,0 +1,6 @@
{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true %}
{% if loadbalancer_address != '' %}
set_real_ip_from {{ loadbalancer_address }};
real_ip_header proxy_protocol;
{% endif %}
{% endif %}

View File

@@ -15,6 +15,7 @@ server {
{% include "_exploits.conf" %}
{% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
{% include "_proxy_protocol.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;

View File

@@ -1,28 +1,39 @@
# ------------------------------------------------------------
# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }}
# ------------------------------------------------------------
{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true -%}
{% capture listen_extra_args %}proxy_protocol{% endcapture -%}
{% endif -%}
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{%- include "_certificates_stream.conf" %}
listen {{ incoming_port }} {{ listen_extra_args }};
{% if ipv6 -%}
listen [::]:{{ incoming_port }} {{ listen_extra_args }};
{% else -%}
#listen [::]:{{ incoming_port }}{{ listen_extra_args }};
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
{% include '_proxy_protocol.conf' %}
# Custom
include /data/nginx/custom/server_stream[.]conf;
include /data/nginx/custom/server_stream_tcp[.]conf;
}
{% endif %}
{% if udp_forwarding == 1 or udp_forwarding == true -%}
{% if udp_forwarding == 1 or udp_forwarding == true %}
{% # Proxy Protocol is not supported for UDP %}
{% assign listen_extra_args = "" %}
server {
listen {{ incoming_port }} udp;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
listen {{ incoming_port }} udp {{ listen_extra_args }};
{% if ipv6 -%}
listen [::]:{{ incoming_port }} udp {{ listen_extra_args }};
{% else -%}
#listen [::]:{{ incoming_port }} udp {{ listen_extra_args }};
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom

View File

@@ -35,7 +35,8 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
COPY docker/scripts/install-s6 /tmp/install-s6
RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
EXPOSE 80 81 443
# http admin_ui http_proxy_protocol https https_proxy_protocol
EXPOSE 80 81 88 443 444
COPY backend /app
COPY frontend/dist /app/frontend

View File

@@ -36,5 +36,6 @@ RUN rm -f /etc/nginx/conf.d/production.conf \
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443
# http admin_ui http_proxy_protocol https https_proxy_protocol
EXPOSE 80 81 88 443 444
ENTRYPOINT [ "/init" ]

View File

@@ -22,10 +22,6 @@ services:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
expose:
- '80-81/tcp'
- '443/tcp'
- '1500-1503/tcp'
networks:
fulltest:
aliases:
@@ -101,7 +97,7 @@ services:
HTTP_PROXY: 'squid:3128'
HTTPS_PROXY: 'squid:3128'
volumes:
- 'cypress_logs:/test/results'
- 'cypress_logs:/results'
- './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro'
command: cypress run --browser chrome --config-file=cypress/config/ci.js

View File

@@ -10,7 +10,9 @@ services:
ports:
- 3080:80
- 3081:81
- 3088:88
- 3443:443
- 3444:444
networks:
nginx_proxy_manager:
aliases:

View File

@@ -1,2 +0,0 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL_stream:50m;

View File

@@ -1,2 +0,0 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;

View File

@@ -1,3 +1,6 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
# intermediate configuration. tweak to your needs.
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';

View File

@@ -8,7 +8,7 @@ BLUE='\E[1;34m'
GREEN='\E[1;32m'
RESET='\E[0m'
S6_OVERLAY_VERSION=3.2.0.2
S6_OVERLAY_VERSION=3.1.5.0
TARGETPLATFORM=${1:-linux/amd64}
# Determine the correct binary file for the architecture given

View File

@@ -220,3 +220,27 @@ To enable the geoip2 module, you can create the custom configuration file `/data
load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so;
load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;
```
## Enabling PROXY protocol for Proxy Hosts
When running NPM behind a load balancer, you might want to use the [PROXY procotol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to receive client information such as the source IP address (useful for banning IPs).
When configuring the PROXY protocol for proxy hosts, NPM uses the ports 88 for http and 444 for https traffic to allow you to decide on a per host basis whether to use the PROSY protocol.
To enable the PROXY protocol for your hosts you need to perform the following steps:
1. Expose the ports `88` (and `444` is applicable) by adjusting your `docker-compose.yml`
2. Edit your proxy hosts to enable the PROXY protocol
3. Edit your upstream load balancer to redirect traffic to the port `88`/`444` and enable the PROXY protocol
## Enabling PROXY protocol for Streams
When running NPM behind a load balancer, you might want to use the [PROXY procotol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to receive client information such as the source IP address (useful for banning IPs).
Keep in mind that the PROXY procotol cannot be enabled for udp endpoints.
To enable the PROXY protocol for streams:
1. Expose the desired port by adjusting you `docker-compose.yml`
2. Edit the Stream to enable the PROXY protocol
3. Edit your upstream load balancer to enable the PROXY protocol

View File

@@ -18,6 +18,8 @@ services:
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# - '88:88' # Public HTTP Port with proxy_protocol enabled
# - '444:444' # Public HTTPS Port with proxy_protocol enabled
# Add any other Stream port you want to expose
# - '21:21' # FTP

View File

@@ -1065,9 +1065,9 @@ vfile@^6.0.0:
vfile-message "^4.0.0"
vite@^5.4.8:
version "5.4.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
version "5.4.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"

View File

@@ -26,7 +26,7 @@ module.exports = {
* Users
*/
showUsers: function () {
const controller = this;
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './users/main'], (App, View) => {
controller.navigate('/users');
@@ -93,7 +93,8 @@ module.exports = {
* Dashboard
*/
showDashboard: function () {
const controller = this;
let controller = this;
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
@@ -105,7 +106,7 @@ module.exports = {
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
const controller = this;
let controller = this;
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
@@ -145,7 +146,8 @@ module.exports = {
*/
showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
const controller = this;
let controller = this;
require(['./main', './nginx/redirection/main'], (App, View) => {
controller.navigate('/nginx/redirection');
App.UI.showAppContent(new View());
@@ -184,7 +186,8 @@ module.exports = {
*/
showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
const controller = this;
let controller = this;
require(['./main', './nginx/stream/main'], (App, View) => {
controller.navigate('/nginx/stream');
App.UI.showAppContent(new View());
@@ -223,7 +226,8 @@ module.exports = {
*/
showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
const controller = this;
let controller = this;
require(['./main', './nginx/dead/main'], (App, View) => {
controller.navigate('/nginx/404');
App.UI.showAppContent(new View());
@@ -274,7 +278,8 @@ module.exports = {
*/
showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
const controller = this;
let controller = this;
require(['./main', './nginx/access/main'], (App, View) => {
controller.navigate('/nginx/access');
App.UI.showAppContent(new View());
@@ -313,7 +318,8 @@ module.exports = {
*/
showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
const controller = this;
let controller = this;
require(['./main', './nginx/certificates/main'], (App, View) => {
controller.navigate('/nginx/certificates');
App.UI.showAppContent(new View());
@@ -377,7 +383,7 @@ module.exports = {
* Audit Log
*/
showAuditLog: function () {
const controller = this;
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/main'], (App, View) => {
controller.navigate('/audit-log');
@@ -405,7 +411,7 @@ module.exports = {
* Settings
*/
showSettings: function () {
const controller = this;
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './settings/main'], (App, View) => {
controller.navigate('/settings');

View File

@@ -24,7 +24,7 @@ module.exports = Mn.View.extend({
},
templateContext: function () {
const view = this;
let view = this;
return {
getUserName: function () {
@@ -48,7 +48,8 @@ module.exports = Mn.View.extend({
},
onRender: function () {
const view = this;
let view = this;
if (typeof view.stats.hosts === 'undefined') {
Api.Reports.getHostStats()
.then(response => {
@@ -71,7 +72,8 @@ module.exports = Mn.View.extend({
// calculate the available columns based on permissions for the objects
// and store as a variable
const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
//let view = this;
let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
perms.map(perm => {
this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;

View File

@@ -33,13 +33,6 @@
<td class="<%- isExpired() ? 'text-danger' : '' %>">
<%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %>
</td>
<td>
<% if (active_domain_names().length > 0) { %>
<span class="status-icon bg-success"></span> <%- i18n('certificates', 'in-use') %>
<% } else { %>
<span class="status-icon bg-danger"></span> <%- i18n('certificates', 'inactive') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
@@ -55,13 +48,6 @@
<div class="dropdown-divider"></div>
<% } %>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
<% if (active_domain_names().length > 0) { %>
<div class="dropdown-divider"></div>
<span class="dropdown-header"><%- i18n('certificates', 'active-domain_names') %></span>
<% active_domain_names().forEach(function(host) { %>
<a href="https://<%- host %>" class="dropdown-item" target="_blank"><%- host %></a>
<% }); %>
<% } %>
</div>
</div>
</td>

View File

@@ -44,24 +44,14 @@ module.exports = Mn.View.extend({
},
},
templateContext: function () {
return {
templateContext: {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
},
dns_providers: dns_providers,
active_domain_names: function () {
const { proxy_hosts = [], redirect_hosts = [], dead_hosts = [] } = this;
return [...proxy_hosts, ...redirect_hosts, ...dead_hosts].reduce((acc, host) => {
acc.push(...(host.domain_names || []));
return acc;
}, []);
}
};
dns_providers: dns_providers
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}

View File

@@ -3,7 +3,6 @@
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('all-hosts', 'cert-provider') %></th>
<th><%- i18n('str', 'expires') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>

View File

@@ -74,7 +74,7 @@ module.exports = Mn.View.extend({
e.preventDefault();
let query = this.ui.query.val();
this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], query)
this.fetch(['owner'], query)
.then(response => this.showData(response))
.catch(err => {
this.showError(err);
@@ -89,7 +89,7 @@ module.exports = Mn.View.extend({
onRender: function () {
let view = this;
view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
view.fetch(['owner'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@@ -72,7 +72,7 @@
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="allow_websocket_upgrade" value="1"<%- allow_websocket_upgrade ? ' checked' : '' %>>
@@ -81,6 +81,24 @@
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="proxy_protocol_enabled" value="1"<%- proxy_protocol_enabled ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('proxy-hosts', 'enable-proxy-protocol') %><a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/#introduction" target="_blank"><i class="fe fe-help-circle"></i></a></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'load-balancer-ip') %> <a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/#changing-the-load-balancers-ip-address-to-the-client-ip-address" target="_blank"><i class="fe fe-help-circle"></i></a></label>
<input type="text" name="loadbalancer_address" class="form-control text-monospace" placeholder="" value="<%- loadbalancer_address %>" autocomplete="off" maxlength="255" <%- proxy_protocol_enabled ? '' : ' disabled' %>>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">

View File

@@ -43,7 +43,9 @@ module.exports = Mn.View.extend({
dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
propagation_seconds: 'input[name="meta[propagation_seconds]"]',
forward_scheme: 'select[name="forward_scheme"]',
letsencrypt: '.letsencrypt'
letsencrypt: '.letsencrypt',
proxy_protocol_enabled: 'input[name="proxy_protocol_enabled"]',
loadbalancer_address: 'input[name="loadbalancer_address"]',
},
regions: {
@@ -101,6 +103,14 @@ module.exports = Mn.View.extend({
}
},
'change @ui.proxy_protocol_enabled': function () {
let checked = this.ui.proxy_protocol_enabled.prop('checked');
this.ui.loadbalancer_address
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
},
'change @ui.dns_challenge_switch': function () {
const checked = this.ui.dns_challenge_switch.prop('checked');
if (checked) {
@@ -167,6 +177,7 @@ module.exports = Mn.View.extend({
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced;
data.proxy_protocol_enabled = !!data.proxy_protocol_enabled;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
@@ -266,6 +277,7 @@ module.exports = Mn.View.extend({
this.ui.ssl_forced.trigger('change');
this.ui.hsts_enabled.trigger('change');
this.ui.proxy_protocol_enabled.trigger('change');
// Domain names
this.ui.domain_names.selectize({

View File

@@ -3,16 +3,8 @@
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body has-tabs">
<div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
<div class="modal-body">
<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="col-sm-12 col-md-12">
<div class="form-group">
@@ -50,141 +42,28 @@
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="proxy_protocol_enabled" value="1"<%- proxy_protocol_enabled ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'enable-proxy-protocol') %><a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/#introduction" target="_blank"><i class="fe fe-help-circle"></i></a></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'load-balancer-ip') %><a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/#changing-the-load-balancers-ip-address-to-the-client-ip-address" target="_blank"><i class="fe fe-help-circle"></i></a></label>
<input type="text" name="loadbalancer_address" class="form-control text-monospace" placeholder="" value="<%- loadbalancer_address %>" autocomplete="off" maxlength="255" <%- proxy_protocol_enabled ? '' : ' disabled' %>>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></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="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- 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>
</div>
<div class="modal-footer">

View File

@@ -2,14 +2,10 @@ const Mn = require('backbone.marionette');
const App = require('../../main');
const StreamModel = require('../../../models/stream');
const template = require('./form.ejs');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson');
require('jquery-mask-plugin');
require('selectize');
const Helpers = require("../../../lib/helpers");
const certListItemTemplate = require("../certificates-list-item.ejs");
const i18n = require("../../i18n");
module.exports = Mn.View.extend({
template: template,
@@ -18,27 +14,26 @@ module.exports = Mn.View.extend({
ui: {
form: 'form',
forwarding_host: 'input[name="forwarding_host"]',
proxy_protocol_enabled: 'input[name="proxy_protocol_enabled"]',
loadbalancer_address: 'input[name="loadbalancer_address"]',
type_error: '.forward-type-error',
buttons: '.modal-footer button',
switches: '.custom-switch-input',
cancel: 'button.cancel',
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'
save: 'button.save'
},
events: {
'change @ui.switches': function () {
this.ui.type_error.hide();
},
'change @ui.proxy_protocol_enabled': function () {
let checked = this.ui.proxy_protocol_enabled.prop('checked');
this.ui.loadbalancer_address
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
},
'click @ui.save': function (e) {
e.preventDefault();
@@ -61,35 +56,7 @@ module.exports = Mn.View.extend({
data.forwarding_port = parseInt(data.forwarding_port, 10);
data.tcp_forwarding = !!data.tcp_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);
}
data.proxy_protocol_enabled = !!data.proxy_protocol_enabled;
let method = App.Api.Nginx.Streams.create;
let is_new = true;
@@ -113,108 +80,14 @@ module.exports = Mn.View.extend({
});
})
.catch(err => {
let more_info = '';
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();
alert(err.message);
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'));
}
});
this.ui.proxy_protocol_enabled.trigger('change');
},
initialize: function (options) {

View File

@@ -16,10 +16,7 @@
</td>
<td>
<div>
<% if (certificate) { %>
<span class="tag"><%- i18n('streams', 'tcp+ssl') %></span>
<% }
else if (tcp_forwarding) { %>
<% if (tcp_forwarding) { %>
<span class="tag"><%- i18n('streams', 'tcp') %></span>
<% }
if (udp_forwarding) { %>
@@ -27,9 +24,6 @@
<% } %>
</div>
</td>
<td>
<div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div>
</td>
<td>
<%
var o = isOnline();

View File

@@ -3,7 +3,6 @@
<th><%- i18n('streams', 'incoming-port') %></th>
<th><%- i18n('str', 'destination') %></th>
<th><%- i18n('streams', 'protocol') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>

View File

@@ -88,7 +88,7 @@ module.exports = Mn.View.extend({
onRender: function () {
let view = this;
view.fetch(['owner', 'certificate'])
view.fetch(['owner'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@@ -133,7 +133,9 @@
"allow-websocket-upgrade": "Websockets Support",
"ignore-invalid-upstream-ssl": "Ignore Invalid SSL",
"custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/",
"search": "Search Host…"
"search": "Search Host…",
"enable-proxy-protocol": "Enable Proxy Protocol",
"load-balancer-ip": "Load balancer or TCP proxy IP / CIDR range"
},
"redirection-hosts": {
"title": "Redirection Hosts",
@@ -180,8 +182,8 @@
"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.",
"search": "Search Incoming Port…",
"ssl-certificate": "SSL Certificate for TCP Forwarding",
"tcp+ssl": "TCP+SSL"
"enable-proxy-protocol": "Enable Proxy Protocol",
"load-balancer-ip": "Load balancer or TCP proxy IP / CIDR range"
},
"certificates": {
"title": "SSL Certificates",
@@ -208,10 +210,7 @@
"reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"download": "Download",
"renew-title": "Renew Let's Encrypt Certificate",
"search": "Search Certificate…",
"in-use" : "In use",
"inactive": "Inactive",
"active-domain_names": "Active domain names"
"search": "Search Certificate…"
},
"access-lists": {
"title": "Access Lists",

View File

@@ -21,6 +21,8 @@ const model = Backbone.Model.extend({
allow_websocket_upgrade: false,
block_exploits: false,
http2_support: false,
proxy_protocol_enabled: false,
loadbalancer_address: '',
advanced_config: '',
enabled: true,
meta: {},

View File

@@ -13,13 +13,12 @@ const model = Backbone.Model.extend({
forwarding_port: null,
tcp_forwarding: true,
udp_forwarding: false,
proxy_protocol_enabled: false,
loadbalancer_address: "",
enabled: true,
meta: {},
certificate_id: 0,
domain_names: [],
// The following are expansions:
owner: null,
certificate: null
owner: null
};
}
});

View File

@@ -161,11 +161,11 @@
},
"domainoffensive": {
"name": "DomainOffensive (do.de)",
"package_name": "certbot-dns-domainoffensive",
"version": "~=2.0.0",
"package_name": "certbot-dns-do",
"version": "~=0.31.0",
"dependencies": "",
"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-domainoffensive"
"full_plugin_name": "dns-do"
},
"domeneshop": {
"name": "Domeneshop",
@@ -364,7 +364,7 @@
"package_name": "certbot-dns-mijn-host",
"version": "~=0.0.4",
"dependencies": "",
"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
"credentials": "dns-mijn-host-credentials = /etc/letsencrypt/mijnhost-credentials.ini",
"full_plugin_name": "dns-mijn-host"
},
"namecheap": {
@@ -534,13 +534,5 @@
"dependencies": "",
"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf",
"full_plugin_name": "edgedns"
},
"zoneedit": {
"name": "ZoneEdit",
"package_name": "certbot-dns-zoneedit",
"version": "~=0.3.2",
"dependencies": "--no-deps dnspython",
"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>",
"full_plugin_name": "dns-zoneedit"
}
}

View File

@@ -1,22 +1,11 @@
FROM cypress/included:14.0.1
FROM cypress/included:13.9.0
COPY --chown=1000 ./test /test
# Disable Cypress CLI colors
ENV FORCE_COLOR=0
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
RUN yarn install && yarn cache clean
ENTRYPOINT []

View File

@@ -32,7 +32,9 @@ describe('Proxy Hosts endpoints', () => {
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
ssl_forced: false
ssl_forced: false,
proxy_protocol_enabled: false,
loadbalancer_address: '',
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);

View File

@@ -1,213 +0,0 @@
/// <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);
}
});
});
});
});
});
});
});

View File

@@ -4,18 +4,18 @@
"description": "",
"main": "index.js",
"dependencies": {
"@jc21/cypress-swagger-validation": "^0.3.2",
"axios": "^1.7.9",
"cypress": "^14.0.1",
"cypress-multi-reporters": "^2.0.5",
"@jc21/cypress-swagger-validation": "^0.3.1",
"axios": "^1.7.7",
"cypress": "^13.15.0",
"cypress-multi-reporters": "^1.6.4",
"cypress-wait-until": "^3.0.2",
"eslint": "^9.19.0",
"eslint": "^9.12.0",
"eslint-plugin-align-assignments": "^1.1.2",
"eslint-plugin-chai-friendly": "^1.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-cypress": "^3.5.0",
"form-data": "^4.0.1",
"lodash": "^4.17.21",
"mocha": "^11.1.0",
"mocha": "^10.7.3",
"mocha-junit-reporter": "^2.2.1"
},
"scripts": {