Compare commits

...

41 Commits

Author SHA1 Message Date
Leonardo J. Caballero G.
7fe117602e
Merge ff102f41073559bf8bd0adcefaef07dec14ed3fc into 79d28f03d035114b80dcd04845306ecb98175074 2025-02-24 06:13:36 -06:00
jc21
79d28f03d0
Merge pull request #4346 from Sander0542/feature/security-schemes-component
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
API Schema Improvements
2025-02-07 12:39:49 +10:00
Jamie Curnow
c4df89df1f
Fix dashboard loading loop and freezing the page 2025-02-06 13:38:47 +10:00
jc21
34c703f8b4
Merge branch 'master' into develop 2025-02-06 08:52:55 +10:00
Jamie Curnow
0a05d8f0ad
Bump version 2025-02-06 08:39:03 +10:00
jc21
0a9141fad5
Merge pull request #4208 from badkeyy/feature/add-zone-edit-certbot-plugin
Add ZoneEdit certbot plugin
2025-02-06 08:33:11 +10:00
jc21
42836774b7
Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-02-06 08:33:01 +10:00
jc21
2a07544f58
Merge pull request #4235 from FabianK3/update-domainoffensive-certbot-plugin
Update DomainOffensive certbot plugin
2025-02-06 08:30:09 +10:00
jc21
dc9d884743
Merge pull request #4292 from icaksh/patch-1
feat: change htpasswd to openssl
2025-02-06 08:29:15 +10:00
jc21
0d5d2b1b7c
Merge pull request #4283 from badkeyy/feature/show-active-host-in-cert-list
SSL Certificates: Show if cert is in use on host
2025-02-06 07:43:12 +10:00
Sander Jochems
df48b835c4
Update order to match others 2025-02-05 22:20:21 +01:00
Sander Jochems
8a1557154a
Add certificate fields to boolFields 2025-02-05 22:15:12 +01:00
Sander Jochems
a6af5ec2c7
Remove certificate as required from proxy host 2025-02-05 18:18:50 +01:00
Sander Jochems
14d7c35fd7
Fix whitespaces 2025-02-05 17:31:09 +01:00
Sander Jochems
cfcf78aaee
Set bearer auth security component 2025-02-05 17:29:40 +01:00
jc21
3a01b2c84f
Merge pull request #4334 from nwagenmakers/mijn-host-patch
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Update certbot-dns-plugins.json (mijn-host)
2025-02-05 20:36:06 +10:00
jc21
e1c84a5c10
Merge pull request #4338 from Sander0542/fix/token-expires-type
Fix type for token.expires
2025-02-05 20:35:33 +10:00
jc21
c56c95a59a
Merge pull request #4344 from NginxProxyManager/stream-ssl
SSL for Streams - 2025
2025-02-05 18:22:51 +10:00
Jamie Curnow
6a60627833
Cypress test for Streams
and updated cypress + packages
2025-02-05 16:02:17 +10:00
Jamie Curnow
b4793d3c16
Adds testssl.sh and mkcert to cypress stack 2025-02-05 08:10:11 +10:00
Jamie Curnow
68a7803513
Fix api schema after merging latest changes 2025-02-04 17:55:28 +10:00
jbowring
2657af97cf
Fix stream update not persisting 2025-02-04 17:14:07 +10:00
jbowring
4452f014b9
Fix whitespace in nginx stream config 2025-02-04 17:14:07 +10:00
jbowring
cd80cc8e4d
Add certificate to streams database model 2025-02-04 17:14:04 +10:00
jbowring
ee4250d770
Add SSL column to streams table UI 2025-02-04 17:12:05 +10:00
jbowring
3dbc70faa6
Add SSL tab to stream UI 2025-02-04 17:12:04 +10:00
jbowring
3091c21cae
Add SSL certificate to TCP streams if certificate in database 2025-02-04 17:12:04 +10:00
Sander Jochems
57cd2a1919
Fix type for token.expires 2025-02-03 21:47:41 +01:00
nwagenmakers
ad5936c530
Update certbot-dns-plugins.json (mijn-host)
Updated credentials hint/text in mijn-host plugin entry
2025-02-01 13:10:53 +01:00
Julian Gassner
c05f9695d0
Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-01-15 15:37:53 +01:00
Julian Gassner
6343b398f0 Add --no-deps 2025-01-15 14:36:38 +00:00
icaksh
59362b7477 feat: change htpasswd to openssl 2025-01-12 19:16:38 +07:00
Julian Gassner
aedaaa18e0 Fix whitespace 2025-01-10 05:20:28 +01:00
Julian Gassner
080bd0b749 Added status of certificates to the certificate list and show on which domain names the certificates are in use 2025-01-10 05:15:22 +01:00
jc21
b4f49969d6
Merge pull request #4261 from NginxProxyManager/develop
v2.12.2
2024-12-29 14:40:05 +10:00
FabianK3
5d087f1256 Update DomainOffensive certbot plugin 2024-12-15 11:35:58 +01:00
Julian Gassner
1e322804ce Add ZoneEdit certbot plugin 2024-12-04 16:47:36 +01:00
jc21
5084cb7296
Merge pull request #4077 from NginxProxyManager/develop
v2.12.1
2024-10-17 09:49:07 +10:00
jc21
e677bfa2e8
Merge pull request #4073 from NginxProxyManager/develop
v2.12.0
2024-10-16 15:41:55 +10:00
Leonardo J. Caballero G
ff102f4107 [FIX] Fixed a typo for Spanish translations 2022-04-20 11:42:28 -04:00
Leonardo J. Caballero G
a69d747007 Added the Spanish translations #2012 2022-04-19 03:09:06 -04:00
44 changed files with 1700 additions and 685 deletions

