Compare commits

..

15 Commits

Author SHA1 Message Date
Julian Gassner
8796b36d75 Merge 5d2a76adfe into 498109addb 2025-02-05 07:10:39 +00:00
Julian Gassner
5d2a76adfe Merge branch 'feature/add-multi-factor-authentication' of https://github.com/badkeyy/nginx-proxy-manager into feature/add-multi-factor-authentication 2025-02-05 07:10:35 +00:00
Julian Gassner
33906bfcdc Remove package-lock 2025-02-05 07:10:34 +00:00
Julian Gassner
6e51d819fa Merge branch 'develop' into feature/add-multi-factor-authentication 2025-02-05 08:08:51 +01:00
Julian Gassner
8aa173a732 Finish MFA implementation 2025-02-05 07:05:15 +00:00
Julian Gassner
35938db24b Added MFA to model and internal user 2025-01-15 14:33:11 +00:00
Julian Gassner
69f7920675 Remove console.logs 2025-01-09 23:04:13 +00:00
Julian Gassner
45fc63875c Merge branch 'feature/show-active-host-in-cert-list' of https://github.com/badkeyy/nginx-proxy-manager into feature/show-active-host-in-cert-list 2025-01-09 22:49:21 +00:00
Julian Gassner
f3fee7d886 Merge 2025-01-09 22:48:18 +00:00
Julian Gassner
50f7bfc726 Add dead_hosts and redirection_hosts 2025-01-09 22:47:08 +00:00
Julian Gassner
8fb9d9fec6 Removed whitespace 2025-01-09 18:32:45 +01:00
Julian Gassner
cea9a17218 Retrigger build 2025-01-09 18:07:50 +01:00
Julian Gassner
6bbe7d4cc4 Fix alignment 2025-01-08 17:48:09 +01:00
Julian Gassner
c6ab315165 Add status indicator to certificates and show active domain names 2025-01-08 05:43:10 +01:00
Julian Gassner
2c1595756d Add hosts to cert list 2024-12-18 18:10:20 +01:00
65 changed files with 2337 additions and 3699 deletions

View File

@@ -1 +1 @@
2.12.4
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.4-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

@@ -3,8 +3,6 @@
const schema = require('./schema');
const logger = require('./logger').global;
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false';
async function appStart () {
const migrate = require('./migrate');
const setup = require('./setup');
@@ -15,16 +13,7 @@ async function appStart () {
return migrate.latest()
.then(setup)
.then(schema.getCompiledSchema)
.then(() => {
if (IP_RANGES_FETCH_ENABLED) {
logger.info('IP Ranges fetch is enabled');
return internalIpRanges.fetch().catch((err) => {
logger.error('IP Ranges fetch failed, continuing anyway:', err.message);
});
} else {
logger.info('IP Ranges fetch is disabled by environment variable');
}
})
.then(internalIpRanges.fetch)
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();

View File

@@ -508,13 +508,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) => {

75
backend/internal/mfa.js Normal file
View File

@@ -0,0 +1,75 @@
const authModel = require('../models/auth');
const error = require('../lib/error');
const speakeasy = require('speakeasy');
module.exports = {
validateMfaTokenForUser: (userId, token) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth || !auth.mfa_enabled) {
throw new error.AuthError('MFA is not enabled for this user.');
}
const verified = speakeasy.totp.verify({
secret: auth.mfa_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
throw new error.AuthError('Invalid MFA token.');
}
return true;
});
},
isMfaEnabledForUser: (userId) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth) {
throw new error.AuthError('User not found.');
}
return auth.mfa_enabled === 1;
});
},
createMfaSecretForUser: (userId) => {
const secret = speakeasy.generateSecret({ length: 20 });
console.log(secret);
return authModel
.query()
.where('user_id', userId)
.update({
mfa_secret: secret.base32
})
.then(() => secret);
},
enableMfaForUser: (userId, token) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth || !auth.mfa_secret) {
throw new error.AuthError('MFA is not set up for this user.');
}
const verified = speakeasy.totp.verify({
secret: auth.mfa_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
throw new error.AuthError('Invalid MFA token.');
}
return authModel
.query()
.where('user_id', userId)
.update({ mfa_enabled: 1 })
.then(() => true);
});
},
};

View File

@@ -1,15 +1,13 @@
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');
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');
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,8 +298,8 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
.orderBy('incoming_port', 'ASC');
.allowGraph('[owner]')
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
@@ -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

