@axlotl-lab/navigrator
Version:
A powerful local domain manager for development environments. Navigrator helps you manage local domains and SSL certificates with a simple web interface.
317 lines (313 loc) • 13.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CertificateManager = void 0;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs/promises"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const util_1 = require("util");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class CertificateManager {
constructor(certsDir) {
this.certsDir = certsDir || path.join(os.homedir(), '.navigrator', 'certs');
this.caDir = path.join(this.certsDir, 'ca');
this.hasOpenSSL = false; // Will be validated in initialize()
}
/**
* Check if OpenSSL is installed on the system
*/
checkOpenSSLInstalled() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield execAsync('openssl version');
return true;
}
catch (error) {
return false;
}
});
}
/**
* Initialize the certificate directory and validate OpenSSL
*/
initialize() {
return __awaiter(this, void 0, void 0, function* () {
try {
// Verify OpenSSL
this.hasOpenSSL = yield this.checkOpenSSLInstalled();
if (!this.hasOpenSSL) {
throw new Error('OpenSSL is not installed. Please install OpenSSL to continue.');
}
// Create required directories
yield fs.mkdir(this.certsDir, { recursive: true });
yield fs.mkdir(this.caDir, { recursive: true });
// Create local CA if it doesn't exist
yield this.initLocalCA();
}
catch (error) {
console.error('Error initializing certificate manager:', error);
throw new Error(`Failed to initialize certificate manager: ${error === null || error === void 0 ? void 0 : error.message}`);
}
});
}
/**
* Initialize a local CA to sign certificates
*/
initLocalCA() {
return __awaiter(this, void 0, void 0, function* () {
const caKeyPath = path.join(this.caDir, 'rootCA.key');
const caCertPath = path.join(this.caDir, 'rootCA.crt');
// Check if CA already exists
try {
yield fs.access(caKeyPath);
yield fs.access(caCertPath);
return; // CA already exists
}
catch (error) {
// Doesn't exist, need to create it
}
// Create private key for the CA
yield execAsync(`openssl genrsa -out "${caKeyPath}" 4096`);
// Create certificate for the CA (valid for 10 years)
yield execAsync(`openssl req -x509 -new -nodes -key "${caKeyPath}" -sha256 -days 3650 ` +
`-out "${caCertPath}" -subj "/CN=Navigrator Local CA/O=Axlotl Lab/OU=Development"`);
console.log('Local CA created successfully');
});
}
/**
* Create a certificate for a domain using OpenSSL
*/
createCertificate(domain) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.hasOpenSSL) {
throw new Error('OpenSSL is not available');
}
try {
const keyPath = path.join(this.certsDir, `${domain}.key`);
const csrPath = path.join(this.certsDir, `${domain}.csr`);
const certPath = path.join(this.certsDir, `${domain}.crt`);
const configPath = path.join(this.certsDir, `${domain}.cnf`);
// Create configuration file for the certificate
const configContent = this.generateOpenSSLConfig(domain);
yield fs.writeFile(configPath, configContent);
// Generate private key for the domain
yield execAsync(`openssl genrsa -out "${keyPath}" 2048`);
// Create CSR (Certificate Signing Request)
yield execAsync(`openssl req -new -key "${keyPath}" -out "${csrPath}" ` +
`-config "${configPath}" -subj "/CN=${domain}/O=Axlotl Lab/OU=Development"`);
// Sign the certificate with our CA
const caKeyPath = path.join(this.caDir, 'rootCA.key');
const caCertPath = path.join(this.caDir, 'rootCA.crt');
yield execAsync(`openssl x509 -req -in "${csrPath}" -CA "${caCertPath}" ` +
`-CAkey "${caKeyPath}" -CAcreateserial ` +
`-out "${certPath}" -days 365 -sha256 ` +
`-extensions v3_req -extfile "${configPath}"`);
// Remove temporary files
yield fs.unlink(csrPath);
yield fs.unlink(configPath);
// Verify the created certificate
const certInfo = yield this.parseCertificate(domain);
if (!certInfo) {
throw new Error(`Failed to verify certificate for ${domain} after creation`);
}
return certInfo;
}
catch (error) {
console.error(`Error creating certificate for ${domain}:`, error);
throw new Error(`Failed to create certificate: ${error === null || error === void 0 ? void 0 : error.message}`);
}
});
}
/**
* Generate an OpenSSL configuration file for the certificate
*/
generateOpenSSLConfig(domain) {
return `[req]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = ${domain}
O = Axlotl Lab
OU = Development
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${domain}
`;
}
/**
* Delete a certificate for a domain
*/
deleteCertificate(domain) {
return __awaiter(this, void 0, void 0, function* () {
try {
const certInfo = yield this.verifyCertificate(domain);
if (!certInfo || !certInfo.certFilePath || !certInfo.keyFilePath) {
return false;
}
try {
yield fs.unlink(certInfo.certFilePath);
yield fs.unlink(certInfo.keyFilePath);
return true;
}
catch (error) {
console.error(`Error deleting certificate files for ${domain}:`, error);
return false;
}
}
catch (error) {
console.error(`Error deleting certificate for ${domain}:`, error);
return false;
}
});
}
/**
* Verify if a valid certificate exists for the domain
*/
verifyCertificate(domain) {
return __awaiter(this, void 0, void 0, function* () {
try {
const certPath = path.join(this.certsDir, `${domain}.crt`);
const keyPath = path.join(this.certsDir, `${domain}.key`);
// Check if files exist
try {
yield fs.access(certPath);
yield fs.access(keyPath);
}
catch (error) {
return null; // If certificates don't exist, there is no valid certificate
}
return yield this.parseCertificate(domain);
}
catch (error) {
console.error(`Error verifying certificate for ${domain}:`, error);
return null;
}
});
}
/**
* Parse an existing certificate to get its information
*/
parseCertificate(domain) {
return __awaiter(this, void 0, void 0, function* () {
try {
const certPath = path.join(this.certsDir, `${domain}.crt`);
const keyPath = path.join(this.certsDir, `${domain}.key`);
// Verify that the certificate exists
try {
yield fs.access(certPath);
yield fs.access(keyPath);
}
catch (error) {
return null;
}
// Validate certificate dates
const { stdout: dates } = yield execAsync(`openssl x509 -in "${certPath}" -noout -dates`);
// Parse dates (format: notBefore=May 30 12:00:00 2023 GMT / notAfter=May 30 12:00:00 2024 GMT)
const notBeforeMatch = dates.match(/notBefore=(.+)$/m);
const notAfterMatch = dates.match(/notAfter=(.+)$/m);
if (!notBeforeMatch || !notAfterMatch) {
throw new Error('Could not extract dates from certificate');
}
const validFrom = new Date(notBeforeMatch[1]);
const validTo = new Date(notAfterMatch[1]);
// Get issuer name
const { stdout: issuerData } = yield execAsync(`openssl x509 -in "${certPath}" -noout -issuer`);
// Extract CN from issuer
const issuerCNMatch = issuerData.match(/CN\s*=\s*([^,\/]+)/);
const issuer = issuerCNMatch ? issuerCNMatch[1].trim() : 'Unknown';
// Verify that the domain is included in the certificate
const { stdout: subjectAltNames } = yield execAsync(`openssl x509 -in "${certPath}" -noout -ext subjectAltName`);
const domainIncluded = subjectAltNames.includes(`DNS:${domain}`);
// Verify certificate validity
const now = new Date();
const isValid = now >= validFrom && now <= validTo && domainIncluded;
return {
domain,
validFrom,
validTo,
issuer,
isValid,
certFilePath: certPath,
keyFilePath: keyPath
};
}
catch (error) {
console.error(`Error parsing certificate for ${domain}:`, error);
return null;
}
});
}
/**
* List all certificates created by the application
*/
listCertificates() {
return __awaiter(this, void 0, void 0, function* () {
try {
const files = yield fs.readdir(this.certsDir);
const certFiles = files.filter(file => file.endsWith('.crt'));
const certificates = [];
for (const certFile of certFiles) {
const domain = certFile.replace('.crt', '');
const certInfo = yield this.verifyCertificate(domain);
if (certInfo) {
certificates.push(certInfo);
}
}
return certificates;
}
catch (error) {
console.error('Error listing certificates:', error);
return [];
}
});
}
}
exports.CertificateManager = CertificateManager;