View File

@ -1 +1 @@
2.12.2
2.12.3

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.2-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.12.3-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

@ -508,8 +508,13 @@ const internalAccessList = {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
.then((/*result*/) => {
utils.execFile('openssl', ['passwd', '-apr1', item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'});
} catch (err) {
reject(err);
}
next();
})
.catch((err) => {

View File

@ -313,6 +313,9 @@ 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') {
@ -464,6 +467,9 @@ 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

@ -1,13 +1,15 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const streamModel = require('../models/stream');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const {castJsonIfNeed} = require('../lib/helpers');
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const streamModel = require('../models/stream');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const internalHost = require('./host');
const {castJsonIfNeed} = require('../lib/helpers');
function omissions () {
return ['is_deleted'];
return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
}
const internalStream = {
@ -18,6 +20,12 @@ const internalStream = {
* @returns {Promise}
*/
create: (access, data) => {
const create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('streams:create', data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
@ -27,16 +35,44 @@ const internalStream = {
data.meta = {};
}
// streams aren't routed by domain name so don't store domain names in the DB
let data_no_domains = structuredClone(data);
delete data_no_domains.domain_names;
return streamModel
.query()
.insertAndFetch(data)
.insertAndFetch(data_no_domains)
.then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, 'stream', row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
return row;
});
})
.then((row) => {
@ -60,6 +96,12 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
const create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('streams:update', data.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
@ -71,16 +113,32 @@ const internalStream = {
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
return streamModel
.query()
.patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
@ -93,6 +151,17 @@ const internalStream = {
return saved_row;
});
});
})
.then(() => {
return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
.then((row) => {
return internalNginx.configure(streamModel, 'stream', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
});
},
@ -115,7 +184,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.first();
if (access_data.permission_visibility !== 'all') {
@ -132,6 +201,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
@ -197,14 +267,14 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ['owner']
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
throw new error.ValidationError('Stream is already enabled');
}
row.enabled = 1;
@ -250,7 +320,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
throw new error.ValidationError('Stream is already disabled');
}
row.enabled = 0;
@ -298,7 +368,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
if (access_data.permission_visibility !== 'all') {
@ -317,6 +387,13 @@ const internalStream = {
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},

View 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');
});
};

View File

@ -4,7 +4,6 @@
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);
@ -68,6 +67,11 @@ 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,
@ -79,6 +83,39 @@ 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

@ -12,7 +12,11 @@ Model.knex(db);
const boolFields = [
'is_deleted',
'ssl_forced',
'http2_support',
'enabled',
'hsts_enabled',
'hsts_subdomains',
];
class DeadHost extends Model {

View File

@ -1,16 +1,15 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
const Model = require('objection').Model;
const db = require('../db');
const helpers = require('../lib/helpers');
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
const boolFields = [
'is_deleted',
'enabled',
'tcp_forwarding',
'udp_forwarding',
];
@ -64,6 +63,17 @@ class Stream extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: 'stream.certificate_id',
to: 'certificate.id'
},
modify: function (qb) {
qb.where('certificate.is_deleted', 0);
}
}
};
}

View File

@ -22,8 +22,7 @@
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains",
"certificate"
"hsts_subdomains"
],
"additionalProperties": false,
"properties": {

View File

@ -19,9 +19,7 @@
"incoming_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"if": {"properties": {"tcp_forwarding": {"const": true}}},
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
"maximum": 65535
},
"forwarding_host": {
"anyOf": [
@ -55,8 +53,24 @@
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"meta": {
"type": "object"
},
"owner": {
"$ref": "./user-object.json"
},
"certificate": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./certificate-object.json"
}
]
}
}
}

View File

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

View File

@ -14,7 +14,7 @@
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["access_list", "owner", "certificate"]
"enum": ["owner", "certificate"]
}
}
],
@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
"enabled": true
"enabled": true,
"certificate_id": 0
}
]
}

View File

@ -32,6 +32,9 @@
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
}
@ -73,7 +76,8 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
}
},
"certificate_id": 0
}
}
},

View File

@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
"enabled": true
"enabled": true,
"certificate_id": 0
}
}
},

View File

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

View File

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

View File

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

View File

