Compare commits

...

30 Commits
1.0.1 ... 1.1.2

Author SHA1 Message Date
b14804cac7 Version bump 2018-05-09 09:59:39 +10:00
ef5c2c53e3 Updated readme 2018-05-09 09:52:43 +10:00
f86e91bef2 Updated vulnerable dependancy 2018-05-09 09:47:51 +10:00
f4544778a7 Updated vulnerable package 2018-05-09 09:36:47 +10:00
6647c4cbb6 Added docker experimental features 2018-05-08 10:37:26 +10:00
a892f0688e Updated s6-overlay 2018-05-08 08:30:33 +10:00
3612c25ada Fix CI 2018-05-04 14:03:29 +10:00
a66c96ef1b Fix CI 2018-05-04 11:41:50 +10:00
b9c940efa8 Fix CI 2018-05-04 11:38:52 +10:00
2900a0b9e4 Fix CI pipeline 2018-05-04 00:13:36 +10:00
a38da77356 Fix CI pipeline 2018-05-04 00:05:29 +10:00
f1c86a7d49 Fix CI pipeline 2018-05-03 23:46:03 +10:00
4bb63fcf8d Fix CI pipeline 2018-05-03 17:03:34 +10:00
dba7f964d2 Fix CI pipeline 2018-05-03 16:57:10 +10:00
eaf0d27a3b Fix CI pipeline 2018-05-03 16:10:36 +10:00
2d69f287b7 Fix CI pipeline 2018-05-03 16:07:34 +10:00
e1aa975987 Fix CI pipeline 2018-05-03 16:03:53 +10:00
074a8377a3 Fix CI pipeline 2018-05-03 16:00:29 +10:00
c7852125a8 Fix CI pipeline 2018-05-03 15:56:35 +10:00
31ab201175 Added CI pipeline 2018-05-03 15:43:04 +10:00
ab1c5ad382 Added metadata to dockerfile 2018-04-16 12:10:12 +10:00
6428423274 Fix bug with ssl renew 2018-03-18 11:26:15 +10:00
36896bcfc9 Bypass basic auth for letsencrypt acme requests, reload nginx after ssl renewals 2018-03-16 10:53:50 +10:00
b324110c49 Trying something to fix the auto ssl renewal process 2018-03-16 10:32:35 +10:00
b38e239fa2 Bumped version 2018-03-15 15:30:48 +10:00
f60ffd85da Remove spammy ssl renewal process and replace with the system checker and run it every 6 hours 2018-03-15 15:29:36 +10:00
f10d8e4aa9 Merge branch 'master' of github.com:jc21/nginx-proxy-manager 2018-02-16 16:58:13 +10:00
b57d1e5a66 Added Stream forwarding support 2018-02-16 16:57:54 +10:00
001f0cb9f6 Updated readme 2018-02-13 15:51:44 +10:00
d2130a24a1 Fix error when updating hosts 2018-01-05 10:47:44 +10:00
33 changed files with 434 additions and 10253 deletions

View File

@ -1,5 +1,8 @@
FROM jc21/nginx-proxy-manager-base FROM jc21/nginx-proxy-manager-base
MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
ENV SUPPRESS_NO_CONFIG_WARNING=1 ENV SUPPRESS_NO_CONFIG_WARNING=1
ENV S6_FIX_ATTRS_HIDDEN=1 ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
@ -8,7 +11,7 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
COPY rootfs / COPY rootfs /
# s6 overlay # s6 overlay
RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.2.1/s6-overlay-amd64.tar.gz" \ RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.4.0/s6-overlay-amd64.tar.gz" \
&& tar xzf /tmp/s6-overlay-amd64.tar.gz -C / && tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
# App # App

