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 -![Version](https://img.shields.io/badge/version-2.0.9-green.svg?style=for-the-badge) +![Version](https://img.shields.io/badge/version-2.0.12-green.svg?style=for-the-badge) ![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) @@ -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 -![Version](https://img.shields.io/badge/version-2.0.9-green.svg?style=for-the-badge) +![Version](https://img.shields.io/badge/version-2.0.12-green.svg?style=for-the-badge) ![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) 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 @@
+ + +
+
+
+ +
+
+
+
+
@@ -152,6 +164,12 @@
+

Nginx variables available to you are:

+
    +
  • $server # Host/IP
  • +
  • $port # Port Number
  • +
  • $forward_scheme # http or https
  • +
diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index fcc394de..1e26bcf5 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -3,11 +3,14 @@ const Mn = require('backbone.marionette'); const App = require('../../main'); const ProxyHostModel = require('../../../models/proxy-host'); +const ProxyLocationModel = require('../../../models/proxy-host-location'); const template = require('./form.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs'); const accessListItemTemplate = require('./access-list-item.ejs'); +const CustomLocation = require('./location'); const Helpers = require('../../../lib/helpers'); + require('jquery-serializejson'); require('selectize'); @@ -15,6 +18,8 @@ module.exports = Mn.View.extend({ template: template, className: 'modal-dialog', + locationsCollection: new ProxyLocationModel.Collection(), + ui: { form: 'form', domain_names: 'input[name="domain_names"]', @@ -22,6 +27,8 @@ module.exports = Mn.View.extend({ buttons: '.modal-footer button', cancel: 'button.cancel', save: 'button.save', + add_location_btn: 'button.add_location', + locations_container:'.locations_container', certificate_select: 'select[name="certificate_id"]', access_list_select: 'select[name="access_list_id"]', ssl_forced: 'input[name="ssl_forced"]', @@ -32,6 +39,10 @@ module.exports = Mn.View.extend({ letsencrypt: '.letsencrypt' }, + regions: { + locations_regions: '@ui.locations_container' + }, + events: { 'change @ui.certificate_select': function () { let id = this.ui.certificate_select.val(); @@ -82,6 +93,13 @@ module.exports = Mn.View.extend({ } }, + 'click @ui.add_location_btn': function (e) { + e.preventDefault(); + + const model = new ProxyLocationModel.Model(); + this.locationsCollection.add(model); + }, + 'click @ui.save': function (e) { e.preventDefault(); @@ -93,6 +111,16 @@ module.exports = Mn.View.extend({ let view = this; let data = this.ui.form.serializeJSON(); + // Add locations + data.locations = []; + this.locationsCollection.models.forEach((location) => { + data.locations.push(location.toJSON()); + }); + + // Serialize collects path from custom locations + // This field must be removed from root object + delete data.path; + // Manipulate data.forward_port = parseInt(data.forward_port, 10); data.block_exploits = !!data.block_exploits; @@ -246,5 +274,20 @@ module.exports = Mn.View.extend({ if (typeof options.model === 'undefined' || !options.model) { this.model = new ProxyHostModel.Model(); } + + this.locationsCollection = new ProxyLocationModel.Collection(); + + // Custom locations + this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({ + collection: this.locationsCollection + })); + + // Check wether there are any location defined + if (options.model && Array.isArray(options.model.attributes.locations)) { + options.model.attributes.locations.forEach((location) => { + let m = new ProxyLocationModel.Model(location); + this.locationsCollection.add(m); + }); + } } }); diff --git a/src/frontend/js/app/nginx/proxy/location-item.ejs b/src/frontend/js/app/nginx/proxy/location-item.ejs new file mode 100644 index 00000000..d6f362a0 --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/location-item.ejs @@ -0,0 +1,64 @@ +
+
+
+
+
+ +
+
+
+ + location + + +
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+ + + <%- i18n('proxy-hosts', 'cutom-forward-host-help') %> +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + <%- i18n('locations', 'delete') %> + +
+
\ No newline at end of file diff --git a/src/frontend/js/app/nginx/proxy/location.js b/src/frontend/js/app/nginx/proxy/location.js new file mode 100644 index 00000000..e9513a48 --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/location.js @@ -0,0 +1,54 @@ +const locationItemTemplate = require('./location-item.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); + +const LocationView = Mn.View.extend({ + template: locationItemTemplate, + className: 'location_block', + + ui: { + toggle: 'input[type="checkbox"]', + config: '.config', + delete: '.location-delete' + }, + + events: { + 'change @ui.toggle': function(el) { + if (el.target.checked) { + this.ui.config.show(); + } else { + this.ui.config.hide(); + } + }, + + 'change .model': function (e) { + const map = {}; + map[e.target.name] = e.target.value; + this.model.set(map); + }, + + 'click @ui.delete': function () { + this.model.destroy(); + } + }, + + onRender: function() { + $(this.ui.config).hide(); + }, + + templateContext: function() { + return { + i18n: App.i18n + } + } +}); + +const LocationCollectionView = Mn.CollectionView.extend({ + className: 'locations_container', + childView: LocationView +}); + +module.exports = { + LocationCollectionView, + LocationView +} \ No newline at end of file diff --git a/src/frontend/js/app/router.js b/src/frontend/js/app/router.js index f6b686f2..790ef817 100644 --- a/src/frontend/js/app/router.js +++ b/src/frontend/js/app/router.js @@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({ 'nginx/access': 'showNginxAccess', 'nginx/certificates': 'showNginxCertificates', 'audit-log': 'showAuditLog', + 'settings': 'showSettings', '*default': 'showDashboard' } }); diff --git a/src/frontend/js/app/settings/default-site/main.ejs b/src/frontend/js/app/settings/default-site/main.ejs new file mode 100644 index 00000000..126c9d0a --- /dev/null +++ b/src/frontend/js/app/settings/default-site/main.ejs @@ -0,0 +1,53 @@ + diff --git a/src/frontend/js/app/settings/default-site/main.js b/src/frontend/js/app/settings/default-site/main.js new file mode 100644 index 00000000..4bd14e5c --- /dev/null +++ b/src/frontend/js/app/settings/default-site/main.js @@ -0,0 +1,71 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./main.ejs'); + +require('jquery-serializejson'); +require('selectize'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save', + options: '.option-item', + value: 'input[name="value"]', + redirect: '.redirect-input', + html: '.html-content' + }, + + events: { + 'change @ui.value': function (e) { + let val = this.ui.value.filter(':checked').val(); + this.ui.options.hide(); + this.ui.options.filter('.option-' + val).show(); + }, + + 'click @ui.save': function (e) { + e.preventDefault(); + + let val = this.ui.value.filter(':checked').val(); + + // Clear redirect field before validation + if (val !== 'redirect') { + this.ui.redirect.val('').attr('required', false); + } else { + this.ui.redirect.attr('required', true); + } + + this.ui.html.attr('required', val === 'html'); + + if (!this.ui.form[0].checkValidity()) { + $('').hide().appendTo(this.ui.form).click().remove(); + return; + } + + let view = this; + let data = this.ui.form.serializeJSON(); + data.id = this.model.get('id'); + + this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + App.Api.Settings.update(data) + .then(result => { + view.model.set(result); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + }, + + onRender: function () { + this.ui.value.trigger('change'); + } +}); diff --git a/src/frontend/js/app/settings/list/item.ejs b/src/frontend/js/app/settings/list/item.ejs new file mode 100644 index 00000000..4f81b450 --- /dev/null +++ b/src/frontend/js/app/settings/list/item.ejs @@ -0,0 +1,21 @@ + +
<%- name %>
+
+ <%- description %> +
+ + +
+ <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site-' + value) %> + <% } %> +
+ + + + \ No newline at end of file diff --git a/src/frontend/js/app/settings/list/item.js b/src/frontend/js/app/settings/list/item.js new file mode 100644 index 00000000..c79b73b2 --- /dev/null +++ b/src/frontend/js/app/settings/list/item.js @@ -0,0 +1,25 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./item.ejs'); + +module.exports = Mn.View.extend({ + template: template, + tagName: 'tr', + + ui: { + edit: 'a.edit' + }, + + events: { + 'click @ui.edit': function (e) { + e.preventDefault(); + App.Controller.showSettingForm(this.model); + } + }, + + initialize: function () { + this.listenTo(this.model, 'change', this.render); + } +}); diff --git a/src/frontend/js/app/settings/list/main.ejs b/src/frontend/js/app/settings/list/main.ejs new file mode 100644 index 00000000..c96e923a --- /dev/null +++ b/src/frontend/js/app/settings/list/main.ejs @@ -0,0 +1,8 @@ + + <%- i18n('str', 'name') %> + <%- i18n('str', 'value') %> +   + + + + diff --git a/src/frontend/js/app/settings/list/main.js b/src/frontend/js/app/settings/list/main.js new file mode 100644 index 00000000..bbe75beb --- /dev/null +++ b/src/frontend/js/app/settings/list/main.js @@ -0,0 +1,29 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const ItemView = require('./item'); +const template = require('./main.ejs'); + +const TableBody = Mn.CollectionView.extend({ + tagName: 'tbody', + childView: ItemView +}); + +module.exports = Mn.View.extend({ + tagName: 'table', + className: 'table table-hover table-outline table-vcenter text-nowrap card-table', + template: template, + + regions: { + body: { + el: 'tbody', + replaceElement: true + } + }, + + onRender: function () { + this.showChildView('body', new TableBody({ + collection: this.collection + })); + } +}); diff --git a/src/frontend/js/app/settings/main.ejs b/src/frontend/js/app/settings/main.ejs new file mode 100644 index 00000000..2b02769f --- /dev/null +++ b/src/frontend/js/app/settings/main.ejs @@ -0,0 +1,14 @@ +
+
+
+

<%- i18n('settings', 'title') %>

+
+
+
+
+
+ +
+
+
+
diff --git a/src/frontend/js/app/settings/main.js b/src/frontend/js/app/settings/main.js new file mode 100644 index 00000000..348f467f --- /dev/null +++ b/src/frontend/js/app/settings/main.js @@ -0,0 +1,50 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../main'); +const SettingModel = require('../../models/setting'); +const ListView = require('./list/main'); +const ErrorView = require('../error/main'); +const template = require('./main.ejs'); + +module.exports = Mn.View.extend({ + id: 'settings', + template: template, + + ui: { + list_region: '.list-region', + add: '.add-item', + dimmer: '.dimmer' + }, + + regions: { + list_region: '@ui.list_region' + }, + + onRender: function () { + let view = this; + + App.Api.Settings.getAll() + .then(response => { + if (!view.isDestroyed() && response && response.length) { + view.showChildView('list_region', new ListView({ + collection: new SettingModel.Collection(response) + })); + } + }) + .catch(err => { + view.showChildView('list_region', new ErrorView({ + code: err.code, + message: err.message, + retry: function () { + App.Controller.showSettings(); + } + })); + + console.error(err); + }) + .then(() => { + view.ui.dimmer.removeClass('active'); + }); + } +}); diff --git a/src/frontend/js/app/ui/menu/main.ejs b/src/frontend/js/app/ui/menu/main.ejs index 3363640c..671b4e3b 100644 --- a/src/frontend/js/app/ui/menu/main.ejs +++ b/src/frontend/js/app/ui/menu/main.ejs @@ -42,6 +42,9 @@ + <% } %>
diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index 8c0dcdfb..a51f27c6 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -31,7 +31,8 @@ "online": "Online", "offline": "Offline", "unknown": "Unknown", - "expires": "Expires" + "expires": "Expires", + "value": "Value" }, "login": { "title": "Login to your account" @@ -81,7 +82,14 @@ "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", "advanced-config": "Custom Nginx Configuration", "hsts-enabled": "HSTS Enabled", - "hsts-subdomains": "HSTS Subdomains" + "hsts-subdomains": "HSTS Subdomains", + "locations": "Custom locations" + }, + "locations": { + "new_location": "Add location", + "path": "/path", + "location_label": "Define location", + "delete": "Delete" }, "ssl": { "letsencrypt": "Let's Encrypt", @@ -106,7 +114,8 @@ "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", "access-list": "Access List", "allow-websocket-upgrade": "Websockets Support", - "ignore-invalid-upstream-ssl": "Ignore Invalid SSL" + "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", + "cutom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding" }, "redirection-hosts": { "title": "Redirection Hosts", @@ -222,6 +231,14 @@ "meta-title": "Details for Event", "view-meta": "View Details", "date": "Date" + }, + "settings": { + "title": "Settings", + "default-site": "Default Site", + "default-site-congratulations": "Congratulations Page", + "default-site-404": "404 Page", + "default-site-html": "Custom Page", + "default-site-redirect": "Redirect" } } } diff --git a/src/frontend/js/models/proxy-host-location.js b/src/frontend/js/models/proxy-host-location.js new file mode 100644 index 00000000..08459138 --- /dev/null +++ b/src/frontend/js/models/proxy-host-location.js @@ -0,0 +1,37 @@ +'use strict'; + +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function() { + return { + opened: false, + path: '', + advanced_config: '', + forward_scheme: 'http', + forward_host: '', + forward_port: '80' + } + }, + + toJSON() { + const r = Object.assign({}, this.attributes); + delete r.opened; + return r; + }, + + toggleVisibility: function () { + this.save({ + opened: !this.get('opened') + }); + } +}) + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model + }) +} \ No newline at end of file diff --git a/src/frontend/js/models/setting.js b/src/frontend/js/models/setting.js new file mode 100644 index 00000000..4ee198df --- /dev/null +++ b/src/frontend/js/models/setting.js @@ -0,0 +1,25 @@ +'use strict'; + +const _ = require('underscore'); +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function () { + return { + id: undefined, + name: '', + description: '', + value: null, + meta: [] + }; + } +}); + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model: model + }) +};