@@ -4,6 +4,7 @@ const userModel = require('../models/user');
const authModel = require('../models/auth');
const helpers = require('../lib/helpers');
const TokenModel = require('../models/token');
const mfa = require('../internal/mfa'); // <-- added MFA import
const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
@@ -21,6 +22,8 @@ module.exports = {
getTokenFromEmail: (data, issuer) => {
let Token = new TokenModel();
console.log(data);
data.scope = data.scope || 'user';
data.expiry = data.expiry || '1d';
@@ -41,34 +44,66 @@ module.exports = {
.then((auth) => {
if (auth) {
return auth.verifyPassword(data.secret)
.then((valid) => {
.then(async (valid) => {
if (valid) {
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
return await mfa.isMfaEnabledForUser(user.id)
.then((mfaEnabled) => {
if (mfaEnabled) {
if (!data.mfa_token) {
throw new error.AuthError('MFA token required');
}
console.log(data.mfa_token);
return mfa.validateMfaTokenForUser(user.id, data.mfa_token)
.then((mfaValid) => {
if (!mfaValid) {
throw new error.AuthError('Invalid MFA token');
}
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
});
} else {
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
}
});
} else {
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);

View File

@@ -507,7 +507,8 @@ const internalUser = {
.then((user) => {
return internalToken.getTokenFromUser(user);
});
}
},
};
module.exports = internalUser;

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,45 @@
const migrate_name = 'identifier_for_migrate';
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.alterTable('auth', (table) => {
table.string('mfa_secret');
table.boolean('mfa_enabled').defaultTo(false);
})
.then(() => {
logger.info('[' + migrate_name + '] User Table altered');
logger.info('[' + migrate_name + '] Migrating Up Complete');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema.alterTable('auth', (table) => {
table.dropColumn('mfa_key');
table.dropColumn('mfa_enabled');
})
.then(() => {
logger.info('[' + migrate_name + '] User Table altered');
logger.info('[' + migrate_name + '] Migrating Down Complete');
});
};

View File

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

View File

@@ -1,15 +1,16 @@
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');
// 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');
Model.knex(db);
const boolFields = [
'is_deleted',
'enabled',
'tcp_forwarding',
'udp_forwarding',
];
@@ -63,17 +64,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

@@ -23,8 +23,10 @@
"node-rsa": "^1.0.8",
"objection": "3.0.1",
"path": "^0.12.7",
"qrcode": "^1.5.4",
"pg": "^8.13.1",
"signale": "1.4.0",
"speakeasy": "^2.0.0",
"sqlite3": "5.1.6",
"temp-write": "^4.0.0"
},

View File

@@ -27,6 +27,7 @@ router.get('/', (req, res/*, next*/) => {
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/mfa', require('./mfa'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));

89
backend/routes/mfa.js Normal file
View File

@@ -0,0 +1,89 @@
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const internalToken = require('../internal/token');
const schema = require('../schema');
const internalMfa = require('../internal/mfa');
const qrcode = require('qrcode');
const speakeasy = require('speakeasy');
const userModel = require('../models/user');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((_, res) => {
res.sendStatus(204);
})
.get(async (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
router
.route('/create')
.post(jwtdecode(), (req, res, next) => {
if (!res.locals.access) {
return next(new Error('Invalid token'));
}
const userId = res.locals.access.token.getUserId();
internalMfa.createMfaSecretForUser(userId)
.then((secret) => {
return userModel.query()
.where('id', '=', userId)
.first()
.then((user) => {
if (!user) {
return next(new Error('User not found'));
}
return { secret, user };
});
})
.then(({ secret, user }) => {
const otpAuthUrl = speakeasy.otpauthURL({
secret: secret.ascii,
label: user.email,
issuer: 'Nginx Proxy Manager'
});
qrcode.toDataURL(otpAuthUrl, (err, dataUrl) => {
if (err) {
console.error('Error generating QR code:', err);
return next(err);
}
res.status(200).send({ qrCode: dataUrl });
});
})
.catch(next);
});
router
.route('/enable')
.post(jwtdecode(), (req, res, next) => {
apiValidator(schema.getValidationSchema('/mfa', 'post'), req.body).then((params) => {
internalMfa.enableMfaForUser(res.locals.access.token.getUserId(), params.token)
.then(() => res.status(200).send({ success: true }))
.catch(next);
}
);});
router
.route('/check')
.get(jwtdecode(), (req, res, next) => {
internalMfa.isMfaEnabledForUser(res.locals.access.token.getUserId())
.then((active) => res.status(200).send({ active }))
.catch(next);
});
module.exports = router;

View File

@@ -181,7 +181,7 @@ router
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
res.status(200)
res.status(201)
.send(result);
})
.catch(next);
@@ -212,7 +212,7 @@ router
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
res.status(200)
res.status(201)
.send(result);
})
.catch(next);
@@ -238,7 +238,7 @@ router
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
res.status(200)
res.status(201)
.send(result);
})
.catch(next);

View File

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

View File

@@ -1,7 +1,7 @@
{
"type": "array",
"description": "Streams list",
"description": "Proxy Hosts list",
"items": {
"$ref": "./stream-object.json"
"$ref": "./proxy-host-object.json"
}
}

View File

@@ -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,8 @@
"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,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

@@ -0,0 +1,41 @@
{
"operationId": "enableMfa",
"summary": "Enable multi-factor authentication for a user",
"tags": ["MFA"],
"requestBody": {
"description": "MFA Token Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"token": {
"type": "string",
"minLength": 1
}
},
"required": ["token"]
}
}
}
},
"responses": {
"200": {
"description": "MFA enabled successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
}
}
}

View File

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

View File

@@ -32,9 +32,6 @@
"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"
}
@@ -76,8 +73,7 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"certificate_id": 0
}
}
}
},

