Compare commits

..

No commits in common. "develop" and "v2.2.1" have entirely different histories.

197 changed files with 6903 additions and 13430 deletions

View File

@ -6,30 +6,15 @@ labels: bug
assignees: '' 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?
- Yes / No
- Are you sure you're not using someone else's docker image? - Are you sure you're not using someone else's docker image?
- Yes / No - If having problems with Lets Encrypt, have you made absolutely sure your site is accessible from outside of your network?
- Have you searched for similar issues (both open and closed)?
- Yes / No
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. --> - A clear and concise description of what the bug is.
- What version of Nginx Proxy Manager is reported on the login page?
**Nginx Proxy Manager Version**
<!-- What version of Nginx Proxy Manager is reported on the login page? -->
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
@ -38,18 +23,14 @@ Steps to reproduce the behavior:
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Expected behavior** **Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
<!-- If applicable, add screenshots to help explain your problem. --> If applicable, add screenshots to help explain your problem.
**Operating System** **Operating System**
<!-- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. --> - Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error.
**Additional context** **Additional context**
<!-- Add any other context about the problem here, docker version, browser version, logs if applicable to the problem. Too much info is better than too little. --> Add any other context about the problem here, docker version, browser version if applicable to the problem. Too much info is better than too little.

View File

@ -1,18 +0,0 @@
---
name: DNS challenge provider request
about: Suggest a new provider to be available for a certificate DNS challenge
title: ''
labels: dns provider request
assignees: ''
---
**What provider would you like to see added to NPM?**
<!-- What is this provider called? -->
**Have you checked if a certbot plugin exists?**
<!--
Currently NPM only supports DNS challenge providers for which a certbot plugin exists.
You can visit pypi.org, and search for a package with the name `certbot-dns-<privider>`.
-->

View File

@ -7,26 +7,14 @@ 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.*
-->
**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 [...]
**Describe the solution you'd like** **Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. --> A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. --> A clear and concise description of any alternative solutions or features you've considered.
**Additional context** **Additional context**
<!-- Add any other context or screenshots about the feature request here. --> Add any other context or screenshots about the feature request here.

2
.gitignore vendored
View File

@ -2,4 +2,4 @@
.idea .idea
._* ._*
.vscode .vscode
certbot-help.txt

10
.jenkins/config.json Normal file
View File

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

View File

@ -1 +1 @@
2.9.19 2.2.1

64
Jenkinsfile vendored
View File

