mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-14 17:18:27 +00:00
Some paths rendered host configs without fully loading the relations the templates depend on, silently dropping access rules (and SSL): - Enabling a proxy host loaded the access list without its clients or auth items, so the host came back with no access restrictions. - Requesting a Let's Encrypt certificate re-rendered every host sharing its domains from bare rows, dropping access rules and SSL. generateConfig now loads any referenced certificate or access list that isn't fully loaded, so no caller can render a partial config. The two callers above also fetch the relations themselves, and a warning is logged when a host's access list is missing or has no rules. Adds integration tests covering access lists across disable/enable. Fixes #5919, #4286, #5710 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
5.9 KiB
JavaScript
238 lines
5.9 KiB
JavaScript
import _ from "lodash";
|
|
import { castJsonIfNeed } from "../lib/helpers.js";
|
|
import deadHostModel from "../models/dead_host.js";
|
|
import proxyHostModel from "../models/proxy_host.js";
|
|
import redirectionHostModel from "../models/redirection_host.js";
|
|
|
|
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: (data, existingData) => {
|
|
const combinedData = _.assign({}, existingData || {}, data);
|
|
|
|
if (!combinedData.certificate_id) {
|
|
combinedData.ssl_forced = false;
|
|
combinedData.http2_support = false;
|
|
}
|
|
|
|
if (!combinedData.ssl_forced) {
|
|
combinedData.hsts_enabled = false;
|
|
}
|
|
|
|
if (!combinedData.hsts_enabled) {
|
|
combinedData.hsts_subdomains = false;
|
|
}
|
|
|
|
return combinedData;
|
|
},
|
|
|
|
/**
|
|
* used by the getAll functions of hosts, this removes the certificate meta if present
|
|
*
|
|
* @param {Array} rows
|
|
* @returns {Array}
|
|
*/
|
|
cleanAllRowsCertificateMeta: (rows) => {
|
|
rows.map((_, idx) => {
|
|
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) {
|
|
rows[idx].certificate.meta = {};
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return rows;
|
|
},
|
|
|
|
/**
|
|
* used by the get/update functions of hosts, this removes the certificate meta if present
|
|
*
|
|
* @param {Object} row
|
|
* @returns {Object}
|
|
*/
|
|
cleanRowCertificateMeta: (row) => {
|
|
if (typeof row.certificate !== "undefined" && row.certificate) {
|
|
row.certificate.meta = {};
|
|
}
|
|
|
|
return row;
|
|
},
|
|
|
|
/**
|
|
* This returns all the host types with any domain listed in the provided domainNames array.
|
|
* This is used by the certificates to temporarily disable any host that is using the domain
|
|
*
|
|
* @param {Array} domainNames
|
|
* @returns {Promise}
|
|
*/
|
|
getHostsWithDomains: async (domainNames) => {
|
|
const responseObject = {
|
|
total_count: 0,
|
|
dead_hosts: [],
|
|
proxy_hosts: [],
|
|
redirection_hosts: [],
|
|
};
|
|
|
|
const proxyRes = await proxyHostModel
|
|
.query()
|
|
.where("is_deleted", 0)
|
|
.withGraphFetched("[certificate, access_list.[clients,items]]");
|
|
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
|
|
responseObject.total_count += responseObject.proxy_hosts.length;
|
|
|
|
const redirRes = await redirectionHostModel.query().where("is_deleted", 0).withGraphFetched("[certificate]");
|
|
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);
|
|
responseObject.total_count += responseObject.redirection_hosts.length;
|
|
|
|
const deadRes = await deadHostModel.query().where("is_deleted", 0).withGraphFetched("[certificate]");
|
|
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);
|
|
responseObject.total_count += responseObject.dead_hosts.length;
|
|
|
|
return responseObject;
|
|
},
|
|
|
|
/**
|
|
* Internal use only, checks to see if the domain is already taken by any other record
|
|
*
|
|
* @param {String} hostname
|
|
* @param {String} [ignore_type] 'proxy', 'redirection', 'dead'
|
|
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
|
|
* @returns {Promise}
|
|
*/
|
|
isHostnameTaken: (hostname, ignore_type, ignore_id) => {
|
|
const promises = [
|
|
proxyHostModel
|
|
.query()
|
|
.where("is_deleted", 0)
|
|
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
|
redirectionHostModel
|
|
.query()
|
|
.where("is_deleted", 0)
|
|
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
|
deadHostModel
|
|
.query()
|
|
.where("is_deleted", 0)
|
|
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
|
];
|
|
|
|
return Promise.all(promises).then((promises_results) => {
|
|
let is_taken = false;
|
|
|
|
if (promises_results[0]) {
|
|
// Proxy Hosts
|
|
if (
|
|
internalHost._checkHostnameRecordsTaken(
|
|
hostname,
|
|
promises_results[0],
|
|
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
|
|
)
|
|
) {
|
|
is_taken = true;
|
|
}
|
|
}
|
|
|
|
if (promises_results[1]) {
|
|
// Redirection Hosts
|
|
if (
|
|
internalHost._checkHostnameRecordsTaken(
|
|
hostname,
|
|
promises_results[1],
|
|
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
|
|
)
|
|
) {
|
|
is_taken = true;
|
|
}
|
|
}
|
|
|
|
if (promises_results[2]) {
|
|
// Dead Hosts
|
|
if (
|
|
internalHost._checkHostnameRecordsTaken(
|
|
hostname,
|
|
promises_results[2],
|
|
ignore_type === "dead" && ignore_id ? ignore_id : 0,
|
|
)
|
|
) {
|
|
is_taken = true;
|
|
}
|
|
}
|
|
|
|
return {
|
|
hostname: hostname,
|
|
is_taken: is_taken,
|
|
};
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Private call only
|
|
*
|
|
* @param {String} hostname
|
|
* @param {Array} existingRows
|
|
* @param {Integer} [ignoreId]
|
|
* @returns {Boolean}
|
|
*/
|
|
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {
|
|
let isTaken = false;
|
|
|
|
if (existingRows?.length) {
|
|
existingRows.map((existingRow) => {
|
|
existingRow.domain_names.map((existingHostname) => {
|
|
// Does this domain match?
|
|
if (existingHostname.toLowerCase() === hostname.toLowerCase()) {
|
|
if (!ignoreId || ignoreId !== existingRow.id) {
|
|
isTaken = true;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return isTaken;
|
|
},
|
|
|
|
/**
|
|
* Private call only
|
|
*
|
|
* @param {Array} hosts
|
|
* @param {Array} domainNames
|
|
* @returns {Array}
|
|
*/
|
|
_getHostsWithDomains: (hosts, domainNames) => {
|
|
const response = [];
|
|
|
|
if (hosts?.length) {
|
|
hosts.map((host) => {
|
|
let hostMatches = false;
|
|
|
|
domainNames.map((domainName) => {
|
|
host.domain_names.map((hostDomainName) => {
|
|
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) {
|
|
hostMatches = true;
|
|
}
|
|
return true;
|
|
});
|
|
return true;
|
|
});
|
|
|
|
if (hostMatches) {
|
|
response.push(host);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return response;
|
|
},
|
|
};
|
|
|
|
export default internalHost;
|