Merge branch 'develop' into pg-support

# Conflicts:
#	docker/dev/Dockerfile
This commit is contained in:
jeff 2024-10-18 16:13:20 -03:00
commit 57d90948d8
38 changed files with 584 additions and 155 deletions

View File

@ -1 +1 @@
2.12.0 2.12.1

20
Jenkinsfile vendored
View File

@ -43,7 +43,7 @@ pipeline {
steps { steps {
script { script {
// Defaults to the Branch name, which is applies to all branches AND pr's // Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
} }
} }
} }
@ -127,6 +127,11 @@ pipeline {
junit 'test/results/junit/*' junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable {
dir(path: 'testing/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
} }
} }
stage('Test Mysql') { stage('Test Mysql') {
@ -155,6 +160,11 @@ pipeline {
junit 'test/results/junit/*' junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable {
dir(path: 'testing/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
} }
} }
stage('MultiArch Build') { stage('MultiArch Build') {
@ -193,7 +203,13 @@ pipeline {
} }
steps { steps {
script { script {
npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true) npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on
[DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev)
as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}`
**Note:** 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
""", 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.0-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.12.1-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,27 +3,29 @@ const fs = require('fs');
const https = require('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 path = require('path');
const { isArray } = require('lodash');
const logger = require('../logger').ssl; const logger = require('../logger').ssl;
const config = require('../lib/config'); const config = require('../lib/config');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils'); const utils = require('../lib/utils');
const certbot = require('../lib/certbot');
const certificateModel = require('../models/certificate'); const certificateModel = require('../models/certificate');
const tokenModel = require('../models/token'); const tokenModel = require('../models/token');
const dnsPlugins = require('../global/certbot-dns-plugins.json'); const dnsPlugins = require('../global/certbot-dns-plugins.json');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalHost = require('./host'); const internalHost = require('./host');
const certbot = require('../lib/certbot');
const archiver = require('archiver');
const path = require('path');
const { isArray } = require('lodash');
const letsencryptStaging = config.useLetsencryptStaging(); const letsencryptStaging = config.useLetsencryptStaging();
const letsencryptServer = config.useLetsencryptServer();
const letsencryptConfig = '/etc/letsencrypt.ini'; const letsencryptConfig = '/etc/letsencrypt.ini';
const certbotCommand = 'certbot'; const certbotCommand = 'certbot';
function omissions() { function omissions() {
return ['is_deleted']; return ['is_deleted', 'owner.is_deleted'];
} }
const internalCertificate = { const internalCertificate = {
@ -207,6 +209,7 @@ const internalCertificate = {
.patchAndFetchById(certificate.id, { .patchAndFetchById(certificate.id, {
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
}) })
.then(utils.omitRow(omissions()))
.then((saved_row) => { .then((saved_row) => {
// Add cert data for audit log // Add cert data for audit log
saved_row.meta = _.assign({}, saved_row.meta, { saved_row.meta = _.assign({}, saved_row.meta, {
@ -730,29 +733,29 @@ const internalCertificate = {
return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
.then((result) => { .then((result) => {
// Examples:
// subject=CN = *.jc21.com
// subject=CN = something.example.com // subject=CN = something.example.com
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 (typeof match[1] === 'undefined') { certData['cn'] = match[1];
throw new error.ValidationError('Could not determine subject from certificate: ' + result);
} }
certData['cn'] = match[1];
}) })
.then(() => { .then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
}) })
.then((result) => { .then((result) => {
// Examples:
// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
// issuer=C = US, O = Let's Encrypt, CN = E5
// issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA
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 (typeof match[1] === 'undefined') { certData['issuer'] = match[1];
throw new error.ValidationError('Could not determine issuer from certificate: ' + result);
} }
certData['issuer'] = match[1];
}) })
.then(() => { .then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
@ -827,17 +830,18 @@ const internalCertificate = {
requestLetsEncryptSsl: (certificate) => { requestLetsEncryptSsl: (certificate) => {
logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const cmd = certbotCommand + ' certonly ' + const cmd = `${certbotCommand} certonly ` +
'--config "' + letsencryptConfig + '" ' + `--config '${letsencryptConfig}' ` +
'--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}" ` +
'--agree-tos ' + '--agree-tos ' +
'--authenticator webroot ' + '--authenticator webroot ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' + `--email '${certificate.meta.letsencrypt_email}' ` +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--domains "' + certificate.domain_names.join(',') + '" ' + `--domains "${certificate.domain_names.join(',')}" ` +
(letsencryptStaging ? '--staging' : ''); (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
logger.info('Command:', cmd); logger.info('Command:', cmd);
@ -868,25 +872,26 @@ const internalCertificate = {
const hasConfigArg = certificate.meta.dns_provider !== 'route53'; const hasConfigArg = certificate.meta.dns_provider !== 'route53';
let mainCmd = certbotCommand + ' certonly ' + let mainCmd = certbotCommand + ' certonly ' +
'--config "' + letsencryptConfig + '" ' + `--config '${letsencryptConfig}' ` +
'--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}' ` +
'--agree-tos ' + '--agree-tos ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' + `--email '${certificate.meta.letsencrypt_email}' ` +
'--domains "' + certificate.domain_names.join(',') + '" ' + `--domains '${certificate.domain_names.join(',')}' ` +
'--authenticator ' + dnsPlugin.full_plugin_name + ' ' + `--authenticator '${dnsPlugin.full_plugin_name}' ` +
( (
hasConfigArg hasConfigArg
? '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' ? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' `
: '' : ''
) + ) +
( (
certificate.meta.propagation_seconds !== undefined certificate.meta.propagation_seconds !== undefined
? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds ? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' `
: '' : ''
) + ) +
(letsencryptStaging ? ' --staging' : ''); (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Prepend the path to the credentials file as an environment variable // Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') { if (certificate.meta.dns_provider === 'route53') {
@ -963,14 +968,15 @@ const internalCertificate = {
logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const cmd = certbotCommand + ' renew --force-renewal ' + const cmd = certbotCommand + ' renew --force-renewal ' +
'--config "' + letsencryptConfig + '" ' + `--config '${letsencryptConfig}' ` +
'--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}' ` +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--no-random-sleep-on-renew ' + '--no-random-sleep-on-renew ' +
'--disable-hook-validation ' + '--disable-hook-validation ' +
(letsencryptStaging ? '--staging' : ''); (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
logger.info('Command:', cmd); logger.info('Command:', cmd);
@ -995,13 +1001,14 @@ const internalCertificate = {
logger.info(`Renewing Let'sEncrypt 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(', ')}`);
let mainCmd = certbotCommand + ' renew --force-renewal ' + let mainCmd = certbotCommand + ' renew --force-renewal ' +
'--config "' + letsencryptConfig + '" ' + `--config "${letsencryptConfig}" ` +
'--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}' ` +
'--disable-hook-validation ' + '--disable-hook-validation ' +
'--no-random-sleep-on-renew ' + '--no-random-sleep-on-renew ' +
(letsencryptStaging ? ' --staging' : ''); (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Prepend the path to the credentials file as an environment variable // Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') { if (certificate.meta.dns_provider === 'route53') {
@ -1027,12 +1034,13 @@ const internalCertificate = {
logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const mainCmd = certbotCommand + ' revoke ' + const mainCmd = certbotCommand + ' revoke ' +
'--config "' + letsencryptConfig + '" ' + `--config '${letsencryptConfig}' ` +
'--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' ` +
'--delete-after-revoke ' + '--delete-after-revoke ' +
(letsencryptStaging ? '--staging' : ''); (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Don't fail command if file does not exist // Don't fail command if file does not exist
const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;

View File

@ -181,7 +181,9 @@ const internalNginx = {
* @param {Object} host * @param {Object} host
* @returns {Promise} * @returns {Promise}
*/ */
generateConfig: (host_type, host) => { generateConfig: (host_type, host_row) => {
// Prevent modifying the original object:
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()) {

View File

@ -199,5 +199,15 @@ module.exports = {
*/ */
useLetsencryptStaging: function () { useLetsencryptStaging: function () {
return !!process.env.LE_STAGING; return !!process.env.LE_STAGING;
},
/**
* @returns {string|null}
*/
useLetsencryptServer: function () {
if (process.env.LE_SERVER) {
return process.env.LE_SERVER;
}
return null;
} }
}; };

View File

@ -24,22 +24,34 @@
"description": "Nice Name for the custom certificate" "description": "Nice Name for the custom certificate"
}, },
"domain_names": { "domain_names": {
"$ref": "../common.json#/properties/domain_names" "description": "Domain Names separated by a comma",
"type": "array",
"maxItems": 100,
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
}
}, },
"expires_on": { "expires_on": {
"description": "Date and time of expiration", "description": "Date and time of expiration",
"readOnly": true, "readOnly": true,
"type": "string" "type": "string"
}, },
"owner": {
"$ref": "./user-object.json"
},
"meta": { "meta": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"letsencrypt_email": { "certificate": {
"type": "string" "type": "string",
"minLength": 1
}, },
"letsencrypt_agree": { "certificate_key": {
"type": "boolean" "type": "string",
"minLength": 1
}, },
"dns_challenge": { "dns_challenge": {
"type": "boolean" "type": "boolean"
@ -50,13 +62,18 @@
"dns_provider_credentials": { "dns_provider_credentials": {
"type": "string" "type": "string"
}, },
"letsencrypt_agree": {
"type": "boolean"
},
"letsencrypt_certificate": {
"type": "object"
},
"letsencrypt_email": {
"type": "string"
},
"propagation_seconds": { "propagation_seconds": {
"anyOf": [ "type": "integer",
{ "minimum": 0
"type": "integer",
"minimum": 0
}
]
} }
} }
} }

View File

@ -23,9 +23,7 @@
"locations", "locations",
"hsts_enabled", "hsts_enabled",
"hsts_subdomains", "hsts_subdomains",
"certificate", "certificate"
"use_default_location",
"ipv6"
], ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -151,12 +149,6 @@
"$ref": "./access-list-object.json" "$ref": "./access-list-object.json"
} }
] ]
},
"use_default_location": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
} }
} }
} }

