fix(smart-proxy): disable built-in Rust ACME when a certProvisionFunction is provided and improve certificate provisioning flow
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-13 - 23.1.6 - fix(smart-proxy)
|
||||||
|
disable built-in Rust ACME when a certProvisionFunction is provided and improve certificate provisioning flow
|
||||||
|
|
||||||
|
- Pass an optional ACME override into buildRustConfig so Rust ACME can be disabled per-run
|
||||||
|
- Disable Rust ACME when certProvisionFunction is configured to avoid provisioning race conditions
|
||||||
|
- Normalize routing glob patterns into concrete domain identifiers for certificate provisioning (expand leading-star globs and warn on unsupported patterns)
|
||||||
|
- Deduplicate domains during provisioning to avoid repeated attempts
|
||||||
|
- When the callback returns 'http01', explicitly trigger Rust ACME for the route via bridge.provisionCertificate and log success/failure
|
||||||
|
|
||||||
## 2026-02-13 - 23.1.5 - fix(smart-proxy)
|
## 2026-02-13 - 23.1.5 - fix(smart-proxy)
|
||||||
provision certificates for wildcard domains instead of skipping them
|
provision certificates for wildcard domains instead of skipping them
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
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.'
|
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';
|
import { Mutex } from './utils/mutex.js';
|
||||||
|
|
||||||
// Types
|
// 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 { IRouteConfig } from './models/route-types.js';
|
||||||
import type { IMetrics } from './models/metrics-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)
|
// Preprocess routes (strip JS functions, convert socket-handler routes)
|
||||||
const rustRoutes = this.preprocessor.preprocessForRust(this.settings.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
|
// Build Rust config
|
||||||
const config = this.buildRustConfig(rustRoutes);
|
const config = this.buildRustConfig(rustRoutes, acmeForRust);
|
||||||
|
|
||||||
// Start the Rust proxy
|
// Start the Rust proxy
|
||||||
await this.bridge.startProxy(config);
|
await this.bridge.startProxy(config);
|
||||||
@@ -334,20 +342,21 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Build the Rust configuration object from TS settings.
|
* 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 {
|
return {
|
||||||
routes,
|
routes,
|
||||||
defaults: this.settings.defaults,
|
defaults: this.settings.defaults,
|
||||||
acme: this.settings.acme
|
acme: acme
|
||||||
? {
|
? {
|
||||||
enabled: this.settings.acme.enabled,
|
enabled: acme.enabled,
|
||||||
email: this.settings.acme.email,
|
email: acme.email,
|
||||||
useProduction: this.settings.acme.useProduction,
|
useProduction: acme.useProduction,
|
||||||
port: this.settings.acme.port,
|
port: acme.port,
|
||||||
renewThresholdDays: this.settings.acme.renewThresholdDays,
|
renewThresholdDays: acme.renewThresholdDays,
|
||||||
autoRenew: this.settings.acme.autoRenew,
|
autoRenew: acme.autoRenew,
|
||||||
certificateStore: this.settings.acme.certificateStore,
|
certificateStore: acme.certificateStore,
|
||||||
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours,
|
renewCheckIntervalHours: acme.renewCheckIntervalHours,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
connectionTimeout: this.settings.connectionTimeout,
|
connectionTimeout: this.settings.connectionTimeout,
|
||||||
@@ -374,18 +383,31 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
const provisionFn = this.settings.certProvisionFunction;
|
const provisionFn = this.settings.certProvisionFunction;
|
||||||
if (!provisionFn) return;
|
if (!provisionFn) return;
|
||||||
|
|
||||||
|
const provisionedDomains = new Set<string>();
|
||||||
|
|
||||||
for (const route of this.settings.routes) {
|
for (const route of this.settings.routes) {
|
||||||
if (route.action.tls?.certificate !== 'auto') continue;
|
if (route.action.tls?.certificate !== 'auto') continue;
|
||||||
if (!route.match.domains) 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 {
|
try {
|
||||||
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
||||||
|
|
||||||
if (result === 'http01') {
|
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;
|
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 {
|
private isValidDomain(domain: string): boolean {
|
||||||
if (!domain || domain.length === 0) return false;
|
if (!domain || domain.length === 0) return false;
|
||||||
if (domain.includes('*')) return false;
|
if (domain.includes('*')) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user