68
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,68 @@
pipeline {
options {
buildDiscarder(logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '', numToKeepStr: '10'))
disableConcurrentBuilds()
}
agent any
environment {
IMAGE_NAME = "nginx-proxy-manager"
TEMP_IMAGE_NAME = "nginx-proxy-manager-build_${BUILD_NUMBER}"
TAG_VERSION = getPackageVersion()
}
stages {
stage('Prepare') {
steps {
sh 'docker pull jc21/$IMAGE_NAME-base'
sh 'docker pull $DOCKER_CI_TOOLS'
}
}
stage('Build') {
steps {
sh 'docker run --rm -v $(pwd)/manager:/srv/manager -w /srv/manager jc21/$IMAGE_NAME-base yarn --registry=$NPM_REGISTRY install'
sh 'docker run --rm -v $(pwd)/manager:/srv/manager -w /srv/manager jc21/$IMAGE_NAME-base gulp build'
sh 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd)/manager:/srv/manager -w /srv/manager jc21/$IMAGE_NAME-base yarn --registry=$NPM_REGISTRY install --prod'
sh 'docker run --rm -v $(pwd)/manager:/data $DOCKER_CI_TOOLS node-prune'
sh 'docker build --squash --compress -t $TEMP_IMAGE_NAME .'
}
}
stage('Publish') {
when {
branch 'master'
}
steps {
sh 'docker tag $TEMP_IMAGE_NAME ${DOCKER_PRIVATE_REGISTRY}/$IMAGE_NAME:latest'
sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/$IMAGE_NAME:latest'
sh 'docker tag $TEMP_IMAGE_NAME ${DOCKER_PRIVATE_REGISTRY}/$IMAGE_NAME:$TAG_VERSION'
sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/$IMAGE_NAME:$TAG_VERSION'
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:latest'
sh 'docker tag $TEMP_IMAGE_NAME docker.io/jc21/$IMAGE_NAME:$TAG_VERSION'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'"
sh 'docker push docker.io/jc21/$IMAGE_NAME:latest'
sh 'docker push docker.io/jc21/$IMAGE_NAME:$TAG_VERSION'
}
}
}
}
triggers {
bitbucketPush()
}
post {
success {
slackSend color: "#72c900", message: "SUCCESS: <${BUILD_URL}|${JOB_NAME}> build #${BUILD_NUMBER} - ${currentBuild.durationString}"
}
failure {
slackSend color: "#d61111", message: "FAILED: <${BUILD_URL}|${JOB_NAME}> build #${BUILD_NUMBER} - ${currentBuild.durationString}"
}
always {
sh 'docker rmi $TEMP_IMAGE_NAME'
}
}
}
def getPackageVersion() {
ver = sh(script: 'docker run --rm -v $(pwd)/manager:/data $DOCKER_CI_TOOLS bash -c "cat /data/package.json|jq -r \'.version\'"', returnStdout: true)
return ver.trim()
}

View File

