Compare commits

...

28 Commits

Author SHA1 Message Date
f3e6f64c0c Version Bump 2019-03-05 08:25:12 +10:00
d04b7a0d88 Bug fixes 2019-03-05 08:25:12 +10:00
71dfd5d8f8 Feature/custom locations (#74)
* New feature: custom locations

* Custom locations: exteding config generator

* Custom locations: refactoring

* Fixing proxy_host table on small screens

* Custom locations: translations

* Custom locations bugfix

* Custom locations bugfix

* PR #74 fixes
2019-03-05 08:21:02 +10:00
133d66c2fe Default Site customisation and new Settings space (#91) 2019-03-04 21:19:36 +10:00
6f1d38a0e2 Fixes #88 - Allow specifying X-FRAME-OPTIONS with an environment variable (#89) 2019-03-04 10:16:46 +10:00
aad9ecde6b CI: Prevent having to spin up resources when not Master branch 2019-03-01 20:12:49 +10:00
ae9324295c Merge branch 'develop' of github.com:jc21/nginx-proxy-manager into develop 2019-03-01 13:48:30 +10:00
0acec1105b CI: Prevent having to spin up resources when not Master branch 2019-03-01 20:12:49 +10:00
5a9a716ca6 CI: Prevent having to spin up resources when not Master branch 2019-03-01 13:47:49 +10:00
418899d425 Version bump 2019-02-27 17:52:30 +10:00
e7379e3683 Ignore default location when defined in advanced config (#79) 2019-02-25 10:42:16 +10:00
29bebcc73e Ignore default location when defined in advanced config (#79) 2019-02-25 10:34:55 +10:00
26064b20b8 Fix PR docker image pushing to wrong repo 2019-02-20 08:25:12 +10:00
3dc9b20543 CI Improvements (#77) 2019-02-20 14:35:10 +10:00
444dbd5160 Added PR build steps to CI 2019-02-20 08:25:12 +10:00
c2f99e253c Merge branch 'master' of github.com:jc21/nginx-proxy-manager 2019-02-20 10:04:16 +10:00
5c7fb7b698 Added armv6 Dockerfile 2019-02-20 08:25:12 +10:00
733d7d9583 Update DOCKERHUB.md 2019-02-19 17:05:26 +10:00
6d2f532806 Updated arm instructions 2019-02-18 21:14:26 +10:00
f76c9226c8 Fix workdir perms for subsequent builds 2019-02-18 21:14:26 +10:00
ecbc41b622 Arm64 build process doesn't run as root 2019-02-18 21:14:26 +10:00
4f60d3e7df Fix CI now that tags are changes 2019-02-18 21:14:26 +10:00
7d86fd223e Fix base docker images for arm packages 2019-02-18 21:12:46 +10:00
e3ed216a70 Added arm64 build 2019-02-18 21:12:41 +10:00
2a3d792591 Fixes #68 - HSTS is now part of the UI 2019-02-18 18:21:45 +10:00
4d754275ab Fixes #61 - Http/2 support can now be disabled 2019-02-18 15:33:32 +10:00
44e5f0957c Whoops, missing comma 2019-01-16 10:12:10 +10:00
83ef426b93 Increased custom ssl file size limits 2019-01-16 10:11:51 +10:00
69 changed files with 1963 additions and 227 deletions

38
Dockerfile.arm64 Normal file
View File

@ -0,0 +1,38 @@
FROM jc21/nginx-proxy-manager-base:arm64
MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
ENV SUPPRESS_NO_CONFIG_WARNING=1
ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
# Nginx, Node and required packages should already be installed from the base image
# root filesystem
COPY rootfs /
# s6 overlay
RUN curl -L -o /tmp/s6-overlay-aarch64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-aarch64.tar.gz" \
&& tar xzf /tmp/s6-overlay-aarch64.tar.gz -C /
# App
ENV NODE_ENV=production
ADD dist /app/dist
ADD node_modules /app/node_modules
ADD src/backend /app/src/backend
ADD package.json /app/package.json
ADD knexfile.js /app/knexfile.js
# Volumes
VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ]
# Ports
EXPOSE 80
EXPOSE 81
EXPOSE 443
EXPOSE 9876
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1

View File

@ -1,4 +1,4 @@
FROM jc21/nginx-proxy-manager-base:latest-armhf
FROM jc21/nginx-proxy-manager-base:armhf
MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"

38
Dockerfile.armv6 Normal file
View File

@ -0,0 +1,38 @@
FROM jc21/nginx-proxy-manager-base:armv6
MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
ENV SUPPRESS_NO_CONFIG_WARNING=1
ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
# Nginx, Node and required packages should already be installed from the base image
# root filesystem
COPY rootfs /
# s6 overlay
RUN curl -L -o /tmp/s6-overlay-arm.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-arm.tar.gz" \
&& tar xzf /tmp/s6-overlay-arm.tar.gz -C /
# App
ENV NODE_ENV=production
ADD dist /app/dist
ADD node_modules /app/node_modules
ADD src/backend /app/src/backend
ADD package.json /app/package.json
ADD knexfile.js /app/knexfile.js
# Volumes
VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ]
# Ports
EXPOSE 80
EXPOSE 81
EXPOSE 443
EXPOSE 9876
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1

203
Jenkinsfile vendored
View File

@ -5,17 +5,45 @@ pipeline {
}
agent any
environment {
IMAGE_NAME = "nginx-proxy-manager"
BASE_IMAGE_NAME = "jc21/nginx-proxy-manager-base:latest"
TEMP_IMAGE_NAME = "nginx-proxy-manager-build_${BUILD_NUMBER}"
TEMP_IMAGE_NAME_ARM = "nginx-proxy-manager-arm-build_${BUILD_NUMBER}"
TAG_VERSION = getPackageVersion()
MAJOR_VERSION = "2"
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}"
TAG_VERSION = getPackageVersion()
MAJOR_VERSION = "2"
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase()}"
}
stages {
stage('Prepare') {
stage('Build PR') {
when {
changeRequest()
}
steps {
sh 'docker pull $DOCKER_CI_TOOLS'
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'
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} .'
// Dockerhub
sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}'
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 rmi ${TEMP_IMAGE}'
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}`")
}
}
}
}
stage('Build Develop') {
@ -25,122 +53,165 @@ pipeline {
steps {
ansiColor('xterm') {
// Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME 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 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME yarn install --prod'
sh 'docker run --rm -v $(pwd):/data $DOCKER_CI_TOOLS node-prune'
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod'
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_NAME .'
sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .'
// Dockerhub
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:develop'
sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:develop'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'"
sh 'docker push docker.io/jc21/$IMAGE_NAME:develop'
sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/${IMAGE}:develop'
}
// Private Registry
sh 'docker tag $TEMP_IMAGE_NAME $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:develop'
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_NAME:develop'
sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}"
sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:develop'
}
sh 'docker rmi $TEMP_IMAGE_NAME'
sh 'docker rmi ${TEMP_IMAGE}'
}
}
}
stage('Build Master') {
when {
branch 'master'
}
parallel {
stage('x86_64') {
when {
branch 'master'
}
steps {
ansiColor('xterm') {
// Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME 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 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME yarn install --prod'
sh 'docker run --rm -v $(pwd):/data $DOCKER_CI_TOOLS node-prune'
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod'
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_NAME .'
sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .'
// Dockerhub
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:$TAG_VERSION'
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:$MAJOR_VERSION'
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:latest'
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'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'"
sh 'docker push docker.io/jc21/$IMAGE_NAME:$TAG_VERSION'
sh 'docker push docker.io/jc21/$IMAGE_NAME:$MAJOR_VERSION'
sh 'docker push docker.io/jc21/$IMAGE_NAME:latest'
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'
}
// Private Registry
sh 'docker tag $TEMP_IMAGE_NAME $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$TAG_VERSION'
sh 'docker tag $TEMP_IMAGE_NAME $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$MAJOR_VERSION'
sh 'docker tag $TEMP_IMAGE_NAME $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:latest'
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_NAME:$TAG_VERSION'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$MAJOR_VERSION'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:latest'
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_NAME'
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_NAME-armhf yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE_NAME-armhf npm run-script build'
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_NAME-armhf yarn install --prod'
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_NAME_ARM -f Dockerfile.armhf .'
sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM} -f Dockerfile.armhf .'
// Dockerhub
sh 'docker tag $TEMP_IMAGE_NAME_ARM docker.io/jc21/$IMAGE_NAME:$TAG_VERSION-armhf'
sh 'docker tag $TEMP_IMAGE_NAME_ARM docker.io/jc21/$IMAGE_NAME:$MAJOR_VERSION-armhf'
sh 'docker tag $TEMP_IMAGE_NAME_ARM docker.io/jc21/$IMAGE_NAME:latest-armhf'
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_NAME:$TAG_VERSION-armhf'
sh 'docker push docker.io/jc21/$IMAGE_NAME:$MAJOR_VERSION-armhf'
sh 'docker push docker.io/jc21/$IMAGE_NAME:latest-armhf'
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_NAME_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$TAG_VERSION-armhf'
sh 'docker tag $TEMP_IMAGE_NAME_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$MAJOR_VERSION-armhf'
sh 'docker tag $TEMP_IMAGE_NAME_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:latest-armhf'
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_NAME:$TAG_VERSION-armhf'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:$MAJOR_VERSION-armhf'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE_NAME:latest-armhf'
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_NAME_ARM'
sh 'docker rmi ${TEMP_IMAGE_ARM}'
}
}
}
stage('arm64') {
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 'sudo rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 yarn install --prod'
// Docker Build
sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM64} -f Dockerfile.arm64 .'
// 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'
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'
}
// 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 *'
}
}
}
@ -160,7 +231,7 @@ pipeline {
}
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)
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()
}

View File

@ -2,7 +2,7 @@
# Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-2.0.8-green.svg?style=for-the-badge)
![Version](https://img.shields.io/badge/version-2.0.11-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.

View File

@ -2,7 +2,7 @@
# Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-2.0.8-green.svg?style=for-the-badge)
![Version](https://img.shields.io/badge/version-2.0.11-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)
@ -16,6 +16,7 @@ running at home or otherwise, including free SSL, without having to know too muc
* latest 2, 2.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile))
* latest-armhf, 2-armhf, 2.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.armhf))
* latest-arm64, 2-arm64, 2.x.x-arm64 ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.arm64))
* 1, 1.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile))
* 1-armhf, 1.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile.armhf))

View File

@ -44,7 +44,7 @@ It's easy to use another docker container for your database also and link it as
version: "3"
services:
app:
image: jc21/nginx-proxy-manager:2
image: jc21/nginx-proxy-manager:latest
restart: always
ports:
- 80:80
@ -77,7 +77,7 @@ Via `docker-compose`:
version: "3"
services:
app:
image: jc21/nginx-proxy-manager:2
image: jc21/nginx-proxy-manager:latest
restart: always
ports:
- 80:80
@ -100,15 +100,17 @@ docker run -d \
-v /path/to/config.json:/app/config/production.json \
-v /path/to/data:/data \
-v /path/to/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:2
jc21/nginx-proxy-manager:latest
```
### Running on Raspberry PI / `armhf`
I have created a `armhf` docker container just for you. There may be issues with it,
I have created `armhf` and `arm64` docker containers just for you. There may be issues with it,
if you have issues please report them here.
Note: Rpi v2 and below won't work with these images.
```bash
docker run -d \
--name nginx-proxy-manager-app \
@ -118,7 +120,7 @@ docker run -d \
-v /path/to/config.json:/app/config/production.json \
-v /path/to/data:/data \
-v /path/to/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:2-armhf
jc21/nginx-proxy-manager:latest-armhf
```
@ -141,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" ...
```

View File

@ -1,6 +1,6 @@
{
"name": "nginx-proxy-manager",
"version": "2.0.8",
"version": "2.0.11",
"description": "A beautiful interface for creating Nginx endpoints",
"main": "src/backend/index.js",
"devDependencies": {

View File

@ -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;

View File

@ -7,6 +7,3 @@ ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA
ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE
S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;

View File

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

View File

@ -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 \

View File

@ -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

View File

@ -1,7 +1,5 @@
#!/usr/bin/env node
'use strict';
const logger = require('./logger').global;
function appStart () {

View File

@ -47,6 +47,7 @@ const internalDeadHost = {
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
return deadHostModel
.query()
@ -89,11 +90,11 @@ const internalDeadHost = {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
action: 'created',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@ -144,9 +145,9 @@ const internalDeadHost = {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then(cert => {
// update host with cert id
data.certificate_id = cert.id;
@ -162,7 +163,9 @@ const internalDeadHost = {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
},data);
}, data);
data = internalHost.cleanSslHstsData(data, row);
return deadHostModel
.query()
@ -171,11 +174,11 @@ const internalDeadHost = {
.then(saved_row => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
@ -183,15 +186,15 @@ const internalDeadHost = {
})
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
id: data.id,
expand: ['owner', 'certificate']
})
.then(row => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then(new_meta => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});

View File

@ -1,11 +1,40 @@
'use strict';
const _ = require('lodash');
const proxyHostModel = require('../models/proxy_host');
const redirectionHostModel = require('../models/redirection_host');
const deadHostModel = require('../models/dead_host');
const internalHost = {
/**
* Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off.
* if force_ssl is off, then hsts_enabled is definitely off.
*
* @param {object} data
* @param {object} [existing_data]
* @returns {object}
*/
cleanSslHstsData: function (data, existing_data) {
existing_data = existing_data === undefined ? {} : existing_data;
let combined_data = _.assign({}, existing_data, data);
if (!combined_data.certificate_id) {
combined_data.ssl_forced = false;
combined_data.http2_support = false;
}
if (!combined_data.ssl_forced) {
combined_data.hsts_enabled = false;
}
if (!combined_data.hsts_enabled) {
combined_data.hsts_subdomains = false;
}
return combined_data;
},
/**
* used by the getAll functions of hosts, this removes the certificate meta if present
*

View File

@ -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,43 @@ 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++) {
renderedLocations += await renderer.parseAndRender(template, host.locations[i]);
}
}
locationRendering().then(() => resolve(renderedLocations));
});
},
/**
* @param {String} host_type
* @param {Object} host
@ -146,6 +178,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 +186,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 +313,7 @@ const internalNginx = {
/**
* @param {String} host_type
* @param {Object} host
* @param {Object} [host]
* @param {Boolean} [throw_errors]
* @returns {Promise}
*/
@ -264,7 +322,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 +370,14 @@ const internalNginx = {
});
return Promise.all(promises);
},
/**
* @param {string} config
* @returns {boolean}
*/
advancedConfigHasDefaultLocation: function (config) {
return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
}
};

View File

@ -1,5 +1,3 @@
'use strict';
const _ = require('lodash');
const error = require('../lib/error');
const proxyHostModel = require('../models/proxy_host');
@ -47,6 +45,7 @@ const internalProxyHost = {
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
return proxyHostModel
.query()
@ -90,11 +89,11 @@ const internalProxyHost = {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
action: 'created',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@ -145,9 +144,9 @@ const internalProxyHost = {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then(cert => {
// update host with cert id
data.certificate_id = cert.id;
@ -165,6 +164,8 @@ const internalProxyHost = {
domain_names: row.domain_names
}, data);
data = internalHost.cleanSslHstsData(data, row);
return proxyHostModel
.query()
.where({id: data.id})
@ -172,11 +173,11 @@ const internalProxyHost = {
.then(saved_row => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
@ -184,9 +185,9 @@ const internalProxyHost = {
})
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ['owner', 'certificate', 'access_list']
})
id: data.id,
expand: ['owner', 'certificate', 'access_list']
})
.then(row => {
// Configure nginx
return internalNginx.configure(proxyHostModel, 'proxy_host', row)

View File

@ -47,6 +47,7 @@ const internalRedirectionHost = {
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
return redirectionHostModel
.query()
@ -89,11 +90,11 @@ const internalRedirectionHost = {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
action: 'created',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@ -144,9 +145,9 @@ const internalRedirectionHost = {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then(cert => {
// update host with cert id
data.certificate_id = cert.id;
@ -162,7 +163,9 @@ const internalRedirectionHost = {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
},data);
}, data);
data = internalHost.cleanSslHstsData(data, row);
return redirectionHostModel
.query()
@ -171,11 +174,11 @@ const internalRedirectionHost = {
.then(saved_row => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
@ -183,15 +186,15 @@ const internalRedirectionHost = {
})
.then(() => {
return internalRedirectionHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
id: data.id,
expand: ['owner', 'certificate']
})
.then(row => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
.then(new_meta => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});

View File

@ -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;

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -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);
};

View File

@ -0,0 +1,53 @@
'use strict';
const migrate_name = 'hsts';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] dead_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);
};

View File

@ -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);
};

View File

@ -47,7 +47,7 @@ class ProxyHost extends Model {
}
static get jsonAttributes () {
return ['domain_names', 'meta'];
return ['domain_names', 'meta', 'locations'];
}
static get relationMappings () {

View File

@ -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;

View File

@ -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'));

View File

@ -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;

View File

@ -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
@ -187,6 +194,16 @@
"example": false,
"type": "boolean"
},
"hsts_enabled": {
"description": "Is HSTS Enabled",
"example": false,
"type": "boolean"
},
"hsts_subdomains": {
"description": "Is HSTS applicable to all subdomains",
"example": false,
"type": "boolean"
},
"ssl_provider": {
"type": "string",
"pattern": "^(letsencrypt|other)$"

View File

@ -24,6 +24,12 @@
"ssl_forced": {
"$ref": "../definitions.json#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "../definitions.json#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../definitions.json#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "../definitions.json#/definitions/http2_support"
},
@ -56,6 +62,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -113,6 +125,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -153,6 +171,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},

View File

@ -38,6 +38,12 @@
"ssl_forced": {
"$ref": "../definitions.json#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "../definitions.json#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../definitions.json#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "../definitions.json#/definitions/http2_support"
},
@ -63,6 +69,41 @@
},
"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"
},
"advanced_config": {
"type": "string"
}
}
}
}
},
"properties": {
@ -93,6 +134,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -116,6 +163,9 @@
},
"meta": {
"$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
}
},
"links": [
@ -174,6 +224,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -197,6 +253,9 @@
},
"meta": {
"$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
}
}
},
@ -238,6 +297,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -261,6 +326,9 @@
},
"meta": {
"$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
}
}
},

View File

@ -32,6 +32,12 @@
"ssl_forced": {
"$ref": "../definitions.json#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "../definitions.json#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../definitions.json#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "../definitions.json#/definitions/http2_support"
},
@ -73,6 +79,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_subdomains"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -140,6 +152,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},
@ -189,6 +207,12 @@
"ssl_forced": {
"$ref": "#/definitions/ssl_forced"
},
"hsts_enabled": {
"$ref": "#/definitions/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "#/definitions/hsts_enabled"
},
"http2_support": {
"$ref": "#/definitions/http2_support"
},

View File

@ -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"
}
}
}

View File

@ -34,6 +34,9 @@
},
"access-lists": {
"$ref": "endpoints/access-lists.json"
},
"settings": {
"$ref": "endpoints/settings.json"
}
}
}

View File

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

View File

@ -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 }};
{{ advanced_config }}
}

View File

@ -4,11 +4,19 @@
server {
{% include "_listen.conf" %}
{% include "_certificates.conf" %}
{% include "_hsts.conf" %}
access_log /data/logs/dead_host-{{ id }}.log standard;
{{ advanced_config }}
return 404;
{% if use_default_location %}
location / {
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %}
return 404;
}
{% endif %}
}
{% endif %}

View File

@ -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 %}

View File

@ -10,11 +10,16 @@ server {
{% include "_certificates.conf" %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_hsts.conf" %}
access_log /data/logs/proxy_host-{{ id }}.log proxy;
{{ advanced_config }}
{{ locations }}
{% if use_default_location %}
location / {
{%- if access_list_id > 0 -%}
# Access List
@ -23,6 +28,7 @@ server {
{%- endif %}
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;
@ -33,5 +39,7 @@ server {
# Proxy!
include conf.d/include/proxy.conf;
}
{% endif %}
}
{% endif %}

View File

@ -6,15 +6,16 @@ server {
{% include "_certificates.conf" %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_hsts.conf" %}
access_log /data/logs/redirection_host-{{ id }}.log standard;
{{ advanced_config }}
# TODO: Preserve Path Option
{% if use_default_location %}
location / {
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %}
{% if preserve_path == 1 or preserve_path == true %}
return 301 $scheme://{{ forward_domain_name }}$request_uri;
@ -22,5 +23,7 @@ server {
return 301 $scheme://{{ forward_domain_name }};
{% endif %}
}
{% endif %}
}
{% endif %}

View File

@ -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);
}
}
};

View File

@ -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
*/

View File

@ -12,7 +12,7 @@ require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
max_file_size: 5120,
max_file_size: 102400,
ui: {
form: 'form',
@ -56,7 +56,7 @@ module.exports = Mn.View.extend({
return;
} else {
if (this.ui.other_certificate[0].files[0].size > this.max_file_size) {
alert('Certificate file is too large (> 5kb)');
alert('Certificate file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]});
@ -67,7 +67,7 @@ module.exports = Mn.View.extend({
return;
} else {
if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) {
alert('Certificate key file is too large (> 5kb)');
alert('Certificate key file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]});
@ -75,7 +75,7 @@ module.exports = Mn.View.extend({
if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) {
if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) {
alert('Intermediate Certificate file is too large (> 5kb)');
alert('Intermediate Certificate file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]});

View File

@ -54,6 +54,24 @@
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt">

View File

@ -22,6 +22,8 @@ module.exports = Mn.View.extend({
save: 'button.save',
certificate_select: 'select[name="certificate_id"]',
ssl_forced: 'input[name="ssl_forced"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
letsencrypt: '.letsencrypt'
},
@ -36,11 +38,44 @@ module.exports = Mn.View.extend({
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
this.ui.ssl_forced.add(this.ui.http2_support)
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
this.ui.http2_support.prop('disabled', !enabled);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.save': function (e) {
@ -55,13 +90,10 @@ module.exports = Mn.View.extend({
let data = this.ui.form.serializeJSON();
// Manipulate
if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
data.ssl_forced = true;
}
if (typeof data.http2_support !== 'undefined') {
data.http2_support = !!data.http2_support;
}
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.http2_support = !!data.http2_support;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');

View File

@ -7,10 +7,22 @@
<form>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
<li role="presentation" class="nav-item"><a href="#locations" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-layers"></i> <%- i18n('all-hosts', 'locations') %></a></li>
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></a></li>
</ul>
<div class="tab-content">
<!-- Locations -->
<div class="tab-pane" id="locations">
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-secondary add_location"><%- i18n('locations', 'new_location') %></button>
<div class="locations_container mt-3"></div>
</div>
</div>
</div>
<!-- Details -->
<div role="tabpanel" class="tab-pane active" id="details">
<div class="row">
@ -110,6 +122,24 @@
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt">
@ -134,6 +164,12 @@
<div role="tabpanel" class="tab-pane" id="advanced">
<div class="row">
<div class="col-md-12">
<p>Nginx variables available to you are:</p>
<ul class="text-monospace">
<li>$server # Host/IP</li>
<li>$port # Port Number</li>
<li>$forward_scheme # http or https</li>
</ul>
<div class="form-group mb-0">
<label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label>
<textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>

View File

@ -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,14 +27,22 @@ 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"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
forward_scheme: 'select[name="forward_scheme"]',
letsencrypt: '.letsencrypt'
},
regions: {
locations_regions: '@ui.locations_container'
},
events: {
'change @ui.certificate_select': function () {
let id = this.ui.certificate_select.val();
@ -40,10 +53,51 @@ module.exports = Mn.View.extend({
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
this.ui.ssl_forced.add(this.ui.http2_support)
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.add_location_btn': function (e) {
e.preventDefault();
const model = new ProxyLocationModel.Model();
this.locationsCollection.add(model);
},
'click @ui.save': function (e) {
@ -57,19 +111,25 @@ 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;
data.caching_enabled = !!data.caching_enabled;
data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
data.ssl_forced = true;
}
if (typeof data.http2_support !== 'undefined') {
data.http2_support = !!data.http2_support;
}
data.forward_port = parseInt(data.forward_port, 10);
data.block_exploits = !!data.block_exploits;
data.caching_enabled = !!data.caching_enabled;
data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
data.http2_support = !!data.http2_support;
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
@ -131,6 +191,9 @@ module.exports = Mn.View.extend({
onRender: function () {
let view = this;
this.ui.ssl_forced.trigger('change');
this.ui.hsts_enabled.trigger('change');
// Domain names
this.ui.domain_names.selectize({
delimiter: ',',
@ -211,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);
});
}
}
});

View File

@ -0,0 +1,63 @@
<div class="location-block card">
<div class="card-body">
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="form-label"><%- i18n('locations', 'location_label') %> <span class="form-required">*</span></label>
<div class="row gutter-xs">
<div class="col">
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text">location</span>
</span>
<input type="text" name="path" class="form-control model" value="<%- path %>" placeholder="<%- i18n('locations', 'path') %>" required>
</div>
</div>
<div class="col-auto">
<div class="selectgroup">
<label class="selectgroup-item">
<input type="checkbox" class="selectgroup-input">
<span class="selectgroup-button">
<i class="fe fe-settings"></i>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-3 col-md-3">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-scheme') %><span class="form-required">*</span></label>
<select name="forward_scheme" class="form-control custom-select model" placeholder="http">
<option value="http" <%- forward_scheme === 'http' ? 'selected' : '' %>>http</option>
<option value="https" <%- forward_scheme === 'https' ? 'selected' : '' %>>https</option>
</select>
</div>
</div>
<div class="col-sm-5 col-md-5">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label>
<input type="text" name="forward_host" class="form-control text-monospace model" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="50" required>
</div>
</div>
<div class="col-sm-4 col-md-4">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-port') %> <span class="form-required">*</span></label>
<input name="forward_port" type="number" class="form-control text-monospace model" placeholder="80" value="<%- forward_port %>" required>
</div>
</div>
</div>
<div class="row config">
<div class="col-md-12">
<div class="form-group">
<textarea name="advanced_config" rows="8" class="form-control text-monospace model" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
<a href="#" class="card-link location-delete">
<i class="fa fa-trash"></i> <%- i18n('locations', 'delete') %>
</a>
</div>
</div>

View File

@ -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
}

View File

@ -78,6 +78,24 @@
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
</label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<div class="col-sm-12 col-md-12 letsencrypt">

View File

@ -22,6 +22,8 @@ module.exports = Mn.View.extend({
save: 'button.save',
certificate_select: 'select[name="certificate_id"]',
ssl_forced: 'input[name="ssl_forced"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
letsencrypt: '.letsencrypt'
},
@ -36,11 +38,44 @@ module.exports = Mn.View.extend({
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
this.ui.ssl_forced.add(this.ui.http2_support)
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
this.ui.http2_support.prop('disabled', !enabled);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.save': function (e) {
@ -55,16 +90,12 @@ module.exports = Mn.View.extend({
let data = this.ui.form.serializeJSON();
// Manipulate
data.block_exploits = !!data.block_exploits;
data.preserve_path = !!data.preserve_path;
if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
data.ssl_forced = true;
}
if (typeof data.http2_support !== 'undefined') {
data.http2_support = !!data.http2_support;
}
data.block_exploits = !!data.block_exploits;
data.preserve_path = !!data.preserve_path;
data.http2_support = !!data.http2_support;
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');

View File

@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({
'nginx/access': 'showNginxAccess',
'nginx/certificates': 'showNginxCertificates',
'audit-log': 'showAuditLog',
'settings': 'showSettings',
'*default': 'showDashboard'
}
});

View File

@ -0,0 +1,53 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="form-label"><%- description %></div>
<div class="custom-controls-stacked">
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="congratulations" type="radio" required <%- value === 'congratulations' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-congratulations') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="404" type="radio" required <%- value === '404' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-404') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="redirect" type="radio" required <%- value === 'redirect' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-redirect') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="html" type="radio" required <%- value === 'html' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-html') %></div>
</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-redirect">
<div class="form-group">
<div class="form-label">Redirect to</div>
<input class="form-control redirect-input" name="meta[redirect]" placeholder="https://" type="url" value="<%- meta && typeof meta.redirect !== 'undefined' ? meta.redirect : '' %>">
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-html">
<div class="form-group">
<div class="form-label">HTML Content</div>
<textarea class="form-control text-monospace html-content" name="meta[html]" rows="6" placeholder="<!-- Enter your HTML here -->"><%- meta && typeof meta.html !== 'undefined' ? meta.html : '' %></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -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()) {
$('<input type="submit">').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');
}
});

View File

@ -0,0 +1,21 @@
<td>
<div><%- name %></div>
<div class="small text-muted">
<%- description %>
</div>
</td>
<td>
<div>
<% if (id === 'default-site') { %>
<%- i18n('settings', 'default-site-' + value) %>
<% } %>
</div>
</td>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
</div>
</div>
</td>

View File

@ -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);
}
});

View File

@ -0,0 +1,8 @@
<thead>
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('str', 'value') %></th>
<th>&nbsp;</th>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -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
}));
}
});

View File

@ -0,0 +1,14 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('settings', 'title') %></h3>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -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');
});
}
});

View File

@ -42,6 +42,9 @@
<li class="nav-item">
<a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a>
</li>
<li class="nav-item">
<a href="/settings" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('settings', 'title') %></a>
</li>
<% } %>
</ul>
</div>

View File

@ -31,7 +31,8 @@
"online": "Online",
"offline": "Offline",
"unknown": "Unknown",
"expires": "Expires"
"expires": "Expires",
"value": "Value"
},
"login": {
"title": "Login to your account"
@ -79,7 +80,16 @@
"no-ssl": "This host will not use HTTPS",
"advanced": "Advanced",
"advanced-warning": "Enter your custom Nginx configuration here at your own risk!",
"advanced-config": "Custom Nginx Configuration"
"advanced-config": "Custom Nginx Configuration",
"hsts-enabled": "HSTS Enabled",
"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",
@ -220,6 +230,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"
}
}
}

View File

@ -14,6 +14,8 @@ const model = Backbone.Model.extend({
certificate_id: 0,
ssl_forced: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
enabled: true,
meta: {},
advanced_config: '',

View File

@ -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
})
}

View File

@ -17,6 +17,8 @@ const model = Backbone.Model.extend({
access_list_id: 0,
certificate_id: 0,
ssl_forced: false,
hsts_enabled: false,
hsts_subdomains: false,
caching_enabled: false,
allow_websocket_upgrade: false,
block_exploits: false,

View File

@ -15,6 +15,8 @@ const model = Backbone.Model.extend({
preserve_path: true,
certificate_id: 0,
ssl_forced: false,
hsts_enabled: false,
hsts_subdomains: false,
block_exploits: false,
http2_support: false,
advanced_config: '',

View File

@ -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
})
};