Compare commits

..

30 Commits

Author SHA1 Message Date
milad nazari
074a07cea9 Merge 3856b6b03d into 498109addb 2025-01-28 17:22:57 +03:30
Milad Nazari
3856b6b03d remove default server from certificate object 2025-01-08 10:33:21 +03:30
Milad Nazari
f34cb59711 Revert "remove unesessary default values"
This reverts commit 408eab842d.
2025-01-04 21:16:32 +03:30
Milad Nazari
c135880625 Revert "remove default_server from certificate object"
This reverts commit 101afa0013.
2025-01-04 17:56:14 +03:30
Milad Nazari
408eab842d remove unesessary default values 2025-01-04 12:15:14 +03:30
Milad Nazari
101afa0013 remove default_server from certificate object 2025-01-03 18:43:07 +03:30
Milad Nazari
2cab405190 Merge branch 'fix-bugs' into develop
- show ssl key type select box just for create new one
- fix migration names and combine them
- make ssl key type optional
2025-01-02 00:17:44 +03:30
Milad Nazari
d3a5fac51b make ssl_key_type optional 2025-01-01 23:47:32 +03:30
Milad Nazari
a121cb124e remove unnecessary whitespace 2025-01-01 23:41:40 +03:30
Milad Nazari
65f971fd36 add migration names and combine ssl key migrations 2025-01-01 23:26:28 +03:30
Milad Nazari
ad36fb5c2d show select ssl key type just for create new ssl 2025-01-01 22:42:19 +03:30
milad nazari
c6d884dab6 fix indent 2024-12-22 17:29:23 +03:30
milad nazari
5dc78df0bb fix messages indent: convert to space 2024-12-22 11:12:35 +03:30
milad nazari
04636b71a9 add feature: set default server 2024-12-22 01:49:05 +03:30
milad nazari
1353937c36 fix copy address 2024-12-21 21:12:24 +03:30
milad nazari
f68c1b7c29 add Diffie-Hellman Parameters to cipher suites 2024-12-21 21:05:09 +03:30
milad nazari
32e0784865 support more cipher suites 2024-12-21 20:20:54 +03:30
milad nazari
f386f6b640 remove elliptic-curve 2024-12-14 01:40:01 +03:30
milad nazari
5ba7363c9e fix ssl cipher bug 2024-12-13 11:30:58 +03:30
milad nazari
2e45444328 change ssl_ciphers for more compatibility 2024-12-12 23:48:51 +03:30
milad nazari
eb5c51a657 add support more cipher suites
this cipher suites need for old iot devices
2024-12-12 20:42:22 +03:30
milad nazari
cb795565ea add ssl_key_type in swagger
fix ci test error
2024-12-12 12:08:03 +03:30
milad nazari
04b3608b4e remove elliptic-curve from certbot command options 2024-12-12 01:49:57 +03:30
milad nazari
111fc287eb Revert "add elliptic-curve"
This reverts commit 95a94a4f8c.
2024-12-12 01:49:19 +03:30
milad nazari
95a94a4f8c add elliptic-curve 2024-12-12 01:15:39 +03:30
milad nazari
5e7b69c396 add update cipher suites 2024-12-12 00:46:14 +03:30
milad nazari
2723de24fd add ssl_ecdh_curve for more compatibility 2024-12-11 23:31:39 +03:30
milad nazari
891877afb6 fix ssl key-type certificate 2024-12-11 11:51:58 +03:30
milad nazari
8e9e033a72 fix indent: tab to space 2024-12-09 11:30:10 +03:30
milad nazari
e6ec74c2f7 feat: add support for selecting SSL key type (ECDSA/RSA)
Added the ability to specify the SSL key type (ECDSA or RSA) for each site in the Nginx Proxy Manager. This enhancement is particularly useful for environments with IoT devices that have limitations with specific key types, such as RSA-only support. The implementation includes:

- Backend support for storing and validating the `ssl_key_type` field.
- Swagger schema updated to validate the new input.
- Frontend update to allow users to select the SSL key type via a dropdown menu.

This feature ensures greater flexibility and compatibility in managing SSL certificates for diverse setups.
2024-12-09 11:27:52 +03:30
95 changed files with 2755 additions and 2944 deletions

View File

@@ -1 +1 @@
2.12.6 2.12.2

15
Jenkinsfile vendored
View File