View File

@ -28,7 +28,7 @@
}, },
"forward_scheme": { "forward_scheme": {
"type": "string", "type": "string",
"enum": ["http", "https"] "enum": ["auto", "http", "https"]
}, },
"forward_domain_name": { "forward_domain_name": {
"description": "Domain Name", "description": "Domain Name",

View File

@ -25,7 +25,7 @@
"value": { "value": {
"description": "Value in almost any form", "description": "Value in almost any form",
"example": "congratulations", "example": "congratulations",
"oneOf": [ "anyOf": [
{ {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
@ -46,7 +46,10 @@
}, },
"meta": { "meta": {
"description": "Extra metadata", "description": "Extra metadata",
"example": {}, "example": {
"redirect": "http://example.com",
"html": "<h1>404</h1>"
},
"type": "object" "type": "object"
} }
} }

View File

@ -55,6 +55,25 @@
"certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n" "certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n"
} }
} }
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["certificate", "certificate_key"],
"properties": {
"certificate": {
"type": "string",
"minLength": 1
},
"certificate_key": {
"type": "string",
"minLength": 1
},
"intermediate_certificate": {
"type": "string",
"minLength": 1
}
}
} }
} }
} }

View File

@ -94,9 +94,7 @@
"avatar": "", "avatar": "",
"roles": ["admin"] "roles": ["admin"]
}, },
"certificate": null, "certificate": null
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -79,9 +79,7 @@
"nickname": "Admin", "nickname": "Admin",
"avatar": "", "avatar": "",
"roles": ["admin"] "roles": ["admin"]
}, }
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -129,9 +129,7 @@
"roles": ["admin"] "roles": ["admin"]
}, },
"certificate": null, "certificate": null,
"access_list": null, "access_list": null
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -114,9 +114,7 @@
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": ["admin"] "roles": ["admin"]
}, },
"access_list": null, "access_list": null
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -114,9 +114,7 @@
"avatar": "", "avatar": "",
"roles": ["admin"] "roles": ["admin"]
}, },
"certificate": null, "certificate": null
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -99,9 +99,7 @@
"nickname": "Admin", "nickname": "Admin",
"avatar": "", "avatar": "",
"roles": ["admin"] "roles": ["admin"]
}, }
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -129,9 +129,7 @@
"roles": ["admin"] "roles": ["admin"]
}, },
"certificate": null, "certificate": null,
"access_list": null, "access_list": null
"use_default_location": true,
"ipv6": true
} }
} }
}, },

