update
This commit is contained in:
parent
a59ebd6202
commit
09aadc702e
@ -30,6 +30,7 @@
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@tsclass/tsclass": "^9.1.0",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ importers:
|
||||
'@push.rocks/smartstring':
|
||||
specifier: ^4.0.15
|
||||
version: 4.0.15
|
||||
'@push.rocks/taskbuffer':
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0
|
||||
@ -6301,7 +6304,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
@ -6920,7 +6922,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.7
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
|
||||
|
@ -1,31 +1,26 @@
|
||||
## Plan: Integrate @push.rocks/smartacme into Port80Handler
|
||||
## Plan: Centralize Certificate Renewal for all certificates
|
||||
|
||||
- [x] read the complete README of @push.rocks/smartacme and understand the API.
|
||||
- [x] Add imports to ts/plugins.ts:
|
||||
- import * as smartacme from '@push.rocks/smartacme';
|
||||
- export { smartacme };
|
||||
- [x] In Port80Handler.start():
|
||||
- Instantiate SmartAcme and use the in memory certmanager.
|
||||
- use the DisklessHttp01Handler implemented in classes.port80handler.ts
|
||||
- Call `await smartAcme.start()` before binding HTTP server.
|
||||
- [x] Replace old ACME flow in `obtainCertificate()` to use `await smartAcme.getCertificateForDomain(domain)` and process returned cert object. Remove old code.
|
||||
- [x] Update `handleRequest()` to let DisklessHttp01Handler serve challenges.
|
||||
- [x] Remove legacy methods: `getAcmeClient()`, `handleAcmeChallenge()`, `processAuthorizations()`, and related token bookkeeping in domainInfo.
|
||||
|
||||
## Plan: Certificate Provider Hook & Observable Emission
|
||||
|
||||
- [x] Extend IPortProxySettings (ts/smartproxy/classes.pp.interfaces.ts):
|
||||
- Define type ISmartProxyCertProvisionObject = tsclass.network.ICert | 'http01'`.
|
||||
- Add optional `certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>`.
|
||||
- [x] Enhance SmartProxy (ts/smartproxy/classes.smartproxy.ts):
|
||||
- Import `EventEmitter` and change class signature to `export class SmartProxy extends EventEmitter`.
|
||||
- Call `super()` in constructor.
|
||||
- In `initializePort80Handler` and `updateDomainConfigs`, for each non-wildcard domain:
|
||||
- Invoke `certProvider(domain)` if provided, defaulting to `'http01'`.
|
||||
- If result is `'http01'`, register domain with `Port80Handler` for ACME challenges.
|
||||
- If static cert returned, bypass `Port80Handler`, apply via `NetworkProxyBridge`
|
||||
- Subscribe to `Port80HandlerEvents.CERTIFICATE_ISSUED` and `CERTIFICATE_RENEWED` and re-emit on `SmartProxy` as `'certificate'` events (include `domain`, `publicKey`, `privateKey`, `expiryDate`, `source: 'http01'`, `isRenewal` flag).
|
||||
- [x] Extend NetworkProxyBridge (ts/smartproxy/classes.pp.networkproxybridge.ts):
|
||||
- Add public method `applyExternalCertificate(data: ICertificateData): void` to forward static certs into `NetworkProxy`.
|
||||
- [ ] Define `SmartProxy` `'certificate'` event interface in TypeScript and update documentation.
|
||||
- [ ] Update README with usage examples showing `certProvider` callback and listening for `'certificate'` events.
|
||||
- [ ] Remove renewal logic from Port80Handler
|
||||
- Delete `startRenewalTimer()` and `checkForRenewals()` methods
|
||||
- Remove `renewThresholdDays` and `renewCheckIntervalHours` options from `IPort80HandlerOptions`
|
||||
- [ ] Expose certificate status from Port80Handler
|
||||
- Ensure `getDomainCertificateStatus()` returns `{certObtained, expiryDate}` for each domain
|
||||
- [ ] Add renewal settings to SmartProxy
|
||||
- Extend `port80HandlerConfig` to include `renewThresholdDays` and `renewCheckIntervalHours`
|
||||
- [ ] Implement renewal scheduler in SmartProxy using taskbuffer
|
||||
- Add dependency on `@push.rocks/taskbuffer` and import `{ Task, TaskManager }` in `SmartProxy`
|
||||
- Add `performRenewals()` to iterate domains and trigger renewals where `daysRemaining <= renewThresholdDays`
|
||||
- Instantiate a `TaskManager` and define a `Task` that wraps `performRenewals()`
|
||||
- Use `taskManager.addAndScheduleTask(task, cronExpr)` to schedule renewals, building `cronExpr` from `renewCheckIntervalHours` (e.g. `0 0 */${renewCheckIntervalHours} * * *`)
|
||||
- Call `taskManager.start()` in `SmartProxy.start()`
|
||||
- [ ] Clean shutdown handling
|
||||
- Call `taskManager.stop()` in `SmartProxy.stop()` alongside other cleanup
|
||||
- [ ] Throttling and safety
|
||||
- Skip domains already in `obtainingInProgress`
|
||||
- Optionally batch or stagger renewal calls for large domain sets
|
||||
- [ ] Tests
|
||||
- Unit test `performRenewals()`, mocking `getDomainCertificateStatus()` to simulate expiring certificates
|
||||
- Integration test using an in-memory `Port80Handler` to verify that scheduled renewals invoke `obtainCertificate()` correctly
|
||||
- [ ] Documentation
|
||||
- Update `readme.plan.md` (this section)
|
||||
- Update `README.md` and code comments to document new renewal settings and workflow
|
||||
|
45
test/test.smartproxy.renewals.node.ts
Normal file
45
test/test.smartproxy.renewals.node.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { SmartProxy } from '../ts/smartproxy/classes.smartproxy.js';
|
||||
|
||||
tap.test('performRenewals only renews domains below threshold', async () => {
|
||||
// Set up SmartProxy instance without real servers
|
||||
const proxy = new SmartProxy({
|
||||
fromPort: 0,
|
||||
toPort: 0,
|
||||
domainConfigs: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: [],
|
||||
globalPortRanges: []
|
||||
});
|
||||
// Stub port80Handler status and renewal
|
||||
const statuses = new Map<string, any>();
|
||||
const now = new Date();
|
||||
statuses.set('expiring.com', {
|
||||
certObtained: true,
|
||||
expiryDate: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000),
|
||||
obtainingInProgress: false
|
||||
});
|
||||
statuses.set('ok.com', {
|
||||
certObtained: true,
|
||||
expiryDate: new Date(now.getTime() + 100 * 24 * 60 * 60 * 1000),
|
||||
obtainingInProgress: false
|
||||
});
|
||||
const renewed: string[] = [];
|
||||
// Inject fake handler
|
||||
(proxy as any).port80Handler = {
|
||||
getDomainCertificateStatus: () => statuses,
|
||||
renewCertificate: async (domain: string) => { renewed.push(domain); }
|
||||
};
|
||||
// Configure threshold
|
||||
proxy.settings.port80HandlerConfig.enabled = true;
|
||||
proxy.settings.port80HandlerConfig.autoRenew = true;
|
||||
proxy.settings.port80HandlerConfig.renewThresholdDays = 10;
|
||||
|
||||
// Execute renewals
|
||||
await (proxy as any).performRenewals();
|
||||
|
||||
// Only the expiring.com domain should be renewed
|
||||
expect(renewed).toEqual(['expiring.com']);
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -7,7 +7,6 @@ import * as tls from 'tls';
|
||||
import * as url from 'url';
|
||||
import * as http2 from 'http2';
|
||||
|
||||
|
||||
export { EventEmitter, http, https, net, tls, url, http2 };
|
||||
|
||||
// tsclass scope
|
||||
@ -25,7 +24,19 @@ import * as smartstring from '@push.rocks/smartstring';
|
||||
import * as smartacme from '@push.rocks/smartacme';
|
||||
import * as smartacmePlugins from '@push.rocks/smartacme/dist_ts/smartacme.plugins.js';
|
||||
import * as smartacmeHandlers from '@push.rocks/smartacme/dist_ts/handlers/index.js';
|
||||
export { lik, smartdelay, smartrequest, smartpromise, smartstring, smartacme, smartacmePlugins, smartacmeHandlers };
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartdelay,
|
||||
smartrequest,
|
||||
smartpromise,
|
||||
smartstring,
|
||||
smartacme,
|
||||
smartacmePlugins,
|
||||
smartacmeHandlers,
|
||||
taskbuffer,
|
||||
};
|
||||
|
||||
// third party scope
|
||||
import prettyMs from 'pretty-ms';
|
||||
|
@ -85,9 +85,7 @@ interface IPort80HandlerOptions {
|
||||
port?: number;
|
||||
contactEmail?: string;
|
||||
useProduction?: boolean;
|
||||
renewThresholdDays?: number;
|
||||
httpsRedirectPort?: number;
|
||||
renewCheckIntervalHours?: number;
|
||||
enabled?: boolean; // Whether ACME is enabled at all
|
||||
autoRenew?: boolean; // Whether to automatically renew certificates
|
||||
certificateStore?: string; // Directory to store certificates
|
||||
@ -146,7 +144,8 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// SmartAcme instance for certificate management
|
||||
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
||||
private server: plugins.http.Server | null = null;
|
||||
private renewalTimer: NodeJS.Timeout | null = null;
|
||||
// Renewal scheduling is handled externally by SmartProxy
|
||||
// (Removed internal renewal timer)
|
||||
private isShuttingDown: boolean = false;
|
||||
private options: Required<IPort80HandlerOptions>;
|
||||
|
||||
@ -163,9 +162,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
port: options.port ?? 80,
|
||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||
renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
|
||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
||||
enabled: options.enabled ?? true, // Enable by default
|
||||
autoRenew: options.autoRenew ?? true, // Auto-renew by default
|
||||
certificateStore: options.certificateStore ?? './certs', // Default store location
|
||||
@ -223,7 +220,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.server.listen(this.options.port, () => {
|
||||
console.log(`Port80Handler is listening on port ${this.options.port}`);
|
||||
this.startRenewalTimer();
|
||||
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
|
||||
|
||||
// Start certificate process for domains with acmeMaintenance enabled
|
||||
@ -260,11 +256,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Stop the renewal timer
|
||||
if (this.renewalTimer) {
|
||||
clearInterval(this.renewalTimer);
|
||||
this.renewalTimer = null;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.server) {
|
||||
@ -830,89 +821,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the certificate renewal timer
|
||||
*/
|
||||
private startRenewalTimer(): void {
|
||||
if (this.renewalTimer) {
|
||||
clearInterval(this.renewalTimer);
|
||||
}
|
||||
|
||||
// Convert hours to milliseconds
|
||||
const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
|
||||
|
||||
this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
|
||||
|
||||
// Prevent the timer from keeping the process alive
|
||||
if (this.renewalTimer.unref) {
|
||||
this.renewalTimer.unref();
|
||||
}
|
||||
|
||||
console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for certificates that need renewal
|
||||
*/
|
||||
private checkForRenewals(): void {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip renewal if auto-renewal is disabled
|
||||
if (this.options.autoRenew === false) {
|
||||
console.log('Auto-renewal is disabled, skipping certificate renewal check');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking for certificates that need renewal...');
|
||||
|
||||
const now = new Date();
|
||||
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;
|
||||
}
|
||||
|
||||
// Skip domains without certificates or already in renewal
|
||||
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains without expiry dates
|
||||
if (!domainInfo.expiryDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
|
||||
|
||||
// Check if certificate is near expiry
|
||||
if (timeUntilExpiry <= renewThresholdMs) {
|
||||
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
||||
|
||||
const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
|
||||
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
|
||||
domain,
|
||||
expiryDate: domainInfo.expiryDate,
|
||||
daysRemaining
|
||||
} as ICertificateExpiring);
|
||||
|
||||
// Start renewal process
|
||||
this.obtainCertificate(domain, true).catch(err => {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`Error renewing certificate for ${domain}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract expiry date from certificate using a more robust approach
|
||||
@ -1041,4 +949,16 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
public getConfig(): Required<IPort80HandlerOptions> {
|
||||
return { ...this.options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a certificate renewal for a specific domain.
|
||||
* @param domain The domain to renew.
|
||||
*/
|
||||
public async renewCertificate(domain: string): Promise<void> {
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
throw new Port80HandlerError(`Domain not managed: ${domain}`);
|
||||
}
|
||||
// Trigger renewal via ACME
|
||||
await this.obtainCertificate(domain, true);
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
// Port80Handler for ACME certificate management
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
// Renewal scheduler for certificates
|
||||
private renewManager?: plugins.taskbuffer.TaskManager;
|
||||
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
super();
|
||||
@ -157,9 +159,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
port: config.port,
|
||||
contactEmail: config.contactEmail,
|
||||
useProduction: config.useProduction,
|
||||
renewThresholdDays: config.renewThresholdDays,
|
||||
httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort,
|
||||
renewCheckIntervalHours: config.renewCheckIntervalHours,
|
||||
enabled: config.enabled,
|
||||
autoRenew: config.autoRenew,
|
||||
certificateStore: config.certificateStore,
|
||||
@ -258,6 +258,21 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Start Port80Handler
|
||||
await this.port80Handler.start();
|
||||
console.log(`Port80Handler started on port ${config.port}`);
|
||||
// Schedule certificate renewals using taskbuffer
|
||||
if (config.autoRenew) {
|
||||
this.renewManager = new plugins.taskbuffer.TaskManager();
|
||||
const renewTask = new plugins.taskbuffer.Task({
|
||||
name: 'CertificateRenewals',
|
||||
taskFunction: async () => {
|
||||
await (this as any).performRenewals();
|
||||
}
|
||||
});
|
||||
const hours = config.renewCheckIntervalHours!;
|
||||
const cronExpr = `0 0 */${hours} * * *`;
|
||||
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
|
||||
this.renewManager.start();
|
||||
console.log(`Scheduled certificate renewals every ${hours} hours`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error initializing Port80Handler: ${err}`);
|
||||
}
|
||||
@ -403,6 +418,11 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
public async stop() {
|
||||
console.log('PortProxy shutting down...');
|
||||
this.isShuttingDown = true;
|
||||
// Stop the certificate renewal scheduler if active
|
||||
if (this.renewManager) {
|
||||
this.renewManager.stop();
|
||||
console.log('Certificate renewal scheduler stopped');
|
||||
}
|
||||
|
||||
// Stop the Port80Handler if running
|
||||
if (this.port80Handler) {
|
||||
@ -572,6 +592,27 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform scheduled renewals for managed domains
|
||||
*/
|
||||
private async performRenewals(): Promise<void> {
|
||||
if (!this.port80Handler) return;
|
||||
const statuses = this.port80Handler.getDomainCertificateStatus();
|
||||
const threshold = this.settings.port80HandlerConfig.renewThresholdDays ?? 30;
|
||||
const now = new Date();
|
||||
for (const [domain, status] of statuses.entries()) {
|
||||
if (!status.certObtained || status.obtainingInProgress || !status.expiryDate) continue;
|
||||
const msRemaining = status.expiryDate.getTime() - now.getTime();
|
||||
const daysRemaining = Math.ceil(msRemaining / (24 * 60 * 60 * 1000));
|
||||
if (daysRemaining <= threshold) {
|
||||
try {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} catch (err) {
|
||||
console.error(`Error renewing certificate for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Request a certificate for a specific domain
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user