| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- 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
|