@ -2,11 +2,9 @@
# Nginx Proxy Manager # Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-1.0.1-green.svg) ![Version](https://img.shields.io/badge/version-1.1.2-green.svg?style=for-the-badge)
![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg) ![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) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge)
![Build Status](http://bamboo.jc21.com/plugins/servlet/wittified/build-status/AB-NPM)
This project comes as a pre-built docker image that enables you to easily forward to your websites This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
@ -25,6 +23,11 @@ running at home or otherwise, including free SSL, without having to know too muc
- Return immediate 404's - Return immediate 404's
## Using [Rancher](https://rancher.com)?
Easily start an Nginx Proxy Manager Stack by adding [my template catalog](https://github.com/jc21/rancher-templates).
## Getting started ## Getting started
### Method 1: Using docker-compose ### Method 1: Using docker-compose
@ -101,9 +104,7 @@ I won't go in to too much detail here but here are the basics for someone new to
## TODO ## TODO
- Pass on human readable ssl cert errors to the ui - Pass on human readable ssl cert errors to the ui
- Allow a host to be a redirection to another domain
- UI: Allow column sorting on tables - UI: Allow column sorting on tables
- UI: Allow filtering hosts by types - UI: Allow filtering hosts by types
- Advanced option to overwrite the default location block (or regex to do it automatically) - Advanced option to overwrite the default location block (or regex to do it automatically)
- Add nice upstream error pages - Add nice upstream error pages

View File

@ -16,5 +16,5 @@ fi
cd "$CODEBASE" cd "$CODEBASE"
docker-compose run --no-deps --rm -w /srv/manager app gulp $@ /usr/local/bin/docker-compose run --no-deps --rm -w /srv/manager app gulp $@
exit $? exit $?

View File

@ -16,5 +16,5 @@ fi
cd "$CODEBASE" cd "$CODEBASE"
docker-compose run --no-deps --rm -w /srv/manager app npm $@ /usr/local/bin/docker-compose run --no-deps --rm -w /srv/manager app npm $@
exit $? exit $?

20
bin/yarn Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if hash realpath 2>/dev/null; then
export CODEBASE=$(realpath $SCRIPT_DIR/..)
elif hash grealpath 2>/dev/null; then
export CODEBASE=$(grealpath $SCRIPT_DIR/..)
else
export CODEBASE=$(readlink -e $SCRIPT_DIR/..)
fi
if [ -z "$CODEBASE" ]; then
echo "Unable to determine absolute codebase directory"
exit 1
fi
cd "$CODEBASE"
/usr/local/bin/docker-compose run --no-deps --rm -w /srv/manager app yarn $@
exit $?

10016
manager/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "nginx-proxy-manager", "name": "nginx-proxy-manager",
"version": "1.0.1", "version": "1.1.2",
"description": "Nginx proxt with built in Web based management", "description": "Nginx proxt with built in Web based management",
"main": "src/backend/index.js", "main": "src/backend/index.js",
"dependencies": { "dependencies": {
@ -38,7 +38,7 @@
"gulp-concat-util": "^0.5.5", "gulp-concat-util": "^0.5.5",
"gulp-ejs": "^3.0.1", "gulp-ejs": "^3.0.1",
"gulp-imagemin": "^3.3.0", "gulp-imagemin": "^3.3.0",
"gulp-sass": "^3.1.0", "gulp-sass": "^4.0.1",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"image-size": "^0.6.1", "image-size": "^0.6.1",
"jquery": "^3.2.1", "jquery": "^3.2.1",

View File

@ -44,19 +44,31 @@ const internalHost = {
*/ */
create: payload => { create: payload => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Enforce lowercase hostnames let existing_host = false;
payload.hostname = payload.hostname.toLowerCase();
// 1. Check that the hostname doesn't already exist if (payload.type === 'stream') {
let existing_host = db.hosts.findOne({hostname: payload.hostname}); // Check that the incoming port doesn't already exist
existing_host = db.hosts.findOne({incoming_port: payload.incoming_port});
if (payload.incoming_port === 80 || payload.incoming_port === 81 || payload.incoming_port === 443) {
reject(new error.ConfigurationError('Port ' + payload.incoming_port + ' is reserved'));
return;
}
} else {
payload.hostname = payload.hostname.toLowerCase();
// Check that the hostname doesn't already exist
existing_host = db.hosts.findOne({hostname: payload.hostname});
}
if (existing_host) { if (existing_host) {
reject(new error.ValidationError('Hostname already exists')); reject(new error.ValidationError('Hostname already exists'));
} else { } else {
// 2. Add host to db // Add host to db
let host = db.hosts.save(payload); let host = db.hosts.save(payload);
// 3. Fire the config generation for this host // Fire the config generation for this host
internalHost.configure(host, true) internalHost.configure(host, true)
.then((/*result*/) => { .then((/*result*/) => {
resolve(host); resolve(host);
@ -98,10 +110,16 @@ const internalHost = {
} }
// Check that the hostname doesn't already exist // Check that the hostname doesn't already exist
let other_host = db.hosts.findOne({hostname: payload.hostname}); let other_host = false;
if (typeof payload.incoming_port !== 'undefined') {
other_host = db.hosts.findOne({incoming_port: payload.incoming_port});
} else {
other_host = db.hosts.findOne({hostname: payload.hostname});
}
if (other_host && other_host._id !== id) { if (other_host && other_host._id !== id) {
reject(new error.ValidationError('Hostname already exists')); reject(new error.ValidationError((other_host.type === 'stream' ? 'Source Stream Port' : 'Hostname') + ' already exists'));
} else { } else {
// 2. Update host // 2. Update host
db.hosts.update({_id: id}, payload, {multi: false, upsert: false}); db.hosts.update({_id: id}, payload, {multi: false, upsert: false});
@ -126,17 +144,17 @@ const internalHost = {
return data; return data;
}) })
.then(data => { .then(data => {
if ( if (data.updated.type !== 'stream') {
(data.original.ssl && !data.updated.ssl) || // ssl was enabled and is now disabled if (
(data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host (data.original.ssl && !data.updated.ssl) || // ssl was enabled and is now disabled
) { (data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host
// SSL was turned off or hostname for ssl has changed so we should remove certs for the original ) {
return internalSsl.deleteCerts(data.original) // SSL was turned off or hostname for ssl has changed so we should remove certs for the original
.then(() => { return internalSsl.deleteCerts(data.original)
db.hosts.update({_id: data.updated._id}, {ssl_expires: 0}, {multi: false, upsert: false}); .then(() => {
data.updated.ssl_expires = 0; return data;
return data; });
}); }
} }
return data; return data;

View File

@ -32,6 +32,10 @@ const internalNginx = {
* @returns {String} * @returns {String}
*/ */
getConfigName: host => { getConfigName: host => {
if (host.type === 'stream') {
return '/config/nginx/stream/' + host.incoming_port + '.conf';
}
return '/config/nginx/' + host.hostname + '.conf'; return '/config/nginx/' + host.hostname + '.conf';
}, },

View File

@ -1,13 +1,10 @@
'use strict'; 'use strict';
const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const ejs = require('ejs'); const ejs = require('ejs');
const timestamp = require('unix-timestamp'); const timestamp = require('unix-timestamp');
const batchflow = require('batchflow');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const logger = require('../logger'); const logger = require('../logger');
const db = require('../db');
const utils = require('../lib/utils'); const utils = require('../lib/utils');
const error = require('../lib/error'); const error = require('../lib/error');
@ -15,7 +12,7 @@ timestamp.round = true;
const internalSsl = { const internalSsl = {
interval_timeout: 60 * 1000, interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null, interval: null,
interval_processing: false, interval_processing: false,
@ -28,42 +25,22 @@ const internalSsl = {
*/ */
processExpiringHosts: () => { processExpiringHosts: () => {
if (!internalSsl.interval_processing) { if (!internalSsl.interval_processing) {
let hosts = db.hosts.find(); logger.info('Renewing SSL certs close to expiry...');
return utils.exec('/usr/bin/certbot renew -q')
.then(result => {
logger.info(result);
internalSsl.interval_processing = false;
if (hosts && hosts.length) { return internalNginx.reload()
internalSsl.interval_processing = true; .then(() => {
logger.info('Renew Complete');
batchflow(hosts).sequential() return result;
.each((i, host, next) => { });
if ((typeof host.is_deleted === 'undefined' || !host.is_deleted) && host.ssl && typeof host.ssl_expires !== 'undefined' && !internalSsl.hasValidSslCerts(host)) { })
// This host is due to expire in 1 day, time to renew .catch(err => {
logger.info('Host ' + host.hostname + ' is due for SSL renewal'); logger.error(err);
internalSsl.interval_processing = false;
internalSsl.renewSsl(host) });
.then(() => {
// Certificate was requested ok, update the timestamp on the host
db.hosts.update({_id: host._id}, {ssl_expires: timestamp.now('+90d')}, {
multi: false,
upsert: false
});
})
.then(next)
.catch(err => {
logger.error(err);
next(err);
});
} else {
next();
}
})
.error(err => {
logger.error(err);
internalSsl.interval_processing = false;
})
.end((/*results*/) => {
internalSsl.interval_processing = false;
});
}
} }
}, },
@ -73,8 +50,7 @@ const internalSsl = {
*/ */
hasValidSslCerts: host => { hasValidSslCerts: host => {
return fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/fullchain.pem') && return fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/fullchain.pem') &&
fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/privkey.pem') && fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/privkey.pem');
host.ssl_expires > timestamp.now('+1d');
}, },
/** /**
@ -84,7 +60,7 @@ const internalSsl = {
requestSsl: host => { requestSsl: host => {
logger.info('Requesting SSL certificates for ' + host.hostname); logger.info('Requesting SSL certificates for ' + host.hostname);
return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot --webroot-path=' + host.root_path + ' -d "' + host.hostname + '"') return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot -d "' + host.hostname + '"')
.then(result => { .then(result => {
logger.info(result); logger.info(result);
return result; return result;
@ -98,7 +74,7 @@ const internalSsl = {
renewSsl: host => { renewSsl: host => {
logger.info('Renewing SSL certificates for ' + host.hostname); logger.info('Renewing SSL certificates for ' + host.hostname);
return utils.exec('/usr/bin/letsencrypt renew --force-renewal --disable-hook-validation --cert-name "' + host.hostname + '"') return utils.exec('/usr/bin/certbot renew --force-renewal --disable-hook-validation --cert-name "' + host.hostname + '"')
.then(result => { .then(result => {
logger.info(result); logger.info(result);
return result; return result;
@ -112,7 +88,7 @@ const internalSsl = {
deleteCerts: host => { deleteCerts: host => {
logger.info('Deleting SSL certificates for ' + host.hostname); logger.info('Deleting SSL certificates for ' + host.hostname);
return utils.exec('/usr/bin/letsencrypt delete -n --cert-name "' + host.hostname + '"') return utils.exec('/usr/bin/certbot delete -n --cert-name "' + host.hostname + '"')
.then(result => { .then(result => {
logger.info(result); logger.info(result);
}) })
@ -130,20 +106,17 @@ const internalSsl = {
let filename = internalNginx.getConfigName(host); let filename = internalNginx.getConfigName(host);
let template_data = host; let template_data = host;
template_data.root_path = '/tmp/' + host.hostname; return new Promise((resolve, reject) => {
try {
template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf.ejs', {encoding: 'utf8'});
let config_text = ejs.render(template, template_data);
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
return utils.exec('mkdir -p ' + template_data.root_path) resolve(template_data);
.then(() => { } catch (err) {
try { reject(new error.ConfigurationError(err.message));
template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf.ejs', {encoding: 'utf8'}); }
let config_text = ejs.render(template, template_data); });
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
return template_data;
} catch (err) {
throw new error.ConfigurationError(err.message);
}
});
}, },
/** /**
@ -157,10 +130,6 @@ const internalSsl = {
.then(() => { .then(() => {
return internalSsl.requestSsl(data); return internalSsl.requestSsl(data);
}); });
})
.then(() => {
// Certificate was requested ok, update the timestamp on the host
db.hosts.update({_id: host._id}, {ssl_expires: timestamp.now('+90d')}, {multi: false, upsert: false});
}); });
} }
}; };

View File

@ -30,7 +30,7 @@ function apiValidator (schema, payload/*, description*/) {
resolve(payload); resolve(payload);
} else { } else {
let message = ajv.errorsText(validate.errors); let message = ajv.errorsText(validate.errors);
//debug(validate.errors); //console.log(validate.errors);
let err = new error.ValidationError(message); let err = new error.ValidationError(message);
err.debug = [validate.errors, payload]; err.debug = [validate.errors, payload];

View File

@ -152,38 +152,4 @@ router
.catch(next); .catch(next);
}); });
/**
* Renew Host Action
*
* /api/hosts/123/renew
*/
router
.route('/:host_id/renew')
.options((req, res) => {
res.sendStatus(204);
})
/**
* POST /api/hosts/123/renew
*/
.post((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/_id'
}
}
}, req.params)
.then(data => {
return internalHost.renew(data.host_id);
})
.then(result => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router; module.exports = router;

View File

@ -12,7 +12,7 @@
}, },
"type": { "type": {
"type": "string", "type": "string",
"pattern": "^(proxy|redirection|404)$" "pattern": "^(proxy|redirection|404|stream)$"
}, },
"hostname": { "hostname": {
"$ref": "../definitions.json#/definitions/hostname" "$ref": "../definitions.json#/definitions/hostname"
@ -38,11 +38,6 @@
"ssl": { "ssl": {
"type": "boolean" "type": "boolean"
}, },
"ssl_expires": {
"type": "integer",
"minimum": 0,
"readonly": true
},
"letsencrypt_email": { "letsencrypt_email": {
"type": "string", "type": "string",
"format": "email" "format": "email"
@ -59,6 +54,17 @@
"access_list": { "access_list": {
"type": "object", "type": "object",
"readonly": true "readonly": true
},
"incoming_port": {
"type": "integer",
"minumum": 1,
"maxiumum": 65535
},
"protocols": {
"type": "array",
"items": {
"type": "string"
}
} }
}, },
"links": [ "links": [
@ -86,8 +92,7 @@
"schema": { "schema": {
"type": "object", "type": "object",
"required": [ "required": [
"type", "type"
"hostname"
], ],
"properties": { "properties": {
"type": { "type": {
@ -125,6 +130,12 @@
}, },
"access_list_id": { "access_list_id": {
"$ref": "#/definitions/access_list_id" "$ref": "#/definitions/access_list_id"
},
"incoming_port": {
"$ref": "#/definitions/incoming_port"
},
"protocols": {
"$ref": "#/definitions/protocols"
} }
} }
}, },
@ -146,6 +157,9 @@
"required": [], "required": [],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": {
"$ref": "#/definitions/type"
},
"hostname": { "hostname": {
"$ref": "#/definitions/hostname" "$ref": "#/definitions/hostname"
}, },
@ -178,6 +192,12 @@
}, },
"access_list_id": { "access_list_id": {
"$ref": "#/definitions/access_list_id" "$ref": "#/definitions/access_list_id"
},
"incoming_port": {
"$ref": "#/definitions/incoming_port"
},
"protocols": {
"$ref": "#/definitions/protocols"
} }
} }
}, },
@ -227,9 +247,6 @@
"ssl": { "ssl": {
"$ref": "#/definitions/ssl" "$ref": "#/definitions/ssl"
}, },
"ssl_expires": {
"$ref": "#/definitions/ssl_expires"
},
"letsencrypt_email": { "letsencrypt_email": {
"$ref": "#/definitions/letsencrypt_email" "$ref": "#/definitions/letsencrypt_email"
}, },
@ -244,6 +261,12 @@
}, },
"advanced": { "advanced": {
"$ref": "#/definitions/advanced" "$ref": "#/definitions/advanced"
},
"incoming_port": {
"$ref": "#/definitions/incoming_port"
},
"protocols": {
"$ref": "#/definitions/protocols"
} }
} }
} }

