update structure

This commit is contained in:
2025-05-09 17:00:27 +00:00
parent 4a72d9f3bf
commit f1c0b8bfb7
60 changed files with 4919 additions and 1400 deletions

8
ts/http/index.ts Normal file
View File

@ -0,0 +1,8 @@
/**
* HTTP functionality module
*/
// Export submodules
export * from './port80/index.js';
export * from './router/index.js';
export * from './redirects/index.js';

View File

@ -0,0 +1,106 @@
import * as plugins from '../../plugins.js';
import type {
ForwardConfig,
DomainOptions,
AcmeOptions
} from '../../certificate/models/certificate-types.js';
/**
* HTTP-specific event types
*/
export enum HttpEvents {
REQUEST_RECEIVED = 'request-received',
REQUEST_FORWARDED = 'request-forwarded',
REQUEST_HANDLED = 'request-handled',
REQUEST_ERROR = 'request-error',
}
/**
* HTTP status codes as an enum for better type safety
*/
export enum HttpStatus {
OK = 200,
MOVED_PERMANENTLY = 301,
FOUND = 302,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
SERVICE_UNAVAILABLE = 503,
}
/**
* Represents a domain configuration with certificate status information
*/
export interface DomainCertificate {
options: DomainOptions;
certObtained: boolean;
obtainingInProgress: boolean;
certificate?: string;
privateKey?: string;
expiryDate?: Date;
lastRenewalAttempt?: Date;
}
/**
* Base error class for HTTP-related errors
*/
export class HttpError extends Error {
constructor(message: string) {
super(message);
this.name = 'HttpError';
}
}
/**
* Error related to certificate operations
*/
export class CertificateError extends HttpError {
constructor(
message: string,
public readonly domain: string,
public readonly isRenewal: boolean = false
) {
super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`);
this.name = 'CertificateError';
}
}
/**
* Error related to server operations
*/
export class ServerError extends HttpError {
constructor(message: string, public readonly code?: string) {
super(message);
this.name = 'ServerError';
}
}
/**
* Redirect configuration for HTTP requests
*/
export interface RedirectConfig {
source: string; // Source path or pattern
destination: string; // Destination URL
type: HttpStatus; // Redirect status code
preserveQuery?: boolean; // Whether to preserve query parameters
}
/**
* HTTP router configuration
*/
export interface RouterConfig {
routes: Array<{
path: string;
handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
}>;
notFoundHandler?: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
}
// Backward compatibility interfaces
export { HttpError as Port80HandlerError };
export { CertificateError as CertError };
export type IDomainCertificate = DomainCertificate;

View File

@ -0,0 +1,221 @@
import * as plugins from '../../plugins.js';
import { IncomingMessage, ServerResponse } from 'http';
import {
CertificateEvents
} from '../../certificate/events/certificate-events.js';
import type {
CertificateData,
CertificateFailure,
CertificateExpiring
} from '../../certificate/models/certificate-types.js';
/**
* Handles ACME HTTP-01 challenge responses
*/
export class ChallengeResponder extends plugins.EventEmitter {
private smartAcme: plugins.smartacme.SmartAcme | null = null;
private http01Handler: plugins.smartacme.handlers.Http01MemoryHandler | null = null;
/**
* Creates a new challenge responder
* @param useProduction Whether to use production ACME servers
* @param email Account email for ACME
* @param certificateStore Directory to store certificates
*/
constructor(
private readonly useProduction: boolean = false,
private readonly email: string = 'admin@example.com',
private readonly certificateStore: string = './certs'
) {
super();
}
/**
* Initialize the ACME client
*/
public async initialize(): Promise<void> {
try {
// Initialize SmartAcme
this.smartAcme = new plugins.smartacme.SmartAcme({
useProduction: this.useProduction,
accountEmail: this.email,
directoryUrl: this.useProduction
? 'https://acme-v02.api.letsencrypt.org/directory' // Production
: 'https://acme-staging-v02.api.letsencrypt.org/directory', // Staging
});
// Initialize HTTP-01 challenge handler
this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
this.smartAcme.useHttpChallenge(this.http01Handler);
// Ensure certificate store directory exists
await this.ensureCertificateStore();
// Subscribe to SmartAcme events
this.smartAcme.on('certificate-issued', (data: any) => {
const certData: CertificateData = {
domain: data.domain,
certificate: data.cert,
privateKey: data.key,
expiryDate: new Date(data.expiryDate),
};
this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData);
});
this.smartAcme.on('certificate-renewed', (data: any) => {
const certData: CertificateData = {
domain: data.domain,
certificate: data.cert,
privateKey: data.key,
expiryDate: new Date(data.expiryDate),
};
this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData);
});
this.smartAcme.on('certificate-error', (data: any) => {
const error: CertificateFailure = {
domain: data.domain,
error: data.error instanceof Error ? data.error.message : String(data.error),
isRenewal: data.isRenewal || false,
};
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
});
await this.smartAcme.initialize();
} catch (error) {
throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Ensure certificate store directory exists
*/
private async ensureCertificateStore(): Promise<void> {
try {
await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true });
} catch (error) {
throw new Error(`Failed to create certificate store: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Handle HTTP request and check if it's an ACME challenge
* @param req HTTP request
* @param res HTTP response
* @returns true if the request was handled as an ACME challenge
*/
public handleRequest(req: IncomingMessage, res: ServerResponse): boolean {
if (!this.http01Handler) {
return false;
}
const url = req.url || '/';
// Check if this is an ACME challenge request
if (url.startsWith('/.well-known/acme-challenge/')) {
const token = url.split('/').pop() || '';
if (token) {
const response = this.http01Handler.getResponse(token);
if (response) {
// This is a valid ACME challenge
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
res.writeHead(200);
res.end(response);
return true;
}
}
// Invalid ACME challenge
res.writeHead(404);
res.end('Not found');
return true;
}
return false;
}
/**
* Request a certificate for a domain
* @param domain Domain name
* @param isRenewal Whether this is a renewal
*/
public async requestCertificate(domain: string, isRenewal: boolean = false): Promise<CertificateData> {
if (!this.smartAcme) {
throw new Error('ACME client not initialized');
}
try {
const result = await this.smartAcme.getCertificate(domain);
const certData: CertificateData = {
domain,
certificate: result.cert,
privateKey: result.key,
expiryDate: new Date(result.expiryDate),
};
// Emit appropriate event
if (isRenewal) {
this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData);
} else {
this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData);
}
return certData;
} catch (error) {
// Construct failure object
const failure: CertificateFailure = {
domain,
error: error instanceof Error ? error.message : String(error),
isRenewal,
};
// Emit failure event
this.emit(CertificateEvents.CERTIFICATE_FAILED, failure);
throw new Error(`Failed to ${isRenewal ? 'renew' : 'obtain'} certificate for ${domain}: ${
error instanceof Error ? error.message : String(error)
}`);
}
}
/**
* Check if a certificate is expiring soon
* @param domain Domain name
* @param certificate Certificate data
* @param thresholdDays Days before expiry to trigger a renewal
*/
public checkCertificateExpiry(
domain: string,
certificate: CertificateData,
thresholdDays: number = 30
): void {
if (!certificate.expiryDate) {
return;
}
const now = new Date();
const expiryDate = certificate.expiryDate;
const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysDifference <= thresholdDays) {
const expiryInfo: CertificateExpiring = {
domain,
expiryDate,
daysRemaining: daysDifference,
};
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo);
// Automatically attempt renewal if expiring
if (this.smartAcme) {
this.requestCertificate(domain, true).catch(error => {
console.error(`Failed to auto-renew certificate for ${domain}:`, error);
});
}
}
}
}

