Merge pull request #1 from jc21/master

update
This commit is contained in:
techc0de 2021-02-20 19:43:13 -05:00 committed by GitHub
commit f305e001e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 6844 additions and 4886 deletions

View file

@ -7,6 +7,11 @@ assignees: ''
--- ---
**Are you in the right place?**
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*
**Checklist** **Checklist**
- Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image? - Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image?
- Are you sure you're not using someone else's docker image? - Are you sure you're not using someone else's docker image?

View file

@ -7,6 +7,11 @@ assignees: ''
--- ---
**Are you in the right place?**
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you have a feature request for NPM then you are in the *right place.*
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View file

@ -1,16 +0,0 @@
---
name: Product Support
about: Need help configuring the software?
title: ''
labels: product-support
assignees: ''
---
**Checklist**
- Please read the [setup instructions](https://nginxproxymanager.com/setup/)
- Please read the [FAQ](https://nginxproxymanager.com/faq/)
**What is troubling you?**
_Clear and concise description of what you're trying to do and what isn't working for you_

View file

@ -1,10 +0,0 @@
{
"database": {
"engine": "mysql",
"host": "db",
"name": "npm",
"user": "npm",
"password": "npm",
"port": 3306
}
}

View file

@ -1 +1 @@
2.3.1 2.8.0

42
Jenkinsfile vendored
View file

@ -65,6 +65,7 @@ pipeline {
// See: https://github.com/yarnpkg/yarn/issues/3254 // See: https://github.com/yarnpkg/yarn/issues/3254
sh '''docker run --rm \\ sh '''docker run --rm \\
-v "$(pwd)/backend:/app" \\ -v "$(pwd)/backend:/app" \\
-v "$(pwd)/global:/app/global" \\
-w /app \\ -w /app \\
node:latest \\ node:latest \\
sh -c "yarn install && yarn eslint . && rm -rf node_modules" sh -c "yarn install && yarn eslint . && rm -rf node_modules"
@ -83,23 +84,49 @@ pipeline {
''' '''
} }
} }
stage('Test') { stage('Integration Tests Sqlite') {
steps { steps {
// Bring up a stack // Bring up a stack
sh 'docker-compose up -d fullstack' sh 'docker-compose up -d fullstack-sqlite'
sh './scripts/wait-healthy $(docker-compose ps -q fullstack) 120' sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120'
// Run tests // Run tests
sh 'rm -rf test/results' sh 'rm -rf test/results'
sh 'docker-compose up cypress' sh 'docker-compose up cypress-sqlite'
// Get results // Get results
sh 'docker cp -L "$(docker-compose ps -q cypress):/results" test/' sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/'
} }
post { post {
always { always {
// Dumps to analyze later // Dumps to analyze later
sh 'mkdir -p debug' sh 'mkdir -p debug'
sh 'docker-compose logs fullstack | gzip > debug/docker_fullstack.log.gz' sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz'
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
// Cypress videos and screenshot artifacts
dir(path: 'test/results') {
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
}
junit 'test/results/junit/*'
}
}
}
stage('Integration Tests Mysql') {
steps {
// Bring up a stack
sh 'docker-compose up -d fullstack-mysql'
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120'
// Run tests
sh 'rm -rf test/results'
sh 'docker-compose up cypress-mysql'
// Get results
sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug'
sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz'
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
// Cypress videos and screenshot artifacts // Cypress videos and screenshot artifacts
dir(path: 'test/results') { dir(path: 'test/results') {
@ -136,8 +163,9 @@ pipeline {
} }
steps { steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
// Docker Login
sh "docker login -u '${duser}' -p '${dpass}'" sh "docker login -u '${duser}' -p '${dpass}'"
// Buildx with push // Buildx with push from cache
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}"
} }
} }

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.3.1-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.8.0-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>
@ -155,14 +155,106 @@ Special thanks to the following contributors:
<br /><sub><b>OhHeyAlan</b></sub> <br /><sub><b>OhHeyAlan</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/dogmatic69"> <a href="https://github.com/dogmatic69">
<img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80px;" alt=""/> <img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80px;" alt=""/>
<br /><sub><b>Carl Sutton</b></sub> <br /><sub><b>Carl Sutton</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/tg44">
<img src="https://avatars0.githubusercontent.com/u/31839?s=460&u=ad32f4cadfef5e5fb09cdfa4b7b7b36a99ba6811&v=4" width="80px;" alt=""/>
<br /><sub><b>Gergő Törcsvári</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/vrenjith">
<img src="https://avatars3.githubusercontent.com/u/2093241?s=460&u=96ce93a9bebabdd0a60a2dc96cd093a41d5edaba&v=4" width="80px;" alt=""/>
<br /><sub><b>vrenjith</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/duhruh">
<img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80px;" alt=""/>
<br /><sub><b>David Rivera</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jipjan">
<img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Jaap-Jan de Wit</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jmwebslave">
<img src="https://avatars2.githubusercontent.com/u/6118262?s=460&u=7db409c47135b1e141c366bbb03ed9fae6ac2638&v=4" width="80px;" alt=""/>
<br /><sub><b>James Morgan</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/chaptergy">
<img src="https://avatars2.githubusercontent.com/u/26956711?s=460&u=7d9adebabb6b4e7af7cb05d98d751087a372304b&v=4" width="80px;" alt=""/>
<br /><sub><b>chaptergy</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Philip-Mooney">
<img src="https://avatars0.githubusercontent.com/u/48624631?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Philip Mooney</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/WaterCalm">
<img src="https://avatars1.githubusercontent.com/u/23502129?s=400&v=4" width="80px;" alt=""/>
<br /><sub><b>WaterCalm</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lebrou34">
<img src="https://avatars1.githubusercontent.com/u/16373103?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>lebrou34</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lightglitch">
<img src="https://avatars0.githubusercontent.com/u/196953?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Mário Franco</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/klutchell">
<img src="https://avatars3.githubusercontent.com/u/20458272?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Kyle Harding</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ahgraber">
<img src="https://avatars.githubusercontent.com/u/24922003?s=460&u=8376c9f00af9b6057ba4d2fb03b4f1b20a75277f&v=4" width="80px;" alt=""/>
<br /><sub><b>Alex Graber</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/MooBaloo">
<img src="https://avatars.githubusercontent.com/u/9493496?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>MooBaloo</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Shuro">
<img src="https://avatars.githubusercontent.com/u/944030?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Shuro</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lorisbergeron">
<img src="https://avatars.githubusercontent.com/u/51918567?s=460&u=778e4ff284b7d7304450f98421c99f79298371fb&v=4" width="80px;" alt=""/>
<br /><sub><b>Loris Bergeron</b></sub>
</a>
</td>
</tr> </tr>
</table> </table>
<!-- markdownlint-enable --> <!-- markdownlint-enable -->

View file

@ -66,7 +66,7 @@ app.use(function (err, req, res, next) {
} }
}; };
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
payload.debug = { payload.debug = {
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null, stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous previous: err.previous

View file

@ -0,0 +1,26 @@
{
"database": {
"engine": "knex-native",
"knex": {
"client": "sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},
"pool": {
"min": 0,
"max": 1,
"createTimeoutMillis": 3000,
"acquireTimeoutMillis": 30000,
"idleTimeoutMillis": 30000,
"reapIntervalMillis": 1000,
"createRetryIntervalMillis": 100,
"propagateCreateError": false
},
"migrations": {
"tableName": "migrations",
"stub": "src/backend/lib/migrate_template.js",
"directory": "src/backend/migrations"
}
}
}
}

View file

@ -4,19 +4,27 @@ if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md'); throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md');
} }
let data = { function generateDbConfig() {
client: config.database.engine, if (config.database.engine === 'knex-native') {
connection: { return config.database.knex;
host: config.database.host, } else
user: config.database.user, return {
password: config.database.password, client: config.database.engine,
database: config.database.name, connection: {
port: config.database.port host: config.database.host,
}, user: config.database.user,
migrations: { password: config.database.password,
tableName: 'migrations' database: config.database.name,
} port: config.database.port
}; },
migrations: {
tableName: 'migrations'
}
};
}
let data = generateDbConfig();
if (typeof config.database.version !== 'undefined') { if (typeof config.database.version !== 'undefined') {
data.version = config.database.version; data.version = config.database.version;

View file

@ -2,7 +2,10 @@
const logger = require('./logger').global; const logger = require('./logger').global;
function appStart () { async function appStart () {
// Create config file db settings if environment variables have been set
await createDbConfigFromEnvironment();
const migrate = require('./migrate'); const migrate = require('./migrate');
const setup = require('./setup'); const setup = require('./setup');
const app = require('./app'); const app = require('./app');
@ -39,9 +42,92 @@ function appStart () {
}); });
} }
async function createDbConfigFromEnvironment() {
return new Promise((resolve, reject) => {
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlPort = process.env.DB_MYSQL_PORT || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envSqliteFile = process.env.DB_SQLITE_FILE || null;
if ((envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) || envSqliteFile) {
const fs = require('fs');
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
let configData = {};
try {
configData = require(filename);
} catch (err) {
// do nothing
}
if (configData.database && configData.database.engine && !configData.database.fromEnv) {
logger.info('Manual db configuration already exists, skipping config creation from environment variables');
resolve();
return;
}
if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) {
const newConfig = {
fromEnv: true,
engine: 'mysql',
host: envMysqlHost,
port: envMysqlPort,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating MySQL db configuration from environment variables');
configData.database = newConfig;
} else {
const newConfig = {
fromEnv: true,
engine: 'knex-native',
knex: {
client: 'sqlite3',
connection: {
filename: envSqliteFile
}
}
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating Sqlite db configuration from environment variables');
configData.database = newConfig;
}
// Write config
fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => {
if (err) {
logger.error('Could not write db config to config file: ' + filename);
reject(err);
} else {
logger.info('Wrote db configuration to config file: ' + filename);
resolve();
}
});
} else {
resolve();
}
});
}
try { try {
appStart(); appStart();
} catch (err) { } catch (err) {
logger.error(err.message, err); logger.error(err.message, err);
process.exit(1); process.exit(1);
} }

View file