View File

@ -13,7 +13,8 @@
"name": "settingID", "name": "settingID",
"schema": { "schema": {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1,
"enum": ["default-site"]
}, },
"required": true, "required": true,
"description": "Setting ID", "description": "Setting ID",
@ -31,10 +32,21 @@
"minProperties": 1, "minProperties": 1,
"properties": { "properties": {
"value": { "value": {
"$ref": "../../../components/setting-object.json#/properties/value" "type": "string",
"minLength": 1,
"enum": ["congratulations", "404", "444", "redirect", "html"]
}, },
"meta": { "meta": {
"$ref": "../../../components/setting-object.json#/properties/meta" "type": "object",
"additionalProperties": false,
"properties": {
"redirect": {
"type": "string"
},
"html": {
"type": "string"
}
}
} }
} }
} }

View File

@ -3,6 +3,8 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build # This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -45,6 +47,8 @@ RUN yarn install \
# add late to limit cache-busting by modifications # add late to limit cache-busting by modifications
COPY docker/rootfs / COPY docker/rootfs /
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
# 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 \

View File

@ -1,7 +1,10 @@
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node FROM nginxproxymanager/nginx-full:certbot-node
LABEL maintainer="Jamie Curnow <jc@jc21.com>" LABEL maintainer="Jamie Curnow <jc@jc21.com>"
# See: https://github.com/just-containers/s6-overlay/blob/master/README.md SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV SUPPRESS_NO_CONFIG_WARNING=1 \ ENV SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
@ -17,18 +20,22 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Task # Task
RUN cd /usr \ WORKDIR /usr
&& curl -sL https://taskfile.dev/install.sh | sh \ RUN curl -sL https://taskfile.dev/install.sh | sh
&& cd /root WORKDIR /root
COPY rootfs / COPY rootfs /
RUN rm -f /etc/nginx/conf.d/production.conf
RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager
# s6 overlay
COPY scripts/install-s6 /tmp/install-s6 COPY scripts/install-s6 /tmp/install-s6
RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
RUN chmod 644 -R /root/.cache RUN chmod 644 -R /root/.cache
RUN rm -f /etc/nginx/conf.d/production.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \
&& /tmp/install-s6 "${TARGETPLATFORM}" \
&& rm -f /tmp/install-s6
# Certs for testing purposes
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443 EXPOSE 80 81 443
ENTRYPOINT [ "/init" ] ENTRYPOINT [ "/init" ]

