fix(smart-proxy): disable built-in Rust ACME when a certProvisionFunction is provided and improve certificate provisioning flow
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '23.1.5',
|
||||
version: '23.1.6',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RouteValidator } from './utils/route-validator.js';
|
||||
import { Mutex } from './utils/mutex.js';
|
||||
|
||||
// Types
|
||||
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject } from './models/interfaces.js';
|
||||
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject, IAcmeOptions } from './models/interfaces.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
import type { IMetrics } from './models/metrics-types.js';
|
||||
|
||||
@@ -146,8 +146,16 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Preprocess routes (strip JS functions, convert socket-handler routes)
|
||||
const rustRoutes = this.preprocessor.preprocessForRust(this.settings.routes);
|
||||
|
||||
// When certProvisionFunction handles cert provisioning,
|
||||
// disable Rust's built-in ACME to prevent race condition.
|
||||
let acmeForRust = this.settings.acme;
|
||||
if (this.settings.certProvisionFunction && acmeForRust?.enabled) {
|
||||
acmeForRust = { ...acmeForRust, enabled: false };
|
||||
logger.log('info', 'Rust ACME disabled — certProvisionFunction will handle certificate provisioning', { component: 'smart-proxy' });
|
||||
}
|
||||
|
||||
// Build Rust config
|
||||
const config = this.buildRustConfig(rustRoutes);
|
||||
const config = this.buildRustConfig(rustRoutes, acmeForRust);
|
||||
|
||||
// Start the Rust proxy
|
||||
await this.bridge.startProxy(config);
|
||||
@@ -334,20 +342,21 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* Build the Rust configuration object from TS settings.
|
||||
*/
|
||||
private buildRustConfig(routes: IRouteConfig[]): any {
|
||||
private buildRustConfig(routes: IRouteConfig[], acmeOverride?: IAcmeOptions): any {
|
||||
const acme = acmeOverride !== undefined ? acmeOverride : this.settings.acme;
|
||||
return {
|
||||
routes,
|
||||
defaults: this.settings.defaults,
|
||||
acme: this.settings.acme
|
||||
acme: acme
|
||||
? {
|
||||
enabled: this.settings.acme.enabled,
|
||||
email: this.settings.acme.email,
|
||||
useProduction: this.settings.acme.useProduction,
|
||||
port: this.settings.acme.port,
|
||||
renewThresholdDays: this.settings.acme.renewThresholdDays,
|
||||
autoRenew: this.settings.acme.autoRenew,
|
||||
certificateStore: this.settings.acme.certificateStore,
|
||||
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours,
|
||||
enabled: acme.enabled,
|
||||
email: acme.email,
|
||||
useProduction: acme.useProduction,
|
||||
port: acme.port,
|
||||
renewThresholdDays: acme.renewThresholdDays,
|
||||
autoRenew: acme.autoRenew,
|
||||
certificateStore: acme.certificateStore,
|
||||
renewCheckIntervalHours: acme.renewCheckIntervalHours,
|
||||
}
|
||||
: undefined,
|
||||
connectionTimeout: this.settings.connectionTimeout,
|
||||
@@ -374,18 +383,31 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
const provisionFn = this.settings.certProvisionFunction;
|
||||
if (!provisionFn) return;
|
||||
|
||||
const provisionedDomains = new Set<string>();
|
||||
|
||||
for (const route of this.settings.routes) {
|
||||
if (route.action.tls?.certificate !== 'auto') continue;
|
||||
if (!route.match.domains) continue;
|
||||
|
||||
const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
||||
const rawDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
||||
const certDomains = this.normalizeDomainsForCertProvisioning(rawDomains);
|
||||
|
||||
for (const domain of domains) {
|
||||
for (const domain of certDomains) {
|
||||
if (provisionedDomains.has(domain)) continue;
|
||||
provisionedDomains.add(domain);
|
||||
try {
|
||||
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
||||
|
||||
if (result === 'http01') {
|
||||
// Rust handles ACME for this domain
|
||||
// Callback wants HTTP-01 for this domain — trigger Rust ACME explicitly
|
||||
if (route.name) {
|
||||
try {
|
||||
await this.bridge.provisionCertificate(route.name);
|
||||
logger.log('info', `Triggered Rust ACME for ${domain} (route: ${route.name})`, { component: 'smart-proxy' });
|
||||
} catch (provisionErr: any) {
|
||||
logger.log('warn', `Cannot provision cert for ${domain} — callback returned 'http01' but Rust ACME failed: ${provisionErr.message}`, { component: 'smart-proxy' });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -411,6 +433,43 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize routing glob patterns into valid domain identifiers for cert provisioning.
|
||||
* - `*nevermind.cloud` → `['nevermind.cloud', '*.nevermind.cloud']`
|
||||
* - `*.lossless.digital` → `['*.lossless.digital']` (already valid wildcard)
|
||||
* - `code.foss.global` → `['code.foss.global']` (plain domain)
|
||||
* - `*mid*.example.com` → skipped with warning (unsupported glob)
|
||||
*/
|
||||
private normalizeDomainsForCertProvisioning(rawDomains: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const raw of rawDomains) {
|
||||
// Plain domain — no glob characters
|
||||
if (!raw.includes('*')) {
|
||||
result.push(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid wildcard: *.example.com
|
||||
if (raw.startsWith('*.') && !raw.slice(2).includes('*')) {
|
||||
result.push(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Routing glob like *example.com (leading star, no dot after it)
|
||||
// Convert to bare domain + wildcard pair
|
||||
if (raw.startsWith('*') && !raw.startsWith('*.') && !raw.slice(1).includes('*')) {
|
||||
const baseDomain = raw.slice(1); // Remove leading *
|
||||
result.push(baseDomain);
|
||||
result.push(`*.${baseDomain}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unsupported glob pattern (e.g. *mid*.example.com)
|
||||
logger.log('warn', `Skipping unsupported glob pattern for cert provisioning: ${raw}`, { component: 'smart-proxy' });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private isValidDomain(domain: string): boolean {
|
||||
if (!domain || domain.length === 0) return false;
|
||||
if (domain.includes('*')) return false;
|
||||
|
||||
Reference in New Issue
Block a user