update structure
This commit is contained in:
		| @@ -1,7 +1,9 @@ | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import type { IAcmeOptions } from './types.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import type { AcmeOptions } from '../models/certificate-types.js'; | ||||
| import { ensureCertificateDirectory } from '../utils/certificate-helpers.js'; | ||||
| // We'll need to update this import when we move the Port80Handler | ||||
| import { Port80Handler } from '../../port80handler/classes.port80handler.js'; | ||||
|  | ||||
| /** | ||||
|  * Factory to create a Port80Handler with common setup. | ||||
| @@ -10,14 +12,37 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
|  * @returns A new Port80Handler instance | ||||
|  */ | ||||
| export function buildPort80Handler( | ||||
|   options: IAcmeOptions | ||||
|   options: AcmeOptions | ||||
| ): Port80Handler { | ||||
|   if (options.certificateStore) { | ||||
|     const certStorePath = path.resolve(options.certificateStore); | ||||
|     if (!fs.existsSync(certStorePath)) { | ||||
|       fs.mkdirSync(certStorePath, { recursive: true }); | ||||
|       console.log(`Created certificate store directory: ${certStorePath}`); | ||||
|     } | ||||
|     ensureCertificateDirectory(options.certificateStore); | ||||
|     console.log(`Ensured certificate store directory: ${options.certificateStore}`); | ||||
|   } | ||||
|   return new Port80Handler(options); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates default ACME options with sensible defaults | ||||
|  * @param email Account email for ACME provider | ||||
|  * @param certificateStore Path to store certificates | ||||
|  * @param useProduction Whether to use production ACME servers | ||||
|  * @returns Configured ACME options | ||||
|  */ | ||||
| export function createDefaultAcmeOptions( | ||||
|   email: string, | ||||
|   certificateStore: string, | ||||
|   useProduction: boolean = false | ||||
| ): AcmeOptions { | ||||
|   return { | ||||
|     accountEmail: email, | ||||
|     enabled: true, | ||||
|     port: 80, | ||||
|     useProduction, | ||||
|     httpsRedirectPort: 443, | ||||
|     renewThresholdDays: 30, | ||||
|     renewCheckIntervalHours: 24, | ||||
|     autoRenew: true, | ||||
|     certificateStore, | ||||
|     skipConfiguredCerts: false | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										110
									
								
								ts/certificate/acme/challenge-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								ts/certificate/acme/challenge-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { AcmeOptions, CertificateData } from '../models/certificate-types.js'; | ||||
| import { CertificateEvents } from '../events/certificate-events.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages ACME challenges and certificate validation | ||||
|  */ | ||||
| export class AcmeChallengeHandler extends plugins.EventEmitter { | ||||
|   private options: AcmeOptions; | ||||
|   private client: any; // ACME client from plugins | ||||
|   private pendingChallenges: Map<string, any>; | ||||
|  | ||||
|   /** | ||||
|    * Creates a new ACME challenge handler | ||||
|    * @param options ACME configuration options | ||||
|    */ | ||||
|   constructor(options: AcmeOptions) { | ||||
|     super(); | ||||
|     this.options = options; | ||||
|     this.pendingChallenges = new Map(); | ||||
|  | ||||
|     // Initialize ACME client if needed | ||||
|     // This is just a placeholder implementation since we don't use the actual | ||||
|     // client directly in this implementation - it's handled by Port80Handler | ||||
|     this.client = null; | ||||
|     console.log('Created challenge handler with options:', | ||||
|       options.accountEmail, | ||||
|       options.useProduction ? 'production' : 'staging' | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Gets or creates the ACME account key | ||||
|    */ | ||||
|   private getAccountKey(): Buffer { | ||||
|     // Implementation details would depend on plugin requirements | ||||
|     // This is a simplified version | ||||
|     if (!this.options.certificateStore) { | ||||
|       throw new Error('Certificate store is required for ACME challenges'); | ||||
|     } | ||||
|      | ||||
|     // This is just a placeholder - actual implementation would check for | ||||
|     // existing account key and create one if needed | ||||
|     return Buffer.from('account-key-placeholder'); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Validates a domain using HTTP-01 challenge | ||||
|    * @param domain Domain to validate | ||||
|    * @param challengeToken ACME challenge token | ||||
|    * @param keyAuthorization Key authorization for the challenge | ||||
|    */ | ||||
|   public async handleHttpChallenge( | ||||
|     domain: string, | ||||
|     challengeToken: string, | ||||
|     keyAuthorization: string | ||||
|   ): Promise<void> { | ||||
|     // Store challenge for response | ||||
|     this.pendingChallenges.set(challengeToken, keyAuthorization); | ||||
|      | ||||
|     try { | ||||
|       // Wait for challenge validation - this would normally be handled by the ACME client | ||||
|       await new Promise(resolve => setTimeout(resolve, 1000)); | ||||
|       this.emit(CertificateEvents.CERTIFICATE_ISSUED, { | ||||
|         domain, | ||||
|         success: true | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       this.emit(CertificateEvents.CERTIFICATE_FAILED, { | ||||
|         domain, | ||||
|         error: error instanceof Error ? error.message : String(error), | ||||
|         isRenewal: false | ||||
|       }); | ||||
|       throw error; | ||||
|     } finally { | ||||
|       // Clean up the challenge | ||||
|       this.pendingChallenges.delete(challengeToken); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Responds to an HTTP-01 challenge request | ||||
|    * @param token Challenge token from the request path | ||||
|    * @returns The key authorization if found | ||||
|    */ | ||||
|   public getChallengeResponse(token: string): string | null { | ||||
|     return this.pendingChallenges.get(token) || null; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Checks if a request path is an ACME challenge | ||||
|    * @param path Request path | ||||
|    * @returns True if this is an ACME challenge request | ||||
|    */ | ||||
|   public isAcmeChallenge(path: string): boolean { | ||||
|     return path.startsWith('/.well-known/acme-challenge/'); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Extracts the challenge token from an ACME challenge path | ||||
|    * @param path Request path | ||||
|    * @returns The challenge token if valid | ||||
|    */ | ||||
|   public extractChallengeToken(path: string): string | null { | ||||
|     if (!this.isAcmeChallenge(path)) return null; | ||||
|      | ||||
|     const parts = path.split('/'); | ||||
|     return parts[parts.length - 1] || null; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								ts/certificate/acme/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts/certificate/acme/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /** | ||||
|  * ACME certificate provisioning | ||||
|  */ | ||||
							
								
								
									
										32
									
								
								ts/certificate/events/certificate-events.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ts/certificate/events/certificate-events.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| /** | ||||
|  * Certificate-related events emitted by certificate management components | ||||
|  */ | ||||
| export enum CertificateEvents { | ||||
|   CERTIFICATE_ISSUED = 'certificate-issued', | ||||
|   CERTIFICATE_RENEWED = 'certificate-renewed', | ||||
|   CERTIFICATE_FAILED = 'certificate-failed', | ||||
|   CERTIFICATE_EXPIRING = 'certificate-expiring', | ||||
|   CERTIFICATE_APPLIED = 'certificate-applied', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Port80Handler-specific events including certificate-related ones | ||||
|  */ | ||||
| export enum Port80HandlerEvents { | ||||
|   CERTIFICATE_ISSUED = 'certificate-issued', | ||||
|   CERTIFICATE_RENEWED = 'certificate-renewed', | ||||
|   CERTIFICATE_FAILED = 'certificate-failed', | ||||
|   CERTIFICATE_EXPIRING = 'certificate-expiring', | ||||
|   MANAGER_STARTED = 'manager-started', | ||||
|   MANAGER_STOPPED = 'manager-stopped', | ||||
|   REQUEST_FORWARDED = 'request-forwarded', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate provider events | ||||
|  */ | ||||
| export enum CertProvisionerEvents { | ||||
|   CERTIFICATE_ISSUED = 'certificate', | ||||
|   CERTIFICATE_RENEWED = 'certificate', | ||||
|   CERTIFICATE_FAILED = 'certificate-failed' | ||||
| } | ||||
							
								
								
									
										67
									
								
								ts/certificate/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								ts/certificate/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /** | ||||
|  * Certificate management module for SmartProxy | ||||
|  * Provides certificate provisioning, storage, and management capabilities | ||||
|  */ | ||||
|  | ||||
| // Certificate types and models | ||||
| export * from './models/certificate-types.js'; | ||||
|  | ||||
| // Certificate events | ||||
| export * from './events/certificate-events.js'; | ||||
|  | ||||
| // Certificate providers | ||||
| export * from './providers/cert-provisioner.js'; | ||||
|  | ||||
| // ACME related exports | ||||
| export * from './acme/acme-factory.js'; | ||||
| export * from './acme/challenge-handler.js'; | ||||
|  | ||||
| // Certificate utilities | ||||
| export * from './utils/certificate-helpers.js'; | ||||
|  | ||||
| // Certificate storage | ||||
| export * from './storage/file-storage.js'; | ||||
|  | ||||
| // Convenience function to create a certificate provisioner with common settings | ||||
| import { CertProvisioner } from './providers/cert-provisioner.js'; | ||||
| import { buildPort80Handler } from './acme/acme-factory.js'; | ||||
| import type { AcmeOptions, DomainForwardConfig } from './models/certificate-types.js'; | ||||
| import type { DomainConfig } from '../forwarding/config/domain-config.js'; | ||||
|  | ||||
| /** | ||||
|  * Creates a complete certificate provisioning system with default settings | ||||
|  * @param domainConfigs Domain configurations | ||||
|  * @param acmeOptions ACME options for certificate provisioning | ||||
|  * @param networkProxyBridge Bridge to apply certificates to network proxy | ||||
|  * @param certProvider Optional custom certificate provider | ||||
|  * @returns Configured CertProvisioner | ||||
|  */ | ||||
| export function createCertificateProvisioner( | ||||
|   domainConfigs: DomainConfig[], | ||||
|   acmeOptions: AcmeOptions, | ||||
|   networkProxyBridge: any, // Placeholder until NetworkProxyBridge is migrated | ||||
|   certProvider?: any // Placeholder until cert provider type is properly defined | ||||
| ): CertProvisioner { | ||||
|   // Build the Port80Handler for ACME challenges | ||||
|   const port80Handler = buildPort80Handler(acmeOptions); | ||||
|  | ||||
|   // Extract ACME-specific configuration | ||||
|   const { | ||||
|     renewThresholdDays = 30, | ||||
|     renewCheckIntervalHours = 24, | ||||
|     autoRenew = true, | ||||
|     domainForwards = [] | ||||
|   } = acmeOptions; | ||||
|  | ||||
|   // Create and return the certificate provisioner | ||||
|   return new CertProvisioner( | ||||
|     domainConfigs, | ||||
|     port80Handler, | ||||
|     networkProxyBridge, | ||||
|     certProvider, | ||||
|     renewThresholdDays, | ||||
|     renewCheckIntervalHours, | ||||
|     autoRenew, | ||||
|     domainForwards | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										97
									
								
								ts/certificate/models/certificate-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								ts/certificate/models/certificate-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Certificate data structure containing all necessary information | ||||
|  * about a certificate | ||||
|  */ | ||||
| export interface CertificateData { | ||||
|   domain: string; | ||||
|   certificate: string; | ||||
|   privateKey: string; | ||||
|   expiryDate: Date; | ||||
|   // Optional source and renewal information for event emissions | ||||
|   source?: 'static' | 'http01' | 'dns01'; | ||||
|   isRenewal?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificates pair (private and public keys) | ||||
|  */ | ||||
| export interface Certificates { | ||||
|   privateKey: string; | ||||
|   publicKey: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate failure payload type | ||||
|  */ | ||||
| export interface CertificateFailure { | ||||
|   domain: string; | ||||
|   error: string; | ||||
|   isRenewal: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate expiry payload type | ||||
|  */ | ||||
| export interface CertificateExpiring { | ||||
|   domain: string; | ||||
|   expiryDate: Date; | ||||
|   daysRemaining: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain forwarding configuration | ||||
|  */ | ||||
| export interface ForwardConfig { | ||||
|   ip: string; | ||||
|   port: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain-specific forwarding configuration for ACME challenges | ||||
|  */ | ||||
| export interface DomainForwardConfig { | ||||
|   domain: string; | ||||
|   forwardConfig?: ForwardConfig; | ||||
|   acmeForwardConfig?: ForwardConfig; | ||||
|   sslRedirect?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain configuration options | ||||
|  */ | ||||
| export interface DomainOptions { | ||||
|   domainName: string; | ||||
|   sslRedirect: boolean;   // if true redirects the request to port 443 | ||||
|   acmeMaintenance: boolean; // tries to always have a valid cert for this domain | ||||
|   forward?: ForwardConfig; // forwards all http requests to that target | ||||
|   acmeForward?: ForwardConfig; // forwards letsencrypt requests to this config | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified ACME configuration options used across proxies and handlers | ||||
|  */ | ||||
| export interface AcmeOptions { | ||||
|   accountEmail?: string;          // Email for Let's Encrypt account | ||||
|   enabled?: boolean;              // Whether ACME is enabled | ||||
|   port?: number;                  // Port to listen on for ACME challenges (default: 80) | ||||
|   useProduction?: boolean;        // Use production environment (default: staging) | ||||
|   httpsRedirectPort?: number;     // Port to redirect HTTP requests to HTTPS (default: 443) | ||||
|   renewThresholdDays?: number;    // Days before expiry to renew certificates | ||||
|   renewCheckIntervalHours?: number; // How often to check for renewals (in hours) | ||||
|   autoRenew?: boolean;            // Whether to automatically renew certificates | ||||
|   certificateStore?: string;      // Directory to store certificates | ||||
|   skipConfiguredCerts?: boolean;  // Skip domains with existing certificates | ||||
|   domainForwards?: DomainForwardConfig[]; // Domain-specific forwarding configs | ||||
| } | ||||
|  | ||||
| // Backwards compatibility interfaces | ||||
| export interface ICertificates extends Certificates {} | ||||
| export interface ICertificateData extends CertificateData {} | ||||
| export interface ICertificateFailure extends CertificateFailure {} | ||||
| export interface ICertificateExpiring extends CertificateExpiring {} | ||||
| export interface IForwardConfig extends ForwardConfig {} | ||||
| export interface IDomainForwardConfig extends DomainForwardConfig {} | ||||
| export interface IDomainOptions extends DomainOptions {} | ||||
| export interface IAcmeOptions extends AcmeOptions {} | ||||
| @@ -1,27 +1,40 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80HandlerEvents } from '../common/types.js'; | ||||
| import { subscribeToPort80Handler } from '../common/eventUtils.js'; | ||||
| import type { ICertificateData } from '../common/types.js'; | ||||
| import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { DomainConfig } from '../../forwarding/config/domain-config.js'; | ||||
| import type { CertificateData, DomainForwardConfig, DomainOptions } from '../models/certificate-types.js'; | ||||
| import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js'; | ||||
| import { Port80Handler } from '../../port80handler/classes.port80handler.js'; | ||||
| // We need to define this interface until we migrate NetworkProxyBridge | ||||
| interface NetworkProxyBridge { | ||||
|   applyExternalCertificate(certData: CertificateData): void; | ||||
| } | ||||
|  | ||||
| // This will be imported after NetworkProxyBridge is migrated | ||||
| // import type { NetworkProxyBridge } from '../../proxies/smart-proxy/network-proxy-bridge.js'; | ||||
|  | ||||
| // For backward compatibility | ||||
| export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||
|  | ||||
| /** | ||||
|  * Type for static certificate provisioning | ||||
|  */ | ||||
| export type CertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01'; | ||||
|  | ||||
| /** | ||||
|  * CertProvisioner manages certificate provisioning and renewal workflows, | ||||
|  * unifying static certificates and HTTP-01 challenges via Port80Handler. | ||||
|  */ | ||||
| export class CertProvisioner extends plugins.EventEmitter { | ||||
|   private domainConfigs: IDomainConfig[]; | ||||
|   private domainConfigs: DomainConfig[]; | ||||
|   private port80Handler: Port80Handler; | ||||
|   private networkProxyBridge: NetworkProxyBridge; | ||||
|   private certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>; | ||||
|   private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>; | ||||
|   private certProvisionFunction?: (domain: string) => Promise<CertProvisionObject>; | ||||
|   private forwardConfigs: DomainForwardConfig[]; | ||||
|   private renewThresholdDays: number; | ||||
|   private renewCheckIntervalHours: number; | ||||
|   private autoRenew: boolean; | ||||
|   private renewManager?: plugins.taskbuffer.TaskManager; | ||||
|   // Track provisioning type per domain: 'http01' or 'static' | ||||
|   private provisionMap: Map<string, 'http01' | 'static'>; | ||||
|   // Track provisioning type per domain | ||||
|   private provisionMap: Map<string, 'http01' | 'dns01' | 'static'>; | ||||
|  | ||||
|   /** | ||||
|    * @param domainConfigs Array of domain configuration objects | ||||
| @@ -31,16 +44,17 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|    * @param renewThresholdDays Days before expiry to trigger renewals | ||||
|    * @param renewCheckIntervalHours Interval in hours to check for renewals | ||||
|    * @param autoRenew Whether to automatically schedule renewals | ||||
|    * @param forwardConfigs Domain forwarding configurations for ACME challenges | ||||
|    */ | ||||
|   constructor( | ||||
|     domainConfigs: IDomainConfig[], | ||||
|     domainConfigs: DomainConfig[], | ||||
|     port80Handler: Port80Handler, | ||||
|     networkProxyBridge: NetworkProxyBridge, | ||||
|     certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>, | ||||
|     certProvider?: (domain: string) => Promise<CertProvisionObject>, | ||||
|     renewThresholdDays: number = 30, | ||||
|     renewCheckIntervalHours: number = 24, | ||||
|     autoRenew: boolean = true, | ||||
|     forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }> = [] | ||||
|     forwardConfigs: DomainForwardConfig[] = [] | ||||
|   ) { | ||||
|     super(); | ||||
|     this.domainConfigs = domainConfigs; | ||||
| @@ -59,99 +73,180 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     // Subscribe to Port80Handler certificate events | ||||
|     subscribeToPort80Handler(this.port80Handler, { | ||||
|       onCertificateIssued: (data: ICertificateData) => { | ||||
|         this.emit('certificate', { ...data, source: 'http01', isRenewal: false }); | ||||
|       }, | ||||
|       onCertificateRenewed: (data: ICertificateData) => { | ||||
|         this.emit('certificate', { ...data, source: 'http01', isRenewal: true }); | ||||
|       } | ||||
|     }); | ||||
|     this.setupEventSubscriptions(); | ||||
|  | ||||
|     // Apply external forwarding for ACME challenges | ||||
|     this.setupForwardingConfigs(); | ||||
|  | ||||
|     // Apply external forwarding for ACME challenges (e.g. Synology) | ||||
|     for (const f of this.forwardConfigs) { | ||||
|       this.port80Handler.addDomain({ | ||||
|         domainName: f.domain, | ||||
|         sslRedirect: f.sslRedirect, | ||||
|         acmeMaintenance: false, | ||||
|         forward: f.forwardConfig, | ||||
|         acmeForward: f.acmeForwardConfig | ||||
|       }); | ||||
|     } | ||||
|     // Initial provisioning for all domains | ||||
|     const domains = this.domainConfigs.flatMap(cfg => cfg.domains); | ||||
|     for (const domain of domains) { | ||||
|       const isWildcard = domain.includes('*'); | ||||
|       let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01'; | ||||
|       if (this.certProvisionFunction) { | ||||
|         try { | ||||
|           provision = await this.certProvisionFunction(domain); | ||||
|         } catch (err) { | ||||
|           console.error(`certProvider error for ${domain}:`, err); | ||||
|         } | ||||
|       } else if (isWildcard) { | ||||
|         // No certProvider: cannot handle wildcard without DNS-01 support | ||||
|         console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); | ||||
|         continue; | ||||
|       } | ||||
|       if (provision === 'http01') { | ||||
|         if (isWildcard) { | ||||
|           console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); | ||||
|           continue; | ||||
|         } | ||||
|         this.provisionMap.set(domain, 'http01'); | ||||
|         this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); | ||||
|       } else { | ||||
|         // Static certificate (e.g., DNS-01 provisioned or user-provided) supports wildcard domains | ||||
|         this.provisionMap.set(domain, 'static'); | ||||
|         const certObj = provision as plugins.tsclass.network.ICert; | ||||
|         const certData: ICertificateData = { | ||||
|           domain: certObj.domainName, | ||||
|           certificate: certObj.publicKey, | ||||
|           privateKey: certObj.privateKey, | ||||
|           expiryDate: new Date(certObj.validUntil) | ||||
|         }; | ||||
|         this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|         this.emit('certificate', { ...certData, source: 'static', isRenewal: false }); | ||||
|       } | ||||
|     } | ||||
|     await this.provisionAllDomains(); | ||||
|  | ||||
|     // Schedule renewals if enabled | ||||
|     if (this.autoRenew) { | ||||
|       this.renewManager = new plugins.taskbuffer.TaskManager(); | ||||
|       const renewTask = new plugins.taskbuffer.Task({ | ||||
|         name: 'CertificateRenewals', | ||||
|         taskFunction: async () => { | ||||
|           for (const [domain, type] of this.provisionMap.entries()) { | ||||
|             // Skip wildcard domains | ||||
|             if (domain.includes('*')) continue; | ||||
|             try { | ||||
|               if (type === 'http01') { | ||||
|                 await this.port80Handler.renewCertificate(domain); | ||||
|               } else if (type === 'static' && this.certProvisionFunction) { | ||||
|                 const provision2 = await this.certProvisionFunction(domain); | ||||
|                 if (provision2 !== 'http01') { | ||||
|                   const certObj = provision2 as plugins.tsclass.network.ICert; | ||||
|                   const certData: ICertificateData = { | ||||
|                     domain: certObj.domainName, | ||||
|                     certificate: certObj.publicKey, | ||||
|                     privateKey: certObj.privateKey, | ||||
|                     expiryDate: new Date(certObj.validUntil) | ||||
|                   }; | ||||
|                   this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|                   this.emit('certificate', { ...certData, source: 'static', isRenewal: true }); | ||||
|                 } | ||||
|               } | ||||
|             } catch (err) { | ||||
|               console.error(`Renewal error for ${domain}:`, err); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       this.scheduleRenewals(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set up event subscriptions for certificate events | ||||
|    */ | ||||
|   private setupEventSubscriptions(): void { | ||||
|     // We need to reimplement subscribeToPort80Handler here | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: CertificateData) => { | ||||
|       this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, { ...data, source: 'http01', isRenewal: false }); | ||||
|     }); | ||||
|  | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: CertificateData) => { | ||||
|       this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, { ...data, source: 'http01', isRenewal: true }); | ||||
|     }); | ||||
|  | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (error) => { | ||||
|       this.emit(CertProvisionerEvents.CERTIFICATE_FAILED, error); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set up forwarding configurations for the Port80Handler | ||||
|    */ | ||||
|   private setupForwardingConfigs(): void { | ||||
|     for (const config of this.forwardConfigs) { | ||||
|       const domainOptions: DomainOptions = { | ||||
|         domainName: config.domain, | ||||
|         sslRedirect: config.sslRedirect || false, | ||||
|         acmeMaintenance: false, | ||||
|         forward: config.forwardConfig, | ||||
|         acmeForward: config.acmeForwardConfig | ||||
|       }; | ||||
|       this.port80Handler.addDomain(domainOptions); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Provision certificates for all configured domains | ||||
|    */ | ||||
|   private async provisionAllDomains(): Promise<void> { | ||||
|     const domains = this.domainConfigs.flatMap(cfg => cfg.domains); | ||||
|  | ||||
|     for (const domain of domains) { | ||||
|       await this.provisionDomain(domain); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Provision a certificate for a single domain | ||||
|    * @param domain Domain to provision | ||||
|    */ | ||||
|   private async provisionDomain(domain: string): Promise<void> { | ||||
|     const isWildcard = domain.includes('*'); | ||||
|     let provision: CertProvisionObject = 'http01'; | ||||
|  | ||||
|     // Try to get a certificate from the provision function | ||||
|     if (this.certProvisionFunction) { | ||||
|       try { | ||||
|         provision = await this.certProvisionFunction(domain); | ||||
|       } catch (err) { | ||||
|         console.error(`certProvider error for ${domain}:`, err); | ||||
|       } | ||||
|     } else if (isWildcard) { | ||||
|       // No certProvider: cannot handle wildcard without DNS-01 support | ||||
|       console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Handle different provisioning methods | ||||
|     if (provision === 'http01') { | ||||
|       if (isWildcard) { | ||||
|         console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.provisionMap.set(domain, 'http01'); | ||||
|       this.port80Handler.addDomain({ | ||||
|         domainName: domain, | ||||
|         sslRedirect: true, | ||||
|         acmeMaintenance: true | ||||
|       }); | ||||
|       const hours = this.renewCheckIntervalHours; | ||||
|       const cronExpr = `0 0 */${hours} * * *`; | ||||
|       this.renewManager.addAndScheduleTask(renewTask, cronExpr); | ||||
|       this.renewManager.start(); | ||||
|     } else if (provision === 'dns01') { | ||||
|       // DNS-01 challenges would be handled by the certProvisionFunction | ||||
|       this.provisionMap.set(domain, 'dns01'); | ||||
|       // DNS-01 handling would go here if implemented | ||||
|     } else { | ||||
|       // Static certificate (e.g., DNS-01 provisioned or user-provided) | ||||
|       this.provisionMap.set(domain, 'static'); | ||||
|       const certObj = provision as plugins.tsclass.network.ICert; | ||||
|       const certData: CertificateData = { | ||||
|         domain: certObj.domainName, | ||||
|         certificate: certObj.publicKey, | ||||
|         privateKey: certObj.privateKey, | ||||
|         expiryDate: new Date(certObj.validUntil), | ||||
|         source: 'static', | ||||
|         isRenewal: false | ||||
|       }; | ||||
|  | ||||
|       this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|       this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Schedule certificate renewals using a task manager | ||||
|    */ | ||||
|   private scheduleRenewals(): void { | ||||
|     this.renewManager = new plugins.taskbuffer.TaskManager(); | ||||
|  | ||||
|     const renewTask = new plugins.taskbuffer.Task({ | ||||
|       name: 'CertificateRenewals', | ||||
|       taskFunction: async () => await this.performRenewals() | ||||
|     }); | ||||
|  | ||||
|     const hours = this.renewCheckIntervalHours; | ||||
|     const cronExpr = `0 0 */${hours} * * *`; | ||||
|  | ||||
|     this.renewManager.addAndScheduleTask(renewTask, cronExpr); | ||||
|     this.renewManager.start(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Perform renewals for all domains that need it | ||||
|    */ | ||||
|   private async performRenewals(): Promise<void> { | ||||
|     for (const [domain, type] of this.provisionMap.entries()) { | ||||
|       // Skip wildcard domains for HTTP-01 challenges | ||||
|       if (domain.includes('*') && type === 'http01') continue; | ||||
|  | ||||
|       try { | ||||
|         await this.renewDomain(domain, type); | ||||
|       } catch (err) { | ||||
|         console.error(`Renewal error for ${domain}:`, err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Renew a certificate for a specific domain | ||||
|    * @param domain Domain to renew | ||||
|    * @param provisionType Type of provisioning for this domain | ||||
|    */ | ||||
|   private async renewDomain(domain: string, provisionType: 'http01' | 'dns01' | 'static'): Promise<void> { | ||||
|     if (provisionType === 'http01') { | ||||
|       await this.port80Handler.renewCertificate(domain); | ||||
|     } else if ((provisionType === 'static' || provisionType === 'dns01') && this.certProvisionFunction) { | ||||
|       const provision = await this.certProvisionFunction(domain); | ||||
|  | ||||
|       if (provision !== 'http01' && provision !== 'dns01') { | ||||
|         const certObj = provision as plugins.tsclass.network.ICert; | ||||
|         const certData: CertificateData = { | ||||
|           domain: certObj.domainName, | ||||
|           certificate: certObj.publicKey, | ||||
|           privateKey: certObj.privateKey, | ||||
|           expiryDate: new Date(certObj.validUntil), | ||||
|           source: 'static', | ||||
|           isRenewal: true | ||||
|         }; | ||||
|  | ||||
|         this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|         this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, certData); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -159,7 +254,6 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|    * Stop all scheduled renewal tasks. | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     // Stop scheduled renewals | ||||
|     if (this.renewManager) { | ||||
|       this.renewManager.stop(); | ||||
|     } | ||||
| @@ -171,30 +265,62 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async requestCertificate(domain: string): Promise<void> { | ||||
|     const isWildcard = domain.includes('*'); | ||||
|  | ||||
|     // Determine provisioning method | ||||
|     let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01'; | ||||
|     let provision: CertProvisionObject = 'http01'; | ||||
|  | ||||
|     if (this.certProvisionFunction) { | ||||
|       provision = await this.certProvisionFunction(domain); | ||||
|     } else if (isWildcard) { | ||||
|       // Cannot perform HTTP-01 on wildcard without certProvider | ||||
|       throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`); | ||||
|     } | ||||
|  | ||||
|     if (provision === 'http01') { | ||||
|       if (isWildcard) { | ||||
|         throw new Error(`Cannot request HTTP-01 certificate for wildcard domain: ${domain}`); | ||||
|       } | ||||
|       await this.port80Handler.renewCertificate(domain); | ||||
|     } else if (provision === 'dns01') { | ||||
|       // DNS-01 challenges would be handled by external mechanisms | ||||
|       // This is a placeholder for future implementation | ||||
|       console.log(`DNS-01 challenge requested for ${domain}`); | ||||
|     } else { | ||||
|       // Static certificate (e.g., DNS-01 provisioned) supports wildcards | ||||
|       const certObj = provision as plugins.tsclass.network.ICert; | ||||
|       const certData: ICertificateData = { | ||||
|       const certData: CertificateData = { | ||||
|         domain: certObj.domainName, | ||||
|         certificate: certObj.publicKey, | ||||
|         privateKey: certObj.privateKey, | ||||
|         expiryDate: new Date(certObj.validUntil) | ||||
|         expiryDate: new Date(certObj.validUntil), | ||||
|         source: 'static', | ||||
|         isRenewal: false | ||||
|       }; | ||||
|  | ||||
|       this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|       this.emit('certificate', { ...certData, source: 'static', isRenewal: false }); | ||||
|       this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   /** | ||||
|    * Add a new domain for certificate provisioning | ||||
|    * @param domain Domain to add | ||||
|    * @param options Domain configuration options | ||||
|    */ | ||||
|   public async addDomain(domain: string, options?: { | ||||
|     sslRedirect?: boolean; | ||||
|     acmeMaintenance?: boolean; | ||||
|   }): Promise<void> { | ||||
|     const domainOptions: DomainOptions = { | ||||
|       domainName: domain, | ||||
|       sslRedirect: options?.sslRedirect || true, | ||||
|       acmeMaintenance: options?.acmeMaintenance || true | ||||
|     }; | ||||
|  | ||||
|     this.port80Handler.addDomain(domainOptions); | ||||
|     await this.provisionDomain(domain); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // For backward compatibility | ||||
| export { CertProvisioner as CertificateProvisioner } | ||||
							
								
								
									
										3
									
								
								ts/certificate/providers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts/certificate/providers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /** | ||||
|  * Certificate providers | ||||
|  */ | ||||
							
								
								
									
										234
									
								
								ts/certificate/storage/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								ts/certificate/storage/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { CertificateData, Certificates } from '../models/certificate-types.js'; | ||||
| import { ensureCertificateDirectory } from '../utils/certificate-helpers.js'; | ||||
|  | ||||
| /** | ||||
|  * FileStorage provides file system storage for certificates | ||||
|  */ | ||||
| export class FileStorage { | ||||
|   private storageDir: string; | ||||
|    | ||||
|   /** | ||||
|    * Creates a new file storage provider | ||||
|    * @param storageDir Directory to store certificates | ||||
|    */ | ||||
|   constructor(storageDir: string) { | ||||
|     this.storageDir = path.resolve(storageDir); | ||||
|     ensureCertificateDirectory(this.storageDir); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Save a certificate to the file system | ||||
|    * @param domain Domain name  | ||||
|    * @param certData Certificate data to save | ||||
|    */ | ||||
|   public async saveCertificate(domain: string, certData: CertificateData): Promise<void> { | ||||
|     const sanitizedDomain = this.sanitizeDomain(domain); | ||||
|     const certDir = path.join(this.storageDir, sanitizedDomain); | ||||
|     ensureCertificateDirectory(certDir); | ||||
|      | ||||
|     const certPath = path.join(certDir, 'fullchain.pem'); | ||||
|     const keyPath = path.join(certDir, 'privkey.pem'); | ||||
|     const metaPath = path.join(certDir, 'metadata.json'); | ||||
|      | ||||
|     // Write certificate and private key | ||||
|     await fs.promises.writeFile(certPath, certData.certificate, 'utf8'); | ||||
|     await fs.promises.writeFile(keyPath, certData.privateKey, 'utf8'); | ||||
|      | ||||
|     // Write metadata | ||||
|     const metadata = { | ||||
|       domain: certData.domain, | ||||
|       expiryDate: certData.expiryDate.toISOString(), | ||||
|       source: certData.source || 'unknown', | ||||
|       issuedAt: new Date().toISOString() | ||||
|     }; | ||||
|      | ||||
|     await fs.promises.writeFile( | ||||
|       metaPath,  | ||||
|       JSON.stringify(metadata, null, 2),  | ||||
|       'utf8' | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Load a certificate from the file system | ||||
|    * @param domain Domain name | ||||
|    * @returns Certificate data if found, null otherwise | ||||
|    */ | ||||
|   public async loadCertificate(domain: string): Promise<CertificateData | null> { | ||||
|     const sanitizedDomain = this.sanitizeDomain(domain); | ||||
|     const certDir = path.join(this.storageDir, sanitizedDomain); | ||||
|      | ||||
|     if (!fs.existsSync(certDir)) { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     const certPath = path.join(certDir, 'fullchain.pem'); | ||||
|     const keyPath = path.join(certDir, 'privkey.pem'); | ||||
|     const metaPath = path.join(certDir, 'metadata.json'); | ||||
|      | ||||
|     try { | ||||
|       // Check if all required files exist | ||||
|       if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       // Read certificate and private key | ||||
|       const certificate = await fs.promises.readFile(certPath, 'utf8'); | ||||
|       const privateKey = await fs.promises.readFile(keyPath, 'utf8'); | ||||
|        | ||||
|       // Try to read metadata if available | ||||
|       let expiryDate = new Date(); | ||||
|       let source: 'static' | 'http01' | 'dns01' | undefined; | ||||
|        | ||||
|       if (fs.existsSync(metaPath)) { | ||||
|         const metaContent = await fs.promises.readFile(metaPath, 'utf8'); | ||||
|         const metadata = JSON.parse(metaContent); | ||||
|          | ||||
|         if (metadata.expiryDate) { | ||||
|           expiryDate = new Date(metadata.expiryDate); | ||||
|         } | ||||
|          | ||||
|         if (metadata.source) { | ||||
|           source = metadata.source as 'static' | 'http01' | 'dns01'; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         domain, | ||||
|         certificate, | ||||
|         privateKey, | ||||
|         expiryDate, | ||||
|         source | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       console.error(`Error loading certificate for ${domain}:`, error); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Delete a certificate from the file system | ||||
|    * @param domain Domain name | ||||
|    */ | ||||
|   public async deleteCertificate(domain: string): Promise<boolean> { | ||||
|     const sanitizedDomain = this.sanitizeDomain(domain); | ||||
|     const certDir = path.join(this.storageDir, sanitizedDomain); | ||||
|      | ||||
|     if (!fs.existsSync(certDir)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Recursively delete the certificate directory | ||||
|       await this.deleteDirectory(certDir); | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error(`Error deleting certificate for ${domain}:`, error); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * List all domains with stored certificates | ||||
|    * @returns Array of domain names | ||||
|    */ | ||||
|   public async listCertificates(): Promise<string[]> { | ||||
|     try { | ||||
|       const entries = await fs.promises.readdir(this.storageDir, { withFileTypes: true }); | ||||
|       return entries | ||||
|         .filter(entry => entry.isDirectory()) | ||||
|         .map(entry => entry.name); | ||||
|     } catch (error) { | ||||
|       console.error('Error listing certificates:', error); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a certificate is expiring soon | ||||
|    * @param domain Domain name | ||||
|    * @param thresholdDays Days threshold to consider expiring | ||||
|    * @returns Information about expiring certificate or null | ||||
|    */ | ||||
|   public async isExpiringSoon( | ||||
|     domain: string,  | ||||
|     thresholdDays: number = 30 | ||||
|   ): Promise<{ domain: string; expiryDate: Date; daysRemaining: number } | null> { | ||||
|     const certData = await this.loadCertificate(domain); | ||||
|      | ||||
|     if (!certData) { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     const now = new Date(); | ||||
|     const expiryDate = certData.expiryDate; | ||||
|     const timeRemaining = expiryDate.getTime() - now.getTime(); | ||||
|     const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24)); | ||||
|      | ||||
|     if (daysRemaining <= thresholdDays) { | ||||
|       return { | ||||
|         domain, | ||||
|         expiryDate, | ||||
|         daysRemaining | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     return null; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check all certificates for expiration | ||||
|    * @param thresholdDays Days threshold to consider expiring | ||||
|    * @returns List of expiring certificates | ||||
|    */ | ||||
|   public async getExpiringCertificates( | ||||
|     thresholdDays: number = 30 | ||||
|   ): Promise<Array<{ domain: string; expiryDate: Date; daysRemaining: number }>> { | ||||
|     const domains = await this.listCertificates(); | ||||
|     const expiringCerts = []; | ||||
|      | ||||
|     for (const domain of domains) { | ||||
|       const expiring = await this.isExpiringSoon(domain, thresholdDays); | ||||
|       if (expiring) { | ||||
|         expiringCerts.push(expiring); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return expiringCerts; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Delete a directory recursively | ||||
|    * @param directoryPath Directory to delete | ||||
|    */ | ||||
|   private async deleteDirectory(directoryPath: string): Promise<void> { | ||||
|     if (fs.existsSync(directoryPath)) { | ||||
|       const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); | ||||
|        | ||||
|       for (const entry of entries) { | ||||
|         const fullPath = path.join(directoryPath, entry.name); | ||||
|          | ||||
|         if (entry.isDirectory()) { | ||||
|           await this.deleteDirectory(fullPath); | ||||
|         } else { | ||||
|           await fs.promises.unlink(fullPath); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       await fs.promises.rmdir(directoryPath); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Sanitize a domain name for use as a directory name | ||||
|    * @param domain Domain name | ||||
|    * @returns Sanitized domain name | ||||
|    */ | ||||
|   private sanitizeDomain(domain: string): string { | ||||
|     // Replace wildcard and any invalid filesystem characters | ||||
|     return domain.replace(/\*/g, '_wildcard_').replace(/[/\\:*?"<>|]/g, '_'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								ts/certificate/storage/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts/certificate/storage/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /** | ||||
|  * Certificate storage mechanisms | ||||
|  */ | ||||
| @@ -1,17 +1,18 @@ | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import type { Certificates } from '../models/certificate-types.js'; | ||||
|  | ||||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||||
|  | ||||
| export interface ICertificates { | ||||
|   privateKey: string; | ||||
|   publicKey: string; | ||||
| } | ||||
|  | ||||
| export function loadDefaultCertificates(): ICertificates { | ||||
| /** | ||||
|  * Loads the default SSL certificates from the assets directory | ||||
|  * @returns The certificate key pair | ||||
|  */ | ||||
| export function loadDefaultCertificates(): Certificates { | ||||
|   try { | ||||
|     const certPath = path.join(__dirname, '..', 'assets', 'certs'); | ||||
|     // Need to adjust path from /ts/certificate/utils to /assets/certs | ||||
|     const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs'); | ||||
|     const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'); | ||||
|     const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8'); | ||||
|  | ||||
| @@ -28,3 +29,22 @@ export function loadDefaultCertificates(): ICertificates { | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Checks if a certificate file exists at the specified path | ||||
|  * @param certPath Path to check for certificate | ||||
|  * @returns True if the certificate exists, false otherwise | ||||
|  */ | ||||
| export function certificateExists(certPath: string): boolean { | ||||
|   return fs.existsSync(certPath); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Ensures the certificate directory exists | ||||
|  * @param dirPath Path to the certificate directory | ||||
|  */ | ||||
| export function ensureCertificateDirectory(dirPath: string): void { | ||||
|   if (!fs.existsSync(dirPath)) { | ||||
|     fs.mkdirSync(dirPath, { recursive: true }); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user