Compare commits

...

75 Commits

Author SHA1 Message Date
eb2d759fd8 Merge 08f95a9be6 into 79d28f03d0 2025-02-23 16:21:31 +00:00
08f95a9be6 Merge remote-tracking branch 'upstream/develop' into develop 2025-02-23 19:51:19 +03:30
79d28f03d0 Merge pull request #4346 from Sander0542/feature/security-schemes-component
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
API Schema Improvements
2025-02-07 12:39:49 +10:00
c4df89df1f Fix dashboard loading loop and freezing the page 2025-02-06 13:38:47 +10:00
34c703f8b4 Merge branch 'master' into develop 2025-02-06 08:52:55 +10:00
0a05d8f0ad Bump version 2025-02-06 08:39:03 +10:00
0a9141fad5 Merge pull request #4208 from badkeyy/feature/add-zone-edit-certbot-plugin
Add ZoneEdit certbot plugin
2025-02-06 08:33:11 +10:00
42836774b7 Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-02-06 08:33:01 +10:00
2a07544f58 Merge pull request #4235 from FabianK3/update-domainoffensive-certbot-plugin
Update DomainOffensive certbot plugin
2025-02-06 08:30:09 +10:00
dc9d884743 Merge pull request #4292 from icaksh/patch-1
feat: change htpasswd to openssl
2025-02-06 08:29:15 +10:00
0d5d2b1b7c Merge pull request #4283 from badkeyy/feature/show-active-host-in-cert-list
SSL Certificates: Show if cert is in use on host
2025-02-06 07:43:12 +10:00
df48b835c4 Update order to match others 2025-02-05 22:20:21 +01:00
8a1557154a Add certificate fields to boolFields 2025-02-05 22:15:12 +01:00
a6af5ec2c7 Remove certificate as required from proxy host 2025-02-05 18:18:50 +01:00
14d7c35fd7 Fix whitespaces 2025-02-05 17:31:09 +01:00
cfcf78aaee Set bearer auth security component 2025-02-05 17:29:40 +01:00
3a01b2c84f Merge pull request #4334 from nwagenmakers/mijn-host-patch
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Update certbot-dns-plugins.json (mijn-host)
2025-02-05 20:36:06 +10:00
e1c84a5c10 Merge pull request #4338 from Sander0542/fix/token-expires-type
Fix type for token.expires
2025-02-05 20:35:33 +10:00
c56c95a59a Merge pull request #4344 from NginxProxyManager/stream-ssl
SSL for Streams - 2025
2025-02-05 18:22:51 +10:00
6a60627833 Cypress test for Streams
and updated cypress + packages
2025-02-05 16:02:17 +10:00
b4793d3c16 Adds testssl.sh and mkcert to cypress stack 2025-02-05 08:10:11 +10:00
68a7803513 Fix api schema after merging latest changes 2025-02-04 17:55:28 +10:00
2657af97cf Fix stream update not persisting 2025-02-04 17:14:07 +10:00
4452f014b9 Fix whitespace in nginx stream config 2025-02-04 17:14:07 +10:00
cd80cc8e4d Add certificate to streams database model 2025-02-04 17:14:04 +10:00
ee4250d770 Add SSL column to streams table UI 2025-02-04 17:12:05 +10:00
3dbc70faa6 Add SSL tab to stream UI 2025-02-04 17:12:04 +10:00
3091c21cae Add SSL certificate to TCP streams if certificate in database 2025-02-04 17:12:04 +10:00
57cd2a1919 Fix type for token.expires 2025-02-03 21:47:41 +01:00
ad5936c530 Update certbot-dns-plugins.json (mijn-host)
Updated credentials hint/text in mijn-host plugin entry
2025-02-01 13:10:53 +01:00
498109addb Merge pull request #4310 from NginxProxyManager/dependabot/npm_and_yarn/docs/vite-5.4.14
All checks were successful
Close stale issues and PRs / stale (push) Successful in 3s
Bump vite from 5.4.8 to 5.4.14 in /docs
2025-01-28 18:08:46 +10:00
3f3aacd7ec Merge pull request #4274 from Dim145/develop
[Postgres] fix error in access_list get
2025-01-28 14:03:07 +10:00
bb4ecf812d Bump vite from 5.4.8 to 5.4.14 in /docs
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.8 to 5.4.14.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 07:09:04 +00:00
c05f9695d0 Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-01-15 15:37:53 +01:00
6343b398f0 Add --no-deps 2025-01-15 14:36:38 +00:00
59362b7477 feat: change htpasswd to openssl 2025-01-12 19:16:38 +07:00
aedaaa18e0 Fix whitespace 2025-01-10 05:20:28 +01:00
080bd0b749 Added status of certificates to the certificate list and show on which domain names the certificates are in use 2025-01-10 05:15:22 +01:00
3856b6b03d remove default server from certificate object 2025-01-08 10:33:21 +03:30
9687e9e450 Use previous version of powerdns image, newer version is broken
All checks were successful
Close stale issues and PRs / stale (push) Successful in 3s
2025-01-07 10:30:08 +10:00
5a234bb88c Fix incorrect test folder in ci results 2025-01-07 08:13:04 +10:00
4de4b65036 Merge pull request #4252 from GergelyGombai/develop
Add Gcore DNS Provider
2025-01-07 07:54:44 +10:00
f34cb59711 Revert "remove unesessary default values"
This reverts commit 408eab842d.
2025-01-04 21:16:32 +03:30
c135880625 Revert "remove default_server from certificate object"
This reverts commit 101afa0013.
2025-01-04 17:56:14 +03:30
408eab842d remove unesessary default values 2025-01-04 12:15:14 +03:30
101afa0013 remove default_server from certificate object 2025-01-03 18:43:07 +03:30
f1c97c7c36 fix: add missing group_by clause for access_list get 2025-01-03 00:39:29 +01:00
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
d3a5fac51b make ssl_key_type optional 2025-01-01 23:47:32 +03:30
a121cb124e remove unnecessary whitespace 2025-01-01 23:41:40 +03:30
65f971fd36 add migration names and combine ssl key migrations 2025-01-01 23:26:28 +03:30
ad36fb5c2d show select ssl key type just for create new ssl 2025-01-01 22:42:19 +03:30
c6d884dab6 fix indent 2024-12-22 17:29:23 +03:30
5dc78df0bb fix messages indent: convert to space 2024-12-22 11:12:35 +03:30
73110d5e1e Update Gcore apikey format
I managed to mis-write the format in my previous commit
2024-12-22 01:44:52 +01:00
356b98bf7e Add Gcore DNS Provider 2024-12-22 01:02:47 +01:00
04636b71a9 add feature: set default server 2024-12-22 01:49:05 +03:30
1353937c36 fix copy address 2024-12-21 21:12:24 +03:30
f68c1b7c29 add Diffie-Hellman Parameters to cipher suites 2024-12-21 21:05:09 +03:30
32e0784865 support more cipher suites 2024-12-21 20:20:54 +03:30
5d087f1256 Update DomainOffensive certbot plugin 2024-12-15 11:35:58 +01:00
f386f6b640 remove elliptic-curve 2024-12-14 01:40:01 +03:30
5ba7363c9e fix ssl cipher bug 2024-12-13 11:30:58 +03:30
2e45444328 change ssl_ciphers for more compatibility 2024-12-12 23:48:51 +03:30
eb5c51a657 add support more cipher suites
this cipher suites need for old iot devices
2024-12-12 20:42:22 +03:30
cb795565ea add ssl_key_type in swagger
fix ci test error
2024-12-12 12:08:03 +03:30
04b3608b4e remove elliptic-curve from certbot command options 2024-12-12 01:49:57 +03:30
111fc287eb Revert "add elliptic-curve"
This reverts commit 95a94a4f8c.
2024-12-12 01:49:19 +03:30
95a94a4f8c add elliptic-curve 2024-12-12 01:15:39 +03:30
5e7b69c396 add update cipher suites 2024-12-12 00:46:14 +03:30
2723de24fd add ssl_ecdh_curve for more compatibility 2024-12-11 23:31:39 +03:30
891877afb6 fix ssl key-type certificate 2024-12-11 11:51:58 +03:30
8e9e033a72 fix indent: tab to space 2024-12-09 11:30:10 +03:30
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
1e322804ce Add ZoneEdit certbot plugin 2024-12-04 16:47:36 +01:00
66 changed files with 1665 additions and 708 deletions

