mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 14:26:39 +00:00
Handle wildcard certs
This commit is contained in:
@@ -10,6 +10,7 @@ export async function getValidCertificatesForDomainsHybrid(domains: Set<string>)
|
|||||||
Array<{
|
Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
wildcard: boolean;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@@ -68,6 +69,7 @@ export async function getValidCertificatesForDomains(domains: Set<string>): Prom
|
|||||||
Array<{
|
Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
wildcard: boolean;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
|
|||||||
235
server/lib/traefikConfig.test.ts
Normal file
235
server/lib/traefikConfig.test.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { assertEquals } from "@test/assert";
|
||||||
|
import { isDomainCoveredByWildcard } from "./traefikConfig";
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('Running wildcard domain coverage tests...');
|
||||||
|
|
||||||
|
// Test case 1: Basic wildcard certificate at example.com
|
||||||
|
const basicWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should match first-level subdomains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('level1.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match level1.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('api.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match api.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('www.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match www.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should match the root domain (exact match)
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match example.com itself'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT match second-level subdomains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('level2.level1.example.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match level2.level1.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.nested.subdomain.example.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match deep.nested.subdomain.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT match different domains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.otherdomain.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match test.otherdomain.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('notexample.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match notexample.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 2: Multiple wildcard certificates
|
||||||
|
const multipleWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: true }],
|
||||||
|
['test.org', { exists: true, wildcard: true }],
|
||||||
|
['api.service.net', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('app.example.com', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of first wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('staging.test.org', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of second wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('v1.api.service.net', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.nested.api.service.net', multipleWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test exact domain matches for multiple certs
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of first wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.org', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of second wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('api.service.net', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 3: Non-wildcard certificates (should not match anything)
|
||||||
|
const nonWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: false }],
|
||||||
|
['specific.domain.com', { exists: true, wildcard: false }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.example.com', nonWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Non-wildcard cert should not match subdomains'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', nonWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Non-wildcard cert should not match even exact domain via this function'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 4: Non-existent certificates (should not match)
|
||||||
|
const nonExistentCerts = new Map([
|
||||||
|
['example.com', { exists: false, wildcard: true }],
|
||||||
|
['missing.com', { exists: false, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.example.com', nonExistentCerts),
|
||||||
|
false,
|
||||||
|
'Non-existent wildcard cert should not match'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 5: Edge cases with special domain names
|
||||||
|
const specialDomainCerts = new Map([
|
||||||
|
['localhost', { exists: true, wildcard: true }],
|
||||||
|
['127-0-0-1.nip.io', { exists: true, wildcard: true }],
|
||||||
|
['xn--e1afmkfd.xn--p1ai', { exists: true, wildcard: true }] // IDN domain
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('app.localhost', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of localhost wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.127-0-0-1.nip.io', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of nip.io wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.xn--e1afmkfd.xn--p1ai', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of IDN wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 6: Empty input and edge cases
|
||||||
|
const emptyCerts = new Map();
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('any.domain.com', emptyCerts),
|
||||||
|
false,
|
||||||
|
'Empty certificate map should not match any domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 7: Domains with single character components
|
||||||
|
const singleCharCerts = new Map([
|
||||||
|
['a.com', { exists: true, wildcard: true }],
|
||||||
|
['x.y.z', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('b.a.com', singleCharCerts),
|
||||||
|
true,
|
||||||
|
'Should match single character subdomain'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('w.x.y.z', singleCharCerts),
|
||||||
|
true,
|
||||||
|
'Should match single character subdomain of multi-part domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('v.w.x.y.z', singleCharCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain of single char domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 8: Domains with numbers and hyphens
|
||||||
|
const numericCerts = new Map([
|
||||||
|
['api-v2.service-1.com', { exists: true, wildcard: true }],
|
||||||
|
['123.456.net', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('staging.api-v2.service-1.com', numericCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain with hyphens and numbers'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.123.456.net', numericCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain with numeric components'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.staging.api-v2.service-1.com', numericCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain with hyphens and numbers'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('All wildcard domain coverage tests passed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
try {
|
||||||
|
runTests();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export class TraefikConfigManager {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
lastModified: Date | null;
|
lastModified: Date | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
|
wildcard: boolean;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ export class TraefikConfigManager {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
lastModified: Date | null;
|
lastModified: Date | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
|
wildcard: boolean;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
> {
|
> {
|
||||||
@@ -136,13 +138,16 @@ export class TraefikConfigManager {
|
|||||||
const certPath = path.join(domainDir, "cert.pem");
|
const certPath = path.join(domainDir, "cert.pem");
|
||||||
const keyPath = path.join(domainDir, "key.pem");
|
const keyPath = path.join(domainDir, "key.pem");
|
||||||
const lastUpdatePath = path.join(domainDir, ".last_update");
|
const lastUpdatePath = path.join(domainDir, ".last_update");
|
||||||
|
const wildcardPath = path.join(domainDir, ".wildcard");
|
||||||
|
|
||||||
const certExists = await this.fileExists(certPath);
|
const certExists = await this.fileExists(certPath);
|
||||||
const keyExists = await this.fileExists(keyPath);
|
const keyExists = await this.fileExists(keyPath);
|
||||||
const lastUpdateExists = await this.fileExists(lastUpdatePath);
|
const lastUpdateExists = await this.fileExists(lastUpdatePath);
|
||||||
|
const wildcardExists = await this.fileExists(wildcardPath);
|
||||||
|
|
||||||
let lastModified: Date | null = null;
|
let lastModified: Date | null = null;
|
||||||
const expiresAt: Date | null = null;
|
const expiresAt: Date | null = null;
|
||||||
|
let wildcard = false;
|
||||||
|
|
||||||
if (lastUpdateExists) {
|
if (lastUpdateExists) {
|
||||||
try {
|
try {
|
||||||
@@ -161,10 +166,26 @@ export class TraefikConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a wildcard certificate
|
||||||
|
if (wildcardExists) {
|
||||||
|
try {
|
||||||
|
const wildcardContent = fs
|
||||||
|
.readFileSync(wildcardPath, "utf8")
|
||||||
|
.trim();
|
||||||
|
wildcard = wildcardContent === "true";
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Could not read wildcard file for ${domain}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.set(domain, {
|
state.set(domain, {
|
||||||
exists: certExists && keyExists,
|
exists: certExists && keyExists,
|
||||||
lastModified,
|
lastModified,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
wildcard
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -192,19 +213,36 @@ export class TraefikConfigManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch if domains have changed
|
// Filter out domains covered by wildcard certificates
|
||||||
|
const domainsNeedingCerts = new Set<string>();
|
||||||
|
for (const domain of currentDomains) {
|
||||||
|
if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) {
|
||||||
|
domainsNeedingCerts.add(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch if domains needing certificates have changed
|
||||||
|
const lastDomainsNeedingCerts = new Set<string>();
|
||||||
|
for (const domain of this.lastKnownDomains) {
|
||||||
|
if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) {
|
||||||
|
lastDomainsNeedingCerts.add(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.lastKnownDomains.size !== currentDomains.size ||
|
domainsNeedingCerts.size !== lastDomainsNeedingCerts.size ||
|
||||||
!Array.from(this.lastKnownDomains).every((domain) =>
|
!Array.from(domainsNeedingCerts).every((domain) =>
|
||||||
currentDomains.has(domain)
|
lastDomainsNeedingCerts.has(domain)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
logger.info("Fetching certificates due to domain changes");
|
logger.info(
|
||||||
|
"Fetching certificates due to domain changes (after wildcard filtering)"
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any local certificates are missing or appear to be outdated
|
// Check if any local certificates are missing or appear to be outdated
|
||||||
for (const domain of currentDomains) {
|
for (const domain of domainsNeedingCerts) {
|
||||||
const localState = this.lastLocalCertificateState.get(domain);
|
const localState = this.lastLocalCertificateState.get(domain);
|
||||||
if (!localState || !localState.exists) {
|
if (!localState || !localState.exists) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -273,6 +311,7 @@ export class TraefikConfigManager {
|
|||||||
let validCertificates: Array<{
|
let validCertificates: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
wildcard: boolean;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@@ -280,23 +319,50 @@ export class TraefikConfigManager {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (this.shouldFetchCertificates(domains)) {
|
if (this.shouldFetchCertificates(domains)) {
|
||||||
// Get valid certificates for active domains
|
// Filter out domains that are already covered by wildcard certificates
|
||||||
if (config.isManagedMode()) {
|
const domainsToFetch = new Set<string>();
|
||||||
validCertificates =
|
for (const domain of domains) {
|
||||||
await getValidCertificatesForDomainsHybrid(domains);
|
if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) {
|
||||||
} else {
|
domainsToFetch.add(domain);
|
||||||
validCertificates =
|
} else {
|
||||||
await getValidCertificatesForDomains(domains);
|
logger.debug(
|
||||||
|
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.lastCertificateFetch = new Date();
|
|
||||||
this.lastKnownDomains = new Set(domains);
|
|
||||||
|
|
||||||
logger.info(
|
if (domainsToFetch.size > 0) {
|
||||||
`Fetched ${validCertificates.length} certificates from remote`
|
// Get valid certificates for domains not covered by wildcards
|
||||||
);
|
if (config.isManagedMode()) {
|
||||||
|
validCertificates =
|
||||||
|
await getValidCertificatesForDomainsHybrid(
|
||||||
|
domainsToFetch
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
validCertificates =
|
||||||
|
await getValidCertificatesForDomains(
|
||||||
|
domainsToFetch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
|
||||||
// Download and decrypt new certificates
|
logger.info(
|
||||||
await this.processValidCertificates(validCertificates);
|
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download and decrypt new certificates
|
||||||
|
await this.processValidCertificates(validCertificates);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||||
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always ensure all existing certificates (including wildcards) are in the config
|
||||||
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
} else {
|
} else {
|
||||||
const timeSinceLastFetch = this.lastCertificateFetch
|
const timeSinceLastFetch = this.lastCertificateFetch
|
||||||
? Math.round(
|
? Math.round(
|
||||||
@@ -544,7 +610,11 @@ export class TraefikConfigManager {
|
|||||||
// Clear existing certificates and rebuild from local state
|
// Clear existing certificates and rebuild from local state
|
||||||
dynamicConfig.tls.certificates = [];
|
dynamicConfig.tls.certificates = [];
|
||||||
|
|
||||||
|
// Keep track of certificates we've already added to avoid duplicates
|
||||||
|
const addedCertPaths = new Set<string>();
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
|
// First, try to find an exact match certificate
|
||||||
const localState = this.lastLocalCertificateState.get(domain);
|
const localState = this.lastLocalCertificateState.get(domain);
|
||||||
if (localState && localState.exists) {
|
if (localState && localState.exists) {
|
||||||
const domainDir = path.join(
|
const domainDir = path.join(
|
||||||
@@ -554,11 +624,47 @@ export class TraefikConfigManager {
|
|||||||
const certPath = path.join(domainDir, "cert.pem");
|
const certPath = path.join(domainDir, "cert.pem");
|
||||||
const keyPath = path.join(domainDir, "key.pem");
|
const keyPath = path.join(domainDir, "key.pem");
|
||||||
|
|
||||||
const certEntry = {
|
if (!addedCertPaths.has(certPath)) {
|
||||||
certFile: certPath,
|
const certEntry = {
|
||||||
keyFile: keyPath
|
certFile: certPath,
|
||||||
};
|
keyFile: keyPath
|
||||||
dynamicConfig.tls.certificates.push(certEntry);
|
};
|
||||||
|
dynamicConfig.tls.certificates.push(certEntry);
|
||||||
|
addedCertPaths.add(certPath);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, check for wildcard certificates that cover this domain
|
||||||
|
for (const [certDomain, certState] of this.lastLocalCertificateState) {
|
||||||
|
if (certState.exists && certState.wildcard) {
|
||||||
|
// Check if this wildcard certificate covers the domain
|
||||||
|
if (domain.endsWith("." + certDomain)) {
|
||||||
|
// Verify it's only one level deep (wildcard only covers one level)
|
||||||
|
const prefix = domain.substring(
|
||||||
|
0,
|
||||||
|
domain.length - ("." + certDomain).length
|
||||||
|
);
|
||||||
|
if (!prefix.includes(".")) {
|
||||||
|
const domainDir = path.join(
|
||||||
|
config.getRawConfig().traefik.certificates_path,
|
||||||
|
certDomain
|
||||||
|
);
|
||||||
|
const certPath = path.join(domainDir, "cert.pem");
|
||||||
|
const keyPath = path.join(domainDir, "key.pem");
|
||||||
|
|
||||||
|
if (!addedCertPaths.has(certPath)) {
|
||||||
|
const certEntry = {
|
||||||
|
certFile: certPath,
|
||||||
|
keyFile: keyPath
|
||||||
|
};
|
||||||
|
dynamicConfig.tls.certificates.push(certEntry);
|
||||||
|
addedCertPaths.add(certPath);
|
||||||
|
}
|
||||||
|
break; // Found a wildcard that covers this domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +683,7 @@ export class TraefikConfigManager {
|
|||||||
validCertificates: Array<{
|
validCertificates: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
wildcard: boolean;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@@ -651,15 +758,24 @@ export class TraefikConfigManager {
|
|||||||
"utf8"
|
"utf8"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if this is a wildcard certificate and store it
|
||||||
|
const wildcardPath = path.join(domainDir, ".wildcard");
|
||||||
|
fs.writeFileSync(
|
||||||
|
wildcardPath,
|
||||||
|
cert.wildcard ? "true" : "false",
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Certificate updated for domain: ${cert.domain}`
|
`Certificate updated for domain: ${cert.domain}${cert.wildcard ? " (wildcard)" : ""}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update local state tracking
|
// Update local state tracking
|
||||||
this.lastLocalCertificateState.set(cert.domain, {
|
this.lastLocalCertificateState.set(cert.domain, {
|
||||||
exists: true,
|
exists: true,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
expiresAt: cert.expiresAt
|
expiresAt: cert.expiresAt,
|
||||||
|
wildcard: cert.wildcard
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,14 +926,8 @@ export class TraefikConfigManager {
|
|||||||
this.lastLocalCertificateState.delete(dirName);
|
this.lastLocalCertificateState.delete(dirName);
|
||||||
|
|
||||||
// Remove from dynamic config
|
// Remove from dynamic config
|
||||||
const certFilePath = path.join(
|
const certFilePath = path.join(domainDir, "cert.pem");
|
||||||
domainDir,
|
const keyFilePath = path.join(domainDir, "key.pem");
|
||||||
"cert.pem"
|
|
||||||
);
|
|
||||||
const keyFilePath = path.join(
|
|
||||||
domainDir,
|
|
||||||
"key.pem"
|
|
||||||
);
|
|
||||||
const before = dynamicConfig.tls.certificates.length;
|
const before = dynamicConfig.tls.certificates.length;
|
||||||
dynamicConfig.tls.certificates =
|
dynamicConfig.tls.certificates =
|
||||||
dynamicConfig.tls.certificates.filter(
|
dynamicConfig.tls.certificates.filter(
|
||||||
@@ -894,14 +1004,58 @@ export class TraefikConfigManager {
|
|||||||
monitorInterval: number;
|
monitorInterval: number;
|
||||||
lastCertificateFetch: Date | null;
|
lastCertificateFetch: Date | null;
|
||||||
localCertificateCount: number;
|
localCertificateCount: number;
|
||||||
|
wildcardCertificates: string[];
|
||||||
|
domainsCoveredByWildcards: string[];
|
||||||
} {
|
} {
|
||||||
|
const wildcardCertificates: string[] = [];
|
||||||
|
const domainsCoveredByWildcards: string[] = [];
|
||||||
|
|
||||||
|
// Find wildcard certificates
|
||||||
|
for (const [domain, state] of this.lastLocalCertificateState) {
|
||||||
|
if (state.exists && state.wildcard) {
|
||||||
|
wildcardCertificates.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find domains covered by wildcards
|
||||||
|
for (const domain of this.activeDomains) {
|
||||||
|
if (isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) {
|
||||||
|
domainsCoveredByWildcards.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isRunning: this.isRunning,
|
isRunning: this.isRunning,
|
||||||
activeDomains: Array.from(this.activeDomains),
|
activeDomains: Array.from(this.activeDomains),
|
||||||
monitorInterval:
|
monitorInterval:
|
||||||
config.getRawConfig().traefik.monitor_interval || 5000,
|
config.getRawConfig().traefik.monitor_interval || 5000,
|
||||||
lastCertificateFetch: this.lastCertificateFetch,
|
lastCertificateFetch: this.lastCertificateFetch,
|
||||||
localCertificateCount: this.lastLocalCertificateState.size
|
localCertificateCount: this.lastLocalCertificateState.size,
|
||||||
|
wildcardCertificates,
|
||||||
|
domainsCoveredByWildcards
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a domain is covered by existing wildcard certificates
|
||||||
|
*/
|
||||||
|
export function isDomainCoveredByWildcard(domain: string, lastLocalCertificateState: Map<string, { exists: boolean; wildcard: boolean }>): boolean {
|
||||||
|
for (const [certDomain, state] of lastLocalCertificateState) {
|
||||||
|
if (state.exists && state.wildcard) {
|
||||||
|
// If stored as example.com but is wildcard, check subdomains
|
||||||
|
if (domain.endsWith("." + certDomain)) {
|
||||||
|
// Check that it's only one level deep (wildcard only covers one level)
|
||||||
|
const prefix = domain.substring(
|
||||||
|
0,
|
||||||
|
domain.length - ("." + certDomain).length
|
||||||
|
);
|
||||||
|
// If prefix contains a dot, it's more than one level deep
|
||||||
|
if (!prefix.includes(".")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user