|
|
|
|
@@ -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;
|
|
|
|
|
|