View File

@ -6,6 +6,6 @@ server {
access_log /config/logs/letsencrypt.log proxy; access_log /config/logs/letsencrypt.log proxy;
location / { location / {
root <%- root_path %>; root /config/letsencrypt-acme-challenge;
} }
} }

View File

@ -14,19 +14,19 @@ server {
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %> <%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
<% if (typeof ssl !== 'undefined' && ssl) { -%> <% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-ciphers.conf; include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%> <% } -%>
<% if (typeof access_list_id !== 'undefined' && access_list_id) { -%>
auth_basic "Authorization required";
auth_basic_user_file /config/access/<%- access_list_id %>;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> <%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
location / { location / {
<% if (typeof access_list_id !== 'undefined' && access_list_id) { -%>
auth_basic "Authorization required";
auth_basic_user_file /config/access/<%- access_list_id %>;
<% } -%>
<%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %> <%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %>
include conf.d/include/proxy.conf; include conf.d/include/proxy.conf;
} }

View File

@ -0,0 +1,11 @@
# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %>
<%
protocols.forEach(function (protocol) {
%>
server {
listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>;
proxy_pass <%- forward_server %>:<%- forward_port %>;
}
<%
});
%>

View File

@ -118,14 +118,6 @@ module.exports = {
*/ */
reconfigure: function (_id) { reconfigure: function (_id) {
return fetch('post', 'hosts/' + _id + '/reconfigure'); return fetch('post', 'hosts/' + _id + '/reconfigure');
},
/**
* @param {String} _id
* @returns {Promise}
*/
renew: function (_id) {
return fetch('post', 'hosts/' + _id + '/renew');
} }
}, },

View File

@ -86,6 +86,17 @@ module.exports = {
}); });
}, },
/**
* Show Stream Host Form
*
* @param model
*/
showStreamHostForm: function (model) {
require(['./main', './host/stream_form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
},
/** /**
* Show Delete Host Confirmation * Show Delete Host Confirmation
* *
@ -108,17 +119,6 @@ module.exports = {
}); });
}, },
/**
* Show Renew Host
*
* @param model
*/
showRenewHost: function (model) {
require(['./main', './host/renew'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
},
/** /**
* Show Advanced Host * Show Advanced Host
* *

View File

@ -1,6 +1,6 @@
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<thead> <thead>
<th>Hostname</th> <th>Source</th>
<th>Destination</th> <th>Destination</th>
<th>SSL</th> <th>SSL</th>
<th>Access List</th> <th>Access List</th>
@ -13,6 +13,7 @@
<li><a href="#" class="new-proxy">Proxy Host</a></li> <li><a href="#" class="new-proxy">Proxy Host</a></li>
<li><a href="#" class="new-redirection">Redirection Host</a></li> <li><a href="#" class="new-redirection">Redirection Host</a></li>
<li><a href="#" class="new-404">404 Host</a></li> <li><a href="#" class="new-404">404 Host</a></li>
<li><a href="#" class="new-stream">Stream Host</a></li>
</ul> </ul>
</div> </div>
</th> </th>

View File

@ -28,7 +28,8 @@ module.exports = Mn.View.extend({
ui: { ui: {
new_proxy: 'th .new-proxy', new_proxy: 'th .new-proxy',
new_redirection: 'th .new-redirection', new_redirection: 'th .new-redirection',
new_404: 'th .new-404' new_404: 'th .new-404',
new_stream: 'th .new-stream'
}, },
events: { events: {
@ -45,6 +46,11 @@ module.exports = Mn.View.extend({
'click @ui.new_404': function (e) { 'click @ui.new_404': function (e) {
e.preventDefault(); e.preventDefault();
Controller.show404HostForm(new HostModel.Model); Controller.show404HostForm(new HostModel.Model);
},
'click @ui.new_stream': function (e) {
e.preventDefault();
Controller.showStreamHostForm(new HostModel.Model);
} }
}, },

View File

@ -1,7 +1,14 @@
<td><a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a></td> <td>
<% if (type === 'stream') { %>
<%- incoming_port %>
<%- protocols.join(', ').toUpperCase() %>
<% } else { %>
<a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a>
<% } %>
</td>
<td> <td>
<span class="monospace"> <span class="monospace">
<% if (type === 'proxy') { %> <% if (type === 'proxy' || type === 'stream') { %>
<%- forward_server %>:<%- forward_port %> <%- forward_server %>:<%- forward_port %>
<% } else if (type === 'redirection') { %> <% } else if (type === 'redirection') { %>
<%- forward_host %> <%- forward_host %>
@ -11,27 +18,32 @@
</span> </span>
</td> </td>
<td> <td>
<% if (ssl && force_ssl) { %> <% if (type === 'stream') { %>
Forced -
<% } else if (ssl) { %>
Enabled
<% } else { %> <% } else { %>
No <% if (ssl && force_ssl) { %>
Forced
<% } else if (ssl) { %>
Enabled
<% } else { %>
No
<% } %>
<% } %> <% } %>
</td> </td>
<td> <td>
<% if (access_list) { %> <% if (type === 'stream') { %>
<a href="#" class="access_list"><%- access_list.name %></a> -
<% } else { %> <% } else { %>
<em>None</em> <% if (access_list) { %>
<a href="#" class="access_list"><%- access_list.name %></a>
<% } else { %>
<em>None</em>
<% } %>
<% } %> <% } %>
</td> </td>
<td class="text-right"> <td class="text-right">
<% if (ssl) { %>
<button type="button" class="btn btn-default btn-xs renew" title="Renew SSL"><i class="fa fa-shield" aria-hidden="true"></i></button>
<% } %>
<button type="button" class="btn btn-default btn-xs reconfigure" title="Reconfigure Nginx"><i class="fa fa-refresh" aria-hidden="true"></i></button> <button type="button" class="btn btn-default btn-xs reconfigure" title="Reconfigure Nginx"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"><i class="fa fa-code" aria-hidden="true"></i></button> <button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"<%- type === 'stream' ? ' disabled' : '' %>><i class="fa fa-code" aria-hidden="true"></i></button>
<button type="button" class="btn btn-warning btn-xs edit" title="Edit"><i class="fa fa-pencil" aria-hidden="true"></i></button> <button type="button" class="btn btn-warning btn-xs edit" title="Edit"><i class="fa fa-pencil" aria-hidden="true"></i></button>
<button type="button" class="btn btn-danger btn-xs delete" title="Delete"><i class="fa fa-times" aria-hidden="true"></i></button> <button type="button" class="btn btn-danger btn-xs delete" title="Delete"><i class="fa fa-times" aria-hidden="true"></i></button>
</td> </td>

View File

@ -15,7 +15,6 @@ module.exports = Mn.View.extend({
delete: 'button.delete', delete: 'button.delete',
access_list: 'a.access_list', access_list: 'a.access_list',
reconfigure: 'button.reconfigure', reconfigure: 'button.reconfigure',
renew: 'button.renew',
advanced: 'button.advanced' advanced: 'button.advanced'
}, },
@ -32,6 +31,9 @@ module.exports = Mn.View.extend({
case '404': case '404':
Controller.show404HostForm(this.model); Controller.show404HostForm(this.model);
break; break;
case 'stream':
Controller.showStreamHostForm(this.model);
break;
} }
}, },
@ -50,11 +52,6 @@ module.exports = Mn.View.extend({
Controller.showReconfigureHost(this.model); Controller.showReconfigureHost(this.model);
}, },
'click @ui.renew': function (e) {
e.preventDefault();
Controller.showRenewHost(this.model);
},
'click @ui.advanced': function (e) { 'click @ui.advanced': function (e) {
e.preventDefault(); e.preventDefault();
Controller.showAdvancedHost(this.model); Controller.showAdvancedHost(this.model);

View File

@ -1,17 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title">Renew SSL Certificates</h4>
</div>
<div class="modal-body">
<p>This will renew the SSL Certificates for the host. This normally happens automatically however if you notice
SSL working incorrectly, this may fix it.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success renew">Renew SSL</button>
</div>
</form>
</div>
</div>

View File

@ -1,33 +0,0 @@
'use strict';
import Mn from 'backbone.marionette';
const template = require('./renew.ejs');
const Api = require('../api');
const App = require('../main');
module.exports = Mn.View.extend({
template: template,
ui: {
buttons: 'form button',
renew: 'button.renew'
},
events: {
'click @ui.renew': function (e) {
e.preventDefault();
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
Api.Hosts.renew(this.model.get('_id'))
.then((/*result*/) => {
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,55 @@
<div class="modal-dialog">
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> Stream Host</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
A Stream Host will forward a TCP/UDP connection directly to a another server on your network. <strong>There is no authentication.</strong>
Note you will also have to open the incoming port in your docker configuration for this to work.
<br>
<br>
You will not be able to use port <strong>80</strong>, <strong>81</strong> or <strong>443</strong> or any other previously configured Stream Host incoming port.
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Incoming Port</label>
<div class="col-sm-8">
<input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="incoming_port" value="<%- incoming_port ? incoming_port : '' %>" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Forwarding IP</label>
<div class="col-sm-8">
<input type="text" class="form-control" placeholder="192.168.0.1" name="forward_server" value="<%- forward_server %>" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Forwarding Port</label>
<div class="col-sm-8">
<input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="forward_port" value="<%- typeof _id === 'undefined' ? '' : forward_port %>" required>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="protocols[]" value="tcp"<%- typeof _id === 'undefined' || hasStreamProtocol('tcp') ? ' checked' : '' %>> TCP Forwarding
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="protocols[]" value="udp"<%- hasStreamProtocol('udp') ? ' checked' : '' %>> UDP Forwarding
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success save">Save</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,63 @@
'use strict';
import Mn from 'backbone.marionette';
const _ = require('lodash');
const template = require('./stream_form.ejs');
const Controller = require('../controller');
const Api = require('../api');
const App = require('../main');
require('jquery-serializejson');
module.exports = Mn.View.extend({
template: template,
ui: {
form: 'form',
buttons: 'form button'
},
events: {
'submit @ui.form': function (e) {
e.preventDefault();
let data = _.extend({}, this.ui.form.serializeJSON());
data.type = 'stream';
// Ports are integers
data.incoming_port = parseInt(data.incoming_port, 10);
data.forward_port = parseInt(data.forward_port, 10);
if (typeof data.protocols === 'undefined' || !data.protocols.length) {
alert('You must select one or more Protocols');
return;
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
let method = Api.Hosts.create;
if (this.model.get('_id')) {
// edit
method = Api.Hosts.update;
data._id = this.model.get('_id');
}
method(data)
.then((/*result*/) => {
App.UI.closeModal();
Controller.showDashboard();
})
.catch((err) => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
templateContext: {
hasStreamProtocol: function (protocol) {
return this.protocols.indexOf(protocol) !== -1;
}
}
});

View File

@ -20,7 +20,9 @@ const model = Backbone.Model.extend({
letsencrypt_email: '', letsencrypt_email: '',
accept_tos: false, accept_tos: false,
access_list_id: '', access_list_id: '',
advanced: '' advanced: '',
incoming_port: 0,
protocols: []
}; };
} }
}); });

View File

@ -0,0 +1,26 @@
# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel
# other regex checks, because in our other config files have regex rule that denies access to files with dotted names.
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
# Set correct content type. According to this:
# https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29
# Current specification requires "text/plain" or no content header at all.
# It seems that "text/plain" is a safe option.
default_type "text/plain";
# This directory must be the same as in /etc/letsencrypt/cli.ini
# as "webroot-path" parameter. Also don't forget to set "authenticator" parameter
# there to "webroot".
# Do NOT use alias, use root! Target directory is located here:
# /var/www/common/letsencrypt/.well-known/acme-challenge/
root /config/letsencrypt-acme-challenge;
}
# Hide /acme-challenge subdirectory and return 404 on all requests.
# It is somewhat more secure than letting Nginx return 403.
# Ending slash is important!
location = /.well-known/acme-challenge/ {
return 404;
}

View File

@ -53,3 +53,7 @@ http {
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /config/nginx/*.conf; include /config/nginx/*.conf;
} }
stream {
include /config/nginx/stream/*.conf;
}

View File

@ -1,4 +1,6 @@
#!/usr/bin/with-contenv bash #!/usr/bin/with-contenv bash
mkdir -p /config/letsencrypt-acme-challenge
cd /srv/manager cd /srv/manager
node --abort_on_uncaught_exception --max_old_space_size=250 /srv/manager/src/backend/index.js node --abort_on_uncaught_exception --max_old_space_size=250 /srv/manager/src/backend/index.js

View File

@ -1,5 +1,5 @@
#!/usr/bin/with-contenv bash #!/usr/bin/with-contenv bash
mkdir -p /tmp/nginx /config/{nginx,logs,access} /var/lib/nginx/cache/{public,private} mkdir -p /tmp/nginx /config/{nginx,logs,access} /config/nginx/stream /var/lib/nginx/cache/{public,private}
chown root /tmp/nginx chown root /tmp/nginx
exec nginx exec nginx

View File

@ -0,0 +1,4 @@
text = True
non-interactive = True
authenticator = webroot
webroot-path = /config/letsencrypt-acme-challenge