index.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import fs from 'fs'
  2. import process from 'process'
  3. import pc from 'picocolors'
  4. import { Logger } from 'vite'
  5. import { debug } from '../lib/logger'
  6. import {
  7. ensureDirExist,
  8. escape,
  9. exec,
  10. exists,
  11. getHash,
  12. prettyLog,
  13. resolvePath
  14. } from '../lib/util'
  15. import Config from './config'
  16. import Downloader from './downloader'
  17. import Record from './record'
  18. import { BaseSource, GithubSource, CodingSource } from './source'
  19. import VersionManger from './version'
  20. export type SourceType = 'github' | 'coding' | BaseSource
  21. export type MkcertOptions = {
  22. /**
  23. * Whether to force generate
  24. */
  25. force?: boolean
  26. /**
  27. * Automatically upgrade mkcert
  28. *
  29. * @default false
  30. */
  31. autoUpgrade?: boolean
  32. /**
  33. * Specify mkcert download source
  34. *
  35. * @default github
  36. */
  37. source?: SourceType
  38. /**
  39. * If your network is restricted, you can specify a local binary file instead of downloading
  40. *
  41. * @description it should be absolute path
  42. * @default none
  43. */
  44. mkcertPath?: string
  45. }
  46. export type MkcertProps = MkcertOptions & {
  47. logger: Logger
  48. }
  49. const KEY_FILE_PATH = resolvePath('certs/dev.key')
  50. const CERT_FILE_PATH = resolvePath('certs/dev.pem')
  51. class Mkcert {
  52. private force?: boolean
  53. private autoUpgrade?: boolean
  54. private mkcertLocalPath?: string
  55. private source: BaseSource
  56. private logger: Logger
  57. private mkcertSavedPath: string
  58. private sourceType: SourceType
  59. private config: Config
  60. public static create(options: MkcertProps) {
  61. return new Mkcert(options)
  62. }
  63. private constructor(options: MkcertProps) {
  64. const { force, autoUpgrade, source, mkcertPath, logger } = options
  65. this.force = force
  66. this.logger = logger
  67. this.autoUpgrade = autoUpgrade
  68. this.mkcertLocalPath = mkcertPath
  69. this.sourceType = source || 'github'
  70. if (this.sourceType === 'github') {
  71. this.source = GithubSource.create()
  72. } else if (this.sourceType === 'coding') {
  73. this.source = CodingSource.create()
  74. } else {
  75. this.source = this.sourceType
  76. }
  77. this.mkcertSavedPath = resolvePath(
  78. process.platform === 'win32' ? 'mkcert.exe' : 'mkcert'
  79. )
  80. this.config = new Config()
  81. }
  82. private async getMkcertBinnary() {
  83. return (await this.checkMkcert())
  84. ? this.mkcertLocalPath || this.mkcertSavedPath
  85. : undefined
  86. }
  87. /**
  88. * Check if mkcert exists
  89. */
  90. private async checkMkcert() {
  91. let exist: boolean
  92. if (this.mkcertLocalPath) {
  93. exist = await exists(this.mkcertLocalPath)
  94. if (!exists) {
  95. this.logger.error(
  96. pc.red(
  97. `${this.mkcertLocalPath} does not exist, please check the mkcertPath paramter`
  98. )
  99. )
  100. }
  101. } else {
  102. exist = await exists(this.mkcertSavedPath)
  103. }
  104. return exist
  105. }
  106. private async getCertificate() {
  107. const key = await fs.promises.readFile(KEY_FILE_PATH)
  108. const cert = await fs.promises.readFile(CERT_FILE_PATH)
  109. return {
  110. key,
  111. cert
  112. }
  113. }
  114. private async createCertificate(hosts: string[]) {
  115. const names = hosts.join(' ')
  116. const mkcertBinnary = await this.getMkcertBinnary()
  117. if (!mkcertBinnary) {
  118. debug(
  119. `Mkcert does not exist, unable to generate certificate for ${names}`
  120. )
  121. }
  122. await ensureDirExist(KEY_FILE_PATH)
  123. await ensureDirExist(CERT_FILE_PATH)
  124. const cmd = `${escape(mkcertBinnary)} -install -key-file ${escape(
  125. KEY_FILE_PATH
  126. )} -cert-file ${escape(CERT_FILE_PATH)} ${names}`
  127. await exec(cmd, {
  128. env: {
  129. ...process.env,
  130. JAVA_HOME: undefined
  131. }
  132. })
  133. this.logger.info(
  134. `The certificate is saved in:\n${KEY_FILE_PATH}\n${CERT_FILE_PATH}`
  135. )
  136. }
  137. private getLatestHash = async () => {
  138. return {
  139. key: await getHash(KEY_FILE_PATH),
  140. cert: await getHash(CERT_FILE_PATH)
  141. }
  142. }
  143. private async regenerate(record: Record, hosts: string[]) {
  144. await this.createCertificate(hosts)
  145. const hash = await this.getLatestHash()
  146. record.update({ hosts, hash })
  147. }
  148. public async init() {
  149. await this.config.init()
  150. const exist = await this.checkMkcert()
  151. if (!exist) {
  152. await this.initMkcert()
  153. } else if (this.autoUpgrade) {
  154. await this.upgradeMkcert()
  155. }
  156. }
  157. private async getSourceInfo() {
  158. const sourceInfo = await this.source.getSourceInfo()
  159. if (!sourceInfo) {
  160. if (typeof this.sourceType === 'string') {
  161. this.logger.error(
  162. 'Failed to request mkcert information, please check your network'
  163. )
  164. if (this.sourceType === 'github') {
  165. this.logger.info(
  166. 'If you are a user in china, maybe you should set "source" paramter to "coding"'
  167. )
  168. }
  169. } else {
  170. this.logger.info(
  171. 'Please check your custom "source", it seems to return invalid result'
  172. )
  173. }
  174. return undefined
  175. }
  176. return sourceInfo
  177. }
  178. private async initMkcert() {
  179. const sourceInfo = await this.getSourceInfo()
  180. debug('The mkcert does not exist, download it now')
  181. if (!sourceInfo) {
  182. this.logger.error(
  183. 'Can not obtain download information of mkcert, init skipped'
  184. )
  185. return
  186. }
  187. await this.downloadMkcert(sourceInfo.downloadUrl, this.mkcertSavedPath)
  188. }
  189. private async upgradeMkcert() {
  190. const versionManger = new VersionManger({ config: this.config })
  191. const sourceInfo = await this.getSourceInfo()
  192. if (!sourceInfo) {
  193. this.logger.error(
  194. 'Can not obtain download information of mkcert, update skipped'
  195. )
  196. return
  197. }
  198. const versionInfo = versionManger.compare(sourceInfo.version)
  199. if (!versionInfo.shouldUpdate) {
  200. debug('Mkcert is kept latest version, update skipped')
  201. return
  202. }
  203. if (versionInfo.breakingChange) {
  204. debug(
  205. 'The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped',
  206. versionInfo.currentVersion,
  207. versionInfo.nextVersion
  208. )
  209. return
  210. }
  211. debug(
  212. 'The current version of mkcert is %s, and the latest version is %s, mkcert will be updated',
  213. versionInfo.currentVersion,
  214. versionInfo.nextVersion
  215. )
  216. await this.downloadMkcert(sourceInfo.downloadUrl, this.mkcertSavedPath)
  217. versionManger.update(versionInfo.nextVersion)
  218. }
  219. private async downloadMkcert(sourceUrl: string, distPath: string) {
  220. const downloader = Downloader.create()
  221. await downloader.download(sourceUrl, distPath)
  222. }
  223. public async renew(hosts: string[]) {
  224. const record = new Record({ config: this.config })
  225. if (this.force) {
  226. debug(`Certificate is forced to regenerate`)
  227. await this.regenerate(record, hosts)
  228. }
  229. if (!record.contains(hosts)) {
  230. debug(
  231. `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate`
  232. )
  233. await this.regenerate(record, hosts)
  234. return
  235. }
  236. const hash = await this.getLatestHash()
  237. if (record.tamper(hash)) {
  238. debug(
  239. `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog(
  240. hash
  241. )}, start regenerate certificate`
  242. )
  243. await this.regenerate(record, hosts)
  244. return
  245. }
  246. debug('Neither hosts nor hash has changed, skip regenerate certificate')
  247. }
  248. /**
  249. * Get certificates
  250. *
  251. * @param hosts host collection
  252. * @returns cretificates
  253. */
  254. public async install(hosts: string[]) {
  255. if (hosts.length) {
  256. await this.renew(hosts)
  257. }
  258. return await this.getCertificate()
  259. }
  260. }
  261. export default Mkcert