Compare commits

..

4 Commits

227 changed files with 17267 additions and 12282 deletions

View File

@ -6,30 +6,20 @@ labels: bug
assignees: '' assignees: ''
--- ---
<!--
Are you in the right place? **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 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 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.* - 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 +28,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,19 @@ assignees: ''
--- ---
<!-- **Are you in the right place?**
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 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 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.* - If you have a feature request for NPM then you are in the *right place.*
-->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] --> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**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.

View File

@ -1,21 +0,0 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:'
stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:'
close-issue-message: 'Issue was closed due to inactivity.'
close-pr-message: 'PR was closed due to inactivity.'
days-before-stale: 182
days-before-close: 365
operations-per-run: 50

View File

@ -1 +1 @@
2.11.2 2.8.1

295
Jenkinsfile vendored
View File

@ -1,9 +1,3 @@
import groovy.transform.Field
@Field
def shOutput = ""
def buildxPushTags = ""
pipeline { pipeline {
agent { agent {
label 'docker-multiarch' label 'docker-multiarch'
@ -14,10 +8,10 @@ pipeline {
ansiColor('xterm') ansiColor('xterm')
} }
environment { environment {
IMAGE = 'nginx-proxy-manager' IMAGE = "nginx-proxy-manager"
BUILD_VERSION = getVersion() BUILD_VERSION = getVersion()
MAJOR_VERSION = '2' MAJOR_VERSION = "2"
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}" BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}"
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_FILE = 'docker/docker-compose.ci.yml' COMPOSE_FILE = 'docker/docker-compose.ci.yml'
COMPOSE_INTERACTIVE_NO_CLI = 1 COMPOSE_INTERACTIVE_NO_CLI = 1
@ -32,7 +26,7 @@ pipeline {
} }
steps { steps {
script { script {
buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
} }
} }
} }
@ -45,7 +39,7 @@ pipeline {
steps { steps {
script { script {
// Defaults to the Branch name, which is applies to all branches AND pr's // Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
} }
} }
} }
@ -60,114 +54,105 @@ pipeline {
} }
} }
} }
stage('Builds') { stage('Frontend') {
parallel { steps {
stage('Project') { sh './scripts/frontend-build'
steps { }
script { }
// Frontend and Backend stage('Backend') {
def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: ''' steps {
set -e echo 'Checking Syntax ...'
./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1 // See: https://github.com/yarnpkg/yarn/issues/3254
./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1 sh '''docker run --rm \\
''') -v "$(pwd)/backend:/app" \\
shOutput = readFile "${env.WORKSPACE}/tmp-sh-build" -v "$(pwd)/global:/app/global" \\
if (shStatusCode != 0) { -w /app \\
error "${shOutput}" node:latest \\
} sh -c "yarn install && yarn eslint . && rm -rf node_modules"
} '''
}
post { echo 'Docker Build ...'
always { sh '''docker build --pull --no-cache --squash --compress \\
sh 'rm -f ${WORKSPACE}/tmp-sh-build' -t "${IMAGE}:ci-${BUILD_NUMBER}" \\
} -f docker/Dockerfile \\
failure { --build-arg TARGETPLATFORM=linux/amd64 \\
npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true) --build-arg BUILDPLATFORM=linux/amd64 \\
} --build-arg BUILD_VERSION="${BUILD_VERSION}" \\
} --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\
} --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\
stage('Docs') { .
steps { '''
dir(path: 'docs') { }
sh 'yarn install' }
sh 'yarn build' stage('Integration Tests Sqlite') {
} steps {
dir(path: 'docs/.vuepress/dist') { // Bring up a stack
sh 'tar -czf ../../docs.tgz *' sh 'docker-compose up -d fullstack-sqlite'
} sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120'
archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false)
} // Run tests
} sh 'rm -rf test/results'
stage('Cypress') { sh 'docker-compose up cypress-sqlite'
steps { // Get results
// Creating will also create the network prior to sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/'
// using it in parallel stages below and mitigating }
// a race condition. post {
sh 'docker-compose build cypress-sqlite' always {
sh 'docker-compose build cypress-mysql' // Dumps to analyze later
sh 'docker-compose create cypress-sqlite' sh 'mkdir -p debug'
sh 'docker-compose create cypress-mysql' sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz'
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
// Cypress videos and screenshot artifacts
dir(path: 'test/results') {
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
} }
junit 'test/results/junit/*'
} }
} }
} }
stage('Integration Tests') { stage('Integration Tests Mysql') {
parallel { steps {
stage('Sqlite') { // Bring up a stack
steps { sh 'docker-compose up -d fullstack-mysql'
// Bring up a stack sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120'
sh 'docker-compose up -d fullstack-sqlite'
sh './scripts/wait-healthy $(docker-compose ps --all -q fullstack-sqlite) 120'
// Stop and Start it, as this will test it's ability to restart with existing data
sh 'docker-compose stop fullstack-sqlite'
sh 'docker-compose start fullstack-sqlite'
sh './scripts/wait-healthy $(docker-compose ps --all -q fullstack-sqlite) 120'
// Run tests // Run tests
sh 'rm -rf test/results-sqlite' sh 'rm -rf test/results'
sh 'docker-compose up cypress-sqlite' sh 'docker-compose up cypress-mysql'
// Get results // Get results
sh 'docker cp -L "$(docker-compose ps --all -q cypress-sqlite):/test/results" test/results-sqlite' sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/'
} }
post { post {
always { always {
// Dumps to analyze later // Dumps to analyze later
sh 'mkdir -p debug/sqlite' sh 'mkdir -p debug'
sh 'docker-compose logs fullstack-sqlite > debug/sqlite/docker_fullstack_sqlite.log' sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz'
// Cypress videos and screenshot artifacts sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
dir(path: 'test/results-sqlite') { // Cypress videos and screenshot artifacts
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' dir(path: 'test/results') {
} archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
junit 'test/results-sqlite/junit/*'
}
} }
junit 'test/results/junit/*'
}
}
}
stage('Docs') {
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
dir(path: 'docs') {
sh 'yarn install'
sh 'yarn build'
} }
stage('Mysql') {
steps {
// Bring up a stack
sh 'docker-compose up -d fullstack-mysql'
sh './scripts/wait-healthy $(docker-compose ps --all -q fullstack-mysql) 120'
// Run tests dir(path: 'docs/.vuepress/dist') {
sh 'rm -rf test/results-mysql' sh 'tar -czf ../../docs.tgz *'
sh 'docker-compose up cypress-mysql'
// Get results
sh 'docker cp -L "$(docker-compose ps --all -q cypress-mysql):/test/results" test/results-mysql'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/mysql'
sh 'docker-compose logs fullstack-mysql > debug/mysql/docker_fullstack_mysql.log'
sh 'docker-compose logs db > debug/mysql/docker_db.log'
// Cypress videos and screenshot artifacts
dir(path: 'test/results-mysql') {
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
}
junit 'test/results-mysql/junit/*'
}
}
} }
archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false)
} }
} }
stage('MultiArch Build') { stage('MultiArch Build') {
@ -178,60 +163,64 @@ pipeline {
} }
steps { steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"' // Docker Login
sh "./scripts/buildx --push ${buildxPushTags}" sh "docker login -u '${duser}' -p '${dpass}'"
// Buildx with push from cache
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}"
} }
} }
} }
stage('Docs / Comment') { stage('Docs Deploy') {
parallel { when {
stage('Master Docs') { allOf {
when { branch 'master'
allOf { not {
branch 'master' equals expected: 'UNSTABLE', actual: currentBuild.result
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
npmDocsReleaseMaster()
} }
} }
stage('Develop Docs') { }
when { steps {
allOf { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) {
branch 'develop' sh """docker run --rm \\
not { --name \${COMPOSE_PROJECT_NAME}-docs-upload \\
equals expected: 'UNSTABLE', actual: currentBuild.result -e S3_BUCKET=jc21-npm-site \\
} -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
} -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
} -v \$(pwd):/app \\
steps { -w /app \\
npmDocsReleaseDevelop() jc21/ci-tools \\
scripts/docs-upload /app/docs/.vuepress/dist/
"""
sh """docker run --rm \\
--name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
jc21/ci-tools \\
aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*'
"""
}
}
}
stage('PR Comment') {
when {
allOf {
changeRequest()
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
} }
} }
stage('PR Comment') { }
when { steps {
allOf { script {
changeRequest() 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}`")
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
script {
npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true)
}
}
} }
} }
} }
} }
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 jc21/ci-tools chown -R $(id -u):$(id -g) /data'
} }
@ -240,12 +229,12 @@ pipeline {
sh 'figlet "SUCCESS"' sh 'figlet "SUCCESS"'
} }
failure { failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
juxtapose event: 'failure' juxtapose event: 'failure'
sh 'figlet "FAILURE"' sh 'figlet "FAILURE"'
} }
unstable { unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
juxtapose event: 'unstable' juxtapose event: 'unstable'
sh 'figlet "UNSTABLE"' sh 'figlet "UNSTABLE"'
} }

286
README.md
View File

@ -1,25 +1,31 @@
<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.11.2-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.8.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>
<a href="https://gitter.im/nginx-proxy-manager/community">
<img alt="Gitter" src="https://img.shields.io/gitter/room/nginx-proxy-manager/community?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/)
## Project Goal ## Project Goal
I created this project to fill a personal need to provide users with an easy way to accomplish reverse I created this project to fill a personal need to provide users with a easy way to accomplish reverse
proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed. proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.
While there might be advanced options they are optional and the project should be as simple as possible While there might be advanced options they are optional and the project should be as simple as possible
so that the barrier for entry here is low. so that the barrier for entry here is low.
@ -46,76 +52,210 @@ I won't go in to too much detail here but here are the basics for someone new to
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 ## Contributors
- [Docker Install documentation](https://docs.docker.com/install/) Special thanks to the following contributors:
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this: <!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
```yml <table>
version: '3.8' <tr>
services: <td align="center">
app: <a href="https://github.com/Subv">
image: 'docker.io/jc21/nginx-proxy-manager:latest' <img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80px;" alt=""/>
restart: unless-stopped <br /><sub><b>Sebastian Valle</b></sub>
ports: </a>
- '80:80' </td>
- '81:81' <td align="center">
- '443:443' <a href="https://github.com/Indemnity83">
volumes: <img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80px;" alt=""/>
- ./data:/data <br /><sub><b>Kyle Klaus</b></sub>
- ./letsencrypt:/etc/letsencrypt </a>
``` </td>
<td align="center">
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. <a href="https://github.com/theraw">
<img src="https://avatars1.githubusercontent.com/u/32969774?s=460&u=6b359971e15685fb0359e6a8c065a399b40dc228&v=4" width="80px;" alt=""/>
3. Bring up your stack by running <br /><sub><b>ƬHE ЯAW</b></sub>
</a>
```bash </td>
docker-compose up -d <td align="center">
<a href="https://github.com/spalger">
# If using docker-compose-plugin <img src="https://avatars2.githubusercontent.com/u/1329312?s=400&u=565223e38f1c052afb4c5dcca3fcf1c63ba17ae7&v=4" width="80px;" alt=""/>
docker compose up -d <br /><sub><b>Spencer</b></sub>
</a>
``` </td>
<td align="center">
4. Log in to the Admin UI <a href="https://github.com/Xantios">
<img src="https://avatars3.githubusercontent.com/u/1507836?s=460&v=4" width="80px;" alt=""/>
When your docker container is running, connect to it on port `81` for the admin interface. <br /><sub><b>Xantios Krugor</b></sub>
Sometimes this can take a little bit because of the entropy of keys. </a>
</td>
[http://127.0.0.1:81](http://127.0.0.1:81) <td align="center">
<a href="https://github.com/dpanesso">
Default Admin User: <img src="https://avatars2.githubusercontent.com/u/2687121?s=460&v=4" width="80px;" alt=""/>
``` <br /><sub><b>David Panesso</b></sub>
Email: admin@example.com </a>
Password: changeme </td>
``` <td align="center">
<a href="https://github.com/IronTooch">
Immediately after logging in with this default user you will be asked to modify your details and change your password. <img src="https://avatars3.githubusercontent.com/u/27360514?s=460&u=69bf854a6647c55725f62ecb8d39249c6c0b2602&v=4" width="80px;" alt=""/>
<br /><sub><b>IronTooch</b></sub>
</a>
## Contributing </td>
</tr>
All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. <tr>
<td align="center">
CI is used in this project. All PR's must pass before being considered. After passing, <a href="https://github.com/damianog">
docker builds for PR's are available on dockerhub for manual verifications. <img src="https://avatars1.githubusercontent.com/u/2786682?s=460&u=76c6136fae797abb76b951cd8a246dcaecaf21af&v=4" width="80px;" alt=""/>
<br /><sub><b>Damiano</b></sub>
Documentation within the `develop` branch is available for preview at </a>
[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com) </td>
<td align="center">
<a href="https://github.com/tfmm">
### Contributors <img src="https://avatars3.githubusercontent.com/u/6880538?s=460&u=ce0160821cc4aa802df8395200f2d4956a5bc541&v=4" width="80px;" alt=""/>
<br /><sub><b>Russ</b></sub>
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). </a>
</td>
<td align="center">
## Getting Support <a href="https://github.com/margaale">
<img src="https://avatars3.githubusercontent.com/u/20794934?s=460&v=4" width="80px;" alt=""/>
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) <br /><sub><b>Marcelo Castagna</b></sub>
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) </a>
3. [Reddit](https://reddit.com/r/nginxproxymanager) </td>
<td align="center">
<a href="https://github.com/Steven-Harris">
<img src="https://avatars2.githubusercontent.com/u/7720242?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Steven Harris</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jlesage">
<img src="https://avatars0.githubusercontent.com/u/1791123?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Jocelyn Le Sage</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/cmer">
<img src="https://avatars0.githubusercontent.com/u/412?s=460&u=67dd8b2e3661bfd6f68ec1eaa5b9821bd8a321cd&v=4" width="80px;" alt=""/>
<br /><sub><b>Carl Mercier</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/the1ts">
<img src="https://avatars1.githubusercontent.com/u/84956?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Paul Mansfield</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/OhHeyAlan">
<img src="https://avatars0.githubusercontent.com/u/11955126?s=460&u=fbaa5a1a4f73ef8960132c703349bfd037fe2630&v=4" width="80px;" alt=""/>
<br /><sub><b>OhHeyAlan</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/dogmatic69">
<img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80px;" alt=""/>
<br /><sub><b>Carl Sutton</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/tg44">
<img src="https://avatars0.githubusercontent.com/u/31839?s=460&u=ad32f4cadfef5e5fb09cdfa4b7b7b36a99ba6811&v=4" width="80px;" alt=""/>
<br /><sub><b>Gergő Törcsvári</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/vrenjith">
<img src="https://avatars3.githubusercontent.com/u/2093241?s=460&u=96ce93a9bebabdd0a60a2dc96cd093a41d5edaba&v=4" width="80px;" alt=""/>
<br /><sub><b>vrenjith</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/duhruh">
<img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80px;" alt=""/>
<br /><sub><b>David Rivera</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jipjan">
<img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Jaap-Jan de Wit</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jmwebslave">
<img src="https://avatars2.githubusercontent.com/u/6118262?s=460&u=7db409c47135b1e141c366bbb03ed9fae6ac2638&v=4" width="80px;" alt=""/>
<br /><sub><b>James Morgan</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/chaptergy">
<img src="https://avatars2.githubusercontent.com/u/26956711?s=460&u=7d9adebabb6b4e7af7cb05d98d751087a372304b&v=4" width="80px;" alt=""/>
<br /><sub><b>chaptergy</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Philip-Mooney">
<img src="https://avatars0.githubusercontent.com/u/48624631?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Philip Mooney</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/WaterCalm">
<img src="https://avatars1.githubusercontent.com/u/23502129?s=400&v=4" width="80px;" alt=""/>
<br /><sub><b>WaterCalm</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lebrou34">
<img src="https://avatars1.githubusercontent.com/u/16373103?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>lebrou34</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lightglitch">
<img src="https://avatars0.githubusercontent.com/u/196953?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Mário Franco</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/klutchell">
<img src="https://avatars3.githubusercontent.com/u/20458272?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Kyle Harding</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ahgraber">
<img src="https://avatars.githubusercontent.com/u/24922003?s=460&u=8376c9f00af9b6057ba4d2fb03b4f1b20a75277f&v=4" width="80px;" alt=""/>
<br /><sub><b>Alex Graber</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/MooBaloo">
<img src="https://avatars.githubusercontent.com/u/9493496?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>MooBaloo</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Shuro">
<img src="https://avatars.githubusercontent.com/u/944030?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Shuro</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lorisbergeron">
<img src="https://avatars.githubusercontent.com/u/51918567?s=460&u=778e4ff284b7d7304450f98421c99f79298371fb&v=4" width="80px;" alt=""/>
<br /><sub><b>Loris Bergeron</b></sub>
</a>
</td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

View File

@ -2,7 +2,6 @@ const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const fileUpload = require('express-fileupload'); const fileUpload = require('express-fileupload');
const compression = require('compression'); const compression = require('compression');
const config = require('./lib/config');
const log = require('./logger').express; const log = require('./logger').express;
/** /**
@ -25,7 +24,7 @@ app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable('strict routing'); app.enable('strict routing');
// pretty print JSON when not live // pretty print JSON when not live
if (config.debug()) { if (process.env.NODE_ENV !== 'production') {
app.set('json spaces', 2); app.set('json spaces', 2);
} }
@ -41,12 +40,13 @@ app.use(function (req, res, next) {
} }
res.set({ res.set({
'X-XSS-Protection': '1; mode=block', 'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block',
'X-Frame-Options': x_frame_options, 'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'X-Frame-Options': x_frame_options,
Pragma: 'no-cache', 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Expires: 0 Pragma: 'no-cache',
Expires: 0
}); });
next(); next();
}); });
@ -66,7 +66,7 @@ app.use(function (err, req, res, next) {
} }
}; };
if (config.debug() || (req.baseUrl + req.path).includes('nginx/certificates')) { if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
payload.debug = { payload.debug = {
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null, stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous previous: err.previous
@ -75,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 (config.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,27 +1,33 @@
const config = require('./lib/config'); const config = require('config');
if (!config.has('database')) { if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/'); 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() { function generateDbConfig() {
const cfg = config.get('database'); if (config.database.engine === 'knex-native') {
if (cfg.engine === 'knex-native') { return config.database.knex;
return cfg.knex; } else
} return {
return { client: config.database.engine,
client: cfg.engine, connection: {
connection: { host: config.database.host,
host: cfg.host, user: config.database.user,
user: cfg.user, password: config.database.password,
password: cfg.password, database: config.database.name,
database: cfg.name, port: config.database.port
port: cfg.port },
}, migrations: {
migrations: { tableName: 'migrations'
tableName: 'migrations' }
} };
};
} }
module.exports = require('knex')(generateDbConfig());
let data = generateDbConfig();
if (typeof config.database.version !== 'undefined') {
data.version = config.database.version;
}
module.exports = require('knex')(data);

View File

@ -40,210 +40,6 @@
} }
} }
}, },
"/nginx/proxy-hosts": {
"get": {
"operationId": "getProxyHosts",
"summary": "Get all proxy hosts",
"tags": ["Proxy Hosts"],
"security": [
{
"BearerAuth": ["users"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["access_list", "owner", "certificate"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2023-03-30T01:12:23.000Z",
"modified_on": "2023-03-30T02:15:40.000Z",
"owner_user_id": 1,
"domain_names": ["aasdasdad"],
"forward_host": "asdasd",
"forward_port": 80,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": 0,
"caching_enabled": 0,
"block_exploits": 0,
"advanced_config": "sdfsdfsdf",
"meta": {
"letsencrypt_agree": false,
"dns_challenge": false,
"nginx_online": false,
"nginx_err": "Command failed: /usr/sbin/nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n"
},
"allow_websocket_upgrade": 0,
"http2_support": 0,
"forward_scheme": "http",
"enabled": 1,
"locations": [],
"hsts_enabled": 0,
"hsts_subdomains": 0,
"owner": {
"id": 1,
"created_on": "2023-03-30T01:11:50.000Z",
"modified_on": "2023-03-30T01:11:50.000Z",
"is_deleted": 0,
"is_disabled": 0,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"access_list": null,
"certificate": null
},
{
"id": 2,
"created_on": "2023-03-30T02:11:49.000Z",
"modified_on": "2023-03-30T02:11:49.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"forward_host": "1.1.1.1",
"forward_port": 80,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": 0,
"caching_enabled": 0,
"block_exploits": 0,
"advanced_config": "",
"meta": {
"letsencrypt_agree": false,
"dns_challenge": false,
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": 0,
"http2_support": 0,
"forward_scheme": "http",
"enabled": 1,
"locations": [],
"hsts_enabled": 0,
"hsts_subdomains": 0,
"owner": {
"id": 1,
"created_on": "2023-03-30T01:11:50.000Z",
"modified_on": "2023-03-30T01:11:50.000Z",
"is_deleted": 0,
"is_disabled": 0,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"access_list": null,
"certificate": null
}
]
}
},
"schema": {
"$ref": "#/components/schemas/ProxyHostsList"
}
}
}
}
}
},
"post": {
"operationId": "createProxyHost",
"summary": "Create a Proxy Host",
"tags": ["Proxy Hosts"],
"security": [
{
"BearerAuth": ["users"]
}
],
"parameters": [
{
"in": "body",
"name": "proxyhost",
"description": "Proxy Host Payload",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProxyHostObject"
}
}
],
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 3,
"created_on": "2023-03-30T02:31:27.000Z",
"modified_on": "2023-03-30T02:31:27.000Z",
"owner_user_id": 1,
"domain_names": ["test2.example.com"],
"forward_host": "1.1.1.1",
"forward_port": 80,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": 0,
"caching_enabled": 0,
"block_exploits": 0,
"advanced_config": "",
"meta": {
"letsencrypt_agree": false,
"dns_challenge": false
},
"allow_websocket_upgrade": 0,
"http2_support": 0,
"forward_scheme": "http",
"enabled": 1,
"locations": [],
"hsts_enabled": 0,
"hsts_subdomains": 0,
"certificate": null,
"owner": {
"id": 1,
"created_on": "2023-03-30T01:11:50.000Z",
"modified_on": "2023-03-30T01:11:50.000Z",
"is_deleted": 0,
"is_disabled": 0,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"access_list": null,
"use_default_location": true,
"ipv6": true
}
}
},
"schema": {
"$ref": "#/components/schemas/ProxyHostObject"
}
}
}
}
}
}
},
"/schema": { "/schema": {
"get": { "get": {
"operationId": "schema", "operationId": "schema",
@ -259,10 +55,14 @@
"get": { "get": {
"operationId": "refreshToken", "operationId": "refreshToken",
"summary": "Refresh your access token", "summary": "Refresh your access token",
"tags": ["Tokens"], "tags": [
"Tokens"
],
"security": [ "security": [
{ {
"BearerAuth": ["tokens"] "BearerAuth": [
"tokens"
]
} }
], ],
"responses": { "responses": {
@ -304,14 +104,19 @@
"scope": { "scope": {
"minLength": 1, "minLength": 1,
"type": "string", "type": "string",
"enum": ["user"] "enum": [
"user"
]
}, },
"secret": { "secret": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
} }
}, },
"required": ["identity", "secret"], "required": [
"identity",
"secret"
],
"type": "object" "type": "object"
} }
} }
@ -339,17 +144,23 @@
} }
}, },
"summary": "Request a new access token from credentials", "summary": "Request a new access token from credentials",
"tags": ["Tokens"] "tags": [
"Tokens"
]
} }
}, },
"/settings": { "/settings": {
"get": { "get": {
"operationId": "getSettings", "operationId": "getSettings",
"summary": "Get all settings", "summary": "Get all settings",
"tags": ["Settings"], "tags": [
"Settings"
],
"security": [ "security": [
{ {
"BearerAuth": ["settings"] "BearerAuth": [
"settings"
]
} }
], ],
"responses": { "responses": {
@ -383,10 +194,14 @@
"get": { "get": {
"operationId": "getSetting", "operationId": "getSetting",
"summary": "Get a setting", "summary": "Get a setting",
"tags": ["Settings"], "tags": [
"Settings"
],
"security": [ "security": [
{ {
"BearerAuth": ["settings"] "BearerAuth": [
"settings"
]
} }
], ],
"parameters": [ "parameters": [
@ -429,10 +244,14 @@
"put": { "put": {
"operationId": "updateSetting", "operationId": "updateSetting",
"summary": "Update a setting", "summary": "Update a setting",
"tags": ["Settings"], "tags": [
"Settings"
],
"security": [ "security": [
{ {
"BearerAuth": ["settings"] "BearerAuth": [
"settings"
]
} }
], ],
"parameters": [ "parameters": [
@ -486,10 +305,14 @@
"get": { "get": {
"operationId": "getUsers", "operationId": "getUsers",
"summary": "Get all users", "summary": "Get all users",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -499,7 +322,9 @@
"description": "Expansions", "description": "Expansions",
"schema": { "schema": {
"type": "string", "type": "string",
"enum": ["permissions"] "enum": [
"permissions"
]
} }
} }
], ],
@ -520,7 +345,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"] "roles": [
"admin"
]
} }
] ]
}, },
@ -535,7 +362,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"], "roles": [
"admin"
],
"permissions": { "permissions": {
"visibility": "all", "visibility": "all",
"proxy_hosts": "manage", "proxy_hosts": "manage",
@ -560,10 +389,14 @@
"post": { "post": {
"operationId": "createUser", "operationId": "createUser",
"summary": "Create a User", "summary": "Create a User",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -593,7 +426,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"], "roles": [
"admin"
],
"permissions": { "permissions": {
"visibility": "all", "visibility": "all",
"proxy_hosts": "manage", "proxy_hosts": "manage",
@ -619,10 +454,14 @@
"get": { "get": {
"operationId": "getUser", "operationId": "getUser",
"summary": "Get a user", "summary": "Get a user",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -662,7 +501,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"] "roles": [
"admin"
]
} }
} }
}, },
@ -677,10 +518,14 @@
"put": { "put": {
"operationId": "updateUser", "operationId": "updateUser",
"summary": "Update a User", "summary": "Update a User",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -729,7 +574,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"] "roles": [
"admin"
]
} }
} }
}, },
@ -744,10 +591,14 @@
"delete": { "delete": {
"operationId": "deleteUser", "operationId": "deleteUser",
"summary": "Delete a User", "summary": "Delete a User",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -786,10 +637,14 @@
"put": { "put": {
"operationId": "updateUserAuth", "operationId": "updateUserAuth",
"summary": "Update a User's Authentication", "summary": "Update a User's Authentication",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -845,10 +700,14 @@
"put": { "put": {
"operationId": "updateUserPermissions", "operationId": "updateUserPermissions",
"summary": "Update a User's Permissions", "summary": "Update a User's Permissions",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -896,10 +755,14 @@
"put": { "put": {
"operationId": "loginAsUser", "operationId": "loginAsUser",
"summary": "Login as this user", "summary": "Login as this user",
"tags": ["Users"], "tags": [
"Users"
],
"security": [ "security": [
{ {
"BearerAuth": ["users"] "BearerAuth": [
"users"
]
} }
], ],
"parameters": [ "parameters": [
@ -934,7 +797,9 @@
"name": "Jamie Curnow", "name": "Jamie Curnow",
"nickname": "James", "nickname": "James",
"avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm",
"roles": ["admin"] "roles": [
"admin"
]
} }
} }
} }
@ -942,7 +807,11 @@
"schema": { "schema": {
"type": "object", "type": "object",
"description": "Login object", "description": "Login object",
"required": ["expires", "token", "user"], "required": [
"expires",
"token",
"user"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"expires": { "expires": {
@ -971,10 +840,14 @@
"get": { "get": {
"operationId": "reportsHosts", "operationId": "reportsHosts",
"summary": "Report on Host Statistics", "summary": "Report on Host Statistics",
"tags": ["Reports"], "tags": [
"Reports"
],
"security": [ "security": [
{ {
"BearerAuth": ["reports"] "BearerAuth": [
"reports"
]
} }
], ],
"responses": { "responses": {
@ -1005,10 +878,14 @@
"get": { "get": {
"operationId": "getAuditLog", "operationId": "getAuditLog",
"summary": "Get Audit Log", "summary": "Get Audit Log",
"tags": ["Audit Log"], "tags": [
"Audit Log"
],
"security": [ "security": [
{ {
"BearerAuth": ["audit-log"] "BearerAuth": [
"audit-log"
]
} }
], ],
"responses": { "responses": {
@ -1048,7 +925,10 @@
"type": "object", "type": "object",
"description": "Health object", "description": "Health object",
"additionalProperties": false, "additionalProperties": false,
"required": ["status", "version"], "required": [
"status",
"version"
],
"properties": { "properties": {
"status": { "status": {
"type": "string", "type": "string",
@ -1064,7 +944,11 @@
"revision": 0 "revision": 0
}, },
"additionalProperties": false, "additionalProperties": false,
"required": ["major", "minor", "revision"], "required": [
"major",
"minor",
"revision"
],
"properties": { "properties": {
"major": { "major": {
"type": "integer", "type": "integer",
@ -1085,7 +969,10 @@
"TokenObject": { "TokenObject": {
"type": "object", "type": "object",
"description": "Token object", "description": "Token object",
"required": ["expires", "token"], "required": [
"expires",
"token"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"expires": { "expires": {
@ -1101,147 +988,16 @@
} }
} }
}, },
"ProxyHostObject": {
"type": "object",
"description": "Proxy Host object",
"required": [
"id",
"created_on",
"modified_on",
"owner_user_id",
"domain_names",
"forward_host",
"forward_port",
"access_list_id",
"certificate_id",
"ssl_forced",
"caching_enabled",
"block_exploits",
"advanced_config",
"meta",
"allow_websocket_upgrade",
"http2_support",
"forward_scheme",
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains",
"certificate",
"use_default_location",
"ipv6"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"description": "Proxy Host ID",
"minimum": 1,
"example": 1
},
"created_on": {
"type": "string",
"description": "Created Date",
"example": "2020-01-30T09:36:08.000Z"
},
"modified_on": {
"type": "string",
"description": "Modified Date",
"example": "2020-01-30T09:41:04.000Z"
},
"owner_user_id": {
"type": "integer",
"minimum": 1,
"example": 1
},
"domain_names": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"forward_host": {
"type": "string",
"minLength": 1
},
"forward_port": {
"type": "integer",
"minimum": 1
},
"access_list_id": {
"type": "integer"
},
"certificate_id": {
"type": "integer"
},
"ssl_forced": {
"type": "integer"
},
"caching_enabled": {
"type": "integer"
},
"block_exploits": {
"type": "integer"
},
"advanced_config": {
"type": "string"
},
"meta": {
"type": "object"
},
"allow_websocket_upgrade": {
"type": "integer"
},
"http2_support": {
"type": "integer"
},
"forward_scheme": {
"type": "string"
},
"enabled": {
"type": "integer"
},
"locations": {
"type": "array"
},
"hsts_enabled": {
"type": "integer"
},
"hsts_subdomains": {
"type": "integer"
},
"certificate": {
"type": "object",
"nullable": true
},
"owner": {
"type": "object",
"nullable": true
},
"access_list": {
"type": "object",
"nullable": true
},
"use_default_location": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
}
}
},
"ProxyHostsList": {
"type": "array",
"description": "Proxyn Hosts list",
"items": {
"$ref": "#/components/schemas/ProxyHostObject"
}
},
"SettingObject": { "SettingObject": {
"type": "object", "type": "object",
"description": "Setting object", "description": "Setting object",
"required": ["id", "name", "description", "value", "meta"], "required": [
"id",
"name",
"description",
"value",
"meta"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"id": { "id": {
@ -1301,7 +1057,17 @@
"UserObject": { "UserObject": {
"type": "object", "type": "object",
"description": "User object", "description": "User object",
"required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"], "required": [
"id",
"created_on",
"modified_on",
"is_disabled",
"email",
"name",
"nickname",
"avatar",
"roles"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"id": { "id": {
@ -1351,7 +1117,9 @@
}, },
"roles": { "roles": {
"description": "Roles applied", "description": "Roles applied",
"example": ["admin"], "example": [
"admin"
],
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
@ -1369,7 +1137,10 @@
"AuthObject": { "AuthObject": {
"type": "object", "type": "object",
"description": "Authentication Object", "description": "Authentication Object",
"required": ["type", "secret"], "required": [
"type",
"secret"
],
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
@ -1396,37 +1167,64 @@
"visibility": { "visibility": {
"type": "string", "type": "string",
"description": "Visibility Type", "description": "Visibility Type",
"enum": ["all", "user"] "enum": [
"all",
"user"
]
}, },
"access_lists": { "access_lists": {
"type": "string", "type": "string",
"description": "Access Lists Permissions", "description": "Access Lists Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
}, },
"dead_hosts": { "dead_hosts": {
"type": "string", "type": "string",
"description": "404 Hosts Permissions", "description": "404 Hosts Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
}, },
"proxy_hosts": { "proxy_hosts": {
"type": "string", "type": "string",
"description": "Proxy Hosts Permissions", "description": "Proxy Hosts Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
}, },
"redirection_hosts": { "redirection_hosts": {
"type": "string", "type": "string",
"description": "Redirection Permissions", "description": "Redirection Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
}, },
"streams": { "streams": {
"type": "string", "type": "string",
"description": "Streams Permissions", "description": "Streams Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
}, },
"certificates": { "certificates": {
"type": "string", "type": "string",
"description": "Certificates Permissions", "description": "Certificates Permissions",
"enum": ["hidden", "view", "manage"] "enum": [
"hidden",
"view",
"manage"
]
} }
} }
}, },

View File

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

View File

@ -3,13 +3,13 @@ const fs = require('fs');
const batchflow = require('batchflow'); const batchflow = require('batchflow');
const logger = require('../logger').access; const logger = require('../logger').access;
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
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 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');
const utils = require('../lib/utils');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -27,13 +27,13 @@ const internalAccessList = {
.then((/*access_data*/) => { .then((/*access_data*/) => {
return accessListModel return accessListModel
.query() .query()
.omit(omissions())
.insertAndFetch({ .insertAndFetch({
name: data.name, name: data.name,
satisfy_any: data.satisfy_any, satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth, pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1) owner_user_id: access.token.getUserId(1)
}) });
.then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
data.id = row.id; data.id = row.id;
@ -118,6 +118,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
@ -217,7 +218,7 @@ 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.[certificate,access_list.[clients,items]]'] expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */); }, true /* <- skip masking */);
}) })
.then((row) => { .then((row) => {
@ -226,7 +227,7 @@ const internalAccessList = {
if (row.proxy_host_count) { if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
} }
}).then(internalNginx.reload) })
.then(() => { .then(() => {
return internalAccessList.maskItems(row); return internalAccessList.maskItems(row);
}); });
@ -255,31 +256,35 @@ 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)
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]') .allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
.omit(['access_list.is_deleted'])
.first(); .first();
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('access_list.owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -376,7 +381,8 @@ 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)
.groupBy('access_list.id') .groupBy('access_list.id')
.allowGraph('[owner,items,clients]') .omit(['access_list.is_deleted'])
.allowEager('[owner,items,clients]')
.orderBy('access_list.name', 'ASC'); .orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -391,10 +397,10 @@ const internalAccessList = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}) })
.then((rows) => { .then((rows) => {
if (rows) { if (rows) {
@ -501,7 +507,7 @@ const internalAccessList = {
if (typeof item.password !== 'undefined' && item.password.length) { if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username); logger.info('Adding: ' + item.username);
utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password]) utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
.then((/*result*/) => { .then((/*result*/) => {
next(); next();
}) })

View File

@ -19,7 +19,7 @@ const internalAuditLog = {
.orderBy('created_on', 'DESC') .orderBy('created_on', 'DESC')
.orderBy('id', 'DESC') .orderBy('id', 'DESC')
.limit(100) .limit(100)
.allowGraph('[user]'); .allowEager('[user]');
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string') {
@ -29,7 +29,7 @@ const internalAuditLog = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query; return query;

View File

@ -1,26 +1,19 @@
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 config = require('../lib/config');
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 tokenModel = require('../models/token');
const dnsPlugins = require('../global/certbot-dns-plugins.json');
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 certbot = require('../lib/certbot'); const certbot_command = '/usr/bin/certbot';
const archiver = require('archiver'); const le_config = '/etc/letsencrypt.ini';
const path = require('path'); const dns_plugins = require('../global/certbot-dns-plugins');
const { isArray } = require('lodash');
const letsencryptStaging = config.useLetsencryptStaging();
const letsencryptConfig = '/etc/letsencrypt.ini';
const certbotCommand = 'certbot';
function omissions() { function omissions() {
return ['is_deleted']; return ['is_deleted'];
@ -28,15 +21,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,
renewBeforeExpirationBy: [30, 'days'],
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();
}, },
@ -45,58 +37,67 @@ 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 expiring within ' + internalCertificate.renewBeforeExpirationBy[0] + ' ' + internalCertificate.renewBeforeExpirationBy[1] + ' ...'); logger.info('Renewing SSL certs close to expiry...');
const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); let cmd = certbot_command + ' renew --non-interactive --quiet ' +
'--config "' + le_config + '" ' +
'--preferred-challenges "dns,http" ' +
'--disable-hook-validation ' +
(le_staging ? '--staging' : '');
// Fetch all the letsencrypt certs from the db that will expire within the configured threshold return utils.exec(cmd)
certificateModel .then((result) => {
.query() if (result) {
.where('is_deleted', 0) logger.info('Renew Result: ' + result);
.andWhere('provider', 'letsencrypt')
.andWhere('expires_on', '<', expirationThreshold)
.then((certificates) => {
if (!certificates || !certificates.length) {
return null;
} }
/** return internalNginx.reload()
* Renews must be run sequentially or we'll get an error 'Another .then(() => {
* instance of Certbot is already running.' logger.info('Renew Complete');
*/ return result;
let sequence = Promise.resolve(); });
certificates.forEach(function (certificate) {
sequence = sequence.then(() =>
internalCertificate
.renew(
{
can: () =>
Promise.resolve({
permission_visibility: 'all',
}),
token: new tokenModel(),
},
{ id: certificate.id },
)
.catch((err) => {
// Don't want to stop the train here, just log the error
logger.error(err.message);
}),
);
});
return sequence;
}) })
.then(() => { .then(() => {
logger.info('Completed SSL cert renew process'); // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times
internalCertificate.intervalProcessing = false; return certificateModel
.query()
.where('is_deleted', 0)
.andWhere('provider', 'letsencrypt')
.then((certificates) => {
if (certificates && certificates.length) {
let promises = [];
certificates.map(function (certificate) {
promises.push(
internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem')
.then((cert_info) => {
return certificateModel
.query()
.where('id', certificate.id)
.andWhere('provider', 'letsencrypt')
.patch({
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
});
})
.catch((err) => {
// Don't want to stop the train here, just log the error
logger.error(err.message);
})
);
});
return Promise.all(promises);
}
});
})
.then(() => {
internalCertificate.interval_processing = false;
}) })
.catch((err) => { .catch((err) => {
logger.error(err); logger.error(err);
internalCertificate.intervalProcessing = false; internalCertificate.interval_processing = false;
}); });
} }
}, },
@ -112,13 +113,13 @@ 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
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((certificate) => { .then((certificate) => {
if (certificate.provider === 'letsencrypt') { if (certificate.provider === 'letsencrypt') {
@ -167,7 +168,6 @@ const internalCertificate = {
// 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);
@ -265,8 +265,8 @@ const internalCertificate = {
return certificateModel return certificateModel
.query() .query()
.omit(omissions())
.patchAndFetchById(row.id, data) .patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()))
.then((saved_row) => { .then((saved_row) => {
saved_row.meta = internalCertificate.cleanMeta(saved_row.meta); saved_row.meta = internalCertificate.cleanMeta(saved_row.meta);
data.meta = internalCertificate.cleanMeta(data.meta); data.meta = internalCertificate.cleanMeta(data.meta);
@ -284,7 +284,7 @@ const internalCertificate = {
meta: _.omit(data, ['expires_on']) // this prevents json circular reference because expires_on might be raw meta: _.omit(data, ['expires_on']) // this prevents json circular reference because expires_on might be raw
}) })
.then(() => { .then(() => {
return saved_row; return _.omit(saved_row, omissions());
}); });
}); });
}); });
@ -309,96 +309,33 @@ const internalCertificate = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner]') .allowEager('[owner]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
/**
* @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
@ -460,7 +397,8 @@ const internalCertificate = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner]') .omit(['is_deleted'])
.allowEager('[owner]')
.orderBy('nice_name', 'ASC'); .orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -470,15 +408,15 @@ 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 + '%');
}); });
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}); });
}, },
@ -510,9 +448,11 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
writeCustomCert: (certificate) => { writeCustomCert: (certificate) => {
logger.info('Writing Custom Certificate:', certificate); if (debug_mode) {
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') {
@ -520,9 +460,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 {
@ -534,7 +474,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 {
@ -584,7 +524,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();
} }
}); });
@ -642,7 +582,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();
} }
}); });
@ -655,12 +595,13 @@ const internalCertificate = {
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
}) })
.then((certificate) => { .then((certificate) => {
console.log('ROWMETA:', row.meta);
certificate.meta = row.meta; certificate.meta = row.meta;
return internalCertificate.writeCustomCert(certificate); return internalCertificate.writeCustomCert(certificate);
}); });
}) })
.then(() => { .then(() => {
return _.pick(row.meta, internalCertificate.allowedSslFiles); return _.pick(row.meta, internalCertificate.allowed_ssl_files);
}); });
}); });
}, },
@ -708,9 +649,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;
@ -726,33 +667,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');
@ -760,39 +701,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);
}); });
@ -806,7 +747,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];
@ -820,26 +761,24 @@ 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 + '" ' +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
'--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" ' +
'--domains "' + certificate.domain_names.join(',') + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
logger.info('Command:', cmd); if (debug_mode) {
logger.info('Command:', cmd);
}
return utils.exec(cmd) return utils.exec(cmd)
.then((result) => { .then((result) => {
@ -849,68 +788,72 @@ const internalCertificate = {
}, },
/** /**
* @param {Object} certificate the certificate row * @param {Object} certificate the certificate row
* @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.json`) * @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 | null} credentials the content of this providers credentials file
* @param {String} propagation_seconds * @param {String} propagation_seconds the cloudflare api token
* @returns {Promise} * @returns {Promise}
*/ */
requestLetsEncryptSslWithDnsChallenge: async (certificate) => { requestLetsEncryptSslWithDnsChallenge: (certificate) => {
await certbot.installPlugin(certificate.meta.dns_provider); const dns_plugin = dns_plugins[certificate.meta.dns_provider];
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
logger.info(`Requesting Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; if (!dns_plugin) {
// Escape single quotes and backslashes throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
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 + '\'';
logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
const credentials_cmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'';
const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version + ' ' + dns_plugin.dependencies;
// Whether the plugin has a --<name>-credentials argument // Whether the plugin has a --<name>-credentials argument
const hasConfigArg = certificate.meta.dns_provider !== 'route53'; const has_config_arg = certificate.meta.dns_provider !== 'route53';
let mainCmd = certbotCommand + ' certonly ' + let main_cmd =
'--config "' + letsencryptConfig + '" ' + certbot_command + ' certonly --non-interactive ' +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
'--cert-name "npm-' + certificate.id + '" ' + '--cert-name "npm-' + certificate.id + '" ' +
'--agree-tos ' + '--agree-tos ' +
'--email "' + certificate.meta.letsencrypt_email + '" ' + '--email "' + certificate.meta.letsencrypt_email + '" ' +
'--domains "' + certificate.domain_names.join(',') + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' +
'--authenticator ' + dnsPlugin.full_plugin_name + ' ' + '--authenticator ' + dns_plugin.full_plugin_name + ' ' +
( (
hasConfigArg has_config_arg
? '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentials_loc + '"'
: '' : ''
) + ) +
( (
certificate.meta.propagation_seconds !== undefined certificate.meta.propagation_seconds !== undefined
? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
: '' : ''
) + ) +
(letsencryptStaging ? ' --staging' : ''); (le_staging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable // Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') { if (certificate.meta.dns_provider === 'route53') {
mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
} }
if (certificate.meta.dns_provider === 'duckdns') { if (debug_mode) {
mainCmd = mainCmd + ' --dns-duckdns-no-txt-restore'; logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd}`);
} }
logger.info('Command:', `${credentialsCmd} && && ${mainCmd}`); return utils.exec(credentials_cmd)
.then(() => {
try { return utils.exec(prepare_cmd)
await utils.exec(credentialsCmd); .then(() => {
const result = await utils.exec(mainCmd); return utils.exec(main_cmd)
logger.info(result); .then(async (result) => {
return result; logger.info(result);
} catch (err) { return result;
// Don't fail if file does not exist });
const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`; });
await utils.exec(delete_credentialsCmd); }).catch(async (err) => {
throw err; // Don't fail if file does not exist
} const delete_credentials_cmd = `rm -f '${credentials_loc}' || true`;
await utils.exec(delete_credentials_cmd);
throw err;
});
}, },
@ -927,7 +870,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; let renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl;
return renewMethod(certificate) return renewMethod(certificate)
.then(() => { .then(() => {
@ -965,17 +908,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 + '" ' +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
'--cert-name "npm-' + certificate.id + '" ' + '--cert-name "npm-' + certificate.id + '" ' +
'--preferred-challenges "dns,http" ' + '--preferred-challenges "dns,http" ' +
'--no-random-sleep-on-renew ' +
'--disable-hook-validation ' + '--disable-hook-validation ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
logger.info('Command:', cmd); if (debug_mode) {
logger.info('Command:', cmd);
}
return utils.exec(cmd) return utils.exec(cmd)
.then((result) => { .then((result) => {
@ -989,32 +931,31 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
renewLetsEncryptSslWithDnsChallenge: (certificate) => { renewLetsEncryptSslWithDnsChallenge: (certificate) => {
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; const dns_plugin = dns_plugins[certificate.meta.dns_provider];
if (!dnsPlugin) { if (!dns_plugin) {
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
} }
logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
let mainCmd = certbotCommand + ' renew --force-renewal ' + let main_cmd =
'--config "' + letsencryptConfig + '" ' + certbot_command + ' renew --non-interactive ' +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
'--cert-name "npm-' + certificate.id + '" ' + '--cert-name "npm-' + certificate.id + '" ' +
'--disable-hook-validation ' + '--disable-hook-validation' +
'--no-random-sleep-on-renew ' + (le_staging ? ' --staging' : '');
(letsencryptStaging ? ' --staging' : '');
// Prepend the path to the credentials file as an environment variable // Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') { if (certificate.meta.dns_provider === 'route53') {
const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
} }
logger.info('Command:', mainCmd); if (debug_mode) {
logger.info('Command:', main_cmd);
}
return utils.exec(mainCmd) return utils.exec(main_cmd)
.then(async (result) => { .then(async (result) => {
logger.info(result); logger.info(result);
return result; return result;
@ -1029,27 +970,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 ' + const main_cmd = certbot_command + ' revoke --non-interactive ' +
'--config "' + letsencryptConfig + '" ' +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
'--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' +
'--delete-after-revoke ' + '--delete-after-revoke ' +
(letsencryptStaging ? '--staging' : ''); (le_staging ? '--staging' : '');
// Don't fail command if file does not exist // Don't fail command if file does not exist
const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; const delete_credentials_cmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); if (debug_mode) {
logger.info('Command:', main_cmd + '; ' + delete_credentials_cmd);
}
return utils.exec(mainCmd) return utils.exec(main_cmd)
.then(async (result) => { .then(async (result) => {
await utils.exec(delete_credentialsCmd); await utils.exec(delete_credentials_cmd);
logger.info(result); logger.info(result);
return result; return result;
}) })
.catch((err) => { .catch((err) => {
logger.error(err.message); if (debug_mode) {
logger.error(err.message);
}
if (throw_errors) { if (throw_errors) {
throw err; throw err;
@ -1062,9 +1004,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');
}, },
/** /**
@ -1125,108 +1067,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: {
'User-Agent': 'Mozilla/5.0',
'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 () {
try {
const parsedBody = JSON.parse(responseBody + '');
if (res.statusCode !== 200) {
logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`);
resolve(undefined);
} else {
resolve(parsedBody);
}
} catch (err) {
if (res.statusCode !== 200) {
logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`);
} else {
logger.warn(`Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`);
}
resolve(undefined);
}
});
});
// 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.error) {
logger.info(`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`);
return `other:${result.error.msg}`;
} 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

@ -1,6 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const deadHostModel = require('../models/dead_host'); const deadHostModel = require('../models/dead_host');
const internalHost = require('./host'); const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
@ -50,8 +49,8 @@ const internalDeadHost = {
return deadHostModel return deadHostModel
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((row) => { .then((row) => {
if (create_certificate) { if (create_certificate) {
@ -219,28 +218,31 @@ const internalDeadHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner,certificate]') .allowEager('[owner,certificate]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -402,7 +404,8 @@ const internalDeadHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,certificate]') .omit(['is_deleted'])
.allowEager('[owner,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy('domain_names', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -417,10 +420,10 @@ const internalDeadHost = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}) })
.then((rows) => { .then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {

View File

@ -2,16 +2,13 @@ const https = require('https');
const fs = require('fs'); const fs = require('fs');
const logger = require('../logger').ip_ranges; const logger = require('../logger').ip_ranges;
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const { Liquid } = require('liquidjs');
const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json'; 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(() => {
@ -119,7 +116,10 @@ const internalIpRanges = {
* @returns {Promise} * @returns {Promise}
*/ */
generateConfig: (ip_ranges) => { generateConfig: (ip_ranges) => {
const renderEngine = utils.getRenderEngine(); let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
let filename = '/etc/nginx/conf.d/include/ip_ranges.conf'; let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';

View File

@ -1,9 +1,10 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const logger = require('../logger').nginx; const logger = require('../logger').nginx;
const config = require('../lib/config'); const utils = require('../lib/utils');
const utils = require('../lib/utils'); const error = require('../lib/error');
const error = require('../lib/error'); const { Liquid } = require('liquidjs');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
const internalNginx = { const internalNginx = {
@ -28,9 +29,7 @@ const internalNginx = {
.then(() => { .then(() => {
// Nginx is OK // Nginx is OK
// We're deleting this config regardless. // We're deleting this config regardless.
// Don't throw errors, as the file may not exist at all return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all
// Delete the .err file too
return internalNginx.deleteConfig(host_type, host, false, true);
}) })
.then(() => { .then(() => {
return internalNginx.generateConfig(host_type, host); return internalNginx.generateConfig(host_type, host);
@ -65,7 +64,7 @@ const internalNginx = {
} }
}); });
if (config.debug()) { if (debug_mode) {
logger.error('Nginx test failed:', valid_lines.join('\n')); logger.error('Nginx test failed:', valid_lines.join('\n'));
} }
@ -81,9 +80,6 @@ const internalNginx = {
.patch({ .patch({
meta: combined_meta meta: combined_meta
}) })
.then(() => {
internalNginx.renameConfigAsError(host_type, host);
})
.then(() => { .then(() => {
return internalNginx.deleteConfig(host_type, host, true); return internalNginx.deleteConfig(host_type, host, true);
}); });
@ -101,7 +97,7 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
test: () => { test: () => {
if (config.debug()) { if (debug_mode) {
logger.info('Testing Nginx configuration'); logger.info('Testing Nginx configuration');
} }
@ -125,10 +121,13 @@ const internalNginx = {
* @returns {String} * @returns {String}
*/ */
getConfigName: (host_type, host_id) => { getConfigName: (host_type, host_id) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
if (host_type === 'default') { if (host_type === 'default') {
return '/data/nginx/default_host/site.conf'; return '/data/nginx/default_host/site.conf';
} }
return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';
return '/data/nginx/' + host_type + '/' + host_id + '.conf';
}, },
/** /**
@ -147,16 +146,12 @@ const internalNginx = {
return; return;
} }
const renderEngine = utils.getRenderEngine(); let renderer = new Liquid();
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('/');
@ -166,13 +161,11 @@ const internalNginx = {
} }
// eslint-disable-next-line // eslint-disable-next-line
renderedLocations += await renderEngine.parseAndRender(template, locationCopy); renderedLocations += await renderer.parseAndRender(template, locationCopy);
} }
}; };
locationRendering().then(() => resolve(renderedLocations)); locationRendering().then(() => resolve(renderedLocations));
}); });
}, },
@ -182,20 +175,22 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
generateConfig: (host_type, host) => { generateConfig: (host_type, host) => {
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); host_type = host_type.replace(new RegExp('-', 'g'), '_');
if (config.debug()) { if (debug_mode) {
logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2)); logger.info('Generating ' + host_type + ' Config:', host);
} }
const renderEngine = utils.getRenderEngine(); let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
let filename = internalNginx.getConfigName(nice_host_type, host.id); let filename = internalNginx.getConfigName(host_type, host.id);
try { try {
template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new error.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
@ -205,7 +200,7 @@ const internalNginx = {
let origLocations; let origLocations;
// Manipulate the data a bit before sending it to the template // Manipulate the data a bit before sending it to the template
if (nice_host_type !== 'default') { if (host_type !== 'default') {
host.use_default_location = true; host.use_default_location = true;
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
@ -213,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,7 +233,7 @@ const internalNginx = {
.then((config_text) => { .then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (config.debug()) { if (debug_mode) {
logger.success('Wrote config:', filename, config_text); logger.success('Wrote config:', filename, config_text);
} }
@ -249,7 +243,7 @@ const internalNginx = {
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
if (config.debug()) { if (debug_mode) {
logger.warn('Could not write ' + filename + ':', err.message); logger.warn('Could not write ' + filename + ':', err.message);
} }
@ -268,11 +262,13 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
generateLetsEncryptRequestConfig: (certificate) => { generateLetsEncryptRequestConfig: (certificate) => {
if (config.debug()) { if (debug_mode) {
logger.info('Generating LetsEncrypt Request Config:', certificate); logger.info('Generating LetsEncrypt Request Config:', certificate);
} }
const renderEngine = utils.getRenderEngine(); let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
@ -292,14 +288,14 @@ const internalNginx = {
.then((config_text) => { .then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (config.debug()) { if (debug_mode) {
logger.success('Wrote config:', filename, config_text); logger.success('Wrote config:', filename, config_text);
} }
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
if (config.debug()) { if (debug_mode) {
logger.warn('Could not write ' + filename + ':', err.message); logger.warn('Could not write ' + filename + ':', err.message);
} }
@ -308,58 +304,33 @@ const internalNginx = {
}); });
}, },
/**
* A simple wrapper around unlinkSync that writes to the logger
*
* @param {String} filename
*/
deleteFile: (filename) => {
logger.debug('Deleting file: ' + filename);
try {
fs.unlinkSync(filename);
} catch (err) {
logger.debug('Could not delete file:', JSON.stringify(err, null, 2));
}
},
/**
*
* @param {String} host_type
* @returns String
*/
getFileFriendlyHostType: (host_type) => {
return host_type.replace(new RegExp('-', 'g'), '_');
},
/** /**
* This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`
* *
* @param {Object} certificate * @param {Object} certificate
* @param {Boolean} [throw_errors]
* @returns {Promise} * @returns {Promise}
*/ */
deleteLetsEncryptRequestConfig: (certificate) => { deleteLetsEncryptRequestConfig: (certificate, throw_errors) => {
const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; return new Promise((resolve, reject) => {
return new Promise((resolve/*, reject*/) => { try {
internalNginx.deleteFile(config_file); let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
resolve();
});
},
/** if (debug_mode) {
* @param {String} host_type logger.warn('Deleting nginx config: ' + config_file);
* @param {Object} [host] }
* @param {Boolean} [delete_err_file]
* @returns {Promise}
*/
deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
const config_file_err = config_file + '.err';
return new Promise((resolve/*, reject*/) => { fs.unlinkSync(config_file);
internalNginx.deleteFile(config_file); } catch (err) {
if (delete_err_file) { if (debug_mode) {
internalNginx.deleteFile(config_file_err); logger.warn('Could not delete config:', err.message);
}
if (throw_errors) {
reject(err);
}
} }
resolve(); resolve();
}); });
}, },
@ -367,20 +338,32 @@ const internalNginx = {
/** /**
* @param {String} host_type * @param {String} host_type
* @param {Object} [host] * @param {Object} [host]
* @param {Boolean} [throw_errors]
* @returns {Promise} * @returns {Promise}
*/ */
renameConfigAsError: (host_type, host) => { deleteConfig: (host_type, host, throw_errors) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); host_type = host_type.replace(new RegExp('-', 'g'), '_');
const config_file_err = config_file + '.err';
return new Promise((resolve/*, reject*/) => { return new Promise((resolve, reject) => {
fs.unlink(config_file, () => { try {
// ignore result, continue let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
fs.rename(config_file, config_file_err, () => {
// also ignore result, as this is a debugging informative file anyway if (debug_mode) {
resolve(); logger.warn('Deleting nginx config: ' + config_file);
}); }
});
fs.unlinkSync(config_file);
} catch (err) {
if (debug_mode) {
logger.warn('Could not delete config:', err.message);
}
if (throw_errors) {
reject(err);
}
}
resolve();
}); });
}, },
@ -401,12 +384,13 @@ const internalNginx = {
/** /**
* @param {String} host_type * @param {String} host_type
* @param {Array} hosts * @param {Array} hosts
* @param {Boolean} [throw_errors]
* @returns {Promise} * @returns {Promise}
*/ */
bulkDeleteConfigs: (host_type, hosts) => { bulkDeleteConfigs: (host_type, hosts, throw_errors) => {
let promises = []; let promises = [];
hosts.map(function (host) { hosts.map(function (host) {
promises.push(internalNginx.deleteConfig(host_type, host, true)); promises.push(internalNginx.deleteConfig(host_type, host, throw_errors));
}); });
return Promise.all(promises); return Promise.all(promises);
@ -416,8 +400,8 @@ const internalNginx = {
* @param {string} config * @param {string} config
* @returns {boolean} * @returns {boolean}
*/ */
advancedConfigHasDefaultLocation: function (cfg) { advancedConfigHasDefaultLocation: function (config) {
return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
}, },
/** /**

View File

@ -1,6 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const proxyHostModel = require('../models/proxy_host'); const proxyHostModel = require('../models/proxy_host');
const internalHost = require('./host'); const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
@ -50,8 +49,8 @@ const internalProxyHost = {
return proxyHostModel return proxyHostModel
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((row) => { .then((row) => {
if (create_certificate) { if (create_certificate) {
@ -171,7 +170,6 @@ const internalProxyHost = {
.query() .query()
.where({id: data.id}) .where({id: data.id})
.patch(data) .patch(data)
.then(utils.omitRow(omissions()))
.then((saved_row) => { .then((saved_row) => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
@ -181,7 +179,7 @@ const internalProxyHost = {
meta: data meta: data
}) })
.then(() => { .then(() => {
return saved_row; return _.omit(saved_row, omissions());
}); });
}); });
}) })
@ -225,29 +223,31 @@ const internalProxyHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner,access_list.[clients,items],certificate]') .allowEager('[owner,access_list,access_list.[clients,items],certificate]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -409,7 +409,8 @@ const internalProxyHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,access_list,certificate]') .omit(['is_deleted'])
.allowEager('[owner,access_list,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy('domain_names', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -424,10 +425,10 @@ const internalProxyHost = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}) })
.then((rows) => { .then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {

View File

@ -1,6 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const redirectionHostModel = require('../models/redirection_host'); const redirectionHostModel = require('../models/redirection_host');
const internalHost = require('./host'); const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
@ -50,8 +49,8 @@ const internalRedirectionHost = {
return redirectionHostModel return redirectionHostModel
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((row) => { .then((row) => {
if (create_certificate) { if (create_certificate) {
@ -66,8 +65,9 @@ const internalRedirectionHost = {
.then(() => { .then(() => {
return row; return row;
}); });
} else {
return row;
} }
return row;
}) })
.then((row) => { .then((row) => {
// re-fetch with cert // re-fetch with cert
@ -218,29 +218,31 @@ const internalRedirectionHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner,certificate]') .allowEager('[owner,certificate]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -402,7 +404,8 @@ const internalRedirectionHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,certificate]') .omit(['is_deleted'])
.allowEager('[owner,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy('domain_names', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -417,10 +420,10 @@ const internalRedirectionHost = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}) })
.then((rows) => { .then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {

View File

@ -1,6 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const streamModel = require('../models/stream'); const streamModel = require('../models/stream');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
@ -28,8 +27,8 @@ const internalStream = {
return streamModel return streamModel
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
@ -72,8 +71,8 @@ const internalStream = {
return streamModel return streamModel
.query() .query()
.omit(omissions())
.patchAndFetchById(row.id, data) .patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()))
.then((saved_row) => { .then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row) return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => { .then(() => {
@ -89,7 +88,7 @@ const internalStream = {
meta: data meta: data
}) })
.then(() => { .then(() => {
return saved_row; return _.omit(saved_row, omissions());
}); });
}); });
}); });
@ -114,28 +113,30 @@ const internalStream = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[owner]') .allowEager('[owner]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -297,7 +298,8 @@ const internalStream = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner]') .omit(['is_deleted'])
.allowEager('[owner]')
.orderBy('incoming_port', 'ASC'); .orderBy('incoming_port', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -312,10 +314,10 @@ const internalStream = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}); });
}, },

View File

@ -24,7 +24,7 @@ module.exports = {
return userModel return userModel
.query() .query()
.where('email', data.identity.toLowerCase().trim()) .where('email', data.identity)
.andWhere('is_deleted', 0) .andWhere('is_deleted', 0)
.andWhere('is_disabled', 0) .andWhere('is_disabled', 0)
.first() .first()

View File

@ -1,6 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const utils = require('../lib/utils');
const userModel = require('../models/user'); const userModel = require('../models/user');
const userPermissionModel = require('../models/user_permission'); const userPermissionModel = require('../models/user_permission');
const authModel = require('../models/auth'); const authModel = require('../models/auth');
@ -36,8 +35,8 @@ const internalUser = {
return userModel return userModel
.query() .query()
.insertAndFetch(data) .omit(omissions())
.then(utils.omitRow(omissions())); .insertAndFetch(data);
}) })
.then((user) => { .then((user) => {
if (auth) { if (auth) {
@ -141,8 +140,11 @@ const internalUser = {
return userModel return userModel
.query() .query()
.omit(omissions())
.patchAndFetchById(user.id, data) .patchAndFetchById(user.id, data)
.then(utils.omitRow(omissions())); .then((saved_user) => {
return _.omit(saved_user, omissions());
});
}) })
.then(() => { .then(() => {
return internalUser.get(access, {id: data.id}); return internalUser.get(access, {id: data.id});
@ -184,24 +186,26 @@ const internalUser = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowGraph('[permissions]') .allowEager('[permissions]')
.first(); .first();
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions // Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit); query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
} }
return row;
}); });
}, },
@ -318,7 +322,8 @@ const internalUser = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[permissions]') .omit(['is_deleted'])
.allowEager('[permissions]')
.orderBy('name', 'ASC'); .orderBy('name', 'ASC');
// Query is used for searching // Query is used for searching
@ -330,10 +335,10 @@ const internalUser = {
} }
if (typeof expand !== 'undefined' && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']'); query.eager('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query;
}); });
}, },

View File

@ -55,8 +55,8 @@ module.exports = function (token_string) {
.where('id', token_data.attrs.id) .where('id', token_data.attrs.id)
.andWhere('is_deleted', 0) .andWhere('is_deleted', 0)
.andWhere('is_disabled', 0) .andWhere('is_disabled', 0)
.allowGraph('[permissions]') .allowEager('[permissions]')
.withGraphFetched('[permissions]') .eager('[permissions]')
.first() .first()
.then((user) => { .then((user) => {
if (user) { if (user) {

View File

@ -1,78 +0,0 @@
const dnsPlugins = require('../global/certbot-dns-plugins.json');
const utils = require('./utils');
const error = require('./error');
const logger = require('../logger').certbot;
const batchflow = require('batchflow');
const CERTBOT_VERSION_REPLACEMENT = '$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')';
const certbot = {
/**
* @param {array} pluginKeys
*/
installPlugins: async function (pluginKeys) {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys).sequential()
.each((i, pluginKey, next) => {
certbot.installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(new error.CommandError('Some plugins failed to install. Please check the logs above', 1));
} else {
resolve();
}
});
});
},
/**
* Installs a cerbot plugin given the key for the object from
* ../global/certbot-dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
installPlugin: async function (pluginKey) {
if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.ItemNotFoundError(pluginKey);
}
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
return utils.exec(cmd)
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
},
};
module.exports = certbot;

View File

@ -1,184 +0,0 @@
const fs = require('fs');
const NodeRSA = require('node-rsa');
const logger = require('../logger').global;
const keysFile = '/data/keys.json';
let instance = null;
// 1. Load from config file first (not recommended anymore)
// 2. Use config env variables next
const configure = () => {
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
if (fs.existsSync(filename)) {
let configData;
try {
configData = require(filename);
} catch (err) {
// do nothing
}
if (configData && configData.database) {
logger.info(`Using configuration from file: ${filename}`);
instance = configData;
instance.keys = getKeys();
return;
}
}
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info('Using MySQL configuration');
instance = {
database: {
engine: 'mysql',
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
},
keys: getKeys(),
};
return;
}
const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite';
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
engine: 'knex-native',
knex: {
client: 'sqlite3',
connection: {
filename: envSqliteFile
},
useNullAsDefault: true
}
},
keys: getKeys(),
};
};
const getKeys = () => {
// Get keys from file
if (!fs.existsSync(keysFile)) {
generateKeys();
} else if (process.env.DEBUG) {
logger.info('Keys file exists OK');
}
try {
return require(keysFile);
} catch (err) {
logger.error('Could not read JWT key pair from config file: ' + keysFile, err);
process.exit(1);
}
};
const generateKeys = () => {
logger.info('Creating a new JWT key pair...');
// Now create the keys and save them in the config.
const key = new NodeRSA({ b: 2048 });
key.generateKeyPair();
const keys = {
key: key.exportKey('private').toString(),
pub: key.exportKey('public').toString(),
};
// Write keys config
try {
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
} catch (err) {
logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' . err.message);
process.exit(1);
}
logger.info('Wrote JWT key pair to config file: ' + keysFile);
};
module.exports = {
/**
*
* @param {string} key ie: 'database' or 'database.engine'
* @returns {boolean}
*/
has: function(key) {
instance === null && configure();
const keys = key.split('.');
let level = instance;
let has = true;
keys.forEach((keyItem) =>{
if (typeof level[keyItem] === 'undefined') {
has = false;
} else {
level = level[keyItem];
}
});
return has;
},
/**
* Gets a specific key from the top level
*
* @param {string} key
* @returns {*}
*/
get: function (key) {
instance === null && configure();
if (key && typeof instance[key] !== 'undefined') {
return instance[key];
}
return instance;
},
/**
* Is this a sqlite configuration?
*
* @returns {boolean}
*/
isSqlite: function () {
instance === null && configure();
return instance.database.knex && instance.database.knex.client === 'sqlite3';
},
/**
* Are we running in debug mdoe?
*
* @returns {boolean}
*/
debug: function () {
return !!process.env.DEBUG;
},
/**
* Returns a public key
*
* @returns {string}
*/
getPublicKey: function () {
instance === null && configure();
return instance.keys.pub;
},
/**
* Returns a private key
*
* @returns {string}
*/
getPrivateKey: function () {
instance === null && configure();
return instance.keys.key;
},
/**
* @returns {boolean}
*/
useLetsencryptStaging: function () {
return !!process.env.LE_STAGING;
}
};

View File

@ -82,16 +82,7 @@ module.exports = {
this.message = message; this.message = message;
this.public = false; this.public = false;
this.status = 400; this.status = 400;
}, }
CommandError: function (stdErr, code, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = stdErr;
this.code = code;
this.public = false;
},
}; };
_.forEach(module.exports, function (error) { _.forEach(module.exports, function (error) {

View File

@ -1,41 +1,14 @@
const _ = require('lodash'); const exec = require('child_process').exec;
const exec = require('child_process').exec;
const execFile = require('child_process').execFile;
const { Liquid } = require('liquidjs');
const logger = require('../logger').global;
const error = require('./error');
module.exports = { module.exports = {
exec: async function(cmd, options = {}) {
logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = exec(cmd, options, (isError, stdout, stderr) => {
if (isError) {
reject(new error.CommandError(stderr, isError));
} else {
resolve({ stdout, stderr });
}
});
child.on('error', (e) => {
reject(new error.CommandError(stderr, 1, e));
});
});
return stdout;
},
/** /**
* @param {String} cmd * @param {String} cmd
* @param {Array} args
* @returns {Promise} * @returns {Promise}
*/ */
execFile: function (cmd, args) { exec: function (cmd) {
// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile(cmd, args, function (err, stdout, /*stderr*/) { exec(cmd, function (err, stdout, /*stderr*/) {
if (err && typeof err === 'object') { if (err && typeof err === 'object') {
reject(err); reject(err);
} else { } else {
@ -43,64 +16,5 @@ module.exports = {
} }
}); });
}); });
},
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
omitRow: function (omissions) {
/**
* @param {Object} row
* @returns {Object}
*/
return (row) => {
return _.omit(row, omissions);
};
},
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
omitRows: function (omissions) {
/**
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
});
return rows;
};
},
/**
* @returns {Object} Liquid render engine
*/
getRenderEngine: function () {
const renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
*/
renderEngine.registerFilter('nginxAccessRule', (v) => {
if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) {
return `${v.directive} ${v.address};`;
}
return '';
});
return renderEngine;
} }
}; };