@@ -241,17 +241,12 @@ pipeline {
} }
steps { steps {
script { script {
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on
``` [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev)
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}`
```
> [!NOTE] **Note:** ensure you backup your NPM instance before testing this image! Especially if there are database changes
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. **Note:** this is a different docker image namespace than the official image
> This is a different docker image namespace than the official image.
> [!WARNING]
> Changes and additions to DNS Providers require verification by at least 2 members of the community!
""", true) """, true)
} }
} }

View File

@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img src="https://nginxproxymanager.com/github.png"> <img src="https://nginxproxymanager.com/github.png">
<br><br> <br><br>
<img src="https://img.shields.io/badge/version-2.12.6-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"> <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"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>

View File

@@ -3,8 +3,6 @@
const schema = require('./schema'); const schema = require('./schema');
const logger = require('./logger').global; const logger = require('./logger').global;
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false';
async function appStart () { async function appStart () {
const migrate = require('./migrate'); const migrate = require('./migrate');
const setup = require('./setup'); const setup = require('./setup');
@@ -15,16 +13,7 @@ async function appStart () {
return migrate.latest() return migrate.latest()
.then(setup) .then(setup)
.then(schema.getCompiledSchema) .then(schema.getCompiledSchema)
.then(() => { .then(internalIpRanges.fetch)
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(() => { .then(() => {
internalCertificate.initTimer(); internalCertificate.initTimer();
internalIpRanges.initTimer(); internalIpRanges.initTimer();

View File

@@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('node:fs'); const fs = require('fs');
const batchflow = require('batchflow'); const batchflow = require('batchflow');
const logger = require('../logger').access; const logger = require('../logger').access;
const error = require('../lib/error'); const error = require('../lib/error');
@@ -38,7 +38,7 @@ const internalAccessList = {
.then((row) => { .then((row) => {
data.id = row.id; data.id = row.id;
const promises = []; let promises = [];
// Now add the items // Now add the items
data.items.map((item) => { data.items.map((item) => {
@@ -116,7 +116,7 @@ const internalAccessList = {
.then((row) => { .then((row) => {
if (row.id !== data.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new error.InternalValidationError(`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`); throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
} }
}) })
.then(() => { .then(() => {
@@ -135,10 +135,10 @@ const internalAccessList = {
.then(() => { .then(() => {
// Check for items and add/update/remove them // Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) { if (typeof data.items !== 'undefined' && data.items) {
const promises = []; let promises = [];
const items_to_keep = []; let items_to_keep = [];
data.items.map((item) => { data.items.map(function (item) {
if (item.password) { if (item.password) {
promises.push(accessListAuthModel promises.push(accessListAuthModel
.query() .query()
@@ -154,7 +154,7 @@ const internalAccessList = {
} }
}); });
const query = accessListAuthModel let query = accessListAuthModel
.query() .query()
.delete() .delete()
.where('access_list_id', data.id); .where('access_list_id', data.id);
@@ -175,9 +175,9 @@ const internalAccessList = {
.then(() => { .then(() => {
// Check for clients and add/update/remove them // Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) { if (typeof data.clients !== 'undefined' && data.clients) {
const promises = []; let promises = [];
data.clients.map((client) => { data.clients.map(function (client) {
if (client.address) { if (client.address) {
promises.push(accessListClientModel promises.push(accessListClientModel
.query() .query()
@@ -190,7 +190,7 @@ const internalAccessList = {
} }
}); });
const query = accessListClientModel let query = accessListClientModel
.query() .query()
.delete() .delete()
.where('access_list_id', data.id); .where('access_list_id', data.id);
@@ -249,7 +249,7 @@ const internalAccessList = {
return access.can('access_lists:get', data.id) return access.can('access_lists:get', data.id)
.then((access_data) => { .then((access_data) => {
const query = accessListModel let query = accessListModel
.query() .query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.leftJoin('proxy_host', function() { .leftJoin('proxy_host', function() {
@@ -267,7 +267,7 @@ const internalAccessList = {
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(', ')}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
@@ -327,7 +327,7 @@ const internalAccessList = {
// 3. reconfigure those hosts, then reload nginx // 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items // set the access_list_id to zero for these items
row.proxy_hosts.map((_val, idx) => { row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0; row.proxy_hosts[idx].access_list_id = 0;
}); });
@@ -340,11 +340,11 @@ const internalAccessList = {
}) })
.then(() => { .then(() => {
// delete the htpasswd file // delete the htpasswd file
const htpasswd_file = internalAccessList.getFilename(row); let htpasswd_file = internalAccessList.getFilename(row);
try { try {
fs.unlinkSync(htpasswd_file); fs.unlinkSync(htpasswd_file);
} catch (_err) { } catch (err) {
// do nothing // do nothing
} }
}) })
@@ -374,7 +374,7 @@ const internalAccessList = {
getAll: (access, expand, search_query) => { getAll: (access, expand, search_query) => {
return access.can('access_lists:list') return access.can('access_lists:list')
.then((access_data) => { .then((access_data) => {
const query = accessListModel let query = accessListModel
.query() .query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.leftJoin('proxy_host', function() { .leftJoin('proxy_host', function() {
@@ -393,19 +393,19 @@ const internalAccessList = {
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string') {
query.where(function () { query.where(function () {
this.where('name', 'like', `%${search_query}%`); this.where('name', 'like', '%' + search_query + '%');
}); });
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched(`[${expand.join(', ')}]`); query.withGraphFetched('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query.then(utils.omitRows(omissions()));
}) })
.then((rows) => { .then((rows) => {
if (rows) { if (rows) {
rows.map((row, idx) => { rows.map(function (row, idx) {
if (typeof row.items !== 'undefined' && row.items) { if (typeof row.items !== 'undefined' && row.items) {
rows[idx] = internalAccessList.maskItems(row); rows[idx] = internalAccessList.maskItems(row);
} }
@@ -424,7 +424,7 @@ const internalAccessList = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = accessListModel let query = accessListModel
.query() .query()
.count('id as count') .count('id as count')
.where('is_deleted', 0); .where('is_deleted', 0);
@@ -445,7 +445,7 @@ const internalAccessList = {
*/ */
maskItems: (list) => { maskItems: (list) => {
if (list && typeof list.items !== 'undefined') { if (list && typeof list.items !== 'undefined') {
list.items.map((val, idx) => { list.items.map(function (val, idx) {
let repeat_for = 8; let repeat_for = 8;
let first_char = '*'; let first_char = '*';
@@ -468,7 +468,7 @@ const internalAccessList = {
* @returns {String} * @returns {String}
*/ */
getFilename: (list) => { getFilename: (list) => {
return `/data/access/${list.id}`; return '/data/access/' + list.id;
}, },
/** /**
@@ -479,15 +479,15 @@ const internalAccessList = {
* @returns {Promise} * @returns {Promise}
*/ */
build: (list) => { build: (list) => {
logger.info(`Building Access file #${list.id} for: ${list.name}`); logger.info('Building Access file #' + list.id + ' for: ' + list.name);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const htpasswd_file = internalAccessList.getFilename(list); let htpasswd_file = internalAccessList.getFilename(list);
// 1. remove any existing access file // 1. remove any existing access file
try { try {
fs.unlinkSync(htpasswd_file); fs.unlinkSync(htpasswd_file);
} catch (_err) { } catch (err) {
// do nothing // do nothing
} }
@@ -504,17 +504,12 @@ const internalAccessList = {
if (list.items.length) { if (list.items.length) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
batchflow(list.items).sequential() batchflow(list.items).sequential()
.each((_i, item, next) => { .each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) { if (typeof item.password !== 'undefined' && item.password.length) {
logger.info(`Adding: ${item.username}`); logger.info('Adding: ' + item.username);
utils.execFile('openssl', ['passwd', '-apr1', item.password]) utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
.then((res) => { .then((/*result*/) => {
try {
fs.appendFileSync(htpasswd_file, `${item.username}:${res}\n`, {encoding: 'utf8'});
} catch (err) {
reject(err);
}
next(); next();
}) })
.catch((err) => { .catch((err) => {
@@ -528,7 +523,7 @@ const internalAccessList = {
reject(err); reject(err);
}) })
.end((results) => { .end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`); logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results); resolve(results);
}); });
}); });

View File

@@ -1,6 +1,6 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('node:fs'); const fs = require('fs');
const https = require('node:https'); const https = require('https');
const tempWrite = require('temp-write'); const tempWrite = require('temp-write');
const moment = require('moment'); const moment = require('moment');
const archiver = require('archiver'); const archiver = require('archiver');
@@ -49,7 +49,7 @@ const internalCertificate = {
processExpiringHosts: () => { processExpiringHosts: () => {
if (!internalCertificate.intervalProcessing) { if (!internalCertificate.intervalProcessing) {
internalCertificate.intervalProcessing = true; internalCertificate.intervalProcessing = true;
logger.info(`Renewing SSL certs expiring within ${internalCertificate.renewBeforeExpirationBy[0]} ${internalCertificate.renewBeforeExpirationBy[1]} ...`); logger.info('Renewing SSL certs expiring within ' + internalCertificate.renewBeforeExpirationBy[0] + ' ' + internalCertificate.renewBeforeExpirationBy[1] + ' ...');
const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss');
@@ -70,7 +70,7 @@ const internalCertificate = {
*/ */
let sequence = Promise.resolve(); let sequence = Promise.resolve();
certificates.forEach((certificate) => { certificates.forEach(function (certificate) {
sequence = sequence.then(() => sequence = sequence.then(() =>
internalCertificate internalCertificate
.renew( .renew(
@@ -202,7 +202,7 @@ const internalCertificate = {
.then(() => { .then(() => {
// At this point, the letsencrypt cert should exist on disk. // At this point, the letsencrypt cert should exist on disk.
// Lets get the expiry date from the file and update the row silently // Lets get the expiry date from the file and update the row silently
return internalCertificate.getCertificateInfoFromFile(`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`) return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem')
.then((cert_info) => { .then((cert_info) => {
return certificateModel return certificateModel
.query() .query()
@@ -263,7 +263,7 @@ const internalCertificate = {
.then((row) => { .then((row) => {
if (row.id !== data.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new error.InternalValidationError(`Certificate could not be updated, IDs do not match: ${row.id} !== ${data.id}`); throw new error.InternalValidationError('Certificate could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
} }
return certificateModel return certificateModel
@@ -308,14 +308,11 @@ const internalCertificate = {
return access.can('certificates:get', data.id) return access.can('certificates:get', data.id)
.then((access_data) => { .then((access_data) => {
const query = certificateModel let query = certificateModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner]') .allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@@ -323,7 +320,7 @@ const internalCertificate = {
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(', ')}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
@@ -354,17 +351,17 @@ const internalCertificate = {
}) })
.then((certificate) => { .then((certificate) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
const zipDirectory = internalCertificate.getLiveCertPath(data.id); const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id;
if (!fs.existsSync(zipDirectory)) { if (!fs.existsSync(zipDirectory)) {
throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`); throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists');
} }
const certFiles = fs.readdirSync(zipDirectory) let certFiles = fs.readdirSync(zipDirectory)
.filter((fn) => fn.endsWith('.pem')) .filter((fn) => fn.endsWith('.pem'))
.map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); .map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
const downloadName = `npm-${data.id}-${Date.now()}.zip`; const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`;
const opName = `/tmp/${downloadName}`; const opName = '/tmp/' + downloadName;
internalCertificate.zipFiles(certFiles, opName) internalCertificate.zipFiles(certFiles, opName)
.then(() => { .then(() => {
logger.debug('zip completed : ', opName); logger.debug('zip completed : ', opName);
@@ -392,7 +389,7 @@ const internalCertificate = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
source source
.map((fl) => { .map((fl) => {
const fileName = path.basename(fl); let fileName = path.basename(fl);
logger.debug(fl, 'added to certificate zip'); logger.debug(fl, 'added to certificate zip');
archive.file(fl, { name: fileName }); archive.file(fl, { name: fileName });
}); });
@@ -462,14 +459,11 @@ const internalCertificate = {
getAll: (access, expand, search_query) => { getAll: (access, expand, search_query) => {
return access.can('certificates:list') return access.can('certificates:list')
.then((access_data) => { .then((access_data) => {
const query = certificateModel let query = certificateModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner]') .allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.orderBy('nice_name', 'ASC'); .orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@@ -479,12 +473,12 @@ const internalCertificate = {
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string') {
query.where(function () { query.where(function () {
this.where('nice_name', 'like', `%${search_query}%`); this.where('nice_name', 'like', '%' + search_query + '%');
}); });
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched(`[${expand.join(', ')}]`); query.withGraphFetched('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query.then(utils.omitRows(omissions()));
@@ -499,7 +493,7 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = certificateModel let query = certificateModel
.query() .query()
.count('id as count') .count('id as count')
.where('is_deleted', 0); .where('is_deleted', 0);
@@ -521,7 +515,7 @@ const internalCertificate = {
writeCustomCert: (certificate) => { writeCustomCert: (certificate) => {
logger.info('Writing Custom Certificate:', certificate); logger.info('Writing Custom Certificate:', certificate);
const dir = `/data/custom_ssl/npm-${certificate.id}`; const dir = '/data/custom_ssl/npm-' + certificate.id;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
@@ -531,7 +525,7 @@ const internalCertificate = {
let certData = certificate.meta.certificate; let certData = certificate.meta.certificate;
if (typeof certificate.meta.intermediate_certificate !== 'undefined') { if (typeof certificate.meta.intermediate_certificate !== 'undefined') {
certData = `${certData}\n${certificate.meta.intermediate_certificate}`; certData = certData + '\n' + certificate.meta.intermediate_certificate;
} }
try { try {
@@ -543,7 +537,7 @@ const internalCertificate = {
return; return;
} }
fs.writeFile(`${dir}/fullchain.pem`, certData, (err) => { fs.writeFile(dir + '/fullchain.pem', certData, function (err) {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -553,7 +547,7 @@ const internalCertificate = {
}) })
.then(() => { .then(() => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => { fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -576,6 +570,7 @@ const internalCertificate = {
return internalCertificate.create(access, { return internalCertificate.create(access, {
provider: 'letsencrypt', provider: 'letsencrypt',
domain_names: data.domain_names, domain_names: data.domain_names,
ssl_key_type: data.ssl_key_type,
meta: data.meta meta: data.meta
}); });
}, },
@@ -591,7 +586,7 @@ const internalCertificate = {
validate: (data) => { validate: (data) => {
return new Promise((resolve) => { return new Promise((resolve) => {
// Put file contents into an object // Put file contents into an object
const files = {}; let files = {};
_.map(data.files, (file, name) => { _.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
files[name] = file.data.toString(); files[name] = file.data.toString();
@@ -603,7 +598,7 @@ const internalCertificate = {
.then((files) => { .then((files) => {
// For each file, create a temp file and write the contents to it // For each file, create a temp file and write the contents to it
// Then test it depending on the file type // Then test it depending on the file type
const promises = []; let promises = [];
_.map(files, (content, type) => { _.map(files, (content, type) => {
promises.push(new Promise((resolve) => { promises.push(new Promise((resolve) => {
if (type === 'certificate_key') { if (type === 'certificate_key') {
@@ -688,11 +683,11 @@ const internalCertificate = {
reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.')); reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.'));
}, 10000); }, 10000);
utils utils
.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `) .exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ')
.then((result) => { .then((result) => {
clearTimeout(failTimeout); clearTimeout(failTimeout);
if (!result.toLowerCase().includes('key is valid')) { if (!result.toLowerCase().includes('key is valid')) {
reject(new error.ValidationError(`Result Validation Error: ${result}`)); reject(new error.ValidationError('Result Validation Error: ' + result));
} }
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
resolve(true); resolve(true);
@@ -700,7 +695,7 @@ const internalCertificate = {
.catch((err) => { .catch((err) => {
clearTimeout(failTimeout); clearTimeout(failTimeout);
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
reject(new error.ValidationError(`Certificate Key is not valid (${err.message})`, err)); reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err));
}); });
}); });
}); });
@@ -735,9 +730,9 @@ const internalCertificate = {
* @param {Boolean} [throw_expired] Throw when the certificate is out of date * @param {Boolean} [throw_expired] Throw when the certificate is out of date
*/ */
getCertificateInfoFromFile: (certificate_file, throw_expired) => { getCertificateInfoFromFile: (certificate_file, throw_expired) => {
const certData = {}; let certData = {};
return utils.execFile('openssl', ['x509', '-in', certificate_file, '-subject', '-noout']) return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
.then((result) => { .then((result) => {
// Examples: // Examples:
// subject=CN = *.jc21.com // subject=CN = *.jc21.com
@@ -745,11 +740,11 @@ const internalCertificate = {
const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
const match = regex.exec(result); const match = regex.exec(result);
if (match && typeof match[1] !== 'undefined') { if (match && typeof match[1] !== 'undefined') {
certData.cn = match[1]; certData['cn'] = match[1];
} }
}) })
.then(() => { .then(() => {
return utils.execFile('openssl', ['x509', '-in', certificate_file, '-issuer', '-noout']); return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
}) })
.then((result) => { .then((result) => {
@@ -760,11 +755,11 @@ const internalCertificate = {
const regex = /^(?:issuer=)?(.*)$/gim; const regex = /^(?:issuer=)?(.*)$/gim;
const match = regex.exec(result); const match = regex.exec(result);
if (match && typeof match[1] !== 'undefined') { if (match && typeof match[1] !== 'undefined') {
certData.issuer = match[1]; certData['issuer'] = match[1];
} }
}) })
.then(() => { .then(() => {
return utils.execFile('openssl', ['x509', '-in', certificate_file, '-dates', '-noout']); return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
}) })
.then((result) => { .then((result) => {
// notBefore=Jul 14 04:04:29 2018 GMT // notBefore=Jul 14 04:04:29 2018 GMT
@@ -773,7 +768,7 @@ const internalCertificate = {
let validTo = null; let validTo = null;
const lines = result.split('\n'); const lines = result.split('\n');
lines.map((str) => { lines.map(function (str) {
const regex = /^(\S+)=(.*)$/gim; const regex = /^(\S+)=(.*)$/gim;
const match = regex.exec(str.trim()); const match = regex.exec(str.trim());
@@ -789,21 +784,21 @@ const internalCertificate = {
}); });
if (!validFrom || !validTo) { if (!validFrom || !validTo) {
throw new error.ValidationError(`Could not determine dates from certificate: ${result}`); throw new error.ValidationError('Could not determine dates from certificate: ' + result);
} }
if (throw_expired && validTo < parseInt(moment().format('X'), 10)) { if (throw_expired && validTo < parseInt(moment().format('X'), 10)) {
throw new error.ValidationError('Certificate has expired'); throw new error.ValidationError('Certificate has expired');
} }
certData.dates = { certData['dates'] = {
from: validFrom, from: validFrom,
to: validTo to: validTo
}; };
return certData; return certData;
}).catch((err) => { }).catch((err) => {
throw new error.ValidationError(`Certificate is not valid (${err.message})`, err); throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err);
}); });
}, },
@@ -814,7 +809,7 @@ const internalCertificate = {
* @param {Boolean} [remove] * @param {Boolean} [remove]
* @returns {Object} * @returns {Object}
*/ */
cleanMeta: (meta, remove) => { cleanMeta: function (meta, remove) {
internalCertificate.allowedSslFiles.map((key) => { internalCertificate.allowedSslFiles.map((key) => {
if (typeof meta[key] !== 'undefined' && meta[key]) { if (typeof meta[key] !== 'undefined' && meta[key]) {
if (remove) { if (remove) {
@@ -834,35 +829,25 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
requestLetsEncryptSsl: (certificate) => { requestLetsEncryptSsl: (certificate) => {
logger.info(`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const args = [ const cmd = `${certbotCommand} certonly ` +
'certonly', `--config '${letsencryptConfig}' ` +
'--config', `--key-type '${certificate.ssl_key_type}' ` +
letsencryptConfig, '--work-dir "/tmp/letsencrypt-lib" ' +
'--work-dir', '--logs-dir "/tmp/letsencrypt-log" ' +
'/tmp/letsencrypt-lib', `--cert-name "npm-${certificate.id}" ` +
'--logs-dir', '--agree-tos ' +
'/tmp/letsencrypt-log', '--authenticator webroot ' +
'--cert-name', `--email '${certificate.meta.letsencrypt_email}' ` +
`npm-${certificate.id}`, '--preferred-challenges "dns,http" ' +
'--agree-tos', `--domains "${certificate.domain_names.join(',')}" ` +
'--authenticator', (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
'webroot', (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
'--email',
certificate.meta.letsencrypt_email,
'--preferred-challenges',
'dns,http',
'--domains',
certificate.domain_names.join(','),
];
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); logger.info('Command:', cmd);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); return utils.exec(cmd)
return utils.execFile(certbotCommand, args, adds.opts)
.then((result) => { .then((result) => {
logger.success(result); logger.success(result);
return result; return result;
@@ -879,48 +864,51 @@ const internalCertificate = {
requestLetsEncryptSslWithDnsChallenge: async (certificate) => { requestLetsEncryptSslWithDnsChallenge: async (certificate) => {
await certbot.installPlugin(certificate.meta.dns_provider); await certbot.installPlugin(certificate.meta.dns_provider);
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
logger.info(`Requesting LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info(`Requesting Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
fs.mkdirSync('/etc/letsencrypt/credentials', { recursive: true }); fs.mkdirSync('/etc/letsencrypt/credentials', { recursive: true });
fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, {mode: 0o600}); fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, {mode: 0o600});
// Whether the plugin has a --<name>-credentials argument // Whether the plugin has a --<name>-credentials argument
const hasConfigArg = certificate.meta.dns_provider !== 'route53'; const hasConfigArg = certificate.meta.dns_provider !== 'route53';
const args = [ let mainCmd = certbotCommand + ' certonly ' +
'certonly', `--config '${letsencryptConfig}' ` +
'--config', `--key-type '${certificate.ssl_key_type}' ` +
letsencryptConfig, '--work-dir "/tmp/letsencrypt-lib" ' +
'--work-dir', '--logs-dir "/tmp/letsencrypt-log" ' +
'/tmp/letsencrypt-lib', `--cert-name 'npm-${certificate.id}' ` +
'--logs-dir', '--agree-tos ' +
'/tmp/letsencrypt-log', `--email '${certificate.meta.letsencrypt_email}' ` +
'--cert-name', `--domains '${certificate.domain_names.join(',')}' ` +
`npm-${certificate.id}`, `--authenticator '${dnsPlugin.full_plugin_name}' ` +
'--agree-tos', (
'--email', hasConfigArg
certificate.meta.letsencrypt_email, ? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' `
'--domains', : ''
certificate.domain_names.join(','), ) +
'--authenticator', (
dnsPlugin.full_plugin_name, certificate.meta.propagation_seconds !== undefined
]; ? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' `
: ''
) +
(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
if (hasConfigArg) { // Prepend the path to the credentials file as an environment variable
args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation); if (certificate.meta.dns_provider === 'route53') {
} mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
if (certificate.meta.propagation_seconds !== undefined) {
args.push(`--${dnsPlugin.full_plugin_name}-propagation-seconds`, certificate.meta.propagation_seconds.toString());
} }
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); if (certificate.meta.dns_provider === 'duckdns') {
args.push(...adds.args); mainCmd = mainCmd + ' --dns-duckdns-no-txt-restore';
}
logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); logger.info('Command:', mainCmd);
try { try {
const result = await utils.execFile(certbotCommand, args, adds.opts); const result = await utils.exec(mainCmd);
logger.info(result); logger.info(result);
return result; return result;
} catch (err) { } catch (err) {
@@ -948,7 +936,7 @@ const internalCertificate = {
return renewMethod(certificate) return renewMethod(certificate)
.then(() => { .then(() => {
return internalCertificate.getCertificateInfoFromFile(`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`); return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem');
}) })
.then((cert_info) => { .then((cert_info) => {
return certificateModel return certificateModel
@@ -980,31 +968,23 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
renewLetsEncryptSsl: (certificate) => { renewLetsEncryptSsl: (certificate) => {
logger.info(`Renewing LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const args = [ const cmd = certbotCommand + ' renew --force-renewal ' +
'renew', `--config '${letsencryptConfig}' ` +
'--force-renewal', `--key-type '${certificate.ssl_key_type}' ` +
'--config', '--work-dir "/tmp/letsencrypt-lib" ' +
letsencryptConfig, '--logs-dir "/tmp/letsencrypt-log" ' +
'--work-dir', `--cert-name 'npm-${certificate.id}' ` +
'/tmp/letsencrypt-lib', '--preferred-challenges "dns,http" ' +
'--logs-dir', '--no-random-sleep-on-renew ' +
'/tmp/letsencrypt-log', '--disable-hook-validation ' +
'--cert-name', (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
`npm-${certificate.id}`, (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
'--preferred-challenges',
'dns,http',
'--no-random-sleep-on-renew',
'--disable-hook-validation',
];
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); logger.info('Command:', cmd);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); return utils.exec(cmd)
return utils.execFile(certbotCommand, args, adds.opts)
.then((result) => { .then((result) => {
logger.info(result); logger.info(result);
return result; return result;
@@ -1022,29 +1002,28 @@ const internalCertificate = {
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
} }
logger.info(`Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
const args = [ let mainCmd = certbotCommand + ' renew --force-renewal ' +
'renew', `--config "${letsencryptConfig}" ` +
'--force-renewal', `--key-type '${certificate.ssl_key_type}' ` +
'--config', '--work-dir "/tmp/letsencrypt-lib" ' +
letsencryptConfig, '--logs-dir "/tmp/letsencrypt-log" ' +
'--work-dir', `--cert-name 'npm-${certificate.id}' ` +
'/tmp/letsencrypt-lib', '--disable-hook-validation ' +
'--logs-dir', '--no-random-sleep-on-renew ' +
'/tmp/letsencrypt-log', (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
'--cert-name', (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
`npm-${certificate.id}`,
'--disable-hook-validation',
'--no-random-sleep-on-renew',
];
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); // Prepend the path to the credentials file as an environment variable
args.push(...adds.args); if (certificate.meta.dns_provider === 'route53') {
const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
}
logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); logger.info('Command:', mainCmd);
return utils.execFile(certbotCommand, args, adds.opts) return utils.exec(mainCmd)
.then(async (result) => { .then(async (result) => {
logger.info(result); logger.info(result);
return result; return result;
@@ -1057,29 +1036,26 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
revokeLetsEncryptSsl: (certificate, throw_errors) => { revokeLetsEncryptSsl: (certificate, throw_errors) => {
logger.info(`Revoking LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const args = [ const mainCmd = certbotCommand + ' revoke ' +
'revoke', `--config '${letsencryptConfig}' ` +
'--config', `--key-type '${certificate.ssl_key_type}' ` +
letsencryptConfig, '--work-dir "/tmp/letsencrypt-lib" ' +
'--work-dir', '--logs-dir "/tmp/letsencrypt-log" ' +
'/tmp/letsencrypt-lib', `--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` +
'--logs-dir', '--delete-after-revoke ' +
'/tmp/letsencrypt-log', (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
'--cert-path', (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
'--delete-after-revoke',
];
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); // Don't fail command if file does not exist
args.push(...adds.args); const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd);
return utils.execFile(certbotCommand, args, adds.opts) return utils.exec(mainCmd)
.then(async (result) => { .then(async (result) => {
await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`); await utils.exec(delete_credentialsCmd);
logger.info(result); logger.info(result);
return result; return result;
}) })
@@ -1097,8 +1073,9 @@ const internalCertificate = {
* @returns {Boolean} * @returns {Boolean}
*/ */
hasLetsEncryptSslCerts: (certificate) => { hasLetsEncryptSslCerts: (certificate) => {
const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id); const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id;
return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`);
return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem');
}, },
/** /**
@@ -1110,7 +1087,7 @@ const internalCertificate = {
*/ */
disableInUseHosts: (in_use_result) => { disableInUseHosts: (in_use_result) => {
if (in_use_result.total_count) { if (in_use_result.total_count) {
const promises = []; let promises = [];
if (in_use_result.proxy_hosts.length) { if (in_use_result.proxy_hosts.length) {
promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts)); promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts));
@@ -1140,7 +1117,7 @@ const internalCertificate = {
*/ */
enableInUseHosts: (in_use_result) => { enableInUseHosts: (in_use_result) => {
if (in_use_result.total_count) { if (in_use_result.total_count) {
const promises = []; let promises = [];
if (in_use_result.proxy_hosts.length) { if (in_use_result.proxy_hosts.length) {
promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts)); promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts));
@@ -1173,12 +1150,12 @@ const internalCertificate = {
// Create a test challenge file // Create a test challenge file
const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge'; const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge';
const testChallengeFile = `${testChallengeDir}/test-challenge`; const testChallengeFile = testChallengeDir + '/test-challenge';
fs.mkdirSync(testChallengeDir, {recursive: true}); fs.mkdirSync(testChallengeDir, {recursive: true});
fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'}); fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'});
async function performTestForDomain (domain) { async function performTestForDomain (domain) {
logger.info(`Testing http challenge for ${domain}`); logger.info('Testing http challenge for ' + domain);
const url = `http://${domain}/.well-known/acme-challenge/test-challenge`; const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`; const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
const options = { const options = {
@@ -1192,16 +1169,13 @@ const internalCertificate = {
const result = await new Promise((resolve) => { const result = await new Promise((resolve) => {
const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, (res) => { const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) {
let responseBody = ''; let responseBody = '';
res.on('data', (chunk) => { res.on('data', (chunk) => responseBody = responseBody + chunk);
responseBody = responseBody + chunk; res.on('end', function () {
});
res.on('end', () => {
try { try {
const parsedBody = JSON.parse(`${responseBody}`); const parsedBody = JSON.parse(responseBody + '');
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`); logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`);
resolve(undefined); resolve(undefined);
@@ -1222,7 +1196,7 @@ const internalCertificate = {
// Make sure to write the request body. // Make sure to write the request body.
req.write(formBody); req.write(formBody);
req.end(); req.end();
req.on('error', (e) => { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
resolve(undefined); }); resolve(undefined); });
}); });
@@ -1264,34 +1238,6 @@ const internalCertificate = {
fs.unlinkSync(testChallengeFile); fs.unlinkSync(testChallengeFile);
return results; return results;
},
getAdditionalCertbotArgs: (certificate_id, dns_provider) => {
const args = [];
if (letsencryptServer !== null) {
args.push('--server', letsencryptServer);
}
if (letsencryptStaging && letsencryptServer === null) {
args.push('--staging');
}
// For route53, add the credentials file as an environment variable,
// inheriting the process env
const opts = {};
if (certificate_id && dns_provider === 'route53') {
opts.env = process.env;
opts.env.AWS_CONFIG_FILE = `/etc/letsencrypt/credentials/credentials-${certificate_id}`;
}
if (dns_provider === 'duckdns') {
args.push('--dns-duckdns-no-txt-restore');
}
return {args: args, opts: opts};
},
getLiveCertPath: (certificate_id) => {
return `/etc/letsencrypt/live/npm-${certificate_id}`;
} }
}; };

View File

@@ -229,8 +229,32 @@ const internalHost = {
} }
return response; return response;
} },
/**
* Internal use only, checks to see if the there is another default server record
*
* @param {String} hostname
* @param {String} [ignore_type] 'proxy', 'redirection', 'dead'
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
* @returns {Promise}
*/
checkDefaultServerNotExist: function (hostname) {
let promises = proxyHostModel
.query()
.where('default_server', true)
.andWhere('domain_names', 'not like', '%' + hostname + '%');
return Promise.resolve(promises)
.then((promises_results) => {
if (promises_results.length > 0){
return false;
}
return true;
});
}
}; };
module.exports = internalHost; module.exports = internalHost;

View File

@@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('node:fs'); const fs = require('fs');
const logger = require('../logger').nginx; const logger = require('../logger').nginx;
const config = require('../lib/config'); const config = require('../lib/config');
const utils = require('../lib/utils'); const utils = require('../lib/utils');
@@ -57,9 +57,9 @@ const internalNginx = {
// It will always look like this: // It will always look like this:
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
const valid_lines = []; let valid_lines = [];
const err_lines = err.message.split('\n'); let err_lines = err.message.split('\n');
err_lines.map((line) => { err_lines.map(function (line) {
if (line.indexOf('/var/log/nginx/error.log') === -1) { if (line.indexOf('/var/log/nginx/error.log') === -1) {
valid_lines.push(line); valid_lines.push(line);
} }
@@ -105,7 +105,7 @@ const internalNginx = {
logger.info('Testing Nginx configuration'); logger.info('Testing Nginx configuration');
} }
return utils.execFile('/usr/sbin/nginx', ['-t', '-g', 'error_log off;']); return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
}, },
/** /**
@@ -115,7 +115,7 @@ const internalNginx = {
return internalNginx.test() return internalNginx.test()
.then(() => { .then(() => {
logger.info('Reloading Nginx'); logger.info('Reloading Nginx');
return utils.execFile('/usr/sbin/nginx', ['-s', 'reload']); return utils.exec('/usr/sbin/nginx -s reload');
}); });
}, },
@@ -128,7 +128,7 @@ const internalNginx = {
if (host_type === 'default') { if (host_type === 'default') {
return '/data/nginx/default_host/site.conf'; return '/data/nginx/default_host/site.conf';
} }
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`; return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';
}, },
/** /**
@@ -141,7 +141,7 @@ const internalNginx = {
let template; let template;
try { try {
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
@@ -152,7 +152,7 @@ const internalNginx = {
const locationRendering = async () => { const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) { for (let i = 0; i < host.locations.length; i++) {
const locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id},
{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits}, {ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},
{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support}, {allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list}, {hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
@@ -183,21 +183,21 @@ const internalNginx = {
*/ */
generateConfig: (host_type, host_row) => { generateConfig: (host_type, host_row) => {
// Prevent modifying the original object: // Prevent modifying the original object:
const host = JSON.parse(JSON.stringify(host_row)); let host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
if (config.debug()) { if (config.debug()) {
logger.info(`Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2));
} }
const renderEngine = utils.getRenderEngine(); const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
const filename = internalNginx.getConfigName(nice_host_type, host.id); let filename = internalNginx.getConfigName(nice_host_type, host.id);
try { try {
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
@@ -252,7 +252,7 @@ const internalNginx = {
}) })
.catch((err) => { .catch((err) => {
if (config.debug()) { if (config.debug()) {
logger.warn(`Could not write ${filename}:`, err.message); logger.warn('Could not write ' + filename + ':', err.message);
} }
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
@@ -278,10 +278,10 @@ const internalNginx = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
try { try {
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
@@ -302,7 +302,7 @@ const internalNginx = {
}) })
.catch((err) => { .catch((err) => {
if (config.debug()) { if (config.debug()) {
logger.warn(`Could not write ${filename}:`, err.message); logger.warn('Could not write ' + filename + ':', err.message);
} }
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
@@ -316,7 +316,7 @@ const internalNginx = {
* @param {String} filename * @param {String} filename
*/ */
deleteFile: (filename) => { deleteFile: (filename) => {
logger.debug(`Deleting file: ${filename}`); logger.debug('Deleting file: ' + filename);
try { try {
fs.unlinkSync(filename); fs.unlinkSync(filename);
} catch (err) { } catch (err) {
@@ -330,7 +330,7 @@ const internalNginx = {
* @returns String * @returns String
*/ */
getFileFriendlyHostType: (host_type) => { getFileFriendlyHostType: (host_type) => {
return host_type.replace(/-/g, '_'); return host_type.replace(new RegExp('-', 'g'), '_');
}, },
/** /**
@@ -340,7 +340,7 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
deleteLetsEncryptRequestConfig: (certificate) => { deleteLetsEncryptRequestConfig: (certificate) => {
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
return new Promise((resolve/*, reject*/) => { return new Promise((resolve/*, reject*/) => {
internalNginx.deleteFile(config_file); internalNginx.deleteFile(config_file);
resolve(); resolve();
@@ -355,7 +355,7 @@ const internalNginx = {
*/ */
deleteConfig: (host_type, host, delete_err_file) => { deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
const config_file_err = `${config_file}.err`; const config_file_err = config_file + '.err';
return new Promise((resolve/*, reject*/) => { return new Promise((resolve/*, reject*/) => {
internalNginx.deleteFile(config_file); internalNginx.deleteFile(config_file);
@@ -373,7 +373,7 @@ const internalNginx = {
*/ */
renameConfigAsError: (host_type, host) => { renameConfigAsError: (host_type, host) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
const config_file_err = `${config_file}.err`; const config_file_err = config_file + '.err';
return new Promise((resolve/*, reject*/) => { return new Promise((resolve/*, reject*/) => {
fs.unlink(config_file, () => { fs.unlink(config_file, () => {
@@ -392,8 +392,8 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
bulkGenerateConfigs: (host_type, hosts) => { bulkGenerateConfigs: (host_type, hosts) => {
const promises = []; let promises = [];
hosts.map((host) => { hosts.map(function (host) {
promises.push(internalNginx.generateConfig(host_type, host)); promises.push(internalNginx.generateConfig(host_type, host));
}); });
@@ -406,8 +406,8 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
bulkDeleteConfigs: (host_type, hosts) => { bulkDeleteConfigs: (host_type, hosts) => {
const promises = []; let promises = [];
hosts.map((host) => { hosts.map(function (host) {
promises.push(internalNginx.deleteConfig(host_type, host, true)); promises.push(internalNginx.deleteConfig(host_type, host, true));
}); });
@@ -418,12 +418,14 @@ const internalNginx = {
* @param {string} config * @param {string} config
* @returns {boolean} * @returns {boolean}
*/ */
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im), advancedConfigHasDefaultLocation: function (cfg) {
return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
},
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
ipv6Enabled: () => { ipv6Enabled: function () {
if (typeof process.env.DISABLE_IPV6 !== 'undefined') { if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
const disabled = process.env.DISABLE_IPV6.toLowerCase(); const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes'); return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');

View File

@@ -44,6 +44,22 @@ const internalProxyHost = {
}); });
}); });
}) })
.then(() => {
// Get a list of the domain names and check each of them against default records
if (data.default_server){
if (data.domain_names.length > 1) {
throw new error.ValidationError('Default server cant be set for multiple domain!');
}
return internalHost
.checkDefaultServerNotExist(data.domain_names[0])
.then((result) => {
if (!result){
throw new error.ValidationError('One default server already exists');
}
});
}
})
.then(() => { .then(() => {
// At this point the domains should have been checked // At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1); data.owner_user_id = access.token.getUserId(1);
@@ -141,6 +157,22 @@ const internalProxyHost = {
}); });
} }
}) })
.then(() => {
// Get a list of the domain names and check each of them against default records
if (data.default_server){
if (data.domain_names.length > 1) {
throw new error.ValidationError('Default server cant be set for multiple domain!');
}
return internalHost
.checkDefaultServerNotExist(data.domain_names[0])
.then((result) => {
if (!result){
throw new error.ValidationError('One default server already exists');
}
});
}
})
.then(() => { .then(() => {
return internalProxyHost.get(access, {id: data.id}); return internalProxyHost.get(access, {id: data.id});
}) })
@@ -153,6 +185,7 @@ const internalProxyHost = {
if (create_certificate) { if (create_certificate) {
return internalCertificate.createQuickCertificate(access, { return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names, domain_names: data.domain_names || row.domain_names,
ssl_key_type: data.ssl_key_type || row.ssl_key_type,
meta: _.assign({}, row.meta, data.meta) meta: _.assign({}, row.meta, data.meta)
}) })
.then((cert) => { .then((cert) => {

View File

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

View File

@@ -11,7 +11,7 @@ const certbot = {
/** /**
* @param {array} pluginKeys * @param {array} pluginKeys
*/ */
installPlugins: async (pluginKeys) => { installPlugins: async function (pluginKeys) {
let hasErrors = false; let hasErrors = false;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -21,7 +21,7 @@ const certbot = {
} }
batchflow(pluginKeys).sequential() batchflow(pluginKeys).sequential()
.each((_i, pluginKey, next) => { .each((i, pluginKey, next) => {
certbot.installPlugin(pluginKey) certbot.installPlugin(pluginKey)
.then(() => { .then(() => {
next(); next();
@@ -51,7 +51,7 @@ const certbot = {
* @param {string} pluginKey * @param {string} pluginKey
* @returns {Object} * @returns {Object}
*/ */
installPlugin: async (pluginKey) => { installPlugin: async function (pluginKey) {
if (typeof dnsPlugins[pluginKey] === 'undefined') { if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`); // throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.ItemNotFoundError(pluginKey); throw new error.ItemNotFoundError(pluginKey);
@@ -63,15 +63,8 @@ const certbot = {
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
// in new versions of Python return utils.exec(cmd)
let env = Object.assign({}, process.env, {SETUPTOOLS_USE_DISTUTILS: 'stdlib'});
if (typeof plugin.env === 'object') {
env = Object.assign(env, plugin.env);
}
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
return utils.exec(cmd, {env})
.then((result) => { .then((result) => {
logger.complete(`Installed ${pluginKey}`); logger.complete(`Installed ${pluginKey}`);
return result; return result;

View File

@@ -1,13 +1,13 @@
const _ = require('lodash'); const _ = require('lodash');
const exec = require('node:child_process').exec; const exec = require('child_process').exec;
const execFile = require('node:child_process').execFile; const execFile = require('child_process').execFile;
const { Liquid } = require('liquidjs'); const { Liquid } = require('liquidjs');
const logger = require('../logger').global; const logger = require('../logger').global;
const error = require('./error'); const error = require('./error');
module.exports = { module.exports = {
exec: async (cmd, options = {}) => { exec: async function(cmd, options = {}) {
logger.debug('CMD:', cmd); logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => { const { stdout, stderr } = await new Promise((resolve, reject) => {
@@ -29,19 +29,15 @@ module.exports = {
/** /**
* @param {String} cmd * @param {String} cmd
* @param {Array} args * @param {Array} args
* @param {Object|undefined} options
* @returns {Promise} * @returns {Promise}
*/ */
execFile: (cmd, args, options) => { execFile: function (cmd, args) {
logger.debug(`CMD: ${cmd} ${args ? args.join(' ') : ''}`); // logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
if (typeof options === 'undefined') {
options = {};
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile(cmd, args, options, (err, stdout, stderr) => { execFile(cmd, args, function (err, stdout, /*stderr*/) {
if (err && typeof err === 'object') { if (err && typeof err === 'object') {
reject(new error.CommandError(stderr, 1, err)); reject(err);
} else { } else {
resolve(stdout.trim()); resolve(stdout.trim());
} }
@@ -55,7 +51,7 @@ module.exports = {
* @param {Array} omissions * @param {Array} omissions
* @returns {Function} * @returns {Function}
*/ */
omitRow: (omissions) => { omitRow: function (omissions) {
/** /**
* @param {Object} row * @param {Object} row
* @returns {Object} * @returns {Object}
@@ -71,7 +67,7 @@ module.exports = {
* @param {Array} omissions * @param {Array} omissions
* @returns {Function} * @returns {Function}
*/ */
omitRows: (omissions) => { omitRows: function (omissions) {
/** /**
* @param {Array} rows * @param {Array} rows
* @returns {Object} * @returns {Object}
@@ -87,9 +83,9 @@ module.exports = {
/** /**
* @returns {Object} Liquid render engine * @returns {Object} Liquid render engine
*/ */
getRenderEngine: () => { getRenderEngine: function () {
const renderEngine = new Liquid({ const renderEngine = new Liquid({
root: `${__dirname}/../templates/` root: __dirname + '/../templates/'
}); });
/** /**

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,51 @@
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) {
logger.info(`[${migrate_name}] Migrating Up...`);
return knex.schema.alterTable('proxy_host', (table) => {
table.enum('ssl_key_type', ['ecdsa', 'rsa']).defaultTo('ecdsa').notNullable();
}).then(() => {
logger.info(`[${migrate_name}] Column 'ssl_key_type' added to table 'proxy_host'`);
return knex.schema.alterTable('certificate', (table) => {
table.enum('ssl_key_type', ['ecdsa', 'rsa']).defaultTo('ecdsa').notNullable();
});
}).then(() => {
logger.info(`[${migrate_name}] Column 'ssl_key_type' added to table 'proxy_host'`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info(`[${migrate_name}] Migrating Down...`);
return knex.schema.alterTable('proxy_host', (table) => {
table.dropColumn('ssl_key_type');
}).then(() => {
logger.info(`[${migrate_name}] Column 'ssl_key_type' removed from table 'proxy_host'`);
return knex.schema.alterTable('certificate', (table) => {
table.dropColumn('ssl_key_type');
});
}).then(() => {
logger.info(`[${migrate_name}] Column 'ssl_key_type' removed from table 'proxy_host'`);
});
};

View File

@@ -0,0 +1,40 @@
const migrate_name = 'default_server';
const logger = require('../logger').migrate;
/**
* Migrate Up
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex) {
logger.info(`[${migrate_name}] Migrating Up...`);
// Add default_server column to proxy_host table
return knex.schema.table('proxy_host', (table) => {
table.boolean('default_server').notNullable().defaultTo(false);
})
.then(() => {
logger.info(`[${migrate_name}] Column 'default_server' added to 'proxy_host' table`);
});
};
/**
* Migrate Down
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info(`[${migrate_name}] Migrating Down...`);
// Remove default_server column from proxy_host table
return knex.schema.table('proxy_host', (table) => {
table.dropColumn('default_server');
})
.then(() => {
logger.info(`[${migrate_name}] Column 'default_server' removed from 'proxy_host' table`);
});
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ const apiValidator = require('../../lib/validator/api');
const internalCertificate = require('../../internal/certificate'); const internalCertificate = require('../../internal/certificate');
const schema = require('../../schema'); const schema = require('../../schema');
const router = express.Router({ let router = express.Router({
caseSensitive: true, caseSensitive: true,
strict: true, strict: true,
mergeParams: true mergeParams: true
@@ -231,7 +231,7 @@ router
*/ */
router router
.route('/:certificate_id/download') .route('/:certificate_id/download')
.options((_req, res) => { .options((req, res) => {
res.sendStatus(204); res.sendStatus(204);
}) })
.all(jwtdecode()) .all(jwtdecode())

View File

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

View File

@@ -110,11 +110,6 @@
"caching_enabled": { "caching_enabled": {
"description": "Should we cache assets", "description": "Should we cache assets",
"type": "boolean" "type": "boolean"
},
"email": {
"description": "Email address",
"type": "string",
"pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
} }
} }
} }

View File

@@ -41,6 +41,11 @@
"owner": { "owner": {
"$ref": "./user-object.json" "$ref": "./user-object.json"
}, },
"ssl_key_type": {
"type": "string",
"enum": ["ecdsa", "rsa"],
"description": "Type of SSL key (either ecdsa or rsa)"
},
"meta": { "meta": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -69,7 +74,7 @@
"type": "object" "type": "object"
}, },
"letsencrypt_email": { "letsencrypt_email": {
"$ref": "../common.json#/properties/email" "type": "string"
}, },
"propagation_seconds": { "propagation_seconds": {
"type": "integer", "type": "integer",

View File

@@ -22,7 +22,9 @@
"enabled", "enabled",
"locations", "locations",
"hsts_enabled", "hsts_enabled",
"hsts_subdomains" "hsts_subdomains",
"default_server",
"certificate"
], ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@@ -148,6 +150,15 @@
"$ref": "./access-list-object.json" "$ref": "./access-list-object.json"
} }
] ]
},
"ssl_key_type": {
"type": "string",
"enum": ["ecdsa", "rsa"],
"description": "Type of SSL key (either ecdsa or rsa)"
},
"default_server": {
"type": "boolean",
"description": "Defines if the server is the default for unmatched requests"
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -79,6 +79,12 @@
}, },
"locations": { "locations": {
"$ref": "../../../../components/proxy-host-object.json#/properties/locations" "$ref": "../../../../components/proxy-host-object.json#/properties/locations"
},
"ssl_key_type": {
"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_key_type"
},
"default_server": {
"$ref": "../../../../components/proxy-host-object.json#/properties/default_server"
} }
} }
} }

View File

@@ -67,6 +67,12 @@
}, },
"locations": { "locations": {
"$ref": "../../../components/proxy-host-object.json#/properties/locations" "$ref": "../../../components/proxy-host-object.json#/properties/locations"
},
"ssl_key_type": {
"$ref": "../../../components/proxy-host-object.json#/properties/ssl_key_type"
},
"default_server": {
"$ref": "../../../components/proxy-host-object.json#/properties/default_server"
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,10 @@ const setupDefaultUser = () => {
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row || !row.id) {
// Create a new user and set password // Create a new user and set password
const email = (process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com').toLowerCase(); const email = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com';
const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme'; const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme';
logger.info(`Creating a new user: ${email} with password: ${password}`); logger.info('Creating a new user: ' + email + ' with password: ' + password);
const data = { const data = {
is_deleted: 0, is_deleted: 0,
@@ -113,20 +113,20 @@ const setupCertbotPlugins = () => {
.andWhere('provider', 'letsencrypt') .andWhere('provider', 'letsencrypt')
.then((certificates) => { .then((certificates) => {
if (certificates && certificates.length) { if (certificates && certificates.length) {
const plugins = []; let plugins = [];
const promises = []; let promises = [];
certificates.map((certificate) => { certificates.map(function (certificate) {
if (certificate.meta && certificate.meta.dns_challenge === true) { if (certificate.meta && certificate.meta.dns_challenge === true) {
if (plugins.indexOf(certificate.meta.dns_provider) === -1) { if (plugins.indexOf(certificate.meta.dns_provider) === -1) {
plugins.push(certificate.meta.dns_provider); plugins.push(certificate.meta.dns_provider);
} }
// Make sure credentials file exists // Make sure credentials file exists
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
// Escape single quotes and backslashes // Escape single quotes and backslashes
const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\');
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`; const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }';
promises.push(utils.exec(credentials_cmd)); promises.push(utils.exec(credentials_cmd));
} }
}); });
@@ -136,7 +136,7 @@ const setupCertbotPlugins = () => {
if (promises.length) { if (promises.length) {
return Promise.all(promises) return Promise.all(promises)
.then(() => { .then(() => {
logger.info(`Added Certbot plugins ${plugins.join(', ')}`); logger.info('Added Certbot plugins ' + plugins.join(', '));
}); });
} }
}); });
@@ -165,7 +165,9 @@ const setupLogrotation = () => {
return runLogrotate(); return runLogrotate();
}; };
module.exports = () => setupDefaultUser() module.exports = function () {
return setupDefaultUser()
.then(setupDefaultSettings) .then(setupDefaultSettings)
.then(setupCertbotPlugins) .then(setupCertbotPlugins)
.then(setupLogrotation); .then(setupLogrotation);
};

View File

@@ -2,7 +2,6 @@
{% if certificate.provider == "letsencrypt" %} {% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL # Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf; include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;

View File

@@ -1,13 +0,0 @@
{% if certificate and certificate_id > 0 %}
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/ssl-cache-stream.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
{%- else %}
# Custom SSL
ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
{%- endif -%}
{%- endif -%}

View File

@@ -1,13 +1,13 @@
listen 80; listen 80{% if default_server == true %} default_server{% endif %};
{% if ipv6 -%} {% if ipv6 -%}
listen [::]:80; listen [::]:80{% if default_server == true %} default_server{% endif %};
{% else -%} {% else -%}
#listen [::]:80; #listen [::]:80;
{% endif %} {% endif %}
{% if certificate -%} {% if certificate -%}
listen 443 ssl; listen 443 ssl{% if default_server == true %} default_server{% endif %};
{% if ipv6 -%} {% if ipv6 -%}
listen [::]:443 ssl; listen [::]:443 ssl{% if default_server == true %} default_server{% endif %};
{% else -%} {% else -%}
#listen [::]:443; #listen [::]:443;
{% endif %} {% endif %}

View File

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

View File

@@ -492,9 +492,9 @@ boxen@^4.2.0:
widest-line "^3.1.0" widest-line "^3.1.0"
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.12" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"

View File

@@ -53,9 +53,11 @@ COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager
# Remove frontend service not required for prod, dev nginx config as well # Remove frontend service not required for prod, dev nginx config as well
RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \ RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager && chmod 644 /etc/logrotate.d/nginx-proxy-manager
COPY docker/start-container /usr/local/bin/start-container
RUN chmod +x /usr/local/bin/start-container
VOLUME [ "/data" ] VOLUME [ "/data" ]
ENTRYPOINT [ "/init" ] ENTRYPOINT [ "start-container" ]
LABEL org.label-schema.schema-version="1.0" \ LABEL org.label-schema.schema-version="1.0" \
org.label-schema.license="MIT" \ org.label-schema.license="MIT" \

View File

@@ -36,5 +36,8 @@ RUN rm -f /etc/nginx/conf.d/production.conf \
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
COPY start-container /usr/local/bin/start-container
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80 81 443 EXPOSE 80 81 443
ENTRYPOINT [ "/init" ] ENTRYPOINT [ "start-container" ]

View File

@@ -1,7 +1,5 @@
text = True text = True
non-interactive = True non-interactive = True
webroot-path = /data/letsencrypt-acme-challenge webroot-path = /data/letsencrypt-acme-challenge
key-type = ecdsa
elliptic-curve = secp384r1
preferred-chain = ISRG Root X1 preferred-chain = ISRG Root X1
server = server =

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
text = True text = True
non-interactive = True non-interactive = True
webroot-path = /data/letsencrypt-acme-challenge webroot-path = /data/letsencrypt-acme-challenge
key-type = ecdsa
elliptic-curve = secp384r1
preferred-chain = ISRG Root X1 preferred-chain = ISRG Root X1

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,4 +1,9 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
# intermediate configuration. tweak to your needs. # intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; ssl_ciphers "ALL:RC4-SHA:AES128-SHA:AES256-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256:RSA-AES256-CBC-SHA:RC4-MD5:DES-CBC3-SHA:AES256-SHA:RC4-SHA:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_dhparam /etc/ssl/certs/dhparam.pem;

View File

@@ -8,53 +8,21 @@ log_info 'Setting ownership ...'
# root # root
chown root /tmp/nginx chown root /tmp/nginx
locations=( # npm user and group
"/data" chown -R "$PUID:$PGID" /data
"/etc/letsencrypt" chown -R "$PUID:$PGID" /etc/letsencrypt
"/run/nginx" chown -R "$PUID:$PGID" /run/nginx
"/tmp/nginx" chown -R "$PUID:$PGID" /tmp/nginx
"/var/cache/nginx" chown -R "$PUID:$PGID" /var/cache/nginx
"/var/lib/logrotate" chown -R "$PUID:$PGID" /var/lib/logrotate
"/var/lib/nginx" chown -R "$PUID:$PGID" /var/lib/nginx
"/var/log/nginx" chown -R "$PUID:$PGID" /var/log/nginx
"/etc/nginx/nginx"
"/etc/nginx/nginx.conf"
"/etc/nginx/conf.d"
)
chownit() { # Don't chown entire /etc/nginx folder as this causes crashes on some systems
local dir="$1" chown -R "$PUID:$PGID" /etc/nginx/nginx
local recursive="${2:-true}" chown -R "$PUID:$PGID" /etc/nginx/nginx.conf
chown -R "$PUID:$PGID" /etc/nginx/conf.d
local have # Prevents errors when installing python certbot plugins when non-root
have="$(stat -c '%u:%g' "$dir")" chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
echo "- $dir ... " find /opt/certbot/lib/python*/site-packages -not -user "$PUID" -execdir chown "$PUID:$PGID" {} \+
if [ "$have" != "$PUID:$PGID" ]; then
if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then
chown -R "$PUID:$PGID" "$dir"
else
chown "$PUID:$PGID" "$dir"
fi
echo " DONE"
else
echo " SKIPPED"
fi
}
for loc in "${locations[@]}"; do
chownit "$loc"
done
if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then
log_info 'Skipping ownership change of certbot directories'
else
log_info 'Changing ownership of certbot directories, this may take some time ...'
chownit "/opt/certbot" false
chownit "/opt/certbot/bin" false
# Handle all site-packages directories efficiently
find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do
chownit "$SITE_PACKAGES_DIR"
done
fi

View File

@@ -5,9 +5,12 @@ set -e
log_info 'Dynamic resolvers ...' log_info 'Dynamic resolvers ...'
DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` # Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm # thanks @tfmm
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ];
then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf

View File

@@ -8,11 +8,14 @@ set -e
log_info 'IPv6 ...' log_info 'IPv6 ...'
# Lowercase
DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
process_folder () { process_folder () {
FILES=$(find "$1" -type f -name "*.conf") FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX= SED_REGEX=
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
# IPV6 is disabled # IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1" echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'

View File

@@ -56,13 +56,3 @@ get_group_id () {
getent group "$1" | cut -d: -f3 getent group "$1" | cut -d: -f3
fi fi
} }
# param $1: value
is_true () {
VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')
if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then
echo '1'
else
echo '0'
fi
}

View File

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

13
docker/start-container Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
FILE="/etc/ssl/certs/dhparam.pem"
if [ ! -f "$FILE" ]; then
echo "the $FILE does not exist, creating..."
openssl dhparam -out "$FILE" 2048
else
echo "the $FILE already exists, skipping..."
fi
echo "run default script"
exec /init

View File

@@ -161,14 +161,6 @@ The easy fix is to add a Docker environment variable to the Nginx Proxy Manager
DISABLE_IPV6: 'true' 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 ## Custom Nginx Configurations

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,7 +72,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12"> <div class="col-sm-6 col-md-6">
<div class="form-group"> <div class="form-group">
<label class="custom-switch"> <label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="allow_websocket_upgrade" value="1"<%- allow_websocket_upgrade ? ' checked' : '' %>> <input type="checkbox" class="custom-switch-input" name="allow_websocket_upgrade" value="1"<%- allow_websocket_upgrade ? ' checked' : '' %>>
@@ -81,6 +81,15 @@
</label> </label>
</div> </div>
</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="default_server" value="1"<%- default_server ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('proxy-hosts', 'default-server') %></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
@@ -105,6 +114,15 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group">
<label class="form-label"><%- i18n('all-hosts', 'ssl-key-type') %></label>
<select name="ssl_key_type" class="form-control custom-select">
<option value="ecdsa" data-data="{&quot;id&quot;:&quot;ecdsa&quot;}" <%- ssl_key_type == 'ecdsa' ? 'selected' : '' %>>ECDSA</option>
<option value="rsa" data-data="{&quot;id&quot;:&quot;rsa&quot;}" <%- ssl_key_type == 'rsa' ? 'selected' : '' %>>RSA</option>
</select>
</div>
</div>
<div class="col-sm-6 col-md-6"> <div class="col-sm-6 col-md-6">
<div class="form-group"> <div class="form-group">
<label class="custom-switch"> <label class="custom-switch">

View File

@@ -167,6 +167,7 @@ module.exports = Mn.View.extend({
data.hsts_enabled = !!data.hsts_enabled; data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains; data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced; data.ssl_forced = !!data.ssl_forced;
data.default_server = !!data.default_server;
if (typeof data.meta === 'undefined') data.meta = {}; if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;

View File

@@ -3,16 +3,8 @@
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5> <h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body has-tabs"> <div class="modal-body">
<div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
<form> <form>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
</ul>
<div class="tab-content">
<!-- Details -->
<div role="tabpanel" class="tab-pane active" id="details">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
@@ -54,137 +46,6 @@
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div> <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
</div> </div>
</div> </div>
</div>
<!-- SSL -->
<div role="tabpanel" class="tab-pane" id="ssl-options">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select>
</div>
</div>
<!-- DNS challenge -->
<div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group">
<label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label>
<input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>">
</div>
<div class="form-group">
<label class="custom-switch">
<input
type="checkbox"
class="custom-switch-input"
name="meta[dns_challenge]"
value="1"
checked
disabled
>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%= i18n('ssl', 'dns-challenge') %></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12 letsencrypt">
<fieldset class="form-fieldset dns-challenge">
<div class="text-red mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'certbot-warning') %></div>
<!-- Certbot DNS plugin selection -->
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label>
<select
name="meta[dns_provider]"
id="dns_provider"
class="form-control custom-select"
>
<option
value=""
disabled
hidden
<%- getDnsProvider() === null ? 'selected' : '' %>
>Please Choose...</option>
<% _.each(dns_plugins, function(plugin_info, plugin_name){ %>
<option
value="<%- plugin_name %>"
<%- getDnsProvider() === plugin_name ? 'selected' : '' %>
><%- plugin_info.name %></option>
<% }); %>
</select>
</div>
</div>
</div>
<!-- Certbot credentials file content -->
<div class="row credentials-file-content">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label>
<textarea
name="meta[dns_provider_credentials]"
class="form-control text-monospace"
id="dns_provider_credentials"
><%- getDnsProviderCredentials() %></textarea>
<div class="text-secondary small">
<i class="fe fe-info"></i>
<%= i18n('ssl', 'credentials-file-content-info') %>
</div>
<div class="text-red small">
<i class="fe fe-alert-triangle"></i>
<%= i18n('ssl', 'stored-as-plaintext-info') %>
</div>
</div>
</div>
</div>
<!-- DNS propagation delay -->
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group mb-0">
<label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label>
<input
type="number"
min="0"
name="meta[propagation_seconds]"
class="form-control"
id="propagation_seconds"
value="<%- getPropagationSeconds() %>"
>
<div class="text-secondary small">
<i class="fe fe-info"></i>
<%= i18n('ssl', 'propagation-seconds-info') %>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group">
<label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
<input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
</div>
</div>
<div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
</label>
</div>
</div>
</div>
</div>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@@ -2,14 +2,10 @@ const Mn = require('backbone.marionette');
const App = require('../../main'); const App = require('../../main');
const StreamModel = require('../../../models/stream'); const StreamModel = require('../../../models/stream');
const template = require('./form.ejs'); const template = require('./form.ejs');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
require('jquery-mask-plugin'); require('jquery-mask-plugin');
require('selectize'); require('selectize');
const Helpers = require("../../../lib/helpers");
const certListItemTemplate = require("../certificates-list-item.ejs");
const i18n = require("../../i18n");
module.exports = Mn.View.extend({ module.exports = Mn.View.extend({
template: template, template: template,
@@ -22,17 +18,7 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button', buttons: '.modal-footer button',
switches: '.custom-switch-input', switches: '.custom-switch-input',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save'
le_error_info: '#le-error-info',
certificate_select: 'select[name="certificate_id"]',
domain_names: 'input[name="domain_names"]',
dns_challenge_switch: 'input[name="meta[dns_challenge]"]',
dns_challenge_content: '.dns-challenge',
dns_provider: 'select[name="meta[dns_provider]"]',
credentials_file_content: '.credentials-file-content',
dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
propagation_seconds: 'input[name="meta[propagation_seconds]"]',
letsencrypt: '.letsencrypt'
}, },
events: { events: {
@@ -62,35 +48,6 @@ module.exports = Mn.View.extend({
data.tcp_forwarding = !!data.tcp_forwarding; data.tcp_forwarding = !!data.tcp_forwarding;
data.udp_forwarding = !!data.udp_forwarding; data.udp_forwarding = !!data.udp_forwarding;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = true;
if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
// Check for any domain names containing wildcards, which are not allowed with letsencrypt
if (data.certificate_id === 'new') {
let domain_err = false;
if (!data.meta.dns_challenge) {
data.domain_names.map(function (name) {
if (name.match(/\*/im)) {
domain_err = true;
}
});
}
if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
} else {
data.certificate_id = parseInt(data.certificate_id, 10);
}
let method = App.Api.Nginx.Streams.create; let method = App.Api.Nginx.Streams.create;
let is_new = true; let is_new = true;
@@ -113,108 +70,10 @@ module.exports = Mn.View.extend({
}); });
}) })
.catch(err => { .catch(err => {
let more_info = ''; alert(err.message);
if (err.code === 500 && err.debug) {
try {
more_info = JSON.parse(err.debug).debug.stack.join("\n");
} catch (e) {
}
}
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`;
this.ui.le_error_info.show();
this.ui.le_error_info[0].scrollIntoView();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
}); });
},
'change @ui.certificate_select': function () {
let id = this.ui.certificate_select.val();
if (id === 'new') {
this.ui.letsencrypt.show().find('input').prop('disabled', false);
this.ui.domain_names.prop('required', 'required');
this.ui.dns_challenge_switch
.prop('disabled', true)
.parents('.form-group')
.css('opacity', 0.5);
this.ui.dns_provider.prop('required', 'required');
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
this.ui.dns_provider_credentials.prop('required', 'required');
} }
this.ui.dns_challenge_content.show();
} else {
this.ui.letsencrypt.hide().find('input').prop('disabled', true);
}
},
'change @ui.dns_provider': function () {
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
this.ui.dns_provider_credentials.prop('required', 'required');
this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
this.ui.credentials_file_content.show();
} else {
this.ui.dns_provider_credentials.prop('required', false);
this.ui.credentials_file_content.hide();
}
},
},
templateContext: {
getLetsencryptEmail: function () {
return App.Cache.User.get('email');
},
getDnsProvider: function () {
return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
},
getDnsProviderCredentials: function () {
return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
},
getPropagationSeconds: function () {
return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
},
dns_plugins: dns_providers,
},
onRender: function () {
let view = this;
// Certificates
this.ui.le_error_info.hide();
this.ui.dns_challenge_content.hide();
this.ui.credentials_file_content.hide();
this.ui.letsencrypt.hide();
this.ui.certificate_select.selectize({
valueField: 'id',
labelField: 'nice_name',
searchField: ['nice_name', 'domain_names'],
create: false,
preload: true,
allowEmptyOption: true,
render: {
option: function (item) {
item.i18n = App.i18n;
item.formatDbDate = Helpers.formatDbDate;
return certListItemTemplate(item);
}
},
load: function (query, callback) {
App.Api.Nginx.Certificates.getAll()
.then(rows => {
callback(rows);
})
.catch(err => {
console.error(err);
callback();
});
},
onLoad: function () {
view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
}
});
}, },
initialize: function (options) { initialize: function (options) {

View File

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

View File

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

View File

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

View File

@@ -60,7 +60,7 @@
}, },
"footer": { "footer": {
"fork-me": "Fork me on Github", "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>" "theme": "Theme by <a href=\"{url}\" target=\"_blank\">Tabler</a>"
}, },
"dashboard": { "dashboard": {
@@ -77,6 +77,7 @@
"block-exploits": "Block Common Exploits", "block-exploits": "Block Common Exploits",
"caching-enabled": "Cache Assets", "caching-enabled": "Cache Assets",
"ssl-certificate": "SSL Certificate", "ssl-certificate": "SSL Certificate",
"ssl-key-type": "SSL Key Type",
"none": "None", "none": "None",
"new-cert": "Request a new SSL Certificate", "new-cert": "Request a new SSL Certificate",
"with-le": "with Let's Encrypt", "with-le": "with Let's Encrypt",
@@ -131,6 +132,7 @@
"help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.",
"access-list": "Access List", "access-list": "Access List",
"allow-websocket-upgrade": "Websockets Support", "allow-websocket-upgrade": "Websockets Support",
"default-server": "Default Server",
"ignore-invalid-upstream-ssl": "Ignore Invalid SSL", "ignore-invalid-upstream-ssl": "Ignore Invalid SSL",
"custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/", "custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/",
"search": "Search Host…" "search": "Search Host…"
@@ -179,9 +181,7 @@
"delete-confirm": "Are you sure you want to delete this Stream?", "delete-confirm": "Are you sure you want to delete this Stream?",
"help-title": "What is a Stream?", "help-title": "What is a Stream?",
"help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
"search": "Search Incoming Port…", "search": "Search Incoming Port…"
"ssl-certificate": "SSL Certificate for TCP Forwarding",
"tcp+ssl": "TCP+SSL"
}, },
"certificates": { "certificates": {
"title": "SSL Certificates", "title": "SSL Certificates",
@@ -208,10 +208,7 @@
"reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"download": "Download", "download": "Download",
"renew-title": "Renew Let's Encrypt Certificate", "renew-title": "Renew Let's Encrypt Certificate",
"search": "Search Certificate…", "search": "Search Certificate…"
"in-use" : "In use",
"inactive": "Inactive",
"active-domain_names": "Active domain names"
}, },
"access-lists": { "access-lists": {
"title": "Access Lists", "title": "Access Lists",

View File

@@ -10,6 +10,8 @@ const model = Backbone.Model.extend({
modified_on: null, modified_on: null,
domain_names: [], domain_names: [],
certificate_id: 0, certificate_id: 0,
ssl_key_type: 'ecdsa',
default_server: false,
ssl_forced: false, ssl_forced: false,
http2_support: false, http2_support: false,
hsts_enabled: false, hsts_enabled: false,

View File

@@ -14,6 +14,8 @@ const model = Backbone.Model.extend({
forward_port: null, forward_port: null,
access_list_id: 0, access_list_id: 0,
certificate_id: 0, certificate_id: 0,
ssl_key_type: 'ecdsa',
default_server: false,
ssl_forced: false, ssl_forced: false,
hsts_enabled: false, hsts_enabled: false,
hsts_subdomains: false, hsts_subdomains: false,

View File

@@ -14,6 +14,8 @@ const model = Backbone.Model.extend({
forward_domain_name: '', forward_domain_name: '',
preserve_path: true, preserve_path: true,
certificate_id: 0, certificate_id: 0,
ssl_key_type: 'ecdsa',
default_server: false,
ssl_forced: false, ssl_forced: false,
hsts_enabled: false, hsts_enabled: false,
hsts_subdomains: false, hsts_subdomains: false,

View File

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

View File

@@ -26,8 +26,8 @@
"messageformat": "^2.3.0", "messageformat": "^2.3.0",
"messageformat-loader": "^0.8.1", "messageformat-loader": "^0.8.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"moment": "^2.30.1", "moment": "^2.29.4",
"sass": "^1.92.1", "node-sass": "^9.0.0",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"sass-loader": "^10.0.0", "sass-loader": "^10.0.0",

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,9 @@
"active24": { "active24": {
"name": "Active24", "name": "Active24",
"package_name": "certbot-dns-active24", "package_name": "certbot-dns-active24",
"version": "~=2.0.0", "version": "~=1.5.1",
"dependencies": "", "dependencies": "",
"credentials": "dns_active24_api_key = <identifier>\ndns_active24_secret = <secret>", "credentials": "dns_active24_token=\"TOKEN\"",
"full_plugin_name": "dns-active24" "full_plugin_name": "dns-active24"
}, },
"aliyun": { "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", "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" "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": { "beget": {
"name":"Beget", "name":"Beget",
"package_name": "certbot-beget-plugin", "package_name": "certbot-beget-plugin",
@@ -55,19 +47,11 @@
"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", "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" "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": { "cloudflare": {
"name": "Cloudflare", "name": "Cloudflare",
"package_name": "certbot-dns-cloudflare", "package_name": "certbot-dns-cloudflare",
"version": "=={{certbot-version}}", "version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}", "dependencies": "cloudflare==2.19.* acme=={{certbot-version}}",
"credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567", "credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-cloudflare" "full_plugin_name": "dns-cloudflare"
}, },
@@ -106,19 +90,11 @@
"cpanel": { "cpanel": {
"name": "cPanel", "name": "cPanel",
"package_name": "certbot-dns-cpanel", "package_name": "certbot-dns-cpanel",
"version": "~=0.4.0", "version": "~=0.2.2",
"dependencies": "", "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" "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": { "desec": {
"name": "deSEC", "name": "deSEC",
"package_name": "certbot-dns-desec", "package_name": "certbot-dns-desec",
@@ -185,11 +161,11 @@
}, },
"domainoffensive": { "domainoffensive": {
"name": "DomainOffensive (do.de)", "name": "DomainOffensive (do.de)",
"package_name": "certbot-dns-domainoffensive", "package_name": "certbot-dns-do",
"version": "~=2.0.0", "version": "~=0.31.0",
"dependencies": "", "dependencies": "",
"credentials": "dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN", "credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-domainoffensive" "full_plugin_name": "dns-do"
}, },
"domeneshop": { "domeneshop": {
"name": "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", "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" "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": { "freedns": {
"name": "FreeDNS", "name": "FreeDNS",
"package_name": "certbot-dns-freedns", "package_name": "certbot-dns-freedns",
@@ -241,8 +209,8 @@
}, },
"gandi": { "gandi": {
"name": "Gandi Live DNS", "name": "Gandi Live DNS",
"package_name": "certbot-dns-gandi", "package_name": "certbot_plugin_gandi",
"version": "~=1.6.1", "version": "~=1.5.0",
"dependencies": "", "dependencies": "",
"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN", "credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
"full_plugin_name": "dns-gandi" "full_plugin_name": "dns-gandi"
@@ -396,7 +364,7 @@
"package_name": "certbot-dns-mijn-host", "package_name": "certbot-dns-mijn-host",
"version": "~=0.0.4", "version": "~=0.0.4",
"dependencies": "", "dependencies": "",
"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef", "credentials": "dns-mijn-host-credentials = /etc/letsencrypt/mijnhost-credentials.ini",
"full_plugin_name": "dns-mijn-host" "full_plugin_name": "dns-mijn-host"
}, },
"namecheap": { "namecheap": {
@@ -415,14 +383,6 @@
"credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123", "credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123",
"full_plugin_name": "dns-netcup" "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": { "njalla": {
"name": "Njalla", "name": "Njalla",
"package_name": "certbot-dns-njalla", "package_name": "certbot-dns-njalla",
@@ -511,29 +471,13 @@
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"full_plugin_name": "dns-route53" "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": { "strato": {
"name": "Strato", "name": "Strato",
"package_name": "certbot-dns-strato", "package_name": "certbot-dns-strato",
"version": "~=0.2.2", "version": "~=0.2.1",
"dependencies": "", "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\"", "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" "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": { "timeweb": {
"name": "Timeweb Cloud", "name": "Timeweb Cloud",
@@ -590,13 +534,5 @@
"dependencies": "", "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", "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" "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 # Disable Cypress CLI colors
ENV FORCE_COLOR=0 ENV FORCE_COLOR=0
ENV NO_COLOR=1 ENV NO_COLOR=1
# testssl.sh and mkcert
RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \
&& tar -xzf /tmp/testssl.tgz -C /tmp \
&& mv /tmp/testssl.sh-3.2rc4 /testssl \
&& rm /tmp/testssl.tgz \
&& apt-get update \
&& apt-get install -y bsdmainutils curl dnsutils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \
&& chmod +x /bin/mkcert
COPY --chown=1000 ./test /test
WORKDIR /test WORKDIR /test
RUN yarn install && yarn cache clean RUN yarn install && yarn cache clean
ENTRYPOINT [] ENTRYPOINT []

View File

@@ -10,7 +10,7 @@ describe('Certificates endpoints', () => {
}); });
}); });
it('Validate custom certificate', () => { it('Validate custom certificate', function() {
cy.task('backendApiPostFiles', { cy.task('backendApiPostFiles', {
token: token, token: token,
path: '/api/nginx/certificates/validate', path: '/api/nginx/certificates/validate',
@@ -25,7 +25,7 @@ describe('Certificates endpoints', () => {
}); });
}); });
it('Custom certificate lifecycle', () => { it('Custom certificate lifecycle', function() {
// Create custom cert // Create custom cert
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,
@@ -73,7 +73,7 @@ describe('Certificates endpoints', () => {
}); });
}); });
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => { it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() {
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,
path: '/api/nginx/certificates', path: '/api/nginx/certificates',
@@ -96,28 +96,4 @@ describe('Certificates endpoints', () => {
expect(data.error.message).to.contain('data/domain_names/0 must match pattern'); expect(data.error.message).to.contain('data/domain_names/0 must match pattern');
}); });
}); });
it('Request Certificate - LE Email Escaped', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
meta: {
dns_challenge: false,
letsencrypt_agree: true,
letsencrypt_email: "admin@example.com' --version;echo hello-world",
},
provider: 'letsencrypt',
},
returnOnError: true,
}).then((data) => {
cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);
expect(data).to.have.property('error');
expect(data.error).to.have.property('message');
expect(data.error).to.have.property('code');
expect(data.error.code).to.equal(400);
expect(data.error.message).to.contain('data/meta/letsencrypt_email must match pattern');
});
});
}); });

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', () => {
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

@@ -9,7 +9,7 @@ describe('Full Certificate Provisions', () => {
}); });
}); });
it('Should be able to create new http certificate', () => { it('Should be able to create new http certificate', function() {
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,
path: '/api/nginx/certificates', path: '/api/nginx/certificates',
@@ -32,7 +32,7 @@ describe('Full Certificate Provisions', () => {
}); });
}); });
it('Should be able to create new DNS certificate with Powerdns', () => { it('Should be able to create new DNS certificate with Powerdns', function() {
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,
path: '/api/nginx/certificates', path: '/api/nginx/certificates',

View File

@@ -1,7 +1,7 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('Basic API checks', () => { describe('Basic API checks', () => {
it('Should return a valid health payload', () => { it('Should return a valid health payload', function () {
cy.task('backendApiGet', { cy.task('backendApiGet', {
path: '/api/', path: '/api/',
}).then((data) => { }).then((data) => {
@@ -10,9 +10,9 @@ describe('Basic API checks', () => {
}); });
}); });
it('Should return a valid schema payload', () => { it('Should return a valid schema payload', function () {
cy.task('backendApiGet', { cy.task('backendApiGet', {
path: `/api/schema?ts=${Date.now()}`, path: '/api/schema?ts=' + Date.now(),
}).then((data) => { }).then((data) => {
expect(data.openapi).to.be.equal('3.1.0'); expect(data.openapi).to.be.equal('3.1.0');
}); });

View File

@@ -1,12 +1,12 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('LDAP with Authentik', () => { describe('LDAP with Authentik', () => {
let _token; let token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => { before(() => {
cy.getToken().then((tok) => { cy.getToken().then((tok) => {
_token = tok; token = tok;
// cy.task('backendApiPut', { // cy.task('backendApiPut', {
// token: token, // token: token,
@@ -45,7 +45,7 @@ describe('LDAP with Authentik', () => {
}); });
}); });
it.skip('Should log in with LDAP', () => { it.skip('Should log in with LDAP', function() {
// cy.task('backendApiPost', { // cy.task('backendApiPost', {
// token: token, // token: token,
// path: '/api/auth', // path: '/api/auth',

View File

@@ -1,12 +1,12 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('OAuth with Authentik', () => { describe('OAuth with Authentik', () => {
let _token; let token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => { before(() => {
cy.getToken().then((tok) => { cy.getToken().then((tok) => {
_token = tok; token = tok;
// cy.task('backendApiPut', { // cy.task('backendApiPut', {
// token: token, // token: token,
@@ -47,7 +47,7 @@ describe('OAuth with Authentik', () => {
}); });
}); });
it.skip('Should log in with OAuth', () => { it.skip('Should log in with OAuth', function() {
// cy.task('backendApiGet', { // cy.task('backendApiGet', {
// path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), // path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')),
// }).then((data) => { // }).then((data) => {

View File

@@ -9,7 +9,7 @@ describe('Proxy Hosts endpoints', () => {
}); });
}); });
it('Should be able to create a http host', () => { it('Should be able to create a http host', function() {
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,
path: '/api/nginx/proxy-hosts', path: '/api/nginx/proxy-hosts',

View File

@@ -9,7 +9,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Get all settings', () => { it('Get all settings', function() {
cy.task('backendApiGet', { cy.task('backendApiGet', {
token: token, token: token,
path: '/api/settings', path: '/api/settings',
@@ -19,7 +19,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Get default-site setting', () => { it('Get default-site setting', function() {
cy.task('backendApiGet', { cy.task('backendApiGet', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',
@@ -30,7 +30,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Default Site congratulations', () => { it('Default Site congratulations', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',
@@ -46,7 +46,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Default Site 404', () => { it('Default Site 404', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',
@@ -62,7 +62,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Default Site 444', () => { it('Default Site 444', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',
@@ -78,7 +78,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Default Site redirect', () => { it('Default Site redirect', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',
@@ -100,7 +100,7 @@ describe('Settings endpoints', () => {
}); });
}); });
it('Default Site html', () => { it('Default Site html', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/settings/default-site', path: '/api/settings/default-site',

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', () => {
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', () => {
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', () => {
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', () => {
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', () => {
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

@@ -9,7 +9,7 @@ describe('Users endpoints', () => {
}); });
}); });
it('Should be able to get yourself', () => { it('Should be able to get yourself', function() {
cy.task('backendApiGet', { cy.task('backendApiGet', {
token: token, token: token,
path: '/api/users/me' path: '/api/users/me'
@@ -20,7 +20,7 @@ describe('Users endpoints', () => {
}); });
}); });
it('Should be able to get all users', () => { it('Should be able to get all users', function() {
cy.task('backendApiGet', { cy.task('backendApiGet', {
token: token, token: token,
path: '/api/users' path: '/api/users'
@@ -30,7 +30,7 @@ describe('Users endpoints', () => {
}); });
}); });
it('Should be able to update yourself', () => { it('Should be able to update yourself', function() {
cy.task('backendApiPut', { cy.task('backendApiPut', {
token: token, token: token,
path: '/api/users/me', path: '/api/users/me',

View File

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

File diff suppressed because it is too large Load Diff