@ -31,6 +31,7 @@ const internalAccessList = {
.insertAndFetch({ .insertAndFetch({
name: data.name, name: data.name,
satisfy_any: data.satisfy_any, satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1) owner_user_id: access.token.getUserId(1)
}); });
}) })
@ -128,6 +129,7 @@ const internalAccessList = {
.patch({ .patch({
name: data.name, name: data.name,
satisfy_any: data.satisfy_any, satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
}); });
} }
}) })
@ -384,7 +386,7 @@ const internalAccessList = {
.orderBy('access_list.name', 'ASC'); .orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching

View file

@ -13,6 +13,7 @@ const internalNginx = require('./nginx');
const internalHost = require('./host'); const internalHost = require('./host');
const certbot_command = '/usr/bin/certbot'; const certbot_command = '/usr/bin/certbot';
const le_config = '/etc/letsencrypt.ini'; const le_config = '/etc/letsencrypt.ini';
const dns_plugins = require('../global/certbot-dns-plugins');
function omissions() { function omissions() {
return ['is_deleted']; return ['is_deleted'];
@ -77,7 +78,7 @@ const internalCertificate = {
.where('id', certificate.id) .where('id', certificate.id)
.andWhere('provider', 'letsencrypt') .andWhere('provider', 'letsencrypt')
.patch({ .patch({
expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
}); });
}) })
.catch((err) => { .catch((err) => {
@ -141,36 +142,60 @@ const internalCertificate = {
}); });
}) })
.then((in_use_result) => { .then((in_use_result) => {
// 3. Generate the LE config // With DNS challenge no config is needed, so skip 3 and 5.
return internalNginx.generateLetsEncryptRequestConfig(certificate) if (certificate.meta.dns_challenge) {
.then(internalNginx.reload) return internalNginx.reload().then(() => {
.then(() => {
// 4. Request cert // 4. Request cert
return internalCertificate.requestLetsEncryptSsl(certificate); return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate);
}) })
.then(() => { .then(internalNginx.reload)
// 5. Remove LE config .then(() => {
return internalNginx.deleteLetsEncryptRequestConfig(certificate); // 6. Re-instate previously disabled hosts
}) return internalCertificate.enableInUseHosts(in_use_result);
.then(internalNginx.reload) })
.then(() => { .then(() => {
// 6. Re-instate previously disabled hosts return certificate;
return internalCertificate.enableInUseHosts(in_use_result); })
}) .catch((err) => {
.then(() => { // In the event of failure, revert things and throw err back
return certificate; return internalCertificate.enableInUseHosts(in_use_result)
}) .then(internalNginx.reload)
.catch((err) => { .then(() => {
// In the event of failure, revert things and throw err back throw err;
return internalNginx.deleteLetsEncryptRequestConfig(certificate) });
.then(() => { });
return internalCertificate.enableInUseHosts(in_use_result); } else {
}) // 3. Generate the LE config
.then(internalNginx.reload) return internalNginx.generateLetsEncryptRequestConfig(certificate)
.then(() => { .then(internalNginx.reload)
throw err; .then(() => {
}); // 4. Request cert
}); return internalCertificate.requestLetsEncryptSsl(certificate);
})
.then(() => {
// 5. Remove LE config
return internalNginx.deleteLetsEncryptRequestConfig(certificate);
})
.then(internalNginx.reload)
.then(() => {
// 6. Re-instate previously disabled hosts
return internalCertificate.enableInUseHosts(in_use_result);
})
.then(() => {
return certificate;
})
.catch((err) => {
// In the event of failure, revert things and throw err back
return internalNginx.deleteLetsEncryptRequestConfig(certificate)
.then(() => {
return internalCertificate.enableInUseHosts(in_use_result);
})
.then(internalNginx.reload)
.then(() => {
throw err;
});
});
}
}) })
.then(() => { .then(() => {
// At this point, the letsencrypt cert should exist on disk. // At this point, the letsencrypt cert should exist on disk.
@ -180,7 +205,7 @@ const internalCertificate = {
return certificateModel return certificateModel
.query() .query()
.patchAndFetchById(certificate.id, { .patchAndFetchById(certificate.id, {
expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
}) })
.then((saved_row) => { .then((saved_row) => {
// Add cert data for audit log // Add cert data for audit log
@ -191,6 +216,13 @@ const internalCertificate = {
return saved_row; return saved_row;
}); });
}); });
}).catch(async (error) => {
// Delete the certificate from the database if it was not created successfully
await certificateModel
.query()
.deleteById(certificate.id);
throw error;
}); });
} else { } else {
return certificate; return certificate;
@ -558,7 +590,7 @@ const internalCertificate = {
// TODO: This uses a mysql only raw function that won't translate to postgres // TODO: This uses a mysql only raw function that won't translate to postgres
return internalCertificate.update(access, { return internalCertificate.update(access, {
id: data.id, id: data.id,
expires_on: certificateModel.raw('FROM_UNIXTIME(' + validations.certificate.dates.to + ')'), expires_on: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'),
domain_names: [validations.certificate.cn], domain_names: [validations.certificate.cn],
meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later
}) })
@ -583,18 +615,26 @@ const internalCertificate = {
checkPrivateKey: (private_key) => { checkPrivateKey: (private_key) => {
return tempWrite(private_key, '/tmp') return tempWrite(private_key, '/tmp')
.then((filepath) => { .then((filepath) => {
return utils.exec('openssl rsa -in ' + filepath + ' -check -noout') return new Promise((resolve, reject) => {
.then((result) => { const failTimeout = setTimeout(() => {
if (!result.toLowerCase().includes('key ok')) { reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.'));
throw new error.ValidationError(result); }, 10000);
} utils
.exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ')
fs.unlinkSync(filepath); .then((result) => {
return true; clearTimeout(failTimeout);
}).catch((err) => { if (!result.toLowerCase().includes('key is valid')) {
fs.unlinkSync(filepath); reject(new error.ValidationError('Result Validation Error: ' + result));
throw new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err); }
}); fs.unlinkSync(filepath);
resolve(true);
})
.catch((err) => {
clearTimeout(failTimeout);
fs.unlinkSync(filepath);
reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err));
});
});
}); });
}, },
@ -733,7 +773,6 @@ const internalCertificate = {
'--agree-tos ' + '--agree-tos ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' + '--email "' + certificate.meta.letsencrypt_email + '" ' +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--webroot ' +
'--domains "' + certificate.domain_names.join(',') + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' +
(le_staging ? '--staging' : ''); (le_staging ? '--staging' : '');
@ -748,6 +787,76 @@ const internalCertificate = {
}); });
}, },
/**
* @param {Object} certificate the certificate row
* @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`)
* @param {String | null} credentials the content of this providers credentials file
* @param {String} propagation_seconds the cloudflare api token
* @returns {Promise}
*/
requestLetsEncryptSslWithDnsChallenge: (certificate) => {
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
if (!dns_plugin) {
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
}
logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
const credentials_cmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'';
const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version + ' ' + dns_plugin.dependencies;
// Whether the plugin has a --<name>-credentials argument
const has_config_arg = certificate.meta.dns_provider !== 'route53';
let main_cmd =
certbot_command + ' certonly --non-interactive ' +
'--cert-name "npm-' + certificate.id + '" ' +
'--agree-tos ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' +
'--domains "' + certificate.domain_names.join(',') + '" ' +
'--authenticator ' + dns_plugin.full_plugin_name + ' ' +
(
has_config_arg
? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentials_loc + '"'
: ''
) +
(
certificate.meta.propagation_seconds !== undefined
? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
: ''
) +
(le_staging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
}
if (debug_mode) {
logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd}`);
}
return utils.exec(credentials_cmd)
.then(() => {
return utils.exec(prepare_cmd)
.then(() => {
return utils.exec(main_cmd)
.then(async (result) => {
logger.info(result);
return result;
});
});
}).catch(async (err) => {
// Don't fail if file does not exist
const delete_credentials_cmd = `rm -f '${credentials_loc}' || true`;
await utils.exec(delete_credentials_cmd);
throw err;
});
},
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
@ -761,7 +870,9 @@ const internalCertificate = {
}) })
.then((certificate) => { .then((certificate) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
return internalCertificate.renewLetsEncryptSsl(certificate) let renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl;
return renewMethod(certificate)
.then(() => { .then(() => {
return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem');
}) })
@ -769,7 +880,7 @@ const internalCertificate = {
return certificateModel return certificateModel
.query() .query()
.patchAndFetchById(certificate.id, { .patchAndFetchById(certificate.id, {
expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
}); });
}) })
.then((updated_certificate) => { .then((updated_certificate) => {
@ -815,6 +926,42 @@ const internalCertificate = {
}); });
}, },
/**
* @param {Object} certificate the certificate row
* @returns {Promise}
*/
renewLetsEncryptSslWithDnsChallenge: (certificate) => {
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
if (!dns_plugin) {
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
}
logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
let main_cmd =
certbot_command + ' renew --non-interactive ' +
'--cert-name "npm-' + certificate.id + '" ' +
'--disable-hook-validation' +
(le_staging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
}
if (debug_mode) {
logger.info('Command:', main_cmd);
}
return utils.exec(main_cmd)
.then(async (result) => {
logger.info(result);
return result;
});
},
/** /**
* @param {Object} certificate the certificate row * @param {Object} certificate the certificate row
* @param {Boolean} [throw_errors] * @param {Boolean} [throw_errors]
@ -823,21 +970,21 @@ const internalCertificate = {
revokeLetsEncryptSsl: (certificate, throw_errors) => { revokeLetsEncryptSsl: (certificate, throw_errors) => {
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(', '));
let cmd = certbot_command + ' revoke --non-interactive ' + const main_cmd = certbot_command + ' revoke --non-interactive ' +
'--config "' + le_config + '" ' +
'--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 ' +
(le_staging ? '--staging' : ''); (le_staging ? '--staging' : '');
// Don't fail command if file does not exist
const delete_credentials_cmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
if (debug_mode) { if (debug_mode) {
logger.info('Command:', cmd); logger.info('Command:', main_cmd + '; ' + delete_credentials_cmd);
} }
return utils.exec(cmd) return utils.exec(main_cmd)
.then((result) => { .then(async (result) => {
if (debug_mode) { await utils.exec(delete_credentials_cmd);
logger.info('Command:', cmd);
}
logger.info(result); logger.info(result);
return result; return result;
}) })

View file

@ -106,7 +106,7 @@ const internalHost = {
response_object.total_count += response_object.redirection_hosts.length; response_object.total_count += response_object.redirection_hosts.length;
} }
if (promises_results[1]) { if (promises_results[2]) {
// Dead Hosts // Dead Hosts
response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names); response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names);
response_object.total_count += response_object.dead_hosts.length; response_object.total_count += response_object.dead_hosts.length;
@ -158,7 +158,7 @@ const internalHost = {
} }
} }
if (promises_results[1]) { if (promises_results[2]) {
// Dead Hosts // Dead Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) { if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
is_taken = true; is_taken = true;

View file

@ -22,22 +22,6 @@ exports.up = function (knex/*, Promise*/) {
}) })
.then(() => { .then(() => {
logger.info('[' + migrate_name + '] setting Table created'); logger.info('[' + migrate_name + '] setting Table created');
// TODO: add settings
let settingModel = require('../models/setting');
return settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {}
});
})
.then(() => {
logger.info('[' + migrate_name + '] Default settings added');
}); });
}; };

View file

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

View file

@ -6,13 +6,14 @@ const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const AccessListAuth = require('./access_list_auth'); const AccessListAuth = require('./access_list_auth');
const AccessListClient = require('./access_list_client'); const AccessListClient = require('./access_list_client');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AccessList extends Model { class AccessList extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -21,7 +22,7 @@ class AccessList extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {
@ -92,6 +93,10 @@ class AccessList extends Model {
get satisfy() { get satisfy() {
return this.satisfy_any ? 'satisfy any' : 'satisfy all'; return this.satisfy_any ? 'satisfy any' : 'satisfy all';
} }
get passauth() {
return this.pass_auth ? '' : 'proxy_set_header Authorization "";';
}
} }
module.exports = AccessList; module.exports = AccessList;

View file

@ -3,13 +3,14 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AccessListAuth extends Model { class AccessListAuth extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -18,7 +19,7 @@ class AccessListAuth extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -3,13 +3,14 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AccessListClient extends Model { class AccessListClient extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -18,7 +19,7 @@ class AccessListClient extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -4,13 +4,14 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AuditLog extends Model { class AuditLog extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -19,7 +20,7 @@ class AuditLog extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -5,6 +5,7 @@ const bcrypt = require('bcrypt');
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
@ -24,8 +25,8 @@ function encryptPassword () {
class Auth extends Model { class Auth extends Model {
$beforeInsert (queryContext) { $beforeInsert (queryContext) {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -36,7 +37,7 @@ class Auth extends Model {
} }
$beforeUpdate (queryContext) { $beforeUpdate (queryContext) {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
return encryptPassword.apply(this, queryContext); return encryptPassword.apply(this, queryContext);
} }

View file

@ -4,17 +4,18 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class Certificate extends Model { class Certificate extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for expires_on // Default for expires_on
if (typeof this.expires_on === 'undefined') { if (typeof this.expires_on === 'undefined') {
this.expires_on = Model.raw('NOW()'); this.expires_on = now();
} }
// Default for domain_names // Default for domain_names
@ -31,7 +32,7 @@ class Certificate extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View file

@ -5,13 +5,14 @@ const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const Certificate = require('./certificate'); const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class DeadHost extends Model { class DeadHost extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -27,7 +28,7 @@ class DeadHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View file

@ -0,0 +1,13 @@
const db = require('../db');
const config = require('config');
const Model = require('objection').Model;
Model.knex(db);
module.exports = function () {
if (config.database.knex && config.database.knex.client === 'sqlite3') {
return Model.raw('datetime(\'now\',\'localtime\')');
} else {
return Model.raw('NOW()');
}
};

View file

@ -6,13 +6,14 @@ const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const AccessList = require('./access_list'); const AccessList = require('./access_list');
const Certificate = require('./certificate'); const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class ProxyHost extends Model { class ProxyHost extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -28,7 +29,7 @@ class ProxyHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View file

@ -5,13 +5,14 @@ const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const Certificate = require('./certificate'); const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class RedirectionHost extends Model { class RedirectionHost extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -27,7 +28,7 @@ class RedirectionHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View file

@ -4,13 +4,14 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class Stream extends Model { class Stream extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -19,7 +20,7 @@ class Stream extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -4,13 +4,14 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const UserPermission = require('./user_permission'); const UserPermission = require('./user_permission');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class User extends Model { class User extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for roles // Default for roles
if (typeof this.roles === 'undefined') { if (typeof this.roles === 'undefined') {
@ -19,7 +20,7 @@ class User extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -3,17 +3,18 @@
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class UserPermission extends Model { class UserPermission extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

@ -6,28 +6,30 @@
"dependencies": { "dependencies": {
"ajv": "^6.12.0", "ajv": "^6.12.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^4.0.1", "bcrypt": "^5.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.3.1", "config": "^3.3.1",
"diskdb": "^0.1.17", "diskdb": "^0.1.17",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.6", "express-fileupload": "^1.1.9",
"gravatar": "^1.8.0", "gravatar": "^1.8.0",
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"json-schema-ref-parser": "^8.0.0", "json-schema-ref-parser": "^8.0.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^0.20.13", "knex": "^0.20.13",
"liquidjs": "^9.11.10", "liquidjs": "^9.11.10",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"moment": "^2.24.0", "moment": "^2.24.0",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.8",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"objection": "^2.1.3", "objection": "^2.1.3",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^7.12.1",
"restler": "^3.4.0", "restler": "^3.4.0",
"signale": "^1.4.0", "signale": "^1.4.0",
"sqlite3": "^4.1.1",
"temp-write": "^4.0.0", "temp-write": "^4.0.0",
"unix-timestamp": "^0.2.0" "unix-timestamp": "^0.2.0"
}, },

View file

@ -58,6 +58,7 @@ router
.post((req, res, next) => { .post((req, res, next) => {
apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body) apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body)
.then((payload) => { .then((payload) => {
req.setTimeout(900000); // 15 minutes timeout
return internalCertificate.create(res.locals.access, payload); return internalCertificate.create(res.locals.access, payload);
}) })
.then((result) => { .then((result) => {
@ -197,6 +198,7 @@ router
* Renew certificate * Renew certificate
*/ */
.post((req, res, next) => { .post((req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
internalCertificate.renew(res.locals.access, { internalCertificate.renew(res.locals.access, {
id: parseInt(req.params.certificate_id, 10) id: parseInt(req.params.certificate_id, 10)
}) })

View file

@ -42,6 +42,9 @@
"satisfy_any": { "satisfy_any": {
"type": "boolean" "type": "boolean"
}, },
"pass_auth": {
"type": "boolean"
},
"meta": { "meta": {
"type": "object" "type": "object"
} }
@ -102,6 +105,9 @@
"satisfy_any": { "satisfy_any": {
"$ref": "#/definitions/satisfy_any" "$ref": "#/definitions/satisfy_any"
}, },
"pass_auth": {
"$ref": "#/definitions/pass_auth"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 0, "minItems": 0,
@ -167,6 +173,9 @@
"satisfy_any": { "satisfy_any": {
"$ref": "#/definitions/satisfy_any" "$ref": "#/definitions/satisfy_any"
}, },
"pass_auth": {
"$ref": "#/definitions/pass_auth"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 0, "minItems": 0,

View file

@ -41,6 +41,24 @@
}, },
"letsencrypt_agree": { "letsencrypt_agree": {
"type": "boolean" "type": "boolean"
},
"dns_challenge": {
"type": "boolean"
},
"dns_provider": {
"type": "string"
},
"dns_provider_credentials": {
"type": "string"
},
"propagation_seconds": {
"anyOf": [
{
"type": "integer",
"minimum": 0
}
]
} }
} }
} }

View file

@ -2,12 +2,21 @@ const fs = require('fs');
const NodeRSA = require('node-rsa'); const NodeRSA = require('node-rsa');
const config = require('config'); const config = require('config');
const logger = require('./logger').setup; const logger = require('./logger').setup;
const certificateModel = require('./models/certificate');
const userModel = require('./models/user'); const userModel = require('./models/user');
const userPermissionModel = require('./models/user_permission'); const userPermissionModel = require('./models/user_permission');
const utils = require('./lib/utils');
const authModel = require('./models/auth'); const authModel = require('./models/auth');
const settingModel = require('./models/setting');
const dns_plugins = require('./global/certbot-dns-plugins');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
module.exports = function () { /**
* Creates a new JWT RSA Keypair if not alread set on the config
*
* @returns {Promise}
*/
const setupJwt = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Now go and check if the jwt gpg keys have been created and if not, create them // Now go and check if the jwt gpg keys have been created and if not, create them
if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) { if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) {
@ -27,12 +36,12 @@ module.exports = function () {
} }
// Now create the keys and save them in the config. // Now create the keys and save them in the config.
let key = new NodeRSA({b: 2048}); let key = new NodeRSA({ b: 2048 });
key.generateKeyPair(); key.generateKeyPair();
config_data.jwt = { config_data.jwt = {
key: key.exportKey('private').toString(), key: key.exportKey('private').toString(),
pub: key.exportKey('public').toString() pub: key.exportKey('public').toString(),
}; };
// Write config // Write config
@ -47,7 +56,6 @@ module.exports = function () {
process.exit(0); process.exit(0);
} }
}); });
} else { } else {
// JWT key pair exists // JWT key pair exists
if (debug_mode) { if (debug_mode) {
@ -56,14 +64,20 @@ module.exports = function () {
resolve(); resolve();
} }
}) });
.then(() => { };
return userModel
.query() /**
.select(userModel.raw('COUNT(`id`) as `count`')) * Creates a default admin users if one doesn't already exist in the database
.where('is_deleted', 0) *
.first(); * @returns {Promise}
}) */
const setupDefaultUser = () => {
return userModel
.query()
.select(userModel.raw('COUNT(`id`) as `count`'))
.where('is_deleted', 0)
.first()
.then((row) => { .then((row) => {
if (!row.count) { if (!row.count) {
// Create a new user and set password // Create a new user and set password
@ -75,7 +89,7 @@ module.exports = function () {
name: 'Administrator', name: 'Administrator',
nickname: 'Admin', nickname: 'Admin',
avatar: '', avatar: '',
roles: ['admin'] roles: ['admin'],
}; };
return userModel return userModel
@ -88,28 +102,109 @@ module.exports = function () {
user_id: user.id, user_id: user.id,
type: 'password', type: 'password',
secret: 'changeme', secret: 'changeme',
meta: {} meta: {},
}) })
.then(() => { .then(() => {
return userPermissionModel return userPermissionModel.query().insert({
.query() user_id: user.id,
.insert({ visibility: 'all',
user_id: user.id, proxy_hosts: 'manage',
visibility: 'all', redirection_hosts: 'manage',
proxy_hosts: 'manage', dead_hosts: 'manage',
redirection_hosts: 'manage', streams: 'manage',
dead_hosts: 'manage', access_lists: 'manage',
streams: 'manage', certificates: 'manage',
access_lists: 'manage', });
certificates: 'manage'
});
}); });
}) })
.then(() => { .then(() => {
logger.info('Initial setup completed'); logger.info('Initial admin setup completed');
}); });
} else if (debug_mode) { } else if (debug_mode) {
logger.debug('Admin user setup not required'); logger.debug('Admin user setup not required');
} }
}); });
}; };
/**
* Creates default settings if they don't already exist in the database
*
* @returns {Promise}
*/
const setupDefaultSettings = () => {
return settingModel
.query()
.select(settingModel.raw('COUNT(`id`) as `count`'))
.where({id: 'default-site'})
.first()
.then((row) => {
if (!row.count) {
settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {},
})
.then(() => {
logger.info('Default settings added');
});
}
if (debug_mode) {
logger.debug('Default setting setup not required');
}
});
};
/**
* Installs all Certbot plugins which are required for an installed certificate
*
* @returns {Promise}
*/
const setupCertbotPlugins = () => {
return certificateModel
.query()
.where('is_deleted', 0)
.andWhere('provider', 'letsencrypt')
.then((certificates) => {
if (certificates && certificates.length) {
let plugins = [];
let promises = [];
certificates.map(function (certificate) {
if (certificate.meta && certificate.meta.dns_challenge === true) {
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
const packages_to_install = `${dns_plugin.package_name}==${dns_plugin.package_version} ${dns_plugin.dependencies}`;
if (plugins.indexOf(packages_to_install) === -1) plugins.push(packages_to_install);
// Make sure credentials file exists
const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }';
promises.push(utils.exec(credentials_cmd));
}
});
if (plugins.length) {
const install_cmd = 'pip3 install ' + plugins.join(' ');
promises.push(utils.exec(install_cmd));
}
if (promises.length) {
return Promise.all(promises)
.then(() => {
logger.info('Added Certbot plugins ' + plugins.join(', '));
});
}
}
});
};
module.exports = function () {
return setupJwt()
.then(setupDefaultUser)
.then(setupDefaultSettings)
.then(setupCertbotPlugins);
};

View file

@ -6,6 +6,11 @@
{%- else %} {%- else %}
server { server {
listen 80 default; listen 80 default;
{% if ipv6 -%}
listen [::]:80;
{% else -%}
#listen [::]:80;
{% endif %}
server_name default-host.localhost; server_name default-host.localhost;
access_log /data/logs/default_host.log combined; access_log /data/logs/default_host.log combined;
{% include "_exploits.conf" %} {% include "_exploits.conf" %}

View file

@ -27,6 +27,8 @@ server {
# Authorization # Authorization
auth_basic "Authorization required"; auth_basic "Authorization required";
auth_basic_user_file /data/access/{{ access_list_id }}; auth_basic_user_file /data/access/{{ access_list_id }};
{{ access_list.passauth }}
{% endif %} {% endif %}
# Access Rules # Access Rules
@ -35,7 +37,9 @@ server {
{% endfor %}deny all; {% endfor %}deny all;
# Access checks must... # Access checks must...
{% if access_list.satisfy %}
{{ access_list.satisfy }}; {{ access_list.satisfy }};
{% endif %}
{% endif %} {% endif %}

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,13 @@ ARG BUILD_DATE
ENV SUPPRESS_NO_CONFIG_WARNING=1 ENV SUPPRESS_NO_CONFIG_WARNING=1
ENV S6_FIX_ATTRS_HIDDEN=1 ENV S6_FIX_ATTRS_HIDDEN=1
ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=1
ENV NODE_ENV=production ENV NODE_ENV=production
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apk update \ && apk update \
&& apk add python2 certbot jq \ && apk add python3 certbot jq \
&& python3 -m ensurepip \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}" ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}"
@ -30,17 +32,20 @@ EXPOSE 80
EXPOSE 81 EXPOSE 81
EXPOSE 443 EXPOSE 443
COPY docker/rootfs /
ADD backend /app ADD backend /app
ADD frontend/dist /app/frontend ADD frontend/dist /app/frontend
COPY global /app/global
WORKDIR /app WORKDIR /app
RUN yarn install RUN yarn install
# add late to limit cache-busting by modifications
COPY docker/rootfs /
# 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/services.d/frontend RUN rm -f /etc/nginx/conf.d/dev.conf RUN rm -rf /etc/services.d/frontend RUN rm -f /etc/nginx/conf.d/dev.conf
VOLUME [ "/data", "/etc/letsencrypt" ] VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ] ENTRYPOINT [ "/init" ]
HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health

View file

@ -7,7 +7,8 @@ ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apk update \ && apk update \
&& apk add python2 certbot jq \ && apk add python3 certbot jq \
&& python3 -m ensurepip \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
# Task # Task
@ -26,6 +27,6 @@ EXPOSE 80
EXPOSE 81 EXPOSE 81
EXPOSE 443 EXPOSE 443
CMD [ "/init" ] ENTRYPOINT [ "/init" ]
HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health

View file

@ -2,14 +2,18 @@
version: "3" version: "3"
services: services:
fullstack: fullstack-mysql:
image: ${IMAGE}:ci-${BUILD_NUMBER} image: ${IMAGE}:ci-${BUILD_NUMBER}
environment: environment:
- NODE_ENV=development NODE_ENV: "development"
- FORCE_COLOR=1 FORCE_COLOR: 1
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
volumes: volumes:
- npm_data:/data - npm_data:/data
- ../.jenkins/config.json:/app/config/production.json
expose: expose:
- 81 - 81
- 80 - 80
@ -17,6 +21,19 @@ services:
depends_on: depends_on:
- db - db
fullstack-sqlite:
image: ${IMAGE}:ci-${BUILD_NUMBER}
environment:
NODE_ENV: "development"
FORCE_COLOR: 1
DB_SQLITE_FILE: "/data/database.sqlite"
volumes:
- npm_data:/data
expose:
- 81
- 80
- 443
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
environment: environment:
@ -27,13 +44,24 @@ services:
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
cypress: cypress-mysql:
image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} image: ${IMAGE}-cypress:ci-${BUILD_NUMBER}
build: build:
context: ../ context: ../test/
dockerfile: test/cypress/Dockerfile dockerfile: cypress/Dockerfile
environment: environment:
CYPRESS_baseUrl: "http://fullstack:81" CYPRESS_baseUrl: "http://fullstack-mysql:81"
volumes:
- cypress-logs:/results
command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json}
cypress-sqlite:
image: ${IMAGE}-cypress:ci-${BUILD_NUMBER}
build:
context: ../test/
dockerfile: cypress/Dockerfile
environment:
CYPRESS_baseUrl: "http://fullstack-sqlite:81"
volumes: volumes:
- cypress-logs:/results - cypress-logs:/results
command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json}

View file

@ -11,21 +11,33 @@ services:
- 3080:80 - 3080:80
- 3081:81 - 3081:81
- 3443:443 - 3443:443
networks:
- nginx_proxy_manager
environment: environment:
- NODE_ENV=development NODE_ENV: "development"
- FORCE_COLOR=1 FORCE_COLOR: 1
- DEVELOPMENT=true DEVELOPMENT: "true"
#- DISABLE_IPV6=true DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
# DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true"
volumes: volumes:
- npm_data:/data - npm_data:/data
- le_data:/etc/letsencrypt - le_data:/etc/letsencrypt
- ..:/app - ../backend:/app
- ../frontend:/app/frontend
- ../global:/app/global
depends_on: depends_on:
- db - db
working_dir: /app working_dir: /app
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
networks:
- nginx_proxy_manager
environment: environment:
MYSQL_ROOT_PASSWORD: "npm" MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: "npm" MYSQL_DATABASE: "npm"
@ -38,6 +50,8 @@ services:
image: 'swaggerapi/swagger-ui:latest' image: 'swaggerapi/swagger-ui:latest'
ports: ports:
- 3001:80 - 3001:80
networks:
- nginx_proxy_manager
environment: environment:
URL: "http://127.0.0.1:3081/api/schema" URL: "http://127.0.0.1:3081/api/schema"
PORT: '80' PORT: '80'
@ -48,3 +62,6 @@ volumes:
npm_data: npm_data:
le_data: le_data:
db_data: db_data:
networks:
nginx_proxy_manager:

View file

@ -1,2 +1,3 @@
* *
!.gitignore !.gitignore
!*.sh

View file

@ -0,0 +1,29 @@
#!/usr/bin/with-contenv bash
# ref: https://github.com/linuxserver/docker-baseimage-alpine/blob/master/root/etc/cont-init.d/01-envfile
# in s6, environmental variables are written as text files for s6 to monitor
# seach through full-path filenames for files ending in "__FILE"
for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do
echo "[secret-init] Evaluating ${FILENAME##*/} ..."
# set SECRETFILE to the contents of the full-path textfile
SECRETFILE=$(cat ${FILENAME})
# SECRETFILE=${FILENAME}
# echo "[secret-init] Set SECRETFILE to ${SECRETFILE}" # DEBUG - rm for prod!
# if SECRETFILE exists / is not null
if [[ -f ${SECRETFILE} ]]; then
# strip the appended "__FILE" from environmental variable name ...
STRIPFILE=$(echo ${FILENAME} | sed "s/__FILE//g")
# echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod!
# ... and set value to contents of secretfile
# since s6 uses text files, this is effectively "export ..."
printf $(cat ${SECRETFILE}) > ${STRIPFILE}
# echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!"
echo "[secret-init] Success! ${STRIPFILE##*/} set from ${FILENAME##*/}"
else
echo "[secret-init] cannot find secret in ${FILENAME}"
fi
done

View file

@ -17,6 +17,9 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:3000/; proxy_pass http://127.0.0.1:3000/;
proxy_read_timeout 15m;
proxy_send_timeout 15m;
} }
location / { location / {

View file

@ -1,196 +1,2 @@
# This should be left blank is it is populated programatically
set_real_ip_from 144.220.0.0/16; # by the application backend.
set_real_ip_from 52.124.128.0/17;
set_real_ip_from 54.230.0.0/16;
set_real_ip_from 54.239.128.0/18;
set_real_ip_from 52.82.128.0/19;
set_real_ip_from 99.84.0.0/16;
set_real_ip_from 204.246.172.0/24;
set_real_ip_from 205.251.192.0/19;
set_real_ip_from 54.239.192.0/19;
set_real_ip_from 70.132.0.0/18;
set_real_ip_from 13.32.0.0/15;
set_real_ip_from 13.224.0.0/14;
set_real_ip_from 13.35.0.0/16;
set_real_ip_from 204.246.164.0/22;
set_real_ip_from 204.246.168.0/22;
set_real_ip_from 71.152.0.0/17;
set_real_ip_from 216.137.32.0/19;
set_real_ip_from 205.251.249.0/24;
set_real_ip_from 99.86.0.0/16;
set_real_ip_from 52.46.0.0/18;
set_real_ip_from 52.84.0.0/15;
set_real_ip_from 204.246.173.0/24;
set_real_ip_from 130.176.0.0/16;
set_real_ip_from 64.252.64.0/18;
set_real_ip_from 204.246.174.0/23;
set_real_ip_from 64.252.128.0/18;
set_real_ip_from 205.251.254.0/24;
set_real_ip_from 143.204.0.0/16;
set_real_ip_from 205.251.252.0/23;
set_real_ip_from 204.246.176.0/20;
set_real_ip_from 13.249.0.0/16;
set_real_ip_from 54.240.128.0/18;
set_real_ip_from 205.251.250.0/23;
set_real_ip_from 52.222.128.0/17;
set_real_ip_from 54.182.0.0/16;
set_real_ip_from 54.192.0.0/16;
set_real_ip_from 13.124.199.0/24;
set_real_ip_from 34.226.14.0/24;
set_real_ip_from 52.15.127.128/26;
set_real_ip_from 35.158.136.0/24;
set_real_ip_from 52.57.254.0/24;
set_real_ip_from 18.216.170.128/25;
set_real_ip_from 13.52.204.0/23;
set_real_ip_from 13.54.63.128/26;
set_real_ip_from 13.59.250.0/26;
set_real_ip_from 13.210.67.128/26;
set_real_ip_from 35.167.191.128/26;
set_real_ip_from 52.47.139.0/24;
set_real_ip_from 52.199.127.192/26;
set_real_ip_from 52.212.248.0/26;
set_real_ip_from 52.66.194.128/26;
set_real_ip_from 13.113.203.0/24;
set_real_ip_from 99.79.168.0/23;
set_real_ip_from 34.195.252.0/24;
set_real_ip_from 35.162.63.192/26;
set_real_ip_from 34.223.12.224/27;
set_real_ip_from 52.56.127.0/25;
set_real_ip_from 34.223.80.192/26;
set_real_ip_from 13.228.69.0/24;
set_real_ip_from 34.216.51.0/25;
set_real_ip_from 3.231.2.0/25;
set_real_ip_from 54.233.255.128/26;
set_real_ip_from 18.200.212.0/23;
set_real_ip_from 52.52.191.128/26;
set_real_ip_from 3.234.232.224/27;
set_real_ip_from 52.78.247.128/26;
set_real_ip_from 52.220.191.0/26;
set_real_ip_from 34.232.163.208/29;
set_real_ip_from 2600:9000:eee::/48;
set_real_ip_from 2600:9000:4000::/36;
set_real_ip_from 2600:9000:3000::/36;
set_real_ip_from 2600:9000:f000::/36;
set_real_ip_from 2600:9000:fff::/48;
set_real_ip_from 2600:9000:2000::/36;
set_real_ip_from 2600:9000:1000::/36;
set_real_ip_from 2600:9000:ddd::/48;
set_real_ip_from 2600:9000:5300::/40;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

View file

@ -3,4 +3,6 @@ proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port; proxy_pass $forward_scheme://$server:$port;

View file

@ -18,6 +18,9 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:3000/; proxy_pass http://127.0.0.1:3000/;
proxy_read_timeout 15m;
proxy_send_timeout 15m;
} }
location / { location / {

View file

@ -27,9 +27,9 @@ http {
tcp_nodelay on; tcp_nodelay on;
client_body_temp_path /tmp/nginx/body 1 2; client_body_temp_path /tmp/nginx/body 1 2;
keepalive_timeout 90s; keepalive_timeout 90s;
proxy_connect_timeout 90s; proxy_connect_timeout 90s;
proxy_send_timeout 90s; proxy_send_timeout 90s;
proxy_read_timeout 90s; proxy_read_timeout 90s;
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
gzip on; gzip on;
proxy_ignore_client_abort off; proxy_ignore_client_abort off;
@ -60,12 +60,18 @@ http {
# Real IP Determination # Real IP Determination
# Docker subnet: # Docker subnet:
set_real_ip_from 172.0.0.0/8; set_real_ip_from 172.0.0.0/8;
# Local subnets:
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.0.0.0/8;
# NPM generated CDN ip ranges: # NPM generated CDN ip ranges:
include conf.d/include/ip_ranges.conf; include conf.d/include/ip_ranges.conf;
# always put the following 2 lines after ip subnets: # always put the following 2 lines after ip subnets:
real_ip_header X-Forwarded-For; real_ip_header X-Real-IP;
real_ip_recursive on; real_ip_recursive on;
# Custom
include /data/nginx/custom/http_top[.]conf;
# Files generated by NPM # Files generated by NPM
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /data/nginx/default_host/*.conf; include /data/nginx/default_host/*.conf;
@ -81,6 +87,9 @@ http {
stream { stream {
# Files generated by NPM # Files generated by NPM
include /data/nginx/stream/*.conf; include /data/nginx/stream/*.conf;
# Custom
include /data/nginx/custom/stream[.]conf;
} }
# Custom # Custom

View file

@ -5,7 +5,7 @@ mkdir -p /data/letsencrypt-acme-challenge
cd /app || echo cd /app || echo
if [ "$DEVELOPMENT" == "true" ]; then if [ "$DEVELOPMENT" == "true" ]; then
cd /app/backend || exit 1 cd /app || exit 1
yarn install yarn install
node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js
else else

View file

@ -45,21 +45,7 @@ footer: MIT Licensed | Copyright © 2016-present jc21.com
- [Docker Install documentation](https://docs.docker.com/install/) - [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) - [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a config file for example 2. Create a docker-compose.yml file similar to this:
```json
{
"database": {
"engine": "mysql",
"host": "db",
"name": "npm",
"user": "npm",
"password": "npm",
"port": 3306
}
}
```
3. Create a docker-compose.yml file similar to this:
```yml ```yml
version: '3' version: '3'
@ -70,12 +56,17 @@ services:
- '80:80' - '80:80'
- '81:81' - '81:81'
- '443:443' - '443:443'
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
volumes: volumes:
- ./config.json:/app/config/production.json
- ./data:/data - ./data:/data
- ./letsencrypt:/etc/letsencrypt - ./letsencrypt:/etc/letsencrypt
db: db:
image: 'jc21/mariadb-aria:10.4' image: 'jc21/mariadb-aria:latest'
environment: environment:
MYSQL_ROOT_PASSWORD: 'npm' MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm' MYSQL_DATABASE: 'npm'
@ -85,13 +76,13 @@ services:
- ./data/mysql:/var/lib/mysql - ./data/mysql:/var/lib/mysql
``` ```
4. Bring up your stack 3. Bring up your stack
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
5. Log in to the Admin UI 4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface. When your docker container is running, connect to it on port `81` for the admin interface.
Sometimes this can take a little bit because of the entropy of keys. Sometimes this can take a little bit because of the entropy of keys.

View file

@ -1,5 +1,114 @@
# Advanced Configuration # Advanced Configuration
## Best Practice: Use a docker network
For those who have a few of their upstream services running in docker on the same docker
host as NPM, here's a trick to secure things a bit better. By creating a custom docker network,
you don't need to publish ports for your upstream services to all of the docker host's interfaces.
Create a network, ie "scoobydoo":
```bash
docker network create scoobydoo
```
Then add the following to the `docker-compose.yml` file for both NPM and any other
services running on this docker host:
```yml
networks:
default:
external:
name: scoobydoo
```
Let's look at a Portainer example:
```yml
version: '3'
services:
portainer:
image: portainer/portainer
privileged: true
volumes:
- './data:/data'
- '/var/run/docker.sock:/var/run/docker.sock'
restart: always
networks:
default:
external:
name: scoobydoo
```
Now in the NPM UI you can create a proxy host with `portainer` as the hostname,
and port `9000` as the port. Even though this port isn't listed in the docker-compose
file, it's "exposed" by the portainer docker image for you and not available on
the docker host outside of this docker network. The service name is used as the
hostname, so make sure your service names are unique when using the same network.
## Docker Secrets
This image supports the use of Docker secrets to import from file and keep sensitive usernames or passwords from being passed or preserved in plaintext.
You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name.
```yml
version: "3.7"
secrets:
# Secrets are single-line text files where the sole content is the secret
# Paths in this example assume that secrets are kept in local folder called ".secrets"
DB_ROOT_PWD:
file: .secrets/db_root_pwd.txt
MYSQL_PWD:
file: .secrets/mysql_pwd.txt
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: always
ports:
# Public HTTP Port:
- '80:80'
# Public HTTPS Port:
- '443:443'
# Admin Web Port:
- '81:81'
environment:
# These are the settings to access your db
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
# DB_MYSQL_PASSWORD: "npm" # use secret instead
DB_MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
DB_MYSQL_NAME: "npm"
# If you would rather use Sqlite uncomment this
# and remove all DB_MYSQL_* lines above
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: jc21/mariadb-aria
restart: always
environment:
# MYSQL_ROOT_PASSWORD: "npm" # use secret instead
MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
# MYSQL_PASSWORD: "npm" # use secret instead
MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
volumes:
- ./data/mysql:/var/lib/mysql
```
## Disabling IPv6 ## Disabling IPv6
On some docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log: On some docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log:
@ -24,6 +133,7 @@ You can add your custom configuration snippet files at `/data/nginx/custom` as f
- `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf - `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf
- `/data/nginx/custom/http.conf`: Included at the end of the main http block - `/data/nginx/custom/http.conf`: Included at the end of the main http block
- `/data/nginx/custom/stream.conf`: Included at the end of the main stream block
- `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block - `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block
- `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block - `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block
- `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block

View file

@ -14,3 +14,10 @@ of dependencies.
Yes! The docker image is multi-arch and is built for a variety of architectures. If yours is Yes! The docker image is multi-arch and is built for a variety of architectures. If yours is
[not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a [not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a
[GitHub issue](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). [GitHub issue](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).
## I can't get my service to proxy properly?
Your best bet is to ask the [Reddit community for support](https://www.reddit.com/r/nginxproxymanager/). There's safety in numbers.
Gitter is best left for anyone contributing to the project to ask for help about internals, code reviews etc.

View file

@ -4,15 +4,15 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@vuepress/plugin-google-analytics": "^1.4.0", "@vuepress/plugin-google-analytics": "^1.5.3",
"abbrev": "^1.1.1", "abbrev": "^1.1.1",
"accepts": "^1.3.7", "accepts": "^1.3.7",
"acorn": "^7.1.1", "acorn": "^7.4.0",
"agentkeepalive": "^4.1.0", "agentkeepalive": "^4.1.3",
"ajv": "^6.12.0", "ajv": "^6.12.3",
"ajv-errors": "^1.0.1", "ajv-errors": "^1.0.1",
"ajv-keywords": "^3.4.1", "ajv-keywords": "^3.5.2",
"algoliasearch": "^4.1.0", "algoliasearch": "^4.3.1",
"alphanum-sort": "^1.0.2", "alphanum-sort": "^1.0.2",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"ansi-escapes": "^4.3.1", "ansi-escapes": "^4.3.1",
@ -30,7 +30,7 @@
"array-uniq": "^2.1.0", "array-uniq": "^2.1.0",
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
"asn1": "^0.2.4", "asn1": "^0.2.4",
"asn1.js": "^5.3.0", "asn1.js": "^5.4.1",
"assert": "^2.0.0", "assert": "^2.0.0",
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
"assign-symbols": "^2.0.2", "assign-symbols": "^2.0.2",
@ -40,11 +40,11 @@
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"atob": "^2.1.2", "atob": "^2.1.2",
"autocomplete.js": "^0.37.1", "autocomplete.js": "^0.37.1",
"autoprefixer": "^9.7.6", "autoprefixer": "^9.8.6",
"aws-sign2": "^0.7.0", "aws-sign2": "^0.7.0",
"aws4": "^1.9.1", "aws4": "^1.10.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"base": "^3.0.0", "base": "^3.0.0",
@ -52,9 +52,9 @@
"batch": "^0.6.1", "batch": "^0.6.1",
"bcrypt-pbkdf": "^1.0.2", "bcrypt-pbkdf": "^1.0.2",
"big.js": "^5.2.2", "big.js": "^5.2.2",
"binary-extensions": "^2.0.0", "binary-extensions": "^2.1.0",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"bn.js": "^5.1.1", "bn.js": "^5.1.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bonjour": "^3.5.0", "bonjour": "^3.5.0",
"boolbase": "^1.0.0", "boolbase": "^1.0.0",
@ -65,18 +65,18 @@
"browserify-cipher": "^1.0.1", "browserify-cipher": "^1.0.1",
"browserify-des": "^1.0.2", "browserify-des": "^1.0.2",
"browserify-rsa": "^4.0.1", "browserify-rsa": "^4.0.1",
"browserify-sign": "^4.0.4", "browserify-sign": "^4.2.1",
"browserify-zlib": "^0.2.0", "browserify-zlib": "^0.2.0",
"browserslist": "^4.11.1", "browserslist": "^4.13.0",
"buffer": "^5.5.0", "buffer": "^5.6.0",
"buffer-from": "^1.1.1", "buffer-from": "^1.1.1",
"buffer-indexof": "^1.1.1", "buffer-indexof": "^1.1.1",
"buffer-json": "^2.0.0", "buffer-json": "^2.0.0",
"buffer-xor": "^2.0.2", "buffer-xor": "^2.0.2",
"builtin-status-codes": "^3.0.0", "builtin-status-codes": "^3.0.0",
"bytes": "^3.1.0", "bytes": "^3.1.0",
"cac": "^6.5.8", "cac": "^6.6.1",
"cacache": "^15.0.0", "cacache": "^15.0.5",
"cache-base": "^4.0.0", "cache-base": "^4.0.0",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"call-me-maybe": "^1.0.1", "call-me-maybe": "^1.0.1",
@ -84,12 +84,12 @@
"caller-path": "^3.0.0", "caller-path": "^3.0.0",
"callsites": "^3.1.0", "callsites": "^3.1.0",
"camel-case": "^4.1.1", "camel-case": "^4.1.1",
"camelcase": "^5.3.1", "camelcase": "^6.0.0",
"caniuse-api": "^3.0.0", "caniuse-api": "^3.0.0",
"caniuse-lite": "^1.0.30001039", "caniuse-lite": "^1.0.30001111",
"caseless": "^0.12.0", "caseless": "^0.12.0",
"chalk": "^4.0.0", "chalk": "^4.1.0",
"chokidar": "^3.3.1", "chokidar": "^3.4.1",
"chownr": "^2.0.0", "chownr": "^2.0.0",
"chrome-trace-event": "^1.0.2", "chrome-trace-event": "^1.0.2",
"ci-info": "^2.0.0", "ci-info": "^2.0.0",
@ -106,7 +106,7 @@
"color-name": "^1.1.4", "color-name": "^1.1.4",
"color-string": "^1.5.3", "color-string": "^1.5.3",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"commander": "^5.0.0", "commander": "^6.0.0",
"commondir": "^1.0.1", "commondir": "^1.0.1",
"component-emitter": "^1.3.0", "component-emitter": "^1.3.0",
"compressible": "^2.0.18", "compressible": "^2.0.18",
@ -114,36 +114,36 @@
"concat-map": "^0.0.1", "concat-map": "^0.0.1",
"concat-stream": "^2.0.0", "concat-stream": "^2.0.0",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"consola": "^2.11.3", "consola": "^2.15.0",
"console-browserify": "^1.2.0", "console-browserify": "^1.2.0",
"consolidate": "^0.15.1", "consolidate": "^0.15.1",
"constants-browserify": "^1.0.0", "constants-browserify": "^1.0.0",
"content-disposition": "^0.5.3", "content-disposition": "^0.5.3",
"content-type": "^1.0.4", "content-type": "^1.0.4",
"convert-source-map": "^1.7.0", "convert-source-map": "^1.7.0",
"cookie": "^0.4.0", "cookie": "^0.4.1",
"cookie-signature": "^1.1.0", "cookie-signature": "^1.1.0",
"copy-concurrently": "^1.0.5", "copy-concurrently": "^1.0.5",
"copy-descriptor": "^0.1.1", "copy-descriptor": "^0.1.1",
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^6.0.3",
"core-js": "^3.6.4", "core-js": "^3.6.5",
"core-util-is": "^1.0.2", "core-util-is": "^1.0.2",
"cosmiconfig": "^6.0.0", "cosmiconfig": "^7.0.0",
"create-ecdh": "^4.0.3", "create-ecdh": "^4.0.4",
"create-hash": "^1.2.0", "create-hash": "^1.2.0",
"create-hmac": "^1.1.7", "create-hmac": "^1.1.7",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.3",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"css": "^2.2.4", "css": "^3.0.0",
"css-color-names": "^1.0.1", "css-color-names": "^1.0.1",
"css-declaration-sorter": "^5.1.2", "css-declaration-sorter": "^5.1.2",
"css-loader": "^3.5.0", "css-loader": "^4.2.0",
"css-parse": "^2.0.0", "css-parse": "^2.0.0",
"css-select": "^2.1.0", "css-select": "^2.1.0",
"css-select-base-adapter": "^0.1.1", "css-select-base-adapter": "^0.1.1",
"css-tree": "^1.0.0-alpha.39", "css-tree": "^1.0.0-alpha.39",
"css-unit-converter": "^1.1.1", "css-unit-converter": "^1.1.2",
"css-what": "^3.2.1", "css-what": "^3.3.0",
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"cssnano-preset-default": "^4.0.7", "cssnano-preset-default": "^4.0.7",
@ -158,9 +158,9 @@
"debug": "^4.1.1", "debug": "^4.1.1",
"decamelize": "^4.0.0", "decamelize": "^4.0.0",
"decode-uri-component": "^0.2.0", "decode-uri-component": "^0.2.0",
"deep-equal": "^2.0.2", "deep-equal": "^2.0.3",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"default-gateway": "^6.0.0", "default-gateway": "^6.0.1",
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"define-property": "^2.0.2", "define-property": "^2.0.2",
"del": "^5.1.0", "del": "^5.1.0",
@ -178,52 +178,52 @@
"dns-txt": "^2.0.2", "dns-txt": "^2.0.2",
"docsearch.js": "^2.6.3", "docsearch.js": "^2.6.3",
"dom-converter": "^0.2.0", "dom-converter": "^0.2.0",
"dom-serializer": "^0.2.2", "dom-serializer": "^1.0.1",
"dom-walk": "^0.1.2", "dom-walk": "^0.1.2",
"domain-browser": "^4.0.0", "domain-browser": "^4.16.0",
"domelementtype": "^2.0.1", "domelementtype": "^2.0.1",
"domhandler": "^3.0.0", "domhandler": "^3.0.0",
"domutils": "^2.0.0", "domutils": "^2.1.0",
"dot-prop": "^5.2.0", "dot-prop": "^5.2.0",
"duplexify": "^4.1.1", "duplexify": "^4.1.1",
"ecc-jsbn": "^0.2.0", "ecc-jsbn": "^0.2.0",
"ee-first": "^1.1.1", "ee-first": "^1.1.1",
"electron-to-chromium": "^1.3.397", "electron-to-chromium": "^1.3.522",
"elliptic": "^6.5.2", "elliptic": "^6.5.3",
"emoji-regex": "^8.0.0", "emoji-regex": "^9.0.0",
"emojis-list": "^3.0.0", "emojis-list": "^3.0.0",
"encodeurl": "^1.0.2", "encodeurl": "^1.0.2",
"end-of-stream": "^1.4.4", "end-of-stream": "^1.4.4",
"enhanced-resolve": "^4.1.1", "enhanced-resolve": "^4.3.0",
"entities": "^2.0.0", "entities": "^2.0.3",
"envify": "^4.1.0", "envify": "^4.1.0",
"envinfo": "^7.5.0", "envinfo": "^7.7.2",
"errno": "^0.1.7", "errno": "^0.1.7",
"error-ex": "^1.3.2", "error-ex": "^1.3.2",
"es-abstract": "^1.17.5", "es-abstract": "^1.17.6",
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^5.0.0", "eslint-scope": "^5.1.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"esrecurse": "^4.2.1", "esrecurse": "^4.2.1",
"estraverse": "^5.0.0", "estraverse": "^5.2.0",
"esutils": "^2.0.3", "esutils": "^2.0.3",
"etag": "^1.8.1", "etag": "^1.8.1",
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.4",
"events": "^3.1.0", "events": "^3.2.0",
"eventsource": "^1.0.7", "eventsource": "^1.0.7",
"evp_bytestokey": "^1.0.3", "evp_bytestokey": "^1.0.3",
"execa": "^4.0.0", "execa": "^4.0.3",
"expand-brackets": "^4.0.0", "expand-brackets": "^4.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"extend": "^3.0.2", "extend": "^3.0.2",
"extend-shallow": "^3.0.2", "extend-shallow": "^3.0.2",
"extglob": "^3.0.0", "extglob": "^3.0.0",
"extsprintf": "^1.4.0", "extsprintf": "^1.4.0",
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-glob": "^3.2.2", "fast-glob": "^3.2.4",
"fast-json-stable-stringify": "^2.1.0", "fast-json-stable-stringify": "^2.1.0",
"faye-websocket": "^0.11.3", "faye-websocket": "^0.11.3",
"figgy-pudding": "^3.5.2", "figgy-pudding": "^3.5.2",
@ -235,7 +235,7 @@
"find-cache-dir": "^3.3.1", "find-cache-dir": "^3.3.1",
"find-up": "^4.1.0", "find-up": "^4.1.0",
"flush-write-stream": "^2.0.0", "flush-write-stream": "^2.0.0",
"follow-redirects": "^1.11.0", "follow-redirects": "^1.12.1",
"for-in": "^1.0.2", "for-in": "^1.0.2",
"foreach": "^2.0.5", "foreach": "^2.0.5",
"forever-agent": "^0.6.1", "forever-agent": "^0.6.1",
@ -244,7 +244,7 @@
"fragment-cache": "^0.2.1", "fragment-cache": "^0.2.1",
"fresh": "^0.5.2", "fresh": "^0.5.2",
"from2": "^2.3.0", "from2": "^2.3.0",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.1",
"fs-write-stream-atomic": "^1.0.10", "fs-write-stream-atomic": "^1.0.10",
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@ -257,21 +257,21 @@
"glob-parent": "^5.1.1", "glob-parent": "^5.1.1",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"global": "^4.4.0", "global": "^4.4.0",
"globals": "^12.4.0", "globals": "^13.1.0",
"globby": "^11.0.0", "globby": "^11.0.1",
"good-listener": "^1.2.2", "good-listener": "^1.2.2",
"graceful-fs": "^4.2.3", "graceful-fs": "^4.2.4",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
"handle-thing": "^2.0.1", "handle-thing": "^2.0.1",
"har-schema": "^2.0.0", "har-schema": "^2.0.0",
"har-validator": "^5.1.3", "har-validator": "^5.1.5",
"has": "^1.0.3", "has": "^1.0.3",
"has-ansi": "^4.0.0", "has-ansi": "^4.0.0",
"has-flag": "^4.0.0", "has-flag": "^4.0.0",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"has-value": "^2.0.2", "has-value": "^2.0.2",
"has-values": "^2.0.1", "has-values": "^2.0.1",
"hash-base": "^3.0.4", "hash-base": "^3.1.0",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"hash.js": "^1.1.7", "hash.js": "^1.1.7",
"he": "^1.2.0", "he": "^1.2.0",
@ -282,24 +282,24 @@
"hsl-regex": "^1.0.0", "hsl-regex": "^1.0.0",
"hsla-regex": "^1.0.0", "hsla-regex": "^1.0.0",
"html-comment-regex": "^1.1.2", "html-comment-regex": "^1.1.2",
"html-entities": "^1.2.1", "html-entities": "^1.3.1",
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"html-tags": "^3.1.0", "html-tags": "^3.1.0",
"htmlparser2": "^4.1.0", "htmlparser2": "^4.1.0",
"http-deceiver": "^1.2.7", "http-deceiver": "^1.2.7",
"http-errors": "^1.7.3", "http-errors": "^1.8.0",
"http-parser-js": "^0.5.2", "http-parser-js": "^0.5.2",
"http-proxy": "^1.18.0", "http-proxy": "^1.18.1",
"http-proxy-middleware": "^1.0.3", "http-proxy-middleware": "^1.0.5",
"http-signature": "^1.3.4", "http-signature": "^1.3.4",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"iconv-lite": "^0.5.1", "iconv-lite": "^0.6.2",
"icss-replace-symbols": "^1.1.0", "icss-replace-symbols": "^1.1.0",
"icss-utils": "^4.1.1", "icss-utils": "^4.1.1",
"ieee754": "^1.1.13", "ieee754": "^1.1.13",
"iferr": "^1.0.2", "iferr": "^1.0.2",
"ignore": "^5.1.4", "ignore": "^5.1.8",
"immediate": "^3.2.3", "immediate": "^3.3.0",
"import-cwd": "^3.0.0", "import-cwd": "^3.0.0",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"import-from": "^3.0.0", "import-from": "^3.0.0",
@ -309,9 +309,9 @@
"infer-owner": "^1.0.4", "infer-owner": "^1.0.4",
"inflight": "^1.0.6", "inflight": "^1.0.6",
"inherits": "^2.0.4", "inherits": "^2.0.4",
"internal-ip": "^6.0.0", "internal-ip": "^6.1.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"invert-kv": "^3.0.0", "invert-kv": "^3.0.1",
"ip": "^1.1.5", "ip": "^1.1.5",
"ip-regex": "^4.1.0", "ip-regex": "^4.1.0",
"ipaddr.js": "^1.9.1", "ipaddr.js": "^1.9.1",
@ -321,7 +321,7 @@
"is-arrayish": "^0.3.2", "is-arrayish": "^0.3.2",
"is-binary-path": "^2.1.0", "is-binary-path": "^2.1.0",
"is-buffer": "^2.0.4", "is-buffer": "^2.0.4",
"is-callable": "^1.1.5", "is-callable": "^1.2.0",
"is-color-stop": "^1.1.0", "is-color-stop": "^1.1.0",
"is-data-descriptor": "^2.0.0", "is-data-descriptor": "^2.0.0",
"is-date-object": "^1.0.2", "is-date-object": "^1.0.2",
@ -337,25 +337,25 @@
"is-path-in-cwd": "^3.0.0", "is-path-in-cwd": "^3.0.0",
"is-path-inside": "^3.0.2", "is-path-inside": "^3.0.2",
"is-plain-obj": "^2.1.0", "is-plain-obj": "^2.1.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^4.1.1",
"is-regex": "^1.0.5", "is-regex": "^1.1.1",
"is-resolvable": "^1.1.0", "is-resolvable": "^1.1.0",
"is-stream": "^2.0.0", "is-stream": "^2.0.0",
"is-svg": "^4.2.1", "is-svg": "^4.2.1",
"is-symbol": "^1.0.3", "is-symbol": "^1.0.3",
"is-typedarray": "^1.0.0", "is-typedarray": "^1.0.0",
"is-windows": "^1.0.2", "is-windows": "^1.0.2",
"is-wsl": "^2.1.1", "is-wsl": "^2.2.0",
"isarray": "^2.0.5", "isarray": "^2.0.5",
"isexe": "^2.0.0", "isexe": "^2.0.0",
"isobject": "^4.0.0", "isobject": "^4.0.0",
"isstream": "^0.1.2", "isstream": "^0.1.2",
"javascript-stringify": "^2.0.1", "javascript-stringify": "^2.0.1",
"js-levenshtein": "^1.1.6", "js-levenshtein": "^1.1.6",
"js-tokens": "^5.0.0", "js-tokens": "^6.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.14.0",
"jsbn": "^1.1.0", "jsbn": "^1.1.0",
"jsesc": "^2.5.2", "jsesc": "^3.0.1",
"json-parse-better-errors": "^1.0.2", "json-parse-better-errors": "^1.0.2",
"json-schema": "^0.2.5", "json-schema": "^0.2.5",
"json-schema-traverse": "^0.4.1", "json-schema-traverse": "^0.4.1",
@ -368,12 +368,12 @@
"kind-of": "^6.0.3", "kind-of": "^6.0.3",
"last-call-webpack-plugin": "^3.0.0", "last-call-webpack-plugin": "^3.0.0",
"lcid": "^3.1.1", "lcid": "^3.1.1",
"linkify-it": "^2.2.0", "linkify-it": "^3.0.2",
"load-script": "^1.0.0", "load-script": "^1.0.0",
"loader-runner": "^3.1.0", "loader-runner": "^4.0.0",
"loader-utils": "^2.0.0", "loader-utils": "^2.0.0",
"locate-path": "^5.0.0", "locate-path": "^5.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"lodash._reinterpolate": "^3.0.0", "lodash._reinterpolate": "^3.0.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
@ -385,37 +385,37 @@
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"lodash.templatesettings": "^4.2.0", "lodash.templatesettings": "^4.2.0",
"lodash.uniq": "^4.5.0", "lodash.uniq": "^4.5.0",
"loglevel": "^1.6.7", "loglevel": "^1.6.8",
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"lower-case": "^2.0.1", "lower-case": "^2.0.1",
"lru-cache": "^5.1.1", "lru-cache": "^6.0.0",
"make-dir": "^3.0.2", "make-dir": "^3.1.0",
"mamacro": "^0.0.7", "mamacro": "^0.0.7",
"map-age-cleaner": "^0.1.3", "map-age-cleaner": "^0.1.3",
"map-cache": "^0.2.2", "map-cache": "^0.2.2",
"map-visit": "^1.0.0", "map-visit": "^1.0.0",
"markdown-it": "^10.0.0", "markdown-it": "^11.0.0",
"markdown-it-anchor": "^5.2.7", "markdown-it-anchor": "^5.3.0",
"markdown-it-chain": "^1.3.0", "markdown-it-chain": "^1.3.0",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"markdown-it-table-of-contents": "^0.4.4", "markdown-it-table-of-contents": "^0.4.4",
"md5.js": "^1.3.5", "md5.js": "^1.3.5",
"mdn-data": "^2.0.8", "mdn-data": "^2.0.11",
"mdurl": "^1.0.1", "mdurl": "^1.0.1",
"media-typer": "^1.1.0", "media-typer": "^1.1.0",
"mem": "^6.0.1", "mem": "^6.1.0",
"memory-fs": "^0.5.0", "memory-fs": "^0.5.0",
"merge-descriptors": "^1.0.1", "merge-descriptors": "^1.0.1",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"merge2": "^1.3.0", "merge2": "^1.4.1",
"methods": "^1.1.2", "methods": "^1.1.2",
"micromatch": "^4.0.2", "micromatch": "^4.0.2",
"miller-rabin": "^4.0.1", "miller-rabin": "^4.0.1",
"mime": "^2.4.4", "mime": "^2.4.6",
"mime-db": "^1.43.0", "mime-db": "^1.44.0",
"mime-types": "^2.1.26", "mime-types": "^2.1.27",
"mimic-fn": "^3.0.0", "mimic-fn": "^3.1.0",
"min-document": "^2.19.0", "min-document": "^2.19.0",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"minimalistic-assert": "^1.0.1", "minimalistic-assert": "^1.0.1",
@ -431,16 +431,16 @@
"multicast-dns-service-types": "^1.1.0", "multicast-dns-service-types": "^1.1.0",
"nanomatch": "^1.2.13", "nanomatch": "^1.2.13",
"negotiator": "^0.6.2", "negotiator": "^0.6.2",
"neo-async": "^2.6.1", "neo-async": "^2.6.2",
"nice-try": "^2.0.1", "nice-try": "^2.0.1",
"no-case": "^3.0.3", "no-case": "^3.0.3",
"node-forge": "^0.9.1", "node-forge": "^0.10.0",
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"node-releases": "^1.1.53", "node-releases": "^1.1.60",
"nopt": "^4.0.3", "nopt": "^4.0.3",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"normalize-url": "^5.0.0", "normalize-url": "^5.1.0",
"npm-run-path": "^4.0.1", "npm-run-path": "^4.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"nth-check": "^1.0.2", "nth-check": "^1.0.2",
@ -449,8 +449,8 @@
"oauth-sign": "^0.9.0", "oauth-sign": "^0.9.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"object-copy": "^1.0.0", "object-copy": "^1.0.0",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-is": "^1.0.2", "object-is": "^1.1.2",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object-visit": "^1.0.1", "object-visit": "^1.0.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.0",
@ -461,7 +461,7 @@
"on-finished": "^2.3.0", "on-finished": "^2.3.0",
"on-headers": "^1.0.2", "on-headers": "^1.0.2",
"once": "^1.4.0", "once": "^1.4.0",
"opencollective-postinstall": "^2.0.2", "opencollective-postinstall": "^2.0.3",
"opn": "^6.0.0", "opn": "^6.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"original": "^1.0.2", "original": "^1.0.2",
@ -470,7 +470,7 @@
"p-defer": "^3.0.0", "p-defer": "^3.0.0",
"p-finally": "^2.0.1", "p-finally": "^2.0.1",
"p-is-promise": "^3.0.0", "p-is-promise": "^3.0.0",
"p-limit": "^2.3.0", "p-limit": "^3.0.2",
"p-locate": "^4.1.0", "p-locate": "^4.1.0",
"p-map": "^4.0.0", "p-map": "^4.0.0",
"p-retry": "^4.2.0", "p-retry": "^4.2.0",
@ -479,7 +479,7 @@
"parallel-transform": "^1.2.0", "parallel-transform": "^1.2.0",
"param-case": "^3.0.3", "param-case": "^3.0.3",
"parse-asn1": "^5.1.5", "parse-asn1": "^5.1.5",
"parse-json": "^5.0.0", "parse-json": "^5.0.1",
"parseurl": "^1.3.3", "parseurl": "^1.3.3",
"pascalcase": "^1.0.0", "pascalcase": "^1.0.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -491,16 +491,16 @@
"path-parse": "^1.0.6", "path-parse": "^1.0.6",
"path-to-regexp": "^6.1.0", "path-to-regexp": "^6.1.0",
"path-type": "^4.0.0", "path-type": "^4.0.0",
"pbkdf2": "^3.0.17", "pbkdf2": "^3.1.1",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"pify": "^5.0.0", "pify": "^5.0.0",
"pinkie": "^2.0.4", "pinkie": "^2.0.4",
"pinkie-promise": "^2.0.1", "pinkie-promise": "^2.0.1",
"pkg-dir": "^4.2.0", "pkg-dir": "^4.2.0",
"pkg-up": "^3.1.0", "pkg-up": "^3.1.0",
"portfinder": "^1.0.25", "portfinder": "^1.0.28",
"posix-character-classes": "^1.0.0", "posix-character-classes": "^1.0.0",
"postcss": "^7.0.27", "postcss": "^7.0.32",
"postcss-calc": "^7.0.2", "postcss-calc": "^7.0.2",
"postcss-colormin": "^4.0.3", "postcss-colormin": "^4.0.3",
"postcss-convert-values": "^4.0.1", "postcss-convert-values": "^4.0.1",
@ -517,7 +517,7 @@
"postcss-minify-params": "^4.0.2", "postcss-minify-params": "^4.0.2",
"postcss-minify-selectors": "^4.0.2", "postcss-minify-selectors": "^4.0.2",
"postcss-modules-extract-imports": "^2.0.0", "postcss-modules-extract-imports": "^2.0.0",
"postcss-modules-local-by-default": "^3.0.2", "postcss-modules-local-by-default": "^3.0.3",
"postcss-modules-scope": "^2.2.0", "postcss-modules-scope": "^2.2.0",
"postcss-modules-values": "^3.0.0", "postcss-modules-values": "^3.0.0",
"postcss-normalize-charset": "^4.0.1", "postcss-normalize-charset": "^4.0.1",
@ -536,9 +536,9 @@
"postcss-selector-parser": "^6.0.2", "postcss-selector-parser": "^6.0.2",
"postcss-svgo": "^4.0.2", "postcss-svgo": "^4.0.2",
"postcss-unique-selectors": "^4.0.1", "postcss-unique-selectors": "^4.0.1",
"postcss-value-parser": "^4.0.3", "postcss-value-parser": "^4.1.0",
"prepend-http": "^3.0.1", "prepend-http": "^3.0.1",
"prettier": "^2.0.4", "prettier": "^2.0.5",
"pretty-error": "^2.1.1", "pretty-error": "^2.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"prismjs": "^1.20.0", "prismjs": "^1.20.0",
@ -555,8 +555,8 @@
"pumpify": "^2.0.1", "pumpify": "^2.0.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"q": "^1.5.1", "q": "^1.5.1",
"qs": "^6.9.3", "qs": "^6.9.4",
"query-string": "^6.12.0", "query-string": "^6.13.1",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"querystring-es3": "^0.2.1", "querystring-es3": "^0.2.1",
"querystringify": "^2.1.1", "querystringify": "^2.1.1",
@ -567,14 +567,14 @@
"readable-stream": "^3.6.0", "readable-stream": "^3.6.0",
"readdirp": "^3.4.0", "readdirp": "^3.4.0",
"reduce": "^1.0.2", "reduce": "^1.0.2",
"regenerate": "^1.4.0", "regenerate": "^1.4.1",
"regenerate-unicode-properties": "^8.2.0", "regenerate-unicode-properties": "^8.2.0",
"regenerator-runtime": "^0.13.5", "regenerator-runtime": "^0.13.7",
"regenerator-transform": "^0.14.4", "regenerator-transform": "^0.14.5",
"regex-not": "^1.0.2", "regex-not": "^1.0.2",
"regexp.prototype.flags": "^1.3.0", "regexp.prototype.flags": "^1.3.0",
"regexpu-core": "^4.7.0", "regexpu-core": "^4.7.0",
"regjsgen": "^0.5.1", "regjsgen": "^0.5.2",
"regjsparser": "^0.6.4", "regjsparser": "^0.6.4",
"relateurl": "^0.2.7", "relateurl": "^0.2.7",
"remove-trailing-separator": "^1.1.0", "remove-trailing-separator": "^1.1.0",
@ -586,7 +586,7 @@
"require-main-filename": "^2.0.0", "require-main-filename": "^2.0.0",
"requires-port": "^1.0.0", "requires-port": "^1.0.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"resolve": "^1.15.1", "resolve": "^1.17.0",
"resolve-cwd": "^3.0.0", "resolve-cwd": "^3.0.0",
"resolve-from": "^5.0.0", "resolve-from": "^5.0.0",
"resolve-url": "^0.2.1", "resolve-url": "^0.2.1",
@ -597,18 +597,18 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ripemd160": "^2.0.2", "ripemd160": "^2.0.2",
"run-queue": "^2.0.1", "run-queue": "^2.0.1",
"safe-buffer": "^5.2.0", "safe-buffer": "^5.2.1",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"safer-buffer": "^2.1.2", "safer-buffer": "^2.1.2",
"sax": "^1.2.4", "sax": "^1.2.4",
"schema-utils": "^2.6.5", "schema-utils": "^2.7.0",
"section-matter": "^1.0.0", "section-matter": "^1.0.0",
"select": "^1.1.2", "select": "^1.1.2",
"select-hose": "^2.0.0", "select-hose": "^2.0.0",
"selfsigned": "^1.10.7", "selfsigned": "^1.10.7",
"semver": "^7.2.1", "semver": "^7.3.2",
"send": "^0.17.1", "send": "^0.17.1",
"serialize-javascript": "^3.0.0", "serialize-javascript": "^4.0.0",
"serve-index": "^1.9.1", "serve-index": "^1.9.1",
"serve-static": "^1.14.1", "serve-static": "^1.14.1",
"set-blocking": "^2.0.0", "set-blocking": "^2.0.0",
@ -620,19 +620,19 @@
"shebang-regex": "^3.0.0", "shebang-regex": "^3.0.0",
"signal-exit": "^3.0.3", "signal-exit": "^3.0.3",
"simple-swizzle": "^0.2.2", "simple-swizzle": "^0.2.2",
"sitemap": "^6.1.0", "sitemap": "^6.2.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"smoothscroll-polyfill": "^0.4.4", "smoothscroll-polyfill": "^0.4.4",
"snapdragon": "^0.12.0", "snapdragon": "^0.12.0",
"snapdragon-node": "^3.0.0", "snapdragon-node": "^3.0.0",
"snapdragon-util": "^5.0.1", "snapdragon-util": "^5.0.1",
"sockjs": "^0.3.20", "sockjs": "^0.3.21",
"sockjs-client": "^1.4.0", "sockjs-client": "^1.5.0",
"sort-keys": "^4.0.0", "sort-keys": "^4.0.0",
"source-list-map": "^2.0.1", "source-list-map": "^2.0.1",
"source-map": "^0.7.3", "source-map": "^0.7.3",
"source-map-resolve": "^0.6.0", "source-map-resolve": "^0.6.0",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.19",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"spdy": "^4.0.2", "spdy": "^4.0.2",
"spdy-transport": "^3.0.0", "spdy-transport": "^3.0.0",
@ -641,13 +641,13 @@
"sshpk": "^1.16.1", "sshpk": "^1.16.1",
"ssri": "^8.0.0", "ssri": "^8.0.0",
"stable": "^0.1.8", "stable": "^0.1.8",
"stack-utils": "^2.0.1", "stack-utils": "^2.0.2",
"static-extend": "^0.1.2", "static-extend": "^0.1.2",
"statuses": "^1.5.0", "statuses": "^2.0.0",
"std-env": "^2.2.1", "std-env": "^2.2.1",
"stream-browserify": "^2.0.2", "stream-browserify": "^3.0.0",
"stream-each": "^1.2.3", "stream-each": "^1.2.3",
"stream-http": "^3.1.0", "stream-http": "^3.1.1",
"stream-shift": "^1.0.1", "stream-shift": "^1.0.1",
"strict-uri-encode": "^2.0.0", "strict-uri-encode": "^2.0.0",
"string-width": "^4.2.0", "string-width": "^4.2.0",
@ -658,17 +658,17 @@
"strip-bom-string": "^1.0.0", "strip-bom-string": "^1.0.0",
"strip-eof": "^2.0.0", "strip-eof": "^2.0.0",
"stylehacks": "^4.0.3", "stylehacks": "^4.0.3",
"stylus": "^0.54.7", "stylus": "^0.54.8",
"stylus-loader": "^3.0.2", "stylus-loader": "^3.0.2",
"supports-color": "^7.1.0", "supports-color": "^7.1.0",
"svg-tags": "^1.0.0", "svg-tags": "^1.0.0",
"svgo": "^1.3.2", "svgo": "^1.3.2",
"tapable": "^1.1.3", "tapable": "^1.1.3",
"terser": "^4.6.10", "terser": "^5.0.0",
"terser-webpack-plugin": "^2.3.5", "terser-webpack-plugin": "^4.0.0",
"text-table": "^0.2.0", "text-table": "^0.2.0",
"through": "^2.3.8", "through": "^2.3.8",
"through2": "^3.0.1", "through2": "^4.0.2",
"thunky": "^1.1.0", "thunky": "^1.1.0",
"timers-browserify": "^2.0.11", "timers-browserify": "^2.0.11",
"timsort": "^0.3.0", "timsort": "^0.3.0",
@ -684,15 +684,15 @@
"toposort": "^2.0.2", "toposort": "^2.0.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tr46": "^2.0.2", "tr46": "^2.0.2",
"tslib": "^1.11.1", "tslib": "^2.0.0",
"tty-browserify": "^0.0.1", "tty-browserify": "^0.0.1",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"type-fest": "^0.13.0", "type-fest": "^0.16.0",
"type-is": "^1.6.18", "type-is": "^1.6.18",
"typedarray": "^0.0.6", "typedarray": "^0.0.6",
"uc.micro": "^1.0.6", "uc.micro": "^1.0.6",
"uglify-js": "^3.8.1", "uglify-js": "^3.10.1",
"unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-canonical-property-names-ecmascript": "^1.0.4",
"unicode-match-property-ecmascript": "^1.0.4", "unicode-match-property-ecmascript": "^1.0.4",
"unicode-match-property-value-ecmascript": "^1.2.0", "unicode-match-property-value-ecmascript": "^1.2.0",
@ -702,7 +702,7 @@
"uniqs": "^2.0.0", "uniqs": "^2.0.0",
"unique-filename": "^1.1.1", "unique-filename": "^1.1.1",
"unique-slug": "^2.0.2", "unique-slug": "^2.0.2",
"universalify": "^1.0.0", "universalify": "^2.0.0",
"unpipe": "^1.0.0", "unpipe": "^1.0.0",
"unquote": "^1.1.1", "unquote": "^1.1.1",
"unset-value": "^1.0.0", "unset-value": "^1.0.0",
@ -711,60 +711,60 @@
"uri-js": "^4.2.2", "uri-js": "^4.2.2",
"urix": "^0.1.0", "urix": "^0.1.0",
"url": "^0.11.0", "url": "^0.11.0",
"url-loader": "^4.0.0", "url-loader": "^4.1.0",
"url-parse": "^1.4.7", "url-parse": "^1.4.7",
"use": "^3.1.1", "use": "^3.1.1",
"util": "^0.12.2", "util": "^0.12.3",
"util-deprecate": "^1.0.2", "util-deprecate": "^1.0.2",
"util.promisify": "^1.0.1", "util.promisify": "^1.0.1",
"utila": "^0.4.0", "utila": "^0.4.0",
"utils-merge": "^1.0.1", "utils-merge": "^1.0.1",
"uuid": "^7.0.3", "uuid": "^8.3.0",
"vary": "^1.1.2", "vary": "^1.1.2",
"vendors": "^1.0.4", "vendors": "^1.0.4",
"verror": "^1.10.0", "verror": "^1.10.0",
"vm-browserify": "^1.1.2", "vm-browserify": "^1.1.2",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-hot-reload-api": "^2.3.4", "vue-hot-reload-api": "^2.3.4",
"vue-loader": "^15.9.1", "vue-loader": "^15.9.3",
"vue-router": "^3.1.6", "vue-router": "^3.4.0",
"vue-server-renderer": "^2.6.11", "vue-server-renderer": "^2.6.11",
"vue-style-loader": "^4.1.2", "vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"vue-template-es2015-compiler": "^1.9.1", "vue-template-es2015-compiler": "^1.9.1",
"vuepress": "^1.4.0", "vuepress": "^1.5.3",
"vuepress-html-webpack-plugin": "^3.2.0", "vuepress-html-webpack-plugin": "^3.2.0",
"vuepress-plugin-container": "^2.1.2", "vuepress-plugin-container": "^2.1.4",
"vuepress-plugin-sitemap": "^2.3.1", "vuepress-plugin-sitemap": "^2.3.1",
"vuepress-plugin-smooth-scroll": "^0.0.9", "vuepress-plugin-smooth-scroll": "^0.0.9",
"vuepress-plugin-zooming": "^1.1.7", "vuepress-plugin-zooming": "^1.1.7",
"watchpack": "^1.6.1", "watchpack": "^1.7.4",
"wbuf": "^1.7.3", "wbuf": "^1.7.3",
"webidl-conversions": "^6.0.0", "webidl-conversions": "^6.1.0",
"webpack": "^4.42.1", "webpack": "^4.44.1",
"webpack-chain": "^6.4.0", "webpack-chain": "^6.5.1",
"webpack-dev-middleware": "^3.7.2", "webpack-dev-middleware": "^3.7.2",
"webpack-dev-server": "^3.10.3", "webpack-dev-server": "^3.11.0",
"webpack-log": "^3.0.1", "webpack-log": "^3.0.1",
"webpack-merge": "^4.2.2", "webpack-merge": "^5.1.1",
"webpack-sources": "^1.4.3", "webpack-sources": "^1.4.3",
"webpackbar": "^4.0.0", "webpackbar": "^4.0.0",
"websocket-driver": "^0.7.3", "websocket-driver": "^0.7.4",
"websocket-extensions": "^0.1.3", "websocket-extensions": "^0.1.4",
"whatwg-url": "^8.0.0", "whatwg-url": "^8.1.0",
"when": "^3.7.8", "when": "^3.7.8",
"which": "^2.0.2", "which": "^2.0.2",
"which-module": "^2.0.0", "which-module": "^2.0.0",
"worker-farm": "^1.7.0", "worker-farm": "^1.7.0",
"wrap-ansi": "^6.2.0", "wrap-ansi": "^7.0.0",
"wrappy": "^1.0.2", "wrappy": "^1.0.2",
"ws": "^7.2.3", "ws": "^7.3.1",
"xmlbuilder": "^15.1.0", "xmlbuilder": "^15.1.1",
"xtend": "^4.0.2", "xtend": "^4.0.2",
"y18n": "^4.0.0", "y18n": "^4.0.0",
"yallist": "^4.0.0", "yallist": "^4.0.0",
"yargs": "^15.3.1", "yargs": "^15.4.1",
"yargs-parser": "^18.1.2", "yargs-parser": "^18.1.3",
"zepto": "^1.2.0" "zepto": "^1.2.0"
}, },
"devDependencies": {}, "devDependencies": {},

View file

@ -1,37 +1,8 @@
# Full Setup Instructions # Full Setup Instructions
### Configuration File ### MySQL Database
**The configuration file needs to be provided by you!** If you opt for the MySQL configuration you will have to provide the database server yourself. You can also use MariaDB. Here are the minimum supported versions:
Don't worry, this is easy to do.
The app requires a configuration file to let it know what database you're using. By default, this file is called `config.json`
Here's an example configuration for `mysql` (or mariadb) that is compatible with the docker-compose example below:
```json
{
"database": {
"engine": "mysql",
"host": "db",
"name": "npm",
"user": "npm",
"password": "npm",
"port": 3306
}
}
```
Once you've created your configuration file it's easy to mount it in the docker container.
**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. These keys
affect the login and session management of the application. If these keys change for any reason, all users will be logged out.
### Database
This app doesn't come with a database, you have to provide one yourself. Currently only `mysql/mariadb` is supported for the minimum versions:
- MySQL v5.7.8+ - MySQL v5.7.8+
- MariaDB v10.2.7+ - MariaDB v10.2.7+
@ -45,7 +16,6 @@ When using a `mariadb` database, the NPM configuration file should still use the
::: :::
### Running the App ### Running the App
Via `docker-compose`: Via `docker-compose`:
@ -54,7 +24,7 @@ Via `docker-compose`:
version: "3" version: "3"
services: services:
app: app:
image: jc21/nginx-proxy-manager:2 image: 'jc21/nginx-proxy-manager:latest'
restart: always restart: always
ports: ports:
# Public HTTP Port: # Public HTTP Port:
@ -64,17 +34,24 @@ services:
# Admin Web Port: # Admin Web Port:
- '81:81' - '81:81'
environment: environment:
# These are the settings to access your db
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
# If you would rather use Sqlite uncomment this
# and remove all DB_MYSQL_* lines above
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host # Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true' # DISABLE_IPV6: 'true'
volumes: volumes:
# Make sure this config.json file exists as per instructions above:
- ./config.json:/app/config/production.json
- ./data:/data - ./data:/data
- ./letsencrypt:/etc/letsencrypt - ./letsencrypt:/etc/letsencrypt
depends_on: depends_on:
- db - db
db: db:
image: jc21/mariadb-aria:10.4 image: 'jc21/mariadb-aria:latest'
restart: always restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: 'npm' MYSQL_ROOT_PASSWORD: 'npm'
@ -85,14 +62,14 @@ services:
- ./data/mysql:/var/lib/mysql - ./data/mysql:/var/lib/mysql
``` ```
_Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use Sqlite._
Then: Then:
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
The config file (config.json) must be present in this directory.
### Running on Raspberry PI / ARM devices ### Running on Raspberry PI / ARM devices
The docker images support the following architectures: The docker images support the following architectures:
@ -130,3 +107,49 @@ Password: changeme
``` ```
Immediately after logging in with this default user you will be asked to modify your details and change your password. Immediately after logging in with this default user you will be asked to modify your details and change your password.
### Configuration File
::: warning
This section is meant for advanced users
:::
If you would like more control over the database settings you can define a custom config JSON file.
Here's an example for `sqlite` configuration as it is generated from the environment variables:
```json
{
"database": {
"engine": "knex-native",
"knex": {
"client": "sqlite3",
"connection": {
"filename": "/data/database.sqlite"
}
}
}
}
```
You can modify the `knex` object with your custom configuration, but note that not all knex clients might be installed in the image.
Once you've created your configuration file you can mount it to `/app/config/production.json` inside you container using:
```
[...]
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
[...]
volumes:
- ./config.json:/app/config/production.json
[...]
[...]
```
**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation.
These keys affect the login and session management of the application. If these keys change for any reason, all users will be logged out.

View file

@ -7,6 +7,10 @@ Known integrations:
- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) - [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager)
- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) - [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager)
- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/lxc/nginx-proxy-manager)
- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf)
If you would like your integration of NPM listed, please open a If you would like your integration of NPM listed, please open a
[Github issue](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) [Github issue](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,7 @@ function fetch(verb, path, data, options) {
contentType: options.contentType || 'application/json; charset=UTF-8', contentType: options.contentType || 'application/json; charset=UTF-8',
processData: options.processData || true, processData: options.processData || true,
crossDomain: true, crossDomain: true,
timeout: options.timeout ? options.timeout : 30000, timeout: options.timeout ? options.timeout : 180000,
xhrFields: { xhrFields: {
withCredentials: true withCredentials: true
}, },
@ -139,7 +139,11 @@ function FileUpload(path, fd) {
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE) { if (this.readyState === XMLHttpRequest.DONE) {
if (xhr.status !== 200 && xhr.status !== 201) { if (xhr.status !== 200 && xhr.status !== 201) {
reject(new Error('Upload failed: ' + xhr.status)); try {
reject(new Error('Upload failed: ' + JSON.parse(xhr.responseText).error.message));
} catch (err) {
reject(new Error('Upload failed: ' + xhr.status));
}
} else { } else {
resolve(xhr.responseText); resolve(xhr.responseText);
} }
@ -587,7 +591,9 @@ module.exports = {
* @param {Object} data * @param {Object} data
*/ */
create: function (data) { create: function (data) {
return fetch('post', 'nginx/certificates', data);
const timeout = 180000 + (data && data.meta && data.meta.propagation_seconds ? Number(data.meta.propagation_seconds) * 1000 : 0);
return fetch('post', 'nginx/certificates', data, {timeout});
}, },
/** /**
@ -630,8 +636,8 @@ module.exports = {
* @param {Number} id * @param {Number} id
* @returns {Promise} * @returns {Promise}
*/ */
renew: function (id) { renew: function (id, timeout = 180000) {
return fetch('post', 'nginx/certificates/' + id + '/renew'); return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout});
} }
} }
}, },