View File

@ -5,7 +5,7 @@ const definitions = require('../../schema/definitions.json');
RegExp.prototype.toJSON = RegExp.prototype.toString; RegExp.prototype.toJSON = RegExp.prototype.toString;
const ajv = require('ajv')({ const ajv = require('ajv')({
verbose: true, verbose: true, //process.env.NODE_ENV === 'development',
allErrors: true, allErrors: true,
format: 'full', // strict regexes for format checks format: 'full', // strict regexes for format checks
coerceTypes: true, coerceTypes: true,

View File

@ -7,7 +7,6 @@ module.exports = {
access: new Signale({scope: 'Access '}), access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}), nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '}), ssl: new Signale({scope: 'SSL '}),
certbot: new Signale({scope: 'Certbot '}),
import: new Signale({scope: 'Importer '}), import: new Signale({scope: 'Importer '}),
setup: new Signale({scope: 'Setup '}), setup: new Signale({scope: 'Setup '}),
ip_ranges: new Signale({scope: 'IP Ranges'}) ip_ranges: new Signale({scope: 'IP Ranges'})

View File

@ -69,6 +69,9 @@ exports.up = function (knex/*, Promise*/) {
table.json('domain_names').notNull(); table.json('domain_names').notNull();
table.string('forward_ip').notNull(); table.string('forward_ip').notNull();
table.integer('forward_port').notNull().unsigned(); table.integer('forward_port').notNull().unsigned();
table.string('root_dir').notNull();
table.string('index_file').notNull();
table.integer('static').notNull().unsigned().defaultTo(0);
table.integer('access_list_id').notNull().unsigned().defaultTo(0); table.integer('access_list_id').notNull().unsigned().defaultTo(0);
table.integer('certificate_id').notNull().unsigned().defaultTo(0); table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0);

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

@ -50,6 +50,7 @@ class AccessList extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
}, },
items: { items: {
@ -58,6 +59,9 @@ class AccessList extends Model {
join: { join: {
from: 'access_list.id', from: 'access_list.id',
to: 'access_list_auth.access_list_id' to: 'access_list_auth.access_list_id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
} }
}, },
clients: { clients: {
@ -66,6 +70,9 @@ class AccessList extends Model {
join: { join: {
from: 'access_list.id', from: 'access_list.id',
to: 'access_list_client.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: {
@ -77,10 +84,19 @@ class AccessList extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('proxy_host.is_deleted', 0); qb.where('proxy_host.is_deleted', 0);
qb.omit(['is_deleted', 'meta']);
} }
} }
}; };
} }
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

