Files
smartacme/ts_server/server.classes.acmeserver.ts

129 lines
4.6 KiB
TypeScript

import * as http from 'node:http';
import type { IAcmeServerOptions } from './server.interfaces.js';
import { NonceManager } from './server.classes.nonce.js';
import { JwsVerifier } from './server.classes.jws.verifier.js';
import { MemoryAccountStore } from './server.classes.account.store.js';
import { MemoryOrderStore } from './server.classes.order.store.js';
import { AcmeServerCA } from './server.classes.ca.js';
import { ChallengeVerifier } from './server.classes.challenge.verifier.js';
import { AcmeRouter } from './server.classes.router.js';
import { createDirectoryHandler } from './server.handlers.directory.js';
import { createNonceHeadHandler, createNonceGetHandler } from './server.handlers.nonce.js';
import { createAccountHandler } from './server.handlers.account.js';
import { createNewOrderHandler, createOrderPollHandler } from './server.handlers.order.js';
import { createAuthzHandler } from './server.handlers.authz.js';
import { createChallengeHandler } from './server.handlers.challenge.js';
import { createFinalizeHandler } from './server.handlers.finalize.js';
import { createCertHandler } from './server.handlers.cert.js';
/**
* ACME Directory Server — a self-contained RFC 8555 Certificate Authority.
*
* Usage:
* ```ts
* const server = new AcmeServer({ port: 14000 });
* await server.start();
* console.log(server.getDirectoryUrl()); // http://localhost:14000/directory
* ```
*/
export class AcmeServer {
private options: Required<Pick<IAcmeServerOptions, 'port' | 'hostname'>> & IAcmeServerOptions;
private httpServer: http.Server | null = null;
private ca: AcmeServerCA;
private baseUrl: string;
constructor(options: IAcmeServerOptions = {}) {
this.options = {
port: options.port ?? 14000,
hostname: options.hostname ?? '0.0.0.0',
...options,
};
this.baseUrl = options.baseUrl ?? `http://localhost:${this.options.port}`;
this.ca = new AcmeServerCA(options.caOptions);
}
async start(): Promise<void> {
// Initialize CA
await this.ca.init();
// Create stores
const accountStore = new MemoryAccountStore();
const orderStore = new MemoryOrderStore();
// Create managers
const nonceManager = new NonceManager();
const jwsVerifier = new JwsVerifier(nonceManager, accountStore);
const challengeVerifier = new ChallengeVerifier(this.options.challengeVerification ?? true);
// Create router and register routes
const router = new AcmeRouter(nonceManager);
// Directory
router.addRoute('GET', '/directory', createDirectoryHandler(this.baseUrl));
// Nonce
router.addRoute('HEAD', '/new-nonce', createNonceHeadHandler());
router.addRoute('GET', '/new-nonce', createNonceGetHandler());
// Account
router.addRoute('POST', '/new-account', createAccountHandler(this.baseUrl, jwsVerifier, accountStore));
// Order
router.addRoute('POST', '/new-order', createNewOrderHandler(this.baseUrl, jwsVerifier, orderStore));
router.addRoute('POST', '/order/:id', createOrderPollHandler(this.baseUrl, jwsVerifier, orderStore));
// Authorization
router.addRoute('POST', '/authz/:id', createAuthzHandler(this.baseUrl, jwsVerifier, orderStore));
// Challenge
router.addRoute('POST', '/challenge/:id', createChallengeHandler(
this.baseUrl,
jwsVerifier,
orderStore,
accountStore,
challengeVerifier,
));
// Finalize
router.addRoute('POST', '/finalize/:id', createFinalizeHandler(this.baseUrl, jwsVerifier, orderStore, this.ca));
// Certificate
router.addRoute('POST', '/cert/:id', createCertHandler(this.baseUrl, jwsVerifier, orderStore));
// Start HTTP server
this.httpServer = http.createServer((req, res) => {
router.handle(req, res).catch((err) => {
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/problem+json' });
res.end(JSON.stringify({
type: 'urn:ietf:params:acme:error:serverInternal',
detail: err instanceof Error ? err.message : 'Unknown error',
status: 500,
}));
}
});
});
await new Promise<void>((resolve) => {
this.httpServer!.listen(this.options.port, this.options.hostname, () => resolve());
});
}
async stop(): Promise<void> {
if (this.httpServer) {
await new Promise<void>((resolve, reject) => {
this.httpServer!.close((err) => (err ? reject(err) : resolve()));
});
this.httpServer = null;
}
}
getDirectoryUrl(): string {
return `${this.baseUrl}/directory`;
}
getCaCertPem(): string {
return this.ca.getCaCertPem();
}
}