View file

@ -31,6 +31,16 @@
</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="pass_auth" value="1"<%- typeof pass_auth !== 'undefined' && pass_auth ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('access-lists', 'pass-auth') %></span>
</label>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -73,6 +73,7 @@ module.exports = Mn.View.extend({
let data = { let data = {
name: form_data.name, name: form_data.name,
satisfy_any: !!form_data.satisfy_any, satisfy_any: !!form_data.satisfy_any,
pass_auth: !!form_data.pass_auth,
items: items_data, items: items_data,
clients: clients_data clients: clients_data
}; };

View file

@ -16,6 +16,8 @@ module.exports = Mn.View.extend({
events: { events: {
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.save.addClass('btn-loading');
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
App.Api.Nginx.Certificates.delete(this.model.get('id')) App.Api.Nginx.Certificates.delete(this.model.get('id'))
.then(() => { .then(() => {
@ -25,6 +27,7 @@ module.exports = Mn.View.extend({
.catch(err => { .catch(err => {
alert(err.message); alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
}); });
} }
} }

View file

@ -1,10 +1,15 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><%- i18n('certificates', 'form-title', {provider: provider}) %></h5> <h5 class="modal-title"><%- i18n('certificates', 'form-title', {provider: provider}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel non-loader-content" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form> <div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
<div class="text-center loader-content">
<div class="loader mx-auto my-6"></div>
<p><%- i18n('ssl', 'processing-info') %></p>
</div>
<form class="non-loader-content">
<div class="row"> <div class="row">
<% if (provider === 'letsencrypt') { %> <% if (provider === 'letsencrypt') { %>
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
@ -20,6 +25,99 @@
<input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required> <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required>
</div> </div>
</div> </div>
<!-- DNS challenge -->
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="custom-switch">
<input
type="checkbox"
class="custom-switch-input"
name="meta[dns_challenge]"
value="1"
<%- getUseDnsChallenge() ? 'checked' : '' %>
>
<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">
<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.display_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>
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
<label class="custom-switch"> <label class="custom-switch">
@ -31,6 +129,9 @@
</div> </div>
<% } else if (provider === 'other') { %> <% } else if (provider === 'other') { %>
<!-- Other --> <!-- Other -->
<div class="col-sm-12 col-md-12">
<div class="text-blue mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'passphrase-protection-support-info') %></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">
<label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span></label> <label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span></label>
@ -42,7 +143,7 @@
<div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div> <div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div>
<div class="custom-file"> <div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required> <input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required>
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> <label id="other_certificate_key_label" class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div> </div>
</div> </div>
</div> </div>
@ -51,7 +152,7 @@
<div class="form-label"><%- i18n('certificates', 'other-certificate') %><span class="form-required">*</span></div> <div class="form-label"><%- i18n('certificates', 'other-certificate') %><span class="form-required">*</span></div>
<div class="custom-file"> <div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate"> <input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate">
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> <label id="other_certificate_label" class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +161,7 @@
<div class="form-label"><%- i18n('certificates', 'other-intermediate-certificate') %></div> <div class="form-label"><%- i18n('certificates', 'other-intermediate-certificate') %></div>
<div class="custom-file"> <div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_intermediate_certificate]" id="other_intermediate_certificate"> <input type="file" class="custom-file-input" name="meta[other_intermediate_certificate]" id="other_intermediate_certificate">
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> <label id="other_intermediate_certificate_label" class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div> </div>
</div> </div>
</div> </div>
@ -69,7 +170,7 @@
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer non-loader-content">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button> <button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div> </div>

View file

@ -3,6 +3,8 @@ const Mn = require('backbone.marionette');
const App = require('../../main'); const App = require('../../main');
const CertificateModel = require('../../../models/certificate'); const CertificateModel = require('../../../models/certificate');
const template = require('./form.ejs'); const template = require('./form.ejs');
const i18n = require('../../i18n');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
require('selectize'); require('selectize');
@ -13,42 +15,105 @@ module.exports = Mn.View.extend({
max_file_size: 102400, max_file_size: 102400,
ui: { ui: {
form: 'form', form: 'form',
domain_names: 'input[name="domain_names"]', loader_content: '.loader-content',
buttons: '.modal-footer button', non_loader_content: '.non-loader-content',
cancel: 'button.cancel', le_error_info: '#le-error-info',
save: 'button.save', domain_names: 'input[name="domain_names"]',
other_certificate: '#other_certificate', buttons: '.modal-footer button',
other_certificate_key: '#other_certificate_key', cancel: 'button.cancel',
other_intermediate_certificate: '#other_intermediate_certificate' save: 'button.save',
other_certificate: '#other_certificate',
other_certificate_label: '#other_certificate_label',
other_certificate_key: '#other_certificate_key',
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]"]',
other_certificate_key_label: '#other_certificate_key_label',
other_intermediate_certificate: '#other_intermediate_certificate',
other_intermediate_certificate_label: '#other_intermediate_certificate_label'
}, },
events: { events: {
'change @ui.dns_challenge_switch': function () {
const checked = this.ui.dns_challenge_switch.prop('checked');
if (checked) {
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.dns_provider.prop('required', false);
this.ui.dns_provider_credentials.prop('required', false);
this.ui.dns_challenge_content.hide();
}
},
'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();
}
},
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.le_error_info.hide();
if (!this.ui.form[0].checkValidity()) { if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
$(this).removeClass('btn-loading');
return; return;
} }
let view = this;
let data = this.ui.form.serializeJSON(); let data = this.ui.form.serializeJSON();
data.provider = this.model.get('provider'); data.provider = this.model.get('provider');
// Manipulate
if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') {
data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree;
}
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
let ssl_files = []; let ssl_files = [];
// check files are attached if (data.provider === 'letsencrypt') {
if (this.model.get('provider') === 'other' && !this.model.hasSslFiles()) { if (typeof data.meta === 'undefined') data.meta = {};
let domain_err = false;
if (!data.meta.dns_challenge) {
data.domain_names.split(',').map(function (name) {
if (name.match(/\*/im)) {
domain_err = true;
}
});
}
if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
// Manipulate
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = data.meta.dns_challenge == 1;
if(!data.meta.dns_challenge){
data.meta.dns_provider = undefined;
data.meta.dns_provider_credentials = undefined;
data.meta.propagation_seconds = undefined;
} else {
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(',');
}
} else if (data.provider === 'other' && !this.model.hasSslFiles()) {
// check files are attached
if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) { if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) {
alert('Certificate file is not attached'); alert('Certificate file is not attached');
return; return;
@ -80,18 +145,19 @@ module.exports = Mn.View.extend({
} }
} }
this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); this.ui.loader_content.show();
this.ui.non_loader_content.hide();
// compile file data // compile file data
let form_data = new FormData(); let form_data = new FormData();
if (view.model.get('provider') && ssl_files.length) { if (data.provider === 'other' && ssl_files.length) {
ssl_files.map(function (file) { ssl_files.map(function (file) {
form_data.append(file.name, file.file); form_data.append(file.name, file.file);
}); });
} }
new Promise(resolve => { new Promise(resolve => {
if (view.model.get('provider') === 'other') { if (data.provider === 'other') {
resolve(App.Api.Nginx.Certificates.validate(form_data)); resolve(App.Api.Nginx.Certificates.validate(form_data));
} else { } else {
resolve(); resolve();
@ -101,13 +167,13 @@ module.exports = Mn.View.extend({
return App.Api.Nginx.Certificates.create(data); return App.Api.Nginx.Certificates.create(data);
}) })
.then(result => { .then(result => {
view.model.set(result); this.model.set(result);
// Now upload the certs if we need to // Now upload the certs if we need to
if (view.model.get('provider') === 'other') { if (data.provider === 'other') {
return App.Api.Nginx.Certificates.upload(view.model.get('id'), form_data) return App.Api.Nginx.Certificates.upload(this.model.get('id'), form_data)
.then(result => { .then(result => {
view.model.set('meta', _.assign({}, view.model.get('meta'), result)); this.model.set('meta', _.assign({}, this.model.get('meta'), result));
}); });
} }
}) })
@ -117,20 +183,52 @@ 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.loader_content.hide();
this.ui.non_loader_content.show();
}); });
},
'change @ui.other_certificate_key': function(e){
this.setFileName("other_certificate_key_label", e)
},
'change @ui.other_certificate': function(e){
this.setFileName("other_certificate_label", e)
},
'change @ui.other_intermediate_certificate': function(e){
this.setFileName("other_intermediate_certificate_label", e)
} }
}, },
setFileName(ui, e){
this.getUI(ui).text(e.target.files[0].name)
},
templateContext: { templateContext: {
getLetsencryptEmail: function () { getLetsencryptEmail: function () {
return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email');
}, },
getLetsencryptAgree: function () { getLetsencryptAgree: function () {
return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false;
} },
getUseDnsChallenge: function () {
return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false;
},
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 () { onRender: function () {
@ -144,8 +242,12 @@ module.exports = Mn.View.extend({
text: input text: input
}; };
}, },
createFilter: /^(?:[^.*]+\.?)+[^.]$/ createFilter: /^(?:[^.]+\.?)+[^.]$/
}); });
this.ui.dns_challenge_content.hide();
this.ui.credentials_file_content.hide();
this.ui.loader_content.hide();
this.ui.le_error_info.hide();
}, },
initialize: function (options) { initialize: function (options) {

View file

@ -28,7 +28,7 @@
</div> </div>
</td> </td>
<td> <td>
<%- i18n('ssl', provider) %> <%- i18n('ssl', provider) %><% if (meta.dns_provider) { %> - <%- dns_providers[meta.dns_provider].display_name %><% } %>
</td> </td>
<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') %>

View file

@ -1,7 +1,8 @@
const Mn = require('backbone.marionette'); const Mn = require('backbone.marionette');
const moment = require('moment'); const moment = require('moment');
const App = require('../../../main'); const App = require('../../../main');
const template = require('./item.ejs'); const template = require('./item.ejs');
const dns_providers = require('../../../../../../global/certbot-dns-plugins')
module.exports = Mn.View.extend({ module.exports = Mn.View.extend({
template: template, template: template,
@ -35,7 +36,8 @@ module.exports = Mn.View.extend({
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
}, },
initialize: function () { initialize: function () {

View file

@ -4,6 +4,7 @@
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body has-tabs"> <div class="modal-body 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"> <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="#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>
@ -73,6 +74,98 @@
</div> </div>
</div> </div>
<!-- DNS challenge -->
<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[dns_challenge]"
value="1"
<%- getUseDnsChallenge() ? 'checked' : '' %>
>
<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.display_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 --> <!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt"> <div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group"> <div class="form-group">

View file

@ -4,6 +4,8 @@ const DeadHostModel = require('../../../models/dead-host');
const template = require('./form.ejs'); const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs');
const Helpers = require('../../../lib/helpers'); const Helpers = require('../../../lib/helpers');
const i18n = require('../../i18n');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
require('selectize'); require('selectize');
@ -13,17 +15,24 @@ module.exports = Mn.View.extend({
className: 'modal-dialog', className: 'modal-dialog',
ui: { ui: {
form: 'form', form: 'form',
domain_names: 'input[name="domain_names"]', domain_names: 'input[name="domain_names"]',
buttons: '.modal-footer button', buttons: '.modal-footer button',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save',
certificate_select: 'select[name="certificate_id"]', le_error_info: '#le-error-info',
ssl_forced: 'input[name="ssl_forced"]', certificate_select: 'select[name="certificate_id"]',
hsts_enabled: 'input[name="hsts_enabled"]', ssl_forced: 'input[name="ssl_forced"]',
hsts_subdomains: 'input[name="hsts_subdomains"]', hsts_enabled: 'input[name="hsts_enabled"]',
http2_support: 'input[name="http2_support"]', hsts_subdomains: 'input[name="hsts_subdomains"]',
letsencrypt: '.letsencrypt' http2_support: 'input[name="http2_support"]',
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: {
@ -31,10 +40,12 @@ module.exports = Mn.View.extend({
let id = this.ui.certificate_select.val(); let id = this.ui.certificate_select.val();
if (id === 'new') { if (id === 'new') {
this.ui.letsencrypt.show().find('input').prop('disabled', false); this.ui.letsencrypt.show().find('input').prop('disabled', false);
this.ui.dns_challenge_content.hide();
} else { } else {
this.ui.letsencrypt.hide().find('input').prop('disabled', true); this.ui.letsencrypt.hide().find('input').prop('disabled', true);
} }
let enabled = id === 'new' || parseInt(id, 10) > 0; let enabled = id === 'new' || parseInt(id, 10) > 0;
let inputs = this.ui.ssl_forced.add(this.ui.http2_support); let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
@ -76,8 +87,37 @@ module.exports = Mn.View.extend({
} }
}, },
'change @ui.dns_challenge_switch': function () {
const checked = this.ui.dns_challenge_switch.prop('checked');
if (checked) {
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.dns_provider.prop('required', false);
this.ui.dns_provider_credentials.prop('required', false);
this.ui.dns_challenge_content.hide();
}
},
'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();
}
},
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.le_error_info.hide();
if (!this.ui.form[0].checkValidity()) { if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
@ -88,30 +128,42 @@ module.exports = Mn.View.extend({
let data = this.ui.form.serializeJSON(); let data = this.ui.form.serializeJSON();
// Manipulate // Manipulate
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.http2_support = !!data.http2_support; data.http2_support = !!data.http2_support;
data.ssl_forced = !!data.ssl_forced; data.ssl_forced = !!data.ssl_forced;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = data.meta.dns_challenge == 1;
if(!data.meta.dns_challenge){
data.meta.dns_provider = undefined;
data.meta.dns_provider_credentials = undefined;
data.meta.propagation_seconds = undefined;
} else {
if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
}
if (typeof data.domain_names === 'string' && data.domain_names) { if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(','); data.domain_names = data.domain_names.split(',');
} }
// Check for any domain names containing wildcards, which are not allowed with letsencrypt // Check for any domain names containing wildcards, which are not allowed with letsencrypt
if (data.certificate_id === 'new') { if (data.certificate_id === 'new') {
let domain_err = false; let domain_err = false;
data.domain_names.map(function (name) { if (!data.meta.dns_challenge) {
if (name.match(/\*/im)) { data.domain_names.map(function (name) {
domain_err = true; if (name.match(/\*/im)) {
} domain_err = true;
}); }
});
if (domain_err) {
alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
return;
} }
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
} else { } else {
data.certificate_id = parseInt(data.certificate_id, 10); data.certificate_id = parseInt(data.certificate_id, 10);
} }
@ -127,6 +179,8 @@ module.exports = Mn.View.extend({
} }
this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
this.ui.save.addClass('btn-loading');
method(data) method(data)
.then(result => { .then(result => {
view.model.set(result); view.model.set(result);
@ -138,8 +192,17 @@ module.exports = Mn.View.extend({
}); });
}) })
.catch(err => { .catch(err => {
alert(err.message); let more_info = '';
if(err.code === 500 && err.debug){
try{
more_info = JSON.parse(err.debug).debug.stack.join("\n");
} catch(e) {}
}
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>`:''}`;
this.ui.le_error_info.show();
this.ui.le_error_info[0].scrollIntoView();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
}); });
} }
}, },
@ -147,7 +210,20 @@ module.exports = Mn.View.extend({
templateContext: { templateContext: {
getLetsencryptEmail: function () { getLetsencryptEmail: function () {
return App.Cache.User.get('email'); return App.Cache.User.get('email');
} },
getUseDnsChallenge: function () {
return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false;
},
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 () { onRender: function () {
@ -168,6 +244,9 @@ module.exports = Mn.View.extend({
}); });
// Certificates // 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.letsencrypt.hide();
this.ui.certificate_select.selectize({ this.ui.certificate_select.selectize({
valueField: 'id', valueField: 'id',

View file

@ -4,6 +4,7 @@
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body has-tabs"> <div class="modal-body 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"> <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="#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>
@ -141,6 +142,98 @@
</div> </div>
</div> </div>
<!-- DNS challenge -->
<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[dns_challenge]"
value="1"
<%- getUseDnsChallenge() ? 'checked' : '' %>
>
<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.display_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 --> <!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt"> <div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group"> <div class="form-group">

View file

@ -7,6 +7,8 @@ const certListItemTemplate = require('../certificates-list-item.ejs');
const accessListItemTemplate = require('./access-list-item.ejs'); const accessListItemTemplate = require('./access-list-item.ejs');
const CustomLocation = require('./location'); const CustomLocation = require('./location');
const Helpers = require('../../../lib/helpers'); const Helpers = require('../../../lib/helpers');
const i18n = require('../../i18n');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
@ -19,22 +21,29 @@ module.exports = Mn.View.extend({
locationsCollection: new ProxyLocationModel.Collection(), locationsCollection: new ProxyLocationModel.Collection(),
ui: { ui: {
form: 'form', form: 'form',
domain_names: 'input[name="domain_names"]', domain_names: 'input[name="domain_names"]',
forward_host: 'input[name="forward_host"]', forward_host: 'input[name="forward_host"]',
buttons: '.modal-footer button', buttons: '.modal-footer button',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save',
add_location_btn: 'button.add_location', add_location_btn: 'button.add_location',
locations_container:'.locations_container', locations_container: '.locations_container',
certificate_select: 'select[name="certificate_id"]', le_error_info: '#le-error-info',
access_list_select: 'select[name="access_list_id"]', certificate_select: 'select[name="certificate_id"]',
ssl_forced: 'input[name="ssl_forced"]', access_list_select: 'select[name="access_list_id"]',
hsts_enabled: 'input[name="hsts_enabled"]', ssl_forced: 'input[name="ssl_forced"]',
hsts_subdomains: 'input[name="hsts_subdomains"]', hsts_enabled: 'input[name="hsts_enabled"]',
http2_support: 'input[name="http2_support"]', hsts_subdomains: 'input[name="hsts_subdomains"]',
forward_scheme: 'select[name="forward_scheme"]', http2_support: 'input[name="http2_support"]',
letsencrypt: '.letsencrypt' 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]"]',
forward_scheme: 'select[name="forward_scheme"]',
letsencrypt: '.letsencrypt'
}, },
regions: { regions: {
@ -46,6 +55,7 @@ module.exports = Mn.View.extend({
let id = this.ui.certificate_select.val(); let id = this.ui.certificate_select.val();
if (id === 'new') { if (id === 'new') {
this.ui.letsencrypt.show().find('input').prop('disabled', false); this.ui.letsencrypt.show().find('input').prop('disabled', false);
this.ui.dns_challenge_content.hide();
} else { } else {
this.ui.letsencrypt.hide().find('input').prop('disabled', true); this.ui.letsencrypt.hide().find('input').prop('disabled', true);
} }
@ -91,6 +101,34 @@ module.exports = Mn.View.extend({
} }
}, },
'change @ui.dns_challenge_switch': function () {
const checked = this.ui.dns_challenge_switch.prop('checked');
if (checked) {
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.dns_provider.prop('required', false);
this.ui.dns_provider_credentials.prop('required', false);
this.ui.dns_challenge_content.hide();
}
},
'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();
}
},
'click @ui.add_location_btn': function (e) { 'click @ui.add_location_btn': function (e) {
e.preventDefault(); e.preventDefault();
@ -100,6 +138,7 @@ module.exports = Mn.View.extend({
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.le_error_info.hide();
if (!this.ui.form[0].checkValidity()) { if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
@ -128,26 +167,38 @@ 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;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = data.meta.dns_challenge == 1;
if(!data.meta.dns_challenge){
data.meta.dns_provider = undefined;
data.meta.dns_provider_credentials = undefined;
data.meta.propagation_seconds = undefined;
} else {
if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
}
if (typeof data.domain_names === 'string' && data.domain_names) { if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(','); data.domain_names = data.domain_names.split(',');
} }
// Check for any domain names containing wildcards, which are not allowed with letsencrypt // Check for any domain names containing wildcards, which are not allowed with letsencrypt
if (data.certificate_id === 'new') { if (data.certificate_id === 'new') {
let domain_err = false; let domain_err = false;
data.domain_names.map(function (name) { if (!data.meta.dns_challenge) {
if (name.match(/\*/im)) { data.domain_names.map(function (name) {
domain_err = true; if (name.match(/\*/im)) {
} domain_err = true;
}); }
});
if (domain_err) {
alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
return;
} }
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
} else { } else {
data.certificate_id = parseInt(data.certificate_id, 10); data.certificate_id = parseInt(data.certificate_id, 10);
} }
@ -163,6 +214,8 @@ module.exports = Mn.View.extend({
} }
this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
this.ui.save.addClass('btn-loading');
method(data) method(data)
.then(result => { .then(result => {
view.model.set(result); view.model.set(result);
@ -174,8 +227,17 @@ module.exports = Mn.View.extend({
}); });
}) })
.catch(err => { .catch(err => {
alert(err.message); let more_info = '';
if(err.code === 500 && err.debug){
try{
more_info = JSON.parse(err.debug).debug.stack.join("\n");
} catch(e) {}
}
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>`:''}`;
this.ui.le_error_info.show();
this.ui.le_error_info[0].scrollIntoView();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
}); });
} }
}, },
@ -183,7 +245,20 @@ module.exports = Mn.View.extend({
templateContext: { templateContext: {
getLetsencryptEmail: function () { getLetsencryptEmail: function () {
return App.Cache.User.get('email'); return App.Cache.User.get('email');
} },
getUseDnsChallenge: function () {
return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false;
},
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 () { onRender: function () {
@ -203,7 +278,7 @@ module.exports = Mn.View.extend({
text: input text: input
}; };
}, },
createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ createFilter: /^(?:\.)?(?:[^.*]+\.?)+[^.]$/
}); });
// Access Lists // Access Lists
@ -237,6 +312,9 @@ module.exports = Mn.View.extend({
}); });
// Certificates // 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.letsencrypt.hide();
this.ui.certificate_select.selectize({ this.ui.certificate_select.selectize({
valueField: 'id', valueField: 'id',

View file

@ -38,7 +38,7 @@
<div class="col-sm-5 col-md-5"> <div class="col-sm-5 col-md-5">
<div class="form-group"> <div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label> <label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label>
<input type="text" name="forward_host" class="form-control text-monospace model" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="50" required> <input type="text" name="forward_host" class="form-control text-monospace model" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="200" required>
<span style="font-size: 9px;"><%- i18n('proxy-hosts', 'custom-forward-host-help') %></span> <span style="font-size: 9px;"><%- i18n('proxy-hosts', 'custom-forward-host-help') %></span>
</div> </div>
</div> </div>
@ -61,4 +61,4 @@
<i class="fa fa-trash"></i> <%- i18n('locations', 'delete') %> <i class="fa fa-trash"></i> <%- i18n('locations', 'delete') %>
</a> </a>
</div> </div>
</div> </div>

View file

@ -4,6 +4,7 @@
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body has-tabs"> <div class="modal-body 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"> <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="#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>
@ -97,6 +98,98 @@
</div> </div>
</div> </div>
<!-- DNS challenge -->
<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[dns_challenge]"
value="1"
<%- getUseDnsChallenge() ? 'checked' : '' %>
>
<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.display_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 --> <!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt"> <div class="col-sm-12 col-md-12 letsencrypt">
<div class="form-group"> <div class="form-group">

View file

@ -4,6 +4,9 @@ const RedirectionHostModel = require('../../../models/redirection-host');
const template = require('./form.ejs'); const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs');
const Helpers = require('../../../lib/helpers'); const Helpers = require('../../../lib/helpers');
const i18n = require('../../i18n');
const dns_providers = require('../../../../../global/certbot-dns-plugins');
require('jquery-serializejson'); require('jquery-serializejson');
require('selectize'); require('selectize');
@ -13,17 +16,24 @@ module.exports = Mn.View.extend({
className: 'modal-dialog', className: 'modal-dialog',
ui: { ui: {
form: 'form', form: 'form',
domain_names: 'input[name="domain_names"]', domain_names: 'input[name="domain_names"]',
buttons: '.modal-footer button', buttons: '.modal-footer button',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save',
certificate_select: 'select[name="certificate_id"]', le_error_info: '#le-error-info',
ssl_forced: 'input[name="ssl_forced"]', certificate_select: 'select[name="certificate_id"]',
hsts_enabled: 'input[name="hsts_enabled"]', ssl_forced: 'input[name="ssl_forced"]',
hsts_subdomains: 'input[name="hsts_subdomains"]', hsts_enabled: 'input[name="hsts_enabled"]',
http2_support: 'input[name="http2_support"]', hsts_subdomains: 'input[name="hsts_subdomains"]',
letsencrypt: '.letsencrypt' http2_support: 'input[name="http2_support"]',
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: {
@ -31,6 +41,7 @@ module.exports = Mn.View.extend({
let id = this.ui.certificate_select.val(); let id = this.ui.certificate_select.val();
if (id === 'new') { if (id === 'new') {
this.ui.letsencrypt.show().find('input').prop('disabled', false); this.ui.letsencrypt.show().find('input').prop('disabled', false);
this.ui.dns_challenge_content.hide();
} else { } else {
this.ui.letsencrypt.hide().find('input').prop('disabled', true); this.ui.letsencrypt.hide().find('input').prop('disabled', true);
} }
@ -76,8 +87,37 @@ module.exports = Mn.View.extend({
} }
}, },
'change @ui.dns_challenge_switch': function () {
const checked = this.ui.dns_challenge_switch.prop('checked');
if (checked) {
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.dns_provider.prop('required', false);
this.ui.dns_provider_credentials.prop('required', false);
this.ui.dns_challenge_content.hide();
}
},
'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();
}
},
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.le_error_info.hide();
if (!this.ui.form[0].checkValidity()) { if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
@ -88,32 +128,44 @@ module.exports = Mn.View.extend({
let data = this.ui.form.serializeJSON(); let data = this.ui.form.serializeJSON();
// Manipulate // Manipulate
data.block_exploits = !!data.block_exploits; data.block_exploits = !!data.block_exploits;
data.preserve_path = !!data.preserve_path; data.preserve_path = !!data.preserve_path;
data.http2_support = !!data.http2_support; data.http2_support = !!data.http2_support;
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;
if (typeof data.meta === 'undefined') data.meta = {};
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
data.meta.dns_challenge = data.meta.dns_challenge == 1;
if(!data.meta.dns_challenge){
data.meta.dns_provider = undefined;
data.meta.dns_provider_credentials = undefined;
data.meta.propagation_seconds = undefined;
} else {
if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
}
if (typeof data.domain_names === 'string' && data.domain_names) { if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(','); data.domain_names = data.domain_names.split(',');
} }
// Check for any domain names containing wildcards, which are not allowed with letsencrypt // Check for any domain names containing wildcards, which are not allowed with letsencrypt
if (data.certificate_id === 'new') { if (data.certificate_id === 'new') {
let domain_err = false; let domain_err = false;
data.domain_names.map(function (name) { if (!data.meta.dns_challenge) {
if (name.match(/\*/im)) { data.domain_names.map(function (name) {
domain_err = true; if (name.match(/\*/im)) {
} domain_err = true;
}); }
});
if (domain_err) {
alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
return;
} }
data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; if (domain_err) {
alert(i18n('ssl', 'no-wildcard-without-dns'));
return;
}
} else { } else {
data.certificate_id = parseInt(data.certificate_id, 10); data.certificate_id = parseInt(data.certificate_id, 10);
} }
@ -129,6 +181,8 @@ module.exports = Mn.View.extend({
} }
this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
this.ui.save.addClass('btn-loading');
method(data) method(data)
.then(result => { .then(result => {
view.model.set(result); view.model.set(result);
@ -140,8 +194,17 @@ module.exports = Mn.View.extend({
}); });
}) })
.catch(err => { .catch(err => {
alert(err.message); let more_info = '';
if(err.code === 500 && err.debug){
try{
more_info = JSON.parse(err.debug).debug.stack.join("\n");
} catch(e) {}
}
this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>`:''}`;
this.ui.le_error_info.show();
this.ui.le_error_info[0].scrollIntoView();
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
this.ui.save.removeClass('btn-loading');
}); });
} }
}, },
@ -149,7 +212,20 @@ module.exports = Mn.View.extend({
templateContext: { templateContext: {
getLetsencryptEmail: function () { getLetsencryptEmail: function () {
return App.Cache.User.get('email'); return App.Cache.User.get('email');
} },
getUseDnsChallenge: function () {
return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false;
},
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 () { onRender: function () {
@ -170,6 +246,9 @@ module.exports = Mn.View.extend({
}); });
// Certificates // 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.letsencrypt.hide();
this.ui.certificate_select.selectize({ this.ui.certificate_select.selectize({
valueField: 'id', valueField: 'id',

View file

@ -101,7 +101,19 @@
"letsencrypt-email": "Email Address for Let's Encrypt", "letsencrypt-email": "Email Address for Let's Encrypt",
"letsencrypt-agree": "I Agree to the <a href=\"{url}\" target=\"_blank\">Let's Encrypt Terms of Service</a>", "letsencrypt-agree": "I Agree to the <a href=\"{url}\" target=\"_blank\">Let's Encrypt Terms of Service</a>",
"delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.",
"hosts-warning": "These domains must be already configured to point to this installation" "hosts-warning": "These domains must be already configured to point to this installation",
"no-wildcard-without-dns": "Cannot request Let's Encrypt Certificate for wildcard domains when not using DNS challenge",
"dns-challenge": "Use a DNS Challenge",
"certbot-warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.",
"dns-provider": "DNS Provider",
"please-choose": "Please Choose...",
"credentials-file-content": "Credentials File Content",
"credentials-file-content-info": "This plugin requires a configuration file containing an API token or other credentials to your provider",
"stored-as-plaintext-info": "This data will be stored as plaintext in the database and in a file!",
"propagation-seconds": "Propagation Seconds",
"propagation-seconds-info": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.",
"processing-info": "Processing... This might take a few minutes.",
"passphrase-protection-support-info": "Key files protected with a passphrase are not supported."
}, },
"proxy-hosts": { "proxy-hosts": {
"title": "Proxy Hosts", "title": "Proxy Hosts",
@ -195,7 +207,8 @@
"authorization": "Authorization", "authorization": "Authorization",
"access": "Access", "access": "Access",
"satisfy": "Satisfy", "satisfy": "Satisfy",
"satisfy-any": "Satisfy Any" "satisfy-any": "Satisfy Any",
"pass-auth": "Pass Auth to Host"
}, },
"users": { "users": {
"title": "Users", "title": "Users",

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,305 @@
/**
* This file contains info about available Certbot DNS plugins.
* This only works for plugins which use the standard argument structure, so:
* --authenticator <plugin-name> --<plugin-name>-credentials <FILE> --<plugin-name>-propagation-seconds <number>
*
* File Structure:
*
* {
* cloudflare: {
* display_name: "Name displayed to the user",
* package_name: "Package name in PyPi repo",
* package_version: "Package version in PyPi repo",
* dependencies: "Additional dependencies, space separated (as you would pass it to pip install)",
* credentials: `Template of the credentials file`,
* full_plugin_name: "The full plugin name as used in the commandline with certbot, including prefixes, e.g. 'certbot-dns-njalla:dns-njalla'",
* },
* ...
* }
*
*/
module.exports = {
aliyun: {
display_name: 'Aliyun',
package_name: 'certbot-dns-aliyun',
package_version: '0.38.1',
dependencies: '',
credentials: `certbot_dns_aliyun:dns_aliyun_access_key = 12345678
certbot_dns_aliyun:dns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef`,
full_plugin_name: 'certbot-dns-aliyun:dns-aliyun',
},
//####################################################//
cloudflare: {
display_name: 'Cloudflare',
package_name: 'certbot-dns-cloudflare',
package_version: '1.8.0',
dependencies: 'cloudflare',
credentials: `# Cloudflare API token
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567`,
full_plugin_name: 'dns-cloudflare',
},
//####################################################//
cloudxns: {
display_name: 'CloudXNS',
package_name: 'certbot-dns-cloudxns',
package_version: '1.8.0',
dependencies: '',
credentials: `dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef
dns_cloudxns_secret_key = 1122334455667788`,
full_plugin_name: 'dns-cloudxns',
},
//####################################################//
corenetworks: {
display_name: 'Core Networks',
package_name: 'certbot-dns-corenetworks',
package_version: '0.1.4',
dependencies: '',
credentials: `certbot_dns_corenetworks:dns_corenetworks_username = asaHB12r
certbot_dns_corenetworks:dns_corenetworks_password = secure_password`,
full_plugin_name: 'certbot-dns-corenetworks:dns-corenetworks',
},
//####################################################//
cpanel: {
display_name: 'cPanel',
package_name: 'certbot-dns-cpanel',
package_version: '0.2.2',
dependencies: '',
credentials: `certbot_dns_cpanel:cpanel_url = https://cpanel.example.com:2083
certbot_dns_cpanel:cpanel_username = user
certbot_dns_cpanel:cpanel_password = hunter2`,
full_plugin_name: 'certbot-dns-cpanel:cpanel',
},
//####################################################//
digitalocean: {
display_name: 'DigitalOcean',
package_name: 'certbot-dns-digitalocean',
package_version: '1.8.0',
dependencies: '',
credentials: 'dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff',
full_plugin_name: 'dns-digitalocean',
},
//####################################################//
directadmin: {
display_name: 'DirectAdmin',
package_name: 'certbot-dns-directadmin',
package_version: '0.0.20',
dependencies: '',
credentials: `directadmin_url = https://my.directadminserver.com:2222
directadmin_username = username
directadmin_password = aSuperStrongPassword`,
full_plugin_name: 'certbot-dns-directadmin:directadmin',
},
//####################################################//
dnsimple: {
display_name: 'DNSimple',
package_name: 'certbot-dns-dnsimple',
package_version: '1.8.0',
dependencies: '',
credentials: 'dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw',
full_plugin_name: 'dns-dnsimple',
},
//####################################################//
dnsmadeeasy: {
display_name: 'DNS Made Easy',
package_name: 'certbot-dns-dnsmadeeasy',
package_version: '1.8.0',
dependencies: '',
credentials: `dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a
dns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55`,
full_plugin_name: 'dns-dnsmadeeasy',
},
//####################################################//
dnspod: {
display_name: 'DNSPod',
package_name: 'certbot-dns-dnspod',
package_version: '0.1.0',
dependencies: '',
credentials: `certbot_dns_dnspod:dns_dnspod_email = "DNSPOD-API-REQUIRES-A-VALID-EMAIL"
certbot_dns_dnspod:dns_dnspod_api_token = "DNSPOD-API-TOKEN"`,
full_plugin_name: 'certbot-dns-dnspod:dns-dnspod',
},
//####################################################//
gandi: {
display_name: 'Gandi Live DNS',
package_name: 'certbot_plugin_gandi',
package_version: '1.2.5',
dependencies: '',
credentials: 'certbot_plugin_gandi:dns_api_key = APIKEY',
full_plugin_name: 'certbot-plugin-gandi:dns',
},
//####################################################//
google: {
display_name: 'Google',
package_name: 'certbot-dns-google',
package_version: '1.8.0',
dependencies: '',
credentials: `{
"type": "service_account",
...
}`,
full_plugin_name: 'dns-google',
},
//####################################################//
hetzner: {
display_name: 'Hetzner',
package_name: 'certbot-dns-hetzner',
package_version: '1.0.4',
dependencies: '',
credentials: 'certbot_dns_hetzner:dns_hetzner_api_token = 0123456789abcdef0123456789abcdef',
full_plugin_name: 'certbot-dns-hetzner:dns-hetzner',
},
//####################################################//
inwx: {
display_name: 'INWX',
package_name: 'certbot-dns-inwx',
package_version: '2.1.2',
dependencies: '',
credentials: `certbot_dns_inwx:dns_inwx_url = https://api.domrobot.com/xmlrpc/
certbot_dns_inwx:dns_inwx_username = your_username
certbot_dns_inwx:dns_inwx_password = your_password
certbot_dns_inwx:dns_inwx_shared_secret = your_shared_secret optional`,
full_plugin_name: 'certbot-dns-inwx:dns-inwx',
},
//####################################################//
ispconfig: {
display_name: 'ISPConfig',
package_name: 'certbot-dns-ispconfig',
package_version: '0.2.0',
dependencies: '',
credentials: `certbot_dns_ispconfig:dns_ispconfig_username = myremoteuser
certbot_dns_ispconfig:dns_ispconfig_password = verysecureremoteuserpassword
certbot_dns_ispconfig:dns_ispconfig_endpoint = https://localhost:8080`,
full_plugin_name: 'certbot-dns-ispconfig:dns-ispconfig',
},
//####################################################//
isset: {
display_name: 'Isset',
package_name: 'certbot-dns-isset',
package_version: '0.0.3',
dependencies: '',
credentials: `certbot_dns_isset:dns_isset_endpoint="https://customer.isset.net/api"
certbot_dns_isset:dns_isset_token="<token>"`,
full_plugin_name: 'certbot-dns-isset:dns-isset',
},
//####################################################//
linode: {
display_name: 'Linode',
package_name: 'certbot-dns-linode',
package_version: '1.8.0',
dependencies: '',
credentials: `dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64
dns_linode_version = [<blank>|3|4]`,
full_plugin_name: 'dns-linode',
},
//####################################################//
luadns: {
display_name: 'LuaDNS',
package_name: 'certbot-dns-luadns',
package_version: '1.8.0',
dependencies: '',
credentials: `dns_luadns_email = user@example.com
dns_luadns_token = 0123456789abcdef0123456789abcdef`,
full_plugin_name: 'dns-luadns',
},
//####################################################//
netcup: {
display_name: 'netcup',
package_name: 'certbot-dns-netcup',
package_version: '1.0.0',
dependencies: '',
credentials: `dns_netcup_customer_id = 123456
dns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567
dns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123`,
full_plugin_name: 'certbot-dns-netcup:dns-netcup',
},
//####################################################//
njalla: {
display_name: 'Njalla',
package_name: 'certbot-dns-njalla',
package_version: '1.0.0',
dependencies: '',
credentials: 'certbot_dns_njalla:dns_njalla_token = 0123456789abcdef0123456789abcdef01234567',
full_plugin_name: 'certbot-dns-njalla:dns-njalla',
},
//####################################################//
nsone: {
display_name: 'NS1',
package_name: 'certbot-dns-nsone',
package_version: '1.8.0',
dependencies: '',
credentials: 'dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw',
full_plugin_name: 'dns-nsone',
},
//####################################################//
ovh: {
display_name: 'OVH',
package_name: 'certbot-dns-ovh',
package_version: '1.8.0',
dependencies: '',
credentials: `dns_ovh_endpoint = ovh-eu
dns_ovh_application_key = MDAwMDAwMDAwMDAw
dns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw
dns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw`,
full_plugin_name: 'dns-ovh',
},
//####################################################//
powerdns: {
display_name: 'PowerDNS',
package_name: 'certbot-dns-powerdns',
package_version: '0.2.0',
dependencies: '',
credentials: `certbot_dns_powerdns:dns_powerdns_api_url = https://api.mypowerdns.example.org
certbot_dns_powerdns:dns_powerdns_api_key = AbCbASsd!@34`,
full_plugin_name: 'certbot-dns-powerdns:dns-powerdns',
},
//####################################################//
rfc2136: {
display_name: 'RFC 2136',
package_name: 'certbot-dns-rfc2136',
package_version: '1.8.0',
dependencies: '',
credentials: `# Target DNS server
dns_rfc2136_server = 192.0.2.1
# Target DNS port
dns_rfc2136_port = 53
# TSIG key name
dns_rfc2136_name = keyname.
# TSIG key secret
dns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==
# TSIG key algorithm
dns_rfc2136_algorithm = HMAC-SHA512`,
full_plugin_name: 'dns-rfc2136',
},
//####################################################//
route53: {
display_name: 'Route 53 (Amazon)',
package_name: 'certbot-dns-route53',
package_version: '1.8.0',
dependencies: '',
credentials: `[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`,
full_plugin_name: 'dns-route53',
},
//####################################################//
vultr: {
display_name: 'Vultr',
package_name: 'certbot-dns-vultr',
package_version: '1.0.3',
dependencies: '',
credentials: 'certbot_dns_vultr:dns_vultr_key = YOUR_VULTR_API_KEY',
full_plugin_name: 'certbot-dns-vultr:dns-vultr',
},
//####################################################//
eurodns: {
display_name: 'EuroDNS',
package_name: 'certbot-dns-eurodns',
package_version: '0.0.4',
dependencies: '',
credentials: `dns_eurodns_applicationId = myuser
dns_eurodns_apiKey = mysecretpassword
dns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy`,
full_plugin_name: 'certbot-dns-eurodns:dns-eurodns',
},
};

View file

@ -10,7 +10,7 @@ if hash docker 2>/dev/null; then
docker pull "${DOCKER_IMAGE}" docker pull "${DOCKER_IMAGE}"
cd "${DIR}/.." cd "${DIR}/.."
echo -e "${BLUE} ${CYAN}Building Frontend ...${RESET}" echo -e "${BLUE} ${CYAN}Building Frontend ...${RESET}"
docker run --rm -e CI=true -v "$(pwd)/frontend:/app/frontend" -w /app/frontend "$DOCKER_IMAGE" sh -c "yarn install && yarn build && yarn build && chown -R $(id -u):$(id -g) /app/frontend" docker run --rm -e CI=true -v "$(pwd)/frontend:/app/frontend" -v "$(pwd)/global:/app/global" -w /app/frontend "$DOCKER_IMAGE" sh -c "yarn install && yarn build && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
echo -e "${BLUE} ${GREEN}Building Frontend Complete${RESET}" echo -e "${BLUE} ${GREEN}Building Frontend Complete${RESET}"
else else
echo -e "${RED} docker command is not available${RESET}" echo -e "${RED} docker command is not available${RESET}"

View file

@ -7,7 +7,7 @@ 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}Testing Dev Stack ...${RESET}" echo -e "${BLUE} ${CYAN}Testing Dev Stack ...${RESET}"
docker-compose exec -T npm bash -c "cd /app/backend && task test" docker-compose exec -T npm bash -c "cd /app && task test"
else else
echo -e "${RED} docker-compose command is not available${RESET}" echo -e "${RED} docker-compose command is not available${RESET}"
fi fi

1
test/.dockerignore Normal file
View file

@ -0,0 +1 @@
node_modules

3
test/.gitignore vendored
View file

@ -1,3 +1,4 @@
.vscode .vscode
node_modules node_modules
results
cypress/videos

View file

@ -1,6 +1,11 @@
FROM cypress/included:4.6.0 FROM cypress/included:5.6.0
COPY --chown=1000 ./test /test COPY --chown=1000 ./ /test
# mkcert
ENV MKCERT=1.4.2
RUN wget -O /usr/bin/mkcert "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT}/mkcert-v${MKCERT}-linux-amd64" \
&& chmod +x /usr/bin/mkcert
WORKDIR /test WORKDIR /test
RUN yarn install RUN yarn install

View file

@ -1,15 +1,12 @@
{ {
"requestTimeout": 30000, "requestTimeout": 30000,
"defaultCommandTimeout": 20000, "defaultCommandTimeout": 20000,
"reporter": "mocha-junit-reporter", "reporter": "cypress-multi-reporters",
"reporterOptions": { "reporterOptions": {
"jenkinsMode": true, "configFile": "multi-reporter.json"
"rootSuiteTitle": "Cypress",
"jenkinsClassnamePrefix": "Cypress.",
"mochaFile": "/results/junit/my-test-output-[hash].xml"
}, },
"videosFolder": "/results/videos", "videosFolder": "results/videos",
"screenshotsFolder": "/results/screenshots", "screenshotsFolder": "results/screenshots",
"env": { "env": {
"swaggerBase": "{{baseUrl}}/api/schema", "swaggerBase": "{{baseUrl}}/api/schema",
"RETRIES": 4 "RETRIES": 4

View file

@ -1,13 +1,14 @@
{ {
"requestTimeout": 30000, "requestTimeout": 30000,
"defaultCommandTimeout": 20000, "defaultCommandTimeout": 20000,
"reporter": "junit", "reporter": "cypress-multi-reporters",
"reporterOptions": { "reporterOptions": {
"mochaFile": "results/junit/my-test-output-[hash].xml" "configFile": "multi-reporter.json"
}, },
"video": false, "videos": false,
"screenshotsFolder": "cypress/results/screenshots", "screenshotsFolder": "results/screenshots",
"env": { "env": {
"swaggerBase": "{{baseUrl}}/api/schema" "swaggerBase": "{{baseUrl}}/api/schema",
"RETRIES": 0
} }
} }

View file

@ -2,17 +2,15 @@
describe('Basic API checks', () => { describe('Basic API checks', () => {
it('Should return a valid health payload', function () { it('Should return a valid health payload', function () {
cy.wait(2000);
cy.task('backendApiGet', { cy.task('backendApiGet', {
path: '/api/', path: '/api/',
}).then((data) => { }).then((data) => {
// Check the swagger schema: // Check the swagger schema:
cy.validateSwaggerSchema('get', '/', data); cy.validateSwaggerSchema('get', 200, '/', data);
}); });
}); });
it('Should return a valid schema payload', function () { it('Should return a valid schema payload', function () {
cy.wait(2000);
cy.task('backendApiGet', { cy.task('backendApiGet', {
path: '/api/schema', path: '/api/schema',
}).then((data) => { }).then((data) => {

View file

@ -0,0 +1,48 @@
/// <reference types="Cypress" />
describe('Users endpoints', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to get yourself', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/users/me'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
});
});
it('Should be able to get all users', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/users'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data.length).to.be.greaterThan(0);
});
});
it('Should be able to update yourself', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/users/me',
data: {
name: 'changed name'
}
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.name).to.be.equal('changed name');
});
});
});

View file

@ -13,82 +13,30 @@
* Check the swagger schema: * Check the swagger schema:
* *
* @param {string} method API Method in swagger doc, "get", "put", "post", "delete" * @param {string} method API Method in swagger doc, "get", "put", "post", "delete"
* @param {number} statusCode API status code in swagger doc
* @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc * @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc
* @param {*} data The API response data to check against the swagger schema * @param {*} data The API response data to check against the swagger schema
*/ */
Cypress.Commands.add('validateSwaggerSchema', (method, path, data) => { Cypress.Commands.add('validateSwaggerSchema', (method, statusCode, path, data) => {
cy.task('validateSwaggerSchema', { cy.task('validateSwaggerSchema', {
file: Cypress.env('swaggerBase'), file: Cypress.env('swaggerBase'),
endpoint: path, endpoint: path,
method: method, method: method,
statusCode: 200, statusCode: statusCode,
responseSchema: data, responseSchema: data,
verbose: true verbose: true
}).should('equal', null); }).should('equal', null);
}); });
Cypress.Commands.add('getToken', () => { Cypress.Commands.add('getToken', () => {
cy.task('backendApiGet', { // login with existing user
path: '/api/', cy.task('backendApiPost', {
}).then((data) => { path: '/api/tokens',
// Check the swagger schema:
cy.task('validateSwaggerSchema', {
endpoint: '/',
method: 'get',
statusCode: 200,
responseSchema: data,
verbose: true,
}).should('equal', null);
if (!data.result.setup) {
cy.log('Setup = false');
// create a new user
cy.createInitialUser().then(() => {
return cy.getToken();
});
} else {
cy.log('Setup = true');
// login with existing user
cy.task('backendApiPost', {
path: '/api/tokens',
data: {
type: 'password',
identity: 'jc@jc21.com',
secret: 'changeme'
}
}).then(res => {
cy.wrap(res.result.token);
});
}
});
});
Cypress.Commands.add('createInitialUser', () => {
return cy.task('backendApiPost', {
path: '/api/users',
data: { data: {
name: 'Jamie Curnow', identity: 'admin@example.com',
nickname: 'James', secret: 'changeme'
email: 'jc@jc21.com',
roles: [],
is_disabled: false,
auth: {
type: 'password',
secret: 'changeme'
}
} }
}).then((data) => { }).then(res => {
// Check the swagger schema: cy.wrap(res.token);
cy.task('validateSwaggerSchema', {
endpoint: '/users',
method: 'post',
statusCode: 201,
responseSchema: data,
verbose: true
}).should('equal', null);
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
cy.wrap(data.result);
}); });
}); });

9
test/multi-reporter.json Normal file
View file

@ -0,0 +1,9 @@
{
"reporterEnabled": "spec, mocha-junit-reporter",
"mochaJunitReporterReporterOptions": {
"jenkinsMode": true,
"rootSuiteTitle": "Cypress.npm",
"jenkinsClassnamePrefix": "Cypress.npm.",
"mochaFile": "results/junit/cypress.npm.[hash].xml"
}
}

View file

@ -4,21 +4,23 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@jc21/cypress-swagger-validation": "^0.0.5", "@jc21/cypress-swagger-validation": "^0.0.9",
"@jc21/restler": "^3.4.0", "@jc21/restler": "^3.4.0",
"chalk": "^3.0.0", "chalk": "^4.1.0",
"cypress": "^4.6.0", "cypress": "^5.6.0",
"cypress-multi-reporters": "^1.4.0",
"cypress-plugin-retries": "^1.5.2", "cypress-plugin-retries": "^1.5.2",
"eslint": "^6.7.2", "eslint": "^7.6.0",
"eslint-plugin-align-assignments": "^1.1.2", "eslint-plugin-align-assignments": "^1.1.2",
"eslint-plugin-chai-friendly": "^0.5.0", "eslint-plugin-chai-friendly": "^0.6.0",
"eslint-plugin-cypress": "^2.8.0", "eslint-plugin-cypress": "^2.11.1",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"mocha": "^6.2.2", "mocha": "^8.1.1",
"mocha-junit-reporter": "^1.23.1" "mocha-junit-reporter": "^2.0.0"
}, },
"scripts": { "scripts": {
"cypress": "cypress open --config-file=cypress/config/dev.json --config baseUrl=http://127.0.0.1:3081" "cypress": "cypress open --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}",
"cypress:headless": "cypress run --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC"

File diff suppressed because it is too large Load diff