@ -45,6 +45,7 @@ class AccessListAuth extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('access_list.is_deleted', 0); qb.where('access_list.is_deleted', 0);
qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']);
} }
} }
}; };

View File

@ -45,10 +45,15 @@ class AccessListClient extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('access_list.is_deleted', 0); 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; module.exports = AccessListClient;

View File

@ -43,6 +43,9 @@ class AuditLog extends Model {
join: { join: {
from: 'audit_log.user_id', from: 'audit_log.user_id',
to: 'user.id' to: 'user.id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'roles']);
} }
} }
}; };

View File

@ -74,6 +74,9 @@ class Auth extends Model {
}, },
filter: { filter: {
is_deleted: 0 is_deleted: 0
},
modify: function (qb) {
qb.omit(['is_deleted']);
} }
} }
}; };

View File

@ -63,6 +63,7 @@ class Certificate extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
} }
}; };

View File

@ -59,6 +59,7 @@ class DeadHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
}, },
certificate: { certificate: {
@ -70,6 +71,7 @@ class DeadHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('certificate.is_deleted', 0); qb.where('certificate.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
} }
} }
}; };

View File

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

View File

@ -60,6 +60,7 @@ class ProxyHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
}, },
access_list: { access_list: {
@ -71,6 +72,7 @@ class ProxyHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('access_list.is_deleted', 0); qb.where('access_list.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
} }
}, },
certificate: { certificate: {
@ -82,6 +84,7 @@ class ProxyHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('certificate.is_deleted', 0); qb.where('certificate.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
} }
} }
}; };