View File

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

View File

@@ -29,26 +29,56 @@
"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"
}
}
}
@@ -64,32 +94,42 @@
"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,
"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

@@ -22,6 +22,10 @@
"secret": {
"minLength": 1,
"type": "string"
},
"mfa_token": {
"minLength": 1,
"type": "string"
}
},
"required": ["identity", "secret"],
@@ -38,7 +42,7 @@
"default": {
"value": {
"result": {
"expires": "2025-02-04T20:40:46.340Z",
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}

View File

@@ -14,7 +14,7 @@
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"additionalProperties": true,
"required": ["name", "nickname", "email"],
"properties": {
"name": {

View File

@@ -9,21 +9,17 @@
"url": "http://127.0.0.1:81/api"
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": {
"/": {
"get": {
"$ref": "./paths/get.json"
}
},
"/mfa": {
"post": {
"$ref": "./paths/mfa/post.json"
}
},
"/audit-log": {
"get": {
"$ref": "./paths/audit-log/get.json"

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

@@ -5,10 +5,12 @@
{% 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 }};
{% if ipv6 -%}
listen [::]:{{ incoming_port }};
{% else -%}
#listen [::]:{{ incoming_port }};
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
@@ -17,12 +19,14 @@ 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;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
{% if ipv6 -%}
listen [::]:{{ incoming_port }} udp;
{% else -%}
#listen [::]:{{ incoming_port }} udp;
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@ services:
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npmpass'
MARIADB_AUTO_UPGRADE: '1'
volumes:
- mysql_vol:/var/lib/mysql
networks:

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

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

@@ -23,19 +23,6 @@ chown -R "$PUID:$PGID" /etc/nginx/nginx
chown -R "$PUID:$PGID" /etc/nginx/nginx.conf
chown -R "$PUID:$PGID" /etc/nginx/conf.d
# Certbot directories - optimized approach
CERT_INIT_FLAG="/opt/certbot/.ownership_initialized"
if [ ! -f "$CERT_INIT_FLAG" ]; then
# Prevents errors when installing python certbot plugins when non-root
chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
# Handle all site-packages directories efficiently
find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do
chown -R "$PUID:$PGID" "$SITE_PACKAGES_DIR"
done
# Create a flag file to skip this step on subsequent runs
touch "$CERT_INIT_FLAG"
chown "$PUID:$PGID" "$CERT_INIT_FLAG"
fi
# Prevents errors when installing python certbot plugins when non-root
chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
find /opt/certbot/lib/python*/site-packages -not -user "$PUID" -execdir chown "$PUID:$PGID" {} \+

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

@@ -161,14 +161,6 @@ The easy fix is to add a Docker environment variable to the Nginx Proxy Manager
DISABLE_IPV6: 'true'
```
## Disabling IP Ranges Fetch
By default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled:
```yml
environment:
IP_RANGES_FETCH_ENABLED: 'false'
```
## Custom Nginx Configurations

View File

@@ -21,7 +21,7 @@ services:
# Add any other Stream port you want to expose
# - '21:21' # FTP
#environment:
environment:
# Uncomment this if you want to change the location of
# the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite"

View File

@@ -1065,9 +1065,9 @@ vfile@^6.0.0:
vfile-message "^4.0.0"
vite@^5.4.8:
version "5.4.19"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
version "5.4.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"

View File

@@ -202,8 +202,47 @@ module.exports = {
return fetch('get', '');
},
Mfa: {
create: function () {
return fetch('post', 'mfa/create');
},
enable: function (token) {
return fetch('post', 'mfa/enable', {token: token});
},
check: function () {
return fetch('get', 'mfa/check');
}
},
Tokens: {
/**
*
* @param {String} identity
* @param {String} secret
* @param {String} token
* @param {Boolean} wipe
* @returns {Promise}
*/
loginWithMFA: function (identity, secret, mfaToken, wipe) {
return fetch('post', 'tokens', {identity: identity, secret: secret, mfa_token: mfaToken})
.then(response => {
if (response.token) {
if (wipe) {
Tokens.clearTokens();
}
// Set storage token
Tokens.addToken(response.token);
return response.token;
} else {
Tokens.clearTokens();
throw(new Error('No token returned'));
}
});
},
/**
* @param {String} identity
* @param {String} secret

View File

@@ -4,438 +4,444 @@ 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 () {
const 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 () {
let 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 () {
const controller = this;
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
});
},
/**
* Dashboard
*/
showDashboard: function () {
let controller = this;
/**
* Nginx Proxy Hosts
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
const controller = this;
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
});
},
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Proxy Hosts
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
let controller = this;
/**
* 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}));
});
}
},
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
App.UI.showAppContent(new View());
});
}
},
/**
* 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 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 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());
});
}
},
/**
* 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 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 Redirection Hosts
*/
showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
let controller = this;
/**
* 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}));
});
}
},
require(['./main', './nginx/redirection/main'], (App, View) => {
controller.navigate('/nginx/redirection');
App.UI.showAppContent(new View());
});
}
},
/**
* 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());
});
}
},
/**
* 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}));
});
}
},
/**
* 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}));
});
}
},
/**
* 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 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}));
});
}
},
/**
* Nginx Stream Hosts
*/
showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
let controller = this;
/**
* 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());
});
}
},
require(['./main', './nginx/stream/main'], (App, View) => {
controller.navigate('/nginx/stream');
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}));
});
}
},
/**
* 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 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}));
});
}
},
/**
* 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}));
});
}
},
/**
* 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 Dead Hosts
*/
showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
let controller = this;
/**
* 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());
});
}
},
require(['./main', './nginx/dead/main'], (App, View) => {
controller.navigate('/nginx/404');
App.UI.showAppContent(new View());
});
}
},
/**
* 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 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}));
});
}
},
/**
* 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}));
});
}
},
/**
* 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 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());
});
}
},
/**
* 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 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}));
});
}
},
/**
* Nginx Access
*/
showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
let controller = this;
/**
* 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}));
});
}
},
require(['./main', './nginx/access/main'], (App, View) => {
controller.navigate('/nginx/access');
App.UI.showAppContent(new View());
});
}
},
/**
* 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}));
});
}
},
/**
* 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 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}));
});
}
},
/**
* 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}));
});
}
},
/**
* 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();
}
},
/**
* Nginx Certificates
*/
showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
let controller = this;
/**
* 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}));
});
}
},
require(['./main', './nginx/certificates/main'], (App, View) => {
controller.navigate('/nginx/certificates');
App.UI.showAppContent(new View());
});
}
},
/**
* 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();
}
},
/**
* 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 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 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}));
});
}
},
/**
* Logout
*/
logout: function () {
Tokens.dropTopToken();
this.showLogin();
}
/**
* 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();
}
};

View File

@@ -6,85 +6,87 @@ 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 () {
const view = this;
templateContext: function () {
let 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 () {
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);
});
}
},
onRender: function () {
let view = this;
/**
* @param {Object} [model]
*/
preRender: function (model) {
this.columns = 0;
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);
});
}
},
// calculate the available columns based on permissions for the objects
// and store as a variable
const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
/**
* @param {Object} [model]
*/
preRender: function (model) {
this.columns = 0;
perms.map(perm => {
this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
});
// 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'];
// Prevent double rendering on initial calls
if (typeof model !== 'undefined') {
this.render();
}
},
perms.map(perm => {
this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
});
initialize: function () {
this.preRender();
this.listenTo(Cache.User, 'change', this.preRender);
}
// Prevent double rendering on initial calls
if (typeof model !== 'undefined') {
this.render();
}
},
initialize: function () {
this.preRender();
this.listenTo(Cache.User, 'change', this.preRender);
}
});

View File

@@ -3,187 +3,48 @@
<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">
<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 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>
<!-- 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 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>
</form>
</div>

View File

@@ -1,38 +1,24 @@
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');
const Mn = require('backbone.marionette');
const App = require('../../main');
const StreamModel = require('../../../models/stream');
const template = require('./form.ejs');
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',
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'
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'
},
events: {
@@ -62,35 +48,6 @@ 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;
@@ -113,108 +70,10 @@ 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'));
}
});
}
},
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

@@ -25,6 +25,16 @@
<div class="invalid-feedback secret-error"></div>
</div>
</div>
<div class="col-sm-12 col-md-12">
<button type="button" class="btn btn-info add-mfa">Add MFA</button>
<p class="qr-instructions" style="display: none;">Scan this QR code in your authenticator app to set up MFA and then enter the current MFA code in the input field.</p>
<div class="mfa-validation-container" style="display: none;">
<label class="form-label"><%- i18n('str', 'mfa') %> <span class="form-required">*</span></label>
<input name="mfa_validation" type="text" class="form-control" placeholder="000000" value="" required>
</div>
</div>
<% if (isAdmin() && !isSelf()) { %>
<div class="col-sm-12 col-md-12">
<div class="form-label"><%- i18n('roles', 'title') %></div>

View File

@@ -14,7 +14,10 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
error: '.secret-error'
error: '.secret-error',
addMfa: '.add-mfa',
mfaValidation: '.mfa-validation-container', // added binding
qrInstructions: '.qr-instructions' // added binding for instructions
},
events: {
@@ -25,6 +28,10 @@ module.exports = Mn.View.extend({
let view = this;
let data = this.ui.form.serializeJSON();
// Save "mfa_validation" value and remove it from data
let mfaToken = data.mfa_validation;
delete data.mfa_validation;
let show_password = this.model.get('email') === 'admin@example.com';
// admin@example.com is not allowed
@@ -62,6 +69,15 @@ module.exports = Mn.View.extend({
}
view.model.set(result);
if (mfaToken) {
return App.Api.Mfa.enable(mfaToken)
.then(() => result);
}
console.log(result);
return result;
})
.then(result => {
App.UI.closeModal(function () {
if (method === App.Api.Users.create) {
// Show permissions dialog immediately
@@ -75,6 +91,18 @@ module.exports = Mn.View.extend({
this.ui.error.text(err.message).show();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
},
'click @ui.addMfa': function (e) {
let view = this;
App.Api.Mfa.create()
.then(response => {
view.ui.addMfa.replaceWith(`<img class="qr-code" src="${response.qrCode}" alt="QR Code">`);
view.ui.qrInstructions.show();
view.ui.mfaValidation.show();
})
.catch(err => {
view.ui.error.text(err.message).show();
});
}
},
@@ -104,5 +132,24 @@ module.exports = Mn.View.extend({
if (typeof options.model === 'undefined' || !options.model) {
this.model = new UserModel.Model();
}
},
onRender: function () {
let view = this;
App.Api.Mfa.check()
.then(response => {
if (response.active) {
view.ui.addMfa.hide();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
} else {
view.ui.addMfa.show();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
}
})
.catch(err => {
view.ui.error.text(err.message).show();
});
}
});

View File

@@ -2,6 +2,7 @@
"en": {
"str": {
"email-address": "Email address",
"mfa": "Multi factor authentication token",
"username": "Username",
"password": "Password",
"sign-in": "Sign in",
@@ -38,7 +39,8 @@
"any": "Any"
},
"login": {
"title": "Login to your account"
"title": "Login to your account",
"mfa_required_text": "Please enter your MFA token to continue"
},
"main": {
"app": "Nginx Proxy Manager",
@@ -60,7 +62,7 @@
},
"footer": {
"fork-me": "Fork me on Github",
"copy": "&copy; 2025 <a href=\"{url}\" target=\"_blank\">jc21.com</a>.",
"copy": "&copy; 2024 <a href=\"{url}\" target=\"_blank\">jc21.com</a>.",
"theme": "Theme by <a href=\"{url}\" target=\"_blank\">Tabler</a>"
},
"dashboard": {
@@ -179,9 +181,7 @@
"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…",
"ssl-certificate": "SSL Certificate for TCP Forwarding",
"tcp+ssl": "TCP+SSL"
"search": "Search Incoming Port…"
},
"certificates": {
"title": "SSL Certificates",

View File

@@ -1,3 +1,4 @@
<div class="container">
<div class="row">
<div class="col col-login mx-auto">
@@ -24,6 +25,12 @@
<input name="secret" type="password" class="form-control" placeholder="<%- i18n('str', 'password') %>" required>
<div class="invalid-feedback secret-error"></div>
</div>
<div class="form-group mfa-group" style="display: none;">
<p class="mfa-info"><%- i18n('login', 'mfa_required_text') %>:</p>
<label class="form-label"><%- i18n('str', 'mfa') %></label>
<input name="mfa_token" type="text" class="form-control" placeholder="<%- i18n('str', 'mfa') %>">
<div class="invalid-feedback mfa-error"></div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
</div>

View File

@@ -13,7 +13,11 @@ module.exports = Mn.View.extend({
identity: 'input[name="identity"]',
secret: 'input[name="secret"]',
error: '.secret-error',
button: 'button'
error_mfa:'.mfa-error',
button: 'button',
mfaGroup: '.mfa-group', // added MFA group selector
mfaToken: 'input[name="mfa_token"]', // added MFA token input
mfaInfo: '.mfa-info' // added MFA info element
},
events: {
@@ -22,14 +26,36 @@ module.exports = Mn.View.extend({
this.ui.button.addClass('btn-loading').prop('disabled', true);
this.ui.error.hide();
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
if(this.ui.mfaToken.val()) {
Api.Tokens.loginWithMFA(this.ui.identity.val(), this.ui.secret.val(), this.ui.mfaToken.val(), true)
.then(() => {
window.location = '/';
})
.catch(err => {
this.ui.error.text(err.message).show();
if (err.message === 'Invalid MFA token.') {
this.ui.error_mfa.text(err.message).show();
} else {
this.ui.error.text(err.message).show();
}
this.ui.button.removeClass('btn-loading').prop('disabled', false);
});
} else {
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
.then(() => {
window.location = '/';
})
.catch(err => {
if (err.message === 'MFA token required') {
this.ui.mfaGroup.show();
this.ui.mfaInfo.show();
} else {
this.ui.error.text(err.message).show();
}
this.ui.button.removeClass('btn-loading').prop('disabled', false);
});
}
}
},
@@ -40,3 +66,5 @@ module.exports = Mn.View.extend({
}
}
});

View File

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

View File

@@ -167,5 +167,4 @@ $pink: #f66d9b;
textarea.form-control.text-monospace {
font-size: 12px;
font-family: monospace;
}

View File

@@ -907,13 +907,6 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
dependencies:
possible-typed-array-names "^1.0.0"
babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -1797,32 +1790,6 @@ cacheable-request@^6.0.0:
normalize-url "^4.1.0"
responselike "^1.0.2"
call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
call-bind@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
dependencies:
call-bind-apply-helpers "^1.0.0"
es-define-property "^1.0.0"
get-intrinsic "^1.2.4"
set-function-length "^1.2.2"
call-bound@^1.0.3, call-bound@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -2265,7 +2232,7 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.5.3"
create-hash@^1.1.0, create-hash@^1.2.0:
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
@@ -2276,17 +2243,7 @@ create-hash@^1.1.0, create-hash@^1.2.0:
ripemd160 "^2.0.1"
sha.js "^2.4.0"
create-hash@~1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
integrity sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==
dependencies:
cipher-base "^1.0.1"
inherits "^2.0.1"
ripemd160 "^2.0.0"
sha.js "^2.4.0"
create-hmac@^1.1.0, create-hmac@^1.1.7:
create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
@@ -2457,15 +2414,6 @@ defer-to-connect@^1.0.1:
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -2631,15 +2579,6 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -2709,9 +2648,9 @@ electron-to-chromium@^1.3.47:
integrity sha512-67V62Z4CFOiAtox+o+tosGfVk0QX4DJgH609tjT8QymbJZVAI/jWnAthnr8c5hnRNziIRwkc9EMQYejiVz3/9Q==
elliptic@^6.5.3, elliptic@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06"
integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==
version "6.6.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.0.tgz#5919ec723286c1edf28685aa89261d4761afa210"
integrity sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
@@ -2823,23 +2762,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-define-property@^1.0.0, es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -3209,13 +3131,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
for-each@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
dependencies:
is-callable "^1.2.7"
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -3276,11 +3191,6 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
@@ -3317,30 +3227,6 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.2.4, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -3507,11 +3393,6 @@ globule@^1.0.0:
lodash "~4.17.10"
minimatch "~3.0.2"
gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
got@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -3561,30 +3442,11 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-symbols@^1.0.0, has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -3633,13 +3495,6 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hash-base@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
integrity sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==
dependencies:
inherits "^2.0.1"
hash-base@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
@@ -3657,13 +3512,6 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
he@1.2.x, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -4008,11 +3856,6 @@ is-callable@^1.1.4, is-callable@^1.2.0:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
is-callable@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@@ -4176,13 +4019,6 @@ is-symbol@^1.0.2:
dependencies:
has-symbols "^1.0.1"
is-typed-array@^1.1.14:
version "1.1.15"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b"
integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
dependencies:
which-typed-array "^1.1.16"
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -4208,11 +4044,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4605,11 +4436,6 @@ marionette.templatecache@^1.0.0:
dependencies:
backbone.marionette "^4.0.0, 4.0.0-beta.1"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -5463,16 +5289,15 @@ path-type@^4.0.0:
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pbkdf2@^3.0.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.3.tgz#8be674d591d65658113424592a95d1517318dd4b"
integrity sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==
version "3.1.1"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
dependencies:
create-hash "~1.1.3"
create-hmac "^1.1.7"
ripemd160 "=2.0.1"
safe-buffer "^5.2.1"
sha.js "^2.4.11"
to-buffer "^1.2.0"
create-hash "^1.1.2"
create-hmac "^1.1.4"
ripemd160 "^2.0.1"
safe-buffer "^5.0.1"
sha.js "^2.4.8"
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
@@ -5501,11 +5326,6 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss-modules-extract-imports@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
@@ -6066,14 +5886,6 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
ripemd160@=2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==
dependencies:
hash-base "^2.0.0"
inherits "^2.0.1"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -6225,18 +6037,6 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@@ -6252,7 +6052,7 @@ setimmediate@^1.0.4:
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
sha.js@^2.4.0, sha.js@^2.4.8:
version "2.4.11"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
@@ -6792,15 +6592,6 @@ to-arraybuffer@^1.0.0:
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
to-buffer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.1.tgz#2ce650cdb262e9112a18e65dc29dcb513c8155e0"
integrity sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==
dependencies:
isarray "^2.0.5"
safe-buffer "^5.2.1"
typed-array-buffer "^1.0.3"
to-fast-properties@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
@@ -6907,15 +6698,6 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
typed-array-buffer@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536"
integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==
dependencies:
call-bound "^1.0.3"
es-errors "^1.3.0"
is-typed-array "^1.1.14"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -7242,19 +7024,6 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-typed-array@^1.1.16:
version "1.1.19"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956"
integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
dependencies:
available-typed-arrays "^1.0.7"
call-bind "^1.0.8"
call-bound "^1.0.4"
for-each "^0.3.5"
get-proto "^1.0.1"
gopd "^1.2.0"
has-tostringtag "^1.0.2"
which@^1.2.14, which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View File

@@ -10,9 +10,9 @@
"active24": {
"name": "Active24",
"package_name": "certbot-dns-active24",
"version": "~=2.0.0",
"version": "~=1.5.1",
"dependencies": "",
"credentials": "dns_active24_api_key = <identifier>\ndns_active24_secret = <secret>",
"credentials": "dns_active24_token=\"TOKEN\"",
"full_plugin_name": "dns-active24"
},
"aliyun": {
@@ -31,14 +31,6 @@
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
"full_plugin_name": "dns-azure"
},
"baidu": {
"name": "baidu",
"package_name": "certbot-dns-baidu",
"version": "~=0.1.1",
"dependencies": "",
"credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef",
"full_plugin_name": "dns-baidu"
},
"beget": {
"name":"Beget",
"package_name": "certbot-beget-plugin",
@@ -55,20 +47,12 @@
"credentials": "# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
"full_plugin_name": "dns-bunny"
},
"cdmon": {
"name": "cdmon",
"package_name": "certbot-dns-cdmon",
"version": "~=0.4.1",
"dependencies": "",
"credentials": "dns_cdmon_api_key=your-cdmon-api-token\ndns_cdmon_domain=your_domain_is_optional",
"full_plugin_name": "dns-cdmon"
},
"cloudflare": {
"name": "Cloudflare",
"package_name": "certbot-dns-cloudflare",
"version": "=={{certbot-version}}",
"dependencies": "cloudflare==4.0.* acme=={{certbot-version}}",
"credentials": "# Cloudflare API credentials used by Certbot\ndns_cloudflare_email = cloudflare@example.com\ndns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234",
"dependencies": "cloudflare==2.19.* acme=={{certbot-version}}",
"credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-cloudflare"
},
"cloudns": {
@@ -106,19 +90,11 @@
"cpanel": {
"name": "cPanel",
"package_name": "certbot-dns-cpanel",
"version": "~=0.4.0",
"version": "~=0.2.2",
"dependencies": "",
"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = your_username\ncpanel_password = your_password\ncpanel_token = your_api_token",
"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = user\ncpanel_password = hunter2",
"full_plugin_name": "cpanel"
},
"ddnss": {
"name": "DDNSS",
"package_name": "certbot-dns-ddnss",
"version": "~=1.1.0",
"dependencies": "",
"credentials": "dns_ddnss_token = YOUR_DDNSS_API_TOKEN",
"full_plugin_name": "dns-ddnss"
},
"desec": {
"name": "deSEC",
"package_name": "certbot-dns-desec",
@@ -185,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_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-domainoffensive"
"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-do"
},
"domeneshop": {
"name": "Domeneshop",
@@ -223,14 +199,6 @@
"credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy",
"full_plugin_name": "dns-eurodns"
},
"firstdomains": {
"name": "First Domains",
"package_name": "certbot-dns-firstdomains",
"version": ">=1.0",
"dependencies": "",
"credentials": "dns_firstdomains_username = myremoteuser\ndns_firstdomains_password = verysecureremoteuserpassword",
"full_plugin_name": "dns-firstdomains"
},
"freedns": {
"name": "FreeDNS",
"package_name": "certbot-dns-freedns",
@@ -241,8 +209,8 @@
},
"gandi": {
"name": "Gandi Live DNS",
"package_name": "certbot-dns-gandi",
"version": "~=1.6.1",
"package_name": "certbot_plugin_gandi",
"version": "~=1.5.0",
"dependencies": "",
"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
"full_plugin_name": "dns-gandi"
@@ -396,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": {
@@ -415,14 +383,6 @@
"credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123",
"full_plugin_name": "dns-netcup"
},
"nicru": {
"name": "nic.ru",
"package_name": "certbot-dns-nicru",
"version": "~=1.0.3",
"dependencies": "",
"credentials": "dns_nicru_client_id = application-id\ndns_nicru_client_secret = application-token\ndns_nicru_username = 0001110/NIC-D\ndns_nicru_password = password\ndns_nicru_scope = .+:.+/zones/example.com(/.+)?\ndns_nicru_service = DNS_SERVICE_NAME\ndns_nicru_zone = example.com",
"full_plugin_name": "dns-nicru"
},
"njalla": {
"name": "Njalla",
"package_name": "certbot-dns-njalla",
@@ -511,30 +471,14 @@
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"full_plugin_name": "dns-route53"
},
"spaceship": {
"name": "Spaceship",
"package_name": "certbot-dns-spaceship",
"version": "~=1.0.4",
"dependencies": "",
"credentials": "[spaceship]\napi_key=your_api_key\napi_secret=your_api_secret",
"full_plugin_name": "dns-spaceship"
},
"strato": {
"name": "Strato",
"package_name": "certbot-dns-strato",
"version": "~=0.2.2",
"version": "~=0.2.1",
"dependencies": "",
"credentials": "dns_strato_username = user\ndns_strato_password = pass\n# uncomment if youre using two factor authentication:\n# dns_strato_totp_devicename = 2fa_device\n# dns_strato_totp_secret = 2fa_secret\n#\n# uncomment if domain name contains special characters\n# insert domain display name as seen on your account page here\n# dns_strato_domain_display_name = my-punicode-url.de\n#\n# if youre not using strato.de or another special endpoint you can customise it below\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\n# dns_strato_custom_api_scheme = https\n# dns_strato_custom_api_host = www.strato.de\n# dns_strato_custom_api_port = 443\n# dns_strato_custom_api_path = \"/apps/CustomerService\"",
"full_plugin_name": "dns-strato"
},
"selectelv2": {
"name": "Selectel api v2",
"package_name": "certbot-dns-selectel-api-v2",
"version": "~=0.3.0",
"dependencies": "",
"credentials": "dns_selectel_api_v2_account_id = your_account_id\ndns_selectel_api_v2_project_name = your_project\ndns_selectel_api_v2_username = your_username\ndns_selectel_api_v2_password = your_password",
"full_plugin_name": "dns-selectel-api-v2"
},
"timeweb": {
"name": "Timeweb Cloud",
"package_name": "certbot-dns-timeweb",
@@ -590,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

@@ -1,25 +0,0 @@
/// <reference types="cypress" />
describe('Dashboard endpoints', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to get host counts', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/reports/hosts'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/reports/hosts', data);
expect(data).to.have.property('dead');
expect(data).to.have.property('proxy');
expect(data).to.have.property('redirection');
expect(data).to.have.property('stream');
});
});
});

View File

@@ -1,225 +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);
}
});
});
});
});
});
});
it('Should be able to List Streams', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/nginx/streams?expand=owner,certificate',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/nginx/streams', data);
expect(data.length).to.be.greaterThan(0);
expect(data[0]).to.have.property('id');
expect(data[0]).to.have.property('enabled');
});
});
});

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": {

File diff suppressed because it is too large Load Diff