import path from 'node:path' import process from 'node:process' import pc from 'picocolors' import type { Logger } from 'vite' import { PLUGIN_DATA_DIR } from '../lib/constant' import { debug } from '../lib/logger' import { copyDir, ensureDirExist, escapeStr, exec, exists, getHash, prettyLog, readDir, readFile } from '../lib/util' import Config from './config' import Downloader from './downloader' import Record from './record' import { type BaseSource, GithubSource, CodingSource } from './source' import VersionManger from './version' export type SourceType = 'github' | 'coding' | BaseSource export type MkcertBaseOptions = { /** * Whether to force generate */ force?: boolean /** * Automatically upgrade mkcert * * @default false */ autoUpgrade?: boolean /** * Specify mkcert download source * * @default github */ source?: SourceType /** * If your network is restricted, you can specify a local binary file instead of downloading, it should be an absolute path * * @default none */ mkcertPath?: string /** * The location to save the files, such as key and cert files */ savePath?: string /** * The name of private key file generated by mkcert */ keyFileName?: string /** * The name of cert file generated by mkcert */ certFileName?: string } export type MkcertOptions = MkcertBaseOptions & { logger: Logger } class Mkcert { private force?: boolean private autoUpgrade?: boolean private sourceType: SourceType private savePath: string private logger: Logger private source: BaseSource private localMkcert?: string private savedMkcert: string private keyFilePath: string private certFilePath: string private config: Config public static create(options: MkcertOptions) { return new Mkcert(options) } private constructor(options: MkcertOptions) { const { force, autoUpgrade, source, mkcertPath, savePath = PLUGIN_DATA_DIR, keyFileName = 'dev.pem', certFileName = 'cert.pem', logger } = options this.force = force this.logger = logger this.autoUpgrade = autoUpgrade this.localMkcert = mkcertPath this.savePath = path.resolve(savePath) this.keyFilePath = path.resolve(savePath, keyFileName) this.certFilePath = path.resolve(savePath, certFileName) this.sourceType = source || 'github' if (this.sourceType === 'github') { this.source = GithubSource.create() } else if (this.sourceType === 'coding') { this.source = CodingSource.create() } else { this.source = this.sourceType } this.savedMkcert = path.resolve( savePath, process.platform === 'win32' ? 'mkcert.exe' : 'mkcert' ) this.config = new Config({ savePath: this.savePath }) } private async getMkcertBinary() { let binary: string | undefined if (this.localMkcert) { if (await exists(this.localMkcert)) { binary = this.localMkcert } else { this.logger.error( pc.red( `${this.localMkcert} does not exist, please check the mkcertPath parameter` ) ) } } else if (await exists(this.savedMkcert)) { binary = this.savedMkcert } return binary } private async checkCAExists() { const files = await readDir(this.savePath) return files.some(file => file.includes('rootCA')) } private async retainExistedCA() { if (await this.checkCAExists()) { return } const mkcertBinary = await this.getMkcertBinary() const commandStatement = `${escapeStr(mkcertBinary)} -CAROOT` debug(`Exec ${commandStatement}`) const commandResult = await exec(commandStatement) const caDirPath = path.resolve( commandResult.stdout.toString().replace(/\n/g, '') ) if (caDirPath === this.savePath) { return } const caDirExists = await exists(caDirPath) if (!caDirExists) { return } await copyDir(caDirPath, this.savePath) } private async getCertificate() { const key = await readFile(this.keyFilePath) const cert = await readFile(this.certFilePath) return { key, cert } } private async createCertificate(hosts: string[]) { const names = hosts.join(' ') const mkcertBinary = await this.getMkcertBinary() if (!mkcertBinary) { debug( `Mkcert does not exist, unable to generate certificate for ${names}` ) } await ensureDirExist(this.savePath) await this.retainExistedCA() const cmd = `${escapeStr(mkcertBinary)} -install -key-file ${escapeStr( this.keyFilePath )} -cert-file ${escapeStr(this.certFilePath)} ${names}` await exec(cmd, { env: { ...process.env, CAROOT: this.savePath, JAVA_HOME: undefined } }) this.logger.info( `The list of generated files:\n${this.keyFilePath}\n${this.certFilePath}` ) } private getLatestHash = async () => { return { key: await getHash(this.keyFilePath), cert: await getHash(this.certFilePath) } } private async regenerate(record: Record, hosts: string[]) { await this.createCertificate(hosts) const hash = await this.getLatestHash() record.update({ hosts, hash }) } public async init() { await ensureDirExist(this.savePath) await this.config.init() const mkcertBinary = await this.getMkcertBinary() if (!mkcertBinary) { await this.initMkcert() } else if (this.autoUpgrade) { await this.upgradeMkcert() } } private async getSourceInfo() { const sourceInfo = await this.source.getSourceInfo() if (!sourceInfo) { const message = typeof this.sourceType === 'string' ? `Unsupported platform. Unable to find a binary file for ${process.platform } platform with ${process.arch} arch on ${this.sourceType === 'github' ? 'https://github.com/FiloSottile/mkcert/releases' : 'https://liuweigl.coding.net/p/github/artifacts?hash=8d4dd8949af543159c1b5ac71ff1ff72' }` : 'Please check your custom "source", it seems to return invalid result' throw new Error(message) } return sourceInfo } private async initMkcert() { const sourceInfo = await this.getSourceInfo() debug('The mkcert does not exist, download it now') await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert) } private async upgradeMkcert() { const versionManger = new VersionManger({ config: this.config }) const sourceInfo = await this.getSourceInfo() if (!sourceInfo) { this.logger.error( 'Can not obtain download information of mkcert, update skipped' ) return } const versionInfo = versionManger.compare(sourceInfo.version) if (!versionInfo.shouldUpdate) { debug('Mkcert is kept latest version, update skipped') return } if (versionInfo.breakingChange) { debug( 'The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped', versionInfo.currentVersion, versionInfo.nextVersion ) return } debug( 'The current version of mkcert is %s, and the latest version is %s, mkcert will be updated', versionInfo.currentVersion, versionInfo.nextVersion ) await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert) versionManger.update(versionInfo.nextVersion) } private async downloadMkcert(sourceUrl: string, distPath: string) { const downloader = Downloader.create() await downloader.download(sourceUrl, distPath) } public async renew(hosts: string[]) { const record = new Record({ config: this.config }) if (this.force) { debug('Certificate is forced to regenerate') await this.regenerate(record, hosts) } if (!record.contains(hosts)) { debug( `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate` ) await this.regenerate(record, hosts) return } const hash = await this.getLatestHash() if (!record.equal(hash)) { debug( `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog( hash )}, start regenerate certificate` ) await this.regenerate(record, hosts) return } debug('Neither hosts nor hash has changed, skip regenerate certificate') } /** * Get certificates * * @param hosts host collection * @returns cretificates */ public async install(hosts: string[]) { if (hosts.length) { await this.renew(hosts) } return await this.getCertificate() } } export default Mkcert