View File

@ -1,4 +1,3 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
@ -60,6 +59,7 @@ class RedirectionHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
}, },
certificate: { certificate: {
@ -71,6 +71,7 @@ class RedirectionHost extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('certificate.is_deleted', 0); qb.where('certificate.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
} }
} }
}; };

View File

@ -46,6 +46,7 @@ class Stream extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('user.is_deleted', 0); qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
} }
} }
}; };

View File

@ -6,36 +6,44 @@
const _ = require('lodash'); const _ = require('lodash');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const crypto = require('crypto'); const crypto = require('crypto');
const config = require('../lib/config');
const error = require('../lib/error'); const error = require('../lib/error');
const logger = require('../logger').global;
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 () {
let token_data = {}; let token_data = {};
const self = { let self = {
/** /**
* @param {Object} payload * @param {Object} payload
* @returns {Promise} * @returns {Promise}
*/ */
create: (payload) => { create: (payload) => {
if (!config.getPrivateKey()) {
logger.error('Private key is empty!');
}
// sign with RSA SHA256 // sign with RSA SHA256
const options = { let options = {
algorithm: ALGO, algorithm: ALGO,
expiresIn: payload.expiresIn || '1d' expiresIn: payload.expiresIn || '1d'
}; };
payload.jti = crypto.randomBytes(12) payload.jti = crypto.randomBytes(12)
.toString('base64') .toString('base64')
.substring(-8); .substr(-8);
checkJWTKeyPair();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jwt.sign(payload, config.getPrivateKey(), options, (err, token) => { jwt.sign(payload, private_key, options, (err, token) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@ -54,15 +62,13 @@ module.exports = function () {
* @returns {Promise} * @returns {Promise}
*/ */
load: function (token) { load: function (token) {
if (!config.getPublicKey()) {
logger.error('Public key is empty!');
}
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'));
} else { } else {
jwt.verify(token, config.getPublicKey(), {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => { jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => {
if (err) { if (err) {
if (err.name === 'TokenExpiredError') { if (err.name === 'TokenExpiredError') {
@ -77,6 +83,8 @@ module.exports = function () {
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'. // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user. // For 30 days at least, we need to replace 'all' with user.
if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) { if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) {
//console.log('Warning! Replacing "all" scope with "user"');
token_data.scope = ['user']; token_data.scope = ['user'];
} }
@ -126,7 +134,7 @@ module.exports = function () {
* @returns {Integer} * @returns {Integer}
*/ */
getUserId: (default_value) => { getUserId: (default_value) => {
const attrs = self.get('attrs'); let attrs = self.get('attrs');
if (attrs && typeof attrs.id !== 'undefined' && attrs.id) { if (attrs && typeof attrs.id !== 'undefined' && attrs.id) {
return attrs.id; return attrs.id;
} }

View File

@ -43,6 +43,9 @@ class User extends Model {
join: { join: {
from: 'user.id', from: 'user.id',
to: 'user_permission.user_id' to: 'user_permission.user_id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'user_id']);
} }
} }
}; };

View File

@ -5,27 +5,33 @@
"main": "js/index.js", "main": "js/index.js",
"dependencies": { "dependencies": {
"ajv": "^6.12.0", "ajv": "^6.12.0",
"archiver": "^5.3.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.19.2", "config": "^3.3.1",
"diskdb": "^0.1.17",
"express": "^4.17.1",
"express-fileupload": "^1.1.9", "express-fileupload": "^1.1.9",
"gravatar": "^1.8.0", "gravatar": "^1.8.0",
"html-entities": "^1.2.1",
"json-schema-ref-parser": "^8.0.0", "json-schema-ref-parser": "^8.0.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^8.5.1",
"knex": "2.4.2", "knex": "^0.20.13",
"liquidjs": "10.6.1", "liquidjs": "^9.11.10",
"lodash": "^4.17.21", "lodash": "^4.17.19",
"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.8",
"objection": "3.0.1", "nodemon": "^2.0.2",
"objection": "^2.1.3",
"path": "^0.12.7", "path": "^0.12.7",
"signale": "1.4.0", "pg": "^7.12.1",
"sqlite3": "5.1.6", "restler": "^3.4.0",
"temp-write": "^4.0.0" "signale": "^1.4.0",
"sqlite3": "^4.1.1",
"temp-write": "^4.0.0",
"unix-timestamp": "^0.2.0"
}, },
"signale": { "signale": {
"displayDate": true, "displayDate": true,
@ -34,9 +40,8 @@
"author": "Jamie Curnow <jc@jc21.com>", "author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"eslint": "^8.36.0", "eslint": "^6.8.0",
"eslint-plugin-align-assignments": "^1.1.2", "eslint-plugin-align-assignments": "^1.1.2",
"nodemon": "^2.0.2",
"prettier": "^2.0.4" "prettier": "^2.0.4"
} }
} }

View File

@ -68,32 +68,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
* *
@ -235,34 +209,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": {
@ -172,7 +172,7 @@
"description": "Domain Names separated by a comma", "description": "Domain Names separated by a comma",
"example": "*.jc21.com,blog.jc21.com", "example": "*.jc21.com,blog.jc21.com",
"type": "array", "type": "array",
"maxItems": 100, "maxItems": 15,
"uniqueItems": true, "uniqueItems": true,
"items": { "items": {
"type": "string", "type": "string",
@ -235,6 +235,11 @@
"description": "Should we cache assets", "description": "Should we cache assets",
"example": true, "example": true,
"type": "boolean" "type": "boolean"
},
"static": {
"description": "Should the proxy point to static files",
"example": true,
"type": "boolean"
} }
} }
} }

View File

@ -157,17 +157,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

@ -24,14 +24,22 @@
}, },
"forward_host": { "forward_host": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 0,
"maxLength": 255 "maxLength": 255
}, },
"forward_port": { "forward_port": {
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 0,
"maximum": 65535 "maximum": 65535
}, },
"root_dir": {
"type": "string",
"minLength": 0,
},
"index_file": {
"type": "string",
"minLength": 0,
},
"certificate_id": { "certificate_id": {
"$ref": "../definitions.json#/definitions/certificate_id" "$ref": "../definitions.json#/definitions/certificate_id"
}, },
@ -53,6 +61,9 @@
"caching_enabled": { "caching_enabled": {
"$ref": "../definitions.json#/definitions/caching_enabled" "$ref": "../definitions.json#/definitions/caching_enabled"
}, },
"static": {
"$ref": "../definitions.json#/definitions/static"
},
"allow_websocket_upgrade": { "allow_websocket_upgrade": {
"description": "Allow Websocket Upgrade for all paths", "description": "Allow Websocket Upgrade for all paths",
"example": true, "example": true,
@ -76,10 +87,7 @@
"items": { "items": {
"type": "object", "type": "object",
"required": [ "required": [
"forward_scheme", "forward_scheme"
"forward_host",
"forward_port",
"path"
], ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -99,6 +107,15 @@
"forward_port": { "forward_port": {
"$ref": "#/definitions/forward_port" "$ref": "#/definitions/forward_port"
}, },
"root_dir": {
"$ref": "#/definitions/root_dir"
},
"index_file": {
"$ref": "#/definitions/index_file"
},
"static": {
"$ref": "#/definitions/static"
},
"forward_path": { "forward_path": {
"type": "string" "type": "string"
}, },
@ -131,6 +148,12 @@
"forward_port": { "forward_port": {
"$ref": "#/definitions/forward_port" "$ref": "#/definitions/forward_port"
}, },
"root_dir": {
"$ref": "#/definitions/root_dir"
},
"index_file": {
"$ref": "#/definitions/index_file"
},
"certificate_id": { "certificate_id": {
"$ref": "#/definitions/certificate_id" "$ref": "#/definitions/certificate_id"
}, },
@ -152,6 +175,9 @@
"caching_enabled": { "caching_enabled": {
"$ref": "#/definitions/caching_enabled" "$ref": "#/definitions/caching_enabled"
}, },
"static": {
"$ref": "#/definitions/static"
},
"allow_websocket_upgrade": { "allow_websocket_upgrade": {
"$ref": "#/definitions/allow_websocket_upgrade" "$ref": "#/definitions/allow_websocket_upgrade"
}, },
@ -204,9 +230,7 @@
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
"domain_names", "domain_names",
"forward_scheme", "forward_scheme"
"forward_host",
"forward_port"
], ],
"properties": { "properties": {
"domain_names": { "domain_names": {
@ -221,6 +245,12 @@
"forward_port": { "forward_port": {
"$ref": "#/definitions/forward_port" "$ref": "#/definitions/forward_port"
}, },
"root_dir": {
"$ref": "#/definitions/root_dir"
},
"index_file": {
"$ref": "#/definitions/index_file"
},
"certificate_id": { "certificate_id": {
"$ref": "#/definitions/certificate_id" "$ref": "#/definitions/certificate_id"
}, },
@ -242,6 +272,9 @@
"caching_enabled": { "caching_enabled": {
"$ref": "#/definitions/caching_enabled" "$ref": "#/definitions/caching_enabled"
}, },
"static": {
"$ref": "#/definitions/static"
},
"allow_websocket_upgrade": { "allow_websocket_upgrade": {
"$ref": "#/definitions/allow_websocket_upgrade" "$ref": "#/definitions/allow_websocket_upgrade"
}, },
@ -294,6 +327,12 @@
"forward_port": { "forward_port": {
"$ref": "#/definitions/forward_port" "$ref": "#/definitions/forward_port"
}, },
"root_dir": {
"$ref": "#/definitions/root_dir"
},
"index_file": {
"$ref": "#/definitions/index_file"
},
"certificate_id": { "certificate_id": {
"$ref": "#/definitions/certificate_id" "$ref": "#/definitions/certificate_id"
}, },
@ -315,6 +354,9 @@
"caching_enabled": { "caching_enabled": {
"$ref": "#/definitions/caching_enabled" "$ref": "#/definitions/caching_enabled"
}, },
"static": {
"$ref": "#/definitions/static"
},
"allow_websocket_upgrade": { "allow_websocket_upgrade": {
"$ref": "#/definitions/allow_websocket_upgrade" "$ref": "#/definitions/allow_websocket_upgrade"
}, },

View File

@ -20,20 +20,9 @@
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"forwarding_host": { "forward_ip": {
"anyOf": [ "type": "string",
{ "format": "ipv4"
"$ref": "../definitions.json#/definitions/domain_name"
},
{
"type": "string",
"format": "ipv4"
},
{
"type": "string",
"format": "ipv6"
}
]
}, },
"forwarding_port": { "forwarding_port": {
"type": "integer", "type": "integer",
@ -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

@ -1,49 +0,0 @@
#!/usr/bin/node
// Usage:
// Install all plugins defined in `certbot-dns-plugins.json`:
// ./install-certbot-plugins
// Install one or more specific plugins:
// ./install-certbot-plugins route53 cloudflare
//
// Usage with a running docker container:
// docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins"
//
const dnsPlugins = require('../global/certbot-dns-plugins.json');
const certbot = require('../lib/certbot');
const logger = require('../logger').certbot;
const batchflow = require('batchflow');
let hasErrors = false;
let failingPlugins = [];
let pluginKeys = Object.keys(dnsPlugins);
if (process.argv.length > 2) {
pluginKeys = process.argv.slice(2);
}
batchflow(pluginKeys).sequential()
.each((i, pluginKey, next) => {
certbot.installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
failingPlugins.push(pluginKey);
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
logger.error('Some plugins failed to install. Please check the logs above. Failing plugins: ' + '\n - ' + failingPlugins.join('\n - '));
process.exit(1);
} else {
logger.complete('Plugins installed successfully');
process.exit(0);
}
});

View File

@ -1,4 +1,6 @@
const config = require('./lib/config'); const fs = require('fs');
const NodeRSA = require('node-rsa');
const config = require('config');
const logger = require('./logger').setup; const logger = require('./logger').setup;
const certificateModel = require('./models/certificate'); const certificateModel = require('./models/certificate');
const userModel = require('./models/user'); const userModel = require('./models/user');
@ -6,7 +8,64 @@ const userPermissionModel = require('./models/user_permission');
const utils = require('./lib/utils'); const utils = require('./lib/utils');
const authModel = require('./models/auth'); const authModel = require('./models/auth');
const settingModel = require('./models/setting'); const settingModel = require('./models/setting');
const certbot = require('./lib/certbot'); const dns_plugins = require('./global/certbot-dns-plugins');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
/**
* Creates a new JWT RSA Keypair if not alread set on the config
*
* @returns {Promise}
*/
const setupJwt = () => {
return new Promise((resolve, reject) => {
// 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')) {
logger.info('Creating a new JWT key pair...');
// jwt keys are not configured properly
const filename = config.util.getEnv('NODE_CONFIG_DIR') + '/' + (config.util.getEnv('NODE_ENV') || 'default') + '.json';
let config_data = {};
try {
config_data = require(filename);
} catch (err) {
// do nothing
if (debug_mode) {
logger.debug(filename + ' config file could not be required');
}
}
// Now create the keys and save them in the config.
let key = new NodeRSA({ b: 2048 });
key.generateKeyPair();
config_data.jwt = {
key: key.exportKey('private').toString(),
pub: key.exportKey('public').toString(),
};
// Write config
fs.writeFile(filename, JSON.stringify(config_data, null, 2), (err) => {
if (err) {
logger.error('Could not write JWT key pair to config file: ' + filename);
reject(err);
} else {
logger.info('Wrote JWT key pair to config file: ' + filename);
delete require.cache[require.resolve('config')];
resolve();
}
});
} else {
// JWT key pair exists
if (debug_mode) {
logger.debug('JWT Keypair already exists');
}
resolve();
}
});
};
/** /**
* Creates a default admin users if one doesn't already exist in the database * Creates a default admin users if one doesn't already exist in the database
* *
@ -60,8 +119,8 @@ const setupDefaultUser = () => {
.then(() => { .then(() => {
logger.info('Initial admin setup completed'); logger.info('Initial admin setup completed');
}); });
} else if (config.debug()) { } else if (debug_mode) {
logger.info('Admin user setup not required'); logger.debug('Admin user setup not required');
} }
}); });
}; };
@ -92,8 +151,8 @@ const setupDefaultSettings = () => {
logger.info('Default settings added'); logger.info('Default settings added');
}); });
} }
if (config.debug()) { if (debug_mode) {
logger.info('Default setting setup not required'); logger.debug('Default setting setup not required');
} }
}); });
}; };
@ -115,56 +174,36 @@ const setupCertbotPlugins = () => {
certificates.map(function (certificate) { certificates.map(function (certificate) {
if (certificate.meta && certificate.meta.dns_challenge === true) { if (certificate.meta && certificate.meta.dns_challenge === true) {
if (plugins.indexOf(certificate.meta.dns_provider) === -1) { const dns_plugin = dns_plugins[certificate.meta.dns_provider];
plugins.push(certificate.meta.dns_provider); const packages_to_install = `${dns_plugin.package_name}==${dns_plugin.package_version} ${dns_plugin.dependencies}`;
}
if (plugins.indexOf(packages_to_install) === -1) plugins.push(packages_to_install);
// Make sure credentials file exists // Make sure credentials file exists
const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
// Escape single quotes and backslashes const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }';
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)); promises.push(utils.exec(credentials_cmd));
} }
}); });
return certbot.installPlugins(plugins) if (plugins.length) {
.then(() => { const install_cmd = 'pip3 install ' + plugins.join(' ');
if (promises.length) { promises.push(utils.exec(install_cmd));
return Promise.all(promises) }
.then(() => {
logger.info('Added Certbot plugins ' + plugins.join(', ')); 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 () { module.exports = function () {
return setupDefaultUser() return setupJwt()
.then(setupDefaultUser)
.then(setupDefaultSettings) .then(setupDefaultSettings)
.then(setupCertbotPlugins) .then(setupCertbotPlugins);
.then(setupLogrotation);
}; };

View File

@ -1,25 +0,0 @@
{% 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 }};
{% if access_list.pass_auth == 0 %}
proxy_set_header Authorization "";
{% endif %}
{% endif %}
# Access Rules: {{ access_list.clients | size }} total
{% for client in access_list.clients %}
{{client | nginxAccessRule}}
{% endfor %}
deny all;
# Access checks must...
{% if access_list.satisfy_any == 1 %}
satisfy any;
{% else %}
satisfy all;
{% endif %}
{% endif %}

View File

@ -2,7 +2,7 @@
{% 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) (63072000 seconds = 2 years)
add_header Strict-Transport-Security $hsts_header always; add_header Strict-Transport-Security "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always;
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -1,3 +0,0 @@
map $scheme $hsts_header {
https "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload";
}

View File

@ -5,11 +5,16 @@
#listen [::]:80; #listen [::]:80;
{% endif %} {% endif %}
{% if certificate -%} {% if certificate -%}
listen 443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %}; listen 443 ssl{% if http2_support %} http2{% endif %};
{% if ipv6 -%} {% if ipv6 -%}
listen [::]:443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %}; listen [::]:443;
{% else -%} {% else -%}
#listen [::]:443; #listen [::]:443;
{% endif %} {% endif %}
{% endif %} {% endif %}
server_name {{ domain_names | join: " " }}; server_name {{ domain_names | join: " " }};
{% if static == 1 or static == true %}
root {{ root_dir }};
index {{ index_file }};
{% endif %}

View File

@ -1,23 +1,15 @@
location {{ path }} { location {{ path }} {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
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 }};
{% include "_access.conf" %}
{% 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 %}
{% if static == 0 or static == false %}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
{% else %}
alias {{ root_dir }}/$1;
try_files $uri /{{ index_file }} =200;
{% endif %}
{{ advanced_config }} {{ advanced_config }}
} }

View File

@ -1,22 +1,18 @@
{% include "_header_comment.conf" %} {% include "_header_comment.conf" %}
{% if enabled %} {% if enabled %}
{% include "_hsts_map.conf" %}
server { 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

@ -7,29 +7,20 @@
server { server {
listen 80 default; listen 80 default;
{% if ipv6 -%} {% if ipv6 -%}
listen [::]:80 default; listen [::]:80;
{% else -%} {% else -%}
#listen [::]:80 default; #listen [::]:80;
{% endif %} {% 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;
} }
{% endif %} {% endif %}
{%- if value == "444" %}
location / {
return 444;
}
{% endif %}
{%- if value == "redirect" %} {%- if value == "redirect" %}
location / { location / {
return 301 {{ meta.redirect }}; return 301 {{ meta.redirect }};
@ -38,6 +29,7 @@ server {
{%- if value == "html" %} {%- if value == "html" %}
root /data/nginx/default_www; root /data/nginx/default_www;
# root /var/www/test2;
location / { location / {
try_files $uri /index.html; try_files $uri /index.html;
} }

View File

@ -8,8 +8,7 @@ server {
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

@ -1,9 +1,6 @@
{% include "_header_comment.conf" %} {% include "_header_comment.conf" %}
{% if enabled %} {% if enabled %}
{% include "_hsts_map.conf" %}
server { server {
set $forward_scheme {{ forward_scheme }}; set $forward_scheme {{ forward_scheme }};
set $server "{{ forward_host }}"; set $server "{{ forward_host }}";
@ -14,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 }}
@ -33,7 +22,28 @@ proxy_http_version 1.1;
location / { location / {
{% include "_access.conf" %} {% 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 "_forced_ssl.conf" %}
{% 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 %}
@ -42,8 +52,14 @@ proxy_http_version 1.1;
proxy_http_version 1.1; proxy_http_version 1.1;
{% endif %} {% endif %}
# Proxy! {% if static == 1 or static == true %}
include conf.d/include/proxy.conf; alias {{ root_dir }}/$1;
try_files $uri /{{index_file}} =200;
{% else %}
# Proxy!
include conf.d/include/proxy.conf;
{% endif %}
} }
{% endif %} {% endif %}

View File

@ -1,24 +1,20 @@
{% include "_header_comment.conf" %} {% include "_header_comment.conf" %}
{% if enabled %} {% if enabled %}
{% include "_hsts_map.conf" %}
server { server {
{% include "_listen.conf" %} {% include "_listen.conf" %}
{% include "_certificates.conf" %} {% include "_certificates.conf" %}
{% 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 %}

View File

@ -12,7 +12,7 @@ server {
#listen [::]:{{ incoming_port }}; #listen [::]:{{ incoming_port }};
{% endif %} {% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; proxy_pass {{ forward_ip }}:{{ forwarding_port }};
# Custom # Custom
include /data/nginx/custom/server_stream[.]conf; include /data/nginx/custom/server_stream[.]conf;
@ -27,7 +27,7 @@ server {
{% else -%} {% else -%}
#listen [::]:{{ incoming_port }} udp; #listen [::]:{{ incoming_port }} udp;
{% endif %} {% endif %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; proxy_pass {{ forward_ip }}:{{ 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,34 +3,29 @@
# 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 jc21/nginx-full:node
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILD_VERSION ARG BUILD_VERSION
ARG BUILD_COMMIT ARG BUILD_COMMIT
ARG BUILD_DATE ARG BUILD_DATE
# See: https://github.com/just-containers/s6-overlay/blob/master/README.md
ENV SUPPRESS_NO_CONFIG_WARNING=1 \ ENV SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_FIX_ATTRS_HIDDEN=1 \ S6_FIX_ATTRS_HIDDEN=1 \
S6_KILL_FINISH_MAXTIME=10000 \ S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_VERBOSITY=1 \
NODE_ENV=production \ NODE_ENV=production \
NPM_BUILD_VERSION="${BUILD_VERSION}" \ NPM_BUILD_VERSION="${BUILD_VERSION}" \
NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ NPM_BUILD_COMMIT="${BUILD_COMMIT}" \
NPM_BUILD_DATE="${BUILD_DATE}" \ NPM_BUILD_DATE="${BUILD_DATE}"
NODE_OPTIONS="--openssl-legacy-provider"
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends jq logrotate \ && apt-get install -y certbot jq python3-pip \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# s6 overlay # s6 overlay
COPY docker/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 81 443
@ -40,18 +35,17 @@ COPY frontend/dist /app/frontend
COPY global /app/global COPY global /app/global
WORKDIR /app WORKDIR /app
RUN yarn install \ RUN yarn install
&& yarn cache clean
# add late to limit cache-busting by modifications # add late to limit cache-busting by modifications
COPY docker/rootfs / 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/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \ RUN rm -rf /etc/services.d/frontend RUN rm -f /etc/nginx/conf.d/dev.conf
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager
VOLUME [ "/data" ] VOLUME [ "/data", "/etc/letsencrypt" ]
ENTRYPOINT [ "/init" ] ENTRYPOINT [ "/init" ]
HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health
LABEL org.label-schema.schema-version="1.0" \ LABEL org.label-schema.schema-version="1.0" \
org.label-schema.license="MIT" \ org.label-schema.license="MIT" \

View File

@ -1,18 +1,13 @@
FROM nginxproxymanager/nginx-full:certbot-node FROM jc21/nginx-full:node
LABEL maintainer="Jamie Curnow <jc@jc21.com>" LABEL maintainer="Jamie Curnow <jc@jc21.com>"
# See: https://github.com/just-containers/s6-overlay/blob/master/README.md ENV S6_LOGGING=0 \
ENV SUPPRESS_NO_CONFIG_WARNING=1 \ SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ S6_FIX_ATTRS_HIDDEN=1
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_FIX_ATTRS_HIDDEN=1 \
S6_KILL_FINISH_MAXTIME=10000 \
S6_VERBOSITY=2 \
NODE_OPTIONS="--openssl-legacy-provider"
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \ && apt-get update \
&& apt-get install -y jq python3-pip logrotate \ && apt-get install -y certbot jq python3-pip \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -23,11 +18,11 @@ 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
COPY scripts/install-s6 /tmp/install-s6 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 /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 && tar -xzf /tmp/s6-overlay-amd64.tar.gz -C /
EXPOSE 80 81 443 EXPOSE 80 81 443
ENTRYPOINT [ "/init" ] ENTRYPOINT [ "/init" ]
HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health

View File

@ -1,91 +1,72 @@
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. # WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
version: '3.8' version: "3"
services: services:
fullstack-mysql: fullstack-mysql:
image: "${IMAGE}:ci-${BUILD_NUMBER}" image: ${IMAGE}:ci-${BUILD_NUMBER}
environment: environment:
DEBUG: 'true' NODE_ENV: "development"
LE_STAGING: 'true'
FORCE_COLOR: 1 FORCE_COLOR: 1
DB_MYSQL_HOST: 'db' DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: '3306' DB_MYSQL_PORT: 3306
DB_MYSQL_USER: 'npm' DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: 'npm' DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: 'npm' DB_MYSQL_NAME: "npm"
volumes: volumes:
- npm_data_mysql:/data - npm_data:/data
- npm_le_mysql:/etc/letsencrypt
expose: expose:
- 81 - 81
- 80 - 80
- 443 - 443
depends_on: depends_on:
- db - db
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
fullstack-sqlite: fullstack-sqlite:
image: "${IMAGE}:ci-${BUILD_NUMBER}" image: ${IMAGE}:ci-${BUILD_NUMBER}
environment: environment:
DEBUG: 'true' NODE_ENV: "development"
LE_STAGING: 'true'
FORCE_COLOR: 1 FORCE_COLOR: 1
DB_SQLITE_FILE: '/data/mydb.sqlite' DB_SQLITE_FILE: "/data/database.sqlite"
PUID: 1000
PGID: 1000
DISABLE_IPV6: 'true'
volumes: volumes:
- npm_data_sqlite:/data - npm_data:/data
- npm_le_sqlite:/etc/letsencrypt
expose: expose:
- 81 - 81
- 80 - 80
- 443 - 443
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
environment: environment:
MYSQL_ROOT_PASSWORD: 'npm' MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: 'npm' MYSQL_DATABASE: "npm"
MYSQL_USER: 'npm' MYSQL_USER: "npm"
MYSQL_PASSWORD: 'npm' MYSQL_PASSWORD: "npm"
volumes: volumes:
- mysql_data:/var/lib/mysql - db_data:/var/lib/mysql
cypress-mysql: cypress-mysql:
image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}" image: ${IMAGE}-cypress:ci-${BUILD_NUMBER}
build: build:
context: ../test/ context: ../test/
dockerfile: cypress/Dockerfile dockerfile: cypress/Dockerfile
environment: environment:
CYPRESS_baseUrl: 'http://fullstack-mysql:81' CYPRESS_baseUrl: "http://fullstack-mysql:81"
volumes: volumes:
- cypress_logs_mysql:/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}
cypress-sqlite: cypress-sqlite:
image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}" image: ${IMAGE}-cypress:ci-${BUILD_NUMBER}
build: build:
context: ../test/ context: ../test/
dockerfile: cypress/Dockerfile dockerfile: cypress/Dockerfile
environment: environment:
CYPRESS_baseUrl: "http://fullstack-sqlite:81" CYPRESS_baseUrl: "http://fullstack-sqlite:81"
volumes: volumes:
- cypress_logs_sqlite:/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}
volumes: volumes:
cypress_logs_mysql: cypress-logs:
cypress_logs_sqlite: npm_data:
npm_data_mysql: db_data:
npm_data_sqlite:
npm_le_sqlite:
npm_le_mysql:
mysql_data:

View File

@ -1,10 +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.8' 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
@ -15,19 +14,14 @@ services:
networks: networks:
- nginx_proxy_manager - nginx_proxy_manager
environment: environment:
PUID: 1000 NODE_ENV: "development"
PGID: 1000
FORCE_COLOR: 1 FORCE_COLOR: 1
# specifically for dev: DEVELOPMENT: "true"
DEBUG: 'true' DB_MYSQL_HOST: "db"
DEVELOPMENT: 'true' DB_MYSQL_PORT: 3306
LE_STAGING: 'true' DB_MYSQL_USER: "npm"
# db: DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_HOST: 'db' DB_MYSQL_NAME: "npm"
DB_MYSQL_PORT: '3306'
DB_MYSQL_USER: 'npm'
DB_MYSQL_PASSWORD: 'npm'
DB_MYSQL_NAME: 'npm'
# DB_SQLITE_FILE: "/data/database.sqlite" # DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true" # DISABLE_IPV6: "true"
volumes: volumes:
@ -42,27 +36,32 @@ services:
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
container_name: npm_db
ports:
- 33306:3306
networks: networks:
- nginx_proxy_manager - nginx_proxy_manager
environment: environment:
MYSQL_ROOT_PASSWORD: 'npm' MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: 'npm' MYSQL_DATABASE: "npm"
MYSQL_USER: 'npm' MYSQL_USER: "npm"
MYSQL_PASSWORD: 'npm' MYSQL_PASSWORD: "npm"
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
swagger:
image: 'swaggerapi/swagger-ui:latest'
ports:
- 3001:80
networks:
- nginx_proxy_manager
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: networks:
nginx_proxy_manager: nginx_proxy_manager:
name: npm_network

View File

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

@ -0,0 +1,2 @@
*
!.gitignore

View File

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

View File

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

View File

@ -0,0 +1,2 @@
*
!.gitignore

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,27 +0,0 @@
/data/logs/*_access.log /data/logs/*/access.log {
su npm npm
create 0644
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 {
su npm npm
create 0644
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,10 +29,11 @@ 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_key /data/nginx/dummykey.pem;
include conf.d/include/ssl-ciphers.conf; include conf.d/include/ssl-ciphers.conf;
ssl_reject_handshake on;
return 444; return 444;
} }

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,10 +1,3 @@
set $test "";
if ($scheme = "http") { if ($scheme = "http") {
set $test "H";
}
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
set $test "${test}T";
}
if ($test = H) {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }

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,7 @@ 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_set_header X-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port$request_uri; proxy_pass $forward_scheme://$server:$port;

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

@ -1,7 +1,7 @@
# run nginx in foreground # run nginx in foreground
daemon off; daemon off;
pid /run/nginx/nginx.pid;
user npm; user root;
# Set number of worker processes automatically based on number of CPU cores. # Set number of worker processes automatically based on number of CPU cores.
worker_processes auto; worker_processes auto;
@ -9,13 +9,13 @@ 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;
events { events {
include /data/nginx/custom/events[.]conf; worker_connections 1024;
} }
http { http {
@ -46,7 +46,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,11 +58,11 @@ http {
} }
# Real IP Determination # Real IP Determination
# Docker subnet:
set_real_ip_from 172.0.0.0/8;
# Local subnets: # Local subnets:
set_real_ip_from 10.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.0.0.0/8;
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:

View File

@ -1,21 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
cd /app || exit 1
log_info 'Starting backend ...'
if [ "${DEVELOPMENT:-}" = 'true' ]; then
s6-setuidgid "$PUID:$PGID" yarn install
exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js"
else
while :
do
s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js"
sleep 1
done
fi

View File

@ -1 +0,0 @@
longrun

View File

@ -1,21 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
# This service is DEVELOPMENT only.
if [ "$DEVELOPMENT" = 'true' ]; then
. /usr/bin/common.sh
cd /app/frontend || exit 1
HOME=$NPMHOME
export HOME
mkdir -p /app/frontend/dist
chown -R "$PUID:$PGID" /app/frontend/dist
log_info 'Starting frontend ...'
s6-setuidgid "$PUID:$PGID" yarn install
exec s6-setuidgid "$PUID:$PGID" yarn watch
else
exit 0
fi

View File

@ -1 +0,0 @@
longrun

View File

@ -1,9 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
log_info 'Starting nginx ...'
exec s6-setuidgid "$PUID:$PGID" nginx

View File

@ -1 +0,0 @@
longrun

View File

@ -1,22 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
if [ "$(id -u)" != "0" ]; then
log_fatal "This docker container must be run as root, do not specify a user.\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization."
fi
if [ "$DEBUG" = "true" ]; then
set -x
fi
. /etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh
. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh

View File

@ -1,40 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info "Configuring $NPMUSER user ..."
if id -u "$NPMUSER" 2>/dev/null; then
# user already exists
usermod -u "$PUID" "$NPMUSER"
else
# Add user
useradd -o -u "$PUID" -U -d "$NPMHOME" -s /bin/false "$NPMUSER"
fi
log_info "Configuring $NPMGROUP group ..."
if [ "$(get_group_id "$NPMGROUP")" = '' ]; then
# Add group. This will not set the id properly if it's already taken
groupadd -f -g "$PGID" "$NPMGROUP"
else
groupmod -o -g "$PGID" "$NPMGROUP"
fi
# Set the group ID and check it
groupmod -o -g "$PGID" "$NPMGROUP"
if [ "$(get_group_id "$NPMGROUP")" != "$PGID" ]; then
echo "ERROR: Unable to set group id properly"
exit 1
fi
# Set the group against the user and check it
usermod -G "$PGID" "$NPMGROUP"
if [ "$(id -g "$NPMUSER")" != "$PGID" ] ; then
echo "ERROR: Unable to set group against the user properly"
exit 1
fi
# Home for user
mkdir -p "$NPMHOME"
chown -R "$PUID:$PGID" "$NPMHOME"

View File

@ -1,41 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Checking paths ...'
# Ensure /data is mounted
if [ ! -d '/data' ]; then
log_fatal '/data is not mounted! Check your docker configuration.'
fi
# Ensure /etc/letsencrypt is mounted
if [ ! -d '/etc/letsencrypt' ]; then
log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.'
fi
# Create required folders
mkdir -p \
/data/nginx \
/data/custom_ssl \
/data/logs \
/data/access \
/data/nginx/default_host \
/data/nginx/default_www \
/data/nginx/proxy_host \
/data/nginx/redirection_host \
/data/nginx/stream \
/data/nginx/dead_host \
/data/nginx/temp \
/data/letsencrypt-acme-challenge \
/run/nginx \
/tmp/nginx/body \
/var/log/nginx \
/var/lib/nginx/cache/public \
/var/lib/nginx/cache/private \
/var/cache/nginx/proxy_temp
touch /var/log/nginx/error.log || true
chmod 777 /var/log/nginx/error.log || true
chmod -R 777 /var/cache/nginx || true
chmod 644 /etc/logrotate.d/nginx-proxy-manager

View File

@ -1,28 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Setting ownership ...'
# root
chown root /tmp/nginx
# npm user and group
chown -R "$PUID:$PGID" /data
chown -R "$PUID:$PGID" /etc/letsencrypt
chown -R "$PUID:$PGID" /run/nginx
chown -R "$PUID:$PGID" /tmp/nginx
chown -R "$PUID:$PGID" /var/cache/nginx
chown -R "$PUID:$PGID" /var/lib/logrotate
chown -R "$PUID:$PGID" /var/lib/nginx
chown -R "$PUID:$PGID" /var/log/nginx
# Don't chown entire /etc/nginx folder as this causes crashes on some systems
chown -R "$PUID:$PGID" /etc/nginx/nginx
chown -R "$PUID:$PGID" /etc/nginx/nginx.conf
chown -R "$PUID:$PGID" /etc/nginx/conf.d
# Prevents errors when installing python certbot plugins when non-root
chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
find /opt/certbot/lib/python*/site-packages -not -user "$PUID" -execdir chown "$PUID:$PGID" {} \+

View File

@ -1,17 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Dynamic resolvers ...'
DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm
if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ];
then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
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

View File

@ -1,39 +0,0 @@
#!/command/with-contenv bash
# shellcheck shell=bash
# This command reads the `DISABLE_IPV6` env var and will either enable
# or disable ipv6 in all nginx configs based on this setting.
set -e
log_info 'IPv6 ...'
# Lowercase
DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
process_folder () {
FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX=
if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
else
# IPV6 is enabled
echo "Enabling IPV6 in hosts in: $1"
SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g'
fi
for FILE in $FILES
do
echo "- ${FILE}"
echo "$(sed -E "$SED_REGEX" "$FILE")" > $FILE
done
# ensure the files are still owned by the npm user
chown -R "$PUID:$PGID" "$1"
}
process_folder /etc/nginx/conf.d
process_folder /data/nginx

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