3
ts/http/port80/index.ts Normal file
View File

@ -0,0 +1,3 @@
/**
* Port 80 handling
*/

View File

@ -1,57 +1,33 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { IncomingMessage, ServerResponse } from 'http';
import { Port80HandlerEvents } from '../common/types.js';
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
import type {
IForwardConfig,
IDomainOptions,
ICertificateData,
ICertificateFailure,
ICertificateExpiring,
IAcmeOptions
} from '../common/types.js';
// (fs and path I/O moved to CertProvisioner)
ForwardConfig,
DomainOptions,
CertificateData,
CertificateFailure,
CertificateExpiring,
AcmeOptions
} from '../../certificate/models/certificate-types.js';
import {
HttpEvents,
HttpStatus,
HttpError,
CertificateError,
ServerError,
DomainCertificate
} from '../models/http-types.js';
import { ChallengeResponder } from './challenge-responder.js';
/**
* Custom error classes for better error handling
*/
export class Port80HandlerError extends Error {
constructor(message: string) {
super(message);
this.name = 'Port80HandlerError';
}
// Re-export for backward compatibility
export {
HttpError as Port80HandlerError,
CertificateError,
ServerError
}
export class CertificateError extends Port80HandlerError {
constructor(
message: string,
public readonly domain: string,
public readonly isRenewal: boolean = false
) {
super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`);
this.name = 'CertificateError';
}
}
export class ServerError extends Port80HandlerError {
constructor(message: string, public readonly code?: string) {
super(message);
this.name = 'ServerError';
}
}
/**
* Represents a domain configuration with certificate status information
*/
interface IDomainCertificate {
options: IDomainOptions;
certObtained: boolean;
obtainingInProgress: boolean;
certificate?: string;
privateKey?: string;
expiryDate?: Date;
lastRenewalAttempt?: Date;
}
// Port80Handler events enum for backward compatibility
export const Port80HandlerEvents = CertificateEvents;
/**
* Configuration options for the Port80Handler
@ -64,25 +40,22 @@ interface IDomainCertificate {
* Now with glob pattern support for domain matching
*/
export class Port80Handler extends plugins.EventEmitter {
private domainCertificates: Map<string, IDomainCertificate>;
// SmartAcme instance for certificate management
private smartAcme: plugins.smartacme.SmartAcme | null = null;
private smartAcmeHttp01Handler!: plugins.smartacme.handlers.Http01MemoryHandler;
private domainCertificates: Map<string, DomainCertificate>;
private challengeResponder: ChallengeResponder | null = null;
private server: plugins.http.Server | null = null;
// Renewal scheduling is handled externally by SmartProxy
// (Removed internal renewal timer)
private isShuttingDown: boolean = false;
private options: Required<IAcmeOptions>;
private options: Required<AcmeOptions>;
/**
* Creates a new Port80Handler
* @param options Configuration options
*/
constructor(options: IAcmeOptions = {}) {
constructor(options: AcmeOptions = {}) {
super();
this.domainCertificates = new Map<string, IDomainCertificate>();
this.domainCertificates = new Map<string, DomainCertificate>();
// Default options
this.options = {
port: options.port ?? 80,
@ -97,6 +70,32 @@ export class Port80Handler extends plugins.EventEmitter {
autoRenew: options.autoRenew ?? true,
domainForwards: options.domainForwards ?? []
};
// Initialize challenge responder
if (this.options.enabled) {
this.challengeResponder = new ChallengeResponder(
this.options.useProduction,
this.options.accountEmail,
this.options.certificateStore
);
// Forward certificate events from the challenge responder
this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: CertificateData) => {
this.emit(CertificateEvents.CERTIFICATE_ISSUED, data);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: CertificateData) => {
this.emit(CertificateEvents.CERTIFICATE_RENEWED, data);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: CertificateFailure) => {
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: CertificateExpiring) => {
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry);
});
}
}
/**

View File

@ -0,0 +1,3 @@
/**
* HTTP redirects
*/

3
ts/http/router/index.ts Normal file
View File

@ -0,0 +1,3 @@
/**
* HTTP routing
*/