129 lines
4.6 KiB
TypeScript
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();
|
||
|
|
}
|
||
|
|
}
|