|
|
|
@ -118,6 +118,7 @@ export interface ICertificateExpiring {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Port80Handler with ACME certificate management and request forwarding capabilities
|
|
|
|
|
* Now with glob pattern support for domain matching
|
|
|
|
|
*/
|
|
|
|
|
export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
private domainCertificates: Map<string, IDomainCertificate>;
|
|
|
|
@ -180,6 +181,12 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
|
|
|
|
|
// Start certificate process for domains with acmeMaintenance enabled
|
|
|
|
|
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
|
|
|
// Skip glob patterns for certificate issuance
|
|
|
|
|
if (this.isGlobPattern(domain)) {
|
|
|
|
|
console.log(`Skipping initial certificate for glob pattern: ${domain}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
|
|
|
|
|
this.obtainCertificate(domain).catch(err => {
|
|
|
|
|
console.error(`Error obtaining initial certificate for ${domain}:`, err);
|
|
|
|
@ -252,8 +259,8 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
hasAcmeForward: !!options.acmeForward
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If acmeMaintenance is enabled, start certificate process immediately
|
|
|
|
|
if (options.acmeMaintenance && this.server) {
|
|
|
|
|
// If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately
|
|
|
|
|
if (options.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) {
|
|
|
|
|
this.obtainCertificate(domainName).catch(err => {
|
|
|
|
|
console.error(`Error obtaining initial certificate for ${domainName}:`, err);
|
|
|
|
|
});
|
|
|
|
@ -288,6 +295,11 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
throw new Port80HandlerError('Domain, certificate and privateKey are required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Don't allow setting certificates for glob patterns
|
|
|
|
|
if (this.isGlobPattern(domain)) {
|
|
|
|
|
throw new Port80HandlerError('Cannot set certificate for glob pattern domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let domainInfo = this.domainCertificates.get(domain);
|
|
|
|
|
|
|
|
|
|
if (!domainInfo) {
|
|
|
|
@ -334,6 +346,11 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
* @param domain The domain to get the certificate for
|
|
|
|
|
*/
|
|
|
|
|
public getCertificate(domain: string): ICertificateData | null {
|
|
|
|
|
// Can't get certificates for glob patterns
|
|
|
|
|
if (this.isGlobPattern(domain)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const domainInfo = this.domainCertificates.get(domain);
|
|
|
|
|
|
|
|
|
|
if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
|
|
|
|
@ -348,6 +365,65 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a domain is a glob pattern
|
|
|
|
|
* @param domain Domain to check
|
|
|
|
|
* @returns True if the domain is a glob pattern
|
|
|
|
|
*/
|
|
|
|
|
private isGlobPattern(domain: string): boolean {
|
|
|
|
|
return domain.includes('*');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get domain info for a specific domain, using glob pattern matching if needed
|
|
|
|
|
* @param requestDomain The actual domain from the request
|
|
|
|
|
* @returns The domain info or null if not found
|
|
|
|
|
*/
|
|
|
|
|
private getDomainInfoForRequest(requestDomain: string): { domainInfo: IDomainCertificate, pattern: string } | null {
|
|
|
|
|
// Try direct match first
|
|
|
|
|
if (this.domainCertificates.has(requestDomain)) {
|
|
|
|
|
return {
|
|
|
|
|
domainInfo: this.domainCertificates.get(requestDomain)!,
|
|
|
|
|
pattern: requestDomain
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then try glob patterns
|
|
|
|
|
for (const [pattern, domainInfo] of this.domainCertificates.entries()) {
|
|
|
|
|
if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) {
|
|
|
|
|
return { domainInfo, pattern };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a domain matches a glob pattern
|
|
|
|
|
* @param domain The domain to check
|
|
|
|
|
* @param pattern The pattern to match against
|
|
|
|
|
* @returns True if the domain matches the pattern
|
|
|
|
|
*/
|
|
|
|
|
private domainMatchesPattern(domain: string, pattern: string): boolean {
|
|
|
|
|
// Handle different glob pattern styles
|
|
|
|
|
if (pattern.startsWith('*.')) {
|
|
|
|
|
// *.example.com matches any subdomain
|
|
|
|
|
const suffix = pattern.substring(2);
|
|
|
|
|
return domain.endsWith(suffix) && domain.includes('.') && domain !== suffix;
|
|
|
|
|
} else if (pattern.endsWith('.*')) {
|
|
|
|
|
// example.* matches any TLD
|
|
|
|
|
const prefix = pattern.substring(0, pattern.length - 2);
|
|
|
|
|
const domainParts = domain.split('.');
|
|
|
|
|
return domain.startsWith(prefix + '.') && domainParts.length >= 2;
|
|
|
|
|
} else if (pattern === '*') {
|
|
|
|
|
// Wildcard matches everything
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
// Exact match (shouldn't reach here as we check exact matches first)
|
|
|
|
|
return domain === pattern;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lazy initialization of the ACME client
|
|
|
|
|
* @returns An ACME client instance
|
|
|
|
@ -397,14 +473,16 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
// Extract domain (ignoring any port in the Host header)
|
|
|
|
|
const domain = hostHeader.split(':')[0];
|
|
|
|
|
|
|
|
|
|
// Check if domain is configured
|
|
|
|
|
if (!this.domainCertificates.has(domain)) {
|
|
|
|
|
// Get domain config, using glob pattern matching if needed
|
|
|
|
|
const domainMatch = this.getDomainInfoForRequest(domain);
|
|
|
|
|
|
|
|
|
|
if (!domainMatch) {
|
|
|
|
|
res.statusCode = 404;
|
|
|
|
|
res.end('Domain not configured');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const domainInfo = this.domainCertificates.get(domain)!;
|
|
|
|
|
const { domainInfo, pattern } = domainMatch;
|
|
|
|
|
const options = domainInfo.options;
|
|
|
|
|
|
|
|
|
|
// If the request is for an ACME HTTP-01 challenge, handle it
|
|
|
|
@ -415,8 +493,11 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.handleAcmeChallenge(req, res, domain);
|
|
|
|
|
return;
|
|
|
|
|
// Only handle ACME challenges for non-glob patterns
|
|
|
|
|
if (!this.isGlobPattern(pattern)) {
|
|
|
|
|
this.handleAcmeChallenge(req, res, domain);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we should forward non-ACME requests
|
|
|
|
@ -426,7 +507,8 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If certificate exists and sslRedirect is enabled, redirect to HTTPS
|
|
|
|
|
if (domainInfo.certObtained && options.sslRedirect) {
|
|
|
|
|
// (Skip for glob patterns as they won't have certificates)
|
|
|
|
|
if (!this.isGlobPattern(pattern) && domainInfo.certObtained && options.sslRedirect) {
|
|
|
|
|
const httpsPort = this.options.httpsRedirectPort;
|
|
|
|
|
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
|
|
|
|
|
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
|
|
|
|
@ -438,7 +520,8 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle case where certificate maintenance is enabled but not yet obtained
|
|
|
|
|
if (options.acmeMaintenance && !domainInfo.certObtained) {
|
|
|
|
|
// (Skip for glob patterns as they can't have certificates)
|
|
|
|
|
if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) {
|
|
|
|
|
// Trigger certificate issuance if not already running
|
|
|
|
|
if (!domainInfo.obtainingInProgress) {
|
|
|
|
|
this.obtainCertificate(domain).catch(err => {
|
|
|
|
@ -559,6 +642,11 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
* @param isRenewal Whether this is a renewal attempt
|
|
|
|
|
*/
|
|
|
|
|
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
|
|
|
|
// Don't allow certificate issuance for glob patterns
|
|
|
|
|
if (this.isGlobPattern(domain)) {
|
|
|
|
|
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the domain info
|
|
|
|
|
const domainInfo = this.domainCertificates.get(domain);
|
|
|
|
|
if (!domainInfo) {
|
|
|
|
@ -752,6 +840,11 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
|
|
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
|
|
|
// Skip glob patterns
|
|
|
|
|
if (this.isGlobPattern(domain)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip domains with acmeMaintenance disabled
|
|
|
|
|
if (!domainInfo.options.acmeMaintenance) {
|
|
|
|
|
continue;
|
|
|
|
|