View File

@ -1 +1 @@
2.12.2 2.12.3

6
Jenkinsfile vendored
View File

@ -128,7 +128,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable { unstable {
dir(path: 'testing/results') { dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
} }
} }
@ -161,7 +161,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable { unstable {
dir(path: 'testing/results') { dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
} }
} }
@ -199,7 +199,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable { unstable {
dir(path: 'testing/results') { dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
} }
} }

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.2-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.12.3-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <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

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

View File

@ -313,6 +313,9 @@ const internalCertificate = {
.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') {
@ -464,6 +467,9 @@ const internalCertificate = {
.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') {
@ -570,6 +576,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
}); });
}, },
@ -832,6 +839,7 @@ const internalCertificate = {
const cmd = `${certbotCommand} certonly ` + const cmd = `${certbotCommand} certonly ` +
`--config '${letsencryptConfig}' ` + `--config '${letsencryptConfig}' ` +
`--key-type '${certificate.ssl_key_type}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' + '--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' + '--logs-dir "/tmp/letsencrypt-log" ' +
`--cert-name "npm-${certificate.id}" ` + `--cert-name "npm-${certificate.id}" ` +
@ -873,6 +881,7 @@ const internalCertificate = {
let mainCmd = certbotCommand + ' certonly ' + let mainCmd = certbotCommand + ' certonly ' +
`--config '${letsencryptConfig}' ` + `--config '${letsencryptConfig}' ` +
`--key-type '${certificate.ssl_key_type}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' + '--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' + '--logs-dir "/tmp/letsencrypt-log" ' +
`--cert-name 'npm-${certificate.id}' ` + `--cert-name 'npm-${certificate.id}' ` +
@ -969,6 +978,7 @@ const internalCertificate = {
const cmd = certbotCommand + ' renew --force-renewal ' + const cmd = certbotCommand + ' renew --force-renewal ' +
`--config '${letsencryptConfig}' ` + `--config '${letsencryptConfig}' ` +
`--key-type '${certificate.ssl_key_type}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' + '--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' + '--logs-dir "/tmp/letsencrypt-log" ' +
`--cert-name 'npm-${certificate.id}' ` + `--cert-name 'npm-${certificate.id}' ` +
@ -1002,6 +1012,7 @@ const internalCertificate = {
let mainCmd = certbotCommand + ' renew --force-renewal ' + let mainCmd = certbotCommand + ' renew --force-renewal ' +
`--config "${letsencryptConfig}" ` + `--config "${letsencryptConfig}" ` +
`--key-type '${certificate.ssl_key_type}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' + '--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' + '--logs-dir "/tmp/letsencrypt-log" ' +
`--cert-name 'npm-${certificate.id}' ` + `--cert-name 'npm-${certificate.id}' ` +
@ -1035,6 +1046,7 @@ const internalCertificate = {
const mainCmd = certbotCommand + ' revoke ' + const mainCmd = certbotCommand + ' revoke ' +
`--config '${letsencryptConfig}' ` + `--config '${letsencryptConfig}' ` +
`--key-type '${certificate.ssl_key_type}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' + '--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' + '--logs-dir "/tmp/letsencrypt-log" ' +
`--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` + `--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` +

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

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

View File

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

View File

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

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,

View File

@ -22,8 +22,8 @@
"enabled", "enabled",
"locations", "locations",
"hsts_enabled", "hsts_enabled",
"hsts_subdomains", "default_server",
"certificate" "hsts_subdomains"
], ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -149,6 +149,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

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

View File

@ -5,10 +5,9 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"expires": { "expires": {
"description": "Token Expiry Unix Time", "description": "Token Expiry ISO Time String",
"example": 1566540249, "example": "2025-02-04T20:40:46.340Z",
"minimum": 1, "type": "string"
"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": ["access_list", "owner", "certificate"] "enum": ["owner", "certificate"]
} }
} }
], ],
@ -40,7 +40,8 @@
"nginx_online": true, "nginx_online": true,
"nginx_err": null "nginx_err": null
}, },
"enabled": true "enabled": true,
"certificate_id": 0
} }
] ]
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,15 @@
"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

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

View File

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

View File

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

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

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

View File

@ -132,7 +132,7 @@ services:
- 8128:3128 - 8128:3128
pdns: pdns:
image: pschiffe/pdns-mysql image: pschiffe/pdns-mysql:4.8
container_name: npm2dev.pdns container_name: npm2dev.pdns
volumes: volumes:
- '/etc/localtime:/etc/localtime:ro' - '/etc/localtime:/etc/localtime:ro'

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

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

View File

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

View File

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

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

@ -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.8" version "5.4.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ== 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 () {
let controller = this; const 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,8 +93,7 @@ module.exports = {
* Dashboard * Dashboard
*/ */
showDashboard: function () { showDashboard: function () {
let controller = this; const 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());
@ -106,7 +105,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')) {
let controller = this; const controller = this;
require(['./main', './nginx/proxy/main'], (App, View) => { require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy'); controller.navigate('/nginx/proxy');
@ -146,8 +145,7 @@ module.exports = {
*/ */
showNginxRedirection: function () { showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
let controller = this; const 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());
@ -186,8 +184,7 @@ module.exports = {
*/ */
showNginxStream: function () { showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) { if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
let controller = this; const 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());
@ -226,8 +223,7 @@ module.exports = {
*/ */
showNginxDead: function () { showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
let controller = this; const 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());
@ -278,8 +274,7 @@ module.exports = {
*/ */
showNginxAccess: function () { showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
let controller = this; const 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());
@ -318,8 +313,7 @@ module.exports = {
*/ */
showNginxCertificates: function () { showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
let controller = this; const 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());
@ -383,7 +377,7 @@ module.exports = {
* Audit Log * Audit Log
*/ */
showAuditLog: function () { showAuditLog: function () {
let controller = this; const 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');
@ -411,7 +405,7 @@ module.exports = {
* Settings * Settings
*/ */
showSettings: function () { showSettings: function () {
let controller = this; const 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 () {
let view = this; const view = this;
return { return {
getUserName: function () { getUserName: function () {
@ -48,8 +48,7 @@ module.exports = Mn.View.extend({
}, },
onRender: function () { onRender: function () {
let view = this; const view = this;
if (typeof view.stats.hosts === 'undefined') { if (typeof view.stats.hosts === 'undefined') {
Api.Reports.getHostStats() Api.Reports.getHostStats()
.then(response => { .then(response => {
@ -72,8 +71,7 @@ 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
//let view = this; const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
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,6 +33,13 @@
<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">
@ -48,6 +55,13 @@
<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,14 +44,24 @@ module.exports = Mn.View.extend({
}, },
}, },
templateContext: { templateContext: function () {
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,6 +3,7 @@
<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'], query) this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], 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']) view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
.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,8 +3,16 @@
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5> <h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body has-tabs">
<div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
<form> <form>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
</ul>
<div class="tab-content">
<!-- Details -->
<div role="tabpanel" class="tab-pane active" id="details">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
@ -46,6 +54,137 @@
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div> <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
</div> </div>
</div> </div>
</div>
<!-- SSL -->
<div role="tabpanel" class="tab-pane" id="ssl-options">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&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,10 +2,14 @@ const Mn = require('backbone.marionette');
const App = require('../../main'); const App = require('../../main');
const StreamModel = require('../../../models/stream'); const StreamModel = require('../../../models/stream');
const template = require('./form.ejs'); const template = require('./form.ejs');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
require('jquery-mask-plugin'); require('jquery-mask-plugin');
require('selectize'); require('selectize');
const Helpers = require("../../../lib/helpers");
const certListItemTemplate = require("../certificates-list-item.ejs");
const i18n = require("../../i18n");
module.exports = Mn.View.extend({ module.exports = Mn.View.extend({
template: template, template: template,
@ -18,7 +22,17 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button', buttons: '.modal-footer button',
switches: '.custom-switch-input', switches: '.custom-switch-input',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save' save: 'button.save',
le_error_info: '#le-error-info',
certificate_select: 'select[name="certificate_id"]',
domain_names: 'input[name="domain_names"]',
dns_challenge_switch: 'input[name="meta[dns_challenge]"]',
dns_challenge_content: '.dns-challenge',
dns_provider: 'select[name="meta[dns_provider]"]',
credentials_file_content: '.credentials-file-content',
dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
propagation_seconds: 'input[name="meta[propagation_seconds]"]',
letsencrypt: '.letsencrypt'
}, },
events: { events: {
@ -48,6 +62,35 @@ module.exports = Mn.View.extend({
data.tcp_forwarding = !!data.tcp_forwarding; data.tcp_forwarding = !!data.tcp_forwarding;
data.udp_forwarding = !!data.udp_forwarding; data.udp_forwarding = !!data.udp_forwarding;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = true;
if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
// Check for any domain names containing wildcards, which are not allowed with letsencrypt
if (data.certificate_id === 'new') {
let domain_err = false;
if (!data.meta.dns_challenge) {
data.domain_names.map(function (name) {
if (name.match(/\*/im)) {
domain_err = true;
}
});
}
if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
} else {
data.certificate_id = parseInt(data.certificate_id, 10);
}
let method = App.Api.Nginx.Streams.create; let method = App.Api.Nginx.Streams.create;
let is_new = true; let is_new = true;
@ -70,10 +113,108 @@ module.exports = Mn.View.extend({
}); });
}) })
.catch(err => { .catch(err => {
alert(err.message); let more_info = '';
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); if (err.code === 500 && err.debug) {
}); try {
more_info = JSON.parse(err.debug).debug.stack.join("\n");
} catch (e) {
} }
}
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`;
this.ui.le_error_info.show();
this.ui.le_error_info[0].scrollIntoView();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
});
},
'change @ui.certificate_select': function () {
let id = this.ui.certificate_select.val();
if (id === 'new') {
this.ui.letsencrypt.show().find('input').prop('disabled', false);
this.ui.domain_names.prop('required', 'required');
this.ui.dns_challenge_switch
.prop('disabled', true)
.parents('.form-group')
.css('opacity', 0.5);
this.ui.dns_provider.prop('required', 'required');
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
this.ui.dns_provider_credentials.prop('required', 'required');
}
this.ui.dns_challenge_content.show();
} else {
this.ui.letsencrypt.hide().find('input').prop('disabled', true);
}
},
'change @ui.dns_provider': function () {
const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
this.ui.dns_provider_credentials.prop('required', 'required');
this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
this.ui.credentials_file_content.show();
} else {
this.ui.dns_provider_credentials.prop('required', false);
this.ui.credentials_file_content.hide();
}
},
},
templateContext: {
getLetsencryptEmail: function () {
return App.Cache.User.get('email');
},
getDnsProvider: function () {
return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
},
getDnsProviderCredentials: function () {
return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
},
getPropagationSeconds: function () {
return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
},
dns_plugins: dns_providers,
},
onRender: function () {
let view = this;
// Certificates
this.ui.le_error_info.hide();
this.ui.dns_challenge_content.hide();
this.ui.credentials_file_content.hide();
this.ui.letsencrypt.hide();
this.ui.certificate_select.selectize({
valueField: 'id',
labelField: 'nice_name',
searchField: ['nice_name', 'domain_names'],
create: false,
preload: true,
allowEmptyOption: true,
render: {
option: function (item) {
item.i18n = App.i18n;
item.formatDbDate = Helpers.formatDbDate;
return certListItemTemplate(item);
}
},
load: function (query, callback) {
App.Api.Nginx.Certificates.getAll()
.then(rows => {
callback(rows);
})
.catch(err => {
console.error(err);
callback();
});
},
onLoad: function () {
view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
}
});
}, },
initialize: function (options) { initialize: function (options) {

View File

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

View File

@ -3,6 +3,7 @@
<th><%- i18n('streams', 'incoming-port') %></th> <th><%- i18n('streams', 'incoming-port') %></th>
<th><%- i18n('str', 'destination') %></th> <th><%- i18n('str', 'destination') %></th>
<th><%- i18n('streams', 'protocol') %></th> <th><%- i18n('streams', 'protocol') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'status') %></th> <th><%- i18n('str', 'status') %></th>
<% if (canManage) { %> <% if (canManage) { %>
<th>&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']) view.fetch(['owner', 'certificate'])
.then(response => { .then(response => {
if (!view.isDestroyed()) { if (!view.isDestroyed()) {
if (response && response.length) { if (response && response.length) {

View File

@ -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,7 +181,9 @@
"delete-confirm": "Are you sure you want to delete this Stream?", "delete-confirm": "Are you sure you want to delete this Stream?",
"help-title": "What is a Stream?", "help-title": "What is a Stream?",
"help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
"search": "Search Incoming Port…" "search": "Search Incoming Port…",
"ssl-certificate": "SSL Certificate for TCP Forwarding",
"tcp+ssl": "TCP+SSL"
}, },
"certificates": { "certificates": {
"title": "SSL Certificates", "title": "SSL Certificates",
@ -206,7 +210,10 @@
"reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", "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,8 +15,11 @@ const model = Backbone.Model.extend({
udp_forwarding: false, udp_forwarding: false,
enabled: true, enabled: true,
meta: {}, meta: {},
certificate_id: 0,
domain_names: [],
// The following are expansions: // The following are expansions:
owner: null owner: null,
certificate: null
}; };
} }
}); });

View File

@ -161,11 +161,11 @@
}, },
"domainoffensive": { "domainoffensive": {
"name": "DomainOffensive (do.de)", "name": "DomainOffensive (do.de)",
"package_name": "certbot-dns-do", "package_name": "certbot-dns-domainoffensive",
"version": "~=0.31.0", "version": "~=2.0.0",
"dependencies": "", "dependencies": "",
"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN", "credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-do" "full_plugin_name": "dns-domainoffensive"
}, },
"domeneshop": { "domeneshop": {
"name": "Domeneshop", "name": "Domeneshop",
@ -215,6 +215,14 @@
"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"
}, },
"gcore": {
"name": "Gcore DNS",
"package_name": "certbot-dns-gcore",
"version": "~=0.1.8",
"dependencies": "",
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-gcore"
},
"godaddy": { "godaddy": {
"name": "GoDaddy", "name": "GoDaddy",
"package_name": "certbot-dns-godaddy", "package_name": "certbot-dns-godaddy",
@ -356,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-credentials = /etc/letsencrypt/mijnhost-credentials.ini", "credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-mijn-host" "full_plugin_name": "dns-mijn-host"
}, },
"namecheap": { "namecheap": {
@ -526,5 +534,13 @@
"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,11 +1,22 @@
FROM cypress/included:13.9.0 FROM cypress/included:14.0.1
COPY --chown=1000 ./test /test
# Disable Cypress CLI colors # Disable Cypress CLI colors
ENV FORCE_COLOR=0 ENV FORCE_COLOR=0
ENV NO_COLOR=1 ENV NO_COLOR=1
# testssl.sh and mkcert
RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \
&& tar -xzf /tmp/testssl.tgz -C /tmp \
&& mv /tmp/testssl.sh-3.2rc4 /testssl \
&& rm /tmp/testssl.tgz \
&& apt-get update \
&& apt-get install -y bsdmainutils curl dnsutils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \
&& chmod +x /bin/mkcert
COPY --chown=1000 ./test /test
WORKDIR /test WORKDIR /test
RUN yarn install && yarn cache clean RUN yarn install && yarn cache clean
ENTRYPOINT [] ENTRYPOINT []

View File

@ -0,0 +1,213 @@
/// <reference types="cypress" />
describe('Streams', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
// Set default site content
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'html',
meta: {
html: '<p>yay it works</p>'
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
});
});
// Create a custom cert pair
cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => {
expect(result.code).to.eq(0);
// Install CA
cy.exec('mkcert -install').then((result) => {
expect(result.code).to.eq(0);
});
});
cy.exec('rm -f /test/results/testssl.json');
});
it('Should be able to create TCP Stream', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1500,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {
dns_provider_credentials: "",
letsencrypt_agree: false,
dns_challenge: true
},
tcp_forwarding: true,
udp_forwarding: false
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', false);
cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => {
expect(result.code).to.eq(0);
expect(result.stdout).to.contain('yay it works');
});
});
});
it('Should be able to create UDP Stream', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1501,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {
dns_provider_credentials: "",
letsencrypt_agree: false,
dns_challenge: true
},
tcp_forwarding: false,
udp_forwarding: true
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', false);
expect(data).to.have.property('udp_forwarding', true);
});
});
it('Should be able to create TCP/UDP Stream', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1502,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {
dns_provider_credentials: "",
letsencrypt_agree: false,
dns_challenge: true
},
tcp_forwarding: true,
udp_forwarding: true
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', true);
cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => {
expect(result.code).to.eq(0);
expect(result.stdout).to.contain('yay it works');
});
});
});
it('Should be able to create SSL TCP Stream', function() {
let certID = 0;
// Create custom cert
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
provider: "other",
nice_name: "Custom Certificate for SSL Stream",
},
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
certID = data.id;
// Upload files
cy.task('backendApiPostFiles', {
token: token,
path: `/api/nginx/certificates/${certID}/upload`,
files: {
certificate: 'website1.pem',
certificate_key: 'website1.key.pem',
},
}).then((data) => {
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
expect(data).to.have.property('certificate');
expect(data).to.have.property('certificate_key');
// Create the stream
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1503,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: certID,
meta: {
dns_provider_credentials: "",
letsencrypt_agree: false,
dns_challenge: true
},
tcp_forwarding: true,
udp_forwarding: false
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property("enabled", true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', false);
expect(data).to.have.property('certificate_id', certID);
// Check the ssl termination
cy.task('log', '[testssl.sh] Running ...');
cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', {
timeout: 120000, // 2 minutes
}).then((result) => {
cy.task('log', '[testssl.sh] ' + result.stdout);
const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"];
const ignoredIDs = [
'cert_chain_of_trust',
'cert_extlifeSpan',
'cert_revocation',
'overall_grade',
];
cy.readFile('/test/results/testssl.json').then((data) => {
// Parse each array item
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (ignoredIDs.includes(item.id)) {
continue;
}
expect(item.severity).to.be.oneOf(allowedSeverities);
}
});
});
});
});
});
});
});

View File

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