@ -5,7 +5,6 @@ pipeline {
options { options {
buildDiscarder(logRotator(numToKeepStr: '5')) buildDiscarder(logRotator(numToKeepStr: '5'))
disableConcurrentBuilds() disableConcurrentBuilds()
ansiColor('xterm')
} }
environment { environment {
IMAGE = "nginx-proxy-manager" IMAGE = "nginx-proxy-manager"
@ -56,19 +55,20 @@ pipeline {
} }
stage('Frontend') { stage('Frontend') {
steps { steps {
ansiColor('xterm') {
sh './scripts/frontend-build' sh './scripts/frontend-build'
} }
} }
}
stage('Backend') { stage('Backend') {
steps { steps {
ansiColor('xterm') {
echo 'Checking Syntax ...' echo 'Checking Syntax ...'
sh 'docker pull nginxproxymanager/nginx-full:certbot-node'
// 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 \\
nginxproxymanager/nginx-full:certbot-node \\ node:latest \\
sh -c "yarn install && yarn eslint . && rm -rf node_modules" sh -c "yarn install && yarn eslint . && rm -rf node_modules"
''' '''
@ -85,49 +85,26 @@ pipeline {
''' '''
} }
} }
stage('Integration Tests Sqlite') { }
stage('Test') {
steps { steps {
ansiColor('xterm') {
// Bring up a stack // Bring up a stack
sh 'docker-compose up -d fullstack-sqlite' sh 'docker-compose up -d fullstack'
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120' sh './scripts/wait-healthy $(docker-compose ps -q fullstack) 120'
// Run tests // Run tests
sh 'rm -rf test/results' sh 'rm -rf test/results'
sh 'docker-compose up cypress-sqlite' sh 'docker-compose up cypress'
// Get results // Get results
sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/' sh 'docker cp -L "$(docker-compose ps -q cypress):/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-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz' sh 'docker-compose logs fullstack | gzip > debug/docker_fullstack.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') {
@ -144,6 +121,7 @@ pipeline {
} }
} }
steps { steps {
ansiColor('xterm') {
dir(path: 'docs') { dir(path: 'docs') {
sh 'yarn install' sh 'yarn install'
sh 'yarn build' sh 'yarn build'
@ -156,6 +134,7 @@ pipeline {
archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false)
} }
} }
}
stage('MultiArch Build') { stage('MultiArch Build') {
when { when {
not { not {
@ -163,14 +142,15 @@ pipeline {
} }
} }
steps { steps {
ansiColor('xterm') {
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 from cache // Buildx with push
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}"
} }
} }
} }
}
stage('Docs Deploy') { stage('Docs Deploy') {
when { when {
allOf { allOf {
@ -213,17 +193,19 @@ pipeline {
} }
} }
steps { steps {
ansiColor('xterm') {
script { script {
def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") def comment = pullRequest.comment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`")
}
} }
} }
} }
} }
post { post {
always { always {
sh 'docker-compose down --remove-orphans --volumes -t 30' sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30'
sh 'echo Reverting ownership' sh 'echo Reverting ownership'
sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} chown -R $(id -u):$(id -g) /data'
} }
success { success {
juxtapose event: 'success' juxtapose event: 'success'

View File

@ -1,19 +1,22 @@
<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.9.19-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.2.1-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>
<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/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge"> <img src="https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>
<a href="https://ci.nginxproxymanager.com/blue/organizations/jenkins/nginx-proxy-manager/branches/">
<img src="https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.nginxproxymanager.com%2Fjob%2Fnginx-proxy-manager%2Fjob%2Fmaster&style=for-the-badge">
</a>
</p> </p>
This project comes as a pre-built docker image that enables you to easily forward to your websites This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup) - [Quick Setup](https://nginxproxymanager.com#quick-setup)
- [Full Setup](https://nginxproxymanager.com/setup/) - [Full Setup](https://nginxproxymanager.com/setup/)
- [Screenshots](https://nginxproxymanager.com/screenshots/) - [Screenshots](https://nginxproxymanager.com/screenshots/)
@ -45,65 +48,3 @@ I won't go in to too much detail here but here are the basics for someone new to
2. Add port forwarding for port 80 and 443 to the server hosting this project 2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) 3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services 4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this:
```yml
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
3. Bring up your stack by running
```bash
docker-compose up -d
# If using docker-compose-plugin
docker compose up -d
```
4. Log in to the Admin UI
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.
[http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributors
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).
## Getting Support
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)
3. [Development Gitter](https://gitter.im/nginx-proxy-manager/community)
4. [Reddit](https://reddit.com/r/nginxproxymanager)

2
backend/.gitignore vendored
View File

@ -4,5 +4,3 @@ yarn-error.log
tmp tmp
certbot.log certbot.log
node_modules node_modules
core.*

View File

@ -40,6 +40,7 @@ app.use(function (req, res, next) {
} }
res.set({ res.set({
'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
'X-XSS-Protection': '1; mode=block', 'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff', 'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': x_frame_options, 'X-Frame-Options': x_frame_options,
@ -65,7 +66,7 @@ app.use(function (err, req, res, next) {
} }
}; };
if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) { if (process.env.NODE_ENV === 'development') {
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
@ -74,7 +75,7 @@ app.use(function (err, req, res, next) {
// Not every error is worth logging - but this is good for now until it gets annoying. // Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== 'undefined' && err.stack) { if (typeof err.stack !== 'undefined' && err.stack) {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { if (process.env.NODE_ENV === 'development') {
log.debug(err.stack); log.debug(err.stack);
} else if (typeof err.public == 'undefined' || !err.public) { } else if (typeof err.public == 'undefined' || !err.public) {
log.warn(err.message); log.warn(err.message);

View File

@ -1,26 +0,0 @@
{
"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,11 +4,7 @@ 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');
} }
function generateDbConfig() { let data = {
if (config.database.engine === 'knex-native') {
return config.database.knex;
} else
return {
client: config.database.engine, client: config.database.engine,
connection: { connection: {
host: config.database.host, host: config.database.host,
@ -21,10 +17,6 @@ function generateDbConfig() {
tableName: '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,10 +2,7 @@
const logger = require('./logger').global; const logger = require('./logger').global;
async function appStart () { 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');
@ -42,94 +39,9 @@ async 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;
let envSqliteFile = process.env.DB_SQLITE_FILE || null;
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) && !envSqliteFile){
envSqliteFile = '/data/database.sqlite';
logger.info(`No valid environment variables for database provided, using default SQLite file '${envSqliteFile}'`);
}
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 knex configuration from environment variables');
configData.database = newConfig;
} else {
const newConfig = {
fromEnv: true,
engine: 'knex-native',
knex: {
client: 'sqlite3',
connection: {
filename: envSqliteFile
},
useNullAsDefault: true
}
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating SQLite knex configuration');
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.debug('Wrote db configuration to config file: ' + filename);
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

@ -5,7 +5,6 @@ const logger = require('../logger').access;
const error = require('../lib/error'); const error = require('../lib/error');
const accessListModel = require('../models/access_list'); const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth'); const accessListAuthModel = require('../models/access_list_auth');
const accessListClientModel = require('../models/access_list_client');
const proxyHostModel = require('../models/proxy_host'); const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
@ -30,17 +29,14 @@ const internalAccessList = {
.omit(omissions()) .omit(omissions())
.insertAndFetch({ .insertAndFetch({
name: data.name, name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1) owner_user_id: access.token.getUserId(1)
}); });
}) })
.then((row) => { .then((row) => {
data.id = row.id; data.id = row.id;
let promises = [];
// Now add the items // Now add the items
let promises = [];
data.items.map((item) => { data.items.map((item) => {
promises.push(accessListAuthModel promises.push(accessListAuthModel
.query() .query()
@ -52,27 +48,13 @@ const internalAccessList = {
); );
}); });
// Now add the clients
if (typeof data.clients !== 'undefined' && data.clients) {
data.clients.map((client) => {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: row.id,
address: client.address,
directive: client.directive
})
);
});
}
return Promise.all(promises); return Promise.all(promises);
}) })
.then(() => { .then(() => {
// re-fetch with expansions // re-fetch with expansions
return internalAccessList.get(access, { return internalAccessList.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] expand: ['owner', 'items']
}, true /* <- skip masking */); }, true /* <- skip masking */);
}) })
.then((row) => { .then((row) => {
@ -82,7 +64,7 @@ const internalAccessList = {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); return internalNginx.reload();
} }
}) })
.then(() => { .then(() => {
@ -118,6 +100,7 @@ const internalAccessList = {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
} }
}) })
.then(() => { .then(() => {
// patch name if specified // patch name if specified
@ -126,9 +109,7 @@ const internalAccessList = {
.query() .query()
.where({id: data.id}) .where({id: data.id})
.patch({ .patch({
name: data.name, name: data.name
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
}); });
} }
}) })
@ -172,39 +153,6 @@ const internalAccessList = {
}); });
} }
}) })
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let query = accessListClientModel
.query()
.delete()
.where('access_list_id', data.id);
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(internalNginx.reload)
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
@ -218,14 +166,14 @@ const internalAccessList = {
// re-fetch with expansions // re-fetch with expansions
return internalAccessList.get(access, { return internalAccessList.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] expand: ['owner', 'items']
}, true /* <- skip masking */); }, true /* <- skip masking */);
}) })
.then((row) => { .then((row) => {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); return internalNginx.reload();
} }
}) })
.then(() => { .then(() => {
@ -256,7 +204,7 @@ const internalAccessList = {
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0) .where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id) .andWhere('access_list.id', data.id)
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]') .allowEager('[owner,items,proxy_hosts]')
.omit(['access_list.is_deleted']) .omit(['access_list.is_deleted'])
.first(); .first();
@ -298,7 +246,7 @@ const internalAccessList = {
delete: (access, data) => { delete: (access, data) => {
return access.can('access_lists:delete', data.id) return access.can('access_lists:delete', data.id)
.then(() => { .then(() => {
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']}); return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items']});
}) })
.then((row) => { .then((row) => {
if (!row) { if (!row) {
@ -382,11 +330,11 @@ const internalAccessList = {
.where('access_list.is_deleted', 0) .where('access_list.is_deleted', 0)
.groupBy('access_list.id') .groupBy('access_list.id')
.omit(['access_list.is_deleted']) .omit(['access_list.is_deleted'])
.allowEager('[owner,items,clients]') .allowEager('[owner,items]')
.orderBy('access_list.name', 'ASC'); .orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching

View File

@ -1,22 +1,18 @@
const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const https = require('https'); const _ = require('lodash');
const tempWrite = require('temp-write');
const moment = require('moment');
const logger = require('../logger').ssl; const logger = require('../logger').ssl;
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const certificateModel = require('../models/certificate'); const certificateModel = require('../models/certificate');
const dnsPlugins = require('../global/certbot-dns-plugins');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const tempWrite = require('temp-write');
const utils = require('../lib/utils');
const moment = require('moment');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
const le_staging = process.env.NODE_ENV !== 'production';
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalHost = require('./host'); const internalHost = require('./host');
const letsencryptStaging = process.env.NODE_ENV !== 'production'; const certbot_command = '/usr/bin/certbot';
const letsencryptConfig = '/etc/letsencrypt.ini'; const le_config = '/etc/letsencrypt.ini';
const certbotCommand = 'certbot';
const archiver = require('archiver');
const path = require('path');
const { isArray } = require('lodash');
function omissions() { function omissions() {
return ['is_deleted']; return ['is_deleted'];
@ -24,14 +20,14 @@ function omissions() {
const internalCertificate = { const internalCertificate = {
allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'],
intervalTimeout: 1000 * 60 * 60, // 1 hour interval_timeout: 1000 * 60 * 60, // 1 hour
interval: null, interval: null,
intervalProcessing: false, interval_processing: false,
initTimer: () => { initTimer: () => {
logger.info('Let\'s Encrypt Renewal Timer initialized'); logger.info('Let\'s Encrypt Renewal Timer initialized');
internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.intervalTimeout); internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.interval_timeout);
// And do this now as well // And do this now as well
internalCertificate.processExpiringHosts(); internalCertificate.processExpiringHosts();
}, },
@ -40,15 +36,15 @@ const internalCertificate = {
* Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required
*/ */
processExpiringHosts: () => { processExpiringHosts: () => {
if (!internalCertificate.intervalProcessing) { if (!internalCertificate.interval_processing) {
internalCertificate.intervalProcessing = true; internalCertificate.interval_processing = true;
logger.info('Renewing SSL certs close to expiry...'); logger.info('Renewing SSL certs close to expiry...');
const cmd = certbotCommand + ' renew --non-interactive --quiet ' + let cmd = certbot_command + ' renew --non-interactive --quiet ' +
'--config "' + letsencryptConfig + '" ' + '--config "' + le_config + '" ' +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--disable-hook-validation ' + '--disable-hook-validation ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
return utils.exec(cmd) return utils.exec(cmd)
.then((result) => { .then((result) => {
@ -81,7 +77,7 @@ const internalCertificate = {
.where('id', certificate.id) .where('id', certificate.id)
.andWhere('provider', 'letsencrypt') .andWhere('provider', 'letsencrypt')
.patch({ .patch({
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')')
}); });
}) })
.catch((err) => { .catch((err) => {
@ -96,11 +92,11 @@ const internalCertificate = {
}); });
}) })
.then(() => { .then(() => {
internalCertificate.intervalProcessing = false; internalCertificate.interval_processing = false;
}) })
.catch((err) => { .catch((err) => {
logger.error(err); logger.error(err);
internalCertificate.intervalProcessing = false; internalCertificate.interval_processing = false;
}); });
} }
}, },
@ -116,7 +112,7 @@ const internalCertificate = {
data.owner_user_id = access.token.getUserId(1); data.owner_user_id = access.token.getUserId(1);
if (data.provider === 'letsencrypt') { if (data.provider === 'letsencrypt') {
data.nice_name = data.domain_names.join(', '); data.nice_name = data.domain_names.sort().join(', ');
} }
return certificateModel return certificateModel
@ -145,33 +141,9 @@ const internalCertificate = {
}); });
}) })
.then((in_use_result) => { .then((in_use_result) => {
// With DNS challenge no config is needed, so skip 3 and 5.
if (certificate.meta.dns_challenge) {
return internalNginx.reload().then(() => {
// 4. Request cert
return internalCertificate.requestLetsEncryptSslWithDnsChallenge(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 internalCertificate.enableInUseHosts(in_use_result)
.then(internalNginx.reload)
.then(() => {
throw err;
});
});
} else {
// 3. Generate the LE config // 3. Generate the LE config
return internalNginx.generateLetsEncryptRequestConfig(certificate) return internalNginx.generateLetsEncryptRequestConfig(certificate)
.then(internalNginx.reload) .then(internalNginx.reload)
.then(async() => await new Promise((r) => setTimeout(r, 5000)))
.then(() => { .then(() => {
// 4. Request cert // 4. Request cert
return internalCertificate.requestLetsEncryptSsl(certificate); return internalCertificate.requestLetsEncryptSsl(certificate);
@ -199,7 +171,6 @@ const internalCertificate = {
throw err; throw err;
}); });
}); });
}
}) })
.then(() => { .then(() => {
// At this point, the letsencrypt cert should exist on disk. // At this point, the letsencrypt cert should exist on disk.
@ -209,7 +180,7 @@ const internalCertificate = {
return certificateModel return certificateModel
.query() .query()
.patchAndFetchById(certificate.id, { .patchAndFetchById(certificate.id, {
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')')
}) })
.then((saved_row) => { .then((saved_row) => {
// Add cert data for audit log // Add cert data for audit log
@ -220,13 +191,6 @@ 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;
@ -340,71 +304,6 @@ const internalCertificate = {
}); });
}, },
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
download: (access, data) => {
return new Promise((resolve, reject) => {
access.can('certificates:get', data)
.then(() => {
return internalCertificate.get(access, data);
})
.then((certificate) => {
if (certificate.provider === 'letsencrypt') {
const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id;
if (!fs.existsSync(zipDirectory)) {
throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists');
}
let certFiles = fs.readdirSync(zipDirectory)
.filter((fn) => fn.endsWith('.pem'))
.map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`;
const opName = '/tmp/' + downloadName;
internalCertificate.zipFiles(certFiles, opName)
.then(() => {
logger.debug('zip completed : ', opName);
const resp = {
fileName: opName
};
resolve(resp);
}).catch((err) => reject(err));
} else {
throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded');
}
}).catch((err) => reject(err));
});
},
/**
* @param {String} source
* @param {String} out
* @returns {Promise}
*/
zipFiles(source, out) {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(out);
return new Promise((resolve, reject) => {
source
.map((fl) => {
let fileName = path.basename(fl);
logger.debug(fl, 'added to certificate zip');
archive.file(fl, { name: fileName });
});
archive
.on('error', (err) => reject(err))
.pipe(stream);
stream.on('close', () => resolve());
archive.finalize();
});
},
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
@ -477,7 +376,7 @@ const internalCertificate = {
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string') {
query.where(function () { query.where(function () {
this.where('nice_name', 'like', '%' + search_query + '%'); this.where('name', 'like', '%' + search_query + '%');
}); });
} }
@ -517,9 +416,11 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
writeCustomCert: (certificate) => { writeCustomCert: (certificate) => {
if (debug_mode) {
logger.info('Writing Custom Certificate:', certificate); logger.info('Writing Custom Certificate:', certificate);
}
const dir = '/data/custom_ssl/npm-' + certificate.id; let dir = '/data/custom_ssl/npm-' + certificate.id;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
@ -527,9 +428,9 @@ const internalCertificate = {
return; return;
} }
let certData = certificate.meta.certificate; let cert_data = certificate.meta.certificate;
if (typeof certificate.meta.intermediate_certificate !== 'undefined') { if (typeof certificate.meta.intermediate_certificate !== 'undefined') {
certData = certData + '\n' + certificate.meta.intermediate_certificate; cert_data = cert_data + '\n' + certificate.meta.intermediate_certificate;
} }
try { try {
@ -541,7 +442,7 @@ const internalCertificate = {
return; return;
} }
fs.writeFile(dir + '/fullchain.pem', certData, function (err) { fs.writeFile(dir + '/fullchain.pem', cert_data, function (err) {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@ -591,7 +492,7 @@ const internalCertificate = {
// Put file contents into an object // Put file contents into an object
let files = {}; let files = {};
_.map(data.files, (file, name) => { _.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { if (internalCertificate.allowed_ssl_files.indexOf(name) !== -1) {
files[name] = file.data.toString(); files[name] = file.data.toString();
} }
}); });
@ -649,7 +550,7 @@ const internalCertificate = {
} }
_.map(data.files, (file, name) => { _.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { if (internalCertificate.allowed_ssl_files.indexOf(name) !== -1) {
row.meta[name] = file.data.toString(); row.meta[name] = file.data.toString();
} }
}); });
@ -657,7 +558,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: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'), expires_on: certificateModel.raw('FROM_UNIXTIME(' + validations.certificate.dates.to + ')'),
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
}) })
@ -668,7 +569,7 @@ const internalCertificate = {
}); });
}) })
.then(() => { .then(() => {
return _.pick(row.meta, internalCertificate.allowedSslFiles); return _.pick(row.meta, internalCertificate.allowed_ssl_files);
}); });
}); });
}, },
@ -682,25 +583,17 @@ const internalCertificate = {
checkPrivateKey: (private_key) => { checkPrivateKey: (private_key) => {
return tempWrite(private_key, '/tmp') return tempWrite(private_key, '/tmp')
.then((filepath) => { .then((filepath) => {
return new Promise((resolve, reject) => { return utils.exec('openssl rsa -in ' + filepath + ' -check -noout')
const failTimeout = setTimeout(() => {
reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.'));
}, 10000);
utils
.exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ')
.then((result) => { .then((result) => {
clearTimeout(failTimeout); if (!result.toLowerCase().includes('key ok')) {
if (!result.toLowerCase().includes('key is valid')) { throw new error.ValidationError(result);
reject(new error.ValidationError('Result Validation Error: ' + result));
} }
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
resolve(true); return true;
}) }).catch((err) => {
.catch((err) => {
clearTimeout(failTimeout);
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err)); throw new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err);
});
}); });
}); });
}, },
@ -716,9 +609,9 @@ const internalCertificate = {
return tempWrite(certificate, '/tmp') return tempWrite(certificate, '/tmp')
.then((filepath) => { .then((filepath) => {
return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired) return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired)
.then((certData) => { .then((cert_data) => {
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
return certData; return cert_data;
}).catch((err) => { }).catch((err) => {
fs.unlinkSync(filepath); fs.unlinkSync(filepath);
throw err; throw err;
@ -734,33 +627,33 @@ const internalCertificate = {
* @param {Boolean} [throw_expired] Throw when the certificate is out of date * @param {Boolean} [throw_expired] Throw when the certificate is out of date
*/ */
getCertificateInfoFromFile: (certificate_file, throw_expired) => { getCertificateInfoFromFile: (certificate_file, throw_expired) => {
let certData = {}; let cert_data = {};
return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
.then((result) => { .then((result) => {
// subject=CN = something.example.com // subject=CN = something.example.com
const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; let regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
const match = regex.exec(result); let match = regex.exec(result);
if (typeof match[1] === 'undefined') { if (typeof match[1] === 'undefined') {
throw new error.ValidationError('Could not determine subject from certificate: ' + result); throw new error.ValidationError('Could not determine subject from certificate: ' + result);
} }
certData['cn'] = match[1]; cert_data['cn'] = match[1];
}) })
.then(() => { .then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
}) })
.then((result) => { .then((result) => {
// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
const regex = /^(?:issuer=)?(.*)$/gim; let regex = /^(?:issuer=)?(.*)$/gim;
const match = regex.exec(result); let match = regex.exec(result);
if (typeof match[1] === 'undefined') { if (typeof match[1] === 'undefined') {
throw new error.ValidationError('Could not determine issuer from certificate: ' + result); throw new error.ValidationError('Could not determine issuer from certificate: ' + result);
} }
certData['issuer'] = match[1]; cert_data['issuer'] = match[1];
}) })
.then(() => { .then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
@ -768,39 +661,39 @@ const internalCertificate = {
.then((result) => { .then((result) => {
// notBefore=Jul 14 04:04:29 2018 GMT // notBefore=Jul 14 04:04:29 2018 GMT
// notAfter=Oct 12 04:04:29 2018 GMT // notAfter=Oct 12 04:04:29 2018 GMT
let validFrom = null; let valid_from = null;
let validTo = null; let valid_to = null;
const lines = result.split('\n'); let lines = result.split('\n');
lines.map(function (str) { lines.map(function (str) {
const regex = /^(\S+)=(.*)$/gim; let regex = /^(\S+)=(.*)$/gim;
const match = regex.exec(str.trim()); let match = regex.exec(str.trim());
if (match && typeof match[2] !== 'undefined') { if (match && typeof match[2] !== 'undefined') {
const date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10); let date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10);
if (match[1].toLowerCase() === 'notbefore') { if (match[1].toLowerCase() === 'notbefore') {
validFrom = date; valid_from = date;
} else if (match[1].toLowerCase() === 'notafter') { } else if (match[1].toLowerCase() === 'notafter') {
validTo = date; valid_to = date;
} }
} }
}); });
if (!validFrom || !validTo) { if (!valid_from || !valid_to) {
throw new error.ValidationError('Could not determine dates from certificate: ' + result); throw new error.ValidationError('Could not determine dates from certificate: ' + result);
} }
if (throw_expired && validTo < parseInt(moment().format('X'), 10)) { if (throw_expired && valid_to < parseInt(moment().format('X'), 10)) {
throw new error.ValidationError('Certificate has expired'); throw new error.ValidationError('Certificate has expired');
} }
certData['dates'] = { cert_data['dates'] = {
from: validFrom, from: valid_from,
to: validTo to: valid_to
}; };
return certData; return cert_data;
}).catch((err) => { }).catch((err) => {
throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err); throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err);
}); });
@ -814,7 +707,7 @@ const internalCertificate = {
* @returns {Object} * @returns {Object}
*/ */
cleanMeta: function (meta, remove) { cleanMeta: function (meta, remove) {
internalCertificate.allowedSslFiles.map((key) => { internalCertificate.allowed_ssl_files.map((key) => {
if (typeof meta[key] !== 'undefined' && meta[key]) { if (typeof meta[key] !== 'undefined' && meta[key]) {
if (remove) { if (remove) {
delete meta[key]; delete meta[key];
@ -828,24 +721,25 @@ const internalCertificate = {
}, },
/** /**
* Request a certificate using the http challenge
* @param {Object} certificate the certificate row * @param {Object} certificate the certificate row
* @returns {Promise} * @returns {Promise}
*/ */
requestLetsEncryptSsl: (certificate) => { requestLetsEncryptSsl: (certificate) => {
logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const cmd = certbotCommand + ' certonly ' + let cmd = certbot_command + ' certonly --non-interactive ' +
'--config "' + letsencryptConfig + '" ' + '--config "' + le_config + '" ' +
'--cert-name "npm-' + certificate.id + '" ' + '--cert-name "npm-' + certificate.id + '" ' +
'--agree-tos ' + '--agree-tos ' +
'--authenticator webroot ' +
'--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(',') + '" ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
if (debug_mode) {
logger.info('Command:', cmd); logger.info('Command:', cmd);
}
return utils.exec(cmd) return utils.exec(cmd)
.then((result) => { .then((result) => {
@ -854,81 +748,6 @@ 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 = dnsPlugins[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 credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
// Escape single quotes and backslashes
const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\');
const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\'';
let prepareCmd = 'pip install ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies;
// Special case for cloudflare
if (dns_plugin.package_name === 'certbot-dns-cloudflare') {
prepareCmd = 'pip install certbot-dns-cloudflare --index-url https://www.piwheels.org/simple --prefer-binary';
}
// Whether the plugin has a --<name>-credentials argument
const hasConfigArg = certificate.meta.dns_provider !== 'route53';
let mainCmd = certbotCommand + ' certonly ' +
'--config "' + letsencryptConfig + '" ' +
'--cert-name "npm-' + certificate.id + '" ' +
'--agree-tos ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' +
'--domains "' + certificate.domain_names.join(',') + '" ' +
'--authenticator ' + dns_plugin.full_plugin_name + ' ' +
(
hasConfigArg
? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
: ''
) +
(
certificate.meta.propagation_seconds !== undefined
? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
: ''
) +
(letsencryptStaging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
}
logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`);
return utils.exec(credentialsCmd)
.then(() => {
return utils.exec(prepareCmd)
.then(() => {
return utils.exec(mainCmd)
.then(async (result) => {
logger.info(result);
return result;
});
});
}).catch(async (err) => {
// Don't fail if file does not exist
const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`;
await utils.exec(delete_credentialsCmd);
throw err;
});
},
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
@ -942,9 +761,7 @@ const internalCertificate = {
}) })
.then((certificate) => { .then((certificate) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
const renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl; return internalCertificate.renewLetsEncryptSsl(certificate)
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');
}) })
@ -952,7 +769,7 @@ const internalCertificate = {
return certificateModel return certificateModel
.query() .query()
.patchAndFetchById(certificate.id, { .patchAndFetchById(certificate.id, {
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')')
}); });
}) })
.then((updated_certificate) => { .then((updated_certificate) => {
@ -980,15 +797,16 @@ const internalCertificate = {
renewLetsEncryptSsl: (certificate) => { renewLetsEncryptSsl: (certificate) => {
logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const cmd = certbotCommand + ' renew --force-renewal ' + let cmd = certbot_command + ' renew --non-interactive ' +
'--config "' + letsencryptConfig + '" ' + '--config "' + le_config + '" ' +
'--cert-name "npm-' + certificate.id + '" ' + '--cert-name "npm-' + certificate.id + '" ' +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--no-random-sleep-on-renew ' +
'--disable-hook-validation ' + '--disable-hook-validation ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
if (debug_mode) {
logger.info('Command:', cmd); logger.info('Command:', cmd);
}
return utils.exec(cmd) return utils.exec(cmd)
.then((result) => { .then((result) => {
@ -997,41 +815,6 @@ const internalCertificate = {
}); });
}, },
/**
* @param {Object} certificate the certificate row
* @returns {Promise}
*/
renewLetsEncryptSslWithDnsChallenge: (certificate) => {
const dns_plugin = dnsPlugins[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 mainCmd = certbotCommand + ' renew ' +
'--config "' + letsencryptConfig + '" ' +
'--cert-name "npm-' + certificate.id + '" ' +
'--disable-hook-validation ' +
'--no-random-sleep-on-renew ' +
(letsencryptStaging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
}
logger.info('Command:', mainCmd);
return utils.exec(mainCmd)
.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]
@ -1040,25 +823,28 @@ 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(', '));
const mainCmd = certbotCommand + ' revoke ' + let cmd = certbot_command + ' revoke --non-interactive ' +
'--config "' + letsencryptConfig + '" ' + '--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 ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
// Don't fail command if file does not exist if (debug_mode) {
const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; logger.info('Command:', cmd);
}
logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); return utils.exec(cmd)
.then((result) => {
return utils.exec(mainCmd) if (debug_mode) {
.then(async (result) => { logger.info('Command:', cmd);
await utils.exec(delete_credentialsCmd); }
logger.info(result); logger.info(result);
return result; return result;
}) })
.catch((err) => { .catch((err) => {
if (debug_mode) {
logger.error(err.message); logger.error(err.message);
}
if (throw_errors) { if (throw_errors) {
throw err; throw err;
@ -1071,9 +857,9 @@ const internalCertificate = {
* @returns {Boolean} * @returns {Boolean}
*/ */
hasLetsEncryptSslCerts: (certificate) => { hasLetsEncryptSslCerts: (certificate) => {
const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id; let le_path = '/etc/letsencrypt/live/npm-' + certificate.id;
return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem'); return fs.existsSync(le_path + '/fullchain.pem') && fs.existsSync(le_path + '/privkey.pem');
}, },
/** /**
@ -1134,94 +920,6 @@ const internalCertificate = {
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
},
testHttpsChallenge: async (access, domains) => {
await access.can('certificates:list');
if (!isArray(domains)) {
throw new error.InternalValidationError('Domains must be an array of strings');
}
if (domains.length === 0) {
throw new error.InternalValidationError('No domains provided');
}
// Create a test challenge file
const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge';
const testChallengeFile = testChallengeDir + '/test-challenge';
fs.mkdirSync(testChallengeDir, {recursive: true});
fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'});
async function performTestForDomain (domain) {
logger.info('Testing http challenge for ' + domain);
const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(formBody)
}
};
const result = await new Promise((resolve) => {
const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) {
let responseBody = '';
res.on('data', (chunk) => responseBody = responseBody + chunk);
res.on('end', function () {
const parsedBody = JSON.parse(responseBody + '');
if (res.statusCode !== 200) {
logger.warn(`Failed to test HTTP challenge for domain ${domain}`, res);
resolve(undefined);
}
resolve(parsedBody);
});
});
// Make sure to write the request body.
req.write(formBody);
req.end();
req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
resolve(undefined); });
});
if (!result) {
// Some error occurred while trying to get the data
return 'failed';
} else if (`${result.responsecode}` === '200' && result.htmlresponse === 'Success') {
// Server exists and has responded with the correct data
return 'ok';
} else if (`${result.responsecode}` === '200') {
// Server exists but has responded with wrong data
logger.info(`HTTP challenge test failed for domain ${domain} because of invalid returned data:`, result.htmlresponse);
return 'wrong-data';
} else if (`${result.responsecode}` === '404') {
// Server exists but responded with a 404
logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`);
return '404';
} else if (`${result.responsecode}` === '0' || (typeof result.reason === 'string' && result.reason.toLowerCase() === 'host unavailable')) {
// Server does not exist at domain
logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`);
return 'no-host';
} else {
// Other errors
logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`);
return `other:${result.responsecode}`;
}
}
const results = {};
for (const domain of domains){
results[domain] = await performTestForDomain(domain);
}
// Remove the test challenge file
fs.unlinkSync(testChallengeFile);
return results;
} }
}; };

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[2]) { if (promises_results[1]) {
// 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[2]) { if (promises_results[1]) {
// 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

@ -9,9 +9,6 @@ const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json';
const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4'; const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4';
const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6'; const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6';
const regIpV4 = /^(\d+\.?){4}\/\d+/;
const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/;
const internalIpRanges = { const internalIpRanges = {
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
@ -77,14 +74,14 @@ const internalIpRanges = {
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL); return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
}) })
.then((cloudfare_data) => { .then((cloudfare_data) => {
let items = cloudfare_data.split('\n').filter((line) => regIpV4.test(line)); let items = cloudfare_data.split('\n');
ip_ranges = [... ip_ranges, ... items]; ip_ranges = [... ip_ranges, ... items];
}) })
.then(() => { .then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
}) })
.then((cloudfare_data) => { .then((cloudfare_data) => {
let items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line)); let items = cloudfare_data.split('\n');
ip_ranges = [... ip_ranges, ... items]; ip_ranges = [... ip_ranges, ... items];
}) })
.then(() => { .then(() => {

View File

@ -136,8 +136,6 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
renderLocations: (host) => { renderLocations: (host) => {
//logger.info('host = ' + JSON.stringify(host, null, 2));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template; let template;
@ -148,18 +146,12 @@ const internalNginx = {
return; return;
} }
let renderer = new Liquid({ let renderer = new Liquid();
root: __dirname + '/../templates/'
});
let renderedLocations = ''; let renderedLocations = '';
const locationRendering = async () => { const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) { for (let i = 0; i < host.locations.length; i++) {
let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, let locationCopy = Object.assign({}, host.locations[i]);
{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},
{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
{certificate: host.certificate}, host.locations[i]);
if (locationCopy.forward_host.indexOf('/') > -1) { if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split('/'); const splitted = locationCopy.forward_host.split('/');
@ -168,16 +160,12 @@ const internalNginx = {
locationCopy.forward_path = `/${splitted.join('/')}`; locationCopy.forward_path = `/${splitted.join('/')}`;
} }
//logger.info('locationCopy = ' + JSON.stringify(locationCopy, null, 2));
// eslint-disable-next-line // eslint-disable-next-line
renderedLocations += await renderer.parseAndRender(template, locationCopy); renderedLocations += await renderer.parseAndRender(template, locationCopy);
} }
}; };
locationRendering().then(() => resolve(renderedLocations)); locationRendering().then(() => resolve(renderedLocations));
}); });
}, },
@ -193,8 +181,6 @@ const internalNginx = {
logger.info('Generating ' + host_type + ' Config:', host); logger.info('Generating ' + host_type + ' Config:', host);
} }
// logger.info('host = ' + JSON.stringify(host, null, 2));
let renderEngine = new Liquid({ let renderEngine = new Liquid({
root: __dirname + '/../templates/' root: __dirname + '/../templates/'
}); });
@ -222,7 +208,6 @@ const internalNginx = {
} }
if (host.locations) { if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations); origLocations = [].concat(host.locations);
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
host.locations = renderedLocations; host.locations = renderedLocations;
@ -239,9 +224,6 @@ const internalNginx = {
locationsPromise = Promise.resolve(); locationsPromise = Promise.resolve();
} }
// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();
locationsPromise.then(() => { locationsPromise.then(() => {
renderEngine renderEngine
.parseAndRender(template, host) .parseAndRender(template, host)
@ -288,7 +270,6 @@ const internalNginx = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
try { try {
template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
@ -296,8 +277,6 @@ const internalNginx = {
return; return;
} }
certificate.ipv6 = internalNginx.ipv6Enabled();
renderEngine renderEngine
.parseAndRender(template, certificate) .parseAndRender(template, certificate)
.then((config_text) => { .then((config_text) => {
@ -417,18 +396,6 @@ const internalNginx = {
*/ */
advancedConfigHasDefaultLocation: function (config) { advancedConfigHasDefaultLocation: function (config) {
return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
},
/**
* @returns {boolean}
*/
ipv6Enabled: function () {
if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
}
return true;
} }
}; };

View File

@ -73,7 +73,7 @@ const internalProxyHost = {
// re-fetch with cert // re-fetch with cert
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: row.id, id: row.id,
expand: ['certificate', 'owner', 'access_list.[clients,items]'] expand: ['certificate', 'owner', 'access_list']
}); });
}) })
.then((row) => { .then((row) => {
@ -186,13 +186,9 @@ const internalProxyHost = {
.then(() => { .then(() => {
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'certificate', 'access_list.[clients,items]'] expand: ['owner', 'certificate', 'access_list']
}) })
.then((row) => { .then((row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
}
// Configure nginx // Configure nginx
return internalNginx.configure(proxyHostModel, 'proxy_host', row) return internalNginx.configure(proxyHostModel, 'proxy_host', row)
.then((new_meta) => { .then((new_meta) => {
@ -223,7 +219,7 @@ const internalProxyHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowEager('[owner,access_list,access_list.[clients,items],certificate]') .allowEager('[owner,access_list,certificate]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {

View File

@ -4,21 +4,11 @@ module.exports = function (req, res, next) {
if (req.headers.origin) { if (req.headers.origin) {
const originSchema = { // very relaxed validation....
oneOf: [ validator({
{
type: 'string', type: 'string',
pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$' pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
}, }, req.headers.origin)
{
type: 'string',
pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$'
}
]
};
// very relaxed validation....
validator(originSchema, req.headers.origin)
.then(function () { .then(function () {
res.set({ res.set({
'Access-Control-Allow-Origin': req.headers.origin, 'Access-Control-Allow-Origin': req.headers.origin,

View File

@ -22,6 +22,22 @@ 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

@ -1,53 +0,0 @@
const migrate_name = 'access_list_client';
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.createTable('access_list_client', (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('address').notNull();
table.string('directive').notNull();
table.json('meta').notNull();
})
.then(function () {
logger.info('[' + migrate_name + '] access_list_client Table created');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('satify_any').notNull().defaultTo(0);
});
})
.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.dropTable('access_list_client')
.then(() => {
logger.info('[' + migrate_name + '] access_list_client Table dropped');
});
};

View File

@ -1,34 +0,0 @@
const migrate_name = 'access_list_client_fix';
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.renameColumn('satify_any', 'satisfy_any');
})
.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.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};

View File

@ -1,41 +0,0 @@
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

@ -1,41 +0,0 @@
const migrate_name = 'redirection_scheme';
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('redirection_host', (table) => {
table.string('forward_scheme').notNull().defaultTo('$scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host 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('redirection_host', (table) => {
table.dropColumn('forward_scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};

View File

@ -1,41 +0,0 @@
const migrate_name = 'redirection_status_code';
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('redirection_host', (table) => {
table.integer('forward_http_code').notNull().unsigned().defaultTo(302);
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host 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('redirection_host', (table) => {
table.dropColumn('forward_http_code');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};

View File

@ -1,40 +0,0 @@
const migrate_name = 'stream_domain';
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('stream', (table) => {
table.renameColumn('forward_ip', 'forwarding_host');
})
.then(function () {
logger.info('[' + migrate_name + '] stream 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('stream', (table) => {
table.renameColumn('forwarding_host', 'forward_ip');
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
});
};

View File

@ -1,50 +0,0 @@
const migrate_name = 'stream_domain';
const logger = require('../logger').migrate;
const internalNginx = require('../internal/nginx');
async function regenerateDefaultHost(knex) {
const row = await knex('setting').select('*').where('id', 'default-site').first();
if (!row) {
return Promise.resolve();
}
return internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.generateConfig('default', row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
});
}
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex) {
logger.info('[' + migrate_name + '] Migrating Up...');
return regenerateDefaultHost(knex);
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info('[' + migrate_name + '] Migrating Down...');
return regenerateDefaultHost(knex);
};

View File

@ -5,15 +5,13 @@ const db = require('../db');
const Model = require('objection').Model; 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 now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AccessList extends Model { class AccessList extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -22,7 +20,7 @@ class AccessList extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {
@ -64,17 +62,6 @@ class AccessList extends Model {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
} }
}, },
clients: {
relation: Model.HasManyRelation,
modelClass: AccessListClient,
join: {
from: 'access_list.id',
to: 'access_list_client.access_list_id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
}
},
proxy_hosts: { proxy_hosts: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: ProxyHost, modelClass: ProxyHost,
@ -89,14 +76,6 @@ class AccessList extends Model {
} }
}; };
} }
get satisfy() {
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,14 +3,13 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -19,7 +18,7 @@ class AccessListAuth extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {

View File

@ -1,59 +0,0 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db);
class AccessListClient extends Model {
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate () {
this.modified_on = now();
}
static get name () {
return 'AccessListClient';
}
static get tableName () {
return 'access_list_client';
}
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings () {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: require('./access_list'),
join: {
from: 'access_list_client.access_list_id',
to: 'access_list.id'
},
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']);
}
}
};
}
get rule() {
return `${this.directive} ${this.address}`;
}
}
module.exports = AccessListClient;

View File

@ -4,14 +4,13 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -20,7 +19,7 @@ class AuditLog extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {

View File

@ -5,7 +5,6 @@ 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);
@ -25,8 +24,8 @@ function encryptPassword () {
class Auth extends Model { class Auth extends Model {
$beforeInsert (queryContext) { $beforeInsert (queryContext) {
this.created_on = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -37,7 +36,7 @@ class Auth extends Model {
} }
$beforeUpdate (queryContext) { $beforeUpdate (queryContext) {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
return encryptPassword.apply(this, queryContext); return encryptPassword.apply(this, queryContext);
} }

View File

@ -4,18 +4,17 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for expires_on // Default for expires_on
if (typeof this.expires_on === 'undefined') { if (typeof this.expires_on === 'undefined') {
this.expires_on = now(); this.expires_on = Model.raw('NOW()');
} }
// Default for domain_names // Default for domain_names
@ -32,7 +31,7 @@ class Certificate extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View File

@ -5,14 +5,13 @@ 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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -28,7 +27,7 @@ class DeadHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View File

@ -1,13 +0,0 @@
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,14 +6,13 @@ 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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -29,7 +28,7 @@ class ProxyHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View File

@ -5,14 +5,13 @@ 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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === 'undefined') { if (typeof this.domain_names === 'undefined') {
@ -28,7 +27,7 @@ class RedirectionHost extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== 'undefined') { if (typeof this.domain_names !== 'undefined') {

View File

@ -4,14 +4,13 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -20,7 +19,7 @@ class Stream extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {

View File

@ -4,23 +4,15 @@
*/ */
const _ = require('lodash'); const _ = require('lodash');
const config = require('config');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const crypto = require('crypto'); const crypto = require('crypto');
const error = require('../lib/error'); const error = require('../lib/error');
const ALGO = 'RS256'; const ALGO = 'RS256';
let public_key = null;
let private_key = null;
function checkJWTKeyPair() {
if (!public_key || !private_key) {
let config = require('config');
public_key = config.get('jwt.pub');
private_key = config.get('jwt.key');
}
}
module.exports = function () { module.exports = function () {
const public_key = config.get('jwt.pub');
const private_key = config.get('jwt.key');
let token_data = {}; let token_data = {};
@ -40,8 +32,6 @@ module.exports = function () {
.toString('base64') .toString('base64')
.substr(-8); .substr(-8);
checkJWTKeyPair();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jwt.sign(payload, private_key, options, (err, token) => { jwt.sign(payload, private_key, options, (err, token) => {
if (err) { if (err) {
@ -63,7 +53,6 @@ module.exports = function () {
*/ */
load: function (token) { load: function (token) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
checkJWTKeyPair();
try { try {
if (!token || token === null || token === 'null') { if (!token || token === null || token === 'null') {
reject(new error.AuthError('Empty token')); reject(new error.AuthError('Empty token'));

View File

@ -4,14 +4,13 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
// Default for roles // Default for roles
if (typeof this.roles === 'undefined') { if (typeof this.roles === 'undefined') {
@ -20,7 +19,7 @@ class User extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {

View File

@ -3,18 +3,17 @@
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 = now(); this.created_on = Model.raw('NOW()');
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = now(); this.modified_on = Model.raw('NOW()');
} }
static get name () { static get name () {

View File

@ -4,30 +4,32 @@
"description": "A beautiful interface for creating Nginx endpoints", "description": "A beautiful interface for creating Nginx endpoints",
"main": "js/index.js", "main": "js/index.js",
"dependencies": { "dependencies": {
"ajv": "^6.12.0", "ajv": "^6.11.0",
"archiver": "^5.3.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^3.0.8",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.3.1", "config": "^3.2.5",
"diskdb": "^0.1.17",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.9", "express-fileupload": "^1.1.6",
"gravatar": "^1.8.0", "gravatar": "^1.8.0",
"json-schema-ref-parser": "^8.0.0", "html-entities": "^1.2.1",
"json-schema-ref-parser": "^7.1.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^0.20.13", "knex": "^0.20.10",
"liquidjs": "^9.11.10", "liquidjs": "^9.7.1",
"lodash": "^4.17.21", "lodash": "^4.17.15",
"moment": "^2.29.4", "moment": "^2.24.0",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.7",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"objection": "^2.2.16", "objection": "^2.1.3",
"path": "^0.12.7", "path": "^0.12.7",
"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"
}, },
"signale": { "signale": {
"displayDate": true, "displayDate": true,
@ -38,6 +40,6 @@
"devDependencies": { "devDependencies": {
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-align-assignments": "^1.1.2", "eslint-plugin-align-assignments": "^1.1.2",
"prettier": "^2.0.4" "prettier": "^1.19.1"
} }
} }

View File

@ -58,7 +58,6 @@ 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) => {
@ -68,32 +67,6 @@ router
.catch(next); .catch(next);
}); });
/**
* Test HTTP challenge for domains
*
* /api/nginx/certificates/test-http
*/
router
.route('/test-http')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.get((req, res, next) => {
internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/** /**
* Specific certificate * Specific certificate
* *
@ -224,7 +197,6 @@ 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)
}) })
@ -235,34 +207,6 @@ router
.catch(next); .catch(next);
}); });
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route('/:certificate_id/download')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get((req, res, next) => {
internalCertificate.download(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.download(result.fileName);
})
.catch(next);
});
/** /**
* Validate Certs before saving * Validate Certs before saving
* *

View File

@ -153,7 +153,7 @@
"example": "john@example.com", "example": "john@example.com",
"format": "email", "format": "email",
"type": "string", "type": "string",
"minLength": 6, "minLength": 8,
"maxLength": 100 "maxLength": 100
}, },
"password": { "password": {
@ -179,19 +179,6 @@
"pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$" "pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$"
} }
}, },
"http_code": {
"description": "Redirect HTTP Status Code",
"example": 302,
"type": "integer",
"minimum": 300,
"maximum": 308
},
"scheme": {
"description": "RFC Protocol",
"example": "HTTPS or $scheme",
"type": "string",
"minLength": 4
},
"enabled": { "enabled": {
"description": "Is Enabled", "description": "Is Enabled",
"example": true, "example": true,

View File

@ -19,32 +19,6 @@
"type": "string", "type": "string",
"description": "Name of the Access List" "description": "Name of the Access List"
}, },
"directive": {
"type": "string",
"enum": ["allow", "deny"]
},
"address": {
"oneOf": [
{
"type": "string",
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
},
{
"type": "string",
"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
},
{
"type": "string",
"pattern": "^all$"
}
]
},
"satisfy_any": {
"type": "boolean"
},
"pass_auth": {
"type": "boolean"
},
"meta": { "meta": {
"type": "object" "type": "object"
} }
@ -97,20 +71,16 @@
"schema": { "schema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["name"], "required": [
"name"
],
"properties": { "properties": {
"name": { "name": {
"$ref": "#/definitions/name" "$ref": "#/definitions/name"
}, },
"satisfy_any": {
"$ref": "#/definitions/satisfy_any"
},
"pass_auth": {
"$ref": "#/definitions/pass_auth"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 0, "minItems": 1,
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -126,22 +96,6 @@
} }
} }
}, },
"clients": {
"type": "array",
"minItems": 0,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"address": {
"$ref": "#/definitions/address"
},
"directive": {
"$ref": "#/definitions/directive"
}
}
}
},
"meta": { "meta": {
"$ref": "#/definitions/meta" "$ref": "#/definitions/meta"
} }
@ -170,15 +124,9 @@
"name": { "name": {
"$ref": "#/definitions/name" "$ref": "#/definitions/name"
}, },
"satisfy_any": {
"$ref": "#/definitions/satisfy_any"
},
"pass_auth": {
"$ref": "#/definitions/pass_auth"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 0, "minItems": 1,
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -193,22 +141,6 @@
} }
} }
} }
},
"clients": {
"type": "array",
"minItems": 0,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"address": {
"$ref": "#/definitions/address"
},
"directive": {
"$ref": "#/definitions/directive"
}
}
}
} }
} }
}, },

View File

@ -41,24 +41,6 @@
}, },
"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
}
]
} }
} }
} }
@ -157,17 +139,6 @@
"targetSchema": { "targetSchema": {
"type": "boolean" "type": "boolean"
} }
},
{
"title": "Test HTTP Challenge",
"description": "Tests whether the HTTP challenge should work",
"href": "/nginx/certificates/{definitions.identity.example}/test-http",
"access": "private",
"method": "GET",
"rel": "info",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
}
} }
] ]
} }

View File

@ -25,7 +25,7 @@
"forward_host": { "forward_host": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"maxLength": 255 "maxLength": 50
}, },
"forward_port": { "forward_port": {
"type": "integer", "type": "integer",

View File

@ -18,12 +18,6 @@
"domain_names": { "domain_names": {
"$ref": "../definitions.json#/definitions/domain_names" "$ref": "../definitions.json#/definitions/domain_names"
}, },
"forward_http_code": {
"$ref": "../definitions.json#/definitions/http_code"
},
"forward_scheme": {
"$ref": "../definitions.json#/definitions/scheme"
},
"forward_domain_name": { "forward_domain_name": {
"$ref": "../definitions.json#/definitions/domain_name" "$ref": "../definitions.json#/definitions/domain_name"
}, },
@ -73,12 +67,6 @@
"domain_names": { "domain_names": {
"$ref": "#/definitions/domain_names" "$ref": "#/definitions/domain_names"
}, },
"forward_http_code": {
"$ref": "#/definitions/forward_http_code"
},
"forward_scheme": {
"$ref": "#/definitions/forward_scheme"
},
"forward_domain_name": { "forward_domain_name": {
"$ref": "#/definitions/forward_domain_name" "$ref": "#/definitions/forward_domain_name"
}, },
@ -146,20 +134,12 @@
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
"domain_names", "domain_names",
"forward_scheme",
"forward_http_code",
"forward_domain_name" "forward_domain_name"
], ],
"properties": { "properties": {
"domain_names": { "domain_names": {
"$ref": "#/definitions/domain_names" "$ref": "#/definitions/domain_names"
}, },
"forward_http_code": {
"$ref": "#/definitions/forward_http_code"
},
"forward_scheme": {
"$ref": "#/definitions/forward_scheme"
},
"forward_domain_name": { "forward_domain_name": {
"$ref": "#/definitions/forward_domain_name" "$ref": "#/definitions/forward_domain_name"
}, },
@ -215,12 +195,6 @@
"domain_names": { "domain_names": {
"$ref": "#/definitions/domain_names" "$ref": "#/definitions/domain_names"
}, },
"forward_http_code": {
"$ref": "#/definitions/forward_http_code"
},
"forward_scheme": {
"$ref": "#/definitions/forward_scheme"
},
"forward_domain_name": { "forward_domain_name": {
"$ref": "#/definitions/forward_domain_name" "$ref": "#/definitions/forward_domain_name"
}, },

View File

@ -20,21 +20,10 @@
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"forwarding_host": { "forward_ip": {
"anyOf": [
{
"$ref": "../definitions.json#/definitions/domain_name"
},
{
"type": "string", "type": "string",
"format": "ipv4" "format": "ipv4"
}, },
{
"type": "string",
"format": "ipv6"
}
]
},
"forwarding_port": { "forwarding_port": {
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
@ -66,8 +55,8 @@
"incoming_port": { "incoming_port": {
"$ref": "#/definitions/incoming_port" "$ref": "#/definitions/incoming_port"
}, },
"forwarding_host": { "forward_ip": {
"$ref": "#/definitions/forwarding_host" "$ref": "#/definitions/forward_ip"
}, },
"forwarding_port": { "forwarding_port": {
"$ref": "#/definitions/forwarding_port" "$ref": "#/definitions/forwarding_port"
@ -118,15 +107,15 @@
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
"incoming_port", "incoming_port",
"forwarding_host", "forward_ip",
"forwarding_port" "forwarding_port"
], ],
"properties": { "properties": {
"incoming_port": { "incoming_port": {
"$ref": "#/definitions/incoming_port" "$ref": "#/definitions/incoming_port"
}, },
"forwarding_host": { "forward_ip": {
"$ref": "#/definitions/forwarding_host" "$ref": "#/definitions/forward_ip"
}, },
"forwarding_port": { "forwarding_port": {
"$ref": "#/definitions/forwarding_port" "$ref": "#/definitions/forwarding_port"
@ -165,8 +154,8 @@
"incoming_port": { "incoming_port": {
"$ref": "#/definitions/incoming_port" "$ref": "#/definitions/incoming_port"
}, },
"forwarding_host": { "forward_ip": {
"$ref": "#/definitions/forwarding_host" "$ref": "#/definitions/forward_ip"
}, },
"forwarding_port": { "forwarding_port": {
"$ref": "#/definitions/forwarding_port" "$ref": "#/definitions/forwarding_port"

View File

@ -2,21 +2,12 @@ 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')) {
@ -41,7 +32,7 @@ const setupJwt = () => {
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
@ -51,10 +42,12 @@ const setupJwt = () => {
reject(err); reject(err);
} else { } else {
logger.info('Wrote JWT key pair to config file: ' + filename); logger.info('Wrote JWT key pair to config file: ' + filename);
delete require.cache[require.resolve('config')];
resolve(); logger.warn('Restarting interface to apply new configuration');
process.exit(0);
} }
}); });
} else { } else {
// JWT key pair exists // JWT key pair exists
if (debug_mode) { if (debug_mode) {
@ -63,20 +56,14 @@ const setupJwt = () => {
resolve(); resolve();
} }
}); })
}; .then(() => {
/**
* Creates a default admin users if one doesn't already exist in the database
*
* @returns {Promise}
*/
const setupDefaultUser = () => {
return userModel return userModel
.query() .query()
.select(userModel.raw('COUNT(`id`) as `count`')) .select(userModel.raw('COUNT(`id`) as `count`'))
.where('is_deleted', 0) .where('is_deleted', 0)
.first() .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
@ -88,7 +75,7 @@ const setupDefaultUser = () => {
name: 'Administrator', name: 'Administrator',
nickname: 'Admin', nickname: 'Admin',
avatar: '', avatar: '',
roles: ['admin'], roles: ['admin']
}; };
return userModel return userModel
@ -101,10 +88,12 @@ const setupDefaultUser = () => {
user_id: user.id, user_id: user.id,
type: 'password', type: 'password',
secret: 'changeme', secret: 'changeme',
meta: {}, meta: {}
}) })
.then(() => { .then(() => {
return userPermissionModel.query().insert({ return userPermissionModel
.query()
.insert({
user_id: user.id, user_id: user.id,
visibility: 'all', visibility: 'all',
proxy_hosts: 'manage', proxy_hosts: 'manage',
@ -112,131 +101,15 @@ const setupDefaultUser = () => {
dead_hosts: 'manage', dead_hosts: 'manage',
streams: 'manage', streams: 'manage',
access_lists: 'manage', access_lists: 'manage',
certificates: 'manage', certificates: 'manage'
}); });
}); });
}) })
.then(() => { .then(() => {
logger.info('Initial admin setup completed'); logger.info('Initial 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 = [];
let install_cloudflare_plugin = false;
certificates.map(function (certificate) {
if (certificate.meta && certificate.meta.dns_challenge === true) {
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
if (dns_plugin.package_name === 'certbot-dns-cloudflare') {
install_cloudflare_plugin = true;
} else {
const packages_to_install = `${dns_plugin.package_name}${dns_plugin.version_requirement || ''} ${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;
// Escape single quotes and backslashes
const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\');
const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }';
promises.push(utils.exec(credentials_cmd));
}
});
if (plugins.length) {
const install_cmd = 'pip install ' + plugins.join(' ');
promises.push(utils.exec(install_cmd));
}
if (install_cloudflare_plugin) {
promises.push(utils.exec('pip install certbot-dns-cloudflare --index-url https://www.piwheels.org/simple --prefer-binary'));
}
if (promises.length) {
return Promise.all(promises)
.then(() => {
logger.info('Added Certbot plugins ' + plugins.join(', '));
});
}
}
});
};
/**
* Starts a timer to call run the logrotation binary every two days
* @returns {Promise}
*/
const setupLogrotation = () => {
const intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days
const runLogrotate = async () => {
try {
await utils.exec('logrotate /etc/logrotate.d/nginx-proxy-manager');
logger.info('Logrotate completed.');
} catch (e) { logger.warn(e); }
};
logger.info('Logrotate Timer initialized');
setInterval(runLogrotate, intervalTimeout);
// And do this now as well
return runLogrotate();
};
module.exports = function () {
return setupJwt()
.then(setupDefaultUser)
.then(setupDefaultSettings)
.then(setupCertbotPlugins)
.then(setupLogrotation);
};

View File

@ -1,8 +1,8 @@
{% if certificate and certificate_id > 0 -%} {% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %} {% if ssl_forced == 1 or ssl_forced == true %}
{% if hsts_enabled == 1 or hsts_enabled == true %} {% if hsts_enabled == 1 or hsts_enabled == true %}
# HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) # HSTS (ngx_http_headers_module is required) (31536000 seconds = 1 year)
add_header Strict-Transport-Security "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always; add_header Strict-Transport-Security "max-age=31536000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always;
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -1,15 +1,5 @@
listen 80; listen 80;
{% if ipv6 -%}
listen [::]:80;
{% else -%}
#listen [::]:80;
{% endif %}
{% if certificate -%} {% if certificate -%}
listen 443 ssl{% if http2_support %} http2{% endif %}; listen 443 ssl{% if http2_support %} http2{% endif %};
{% if ipv6 -%}
listen [::]:443 ssl{% if http2_support %} http2{% endif %};
{% else -%}
#listen [::]:443;
{% endif %}
{% endif %} {% endif %}
server_name {{ domain_names | join: " " }}; server_name {{ domain_names | join: " " }};

View File

@ -3,43 +3,7 @@
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 }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
{% if access_list_id > 0 %}
{% if access_list.items.length > 0 %}
# Authorization
auth_basic "Authorization required";
auth_basic_user_file /data/access/{{ access_list_id }};
{{ access_list.passauth }}
{% endif %}
# Access Rules
{% for client in access_list.clients %}
{{- client.rule -}};
{% endfor %}deny all;
# Access checks must...
{% if access_list.satisfy %}
{{ access_list.satisfy }};
{% endif %}
{% endif %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
{% endif %}
{{ advanced_config }} {{ advanced_config }}
} }

View File

@ -5,15 +5,14 @@ server {
{% include "_listen.conf" %} {% include "_listen.conf" %}
{% include "_certificates.conf" %} {% include "_certificates.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
access_log /data/logs/dead-host-{{ id }}_access.log standard; access_log /data/logs/dead_host-{{ id }}.log standard;
error_log /data/logs/dead-host-{{ id }}_error.log warn;
{{ advanced_config }} {{ advanced_config }}
{% if use_default_location %} {% if use_default_location %}
location / { location / {
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
return 404; return 404;
} }

View File

@ -6,18 +6,10 @@
{%- else %} {%- else %}
server { server {
listen 80 default; listen 80 default;
{% if ipv6 -%}
listen [::]:80 default;
{% else -%}
#listen [::]:80 default;
{% endif %}
server_name default-host.localhost; server_name default-host.localhost;
access_log /data/logs/default-host_access.log combined; access_log /data/logs/default_host.log combined;
error_log /data/logs/default-host_error.log warn;
{% include "_exploits.conf" %} {% include "_exploits.conf" %}
include conf.d/include/letsencrypt-acme-challenge.conf;
{%- if value == "404" %} {%- if value == "404" %}
location / { location / {
return 404; return 404;

View File

@ -2,14 +2,9 @@
server { server {
listen 80; listen 80;
{% if ipv6 -%}
listen [::]:80;
{% endif %}
server_name {{ domain_names | join: " " }}; server_name {{ domain_names | join: " " }};
access_log /data/logs/letsencrypt-requests_access.log standard; access_log /data/logs/letsencrypt-requests.log standard;
error_log /data/logs/letsencrypt-requests_error.log warn;
include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/letsencrypt-acme-challenge.conf;

View File

@ -11,16 +11,8 @@ server {
{% include "_assets.conf" %} {% include "_assets.conf" %}
{% include "_exploits.conf" %} {% include "_exploits.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} access_log /data/logs/proxy_host-{{ id }}.log proxy;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
{% endif %}
access_log /data/logs/proxy-host-{{ id }}_access.log proxy;
error_log /data/logs/proxy-host-{{ id }}_error.log warn;
{{ advanced_config }} {{ advanced_config }}
@ -29,28 +21,13 @@ proxy_http_version 1.1;
{% if use_default_location %} {% if use_default_location %}
location / { location / {
{%- if access_list_id > 0 -%}
{% if access_list_id > 0 %} # Access List
{% if access_list.items.length > 0 %}
# 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 }};
{%- endif %}
{{ access_list.passauth }} {% include "_forced_ssl.conf" %}
{% endif %}
# Access Rules
{% for client in access_list.clients %}
{{- client.rule -}};
{% endfor %}deny all;
# Access checks must...
{% if access_list.satisfy %}
{{ access_list.satisfy }};
{% endif %}
{% endif %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}

View File

@ -7,21 +7,20 @@ server {
{% include "_assets.conf" %} {% include "_assets.conf" %}
{% include "_exploits.conf" %} {% include "_exploits.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
access_log /data/logs/redirection-host-{{ id }}_access.log standard; access_log /data/logs/redirection_host-{{ id }}.log standard;
error_log /data/logs/redirection-host-{{ id }}_error.log warn;
{{ advanced_config }} {{ advanced_config }}
{% if use_default_location %} {% if use_default_location %}
location / { location / {
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
{% if preserve_path == 1 or preserve_path == true %} {% if preserve_path == 1 or preserve_path == true %}
return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri; return 301 $scheme://{{ forward_domain_name }}$request_uri;
{% else %} {% else %}
return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}; return 301 $scheme://{{ forward_domain_name }};
{% endif %} {% endif %}
} }
{% endif %} {% endif %}

View File

@ -6,13 +6,7 @@
{% if tcp_forwarding == 1 or tcp_forwarding == true -%} {% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server { server {
listen {{ incoming_port }}; listen {{ incoming_port }};
{% if ipv6 -%} proxy_pass {{ forward_ip }}:{{ forwarding_port }};
listen [::]:{{ incoming_port }};
{% else -%}
#listen [::]:{{ incoming_port }};
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom # Custom
include /data/nginx/custom/server_stream[.]conf; include /data/nginx/custom/server_stream[.]conf;
@ -22,12 +16,7 @@ server {
{% if udp_forwarding == 1 or udp_forwarding == true %} {% if udp_forwarding == 1 or udp_forwarding == true %}
server { server {
listen {{ incoming_port }} udp; listen {{ incoming_port }} udp;
{% if ipv6 -%} proxy_pass {{ forward_ip }}:{{ forwarding_port }};
listen [::]:{{ incoming_port }} udp;
{% else -%}
#listen [::]:{{ incoming_port }} udp;
{% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom # Custom
include /data/nginx/custom/server_stream[.]conf; include /data/nginx/custom/server_stream[.]conf;

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
rules:
# If the efficiency is measured below X%, mark as failed.
# Expressed as a ratio between 0-1.
lowestEfficiency: 0.99
# If the amount of wasted space is at least X or larger than X, mark as failed.
# Expressed in B, KB, MB, and GB.
highestWastedBytes: 15MB
# If the amount of wasted space makes up for X% or more of the image, mark as failed.
# Note: the base image layer is NOT included in the total image size.
# Expressed as a ratio between 0-1; fails if the threshold is met or crossed.
highestUserWastedPercent: 0.02

View File

@ -3,61 +3,45 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build # This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/nginx-full:certbot-node FROM --platform=${TARGETPLATFORM:-linux/amd64} jc21/alpine-nginx-full:node
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG BUILD_VERSION ARG BUILD_VERSION
ARG BUILD_COMMIT ARG BUILD_COMMIT
ARG BUILD_DATE ARG BUILD_DATE
ENV SUPPRESS_NO_CONFIG_WARNING=1 \ ENV SUPPRESS_NO_CONFIG_WARNING=1
S6_FIX_ATTRS_HIDDEN=1 \ ENV S6_FIX_ATTRS_HIDDEN=1
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ ENV NODE_ENV=production
NODE_ENV=production \
NPM_BUILD_VERSION="${BUILD_VERSION}" \
NPM_BUILD_COMMIT="${BUILD_COMMIT}" \
NPM_BUILD_DATE="${BUILD_DATE}"
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \ && rm -rf /etc/nginx \
&& apt-get install -y --no-install-recommends jq logrotate \ && apk update \
&& apt-get clean \ && apk add python2 certbot jq \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/cache/apk/*
ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}"
# s6 overlay # s6 overlay
COPY scripts/install-s6 /tmp/install-s6 COPY scripts/install-s6 /tmp/install-s6
RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
EXPOSE 80 81 443 EXPOSE 80
EXPOSE 81
EXPOSE 443
COPY backend /app COPY docker/rootfs /
COPY frontend/dist /app/frontend ADD backend /app
COPY global /app/global ADD frontend/dist /app/frontend
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 /etc/nginx/conf.d/dev.conf RUN rm -rf /etc/services.d/frontend RUN rm -f /etc/nginx/conf.d/dev.conf
# Change permission of logrotate config file
RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager
# fix for pip installs
# https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1769
RUN pip uninstall --yes setuptools \
&& pip install "setuptools==58.0.0"
VOLUME [ "/data", "/etc/letsencrypt" ] VOLUME [ "/data", "/etc/letsencrypt" ]
ENTRYPOINT [ "/init" ] CMD [ "/init" ]
LABEL org.label-schema.schema-version="1.0" \ HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health
org.label-schema.license="MIT" \
org.label-schema.name="nginx-proxy-manager" \
org.label-schema.description="Docker container for managing Nginx proxy hosts with a simple, powerful interface " \
org.label-schema.url="https://github.com/jc21/nginx-proxy-manager" \
org.label-schema.vcs-url="https://github.com/jc21/nginx-proxy-manager.git" \
org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:latest"

View File

@ -1,15 +1,15 @@
FROM nginxproxymanager/nginx-full:certbot-node FROM jc21/alpine-nginx-full:node
LABEL maintainer="Jamie Curnow <jc@jc21.com>" LABEL maintainer="Jamie Curnow <jc@jc21.com>"
ENV S6_LOGGING=0 \ ENV S6_LOGGING=0
SUPPRESS_NO_CONFIG_WARNING=1 \ ENV SUPPRESS_NO_CONFIG_WARNING=1
S6_FIX_ATTRS_HIDDEN=1 ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \ && rm -rf /etc/nginx \
&& apt-get install -y certbot jq python3-pip logrotate \ && apk update \
&& apt-get clean \ && apk add python2 certbot jq \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/cache/apk/*
# Task # Task
RUN cd /usr \ RUN cd /usr \
@ -18,12 +18,15 @@ RUN cd /usr \
COPY rootfs / COPY rootfs /
RUN rm -f /etc/nginx/conf.d/production.conf RUN rm -f /etc/nginx/conf.d/production.conf
RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager
# s6 overlay # s6 overlay
RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz" \ RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz" \
&& tar -xzf /tmp/s6-overlay-amd64.tar.gz -C / && tar -xzf /tmp/s6-overlay-amd64.tar.gz -C /
EXPOSE 80 81 443 EXPOSE 80
ENTRYPOINT [ "/init" ] EXPOSE 81
EXPOSE 443
CMD [ "/init" ]
HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health

View File

@ -2,45 +2,20 @@
version: "3" version: "3"
services: services:
fullstack-mysql: fullstack:
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
- 443 - 443
depends_on: depends_on:
- db - db
healthcheck:
test: ["CMD", "/bin/check-health"]
interval: 10s
timeout: 3s
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
healthcheck:
test: ["CMD", "/bin/check-health"]
interval: 10s
timeout: 3s
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
@ -52,24 +27,13 @@ services:
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
cypress-mysql: cypress:
image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} image: ${IMAGE}-cypress:ci-${BUILD_NUMBER}
build: build:
context: ../test/ context: ../
dockerfile: cypress/Dockerfile dockerfile: test/cypress/Dockerfile
environment: environment:
CYPRESS_baseUrl: "http://fullstack-mysql:81" CYPRESS_baseUrl: "http://fullstack: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

@ -1,9 +1,9 @@
# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. # WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
version: "3.5" version: "3"
services: services:
npm: npm:
image: nginxproxymanager:dev image: nginxproxymanager:dev
container_name: npm_core
build: build:
context: ./ context: ./
dockerfile: ./dev/Dockerfile dockerfile: ./dev/Dockerfile
@ -11,36 +11,20 @@ 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
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
- ../backend:/app - ..:/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
container_name: npm_db
ports:
- 33306:3306
networks:
- nginx_proxy_manager
environment: environment:
MYSQL_ROOT_PASSWORD: "npm" MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: "npm" MYSQL_DATABASE: "npm"
@ -49,14 +33,17 @@ services:
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
swagger:
image: 'swaggerapi/swagger-ui:latest'
ports:
- 3001:80
environment:
URL: "http://127.0.0.1:3081/api/schema"
PORT: '80'
depends_on:
- npm
volumes: volumes:
npm_data: npm_data:
name: npm_core_data
le_data: le_data:
name: npm_le_data
db_data: db_data:
name: npm_db_data
networks:
nginx_proxy_manager:
name: npm_network

View File

@ -1,46 +0,0 @@
#!/bin/bash
# This command reads the `DISABLE_IPV6` env var and will either enable
# or disable ipv6 in all nginx configs based on this setting.
# Lowercase
DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
CYAN='\E[1;36m'
BLUE='\E[1;34m'
YELLOW='\E[1;33m'
RED='\E[1;31m'
RESET='\E[0m'
FOLDER=$1
if [ "$FOLDER" == "" ]; then
echo -e "${RED} $0 requires a absolute folder path as the first argument!${RESET}"
echo -e "${YELLOW} ie: $0 /data/nginx${RESET}"
exit 1
fi
FILES=$(find "$FOLDER" -type f -name "*.conf")
if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts"
echo -e "${BLUE} ${CYAN}Disabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}"
# Iterate over configs and run the regex
for FILE in $FILES
do
echo -e " ${BLUE} ${YELLOW}${FILE}${RESET}"
sed -E -i 's/^([^#]*)listen \[::\]/\1#listen [::]/g' "$FILE"
done
else
# IPV6 is enabled
echo -e "${BLUE} ${CYAN}Enabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}"
# Iterate over configs and run the regex
for FILE in $FILES
do
echo -e " ${BLUE} ${YELLOW}${FILE}${RESET}"
sed -E -i 's/^(\s*)#listen \[::\]/\1listen [::]/g' "$FILE"
done
fi

View File

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

View File

@ -1,7 +0,0 @@
#!/usr/bin/with-contenv bash
set -e
mkdir -p /data/logs
echo "Changing ownership of /data/logs to $(id -u):$(id -g)"
chown -R "$(id -u):$(id -g)" /data/logs

View File

@ -1,29 +0,0 @@
#!/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
# search 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

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

View File

@ -1,25 +0,0 @@
/data/logs/*_access.log /data/logs/*/access.log {
create 0644 root root
weekly
rotate 4
missingok
notifempty
compress
sharedscripts
postrotate
/bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}
/data/logs/*_error.log /data/logs/*/error.log {
create 0644 root root
weekly
rotate 10
missingok
notifempty
compress
sharedscripts
postrotate
/bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}

View File

@ -8,11 +8,10 @@ server {
set $port "80"; set $port "80";
server_name localhost-nginx-proxy-manager; server_name localhost-nginx-proxy-manager;
access_log /data/logs/fallback_access.log standard; access_log /data/logs/default.log standard;
error_log /data/logs/fallback_error.log warn; error_log /dev/null crit;
include conf.d/include/assets.conf; include conf.d/include/assets.conf;
include conf.d/include/block-exploits.conf; include conf.d/include/block-exploits.conf;
include conf.d/include/letsencrypt-acme-challenge.conf;
location / { location / {
index index.html; index index.html;
@ -30,7 +29,7 @@ server {
set $port "443"; set $port "443";
server_name localhost; server_name localhost;
access_log /data/logs/fallback_access.log standard; access_log /data/logs/default.log standard;
error_log /dev/null crit; error_log /dev/null crit;
ssl_certificate /data/nginx/dummycert.pem; ssl_certificate /data/nginx/dummycert.pem;
ssl_certificate_key /data/nginx/dummykey.pem; ssl_certificate_key /data/nginx/dummykey.pem;

View File

@ -17,9 +17,6 @@ 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,4 +1,4 @@
location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\.map|js\.map)$ { location ~* ^.*\.(css|js|jpe?g|gif|png|woff|eot|ttf|svg|ico|css\.map|js\.map)$ {
if_modified_since off; if_modified_since off;
# use the public cache # use the public cache

View File

@ -1,2 +1,196 @@
# This should be left blank is it is populated programatically
# by the application backend. set_real_ip_from 144.220.0.0/16;
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

@ -5,7 +5,6 @@ location ^~ /.well-known/acme-challenge/ {
# Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure # Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure
# we need to open up access by turning off auth and IP ACL for this location. # we need to open up access by turning off auth and IP ACL for this location.
auth_basic off; auth_basic off;
auth_request off;
allow all; allow all;
# Set correct content type. According to this: # Set correct content type. According to this:

View File

@ -2,7 +2,5 @@ add_header X-Served-By $host;
proxy_set_header Host $host; 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 $proxy_add_x_forwarded_for; 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$request_uri;

View File

@ -3,5 +3,7 @@ ssl_session_cache shared:SSL:50m;
# intermediate configuration. tweak to your needs. # intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; ssl_ciphers 'EECDH+AESGCM:AES256+EECDH:AES256+EDH:EDH+AESGCM:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-
ssl_prefer_server_ciphers off; ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE
S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES';
ssl_prefer_server_ciphers on;

View File

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

@ -9,7 +9,7 @@ worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing. # Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on; pcre_jit on;
error_log /data/logs/fallback_error.log warn; error_log /data/logs/error.log warn;
# Includes files with directives to load dynamic modules. # Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf; include /etc/nginx/modules/*.conf;
@ -26,15 +26,12 @@ http {
tcp_nopush on; tcp_nopush on;
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 65;
proxy_connect_timeout 90s;
proxy_send_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;
client_max_body_size 2000m; client_max_body_size 2000m;
server_names_hash_bucket_size 1024; server_names_hash_bucket_size 64;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -46,7 +43,8 @@ http {
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"';
access_log /data/logs/fallback_access.log proxy;
access_log /data/logs/default.log proxy;
# Dynamically generated resolvers file # Dynamically generated resolvers file
include /etc/nginx/conf.d/include/resolvers.conf; include /etc/nginx/conf.d/include/resolvers.conf;
@ -57,20 +55,14 @@ http {
} }
# Real IP Determination # Real IP Determination
# Docker subnet:
# Local subnets: set_real_ip_from 172.0.0.0/8;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12; # Includes Docker subnet
set_real_ip_from 192.168.0.0/16;
# 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-Real-IP; real_ip_header X-Forwarded-For;
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;
@ -86,9 +78,6 @@ 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

@ -4,7 +4,6 @@
if [ "$DEVELOPMENT" == "true" ]; then if [ "$DEVELOPMENT" == "true" ]; then
cd /app/frontend || exit 1 cd /app/frontend || exit 1
# If yarn install fails: add --verbose --network-concurrency 1
yarn install yarn install
yarn watch yarn watch
else else

View File

@ -5,8 +5,7 @@ mkdir -p /data/letsencrypt-acme-challenge
cd /app || echo cd /app || echo
if [ "$DEVELOPMENT" == "true" ]; then if [ "$DEVELOPMENT" == "true" ]; then
cd /app || exit 1 cd /app/backend || exit 1
# If yarn install fails: add --verbose --network-concurrency 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

@ -24,12 +24,8 @@ chown root /tmp/nginx
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` # Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm # thanks @tfmm
if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" {print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf);" > /etc/nginx/conf.d/include/resolvers.conf
then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
fi
# Generate dummy self-signed certificate. # Generate dummy self-signed certificate.
if [ ! -f /data/nginx/dummycert.pem ] || [ ! -f /data/nginx/dummykey.pem ] if [ ! -f /data/nginx/dummycert.pem ] || [ ! -f /data/nginx/dummykey.pem ]
then then
@ -40,14 +36,10 @@ then
-days 3650 \ -days 3650 \
-nodes \ -nodes \
-x509 \ -x509 \
-subj '/O=localhost/OU=localhost/CN=localhost' \ -subj '/O=Nginx Proxy Manager/OU=Dummy Certificate/CN=localhost' \
-keyout /data/nginx/dummykey.pem \ -keyout /data/nginx/dummykey.pem \
-out /data/nginx/dummycert.pem -out /data/nginx/dummycert.pem
echo "Complete" echo "Complete"
fi fi
# Handle IPV6 settings
/bin/handle-ipv6-setting /etc/nginx/conf.d
/bin/handle-ipv6-setting /data/nginx
exec nginx exec nginx

View File

@ -16,7 +16,5 @@ alias h='cd ~;clear;'
echo -e -n '\E[1;34m' echo -e -n '\E[1;34m'
figlet -w 120 "NginxProxyManager" figlet -w 120 "NginxProxyManager"
echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, ${ID:-debian} \E[1;32m${VERSION:-unknown}\E[1;36m, Certbot \E[1;32m$(certbot --version)\E[0m" echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev}\E[1;36m (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}, Nginx \E[1;32m${NGINX_VERSION:-unknown}\E[1;36m, Alpine \E[1;32m${VERSION_ID:-unknown}\E[1;36m, Kernel \E[1;32m$(uname -r)\E[0m"
echo -e -n '\E[1;34m' echo
cat /built-for-arch
echo -e '\E[0m'

View File

@ -47,8 +47,6 @@ module.exports = {
["/screenshots/", "Screenshots"], ["/screenshots/", "Screenshots"],
["/setup/", "Setup Instructions"], ["/setup/", "Setup Instructions"],
["/advanced-config/", "Advanced Configuration"], ["/advanced-config/", "Advanced Configuration"],
["/upgrading/", "Upgrading"],
["/faq/", "Frequently Asked Questions"],
["/third-party/", "Third Party"] ["/third-party/", "Third Party"]
] ]
} }

View File

@ -37,3 +37,57 @@ footer: MIT Licensed | Copyright © 2016-present jc21.com
<p>Configure other users to either view or manage their own hosts. Full access permissions are available.</p> <p>Configure other users to either view or manage their own hosts. Full access permissions are available.</p>
</div> </div>
</div> </div>
### Quick Setup
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this:
```yml
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./config.json:/app/config/production.json
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
db:
image: 'jc21/mariadb-aria:10.4'
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
volumes:
- ./data/mysql:/var/lib/mysql
```
3. Bring up your stack
```bash
docker-compose up -d
```
4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface.
[http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.

View File

@ -1,146 +1,6 @@
# Advanced Configuration # Advanced Configuration
## Best Practice: Use a Docker network ### Custom Nginx Configurations
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: true
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: unless-stopped
networks:
default:
external: true
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 Healthcheck
The `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this
feature by adding the following to the service in your `docker-compose.yml` file:
```yml
healthcheck:
test: ["CMD", "/bin/check-health"]
interval: 10s
timeout: 3s
```
## 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: unless-stopped
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
secrets:
- MYSQL_PWD
depends_on:
- db
db:
image: jc21/mariadb-aria
restart: unless-stopped
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
secrets:
- DB_ROOT_PWD
- MYSQL_PWD
```
## Disabling IPv6
On some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log:
> Address family not supported by protocol
The easy fix is to add a Docker environment variable to the Nginx Proxy Manager stack:
```yml
environment:
DISABLE_IPV6: 'true'
```
## Custom Nginx Configurations
If you are a more advanced user, you might be itching for extra Nginx customizability. If you are a more advanced user, you might be itching for extra Nginx customizability.
@ -148,20 +8,18 @@ NPM has the ability to include different custom configuration snippets in differ
You can add your custom configuration snippet files at `/data/nginx/custom` as follow: You can add your custom configuration snippet files at `/data/nginx/custom` as follow:
- `/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_top.conf`: Included at the top of the main http block `/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/server_proxy.conf`: Included at the end of every proxy server block
- `/data/nginx/custom/stream.conf`: Included at the end of the main stream block `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block
- `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block
- `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block
- `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block
- `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block
- `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block
Every file is optional. Every file is optional.
## X-FRAME-OPTIONS Header ### X-FRAME-OPTIONS Header
You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header
value by specifying it as a Docker environment variable. The default if not specified is `deny`. value by specifying it as a Docker environment variable. The default if not specified is `deny`.

View File

@ -1,26 +0,0 @@
# FAQ
## Do I have to use Docker?
Yes, that's how this project is packaged.
This makes it easier to support the project when I have control over the version of Nginx and NodeJS
being used. In future this could change if the backend was no longer using NodeJS and it's long list
of dependencies.
## Can I run it on a Raspberry Pi?
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
[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.
## When adding username and password access control to a proxy host, I can no longer login into the app.
Having an Access Control List (ACL) with username and password requires the browser to always send this username and password in the `Authorization` header on each request. If your proxied app also requires authentication (like Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information, as this is the standardized header meant for this kind of information. However having multiples of the same headers is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization.

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,85 @@
# Full Setup Instructions # Full Setup Instructions
## Running the App ### Configuration File
Create a `docker-compose.yml` file: **The configuration file needs to be provided by you!**
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+
- MariaDB v10.2.7+
It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples
are going to use.
::: warning
When using a `mariadb` database, the NPM configuration file should still use the `mysql` engine!
:::
### Running the App
Via `docker-compose`:
```yml ```yml
version: "3" version: "3"
services: services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: jc21/nginx-proxy-manager:2
restart: unless-stopped restart: always
ports: ports:
# These ports are in format <host-port>:<container-port> # Public HTTP Port:
- '80:80' # Public HTTP Port - '80:80'
- '443:443' # Public HTTPS Port # Public HTTPS Port:
- '81:81' # Admin Web Port - '443:443'
# Add any other Stream port you want to expose # Admin Web Port:
# - '21:21' # FTP - '81:81'
# Uncomment the next line if you uncomment anything in the section
# environment:
# Uncomment this if you want to change the location of
# the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host
# 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:
- db
db:
image: jc21/mariadb-aria:10.4
restart: always
environment:
MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
MYSQL_PASSWORD: "npm"
volumes:
- ./data/mysql:/var/lib/mysql
``` ```
Then: Then:
@ -38,64 +88,9 @@ Then:
docker-compose up -d docker-compose up -d
``` ```
## Using MySQL / MariaDB Database The config file (config.json) must be present in this directory.
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: ### Running on Raspberry PI / ARM devices
- MySQL v5.7.8+
- MariaDB v10.2.7+
It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples
are going to use.
Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container:
```yml
version: "3"
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# These ports are in format <host-port>:<container-port>
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
# 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:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
volumes:
- ./data/mysql:/var/lib/mysql
```
::: warning
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.
:::
## Running on Raspberry PI / ARM devices
The docker images support the following architectures: The docker images support the following architectures:
- amd64 - amd64
@ -105,16 +100,15 @@ The docker images support the following architectures:
The docker images are a manifest of all the architecture docker builds supported, so this means The docker images are a manifest of all the architecture docker builds supported, so this means
you don't have to worry about doing anything special and you can follow the common instructions above. you don't have to worry about doing anything special and you can follow the common instructions above.
Check out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) Check out the [dockerhub tags](https://cloud.docker.com/repository/registry-1.docker.io/jc21/nginx-proxy-manager/tags)
for a list of supported architectures and if you want one that doesn't exist, for a list of supported architectures and if you want one that doesn't exist,
[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). [create a feature request](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).
Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/)
on Raspbian. on Raspbian.
Please note that the `jc21/mariadb-aria:latest` image might have some problems on some ARM devices, if you want a separate database container, use the `yobasystems/alpine-mariadb:latest` image.
## Initial Run ### Initial Run
After the app is running for the first time, the following will happen: After the app is running for the first time, the following will happen:
@ -125,7 +119,7 @@ After the app is running for the first time, the following will happen:
This process can take a couple of minutes depending on your machine. This process can take a couple of minutes depending on your machine.
## Default Administrator User ### Default Administrator User
``` ```
Email: admin@example.com Email: admin@example.com
@ -133,50 +127,3 @@ 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"
},
"useNullAsDefault": true
}
}
}
```
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

@ -1,16 +1,12 @@
# Third Party # Third Party
As this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documentation of those As this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documenation of those
integrations, they are *not supported* by me. integrations, they are *not supported* by me.
Known integrations: 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=)

View File

@ -1,11 +0,0 @@
# Upgrading
```bash
docker-compose pull
docker-compose up -d
```
This project will automatically update any databases or other requirements so you don't have to follow
any crazy instructions. These steps above will pull the latest updates and recreate the docker
containers.

File diff suppressed because it is too large Load Diff

1
frontend/fonts Symbolic link
View File

@ -0,0 +1 @@
./node_modules/tabler-ui/dist/assets/fonts

View File

@ -1 +0,0 @@
../node_modules/tabler-ui/dist/assets/fonts/feather

Some files were not shown because too many files have changed in this diff Show More