@ -9,6 +9,15 @@
"url": "http://127.0.0.1:81/api"
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": {
"/": {
"get": {

View File

@ -2,6 +2,7 @@
{% 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

@ -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 -%}

View File

@ -5,12 +5,10 @@
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
listen {{ incoming_port }};
{% if ipv6 -%}
listen [::]:{{ incoming_port }};
{% else -%}
#listen [::]:{{ incoming_port }};
{% endif %}
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{%- include "_certificates_stream.conf" %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
@ -19,14 +17,12 @@ server {
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 -%}
server {
listen {{ incoming_port }} udp;
{% if ipv6 -%}
listen [::]:{{ incoming_port }} udp;
{% else -%}
#listen [::]:{{ incoming_port }} udp;
{% endif %}
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom

View File

@ -22,6 +22,10 @@ services:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
expose:
- '80-81/tcp'
- '443/tcp'
- '1500-1503/tcp'
networks:
fulltest:
aliases:
@ -97,7 +101,7 @@ services:
HTTP_PROXY: 'squid:3128'
HTTPS_PROXY: 'squid:3128'
volumes:
- 'cypress_logs:/results'
- 'cypress_logs:/test/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

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

View File

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

View File

@ -1,6 +1,3 @@
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.1.5.0
S6_OVERLAY_VERSION=3.2.0.2
TARGETPLATFORM=${1:-linux/amd64}
# Determine the correct binary file for the architecture given

View File

@ -4,444 +4,438 @@ const Tokens = require('./tokens');
module.exports = {
/**
* @param {String} route
* @param {Object} [options]
* @returns {Boolean}
*/
navigate: function (route, options) {
options = options || {};
Backbone.history.navigate(route.toString(), options);
return true;
},
/**
* @param {String} route
* @param {Object} [options]
* @returns {Boolean}
*/
navigate: function (route, options) {
options = options || {};
Backbone.history.navigate(route.toString(), options);
return true;
},
/**
* Login
*/
showLogin: function () {
window.location = '/login';
},
/**
* Login
*/
showLogin: function () {
window.location = '/login';
},
/**
* Users
*/
showUsers: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './users/main'], (App, View) => {
controller.navigate('/users');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Users
*/
showUsers: function () {
const controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './users/main'], (App, View) => {
controller.navigate('/users');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* User Form
*
* @param [model]
*/
showUserForm: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Form
*
* @param [model]
*/
showUserForm: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Permissions Form
*
* @param model
*/
showUserPermissions: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/permissions'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Permissions Form
*
* @param model
*/
showUserPermissions: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/permissions'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Password Form
*
* @param model
*/
showUserPasswordForm: function (model) {
if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
require(['./main', './user/password'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Password Form
*
* @param model
*/
showUserPasswordForm: function (model) {
if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
require(['./main', './user/password'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Delete Confirm
*
* @param model
*/
showUserDeleteConfirm: function (model) {
if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
require(['./main', './user/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Delete Confirm
*
* @param model
*/
showUserDeleteConfirm: function (model) {
if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
require(['./main', './user/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dashboard
*/
showDashboard: function () {
let controller = this;
/**
* Dashboard
*/
showDashboard: function () {
const controller = this;
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
});
},
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
});
},
/**
* Nginx Proxy Hosts
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
const controller = this;
/**
* Nginx Proxy Hosts
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
let controller = this;
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
App.UI.showAppContent(new View());
});
}
},
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Proxy Host Form
*
* @param [model]
*/
showNginxProxyForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Proxy Host Form
*
* @param [model]
*/
showNginxProxyForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Proxy Host Delete Confirm
*
* @param model
*/
showNginxProxyDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Proxy Host Delete Confirm
*
* @param model
*/
showNginxProxyDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Redirection Hosts
*/
showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
const controller = this;
require(['./main', './nginx/redirection/main'], (App, View) => {
controller.navigate('/nginx/redirection');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Redirection Hosts
*/
showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
let controller = this;
/**
* Nginx Redirection Host Form
*
* @param [model]
*/
showNginxRedirectionForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
require(['./main', './nginx/redirection/main'], (App, View) => {
controller.navigate('/nginx/redirection');
App.UI.showAppContent(new View());
});
}
},
/**
* Proxy Redirection Delete Confirm
*
* @param model
*/
showNginxRedirectionDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Redirection Host Form
*
* @param [model]
*/
showNginxRedirectionForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Stream Hosts
*/
showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
const controller = this;
require(['./main', './nginx/stream/main'], (App, View) => {
controller.navigate('/nginx/stream');
App.UI.showAppContent(new View());
});
}
},
/**
* Proxy Redirection Delete Confirm
*
* @param model
*/
showNginxRedirectionDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Stream Form
*
* @param [model]
*/
showNginxStreamForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Stream Hosts
*/
showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
let controller = this;
/**
* Stream Delete Confirm
*
* @param model
*/
showNginxStreamDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
require(['./main', './nginx/stream/main'], (App, View) => {
controller.navigate('/nginx/stream');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Dead Hosts
*/
showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
const controller = this;
require(['./main', './nginx/dead/main'], (App, View) => {
controller.navigate('/nginx/404');
App.UI.showAppContent(new View());
});
}
},
/**
* Stream Form
*
* @param [model]
*/
showNginxStreamForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dead Host Form
*
* @param [model]
*/
showNginxDeadForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Stream Delete Confirm
*
* @param model
*/
showNginxStreamDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dead Host Delete Confirm
*
* @param model
*/
showNginxDeadDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Dead Hosts
*/
showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
let controller = this;
/**
* Help Dialog
*
* @param {String} title
* @param {String} content
*/
showHelp: function (title, content) {
require(['./main', './help/main'], function (App, View) {
App.UI.showModalDialog(new View({title: title, content: content}));
});
},
require(['./main', './nginx/dead/main'], (App, View) => {
controller.navigate('/nginx/404');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Access
*/
showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
const controller = this;
require(['./main', './nginx/access/main'], (App, View) => {
controller.navigate('/nginx/access');
App.UI.showAppContent(new View());
});
}
},
/**
* Dead Host Form
*
* @param [model]
*/
showNginxDeadForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Access List Form
*
* @param [model]
*/
showNginxAccessListForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dead Host Delete Confirm
*
* @param model
*/
showNginxDeadDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Access List Delete Confirm
*
* @param model
*/
showNginxAccessListDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Help Dialog
*
* @param {String} title
* @param {String} content
*/
showHelp: function (title, content) {
require(['./main', './help/main'], function (App, View) {
App.UI.showModalDialog(new View({title: title, content: content}));
});
},
/**
* Nginx Certificates
*/
showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
const controller = this;
require(['./main', './nginx/certificates/main'], (App, View) => {
controller.navigate('/nginx/certificates');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Access
*/
showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
let controller = this;
/**
* Nginx Certificate Form
*
* @param [model]
*/
showNginxCertificateForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
require(['./main', './nginx/access/main'], (App, View) => {
controller.navigate('/nginx/access');
App.UI.showAppContent(new View());
});
}
},
/**
* Certificate Renew
*
* @param model
*/
showNginxCertificateRenew: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/renew'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Access List Form
*
* @param [model]
*/
showNginxAccessListForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Certificate Delete Confirm
*
* @param model
*/
showNginxCertificateDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Access List Delete Confirm
*
* @param model
*/
showNginxAccessListDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Certificate Test Reachability
*
* @param model
*/
showNginxCertificateTestReachability: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/test'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Certificates
*/
showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
let controller = this;
/**
* Audit Log
*/
showAuditLog: function () {
const controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/main'], (App, View) => {
controller.navigate('/audit-log');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
require(['./main', './nginx/certificates/main'], (App, View) => {
controller.navigate('/nginx/certificates');
App.UI.showAppContent(new View());
});
}
},
/**
* Audit Log Metadata
*
* @param model
*/
showAuditMeta: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/meta'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Certificate Form
*
* @param [model]
*/
showNginxCertificateForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Settings
*/
showSettings: function () {
const controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './settings/main'], (App, View) => {
controller.navigate('/settings');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Certificate Renew
*
* @param model
*/
showNginxCertificateRenew: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/renew'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Settings Item Form
*
* @param model
*/
showSettingForm: function (model) {
if (Cache.User.isAdmin()) {
if (model.get('id') === 'default-site') {
require(['./main', './settings/default-site/main'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
}
},
/**
* Certificate Delete Confirm
*
* @param model
*/
showNginxCertificateDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Certificate Test Reachability
*
* @param model
*/
showNginxCertificateTestReachability: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/test'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Audit Log
*/
showAuditLog: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/main'], (App, View) => {
controller.navigate('/audit-log');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Audit Log Metadata
*
* @param model
*/
showAuditMeta: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/meta'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Settings
*/
showSettings: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './settings/main'], (App, View) => {
controller.navigate('/settings');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Settings Item Form
*
* @param model
*/
showSettingForm: function (model) {
if (Cache.User.isAdmin()) {
if (model.get('id') === 'default-site') {
require(['./main', './settings/default-site/main'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
}
},
/**
* Logout
*/
logout: function () {
Tokens.dropTopToken();
this.showLogin();
}
/**
* Logout
*/
logout: function () {
Tokens.dropTopToken();
this.showLogin();
}
};

View File

@ -6,87 +6,85 @@ const Helpers = require('../../lib/helpers');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
template: template,
id: 'dashboard',
columns: 0,
template: template,
id: 'dashboard',
columns: 0,
stats: {},
stats: {},
ui: {
links: 'a'
},
ui: {
links: 'a'
},
events: {
'click @ui.links': function (e) {
e.preventDefault();
Controller.navigate($(e.currentTarget).attr('href'), true);
}
},
events: {
'click @ui.links': function (e) {
e.preventDefault();
Controller.navigate($(e.currentTarget).attr('href'), true);
}
},
templateContext: function () {
let view = this;
templateContext: function () {
const view = this;
return {
getUserName: function () {
return Cache.User.get('nickname') || Cache.User.get('name');
},
return {
getUserName: function () {
return Cache.User.get('nickname') || Cache.User.get('name');
},
getHostStat: function (type) {
if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
return Helpers.niceNumber(view.stats.hosts[type]);
}
getHostStat: function (type) {
if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
return Helpers.niceNumber(view.stats.hosts[type]);
}
return '-';
},
return '-';
},
canShow: function (perm) {
return Cache.User.isAdmin() || Cache.User.canView(perm);
},
canShow: function (perm) {
return Cache.User.isAdmin() || Cache.User.canView(perm);
},
columns: view.columns
};
},
columns: view.columns
};
},
onRender: function () {
let view = this;
onRender: function () {
const view = this;
if (typeof view.stats.hosts === 'undefined') {
Api.Reports.getHostStats()
.then(response => {
if (!view.isDestroyed()) {
view.stats.hosts = response;
view.render();
}
})
.catch(err => {
console.log(err);
});
}
},
if (typeof view.stats.hosts === 'undefined') {
Api.Reports.getHostStats()
.then(response => {
if (!view.isDestroyed()) {
view.stats.hosts = response;
view.render();
}
})
.catch(err => {
console.log(err);
});
}
},
/**
* @param {Object} [model]
*/
preRender: function (model) {
this.columns = 0;
/**
* @param {Object} [model]
*/
preRender: function (model) {
this.columns = 0;
// calculate the available columns based on permissions for the objects
// and store as a variable
const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
// calculate the available columns based on permissions for the objects
// and store as a variable
//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;
});
perms.map(perm => {
this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
});
// Prevent double rendering on initial calls
if (typeof model !== 'undefined') {
this.render();
}
},
// Prevent double rendering on initial calls
if (typeof model !== 'undefined') {
this.render();
}
},
initialize: function () {
this.preRender();
this.listenTo(Cache.User, 'change', this.preRender);
}
initialize: function () {
this.preRender();
this.listenTo(Cache.User, 'change', this.preRender);
}
});

View File

@ -33,6 +33,13 @@
<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">
@ -48,7 +55,14 @@
<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,14 +44,24 @@ module.exports = Mn.View.extend({
},
},
templateContext: {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
},
dns_providers: dns_providers
templateContext: function () {
return {
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;
}, []);
}
};
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}

View File

@ -3,6 +3,7 @@
<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'], query)
this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], 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'])
view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@ -3,48 +3,187 @@
<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">
<div class="modal-body has-tabs">
<div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
<input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
<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">
<label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
<input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
</div>
</div>
<div class="col-sm-8 col-md-8">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
<input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
</div>
</div>
<div class="col-sm-4 col-md-4">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
<input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
</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="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
</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="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
</label>
</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>
<div class="col-sm-8 col-md-8">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
<input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
<!-- 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 class="col-sm-4 col-md-4">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
<input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
</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="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
</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="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
</label>
</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>
</form>
</div>

View File

@ -1,24 +1,38 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const StreamModel = require('../../../models/stream');
const template = require('./form.ejs');
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,
className: 'modal-dialog',
ui: {
form: 'form',
forwarding_host: 'input[name="forwarding_host"]',
type_error: '.forward-type-error',
buttons: '.modal-footer button',
switches: '.custom-switch-input',
cancel: 'button.cancel',
save: 'button.save'
form: 'form',
forwarding_host: 'input[name="forwarding_host"]',
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'
},
events: {
@ -48,6 +62,35 @@ module.exports = Mn.View.extend({
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);
}
let method = App.Api.Nginx.Streams.create;
let is_new = true;
@ -70,10 +113,108 @@ module.exports = Mn.View.extend({
});
})
.catch(err => {
alert(err.message);
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();
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) {

View File

@ -16,7 +16,10 @@
</td>
<td>
<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>
<% }
if (udp_forwarding) { %>
@ -24,6 +27,9 @@
<% } %>
</div>
</td>
<td>
<div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div>
</td>
<td>
<%
var o = isOnline();

View File

@ -3,6 +3,7 @@
<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'])
view.fetch(['owner', 'certificate'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@ -179,7 +179,9 @@
"delete-confirm": "Are you sure you want to delete this 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.",
"search": "Search Incoming Port…"
"search": "Search Incoming Port…",
"ssl-certificate": "SSL Certificate for TCP Forwarding",
"tcp+ssl": "TCP+SSL"
},
"certificates": {
"title": "SSL Certificates",
@ -206,7 +208,10 @@
"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…"
"search": "Search Certificate…",
"in-use" : "In use",
"inactive": "Inactive",
"active-domain_names": "Active domain names"
},
"access-lists": {
"title": "Access Lists",
@ -292,5 +297,297 @@
"default-site-html": "Custom Page",
"default-site-redirect": "Redirect"
}
},
"es": {
"str": {
"email-address": "Dirección de correo electrónico",
"username": "Nombre de usuario",
"password": "Contraseña",
"sign-in": "Registrarse",
"sign-out": "Cerrar sesión",
"try-again": "Intentar otra vez",
"name": "Nombre",
"email": "Correo electrónico",
"roles": "Roles",
"created-on": "Creado: {date}",
"save": "Guardar",
"cancel": "Cancelar",
"close": "Cerrar",
"enable": "Habilitar",
"disable": "Inhabilitar",
"sure": "Si, estoy seguro",
"disabled": "Inhabilitado",
"choose-file": "Elija el archivo",
"source": "Fuente",
"destination": "Destino",
"ssl": "SSL",
"access": "Acceso",
"public": "Público",
"edit": "Editar",
"delete": "Borrar",
"logs": "Registros",
"status": "Estatus",
"online": "En línea",
"offline": "Desconectado",
"unknown": "Desconocido",
"expires": "Caduca",
"value": "Valor",
"please-wait": "Por favor, espere...",
"all": "Todo",
"any": "Ninguno"
},
"login": {
"title": "Ingrese a su cuenta"
},
"main": {
"app": "Nginx Proxy Manager",
"version": "v{version}",
"welcome": "Bienvenidos a Nginx Proxy Manager",
"logged-in": "Has iniciado sesión como {name}",
"unknown-error": "Error al cargar cosas. Vuelva a cargar la aplicación.",
"unknown-user": "Usuario desconocido",
"sign-in-as": "Vuelve a iniciar sesión como {name}"
},
"roles": {
"title": "Roles",
"admin": "Administrador",
"user": "Helicóptero Apache"
},
"menu": {
"dashboard": "Panel",
"hosts": "Servidores"
},
"footer": {
"fork-me": "Bifurcame en Github",
"copy": "&copy; 2022 <a href=\"{url}\" target=\"_blank\">jc21.com</a>.",
"theme": "Tema por <a href=\"{url}\" target=\"_blank\">Tabler</a>"
},
"dashboard": {
"title": "Hola {name}"
},
"all-hosts": {
"empty-subtitle": "{manage, select, true{¿Por qué no creas uno?} other{Y no tienes permiso para crear uno.}}",
"details": "Detalles",
"enable-ssl": "Habilitar SSL",
"force-ssl": "Forzar SSL",
"http2-support": "Soporte HTTP/2",
"domain-names": "Nombres de Dominios",
"cert-provider": "Proveedor de certificados",
"block-exploits": "Bloquear exploits comunes",
"caching-enabled": "Caché de Activos",
"ssl-certificate": "Certificado SSL",
"none": "Ninguno",
"new-cert": "Solicitar un nuevo Certificado SSL",
"with-le": "con Let's Encrypt",
"no-ssl": "Este servidor no usará HTTPS",
"advanced": "Avanzado",
"advanced-warning": "¡Ingrese su configuración Nginx personalizada aquí bajo su propio riesgo!",
"advanced-config": "Configuración Nginx personalizada",
"advanced-config-var-headline": "Estos detalles del proxy están disponibles como variables Nginx:",
"advanced-config-header-info": "Tenga en cuenta que cualquier directiva add_header o set_header agregada aquí no será utilizada por Nginx. Deberá agregar una ubicación personalizada '/' y agregar el encabezado en la configuración personalizada allí.",
"hsts-enabled": "HSTS habilitado",
"hsts-subdomains": "Subdominios HSTS",
"locations": "Ubicaciones personalizadas"
},
"locations": {
"new_location": "Agregar ubicación",
"path": "/ruta",
"location_label": "Definir ubicación",
"delete": "Borrar"
},
"ssl": {
"letsencrypt": "Let's Encrypt",
"other": "Personalizada",
"none": "solo HTTP",
"letsencrypt-email": "Dirección de correo electrónico para Let's Encrypt",
"letsencrypt-agree": "Acepto los <a href=\"{url}\" target=\"_blank\">Términos de servicio de Let's Encrypt</a>",
"delete-ssl": "Los certificados SSL adjuntos NO se eliminarán, deberán eliminarse manualmente.",
"hosts-warning": "Estos dominios ya deben estar configurados para apuntar a esta instalación",
"no-wildcard-without-dns": "No se puede solicitar el certificado de Let's Encrypt para dominios comodín cuando no se usa el desafío de DNS",
"dns-challenge": "Utilice un desafío de DNS",
"certbot-warning": "Esta sección requiere algunos conocimientos sobre Certbot y sus complementos de DNS. Consulte la documentación de los complementos respectivos.",
"dns-provider": "Proveedor de DNS",
"please-choose": "Por favor, elige...",
"credentials-file-content": "Contenido del archivo de credenciales",
"credentials-file-content-info": "Este complemento requiere un archivo de configuración que contenga un token API u otras credenciales para su proveedor",
"stored-as-plaintext-info": "¡Estos datos se almacenarán como texto sin formato en la base de datos y en un archivo!",
"propagation-seconds": "Segundos de propagación",
"propagation-seconds-info": "Deje vacío para usar el valor predeterminado de los complementos. Número de segundos de espera para la propagación de DNS.",
"processing-info": "Procesando... Esto puede tardar unos minutos.",
"passphrase-protection-support-info": "Los archivos clave protegidos con una frase de contraseña no son compatibles."
},
"proxy-hosts": {
"title": "Servidores proxy",
"empty": "No hay Servidores proxy",
"add": "Agregar Servidor proxy",
"form-title": "{id, select, undefined{Nuevo} other{Editar}} Servidor proxy",
"forward-scheme": "Esquema",
"forward-host": "Reenviar nombre de servidor / IP",
"forward-port": "Reenviar Puerto",
"delete": "Borrar Servidor Proxy",
"delete-confirm": "¿Está seguro de que desea eliminar el Servidor Proxy para: <strong>{domains}</strong>?",
"help-title": "¿Qué es un Servidor proxy?",
"help-content": "Un servidor proxy es el punto final entrante para un servicio web que desea reenviar.\nProporciona una terminación SSL opcional para su servicio que podría no tener soporte SSL integrado.\nLos servidores proxy son el uso más común para Nginx Proxy Manager.",
"access-list": "Lista de acceso",
"allow-websocket-upgrade": "Soporte WebSockets",
"ignore-invalid-upstream-ssl": "Ignorar SSL no válido",
"custom-forward-host-help": "Agregue una ruta para el reenvío de subcarpetas.\nEjemplo: 203.0.113.25/ruta",
"search": "Buscar Servidor..."
},
"redirection-hosts": {
"title": "Servidores de redirección",
"empty": "No hay Servidores de redirección",
"add": "Agregar Servidor de redirección",
"form-title": "{id, select, undefined{Nuevo} other{Editar}} Servidor de redirección",
"forward-scheme": "Esquema",
"forward-http-status-code": "Código HTTP",
"forward-domain": "Reenviar dominio",
"preserve-path": "Conservar ruta",
"delete": "Borrar Servidor de redirección",
"delete-confirm": "¿Está seguro de que desea eliminar el servidor de redirección para: <strong>{domains}</strong>?",
"help-title": "¿Qué es un Servidor de redirección?",
"help-content": "Un servidor de redirección redirigirá las solicitudes del dominio entrante y empujará al espectador a otro dominio.\nLa razón más común para usar este tipo de servidor es cuando su sitio web cambia de dominio pero aún tiene un motor de búsqueda o enlaces de referencia que apuntan al dominio anterior.",
"search": "Buscar Servidor..."
},
"dead-hosts": {
"title": "",
"empty": "No hay Servidores 404",
"add": "Agregar Servidor 404",
"form-title": "{id, select, undefined{Nuevo} other{Editar}} Servidor 404",
"delete": "Borrar Servidor 404",
"delete-confirm": "¿Está seguro de que desea eliminar este Servidor 404?",
"help-title": "¿Qué es un Servidor 404?",
"help-content": "Un servidor 404 es simplemente una configuración de servidor que muestra una página 404.\nEsto puede ser útil cuando su dominio aparece en los motores de búsqueda y desea proporcionar una página de error más agradable o específicamente para decirles a los indexadores de búsqueda que las páginas del dominio ya no existen.\nOtro beneficio de tener este servidor es rastrear los registros de visitas y ver las referencias.",
"search": "Buscar Servidor..."
},
"streams": {
"title": "Streams",
"empty": "No hay Streams",
"add": "Agregar Stream",
"form-title": "{id, select, undefined{Nuevo} other{Editar}} Stream",
"incoming-port": "Puerto entrante",
"forwarding-host": "Servidor de reenvío",
"forwarding-port": "Puerto de reenvío",
"tcp-forwarding": "Reenvío TCP",
"udp-forwarding": "Reenvío UDP",
"forward-type-error": "Al menos un tipo de protocolo debe estar habilitado",
"protocol": "Protocol",
"tcp": "TCP",
"udp": "UDP",
"delete": "Borrar Stream",
"delete-confirm": "¿Está seguro de que desea eliminar esta Stream?",
"help-title": "¿Qué es un Stream?",
"help-content": "Una característica relativamente nueva para Nginx, un Stream servirá para reenviar el tráfico TCP/UDP directamente a otra computadora en la red.\nSi está ejecutando servidores de juegos, servidores FTP o SSH, esto puede ser útil.",
"search": "Buscar Puerto entrante..."
},
"certificates": {
"title": "Certificados SSL",
"empty": "No hay Certificados SSL",
"add": "Agregar Certificado SSL",
"form-title": "Agregar {provider, select, letsencrypt Certificado {Let's Encrypt} other{personalizado}}",
"delete": "Borrar Certificado SSL",
"delete-confirm": "¿Está seguro de que desea eliminar este certificado SSL? Cualquier servidor que lo use deberá actualizarse más tarde.",
"help-title": "Certificados SSL",
"help-content": "Los certificados SSL (correctamente conocidos como Certificados TLS) son una forma de clave de encriptación que permite que su sitio sea encriptado para el usuario final.\nNginx Proxy Manager usa un servicio llamado Let's Encrypt para emitir certificados SSL gratis.\nSi tiene algún tipo de información personal, contraseñas o datos confidenciales detrás de Nginx Proxy Manager, probablemente sea una buena idea usar un certificado.\nNginx Proxy Manager también es compatible con la autenticación DNS si no está ejecutando su sitio frente a Internet, o si solo desea un certificado comodín.",
"other-certificate": "Certificado",
"other-certificate-key": "Clave de Certificado",
"other-intermediate-certificate": "Certificado Intermedio",
"force-renew": "Renovar Ahora",
"test-reachability": "Accesibilidad del servidor de prueba",
"reachability-title": "Accesibilidad del servidor de prueba",
"reachability-info": "Pruebe si se puede acceder a los dominios desde la Internet pública utilizando Site24x7. Esto no es necesario cuando se utiliza el Desafío DNS.",
"reachability-failed-to-reach-api": "La comunicación con la API falló, ¿Nginx Proxy Manager se está ejecutando correctamente?",
"reachability-failed-to-check": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com.",
"reachability-ok": "Su servidor es accesible y la creación de certificados debería ser posible.",
"reachability-404": "Hay un servidor encontrado en este dominio, pero no parece ser Nginx Proxy Manager. Asegúrese de que su dominio apunte a la IP donde se ejecuta su instancia de Nginx Proxy Manager.",
"reachability-not-resolved": "No hay servidor disponible en este dominio. Asegúrese de que su dominio exista y apunte a la IP donde se ejecuta su instancia de Nginx Proxy Manager y, si es necesario, se reenvía el puerto 80 en su enrutador.",
"reachability-wrong-data": "Se encontró un servidor en este dominio pero devolvió datos inesperados. ¿Es el servidor Nginx Proxy Manager? Asegúrese de que su dominio apunte a la IP donde se ejecuta su instancia de Nginx Proxy Manager.",
"reachability-other": "Se encontró un servidor en este dominio pero devolvió un código de estado inesperado {code}. ¿Es el servidor Nginx Proxy Manager? Asegúrese de que su dominio apunte a la IP donde se ejecuta su instancia de Nginx Proxy Manager.",
"download": "Descargar",
"renew-title": "Renovar Certificado Let's Encrypt",
"search": "Buscar Certificado..."
},
"access-lists": {
"title": "Listas de acceso",
"empty": "No hay Listas de acceso",
"add": "Agregar Listas de acceso",
"form-title": "{id, select, undefined{Nueva} other{Editar}} Lista de acceso",
"delete": "Borrar Lista de acceso",
"delete-confirm": "¿Está seguro de que desea eliminar esta lista de acceso?",
"public": "Públicamente Accesible",
"public-sub": "Sin restricciones de acceso",
"help-title": "¿Qué es una lista de acceso?",
"help-content": "Las listas de acceso proporcionan una lista negra o una lista blanca de direcciones IP de clientes específicos junto con la autenticación para los servidores proxy a través de la autenticación HTTP básica.\nPuede configurar varias reglas de cliente, nombres de usuario y contraseñas para una sola lista de acceso y luego aplicarla a un servidor proxy.\nEsto es más útil para los servicios web reenviados que no tienen mecanismos de autenticación integrados o que desea proteger del acceso de clientes desconocidos.",
"item-count": "{count} {count, select, 1{Usuario} other{Usuarios}}",
"client-count": "{count} {count, select, 1{Regla} other{Reglas}}",
"proxy-host-count": "{count} {count, select, 1{Servidor Proxy} other{Servidores Proxy}}",
"delete-has-hosts": "Esta lista de acceso está asociada con {count} Servidores Proxy. Estarán disponibles públicamente al eliminarlos.",
"details": "Detalles",
"authorization": "Autorización",
"access": "Acceso",
"satisfy": "Satisfacer",
"satisfy-any": "Satisfacer cualquiera",
"pass-auth": "Pasar autenticación al servidor",
"access-add": "Agregar",
"auth-add": "Agregar",
"search": "Buscar Acceso..."
},
"users": {
"title": "Usuarios",
"default_error": "Se debe cambiar la dirección de correo electrónico predeterminada",
"add": "Agregar Usuario",
"nickname": "Apodo",
"full-name": "Nombre completo",
"edit-details": "Editar Detalles",
"change-password": "Cambia contraseña",
"edit-permissions": "Editar Permisos",
"sign-in-as": "Iniciar sesión como usuario",
"form-title": "{id, select, undefined{Nuevo} other{Editar}} Usuario",
"delete": "Borrar {name, select, undefined{Usuario} other{{name}}}",
"delete-confirm": "¿Está seguro de que desea eliminar <strong>{name}</strong>?",
"password-title": "Cambia la contraseña{self, select, false{ for {name}} other{}}",
"current-password": "Contraseña actual",
"new-password": "Nueva contraseña",
"confirm-password": "Confirmar contraseña",
"permissions-title": "Permisos para {name}",
"admin-perms": "Este usuario es un administrador y algunos elementos no se pueden modificar",
"perms-visibility": "Visibilidad del elemento",
"perms-visibility-user": "Solo elementos creados",
"perms-visibility-all": "Todos los elementos",
"perm-manage": "Administrar",
"perm-view": "Sólo vista",
"perm-hidden": "Oculto",
"search": "Buscar Usuario..."
},
"audit-log": {
"title": "Registro de Auditoría",
"empty": "No hay Registros.",
"empty-subtitle": "Tan pronto como usted u otro usuario cambien algo, el historial de esos eventos aparecerá aquí.",
"proxy-host": "Servidor Proxy",
"redirection-host": "Servidor de redirección",
"dead-host": "Servidor 404",
"stream": "Stream",
"user": "Usuario",
"certificate": "Certificado",
"access-list": "Lista de acceso",
"created": "Creado {name}",
"updated": "Actualizado {name}",
"deleted": "Eliminado {name}",
"enabled": "Activado {name}",
"disabled": "Desactivado {name}",
"renewed": "Renovado {name}",
"meta-title": "Detalles del evento",
"view-meta": "Ver Detalles",
"date": "Fecha",
"search": "Buscar Registro..."
},
"settings": {
"title": "Ajustes",
"default-site": "Sitio por defecto",
"default-site-congratulations": "Página de felicitaciones",
"default-site-404": "Página de 404",
"default-site-html": "Pagina personalizada",
"default-site-redirect": "Redirigir"
}
}
}

View File

@ -15,8 +15,11 @@ const model = Backbone.Model.extend({
udp_forwarding: false,
enabled: true,
meta: {},
certificate_id: 0,
domain_names: [],
// The following are expansions:
owner: null
owner: null,
certificate: null
};
}
});

View File

@ -161,11 +161,11 @@
},
"domainoffensive": {
"name": "DomainOffensive (do.de)",
"package_name": "certbot-dns-do",
"version": "~=0.31.0",
"package_name": "certbot-dns-domainoffensive",
"version": "~=2.0.0",
"dependencies": "",
"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-do"
"full_plugin_name": "dns-domainoffensive"
},
"domeneshop": {
"name": "Domeneshop",
@ -364,7 +364,7 @@
"package_name": "certbot-dns-mijn-host",
"version": "~=0.0.4",
"dependencies": "",
"credentials": "dns-mijn-host-credentials = /etc/letsencrypt/mijnhost-credentials.ini",
"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-mijn-host"
},
"namecheap": {
@ -534,5 +534,13 @@
"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,11 +1,22 @@
FROM cypress/included:13.9.0
COPY --chown=1000 ./test /test
FROM cypress/included:14.0.1
# 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

@ -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);
}
});
});
});
});
});
});
});

View File

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