index.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import path from 'node:path'
  2. import process from 'node:process'
  3. import pc from 'picocolors'
  4. import type { Logger } from 'vite'
  5. import { PLUGIN_DATA_DIR } from '../lib/constant'
  6. import { debug } from '../lib/logger'
  7. import {
  8. copyDir,
  9. ensureDirExist,
  10. escapeStr,
  11. exec,
  12. exists,
  13. getHash,
  14. prettyLog,
  15. readDir,
  16. readFile
  17. } from '../lib/util'
  18. import Config from './config'
  19. import Downloader from './downloader'
  20. import Record from './record'
  21. import { type BaseSource, GithubSource, CodingSource } from './source'
  22. import VersionManger from './version'
  23. export type SourceType = 'github' | 'coding' | BaseSource
  24. export type MkcertBaseOptions = {
  25. /**
  26. * Whether to force generate
  27. */
  28. force?: boolean
  29. /**
  30. * Automatically upgrade mkcert
  31. *
  32. * @default false
  33. */
  34. autoUpgrade?: boolean
  35. /**
  36. * Specify mkcert download source
  37. *
  38. * @default github
  39. */
  40. source?: SourceType
  41. /**
  42. * If your network is restricted, you can specify a local binary file instead of downloading, it should be an absolute path
  43. *
  44. * @default none
  45. */
  46. mkcertPath?: string
  47. /**
  48. * The location to save the files, such as key and cert files
  49. */
  50. savePath?: string
  51. /**
  52. * The name of private key file generated by mkcert
  53. */
  54. keyFileName?: string
  55. /**
  56. * The name of cert file generated by mkcert
  57. */
  58. certFileName?: string
  59. }
  60. export type MkcertOptions = MkcertBaseOptions & {
  61. logger: Logger
  62. }
  63. class Mkcert {
  64. private force?: boolean
  65. private autoUpgrade?: boolean
  66. private sourceType: SourceType
  67. private savePath: string
  68. private logger: Logger
  69. private source: BaseSource
  70. private localMkcert?: string
  71. private savedMkcert: string
  72. private keyFilePath: string
  73. private certFilePath: string
  74. private config: Config
  75. public static create(options: MkcertOptions) {
  76. return new Mkcert(options)
  77. }
  78. private constructor(options: MkcertOptions) {
  79. const {
  80. force,
  81. autoUpgrade,
  82. source,
  83. mkcertPath,
  84. savePath = PLUGIN_DATA_DIR,
  85. keyFileName = 'dev.pem',
  86. certFileName = 'cert.pem',
  87. logger
  88. } = options
  89. this.force = force
  90. this.logger = logger
  91. this.autoUpgrade = autoUpgrade
  92. this.localMkcert = mkcertPath
  93. this.savePath = path.resolve(savePath)
  94. this.keyFilePath = path.resolve(savePath, keyFileName)
  95. this.certFilePath = path.resolve(savePath, certFileName)
  96. this.sourceType = source || 'github'
  97. if (this.sourceType === 'github') {
  98. this.source = GithubSource.create()
  99. } else if (this.sourceType === 'coding') {
  100. this.source = CodingSource.create()
  101. } else {
  102. this.source = this.sourceType
  103. }
  104. this.savedMkcert = path.resolve(
  105. savePath,
  106. process.platform === 'win32' ? 'mkcert.exe' : 'mkcert'
  107. )
  108. this.config = new Config({ savePath: this.savePath })
  109. }
  110. private async getMkcertBinary() {
  111. let binary: string | undefined
  112. if (this.localMkcert) {
  113. if (await exists(this.localMkcert)) {
  114. binary = this.localMkcert
  115. } else {
  116. this.logger.error(
  117. pc.red(
  118. `${this.localMkcert} does not exist, please check the mkcertPath parameter`
  119. )
  120. )
  121. }
  122. } else if (await exists(this.savedMkcert)) {
  123. binary = this.savedMkcert
  124. }
  125. return binary
  126. }
  127. private async checkCAExists() {
  128. const files = await readDir(this.savePath)
  129. return files.some(file => file.includes('rootCA'))
  130. }
  131. private async retainExistedCA() {
  132. if (await this.checkCAExists()) {
  133. return
  134. }
  135. const mkcertBinary = await this.getMkcertBinary()
  136. const commandStatement = `${escapeStr(mkcertBinary)} -CAROOT`
  137. debug(`Exec ${commandStatement}`)
  138. const commandResult = await exec(commandStatement)
  139. const caDirPath = path.resolve(
  140. commandResult.stdout.toString().replace(/\n/g, '')
  141. )
  142. if (caDirPath === this.savePath) {
  143. return
  144. }
  145. const caDirExists = await exists(caDirPath)
  146. if (!caDirExists) {
  147. return
  148. }
  149. await copyDir(caDirPath, this.savePath)
  150. }
  151. private async getCertificate() {
  152. const key = await readFile(this.keyFilePath)
  153. const cert = await readFile(this.certFilePath)
  154. return {
  155. key,
  156. cert
  157. }
  158. }
  159. private async createCertificate(hosts: string[]) {
  160. const names = hosts.join(' ')
  161. const mkcertBinary = await this.getMkcertBinary()
  162. if (!mkcertBinary) {
  163. debug(
  164. `Mkcert does not exist, unable to generate certificate for ${names}`
  165. )
  166. }
  167. await ensureDirExist(this.savePath)
  168. await this.retainExistedCA()
  169. const cmd = `${escapeStr(mkcertBinary)} -install -key-file ${escapeStr(
  170. this.keyFilePath
  171. )} -cert-file ${escapeStr(this.certFilePath)} ${names}`
  172. await exec(cmd, {
  173. env: {
  174. ...process.env,
  175. CAROOT: this.savePath,
  176. JAVA_HOME: undefined
  177. }
  178. })
  179. this.logger.info(
  180. `The list of generated files:\n${this.keyFilePath}\n${this.certFilePath}`
  181. )
  182. }
  183. private getLatestHash = async () => {
  184. return {
  185. key: await getHash(this.keyFilePath),
  186. cert: await getHash(this.certFilePath)
  187. }
  188. }
  189. private async regenerate(record: Record, hosts: string[]) {
  190. await this.createCertificate(hosts)
  191. const hash = await this.getLatestHash()
  192. record.update({ hosts, hash })
  193. }
  194. public async init() {
  195. await ensureDirExist(this.savePath)
  196. await this.config.init()
  197. const mkcertBinary = await this.getMkcertBinary()
  198. if (!mkcertBinary) {
  199. await this.initMkcert()
  200. } else if (this.autoUpgrade) {
  201. await this.upgradeMkcert()
  202. }
  203. }
  204. private async getSourceInfo() {
  205. const sourceInfo = await this.source.getSourceInfo()
  206. if (!sourceInfo) {
  207. const message =
  208. typeof this.sourceType === 'string'
  209. ? `Unsupported platform. Unable to find a binary file for ${process.platform
  210. } platform with ${process.arch} arch on ${this.sourceType === 'github'
  211. ? 'https://github.com/FiloSottile/mkcert/releases'
  212. : 'https://liuweigl.coding.net/p/github/artifacts?hash=8d4dd8949af543159c1b5ac71ff1ff72'
  213. }`
  214. : 'Please check your custom "source", it seems to return invalid result'
  215. throw new Error(message)
  216. }
  217. return sourceInfo
  218. }
  219. private async initMkcert() {
  220. const sourceInfo = await this.getSourceInfo()
  221. debug('The mkcert does not exist, download it now')
  222. await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert)
  223. }
  224. private async upgradeMkcert() {
  225. const versionManger = new VersionManger({ config: this.config })
  226. const sourceInfo = await this.getSourceInfo()
  227. if (!sourceInfo) {
  228. this.logger.error(
  229. 'Can not obtain download information of mkcert, update skipped'
  230. )
  231. return
  232. }
  233. const versionInfo = versionManger.compare(sourceInfo.version)
  234. if (!versionInfo.shouldUpdate) {
  235. debug('Mkcert is kept latest version, update skipped')
  236. return
  237. }
  238. if (versionInfo.breakingChange) {
  239. debug(
  240. 'The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped',
  241. versionInfo.currentVersion,
  242. versionInfo.nextVersion
  243. )
  244. return
  245. }
  246. debug(
  247. 'The current version of mkcert is %s, and the latest version is %s, mkcert will be updated',
  248. versionInfo.currentVersion,
  249. versionInfo.nextVersion
  250. )
  251. await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert)
  252. versionManger.update(versionInfo.nextVersion)
  253. }
  254. private async downloadMkcert(sourceUrl: string, distPath: string) {
  255. const downloader = Downloader.create()
  256. await downloader.download(sourceUrl, distPath)
  257. }
  258. public async renew(hosts: string[]) {
  259. const record = new Record({ config: this.config })
  260. if (this.force) {
  261. debug('Certificate is forced to regenerate')
  262. await this.regenerate(record, hosts)
  263. }
  264. if (!record.contains(hosts)) {
  265. debug(
  266. `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate`
  267. )
  268. await this.regenerate(record, hosts)
  269. return
  270. }
  271. const hash = await this.getLatestHash()
  272. if (!record.equal(hash)) {
  273. debug(
  274. `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog(
  275. hash
  276. )}, start regenerate certificate`
  277. )
  278. await this.regenerate(record, hosts)
  279. return
  280. }
  281. debug('Neither hosts nor hash has changed, skip regenerate certificate')
  282. }
  283. /**
  284. * Get certificates
  285. *
  286. * @param hosts host collection
  287. * @returns cretificates
  288. */
  289. public async install(hosts: string[]) {
  290. if (hosts.length) {
  291. await this.renew(hosts)
  292. }
  293. return await this.getCertificate()
  294. }
  295. }
  296. export default Mkcert