View File

@ -9,6 +9,9 @@ services:
environment: environment:
DEBUG: 'true' DEBUG: 'true'
FORCE_COLOR: 1 FORCE_COLOR: 1
# Required for DNS Certificate provisioning in CI
LE_SERVER: 'https://ca.internal/acme/acme/directory'
REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
volumes: volumes:
- 'npm_data_ci:/data' - 'npm_data_ci:/data'
- 'npm_le_ci:/etc/letsencrypt' - 'npm_le_ci:/etc/letsencrypt'

View File

@ -1,7 +1,7 @@
# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. # WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
services: services:
npm: fullstack:
image: nginxproxymanager:dev image: nginxproxymanager:dev
container_name: npm_core container_name: npm_core
build: build:
@ -33,12 +33,20 @@ services:
DB_MYSQL_NAME: 'npm' DB_MYSQL_NAME: 'npm'
# DB_SQLITE_FILE: "/data/database.sqlite" # DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true" # DISABLE_IPV6: "true"
# Required for DNS Certificate provisioning testing:
LE_SERVER: 'https://ca.internal/acme/acme/directory'
REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
volumes: volumes:
- npm_data:/data - npm_data:/data
- le_data:/etc/letsencrypt - le_data:/etc/letsencrypt
- './dev/resolv.conf:/etc/resolv.conf:ro'
- ../backend:/app - ../backend:/app
- ../frontend:/app/frontend - ../frontend:/app/frontend
- ../global:/app/global - ../global:/app/global
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
depends_on: depends_on:
- db - db
working_dir: /app working_dir: /app
@ -58,6 +66,23 @@ services:
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
stepca:
image: jc21/testca
volumes:
- './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro'
networks:
nginx_proxy_manager:
aliases:
- ca.internal
dnsrouter:
image: jc21/dnsrouter
volumes:
- ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
networks:
- nginx_proxy_manager
swagger: swagger:
image: swaggerapi/swagger-ui:latest image: swaggerapi/swagger-ui:latest
container_name: npm_swagger container_name: npm_swagger
@ -67,19 +92,78 @@ services:
URL: "http://npm:81/api/schema" URL: "http://npm:81/api/schema"
PORT: '80' PORT: '80'
depends_on: depends_on:
- npm - fullstack
squid: squid:
image: ubuntu/squid image: ubuntu/squid
container_name: npm_squid container_name: npm_squid
volumes: volumes:
- './dev/squid.conf:/etc/squid/squid.conf:ro' - './dev/squid.conf:/etc/squid/squid.conf:ro'
- './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro' - '/etc/localtime:/etc/localtime:ro'
networks: networks:
- nginx_proxy_manager - nginx_proxy_manager
ports: ports:
- 8128:3128 - 8128:3128
pdns:
image: pschiffe/pdns-mysql
volumes:
- '/etc/localtime:/etc/localtime:ro'
environment:
PDNS_master: 'yes'
PDNS_api: 'yes'
PDNS_api_key: 'npm'
PDNS_webserver: 'yes'
PDNS_webserver_address: '0.0.0.0'
PDNS_webserver_password: 'npm'
PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
PDNS_version_string: 'anonymous'
PDNS_default_ttl: 1500
PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
PDNS_gmysql_host: pdns-db
PDNS_gmysql_port: 3306
PDNS_gmysql_user: pdns
PDNS_gmysql_password: pdns
PDNS_gmysql_dbname: pdns
depends_on:
- pdns-db
networks:
nginx_proxy_manager:
aliases:
- ns1.pdns
- ns2.pdns
pdns-db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: 'pdns'
MYSQL_DATABASE: 'pdns'
MYSQL_USER: 'pdns'
MYSQL_PASSWORD: 'pdns'
volumes:
- 'pdns_mysql:/var/lib/mysql'
- '/etc/localtime:/etc/localtime:ro'
- './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
networks:
- nginx_proxy_manager
cypress:
image: "npm_dev_cypress"
build:
context: ../
dockerfile: test/cypress/Dockerfile
environment:
HTTP_PROXY: 'squid:3128'
HTTPS_PROXY: 'squid:3128'
volumes:
- '../test/results:/results'
- './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro'
command: cypress run --browser chrome --config-file=cypress/config/ci.js
networks:
- nginx_proxy_manager
volumes: volumes:
npm_data: npm_data:
name: npm_core_data name: npm_core_data
@ -87,6 +171,8 @@ volumes:
name: npm_le_data name: npm_le_data
db_data: db_data:
name: npm_db_data name: npm_db_data
pdns_mysql:
name: npm_pdns_mysql
networks: networks:
nginx_proxy_manager: nginx_proxy_manager:

