diff --git a/Dockerfile.armv6 b/Dockerfile.armv6l similarity index 100% rename from Dockerfile.armv6 rename to Dockerfile.armv6l diff --git a/Dockerfile.armhf b/Dockerfile.armv7l similarity index 100% rename from Dockerfile.armhf rename to Dockerfile.armv7l diff --git a/Jenkinsfile b/Jenkinsfile index c31f7e01..167897ec 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,13 +6,16 @@ pipeline { agent any environment { IMAGE = "nginx-proxy-manager" - BASE_IMAGE = "jc21/nginx-proxy-manager-base" - TEMP_IMAGE = "nginx-proxy-manager-build_${BUILD_NUMBER}" - TEMP_IMAGE_ARM = "nginx-proxy-manager-arm-build_${BUILD_NUMBER}" - TEMP_IMAGE_ARM64 = "nginx-proxy-manager-arm64-build_${BUILD_NUMBER}" + BASE_IMAGE = "jc21/${IMAGE}-base" + TEMP_IMAGE = "${IMAGE}-build_${BUILD_NUMBER}" TAG_VERSION = getPackageVersion() MAJOR_VERSION = "2" BRANCH_LOWER = "${BRANCH_NAME.toLowerCase()}" + // Architectures: + AMD64_TAG = "amd64" + ARMV6_TAG = "armv6l" + ARMV7_TAG = "armv7l" + ARM64_TAG = "arm64" } stages { stage('Build PR') { @@ -29,19 +32,19 @@ pipeline { sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .' + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' // Dockerhub - sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}' + sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG}' withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}' + sh 'docker push docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG}' } - sh 'docker rmi ${TEMP_IMAGE}' + sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' script { - 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}`") + 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}-${AMD64_TAG}`") } } } @@ -60,31 +63,30 @@ pipeline { sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .' + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' // Dockerhub - sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:develop' + sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:develop-${AMD64_TAG}' withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:develop' + sh 'docker push docker.io/jc21/${IMAGE}:develop-${AMD64_TAG}' } - // Private Registry - sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:develop' - withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}" - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:develop' - } - - sh 'docker rmi ${TEMP_IMAGE}' + sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' } } } stage('Build Master') { + when { + branch 'master' + } parallel { - stage('x86_64') { - when { - branch 'master' + // ======================== + // amd64 + // ======================== + stage('amd64') { + agent { + label 'amd64' } steps { ansiColor('xterm') { @@ -96,131 +98,247 @@ pipeline { sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .' + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' // Dockerhub - sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:${TAG_VERSION}' - sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}' - sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:latest' + sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:latest-${AMD64_TAG}' withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}' - sh 'docker push docker.io/jc21/${IMAGE}:latest' + sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:latest-${AMD64_TAG}' } - // Private Registry - sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}' - sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}' - sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest' - - withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}" - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest' - } - - sh 'docker rmi ${TEMP_IMAGE}' - } - } - } - stage('armhf') { - when { - branch 'master' - } - agent { - label 'armhf' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf npm run-script build' - sh 'rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf yarn install --prod' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM} -f Dockerfile.armhf .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:${TAG_VERSION}-armhf' - sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-armhf' - sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:latest-armhf' - - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-armhf' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-armhf' - sh 'docker push docker.io/jc21/${IMAGE}:latest-armhf' - } - - // Private Registry - sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-armhf' - sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-armhf' - sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-armhf' - - withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}" - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-armhf' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-armhf' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-armhf' - } - - sh 'docker rmi ${TEMP_IMAGE_ARM}' + sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' } } } + // ======================== + // arm64 + // ======================== stage('arm64') { - when { - branch 'master' - } agent { label 'arm64' } steps { ansiColor('xterm') { // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 npm run-script build' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' sh 'sudo rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 yarn install --prod' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM64} -f Dockerfile.arm64 .' + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARM64_TAG} -f Dockerfile.${ARM64_TAG} .' // Dockerhub - sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:${TAG_VERSION}-arm64' - sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-arm64' - sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:latest-arm64' + sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:latest-${ARM64_TAG}' withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-arm64' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-arm64' - sh 'docker push docker.io/jc21/${IMAGE}:latest-arm64' + sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARM64_TAG}' } - // Private Registry - sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-arm64' - sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-arm64' - sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-arm64' - - withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}" - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-arm64' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-arm64' - sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-arm64' - } - - sh 'docker rmi ${TEMP_IMAGE_ARM64}' - - // Hack to clean up ec2 instance for next build - sh 'sudo chown -R ec2-user:ec2-user *' + sh 'docker rmi ${TEMP_IMAGE}-${ARM64_TAG}' } } } + // ======================== + // armv7l + // ======================== + stage('armv7l') { + agent { + label 'armv7l' + } + steps { + ansiColor('xterm') { + // Codebase + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' + sh 'rm -rf node_modules' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' + + // Docker Build + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARMV7_TAG} -f Dockerfile.${ARMV7_TAG} .' + + // Dockerhub + sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:latest-${ARMV7_TAG}' + + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh "docker login -u '${duser}' -p '${dpass}'" + sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARMV7_TAG}' + } + + sh 'docker rmi ${TEMP_IMAGE}-${ARMV7_TAG}' + } + } + } + // ======================== + // armv6l - Disabled for the time being + // ======================== + /* + stage('armv6l') { + agent { + label 'armv6l' + } + steps { + ansiColor('xterm') { + // Codebase + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' + sh 'rm -rf node_modules' + sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' + + // Docker Build + sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARMV6_TAG} -f Dockerfile.${ARMV6_TAG} .' + + // Dockerhub + sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' + sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:latest-${ARMV6_TAG}' + + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh "docker login -u '${duser}' -p '${dpass}'" + sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' + sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARMV6_TAG}' + } + + sh 'docker rmi ${TEMP_IMAGE}-${ARMV6_TAG}' + } + } + } + */ + } + } + // ======================== + // latest manifest + // ======================== + stage('Latest Manifest') { + when { + branch 'master' + } + steps { + ansiColor('xterm') { + // ======================= + // latest + // ======================= + sh 'docker pull jc21/${IMAGE}:latest-${AMD64_TAG}' + sh 'docker pull jc21/${IMAGE}:latest-${ARM64_TAG}' + sh 'docker pull jc21/${IMAGE}:latest-${ARMV7_TAG}' + //sh 'docker pull jc21/${IMAGE}:latest-${ARMV6_TAG}' + + sh 'docker manifest push --purge jc21/${IMAGE}:latest || echo ""' + sh 'docker manifest create jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} jc21/${IMAGE}:latest-${ARM64_TAG} jc21/${IMAGE}:latest-${ARMV7_TAG}' + + sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} --arch ${AMD64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' + sh 'docker manifest push --purge jc21/${IMAGE}:latest' + + // ======================= + // major version + // ======================= + sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' + sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' + sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' + //sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' + + sh 'docker manifest push --purge jc21/${IMAGE}:${MAJOR_VERSION} || echo ""' + sh 'docker manifest create jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' + + sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} --arch ${AMD64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' + + // ======================= + // version + // ======================= + sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' + sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' + sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' + //sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' + + sh 'docker manifest push --purge jc21/${IMAGE}:${TAG_VERSION} || echo ""' + sh 'docker manifest create jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' + + sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} --arch ${AMD64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' + sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' + } + } + } + // ======================== + // develop + // ======================== + stage('Develop Manifest') { + when { + branch 'develop' + } + steps { + ansiColor('xterm') { + sh 'docker pull jc21/${IMAGE}:develop-${AMD64_TAG}' + //sh 'docker pull jc21/${IMAGE}:develop-${ARM64_TAG}' + //sh 'docker pull jc21/${IMAGE}:develop-${ARMV7_TAG}' + //sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' + + sh 'docker manifest push --purge jc21/${IMAGE}:develop || :' + sh 'docker manifest create jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG}' + + sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG} --arch ${AMD64_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' + //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' + } + } + } + // ======================== + // cleanup + // ======================== + stage('Latest Cleanup') { + when { + branch 'master' + } + steps { + ansiColor('xterm') { + sh 'docker rmi jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} jc21/${IMAGE}:latest-${ARM64_TAG} jc21/${IMAGE}:latest-${ARMV7_TAG} || echo ""' + sh 'docker rmi jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG} || echo ""' + sh 'docker rmi jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG} || echo ""' + } + } + } + stage('Develop Cleanup') { + when { + branch 'develop' + } + steps { + ansiColor('xterm') { + sh 'docker rmi jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG} || echo ""' + } + } + } + stage('PR Cleanup') { + when { + changeRequest() + } + steps { + ansiColor('xterm') { + sh 'docker rmi jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG} || echo ""' + } } } } @@ -240,4 +358,3 @@ def getPackageVersion() { ver = sh(script: 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} bash -c "cat /data/package.json|jq -r \'.version\'"', returnStdout: true) return ver.trim() } - diff --git a/README.md b/README.md index 38d3c664..18324d8b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Nginx Proxy Manager - +   @@ -57,24 +57,6 @@ Please consult the [installation instructions](doc/INSTALL.md) for a complete gu if you just want to get up and running in the quickest time possible, grab all the files in the `doc/example/` folder and run `docker-compose up -d` -## Importing from Version 1? - -Here's a [guide for you to migrate your configuration](doc/IMPORTING.md). You should definitely read the [installation instructions](doc/INSTALL.md) first though. - -**Why should I?** - -Version 2 has the following improvements: - -- Management security and multiple user access -- User permissions and visibility -- Custom SSL certificate support -- Audit log of changes -- Broken nginx config detection -- Multiple domains in Let's Encrypt certificates -- Wildcard domain name support (not available with a Let's Encrypt certificate though) -- It's super sexy - - ## Administration When your docker container is running, connect to it on port `81` for the admin interface. diff --git a/doc/DOCKERHUB.md b/doc/DOCKERHUB.md index b7f017ca..5f0cde15 100644 --- a/doc/DOCKERHUB.md +++ b/doc/DOCKERHUB.md @@ -2,7 +2,7 @@ # Nginx Proxy Manager - +   diff --git a/doc/INSTALL.md b/doc/INSTALL.md index b7e16056..3b06e410 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -143,3 +143,23 @@ Password: changeme ``` Immediately after logging in with this default user you will be asked to modify your details and change your password. + + +### Advanced Options + +#### X-FRAME-OPTIONS Header + +You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header +value by specifying it as a Docker environment variable. The default if not specified is `deny`. + +```yml + ... + environment: + X_FRAME_OPTIONS: "sameorigin" + ... +``` + +``` +... -e "X_FRAME_OPTIONS=sameorigin" ... +``` + diff --git a/package.json b/package.json index 3db398eb..3ca74ef9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nginx-proxy-manager", - "version": "2.0.9", + "version": "2.0.12", "description": "A beautiful interface for creating Nginx endpoints", "main": "src/backend/index.js", "devDependencies": { diff --git a/rootfs/etc/nginx/conf.d/default.conf b/rootfs/etc/nginx/conf.d/default.conf index 490e2868..2530ec2e 100644 --- a/rootfs/etc/nginx/conf.d/default.conf +++ b/rootfs/etc/nginx/conf.d/default.conf @@ -22,10 +22,10 @@ server { } } -# Default 80 Host, which shows a "You are not configured" page +# "You are not configured" page, which is the default if another default doesn't exist server { - listen 80 default; - server_name localhost; + listen 80; + server_name localhost-nginx-proxy-manager; access_log /data/logs/default.log proxy; @@ -38,9 +38,9 @@ server { } } -# Default 443 Host +# First 443 Host, which is the default if another default doesn't exist server { - listen 443 ssl default; + listen 443 ssl; server_name localhost; access_log /data/logs/default.log proxy; diff --git a/rootfs/etc/nginx/nginx.conf b/rootfs/etc/nginx/nginx.conf index ad51c873..19332564 100644 --- a/rootfs/etc/nginx/nginx.conf +++ b/rootfs/etc/nginx/nginx.conf @@ -19,25 +19,26 @@ events { } http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - sendfile on; - server_tokens off; - tcp_nopush on; - tcp_nodelay on; - client_body_temp_path /tmp/nginx/body 1 2; - keepalive_timeout 65; - ssl_prefer_server_ciphers on; - gzip on; - proxy_ignore_client_abort off; - client_max_body_size 2000m; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Accept-Encoding ""; - proxy_cache off; - proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; - proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + tcp_nopush on; + tcp_nodelay on; + client_body_temp_path /tmp/nginx/body 1 2; + keepalive_timeout 65; + ssl_prefer_server_ciphers on; + gzip on; + proxy_ignore_client_abort off; + client_max_body_size 2000m; + server_names_hash_bucket_size 64; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Accept-Encoding ""; + proxy_cache off; + proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; + proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; # MISS # BYPASS @@ -70,6 +71,7 @@ http { # Files generated by NPM include /etc/nginx/conf.d/*.conf; + include /data/nginx/default_host/*.conf; include /data/nginx/proxy_host/*.conf; include /data/nginx/redirection_host/*.conf; include /data/nginx/dead_host/*.conf; diff --git a/rootfs/etc/services.d/nginx/run b/rootfs/etc/services.d/nginx/run index c7b6181e..f6b59fd6 100755 --- a/rootfs/etc/services.d/nginx/run +++ b/rootfs/etc/services.d/nginx/run @@ -7,6 +7,8 @@ mkdir -p /tmp/nginx/body \ /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 \ diff --git a/src/backend/app.js b/src/backend/app.js index e433013a..59802755 100644 --- a/src/backend/app.js +++ b/src/backend/app.js @@ -40,11 +40,17 @@ app.use(require('./lib/express/cors')); // General security/cache related headers + server header app.use(function (req, res, next) { + let x_frame_options = 'DENY'; + + if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) { + x_frame_options = process.env.X_FRAME_OPTIONS; + } + res.set({ 'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload', 'X-XSS-Protection': '0', 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', + 'X-Frame-Options': x_frame_options, 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', Pragma: 'no-cache', Expires: 0 diff --git a/src/backend/index.js b/src/backend/index.js index cd0a7818..d97450e4 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -1,7 +1,5 @@ #!/usr/bin/env node -'use strict'; - const logger = require('./logger').global; function appStart () { diff --git a/src/backend/internal/nginx.js b/src/backend/internal/nginx.js index a0d84998..faaf77fb 100644 --- a/src/backend/internal/nginx.js +++ b/src/backend/internal/nginx.js @@ -1,5 +1,3 @@ -'use strict'; - const _ = require('lodash'); const fs = require('fs'); const Liquid = require('liquidjs'); @@ -19,9 +17,9 @@ const internalNginx = { * - IF BAD: update the meta with offline status and remove the config entirely * - then reload nginx * - * @param {Object} model - * @param {String} host_type - * @param {Object} host + * @param {Object|String} model + * @param {String} host_type + * @param {Object} host * @returns {Promise} */ configure: (model, host_type, host) => { @@ -92,7 +90,7 @@ const internalNginx = { }) .then(() => { return combined_meta; - }) + }); }, /** @@ -124,9 +122,52 @@ const internalNginx = { */ getConfigName: (host_type, host_id) => { host_type = host_type.replace(new RegExp('-', 'g'), '_'); + + if (host_type === 'default') { + return '/data/nginx/default_host/site.conf'; + } + return '/data/nginx/' + host_type + '/' + host_id + '.conf'; }, + /** + * Generates custom locations + * @param {Object} host + * @returns {Promise} + */ + renderLocations: (host) => { + return new Promise((resolve, reject) => { + let template; + + try { + template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'}); + } catch (err) { + reject(new error.ConfigurationError(err.message)); + return; + } + + let renderer = new Liquid(); + let renderedLocations = ''; + + const locationRendering = async () => { + for (let i = 0; i < host.locations.length; i++) { + let locationCopy = Object.assign({}, host.locations[i]); + + if (locationCopy.forward_host.indexOf('/') > -1) { + const splitted = locationCopy.forward_host.split('/'); + + locationCopy.forward_host = splitted.shift(); + locationCopy.forward_path = `/${splitted.join('/')}`; + } + + renderedLocations += await renderer.parseAndRender(template, locationCopy); + } + } + + locationRendering().then(() => resolve(renderedLocations)); + }); + }, + /** * @param {String} host_type * @param {Object} host @@ -146,6 +187,7 @@ const internalNginx = { return new Promise((resolve, reject) => { let template = null; let filename = internalNginx.getConfigName(host_type, host.id); + try { template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); } catch (err) { @@ -153,24 +195,49 @@ const internalNginx = { return; } - renderEngine - .parseAndRender(template, host) - .then(config_text => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + let locationsPromise; + let origLocations; - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } + // Manipulate the data a bit before sending it to the template + if (host_type !== 'default') { + host.use_default_location = true; + if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { + host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + } + } - resolve(true); - }) - .catch(err => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); + if (host.locations) { + origLocations = [].concat(host.locations); + locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { + host.locations = renderedLocations; }); + } else { + locationsPromise = Promise.resolve(); + } + + locationsPromise.then(() => { + renderEngine + .parseAndRender(template, host) + .then(config_text => { + fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + + if (debug_mode) { + logger.success('Wrote config:', filename, config_text); + } + + // Restore locations array + host.locations = origLocations; + + resolve(true); + }) + .catch(err => { + if (debug_mode) { + logger.warn('Could not write ' + filename + ':', err.message); + } + + reject(new error.ConfigurationError(err.message)); + }); + }); }); }, @@ -255,7 +322,7 @@ const internalNginx = { /** * @param {String} host_type - * @param {Object} host + * @param {Object} [host] * @param {Boolean} [throw_errors] * @returns {Promise} */ @@ -264,7 +331,7 @@ const internalNginx = { return new Promise((resolve, reject) => { try { - let config_file = internalNginx.getConfigName(host_type, host.id); + let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id); if (debug_mode) { logger.warn('Deleting nginx config: ' + config_file); @@ -312,6 +379,14 @@ const internalNginx = { }); return Promise.all(promises); + }, + + /** + * @param {string} config + * @returns {boolean} + */ + advancedConfigHasDefaultLocation: function (config) { + return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); } }; diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 882d6ddd..9f1d9be8 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -108,7 +108,7 @@ const internalProxyHost = { */ update: (access, data) => { let create_certificate = data.certificate_id === 'new'; -console.log('PH UPDATE:', data); + if (create_certificate) { delete data.certificate_id; } diff --git a/src/backend/internal/setting.js b/src/backend/internal/setting.js new file mode 100644 index 00000000..eedb7d3b --- /dev/null +++ b/src/backend/internal/setting.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const error = require('../lib/error'); +const settingModel = require('../models/setting'); +const internalNginx = require('./nginx'); + +const internalSetting = { + + /** + * @param {Access} access + * @param {Object} data + * @param {String} data.id + * @return {Promise} + */ + update: (access, data) => { + return access.can('settings:update', data.id) + .then(access_data => { + return internalSetting.get(access, {id: data.id}); + }) + .then(row => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return settingModel + .query() + .where({id: data.id}) + .patch(data); + }) + .then(() => { + return internalSetting.get(access, { + id: data.id + }); + }) + .then(row => { + if (row.id === 'default-site') { + // write the html if we need to + if (row.value === 'html') { + fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'}); + } + + // Configure nginx + return internalNginx.deleteConfig('default') + .then(() => { + return internalNginx.generateConfig('default', row); + }) + .then(() => { + return internalNginx.test(); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + return row; + }) + .catch((err) => { + internalNginx.deleteConfig('default') + .then(() => { + return internalNginx.test(); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + // I'm being slack here I know.. + throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.'); + }) + }); + } else { + return row; + } + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {String} data.id + * @return {Promise} + */ + get: (access, data) => { + return access.can('settings:get', data.id) + .then(() => { + return settingModel + .query() + .where('id', data.id) + .first(); + }) + .then(row => { + if (row) { + return row; + } else { + throw new error.ItemNotFoundError(data.id); + } + }); + }, + + /** + * This will only count the settings + * + * @param {Access} access + * @returns {*} + */ + getCount: (access) => { + return access.can('settings:list') + .then(() => { + return settingModel + .query() + .count('id as count') + .first(); + }) + .then(row => { + return parseInt(row.count, 10); + }); + }, + + /** + * All settings + * + * @param {Access} access + * @returns {Promise} + */ + getAll: (access) => { + return access.can('settings:list') + .then(() => { + return settingModel + .query() + .orderBy('description', 'ASC'); + }); + } +}; + +module.exports = internalSetting; diff --git a/src/backend/lib/access/settings-get.json b/src/backend/lib/access/settings-get.json new file mode 100644 index 00000000..d2709fd8 --- /dev/null +++ b/src/backend/lib/access/settings-get.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/lib/access/settings-list.json b/src/backend/lib/access/settings-list.json new file mode 100644 index 00000000..d2709fd8 --- /dev/null +++ b/src/backend/lib/access/settings-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/lib/access/settings-update.json b/src/backend/lib/access/settings-update.json new file mode 100644 index 00000000..d2709fd8 --- /dev/null +++ b/src/backend/lib/access/settings-update.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/migrations/20190215115310_customlocations.js b/src/backend/migrations/20190215115310_customlocations.js new file mode 100644 index 00000000..5b55dd5e --- /dev/null +++ b/src/backend/migrations/20190215115310_customlocations.js @@ -0,0 +1,37 @@ +'use strict'; + +const migrate_name = 'custom_locations'; +const logger = require('../logger').migrate; + +/** + * Migrate + * Extends proxy_host table with locations field + * + * @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('proxy_host', function (proxy_host) { + proxy_host.json('locations'); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }) +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex, Promise) { + logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); + return Promise.resolve(true); +}; diff --git a/src/backend/migrations/20190227065017_settings.js b/src/backend/migrations/20190227065017_settings.js new file mode 100644 index 00000000..6ba3653f --- /dev/null +++ b/src/backend/migrations/20190227065017_settings.js @@ -0,0 +1,54 @@ +const migrate_name = 'settings'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.createTable('setting', table => { + table.string('id').notNull().primary(); + table.string('name', 100).notNull(); + table.string('description', 255).notNull(); + table.string('value', 255).notNull(); + table.json('meta').notNull(); + }) + .then(() => { + logger.info('[' + migrate_name + '] setting Table created'); + + // TODO: add settings + let settingModel = require('../models/setting'); + + return settingModel + .query() + .insert({ + id: 'default-site', + name: 'Default Site', + description: 'What to show when Nginx is hit with an unknown Host', + value: 'congratulations', + meta: {} + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] Default settings added'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex, Promise) { + logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.'); + return Promise.resolve(true); +}; diff --git a/src/backend/models/proxy_host.js b/src/backend/models/proxy_host.js index a1217236..faa5d068 100644 --- a/src/backend/models/proxy_host.js +++ b/src/backend/models/proxy_host.js @@ -47,7 +47,7 @@ class ProxyHost extends Model { } static get jsonAttributes () { - return ['domain_names', 'meta']; + return ['domain_names', 'meta', 'locations']; } static get relationMappings () { diff --git a/src/backend/models/setting.js b/src/backend/models/setting.js new file mode 100644 index 00000000..2c3e57ee --- /dev/null +++ b/src/backend/models/setting.js @@ -0,0 +1,30 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const Model = require('objection').Model; + +Model.knex(db); + +class Setting extends Model { + $beforeInsert () { + // Default for meta + if (typeof this.meta === 'undefined') { + this.meta = {}; + } + } + + static get name () { + return 'Setting'; + } + + static get tableName () { + return 'setting'; + } + + static get jsonAttributes () { + return ['meta']; + } +} + +module.exports = Setting; diff --git a/src/backend/routes/api/main.js b/src/backend/routes/api/main.js index cbc352ed..a9c885c4 100644 --- a/src/backend/routes/api/main.js +++ b/src/backend/routes/api/main.js @@ -31,6 +31,7 @@ router.use('/tokens', require('./tokens')); router.use('/users', require('./users')); router.use('/audit-log', require('./audit-log')); router.use('/reports', require('./reports')); +router.use('/settings', require('./settings')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); diff --git a/src/backend/routes/api/settings.js b/src/backend/routes/api/settings.js new file mode 100644 index 00000000..cc56db8f --- /dev/null +++ b/src/backend/routes/api/settings.js @@ -0,0 +1,96 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const internalSetting = require('../../internal/setting'); +const apiValidator = require('../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/settings + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/settings + * + * Retrieve all settings + */ + .get((req, res, next) => { + internalSetting.getAll(res.locals.access) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }); + +/** + * Specific setting + * + * /api/settings/something + */ +router + .route('/:setting_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /settings/something + * + * Retrieve a specific setting + */ + .get((req, res, next) => { + validator({ + required: ['setting_id'], + additionalProperties: false, + properties: { + setting_id: { + $ref: 'definitions#/definitions/setting_id' + } + } + }, { + setting_id: req.params.setting_id + }) + .then(data => { + return internalSetting.get(res.locals.access, { + id: data.setting_id + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/settings/something + * + * Update and existing setting + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body) + .then(payload => { + payload.id = req.params.setting_id; + return internalSetting.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/schema/definitions.json b/src/backend/schema/definitions.json index eaf55958..2aa538b2 100644 --- a/src/backend/schema/definitions.json +++ b/src/backend/schema/definitions.json @@ -9,6 +9,13 @@ "type": "integer", "minimum": 1 }, + "setting_id": { + "description": "Unique identifier for a Setting", + "example": "default-site", + "readOnly": true, + "type": "string", + "minLength": 2 + }, "token": { "type": "string", "minLength": 10 diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json index df7cb119..af87c467 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -69,6 +69,44 @@ }, "meta": { "type": "object" + }, + "locations": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "required": [ + "forward_scheme", + "forward_host", + "forward_port", + "path" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": ["integer", "null"] + }, + "path": { + "type": "string", + "minLength": 1 + }, + "forward_scheme": { + "$ref": "#/definitions/forward_scheme" + }, + "forward_host": { + "$ref": "#/definitions/forward_host" + }, + "forward_port": { + "$ref": "#/definitions/forward_port" + }, + "forward_path": { + "type": "string" + }, + "advanced_config": { + "type": "string" + } + } + } } }, "properties": { @@ -128,6 +166,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } }, "links": [ @@ -215,6 +256,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } } }, @@ -285,6 +329,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } } }, diff --git a/src/backend/schema/endpoints/settings.json b/src/backend/schema/endpoints/settings.json new file mode 100644 index 00000000..29e2865a --- /dev/null +++ b/src/backend/schema/endpoints/settings.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/settings", + "title": "Settings", + "description": "Endpoints relating to Settings", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/setting_id" + }, + "name": { + "description": "Name", + "example": "Default Site", + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "description": { + "description": "Description", + "example": "Default Site", + "type": "string", + "minLength": 2, + "maxLength": 255 + }, + "value": { + "description": "Value", + "example": "404", + "type": "string", + "maxLength": 255 + }, + "meta": { + "type": "object" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Settings", + "href": "/settings", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing Setting", + "href": "/settings/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/value" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + } + ], + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "name": { + "$ref": "#/definitions/description" + }, + "description": { + "$ref": "#/definitions/description" + }, + "value": { + "$ref": "#/definitions/value" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } +} diff --git a/src/backend/schema/index.json b/src/backend/schema/index.json index b61509bd..6e7d1c8a 100644 --- a/src/backend/schema/index.json +++ b/src/backend/schema/index.json @@ -34,6 +34,9 @@ }, "access-lists": { "$ref": "endpoints/access-lists.json" + }, + "settings": { + "$ref": "endpoints/settings.json" } } } diff --git a/src/backend/templates/_location.conf b/src/backend/templates/_location.conf new file mode 100644 index 00000000..0b8894d1 --- /dev/null +++ b/src/backend/templates/_location.conf @@ -0,0 +1,9 @@ + 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_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; + {{ advanced_config }} + } + diff --git a/src/backend/templates/dead_host.conf b/src/backend/templates/dead_host.conf index 8d3534ab..da282a12 100644 --- a/src/backend/templates/dead_host.conf +++ b/src/backend/templates/dead_host.conf @@ -10,10 +10,13 @@ server { {{ advanced_config }} +{% if use_default_location %} location / { {% include "_forced_ssl.conf" %} {% include "_hsts.conf" %} return 404; } +{% endif %} + } {% endif %} diff --git a/src/backend/templates/default.conf b/src/backend/templates/default.conf new file mode 100644 index 00000000..7ed1af97 --- /dev/null +++ b/src/backend/templates/default.conf @@ -0,0 +1,32 @@ +# ------------------------------------------------------------ +# Default Site +# ------------------------------------------------------------ +{% if value == "congratulations" %} +# Skipping output, congratulations page configration is baked in. +{%- else %} +server { + listen 80 default; + server_name default-host.localhost; + access_log /data/logs/default_host.log combined; +{% include "_exploits.conf" %} + +{%- if value == "404" %} + location / { + return 404; + } +{% endif %} + +{%- if value == "redirect" %} + location / { + return 301 {{ meta.redirect }}; + } +{%- endif %} + +{%- if value == "html" %} + root /data/nginx/default_www; + location / { + try_files $uri /index.html; + } +{%- endif %} +} +{% endif %} diff --git a/src/backend/templates/proxy_host.conf b/src/backend/templates/proxy_host.conf index 52e70583..fc58a43b 100644 --- a/src/backend/templates/proxy_host.conf +++ b/src/backend/templates/proxy_host.conf @@ -16,6 +16,10 @@ server { {{ advanced_config }} +{{ locations }} + +{% if use_default_location %} + location / { {%- if access_list_id > 0 -%} # Access List @@ -35,5 +39,7 @@ server { # Proxy! include conf.d/include/proxy.conf; } +{% endif %} + } {% endif %} diff --git a/src/backend/templates/redirection_host.conf b/src/backend/templates/redirection_host.conf index 7f55e91b..3e6c2b44 100644 --- a/src/backend/templates/redirection_host.conf +++ b/src/backend/templates/redirection_host.conf @@ -12,6 +12,7 @@ server { {{ advanced_config }} +{% if use_default_location %} location / { {% include "_forced_ssl.conf" %} {% include "_hsts.conf" %} @@ -22,5 +23,7 @@ server { return 301 $scheme://{{ forward_domain_name }}; {% endif %} } +{% endif %} + } {% endif %} diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index cc3b5ce6..c8d57193 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -662,5 +662,34 @@ module.exports = { getHostStats: function () { return fetch('get', 'reports/hosts'); } + }, + + Settings: { + + /** + * @param {String} setting_id + * @returns {Promise} + */ + getById: function (setting_id) { + return fetch('get', 'settings/' + setting_id); + }, + + /** + * @returns {Promise} + */ + getAll: function () { + return getAllObjects('settings'); + }, + + /** + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'settings/' + id, data); + } } }; diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 3f894748..7e516434 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -383,6 +383,36 @@ module.exports = { } }, + /** + * Settings + */ + showSettings: function () { + let controller = this; + if (Cache.User.isAdmin()) { + require(['./main', './settings/main'], (App, View) => { + controller.navigate('/settings'); + App.UI.showAppContent(new View()); + }); + } else { + this.showDashboard(); + } + }, + + /** + * Settings Item Form + * + * @param model + */ + showSettingForm: function (model) { + if (Cache.User.isAdmin()) { + if (model.get('id') === 'default-site') { + require(['./main', './settings/default-site/main'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + } + }, + /** * Logout */ diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index 0962916f..3f940f12 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -7,10 +7,22 @@