feat(server): add an embedded ACME directory server and certificate authority with challenge, order, and certificate endpoints
This commit is contained in:
128
ts_server/server.classes.acmeserver.ts
Normal file
128
ts_server/server.classes.acmeserver.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user