View File

@ -15,3 +15,13 @@ COMPOSE_PROJECT_NAME="npmdev"
COMPOSE_FILE="docker/docker-compose.dev.yml" COMPOSE_FILE="docker/docker-compose.dev.yml"
export COMPOSE_FILE COMPOSE_PROJECT_NAME export COMPOSE_FILE COMPOSE_PROJECT_NAME
# $1: container_name
get_container_ip () {
local container_name=$1
local container
local ip
container=$(docker-compose ps --all -q "${container_name}" | tail -n1)
ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
echo "$ip"
}

13
scripts/cypress-dev Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Ensure docker-compose exists
if hash docker-compose 2>/dev/null; then
cd "${DIR}/.."
rm -rf "$DIR/../test/results"
docker-compose up --build cypress
else
echo -e "${RED} docker-compose command is not available${RESET}"
fi

View File

@ -7,8 +7,43 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if hash docker-compose 2>/dev/null; then if hash docker-compose 2>/dev/null; then
cd "${DIR}/.." cd "${DIR}/.."
echo -e "${BLUE} ${CYAN}Starting Dev Stack ...${RESET}" echo -e "${BLUE} ${CYAN}Starting Dev Stack ...${RESET}"
echo -e "${BLUE} $(docker-compose config)${RESET}"
docker-compose up -d --remove-orphans --force-recreate --build # Bring up a stack, in steps so we can inject IPs everywhere
docker-compose up -d pdns pdns-db
PDNS_IP=$(get_container_ip "pdns")
echo -e "${BLUE} ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}"
# adjust the dnsrouter config
LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json"
rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp"
# IMPORTANT: changes to dnsrouter-config.json will affect this line:
jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp"
docker-compose up -d dnsrouter
DNSROUTER_IP=$(get_container_ip "dnsrouter")
echo -e "${BLUE} ${YELLOW}DNS Router IP is ${DNSROUTER_IP}"
if [ "${DNSROUTER_IP:-}" = "" ]; then
echo -e "${RED} ERROR: DNS Router IP is not set${RESET}"
exit 1
fi
# mount the resolver
LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf"
rm -rf "${LOCAL_RESOLVE}"
printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
# bring up all remaining containers, except cypress!
docker-compose up -d --remove-orphans stepca squid
docker-compose pull db
docker-compose up -d --remove-orphans --pull=never fullstack
docker-compose up -d --remove-orphans swagger
# docker-compose up -d --remove-orphans --force-recreate --build
# wait for main container to be healthy
bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120
echo "" echo ""
echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}" echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}"

