refactor: move

This commit is contained in:
xiaojunnuo
2021-02-08 00:21:36 +08:00
parent cfb1034450
commit 82f86d9556
150 changed files with 14691 additions and 2059 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@certd/certd",
"version": "0.1.13",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \\\"Error: no test specified\\\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"@certd/acme-client": "^0.1.6",
"@certd/api": "^0.1.13",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"node-forge": "^0.10.0"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"gitHead": "4a421d5b142d453203c68ce6d1036e168ea2455b"
}
+199
View File
@@ -0,0 +1,199 @@
import acme from '@certd/acme-client'
import _ from 'lodash-es'
import { util } from '@certd/api'
const logger = util.logger
export class AcmeService {
constructor (store) {
this.store = store
}
async getAccountConfig (email) {
let conf = this.store.get(this.buildAccountPath(email))
if (conf == null) {
conf = {}
} else {
conf = JSON.parse(conf)
}
return conf
}
buildAccountPath (email) {
return this.store.buildKey(email, 'account.json')
}
saveAccountConfig (email, conf) {
this.store.set(this.buildAccountPath(email), JSON.stringify(conf))
}
async getAcmeClient (email, isTest) {
const conf = await this.getAccountConfig(email)
if (conf.key == null) {
conf.key = await this.createNewKey()
this.saveAccountConfig(email, conf)
}
if (isTest == null) {
isTest = process.env.CERTD_MODE === 'test'
}
const client = new acme.Client({
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
accountKey: conf.key,
accountUrl: conf.accountUrl,
backoffAttempts: 20,
backoffMin: 5000,
backoffMax: 10000
})
if (conf.accountUrl == null) {
const accountPayload = { termsOfServiceAgreed: true, contact: [`mailto:${email}`] }
await client.createAccount(accountPayload)
conf.accountUrl = client.getAccountUrl()
this.saveAccountConfig(email, conf)
}
return client
}
async createNewKey () {
const key = await acme.forge.createPrivateKey()
return key.toString()
}
async challengeCreateFn (authz, challenge, keyAuthorization, dnsProvider) {
logger.info('Triggered challengeCreateFn()')
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
const fileContents = keyAuthorization
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`)
/* Replace this */
logger.info(`Would write "${fileContents}" to path "${filePath}"`)
// await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === 'dns-01') {
/* dns-01 */
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
const recordValue = keyAuthorization
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`)
/* Replace this */
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`)
return await dnsProvider.createRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: recordValue
})
}
}
/**
* Function used to remove an ACME challenge response
*
* @param {object} authz Authorization object
* @param {object} challenge Selected challenge
* @param {string} keyAuthorization Authorization key
* @param recordItem challengeCreateFn create record item
* @param dnsProvider dnsProvider
* @returns {Promise}
*/
async challengeRemoveFn (authz, challenge, keyAuthorization, recordItem, dnsProvider) {
logger.info('Triggered challengeRemoveFn()')
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`)
/* Replace this */
logger.info(`Would remove file on path "${filePath}"`)
// await fs.unlinkAsync(filePath);
} else if (challenge.type === 'dns-01') {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
const recordValue = keyAuthorization
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`)
/* Replace this */
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`)
await dnsProvider.removeRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: keyAuthorization,
record: recordItem
})
}
}
async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo, isTest }) {
const client = await this.getAcmeClient(email, isTest)
let accountUrl
try {
accountUrl = client.getAccountUrl()
} catch (e) {
}
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains)
const [key, csr] = await acme.forge.createCsr({
commonName,
...csrInfo,
altNames
})
if (dnsProvider == null && dnsProviderCreator) {
dnsProvider = await dnsProviderCreator()
}
if (dnsProvider == null) {
throw new Error('dnsProvider 不能为空')
}
/* 自动申请证书 */
const crt = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
challengePriority: ['dns-01'],
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider)
},
challengeRemoveFn: async (authz, challenge, keyAuthorization, recordItem) => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider)
}
})
// 保存账号url
if (!accountUrl) {
try {
accountUrl = client.getAccountUrl()
this.setAccountUrl(email, accountUrl)
} catch (e) {
logger.warn('保存accountUrl出错', e)
}
}
/* Done */
logger.debug(`CSR:\n${csr.toString()}`)
logger.debug(`Certificate:\n${crt.toString()}`)
logger.info('证书申请成功')
return { key, crt, csr }
}
buildCommonNameByDomains (domains) {
if (typeof domains === 'string') {
domains = domains.split(',')
}
if (domains.length === 0) {
throw new Error('domain can not be empty')
}
const ret = {
commonName: domains[0]
}
if (domains.length > 1) {
ret.altNames = _.slice(domains, 1)
}
return ret
}
}
+135
View File
@@ -0,0 +1,135 @@
import { util, Store, dnsProviderRegistry } from '@certd/api'
import { AcmeService } from './acme.js'
import { FileStore } from './store/file-store.js'
import { CertStore } from './store/cert-store.js'
import dayjs from 'dayjs'
import forge from 'node-forge'
import DefaultDnsProviders from '@certd/dns-providers'
const logger = util.logger
DefaultDnsProviders.install()
export class Certd {
constructor (options) {
this.options = options
this.email = options.cert.email
this.domains = options.cert.domains
if (!(options.store instanceof Store)) {
this.store = new FileStore(options.store || {})
}
this.certStore = new CertStore({
store: this.store,
email: options.cert.email,
domains: this.domains
})
this.acme = new AcmeService(this.store)
}
async certApply () {
let oldCert
try {
oldCert = await this.readCurrentCert()
} catch (e) {
logger.warn('读取cert失败:', e)
}
if (oldCert == null) {
logger.info('还未申请过,准备申请新证书')
} else {
const ret = this.isWillExpire(oldCert.expires, this.options.cert.renewDays)
if (!ret.isWillExpire) {
logger.info('证书还未过期:', oldCert.expires, ',剩余', ret.leftDays, '天')
if (this.options.args.forceCert) {
logger.info('准备强制更新证书')
} else {
logger.info('暂不更新证书')
oldCert.isNew = false
return oldCert
}
} else {
logger.info('即将过期,准备更新证书')
}
}
// 执行证书申请步骤
return await this.doCertApply()
}
async doCertApply () {
const options = this.options
const dnsProvider = this.createDnsProvider(options)
const cert = await this.acme.order({
email: options.cert.email,
domains: options.cert.domains,
dnsProvider,
csrInfo: options.cert.csrInfo,
isTest: options.args.test
})
await this.writeCert(cert)
const certRet = await this.readCurrentCert()
certRet.isNew = true
return certRet
}
createDnsProvider (options) {
return this.createProviderByType(options.cert.dnsProvider, options.accessProviders)
}
async writeCert (cert) {
const newPath = await this.certStore.writeCert(cert)
return {
realPath: this.certStore.store.getActualKey(newPath),
currentPath: this.certStore.store.getActualKey(this.certStore.currentMarkPath)
}
}
async readCurrentCert () {
const cert = await this.certStore.readCert()
if (cert == null) {
return null
}
const { detail, expires } = this.getCrtDetail(cert.crt)
const domain = this.certStore.getMainDomain(this.options.cert.domains)
return {
...cert, detail, expires, domain, domains: this.domains, email: this.email
}
}
getCrtDetail (crt) {
const pki = forge.pki
const detail = pki.certificateFromPem(crt.toString())
const expires = detail.validity.notAfter
return { detail, expires }
}
/**
* 检查是否过期,默认提前20天
* @param expires
* @param maxDays
* @returns {boolean}
*/
isWillExpire (expires, maxDays = 20) {
if (expires == null) {
throw new Error('过期时间不能为空')
}
// 检查有效期
const leftDays = dayjs(expires).diff(dayjs(), 'day')
return {
isWillExpire: leftDays < maxDays,
leftDays
}
}
createProviderByType (props, accessProviders) {
const { type } = props
const Provider = dnsProviderRegistry.get(type)
if (Provider == null) {
throw new Error('暂不支持此dnsProvider,请先注册该provider' + type)
}
return new Provider({ accessProviders, props })
}
}
+127
View File
@@ -0,0 +1,127 @@
import dayjs from 'dayjs'
import crypto from 'crypto'
// eslint-disable-next-line no-unused-vars
function md5 (content) {
return crypto.createHash('md5').update(content).digest('hex')
}
export class CertStore {
constructor ({ store, email, domains }) {
this.store = store
this.email = email
this.domains = domains
this.domain = this.getMainDomain(this.domains)
this.safetyDomain = this.getSafetyDomain(this.domain)
// this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains))
this.domainDir = this.safetyDomain
this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentMarkPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current.json')
}
getMainDomain (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
if (domains.length > 0) {
return domains[0]
}
}
getDomainStr (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
return domains.join(',')
}
buildNewCertRootPath (dir) {
if (dir == null) {
dir = dayjs().format('YYYY.MM.DD.HHmmss')
}
return this.store.buildKey(this.certsRootPath, this.domainDir, dir)
}
formatCert (pem) {
pem = pem.replace(/\r/g, '')
pem = pem.replace(/\n\n/g, '\n')
pem = pem.replace(/\n$/g, '')
return pem
}
async writeCert (cert) {
const newDir = this.buildNewCertRootPath()
const crtKey = this.buildKey(newDir, this.safetyDomain + '.crt')
const priKey = this.buildKey(newDir, this.safetyDomain + '.key')
const csrKey = this.buildKey(newDir, this.safetyDomain + '.csr')
await this.store.set(crtKey, this.formatCert(cert.crt.toString()))
await this.store.set(priKey, this.formatCert(cert.key.toString()))
await this.store.set(csrKey, cert.csr.toString())
await this.store.set(this.currentMarkPath, JSON.stringify({ latest: newDir }))
return newDir
}
async readCert (dir) {
if (dir == null) {
dir = await this.getCurrentDir()
}
if (dir == null) {
return
}
const crtKey = this.buildKey(dir, this.safetyDomain + '.crt')
const priKey = this.buildKey(dir, this.safetyDomain + '.key')
const csrKey = this.buildKey(dir, this.safetyDomain + '.csr')
const crt = await this.store.get(crtKey)
if (crt == null) {
return null
}
const key = await this.store.get(priKey)
const csr = await this.store.get(csrKey)
return {
crt: this.formatCert(crt),
key: this.formatCert(key),
csr,
crtPath: this.store.getActualKey(crtKey),
keyPath: this.store.getActualKey(priKey),
certDir: this.store.getActualKey(dir)
}
}
buildKey (...keyItem) {
return this.store.buildKey(...keyItem)
}
getSafetyDomain (domain) {
return domain.replace(/\*/g, '_')
}
async getCurrentDir () {
const current = await this.store.get(this.currentMarkPath)
if (current == null) {
return null
}
return JSON.parse(current).latest
}
async getCurrentFile (file) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.get(key)
}
async setCurrentFile (file, value) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.set(key, value)
}
}
@@ -0,0 +1,66 @@
import { Store, util } from '@certd/api'
import path from 'path'
import fs from 'fs'
const logger = util.logger
export class FileStore extends Store {
constructor (opts) {
super()
if (opts.rootDir != null) {
this.rootDir = opts.rootDir
} else {
this.rootDir = util.path.getUserBasePath()
}
if (opts.test) {
this.rootDir = path.join(this.rootDir, '/test/')
}
}
getActualKey (key) {
// return 前缀+key
return this.getPathByKey(key)
}
buildKey (...keyItem) {
return path.join(...keyItem)
}
getPathByKey (key) {
return path.join(this.rootDir, key)
}
set (key, value) {
const filePath = this.getPathByKey(key)
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(filePath, value)
return filePath
}
get (key) {
const filePath = this.getPathByKey(key)
if (!fs.existsSync(filePath)) {
return null
}
return fs.readFileSync(filePath).toString()
}
link (targetPath, linkPath) {
targetPath = this.getPathByKey(targetPath)
linkPath = this.getPathByKey(linkPath)
if (fs.existsSync(linkPath)) {
try {
fs.unlinkSync(linkPath)
} catch (e) {
logger.error('unlink error:', e)
}
}
fs.symlinkSync(targetPath, linkPath, 'dir')
}
unlink (linkPath) {
linkPath = this.getPathByKey(linkPath)
fs.unlinkSync(linkPath)
}
}
@@ -0,0 +1,18 @@
import pkg from 'chai'
import { createOptions } from '../../../../../test/options.js'
import { Certd } from '../../src'
const { expect } = pkg
describe('AliyunDnsProvider', function () {
it('#申请证书-aliyun', async function () {
this.timeout(300000)
const options = createOptions()
options.args = { forceCert: true, test: false }
const certd = new Certd(options)
const cert = await certd.certApply()
expect(cert).ok
expect(cert.crt).ok
expect(cert.key).ok
expect(cert.detail).ok
expect(cert.expires).ok
})
})
@@ -0,0 +1,20 @@
import pkg from 'chai'
import { Certd } from '../../src'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('DnspodDnsProvider', function () {
it('#申请证书', async function () {
this.timeout(300000)
const options = createOptions()
options.cert.domains = ['*.certd.xyz', '*.test.certd.xyz', '*.base.certd.xyz', 'certd.xyz']
options.cert.dnsProvider = 'dnspod'
options.args = { forceCert: true }
const certd = new Certd(options)
const cert = await certd.certApply()
expect(cert).ok
expect(cert.crt).ok
expect(cert.key).ok
expect(cert.detail).ok
expect(cert.expires).ok
})
})
+88
View File
@@ -0,0 +1,88 @@
import chai from 'chai'
import { Certd } from '../src'
import { createOptions } from '../../../../test/options.js'
const { expect } = chai
const fakeCrt = `-----BEGIN CERTIFICATE-----
MIIFSTCCBDGgAwIBAgITAPoZZk/LhVIyXoic2NnJyxubezANBgkqhkiG9w0BAQsF
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDEyMTQx
NjA1NTFaFw0yMTAzMTQxNjA1NTFaMBsxGTAXBgNVBAMMECouZG9jbWlycm9yLmNs
dWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC75tGrYjly+RpcZehQ
my1EpaXElT4L60pINKV2YDKnBrcSSo1c6rO7nFh12eC/ju4WwYUep0RVmBDF8xD0
I1Sd1uuDTQWP0UT1X9yqdXtjvxpUqoCHAzG633f3sJRFul7mDLuC9tRCuae9o7qP
EZ827XOmjBR35dso9I2GEE4828J3YE3tSKtobZlM+30jozLEcsO0PTyM5mq5PPjP
VI3fGLcEaBmLZf5ixz4XkcY9IAhyAMYf03cT2wRoYPBaDdXblgCYL6sFtIMbzl3M
Di94PB8NyoNSsC2nmBdWi54wFOgBvY/4ljsX/q7X3EqlSvcA0/M6/c/J9kJ3eupv
jV8nAgMBAAGjggJ9MIICeTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAkdTjSCV3KD
x28sf98MrwVfyFYgMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHcG
CCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Auc3RnLWludC14
MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0Zy1p
bnQteDEubGV0c2VuY3J5cHQub3JnLzArBgNVHREEJDAighAqLmRvY21pcnJvci5j
bHVigg5kb2NtaXJyb3IuY2x1YjBMBgNVHSAERTBDMAgGBmeBDAECATA3BgsrBgEE
AYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9y
ZzCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB1ABboacHRlerXw/iXGuPwdgH3jOG2
nTGoUhi2g38xqBUIAAABdmI3LM4AAAQDAEYwRAIgaiNqXSEq+sxp8eqlJXp/KFdO
so5mT50MoRsLF8Inu0ACIDP46+ekng7I0BlmyIPmbqFcZgnZFVWLLCdLYijhVyOL
AHcA3Zk0/KXnJIDJVmh9gTSZCEmySfe1adjHvKs/XMHzbmQAAAF2YjcuxwAABAMA
SDBGAiEAxpeB8/w4YkHZ62nH20h128VtuTSmYDCnF7EK2fQyeZYCIQDbJlF2wehZ
sF1BeE7qnYYqCTP0dYIrQ9HWtBa/MbGOKTANBgkqhkiG9w0BAQsFAAOCAQEAL2di
HKh6XcZtGk0BFxJa51sCZ3MLu9+Zy90kCRD4ooP5x932WxVM25+LBRd+xSzx+TRL
UVrlKp9GdMYX1JXL4Vf2NwzuFO3snPDe/qizD/3+D6yo8eKJ/LD82t5kLWAD2rto
YfVSTKwfNIBBJwHUnjviBPJmheHHCKmz8Ct6/6QxFAeta9TAMn0sFeVCQnmAq7HL
jrunq0tNHR/EKG0ITPLf+6P7MxbmpYNnq918766l0tKsW8oo8ZSGEwKU2LMaSiAa
hasyl/2gMnYXjtKOjDcnR8oLpbrOg0qpVbynmJin1HP835oHPPAZ1gLsqYTTizNz
AHxTaXliTVvS83dogw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
-----END CERTIFICATE-----`
describe('Certd', function () {
it('#buildCertDir', function () {
const options = createOptions()
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.club']
const certd = new Certd(options)
const currentRootPath = certd.certStore.currentMarkPath
console.log('rootDir', currentRootPath)
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club\w*\\current.json/)
})
it('#writeAndReadCert', async function () {
const options = createOptions()
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.domain.cn']
const certd = new Certd(options)
await certd.writeCert({ csr: 'csr', crt: fakeCrt, key: 'bbb' })
const cert = await certd.readCurrentCert()
expect(cert).to.be.ok
expect(cert.crt).ok
expect(cert.key).to.be.ok
expect(cert.detail).to.be.ok
expect(cert.expires).to.be.ok
console.log('cert:', JSON.stringify(cert))
})
})