diff --git a/.version b/.version index d8b698973..e4643748f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.12.0 +2.12.6 diff --git a/Jenkinsfile b/Jenkinsfile index 9b29ee970..af913c2e0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -43,7 +43,7 @@ pipeline { steps { script { // Defaults to the Branch name, which is applies to all branches AND pr's - buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" + buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}" } } } @@ -128,7 +128,7 @@ pipeline { sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } unstable { - dir(path: 'testing/results') { + dir(path: 'test/results') { archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } } @@ -161,7 +161,45 @@ pipeline { sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } unstable { - dir(path: 'testing/results') { + dir(path: 'test/results') { + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') + } + } + } + } + stage('Test Postgres') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml' + } + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + steps { + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + } + post { + always { + // Dumps to analyze later + sh 'mkdir -p debug/postgres' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1' + + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' + } + unstable { + dir(path: 'test/results') { archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } } @@ -203,7 +241,18 @@ pipeline { } steps { script { - npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true) + npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): +``` +nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} +``` + +> [!NOTE] +> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. +> This is a different docker image namespace than the official image. + +> [!WARNING] +> Changes and additions to DNS Providers require verification by at least 2 members of the community! +""", true) } } } diff --git a/README.md b/README.md index 2d1b8da5b..2116a55ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-
+
diff --git a/backend/index.js b/backend/index.js
index 551378251..d334a7c2d 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -3,6 +3,8 @@
const schema = require('./schema');
const logger = require('./logger').global;
+const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false';
+
async function appStart () {
const migrate = require('./migrate');
const setup = require('./setup');
@@ -13,7 +15,16 @@ async function appStart () {
return migrate.latest()
.then(setup)
.then(schema.getCompiledSchema)
- .then(internalIpRanges.fetch)
+ .then(() => {
+ if (IP_RANGES_FETCH_ENABLED) {
+ logger.info('IP Ranges fetch is enabled');
+ return internalIpRanges.fetch().catch((err) => {
+ logger.error('IP Ranges fetch failed, continuing anyway:', err.message);
+ });
+ } else {
+ logger.info('IP Ranges fetch is disabled by environment variable');
+ }
+ })
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js
index 72326be68..f6043e18b 100644
--- a/backend/internal/access-list.js
+++ b/backend/internal/access-list.js
@@ -81,7 +81,7 @@ const internalAccessList = {
return internalAccessList.build(row)
.then(() => {
- if (row.proxy_host_count) {
+ if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
@@ -223,7 +223,7 @@ const internalAccessList = {
.then((row) => {
return internalAccessList.build(row)
.then(() => {
- if (row.proxy_host_count) {
+ if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
}).then(internalNginx.reload)
@@ -252,9 +252,13 @@ const internalAccessList = {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
- .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
+ .leftJoin('proxy_host', function() {
+ this.on('proxy_host.access_list_id', '=', 'access_list.id')
+ .andOn('proxy_host.is_deleted', '=', 0);
+ })
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
+ .groupBy('access_list.id')
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();
@@ -373,7 +377,10 @@ const internalAccessList = {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
- .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
+ .leftJoin('proxy_host', function() {
+ this.on('proxy_host.access_list_id', '=', 'access_list.id')
+ .andOn('proxy_host.is_deleted', '=', 0);
+ })
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.allowGraph('[owner,items,clients]')
@@ -501,8 +508,13 @@ const internalAccessList = {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
- utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
- .then((/*result*/) => {
+ utils.execFile('openssl', ['passwd', '-apr1', item.password])
+ .then((res) => {
+ try {
+ fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'});
+ } catch (err) {
+ reject(err);
+ }
next();
})
.catch((err) => {
diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js
index cb48261b4..60bdd2efa 100644
--- a/backend/internal/audit-log.js
+++ b/backend/internal/audit-log.js
@@ -1,5 +1,6 @@
-const error = require('../lib/error');
-const auditLogModel = require('../models/audit-log');
+const error = require('../lib/error');
+const auditLogModel = require('../models/audit-log');
+const {castJsonIfNeed} = require('../lib/helpers');
const internalAuditLog = {
@@ -22,9 +23,9 @@ const internalAuditLog = {
.allowGraph('[user]');
// Query is used for searching
- if (typeof search_query === 'string') {
+ if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
- this.where('meta', 'like', '%' + search_query + '%');
+ this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%');
});
}
diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js
index 34b8fdf5a..f2e845a24 100644
--- a/backend/internal/certificate.js
+++ b/backend/internal/certificate.js
@@ -313,6 +313,9 @@ const internalCertificate = {
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
+ .allowGraph('[proxy_hosts]')
+ .allowGraph('[redirection_hosts]')
+ .allowGraph('[dead_hosts]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -464,6 +467,9 @@ const internalCertificate = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
+ .allowGraph('[proxy_hosts]')
+ .allowGraph('[redirection_hosts]')
+ .allowGraph('[dead_hosts]')
.orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') {
diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js
index e672775eb..6bbdf61be 100644
--- a/backend/internal/dead-host.js
+++ b/backend/internal/dead-host.js
@@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
+const {castJsonIfNeed} = require('../lib/helpers');
function omissions () {
return ['is_deleted'];
@@ -409,16 +410,16 @@ const internalDeadHost = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
- .orderBy('domain_names', 'ASC');
+ .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
- if (typeof search_query === 'string') {
+ if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
- this.where('domain_names', 'like', '%' + search_query + '%');
+ this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%');
});
}
diff --git a/backend/internal/host.js b/backend/internal/host.js
index 58e1d09a4..52c6d2bda 100644
--- a/backend/internal/host.js
+++ b/backend/internal/host.js
@@ -2,6 +2,7 @@ const _ = require('lodash');
const proxyHostModel = require('../models/proxy_host');
const redirectionHostModel = require('../models/redirection_host');
const deadHostModel = require('../models/dead_host');
+const {castJsonIfNeed} = require('../lib/helpers');
const internalHost = {
@@ -17,7 +18,7 @@ const internalHost = {
cleanSslHstsData: function (data, existing_data) {
existing_data = existing_data === undefined ? {} : existing_data;
- let combined_data = _.assign({}, existing_data, data);
+ const combined_data = _.assign({}, existing_data, data);
if (!combined_data.certificate_id) {
combined_data.ssl_forced = false;
@@ -73,7 +74,7 @@ const internalHost = {
* @returns {Promise}
*/
getHostsWithDomains: function (domain_names) {
- let promises = [
+ const promises = [
proxyHostModel
.query()
.where('is_deleted', 0),
@@ -125,19 +126,19 @@ const internalHost = {
* @returns {Promise}
*/
isHostnameTaken: function (hostname, ignore_type, ignore_id) {
- let promises = [
+ const promises = [
proxyHostModel
.query()
.where('is_deleted', 0)
- .andWhere('domain_names', 'like', '%' + hostname + '%'),
+ .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
redirectionHostModel
.query()
.where('is_deleted', 0)
- .andWhere('domain_names', 'like', '%' + hostname + '%'),
+ .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
deadHostModel
.query()
.where('is_deleted', 0)
- .andWhere('domain_names', 'like', '%' + hostname + '%')
+ .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%')
];
return Promise.all(promises)
diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js
index 77933e733..5f802c004 100644
--- a/backend/internal/nginx.js
+++ b/backend/internal/nginx.js
@@ -181,7 +181,9 @@ const internalNginx = {
* @param {Object} host
* @returns {Promise}
*/
- generateConfig: (host_type, host) => {
+ generateConfig: (host_type, host_row) => {
+ // Prevent modifying the original object:
+ let host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
if (config.debug()) {
diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js
index 61ac8b8c7..32f2bc0dc 100644
--- a/backend/internal/proxy-host.js
+++ b/backend/internal/proxy-host.js
@@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
+const {castJsonIfNeed} = require('../lib/helpers');
function omissions () {
return ['is_deleted', 'owner.is_deleted'];
@@ -416,16 +417,16 @@ const internalProxyHost = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,access_list,certificate]')
- .orderBy('domain_names', 'ASC');
+ .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
- if (typeof search_query === 'string') {
+ if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
- this.where('domain_names', 'like', '%' + search_query + '%');
+ this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
});
}
diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js
index 41ff5b093..6a81b8662 100644
--- a/backend/internal/redirection-host.js
+++ b/backend/internal/redirection-host.js
@@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
+const {castJsonIfNeed} = require('../lib/helpers');
function omissions () {
return ['is_deleted'];
@@ -409,16 +410,16 @@ const internalRedirectionHost = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
- .orderBy('domain_names', 'ASC');
+ .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
- if (typeof search_query === 'string') {
+ if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
- this.where('domain_names', 'like', '%' + search_query + '%');
+ this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
});
}
diff --git a/backend/internal/stream.js b/backend/internal/stream.js
index ee88d46fc..50ce08324 100644
--- a/backend/internal/stream.js
+++ b/backend/internal/stream.js
@@ -1,12 +1,15 @@
-const _ = require('lodash');
-const error = require('../lib/error');
-const utils = require('../lib/utils');
-const streamModel = require('../models/stream');
-const internalNginx = require('./nginx');
-const internalAuditLog = require('./audit-log');
+const _ = require('lodash');
+const error = require('../lib/error');
+const utils = require('../lib/utils');
+const streamModel = require('../models/stream');
+const internalNginx = require('./nginx');
+const internalAuditLog = require('./audit-log');
+const internalCertificate = require('./certificate');
+const internalHost = require('./host');
+const {castJsonIfNeed} = require('../lib/helpers');
function omissions () {
- return ['is_deleted'];
+ return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
}
const internalStream = {
@@ -17,6 +20,12 @@ const internalStream = {
* @returns {Promise}
*/
create: (access, data) => {
+ const create_certificate = data.certificate_id === 'new';
+
+ if (create_certificate) {
+ delete data.certificate_id;
+ }
+
return access.can('streams:create', data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
@@ -26,16 +35,44 @@ const internalStream = {
data.meta = {};
}
+ // streams aren't routed by domain name so don't store domain names in the DB
+ let data_no_domains = structuredClone(data);
+ delete data_no_domains.domain_names;
+
return streamModel
.query()
- .insertAndFetch(data)
+ .insertAndFetch(data_no_domains)
.then(utils.omitRow(omissions()));
})
+ .then((row) => {
+ if (create_certificate) {
+ return internalCertificate.createQuickCertificate(access, data)
+ .then((cert) => {
+ // update host with cert id
+ return internalStream.update(access, {
+ id: row.id,
+ certificate_id: cert.id
+ });
+ })
+ .then(() => {
+ return row;
+ });
+ } else {
+ return row;
+ }
+ })
+ .then((row) => {
+ // re-fetch with cert
+ return internalStream.get(access, {
+ id: row.id,
+ expand: ['certificate', 'owner']
+ });
+ })
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, 'stream', row)
.then(() => {
- return internalStream.get(access, {id: row.id, expand: ['owner']});
+ return row;
});
})
.then((row) => {
@@ -59,6 +96,12 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
+ const create_certificate = data.certificate_id === 'new';
+
+ if (create_certificate) {
+ delete data.certificate_id;
+ }
+
return access.can('streams:update', data.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
@@ -70,16 +113,32 @@ const internalStream = {
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
+ if (create_certificate) {
+ return internalCertificate.createQuickCertificate(access, {
+ 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;
+ })
+ .then(() => {
+ return row;
+ });
+ } else {
+ return row;
+ }
+ })
+ .then((row) => {
+ // 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);
+
return streamModel
.query()
.patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()))
- .then((saved_row) => {
- return internalNginx.configure(streamModel, 'stream', saved_row)
- .then(() => {
- return internalStream.get(access, {id: row.id, expand: ['owner']});
- });
- })
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
@@ -92,6 +151,17 @@ const internalStream = {
return saved_row;
});
});
+ })
+ .then(() => {
+ return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
+ .then((row) => {
+ return internalNginx.configure(streamModel, 'stream', row)
+ .then((new_meta) => {
+ row.meta = new_meta;
+ row = internalHost.cleanRowCertificateMeta(row);
+ return _.omit(row, omissions());
+ });
+ });
});
},
@@ -114,7 +184,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
- .allowGraph('[owner]')
+ .allowGraph('[owner,certificate]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -131,6 +201,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
+ row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
@@ -196,14 +267,14 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
- expand: ['owner']
+ expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
- throw new error.ValidationError('Host is already enabled');
+ throw new error.ValidationError('Stream is already enabled');
}
row.enabled = 1;
@@ -249,7 +320,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
- throw new error.ValidationError('Host is already disabled');
+ throw new error.ValidationError('Stream is already disabled');
}
row.enabled = 0;
@@ -293,11 +364,11 @@ const internalStream = {
getAll: (access, expand, search_query) => {
return access.can('streams:list')
.then((access_data) => {
- let query = streamModel
+ const query = streamModel
.query()
.where('is_deleted', 0)
.groupBy('id')
- .allowGraph('[owner]')
+ .allowGraph('[owner,certificate]')
.orderBy('incoming_port', 'ASC');
if (access_data.permission_visibility !== 'all') {
@@ -305,9 +376,9 @@ const internalStream = {
}
// Query is used for searching
- if (typeof search_query === 'string') {
+ if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
- this.where('incoming_port', 'like', '%' + search_query + '%');
+ this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`);
});
}
@@ -316,6 +387,13 @@ const internalStream = {
}
return query.then(utils.omitRows(omissions()));
+ })
+ .then((rows) => {
+ if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
+ return internalHost.cleanAllRowsCertificateMeta(rows);
+ }
+
+ return rows;
});
},
@@ -327,9 +405,9 @@ const internalStream = {
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
- let query = streamModel
+ const query = streamModel
.query()
- .count('id as count')
+ .count('id AS count')
.where('is_deleted', 0);
if (visibility !== 'all') {
diff --git a/backend/internal/token.js b/backend/internal/token.js
index ed9a45f82..0e6dec5e3 100644
--- a/backend/internal/token.js
+++ b/backend/internal/token.js
@@ -5,6 +5,8 @@ const authModel = require('../models/auth');
const helpers = require('../lib/helpers');
const TokenModel = require('../models/token');
+const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
+
module.exports = {
/**
@@ -69,15 +71,15 @@ module.exports = {
};
});
} else {
- throw new error.AuthError('Invalid password');
+ throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
});
} else {
- throw new error.AuthError('No password auth for user');
+ throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
});
} else {
- throw new error.AuthError('No relevant user found');
+ throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
});
},
diff --git a/backend/lib/certbot.js b/backend/lib/certbot.js
index eb1966dc7..96d947102 100644
--- a/backend/lib/certbot.js
+++ b/backend/lib/certbot.js
@@ -11,7 +11,7 @@ const certbot = {
/**
* @param {array} pluginKeys
*/
- installPlugins: async function (pluginKeys) {
+ installPlugins: async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
@@ -21,7 +21,7 @@ const certbot = {
}
batchflow(pluginKeys).sequential()
- .each((i, pluginKey, next) => {
+ .each((_i, pluginKey, next) => {
certbot.installPlugin(pluginKey)
.then(() => {
next();
@@ -51,7 +51,7 @@ const certbot = {
* @param {string} pluginKey
* @returns {Object}
*/
- installPlugin: async function (pluginKey) {
+ installPlugin: async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.ItemNotFoundError(pluginKey);
@@ -63,8 +63,15 @@ const certbot = {
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
- const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
- return utils.exec(cmd)
+ // SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
+ // in new versions of Python
+ let env = Object.assign({}, process.env, {SETUPTOOLS_USE_DISTUTILS: 'stdlib'});
+ if (typeof plugin.env === 'object') {
+ env = Object.assign(env, plugin.env);
+ }
+
+ const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
+ return utils.exec(cmd, {env})
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
diff --git a/backend/lib/config.js b/backend/lib/config.js
index f7fbdca6f..23184f3e8 100644
--- a/backend/lib/config.js
+++ b/backend/lib/config.js
@@ -2,7 +2,10 @@ const fs = require('fs');
const NodeRSA = require('node-rsa');
const logger = require('../logger').global;
-const keysFile = '/data/keys.json';
+const keysFile = '/data/keys.json';
+const mysqlEngine = 'mysql2';
+const postgresEngine = 'pg';
+const sqliteClientName = 'sqlite3';
let instance = null;
@@ -14,7 +17,7 @@ const configure = () => {
let configData;
try {
configData = require(filename);
- } catch (err) {
+ } catch (_) {
// do nothing
}
@@ -34,7 +37,7 @@ const configure = () => {
logger.info('Using MySQL configuration');
instance = {
database: {
- engine: 'mysql2',
+ engine: mysqlEngine,
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
@@ -46,13 +49,33 @@ const configure = () => {
return;
}
+ const envPostgresHost = process.env.DB_POSTGRES_HOST || null;
+ const envPostgresUser = process.env.DB_POSTGRES_USER || null;
+ const envPostgresName = process.env.DB_POSTGRES_NAME || null;
+ if (envPostgresHost && envPostgresUser && envPostgresName) {
+ // we have enough postgres creds to go with postgres
+ logger.info('Using Postgres configuration');
+ instance = {
+ database: {
+ engine: postgresEngine,
+ host: envPostgresHost,
+ port: process.env.DB_POSTGRES_PORT || 5432,
+ user: envPostgresUser,
+ password: process.env.DB_POSTGRES_PASSWORD,
+ name: envPostgresName,
+ },
+ keys: getKeys(),
+ };
+ return;
+ }
+
const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite';
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
engine: 'knex-native',
knex: {
- client: 'sqlite3',
+ client: sqliteClientName,
connection: {
filename: envSqliteFile
},
@@ -143,7 +166,27 @@ module.exports = {
*/
isSqlite: function () {
instance === null && configure();
- return instance.database.knex && instance.database.knex.client === 'sqlite3';
+ return instance.database.knex && instance.database.knex.client === sqliteClientName;
+ },
+
+ /**
+ * Is this a mysql configuration?
+ *
+ * @returns {boolean}
+ */
+ isMysql: function () {
+ instance === null && configure();
+ return instance.database.engine === mysqlEngine;
+ },
+
+ /**
+ * Is this a postgres configuration?
+ *
+ * @returns {boolean}
+ */
+ isPostgres: function () {
+ instance === null && configure();
+ return instance.database.engine === postgresEngine;
},
/**
diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js
index f7e98bebc..ad3df3c27 100644
--- a/backend/lib/helpers.js
+++ b/backend/lib/helpers.js
@@ -1,4 +1,6 @@
-const moment = require('moment');
+const moment = require('moment');
+const {isPostgres} = require('./config');
+const {ref} = require('objection');
module.exports = {
@@ -45,6 +47,16 @@ module.exports = {
}
});
return obj;
+ },
+
+ /**
+ * Casts a column to json if using postgres
+ *
+ * @param {string} colName
+ * @returns {string|Objection.ReferenceBuilder}
+ */
+ castJsonIfNeed: function (colName) {
+ return isPostgres() ? ref(colName).castText() : colName;
}
};
diff --git a/backend/lib/utils.js b/backend/lib/utils.js
index bcdb3341c..66f2dfd95 100644
--- a/backend/lib/utils.js
+++ b/backend/lib/utils.js
@@ -1,13 +1,13 @@
const _ = require('lodash');
-const exec = require('child_process').exec;
-const execFile = require('child_process').execFile;
+const exec = require('node:child_process').exec;
+const execFile = require('node:child_process').execFile;
const { Liquid } = require('liquidjs');
const logger = require('../logger').global;
const error = require('./error');
module.exports = {
- exec: async function(cmd, options = {}) {
+ exec: async (cmd, options = {}) => {
logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
@@ -31,11 +31,11 @@ module.exports = {
* @param {Array} args
* @returns {Promise}
*/
- execFile: function (cmd, args) {
+ execFile: (cmd, args) => {
// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
return new Promise((resolve, reject) => {
- execFile(cmd, args, function (err, stdout, /*stderr*/) {
+ execFile(cmd, args, (err, stdout, /*stderr*/) => {
if (err && typeof err === 'object') {
reject(err);
} else {
@@ -51,7 +51,7 @@ module.exports = {
* @param {Array} omissions
* @returns {Function}
*/
- omitRow: function (omissions) {
+ omitRow: (omissions) => {
/**
* @param {Object} row
* @returns {Object}
@@ -67,7 +67,7 @@ module.exports = {
* @param {Array} omissions
* @returns {Function}
*/
- omitRows: function (omissions) {
+ omitRows: (omissions) => {
/**
* @param {Array} rows
* @returns {Object}
@@ -83,9 +83,9 @@ module.exports = {
/**
* @returns {Object} Liquid render engine
*/
- getRenderEngine: function () {
+ getRenderEngine: () => {
const renderEngine = new Liquid({
- root: __dirname + '/../templates/'
+ root: `${__dirname}/../templates/`
});
/**
diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js
new file mode 100644
index 000000000..5f47b18ec
--- /dev/null
+++ b/backend/migrations/20240427161436_stream_ssl.js
@@ -0,0 +1,38 @@
+const migrate_name = 'stream_ssl';
+const logger = require('../logger').migrate;
+
+/**
+ * Migrate
+ *
+ * @see http://knexjs.org/#Schema
+ *
+ * @param {Object} knex
+ * @returns {Promise}
+ */
+exports.up = function (knex) {
+ logger.info('[' + migrate_name + '] Migrating Up...');
+
+ return knex.schema.table('stream', (table) => {
+ table.integer('certificate_id').notNull().unsigned().defaultTo(0);
+ })
+ .then(function () {
+ logger.info('[' + migrate_name + '] stream Table altered');
+ });
+};
+
+/**
+ * Undo Migrate
+ *
+ * @param {Object} knex
+ * @returns {Promise}
+ */
+exports.down = function (knex) {
+ logger.info('[' + migrate_name + '] Migrating Down...');
+
+ return knex.schema.table('stream', (table) => {
+ table.dropColumn('certificate_id');
+ })
+ .then(function () {
+ logger.info('[' + migrate_name + '] stream Table altered');
+ });
+};
diff --git a/backend/models/certificate.js b/backend/models/certificate.js
index 534d927cb..d4ea21ad5 100644
--- a/backend/models/certificate.js
+++ b/backend/models/certificate.js
@@ -4,7 +4,6 @@
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
-const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
@@ -68,6 +67,11 @@ class Certificate extends Model {
}
static get relationMappings () {
+ const ProxyHost = require('./proxy_host');
+ const DeadHost = require('./dead_host');
+ const User = require('./user');
+ const RedirectionHost = require('./redirection_host');
+
return {
owner: {
relation: Model.HasOneRelation,
@@ -79,6 +83,39 @@ class Certificate extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
+ },
+ proxy_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: ProxyHost,
+ join: {
+ from: 'certificate.id',
+ to: 'proxy_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('proxy_host.is_deleted', 0);
+ }
+ },
+ dead_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: DeadHost,
+ join: {
+ from: 'certificate.id',
+ to: 'dead_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('dead_host.is_deleted', 0);
+ }
+ },
+ redirection_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: RedirectionHost,
+ join: {
+ from: 'certificate.id',
+ to: 'redirection_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('redirection_host.is_deleted', 0);
+ }
}
};
}
diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js
index 483da3b6b..3386caabf 100644
--- a/backend/models/dead_host.js
+++ b/backend/models/dead_host.js
@@ -12,7 +12,11 @@ Model.knex(db);
const boolFields = [
'is_deleted',
+ 'ssl_forced',
+ 'http2_support',
'enabled',
+ 'hsts_enabled',
+ 'hsts_subdomains',
];
class DeadHost extends Model {
diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js
index 556742f0c..801627916 100644
--- a/backend/models/redirection_host.js
+++ b/backend/models/redirection_host.js
@@ -17,6 +17,9 @@ const boolFields = [
'preserve_path',
'ssl_forced',
'block_exploits',
+ 'hsts_enabled',
+ 'hsts_subdomains',
+ 'http2_support',
];
class RedirectionHost extends Model {
diff --git a/backend/models/stream.js b/backend/models/stream.js
index b96ca5a17..5d1cb6c1c 100644
--- a/backend/models/stream.js
+++ b/backend/models/stream.js
@@ -1,16 +1,15 @@
-// Objection Docs:
-// http://vincit.github.io/objection.js/
-
-const db = require('../db');
-const helpers = require('../lib/helpers');
-const Model = require('objection').Model;
-const User = require('./user');
-const now = require('./now_helper');
+const Model = require('objection').Model;
+const db = require('../db');
+const helpers = require('../lib/helpers');
+const User = require('./user');
+const Certificate = require('./certificate');
+const now = require('./now_helper');
Model.knex(db);
const boolFields = [
'is_deleted',
+ 'enabled',
'tcp_forwarding',
'udp_forwarding',
];
@@ -64,6 +63,17 @@ class Stream extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
+ },
+ certificate: {
+ relation: Model.HasOneRelation,
+ modelClass: Certificate,
+ join: {
+ from: 'stream.certificate_id',
+ to: 'certificate.id'
+ },
+ modify: function (qb) {
+ qb.where('certificate.is_deleted', 0);
+ }
}
};
}
diff --git a/backend/package.json b/backend/package.json
index 1bc3ef165..30984a332 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -23,6 +23,7 @@
"node-rsa": "^1.0.8",
"objection": "3.0.1",
"path": "^0.12.7",
+ "pg": "^8.13.1",
"signale": "1.4.0",
"sqlite3": "5.1.6",
"temp-write": "^4.0.0"
diff --git a/backend/routes/users.js b/backend/routes/users.js
index f8ce366c9..e41bf6cfb 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -181,7 +181,7 @@ router
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
@@ -212,7 +212,7 @@ router
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
@@ -238,7 +238,7 @@ router
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json
index a64a58c8f..e9dcacb5e 100644
--- a/backend/schema/components/proxy-host-object.json
+++ b/backend/schema/components/proxy-host-object.json
@@ -22,10 +22,7 @@
"enabled",
"locations",
"hsts_enabled",
- "hsts_subdomains",
- "certificate",
- "use_default_location",
- "ipv6"
+ "hsts_subdomains"
],
"additionalProperties": false,
"properties": {
@@ -151,12 +148,6 @@
"$ref": "./access-list-object.json"
}
]
- },
- "use_default_location": {
- "type": "boolean"
- },
- "ipv6": {
- "type": "boolean"
}
}
}
diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json
index cc4dbdd2f..e7a495fd3 100644
--- a/backend/schema/components/redirection-host-object.json
+++ b/backend/schema/components/redirection-host-object.json
@@ -28,7 +28,7 @@
},
"forward_scheme": {
"type": "string",
- "enum": ["http", "https"]
+ "enum": ["auto", "http", "https"]
},
"forward_domain_name": {
"description": "Domain Name",
diff --git a/backend/schema/components/setting-object.json b/backend/schema/components/setting-object.json
index e08777264..b9c6a1039 100644
--- a/backend/schema/components/setting-object.json
+++ b/backend/schema/components/setting-object.json
@@ -25,7 +25,7 @@
"value": {
"description": "Value in almost any form",
"example": "congratulations",
- "oneOf": [
+ "anyOf": [
{
"type": "string",
"minLength": 1
@@ -46,7 +46,10 @@
},
"meta": {
"description": "Extra metadata",
- "example": {},
+ "example": {
+ "redirect": "http://example.com",
+ "html": "