View File

@ -15,7 +15,7 @@ module.exports = defineConfig({
return require("../plugins/index.js")(on, config); return require("../plugins/index.js")(on, config);
}, },
env: { env: {
swaggerBase: 'http://fullstack:81/api/schema', swaggerBase: '{{baseUrl}}/api/schema?ts=' + Date.now(),
}, },
baseUrl: 'http://fullstack:81', baseUrl: 'http://fullstack:81',
} }

View File

@ -1,22 +0,0 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
requestTimeout: 30000,
defaultCommandTimeout: 20000,
reporter: 'cypress-multi-reporters',
reporterOptions: {
configFile: 'multi-reporter.json'
},
video: false,
videosFolder: 'results/videos',
screenshotsFolder: 'results/screenshots',
e2e: {
setupNodeEvents(on, config) {
return require("../plugins/index.js")(on, config);
},
env: {
swaggerBase: 'http://npm:81/api/schema',
},
baseUrl: 'http://npm:81',
}
});

View File

@ -1,7 +1,8 @@
/// <reference types="Cypress" /> /// <reference types="cypress" />
describe('Certificates endpoints', () => { describe('Certificates endpoints', () => {
let token; let token;
let certID;
before(() => { before(() => {
cy.getToken().then((tok) => { cy.getToken().then((tok) => {
@ -24,6 +25,54 @@ describe('Certificates endpoints', () => {
}); });
}); });
it('Custom certificate lifecycle', function() {
// Create custom cert
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
provider: "other",
nice_name: "Test Certificate",
},
}).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: 'test.example.com.pem',
certificate_key: 'test.example.com-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');
// Get all certs
cy.task('backendApiGet', {
token: token,
path: '/api/nginx/certificates?expand=owner'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);
expect(data.length).to.be.greaterThan(0);
// Delete cert
cy.task('backendApiDelete', {
token: token,
path: `/api/nginx/certificates/${certID}`
}).then((data) => {
cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);
expect(data).to.be.equal(true);
});
});
});
});
});
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() { it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() {
cy.task('backendApiPost', { cy.task('backendApiPost', {
token: token, token: token,

View File

@ -0,0 +1,62 @@
/// <reference types="cypress" />
describe('Full Certificate Provisions', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to create new http certificate', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: [
'website1.example.com'
],
meta: {
letsencrypt_email: 'admin@example.com',
letsencrypt_agree: true,
dns_challenge: false
},
provider: 'letsencrypt'
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.provider).to.be.equal('letsencrypt');
});
});
it('Should be able to create new DNS certificate with Powerdns', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: [
'website2.example.com'
],
meta: {
letsencrypt_email: "admin@example.com",
dns_challenge: true,
dns_provider: 'powerdns',
dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm',
letsencrypt_agree: true,
propagation_seconds: 5,
},
provider: 'letsencrypt'
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.provider).to.be.equal('letsencrypt');
expect(data.meta.dns_provider).to.be.equal('powerdns');
});
});
});

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" /> /// <reference types="cypress" />
describe('Basic API checks', () => { describe('Basic API checks', () => {
it('Should return a valid health payload', function () { it('Should return a valid health payload', function () {
@ -12,7 +12,7 @@ describe('Basic API checks', () => {
it('Should return a valid schema payload', function () { it('Should return a valid schema payload', function () {
cy.task('backendApiGet', { cy.task('backendApiGet', {
path: '/api/schema', 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,6 +1,6 @@
/// <reference types="Cypress" /> /// <reference types="cypress" />
describe('Hosts endpoints', () => { describe('Proxy Hosts endpoints', () => {
let token; let token;
before(() => { before(() => {

View File

@ -0,0 +1,124 @@
/// <reference types="cypress" />
describe('Settings endpoints', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Get all settings', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data.length).to.be.greaterThan(0);
});
});
it('Get default-site setting', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings/default-site',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
});
});
it('Default Site congratulations', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'congratulations',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('congratulations');
});
});
it('Default Site 404', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: '404',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('404');
});
});
it('Default Site 444', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: '444',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('444');
});
});
it('Default Site redirect', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'redirect',
meta: {
redirect: 'https://www.google.com',
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('redirect');
expect(data).to.have.property('meta');
expect(data.meta).to.have.property('redirect');
expect(data.meta.redirect).to.be.equal('https://www.google.com');
});
});
it('Default Site html', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'html',
meta: {
html: '<p>hello world</p>'
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('html');
expect(data).to.have.property('meta');
expect(data.meta).to.have.property('html');
expect(data.meta.html).to.be.equal('<p>hello world</p>');
});
});
});

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" /> /// <reference types="cypress" />
describe('Users endpoints', () => { describe('Users endpoints', () => {
let token; let token;

View File

@ -7,7 +7,7 @@ const BackendApi = function(config, token) {
this.axios = axios.create({ this.axios = axios.create({
baseURL: config.baseUrl, baseURL: config.baseUrl,
timeout: 5000, timeout: 90000,
}); });
}; };
@ -80,7 +80,7 @@ BackendApi.prototype._handleError = function(err, resolve, reject, returnOnError
* @returns {Promise<object>} * @returns {Promise<object>}
*/ */
BackendApi.prototype.request = function (method, path, returnOnError, data) { BackendApi.prototype.request = function (method, path, returnOnError, data) {
logger(method.toUpperCase(), this.config.baseUrl + path); logger(method.toUpperCase(), path);
const options = this._prepareOptions(returnOnError); const options = this._prepareOptions(returnOnError);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -1,4 +1,4 @@
const {SwaggerValidation} = require('@jc21/cypress-swagger-validation'); const { SwaggerValidation } = require('@jc21/cypress-swagger-validation');
module.exports = (on, config) => { module.exports = (on, config) => {
// Replace swaggerBase config var wildcard // Replace swaggerBase config var wildcard

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@jc21/cypress-swagger-validation": "^0.2.7", "@jc21/cypress-swagger-validation": "^0.3.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"cypress": "^13.15.0", "cypress": "^13.15.0",
"cypress-multi-reporters": "^1.6.4", "cypress-multi-reporters": "^1.6.4",
@ -19,8 +19,8 @@
"mocha-junit-reporter": "^2.2.1" "mocha-junit-reporter": "^2.2.1"
}, },
"scripts": { "scripts": {
"cypress": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/dev.js", "cypress": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/ci.js",
"cypress:headless": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/dev.js" "cypress:headless": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/ci.js"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC"

View File

@ -11,14 +11,13 @@
call-me-maybe "^1.0.1" call-me-maybe "^1.0.1"
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@apidevtools/json-schema-ref-parser@9.0.9": "@apidevtools/json-schema-ref-parser@^11.7.2":
version "9.0.9" version "11.7.2"
resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz#cdf3e0aded21492364a70e193b45b7cf4177f031"
integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== integrity sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==
dependencies: dependencies:
"@jsdevtools/ono" "^7.1.3" "@jsdevtools/ono" "^7.1.3"
"@types/json-schema" "^7.0.6" "@types/json-schema" "^7.0.15"
call-me-maybe "^1.0.1"
js-yaml "^4.1.0" js-yaml "^4.1.0"
"@apidevtools/openapi-schemas@^2.1.0": "@apidevtools/openapi-schemas@^2.1.0":
@ -167,15 +166,16 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
"@jc21/cypress-swagger-validation@^0.2.7": "@jc21/cypress-swagger-validation@^0.3.1":
version "0.2.7" version "0.3.1"
resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.2.7.tgz#64642b12d98b884df8c30b72852162941285d2af" resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.3.1.tgz#1cdd49850a20f876ed62149623f99988264751be"
integrity sha512-4EQ0gfigRwVVl3DnVYbR48/EKGnn7oH5YYdMzf6zqypO+bqYvDHu9kgk/WqkGlT/aauGQ7e0YGMo8ZvR7mL0Ng== integrity sha512-Vdt1gLfj8p0tJhA42Cfn43XBbsKocNfVCEVSwkn7RmZgWUyRKjqhBBRTVa9cKZTozyg8Co/yhBMsNyjmHFVXtQ==
dependencies: dependencies:
"@apidevtools/json-schema-ref-parser" "^11.7.2"
"@apidevtools/swagger-parser" "^10.1.0" "@apidevtools/swagger-parser" "^10.1.0"
ajv "^8.17.1" ajv "^8.17.1"
axios "^1.7.7"
json-schema "^0.4.0" json-schema "^0.4.0"
json-schema-ref-parser "^9.0.9"
jsonpath "^1.1.1" jsonpath "^1.1.1"
lodash "^4.17.21" lodash "^4.17.21"
openapi-types "^12.1.3" openapi-types "^12.1.3"
@ -196,7 +196,7 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.6": "@types/json-schema@^7.0.15":
version "7.0.15" version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@ -1468,13 +1468,6 @@ json-buffer@3.0.1:
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json-schema-ref-parser@^9.0.9:
version "9.0.9"
resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f"
integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==
dependencies:
"@apidevtools/json-schema-ref-parser" "9.0.9"
json-schema-traverse@^0.4.1: json-schema-traverse@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"